Compare commits
1 Commits
main
...
64490e0d17
| Author | SHA1 | Date | |
|---|---|---|---|
| 64490e0d17 |
@@ -1,9 +0,0 @@
|
|||||||
name: check
|
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# actions/checkout v4.2.2, 2026-03-16
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
|
||||||
- run: script/cibuild
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,8 +6,5 @@
|
|||||||
vendor.tzst
|
vendor.tzst
|
||||||
modcache.tzst
|
modcache.tzst
|
||||||
|
|
||||||
# Generated manifest files
|
|
||||||
.index.mf
|
|
||||||
|
|
||||||
# Stale files
|
# Stale files
|
||||||
.drone.yml
|
.drone.yml
|
||||||
|
|||||||
30
AGENTS.md
30
AGENTS.md
@@ -1,30 +0,0 @@
|
|||||||
# Agent Instructions
|
|
||||||
|
|
||||||
Read `REPO_POLICIES.md` before making any changes. It is the authoritative
|
|
||||||
source for coding standards, formatting, linting, and workflow rules.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
- When fixing a bug, write a failing test FIRST. Only after the test fails,
|
|
||||||
write the code to fix the bug. Then ensure the test passes. Leave the test in
|
|
||||||
place and commit it with the bugfix. Don't run shell commands to test bugfixes
|
|
||||||
or reproduce bugs. Write tests!
|
|
||||||
|
|
||||||
- After each change, run `make fmt`, then `make test`, then `make lint`. Fix any
|
|
||||||
failures before committing.
|
|
||||||
|
|
||||||
- After each change, commit only the files you've changed. Push after committing.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
- Never mention Claude, Anthropic, or any AI/LLM tooling in commit messages. Do
|
|
||||||
not use attribution.
|
|
||||||
|
|
||||||
## Repository-Specific Notes
|
|
||||||
|
|
||||||
- This is a Go library + CLI tool for generating `.mf` manifest files.
|
|
||||||
- The proto definition is in `mfer/mf.proto`; generated `.pb.go` files are
|
|
||||||
committed (required for `go get` compatibility).
|
|
||||||
- The format specification is in `FORMAT.md`.
|
|
||||||
- See the TODO section in `README.md` for the 1.0 implementation plan
|
|
||||||
and open design questions.
|
|
||||||
20
CLAUDE.md
Normal file
20
CLAUDE.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Important Rules
|
||||||
|
|
||||||
|
- when fixing a bug, write a failing test FIRST. only after the test fails, write
|
||||||
|
the code to fix the bug. then ensure the test passes. leave the test in
|
||||||
|
place and commit it with the bugfix. don't run shell commands to test
|
||||||
|
bugfixes or reproduce bugs. write tests!
|
||||||
|
|
||||||
|
- never, ever mention claude or anthropic in commit messages. do not use attribution
|
||||||
|
|
||||||
|
- after each change, run "make fmt".
|
||||||
|
|
||||||
|
- after each change, run "make test" and ensure all tests pass.
|
||||||
|
|
||||||
|
- after each change, run "make lint" and ensure no linting errors. fix any
|
||||||
|
you find, one by one.
|
||||||
|
|
||||||
|
- after each change, commit the files you've changed. push after
|
||||||
|
committing.
|
||||||
|
|
||||||
|
- NEVER use `git add -A`. always add only individual files that you've changed.
|
||||||
29
Dockerfile
29
Dockerfile
@@ -1,36 +1,9 @@
|
|||||||
# Lint stage — fast feedback on formatting and lint issues
|
|
||||||
# golangci/golangci-lint:v2.0.2 (2026-03-14)
|
|
||||||
FROM golangci/golangci-lint@sha256:d55581f7797e7a0877a7c3aaa399b01bdc57d2874d6412601a046cc4062cb62e AS lint
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Touch .pb.go so make does not try to regenerate via protoc (file is committed)
|
|
||||||
RUN touch mfer/mf.pb.go
|
|
||||||
|
|
||||||
RUN make fmt-check
|
|
||||||
RUN make lint
|
|
||||||
|
|
||||||
# Build stage — tests and compilation
|
|
||||||
# golang:1.23 (2026-03-14)
|
|
||||||
FROM golang@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 AS builder
|
FROM golang@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 AS builder
|
||||||
|
|
||||||
# Force BuildKit to run the lint stage by creating a stage dependency
|
|
||||||
COPY --from=lint /src/go.sum /dev/null
|
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN go test -v --timeout 30s ./...
|
||||||
# Touch .pb.go so make does not try to regenerate via protoc (file is committed)
|
|
||||||
RUN touch mfer/mf.pb.go
|
|
||||||
|
|
||||||
RUN make test
|
|
||||||
RUN cd cmd/mfer && go build -tags urfave_cli_no_docs -o /mfer .
|
RUN cd cmd/mfer && go build -tags urfave_cli_no_docs -o /mfer .
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|||||||
51
FORMAT.md
51
FORMAT.md
@@ -25,17 +25,17 @@ See [`mfer/mf.proto`](mfer/mf.proto) for exact field numbers and types.
|
|||||||
|
|
||||||
The outer message contains:
|
The outer message contains:
|
||||||
|
|
||||||
| Field | Number | Type | Description |
|
| Field | Number | Type | Description |
|
||||||
| ----------------- | ------ | ---------------- | ------------------------------------------------------------------------ |
|
|--------------------|--------|-------------------|--------------------------------------------------|
|
||||||
| `version` | 101 | enum | Must be `VERSION_ONE` (1) |
|
| `version` | 101 | enum | Must be `VERSION_ONE` (1) |
|
||||||
| `compressionType` | 102 | enum | Compression of `innerMessage`; must be `COMPRESSION_ZSTD` (1) |
|
| `compressionType` | 102 | enum | Compression of `innerMessage`; must be `COMPRESSION_ZSTD` (1) |
|
||||||
| `size` | 103 | int64 | Uncompressed size of `innerMessage` (corruption detection) |
|
| `size` | 103 | int64 | Uncompressed size of `innerMessage` (corruption detection) |
|
||||||
| `sha256` | 104 | bytes | SHA-256 hash of the **compressed** `innerMessage` (corruption detection) |
|
| `sha256` | 104 | bytes | SHA-256 hash of the **compressed** `innerMessage` (corruption detection) |
|
||||||
| `uuid` | 105 | bytes | Random v4 UUID; must match the inner message UUID |
|
| `uuid` | 105 | bytes | Random v4 UUID; must match the inner message UUID |
|
||||||
| `innerMessage` | 199 | bytes | Zstd-compressed serialized `MFFile` message |
|
| `innerMessage` | 199 | bytes | Zstd-compressed serialized `MFFile` message |
|
||||||
| `signature` | 201 | bytes (optional) | GPG signature (ASCII-armored or binary) |
|
| `signature` | 201 | bytes (optional) | GPG signature (ASCII-armored or binary) |
|
||||||
| `signer` | 202 | bytes (optional) | Full GPG key ID of the signer |
|
| `signer` | 202 | bytes (optional) | Full GPG key ID of the signer |
|
||||||
| `signingPubKey` | 203 | bytes (optional) | Full GPG signing public key |
|
| `signingPubKey` | 203 | bytes (optional) | Full GPG signing public key |
|
||||||
|
|
||||||
### SHA-256 Hash
|
### SHA-256 Hash
|
||||||
|
|
||||||
@@ -54,25 +54,25 @@ decompression bombs. The reference implementation limits decompressed size to
|
|||||||
After decompressing `innerMessage`, the result is a serialized `MFFile`
|
After decompressing `innerMessage`, the result is a serialized `MFFile`
|
||||||
(referred to as the manifest):
|
(referred to as the manifest):
|
||||||
|
|
||||||
| Field | Number | Type | Description |
|
| Field | Number | Type | Description |
|
||||||
| ----------- | ------ | --------------------- | ------------------------------------- |
|
|-------------|--------|-----------------------|--------------------------------------------|
|
||||||
| `version` | 100 | enum | Must be `VERSION_ONE` (1) |
|
| `version` | 100 | enum | Must be `VERSION_ONE` (1) |
|
||||||
| `files` | 101 | repeated `MFFilePath` | List of files in the manifest |
|
| `files` | 101 | repeated `MFFilePath` | List of files in the manifest |
|
||||||
| `uuid` | 102 | bytes | Random v4 UUID; must match outer UUID |
|
| `uuid` | 102 | bytes | Random v4 UUID; must match outer UUID |
|
||||||
| `createdAt` | 201 | Timestamp (optional) | When the manifest was created |
|
| `createdAt` | 201 | Timestamp (optional) | When the manifest was created |
|
||||||
|
|
||||||
## File Entries (`MFFilePath`)
|
## File Entries (`MFFilePath`)
|
||||||
|
|
||||||
Each file entry contains:
|
Each file entry contains:
|
||||||
|
|
||||||
| Field | Number | Type | Description |
|
| Field | Number | Type | Description |
|
||||||
| ---------- | ------ | ------------------------- | ----------------------------------- |
|
|------------|--------|---------------------------|--------------------------------------|
|
||||||
| `path` | 1 | string | Relative file path (see Path Rules) |
|
| `path` | 1 | string | Relative file path (see Path Rules) |
|
||||||
| `size` | 2 | int64 | File size in bytes |
|
| `size` | 2 | int64 | File size in bytes |
|
||||||
| `hashes` | 3 | repeated `MFFileChecksum` | At least one hash required |
|
| `hashes` | 3 | repeated `MFFileChecksum` | At least one hash required |
|
||||||
| `mimeType` | 301 | string (optional) | MIME type |
|
| `mimeType` | 301 | string (optional) | MIME type |
|
||||||
| `mtime` | 302 | Timestamp (optional) | Modification time |
|
| `mtime` | 302 | Timestamp (optional) | Modification time |
|
||||||
| `ctime` | 303 | Timestamp (optional) | Change time (inode metadata change) |
|
| `ctime` | 303 | Timestamp (optional) | Change time (inode metadata change) |
|
||||||
|
|
||||||
Field 304 (`atime`) has been removed from the specification. Access time is
|
Field 304 (`atime`) has been removed from the specification. Access time is
|
||||||
volatile and non-deterministic; it is not useful for integrity verification.
|
volatile and non-deterministic; it is not useful for integrity verification.
|
||||||
@@ -111,7 +111,6 @@ ZNAVSRFG-<UUID>-<SHA256>
|
|||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
- `ZNAVSRFG` is the magic bytes string (literal ASCII)
|
- `ZNAVSRFG` is the magic bytes string (literal ASCII)
|
||||||
- `<UUID>` is the hex-encoded UUID from the outer message
|
- `<UUID>` is the hex-encoded UUID from the outer message
|
||||||
- `<SHA256>` is the hex-encoded SHA-256 hash from the outer message (covering compressed data)
|
- `<SHA256>` is the hex-encoded SHA-256 hash from the outer message (covering compressed data)
|
||||||
|
|||||||
38
Makefile
38
Makefile
@@ -5,7 +5,7 @@ export PATH := $(PATH):$(GOPATH)/bin
|
|||||||
PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go
|
PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go
|
||||||
SOURCEFILES := mfer/*.go mfer/*.proto internal/*/*.go cmd/*/*.go go.mod go.sum
|
SOURCEFILES := mfer/*.go mfer/*.proto internal/*/*.go cmd/*/*.go go.mod go.sum
|
||||||
ARCH := $(shell uname -m)
|
ARCH := $(shell uname -m)
|
||||||
GITREV_BUILD := $(shell bash $(PWD)/bin/gitrev.sh 2>/dev/null || echo unknown)
|
GITREV_BUILD := $(shell bash $(PWD)/bin/gitrev.sh)
|
||||||
APPNAME := mfer
|
APPNAME := mfer
|
||||||
VERSION := 0.1.0
|
VERSION := 0.1.0
|
||||||
export DOCKER_IMAGE_CACHE_DIR := $(HOME)/Library/Caches/Docker/$(APPNAME)-$(ARCH)
|
export DOCKER_IMAGE_CACHE_DIR := $(HOME)/Library/Caches/Docker/$(APPNAME)-$(ARCH)
|
||||||
@@ -13,24 +13,18 @@ GOLDFLAGS += -X main.Version=$(VERSION)
|
|||||||
GOLDFLAGS += -X main.Gitrev=$(GITREV_BUILD)
|
GOLDFLAGS += -X main.Gitrev=$(GITREV_BUILD)
|
||||||
GOFLAGS := -ldflags "$(GOLDFLAGS)"
|
GOFLAGS := -ldflags "$(GOLDFLAGS)"
|
||||||
|
|
||||||
.PHONY: bootstrap setup docker default run ci test check lint fmt fmt-check hooks fixme
|
.PHONY: docker default run ci test fixme
|
||||||
|
|
||||||
default: fmt test
|
default: fmt test
|
||||||
|
|
||||||
bootstrap:
|
|
||||||
@script/bootstrap
|
|
||||||
|
|
||||||
setup:
|
|
||||||
@script/setup
|
|
||||||
|
|
||||||
run: ./bin/mfer
|
run: ./bin/mfer
|
||||||
./$<
|
./$<
|
||||||
./$< gen
|
./$< gen
|
||||||
|
|
||||||
ci: test
|
ci: test
|
||||||
|
|
||||||
test:
|
test: $(SOURCEFILES) mfer/mf.pb.go
|
||||||
@script/test
|
go test -v --timeout 10s ./...
|
||||||
|
|
||||||
$(PROTOC_GEN_GO):
|
$(PROTOC_GEN_GO):
|
||||||
test -e $(PROTOC_GEN_GO) || go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
|
test -e $(PROTOC_GEN_GO) || go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
|
||||||
@@ -38,17 +32,8 @@ $(PROTOC_GEN_GO):
|
|||||||
fixme:
|
fixme:
|
||||||
@grep -nir fixme . | grep -v Makefile
|
@grep -nir fixme . | grep -v Makefile
|
||||||
|
|
||||||
check:
|
|
||||||
@script/check
|
|
||||||
|
|
||||||
fmt-check:
|
|
||||||
@script/fmt-check
|
|
||||||
|
|
||||||
hooks:
|
|
||||||
@script/install-precommit
|
|
||||||
|
|
||||||
devprereqs:
|
devprereqs:
|
||||||
which golangci-lint || go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@v2.0.2
|
which golangci-lint || go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
mfer/mf.pb.go: mfer/mf.proto
|
mfer/mf.pb.go: mfer/mf.proto
|
||||||
cd mfer && go generate .
|
cd mfer && go generate .
|
||||||
@@ -60,14 +45,17 @@ bin/mfer: $(SOURCEFILES) mfer/mf.pb.go
|
|||||||
clean:
|
clean:
|
||||||
rm -rfv mfer/*.pb.go bin/mfer cmd/mfer/mfer *.dockerimage
|
rm -rfv mfer/*.pb.go bin/mfer cmd/mfer/mfer *.dockerimage
|
||||||
|
|
||||||
fmt:
|
fmt: mfer/mf.pb.go
|
||||||
@script/fmt
|
gofumpt -l -w mfer internal cmd
|
||||||
|
golangci-lint run --fix
|
||||||
|
-prettier -w *.json
|
||||||
|
-prettier -w *.md
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@script/lint
|
golangci-lint run
|
||||||
|
sh -c 'test -z "$$(gofmt -l .)"'
|
||||||
|
|
||||||
docker:
|
docker: sneak-mfer.$(ARCH).tzst.dockerimage
|
||||||
@script/docker
|
|
||||||
|
|
||||||
sneak-mfer.$(ARCH).tzst.dockerimage: $(SOURCEFILES) vendor.tzst modcache.tzst
|
sneak-mfer.$(ARCH).tzst.dockerimage: $(SOURCEFILES) vendor.tzst modcache.tzst
|
||||||
docker build --progress plain --build-arg GITREV=$(GITREV_BUILD) -t sneak/mfer .
|
docker build --progress plain --build-arg GITREV=$(GITREV_BUILD) -t sneak/mfer .
|
||||||
|
|||||||
278
README.md
278
README.md
@@ -3,79 +3,45 @@
|
|||||||
[mfer](https://git.eeqj.de/sneak/mfer) is a reference implementation library
|
[mfer](https://git.eeqj.de/sneak/mfer) is a reference implementation library
|
||||||
and thin wrapper command-line utility written in [Go](https://golang.org)
|
and thin wrapper command-line utility written in [Go](https://golang.org)
|
||||||
and first published in 2022 under the [WTFPL](https://wtfpl.net) (public
|
and first published in 2022 under the [WTFPL](https://wtfpl.net) (public
|
||||||
domain) license. It specifies and generates `.mf` manifest files over a
|
domain) license. It specifies and generates `.mf` manifest files over a
|
||||||
directory tree of files to encapsulate metadata about them (such as
|
directory tree of files to encapsulate metadata about them (such as
|
||||||
cryptographic checksums or signatures over same) to aid in archiving,
|
cryptographic checksums or signatures over same) to aid in archiving,
|
||||||
downloading, and streaming, or mirroring. The manifest files' data is
|
downloading, and streaming, or mirroring. The manifest files' data is
|
||||||
serialized with Google's [protobuf serialization
|
serialized with Google's [protobuf serialization
|
||||||
format](https://developers.google.com/protocol-buffers). The structure of
|
format](https://developers.google.com/protocol-buffers). The structure of
|
||||||
these files can be found [in the format
|
these files can be found [in the format
|
||||||
specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto)
|
specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto)
|
||||||
which is included in the [project
|
which is included in the [project
|
||||||
repository](https://git.eeqj.de/sneak/mfer).
|
repository](https://git.eeqj.de/sneak/mfer).
|
||||||
|
|
||||||
The current version is pre-1.0 and while the repo was published in 2022,
|
The current version is pre-1.0 and while the repo was published in 2022,
|
||||||
there has not yet been any versioned release. [SemVer](https://semver.org)
|
there has not yet been any versioned release. [SemVer](https://semver.org)
|
||||||
will be used for releases.
|
will be used for releases.
|
||||||
|
|
||||||
This project was started by [@sneak](https://sneak.berlin) to scratch an
|
This project was started by [@sneak](https://sneak.berlin) to scratch an
|
||||||
itch in 2022 and is currently a one-person effort, though the goal is for
|
itch in 2022 and is currently a one-person effort, though the goal is for
|
||||||
this to emerge as a de-facto standard and be incorporated into other
|
this to emerge as a de-facto standard and be incorporated into other
|
||||||
software. A compatible javascript library is planned.
|
software. A compatible javascript library is planned.
|
||||||
|
|
||||||
# Build Status
|
# Build Status
|
||||||
|
|
||||||
CI runs via `script/cibuild` (`docker build .`), which executes `make
|
[](https://drone.datavi.be/sneak/mfer)
|
||||||
check` (formatting, linting, tests). The `main` branch must always be
|
|
||||||
green.
|
|
||||||
|
|
||||||
# Entrypoints
|
|
||||||
|
|
||||||
This repository adheres to the
|
|
||||||
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
|
||||||
standard: normalized scripts in `script/` are the entrypoints for the
|
|
||||||
development workflow, and the Makefile targets are thin shims that call
|
|
||||||
them. We provide:
|
|
||||||
|
|
||||||
- `script/bootstrap` — install all dependencies (Go, golangci-lint, Go
|
|
||||||
module download), idempotently
|
|
||||||
- `script/setup` — make a fresh clone ready for development: runs
|
|
||||||
`script/bootstrap`, then `script/install-precommit`
|
|
||||||
- `script/projectname` — output the project name (`mfer`); used by other
|
|
||||||
scripts such as `script/docker`
|
|
||||||
- `script/test` — run the test suite (`go test`), regenerating the
|
|
||||||
protobuf code first if it is stale
|
|
||||||
- `script/lint` — run `golangci-lint` and verify `gofmt` cleanliness
|
|
||||||
- `script/fmt` — format all code and docs (writes): `gofumpt`,
|
|
||||||
`golangci-lint run --fix`, and prettier for JSON/Markdown
|
|
||||||
- `script/fmt-check` — check formatting without writing
|
|
||||||
- `script/check` — run `script/test`, `script/lint`, and
|
|
||||||
`script/fmt-check`
|
|
||||||
- `script/docker` — build the Docker image tagged with the project name
|
|
||||||
- `script/cibuild` — CI entrypoint: `docker build .` (the Dockerfile
|
|
||||||
runs the checks)
|
|
||||||
- `script/precommit` — pre-commit checks: `go mod tidy` verification,
|
|
||||||
then `script/check`
|
|
||||||
- `script/install-precommit` — install the git pre-commit hook that
|
|
||||||
runs `script/precommit`
|
|
||||||
|
|
||||||
# Participation
|
# Participation
|
||||||
|
|
||||||
The community is as yet nonexistent so there are no defined policies or
|
The community is as yet nonexistent so there are no defined policies or
|
||||||
norms yet. Primary development happens on a privately-run Gitea instance at
|
norms yet. Primary development happens on a privately-run Gitea instance at
|
||||||
[https://git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer) and issues
|
[https://git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer) and issues
|
||||||
are [tracked there](https://git.eeqj.de/sneak/mfer/issues).
|
are [tracked there](https://git.eeqj.de/sneak/mfer/issues).
|
||||||
|
|
||||||
Changes must always be formatted with a standard `go fmt`, syntactically
|
Changes must always be formatted with a standard `go fmt`, syntactically
|
||||||
valid, and must pass the linting defined in the repository (presently only
|
valid, and must pass the linting defined in the repository (presently only
|
||||||
the `golangci-lint` defaults), which can be run with a `make lint`. The
|
the `golangci-lint` defaults), which can be run with a `make lint`. The
|
||||||
`main` branch is protected and all changes must be made via [pull
|
`main` branch is protected and all changes must be made via [pull
|
||||||
requests](https://git.eeqj.de/sneak/mfer/pulls) and pass CI to be merged.
|
requests](https://git.eeqj.de/sneak/mfer/pulls) and pass CI to be merged.
|
||||||
Any changes submitted to this project must also be
|
Any changes submitted to this project must also be
|
||||||
[WTFPL-licensed](https://wtfpl.net) to be considered.
|
[WTFPL-licensed](https://wtfpl.net) to be considered.
|
||||||
|
|
||||||
See [`REPO_POLICIES.md`](REPO_POLICIES.md) for detailed coding standards,
|
|
||||||
tooling requirements, and workflow conventions.
|
|
||||||
|
|
||||||
# Problem Statement
|
# Problem Statement
|
||||||
|
|
||||||
@@ -241,236 +207,24 @@ regardless of filesystem format.
|
|||||||
Please email [`sneak@sneak.berlin`](mailto:sneak@sneak.berlin) with your
|
Please email [`sneak@sneak.berlin`](mailto:sneak@sneak.berlin) with your
|
||||||
desired username for an account on this Gitea instance.
|
desired username for an account on this Gitea instance.
|
||||||
|
|
||||||
# TODO: Remaining Work for 1.0
|
|
||||||
|
|
||||||
## Design Questions (Owner Decision Required)
|
|
||||||
|
|
||||||
These require @sneak's input before implementation. Answers should be added
|
|
||||||
inline below each question.
|
|
||||||
|
|
||||||
### Format Design
|
|
||||||
|
|
||||||
**1. Should `MFFileChecksum` be simplified?** Currently it's a separate
|
|
||||||
message wrapping a single `bytes multiHash` field. Since multihash
|
|
||||||
already self-describes the algorithm, `repeated bytes hashes` directly on
|
|
||||||
`MFFilePath` would be simpler and reduce per-file protobuf overhead. Is
|
|
||||||
the extra message layer intentional (e.g. planning to add per-hash
|
|
||||||
metadata like `verified_at`)?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**2. Should file permissions/mode be stored?** The format stores
|
|
||||||
mtime/ctime but not Unix file permissions. For archival use this may not
|
|
||||||
matter, but for software distribution or filesystem restoration it's a
|
|
||||||
gap. Should we reserve a field now (e.g. `optional uint32 mode = 305`)
|
|
||||||
even if we don't populate it yet?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**3. Should `atime` be removed from the schema?** Access time is
|
|
||||||
volatile, non-deterministic, and often disabled (`noatime`). Including it
|
|
||||||
means two manifests of the same directory at different times will differ,
|
|
||||||
which conflicts with the determinism goal. Remove it, or document it as
|
|
||||||
"never set by default"?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**4. What are the path normalization rules?** The proto has `string path`
|
|
||||||
with no specification about: always forward-slash? Must be relative? No
|
|
||||||
`..` components allowed? UTF-8 NFC vs NFD normalization (macOS vs
|
|
||||||
Linux)? Max path length? This is a security issue (path traversal) and a
|
|
||||||
cross-platform compatibility issue. What rules should the spec mandate?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**5. Should we add a version byte after the magic?** Currently
|
|
||||||
`ZNAVSRFG` is followed immediately by protobuf. Adding a version byte
|
|
||||||
(`ZNAVSRFG\x01`) would allow future framing changes without requiring
|
|
||||||
protobuf parsing to detect the version. `MFFileOuter.Version` serves
|
|
||||||
this purpose but requires successful deserialization to read. Worth the
|
|
||||||
extra byte?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**6. Should we add a length-prefix after the magic?** Protobuf is not
|
|
||||||
self-delimiting. If we ever want to concatenate manifests or append data
|
|
||||||
after the protobuf, the current framing is insufficient. Add a varint or
|
|
||||||
fixed-width length-prefix?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
### Signature Design
|
|
||||||
|
|
||||||
**7. What does the outer SHA-256 hash cover — compressed or uncompressed
|
|
||||||
data?** The code currently hashes compressed data (good for verifying
|
|
||||||
before decompression), but this should be explicitly documented. Which is
|
|
||||||
the intended behavior?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**8. Should `signatureString()` sign raw bytes instead of a hex-encoded
|
|
||||||
string?** Currently the canonical string is `MAGIC-UUID-MULTIHASH` with
|
|
||||||
hex encoding, which adds a transformation layer. Signing the raw `sha256`
|
|
||||||
bytes (or compressed `innerMessage` directly) would be simpler. Keep the
|
|
||||||
string format or switch to raw bytes?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**9. Should we support detached signature files (`.mf.sig`)?** Embedded
|
|
||||||
signatures are better for single-file distribution. Detached `.mf.sig`
|
|
||||||
files follow the familiar `SHASUMS`/`SHASUMS.asc` pattern and are
|
|
||||||
simpler for HTTP serving. Support both modes?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**10. GPG vs pure-Go crypto for signatures?** Shelling out to `gpg` is
|
|
||||||
fragile (may not be installed, version-dependent output).
|
|
||||||
`github.com/ProtonMail/go-crypto` provides pure-Go OpenPGP, or we could
|
|
||||||
use Ed25519/signify (simpler, no key management). Which direction?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
### Implementation Design
|
|
||||||
|
|
||||||
**11. Should manifests be deterministic by default?** This means: sort
|
|
||||||
file entries by path, omit `createdAt` timestamp (or make it opt-in), no
|
|
||||||
`atime`. Should determinism be the default, with a
|
|
||||||
`--include-timestamps` flag to opt in?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**12. Should we consolidate or keep both scanner/checker
|
|
||||||
implementations?** There are two parallel implementations:
|
|
||||||
`mfer/scanner.go` + `mfer/checker.go` (typed with `FileSize`,
|
|
||||||
`RelFilePath`) and `internal/scanner/` + `internal/checker/` (raw
|
|
||||||
`int64`, `string`). The `mfer/` versions are superior. Delete the
|
|
||||||
`internal/` versions?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**13. Should the `manifest` type be exported?** Currently unexported with
|
|
||||||
exported constructors (`NewManifestFromReader`, `NewManifestFromFile`).
|
|
||||||
Consumers can't declare `var m *mfer.manifest`. Export the type, or
|
|
||||||
define an interface?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
**14. What should the Go module path be for 1.0?** Currently
|
|
||||||
`sneak.berlin/go/mfer` in `go.mod` but `git.eeqj.de/sneak/mfer/mfer` in
|
|
||||||
the proto `go_package` option. Which is canonical?
|
|
||||||
|
|
||||||
> _answer:_
|
|
||||||
|
|
||||||
## Implementation Tasks
|
|
||||||
|
|
||||||
### Repo Infrastructure
|
|
||||||
|
|
||||||
- [ ] Add `.golangci.yml` (fetch from
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`)
|
|
||||||
- [ ] Add `.editorconfig`
|
|
||||||
- [ ] Add `.gitea/workflows/check.yml` that runs `docker build .`
|
|
||||||
|
|
||||||
### Format & Correctness
|
|
||||||
|
|
||||||
- [ ] Resolve proto `go_package` path inconsistency
|
|
||||||
(`git.eeqj.de/sneak/mfer/mfer` vs `sneak.berlin/go/mfer`)
|
|
||||||
- [ ] Specify path invariants — add proto comments requiring UTF-8,
|
|
||||||
forward-slash, relative paths, no `..`, no leading `/`; validate
|
|
||||||
in `Builder.AddFile` and `Builder.AddFileWithHash` (pending design
|
|
||||||
question answer)
|
|
||||||
- [ ] Remove or deprecate `atime` from proto (pending design question
|
|
||||||
answer)
|
|
||||||
- [ ] Reserve `optional uint32 mode = 305` in `MFFilePath` for future
|
|
||||||
file permissions (pending design question answer)
|
|
||||||
- [ ] Add version byte after magic — `ZNAVSRFG\x01` for format version
|
|
||||||
1 (pending design question answer)
|
|
||||||
- [ ] Write format specification document — separate from README:
|
|
||||||
magic, outer structure, compression, inner structure, path
|
|
||||||
invariants, signature scheme, canonical serialization
|
|
||||||
|
|
||||||
### Library
|
|
||||||
|
|
||||||
- [ ] Delete `internal/scanner/` and `internal/checker/` — consolidate
|
|
||||||
on `mfer/` package versions; update CLI code (pending design
|
|
||||||
question answer)
|
|
||||||
- [ ] Add deterministic file ordering — sort entries by path
|
|
||||||
(lexicographic, byte-order) in `Builder.Build()`; add test
|
|
||||||
asserting byte-identical output from two runs
|
|
||||||
- [ ] Add decompression size limit — `io.LimitReader` in
|
|
||||||
`deserializeInner()` with `m.pbOuter.Size` as bound
|
|
||||||
- [ ] Fix `errors.Is` dead code in checker — replace with
|
|
||||||
`os.IsNotExist(err)` or `errors.Is(err, fs.ErrNotExist)`
|
|
||||||
- [ ] Fix `AddFile` to verify size — check `totalRead == size` after
|
|
||||||
reading, return error on mismatch
|
|
||||||
- [ ] Export the `manifest` type or define a public interface (pending
|
|
||||||
design question answer) — currently consumers cannot hold a reference
|
|
||||||
to a loaded manifest in their own type declarations
|
|
||||||
- [ ] Replace GPG subprocess calls with pure-Go crypto (pending design
|
|
||||||
question answer) — current implementation shells out to `gpg` which
|
|
||||||
may not be installed
|
|
||||||
- [ ] Add timeout to any remaining subprocess calls
|
|
||||||
|
|
||||||
### CLI
|
|
||||||
|
|
||||||
- [ ] Fix flag naming — all CLI flags should use kebab-case as primary
|
|
||||||
(`--include-dotfiles`, `--follow-symlinks`)
|
|
||||||
- [ ] Fix URL construction in fetch — use `BaseURL.JoinPath()` or
|
|
||||||
`url.JoinPath()` instead of string concatenation
|
|
||||||
- [ ] Add progress rate-limiting to Checker — throttle to once per
|
|
||||||
second, matching Scanner
|
|
||||||
- [ ] Add `--deterministic` flag or make it default — omit `createdAt`,
|
|
||||||
sort files (pending design question answer)
|
|
||||||
- [ ] Wire `--version` flag properly (currently only a `version`
|
|
||||||
subcommand exists; top-level `--version` shows urfave/cli generic
|
|
||||||
output)
|
|
||||||
- [ ] Add retry logic to `fetch` — currently no retries on transient
|
|
||||||
HTTP errors; needs exponential backoff
|
|
||||||
- [ ] `fetch` command uses bare `http.Get` with no timeout — needs
|
|
||||||
`http.Client` with configurable timeout
|
|
||||||
|
|
||||||
### Testing & Robustness
|
|
||||||
|
|
||||||
- [ ] Add fuzzing tests for `NewManifestFromReader` — protobuf
|
|
||||||
deserialization of untrusted input needs fuzz coverage
|
|
||||||
- [ ] Add integration test for `freshen` CLI command — current tests
|
|
||||||
only verify setup, not the actual freshen operation end-to-end
|
|
||||||
- [ ] Add test for `fetch` CLI command end-to-end (currently only
|
|
||||||
`downloadFile` is tested)
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- [ ] Promote `FORMAT.md` as primary spec reference; README should link
|
|
||||||
to it more prominently
|
|
||||||
- [ ] Audit and update all error messages for consistency and
|
|
||||||
helpfulness
|
|
||||||
- [ ] Document the signature scheme more thoroughly (canonical string
|
|
||||||
format, verification steps)
|
|
||||||
|
|
||||||
### Release
|
|
||||||
|
|
||||||
- [ ] Finalize Go module path
|
|
||||||
- [ ] Update version constant in `mfer/constants.go`
|
|
||||||
- [ ] Add `--version` output matching SemVer
|
|
||||||
- [ ] Tag `v1.0.0`
|
|
||||||
|
|
||||||
# See Also
|
# See Also
|
||||||
|
|
||||||
## Prior Art: Metalink
|
## Prior Art: Metalink
|
||||||
|
|
||||||
- [Metalink - Mozilla Wiki](https://wiki.mozilla.org/Metalink)
|
* [Metalink - Mozilla Wiki](https://wiki.mozilla.org/Metalink)
|
||||||
- [Metalink - Wikipedia](https://en.wikipedia.org/wiki/Metalink)
|
* [Metalink - Wikipedia](https://en.wikipedia.org/wiki/Metalink)
|
||||||
- [RFC 5854 - The Metalink Download Description Format](https://datatracker.ietf.org/doc/html/rfc5854)
|
* [RFC 5854 - The Metalink Download Description Format](https://datatracker.ietf.org/doc/html/rfc5854)
|
||||||
- [RFC 6249 - Metalink/HTTP: Mirrors and Hashes](https://www.rfc-editor.org/rfc/rfc6249.html)
|
* [RFC 6249 - Metalink/HTTP: Mirrors and Hashes](https://www.rfc-editor.org/rfc/rfc6249.html)
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- Repo: [https://git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer)
|
* Repo: [https://git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer)
|
||||||
- Issues: [https://git.eeqj.de/sneak/mfer/issues](https://git.eeqj.de/sneak/mfer/issues)
|
* Issues: [https://git.eeqj.de/sneak/mfer/issues](https://git.eeqj.de/sneak/mfer/issues)
|
||||||
|
|
||||||
# Authors
|
# Authors
|
||||||
|
|
||||||
- [@sneak <sneak@sneak.berlin>](mailto:sneak@sneak.berlin)
|
* [@sneak <sneak@sneak.berlin>](mailto:sneak@sneak.berlin)
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
- [WTFPL](https://wtfpl.net)
|
* [WTFPL](https://wtfpl.net)
|
||||||
|
|||||||
408
REPO_POLICIES.md
408
REPO_POLICIES.md
@@ -1,408 +0,0 @@
|
|||||||
---
|
|
||||||
title: Repository Policies
|
|
||||||
last_modified: 2026-07-06
|
|
||||||
---
|
|
||||||
|
|
||||||
This document covers repository structure, tooling, and workflow standards. Code
|
|
||||||
style conventions are in separate documents:
|
|
||||||
|
|
||||||
- [Code Styleguide](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE.md)
|
|
||||||
(general, bash, Docker)
|
|
||||||
- [Go](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_GO.md)
|
|
||||||
- [JavaScript](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_JS.md)
|
|
||||||
- [Python](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_PYTHON.md)
|
|
||||||
- [Go HTTP Server Conventions](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/GO_HTTP_SERVER_CONVENTIONS.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- Cross-project documentation (such as this file) must include
|
|
||||||
`last_modified: YYYY-MM-DD` in the YAML front matter so it can be kept in sync
|
|
||||||
with the authoritative source as policies evolve.
|
|
||||||
|
|
||||||
- **ALL external references must be pinned by cryptographic hash.** This
|
|
||||||
includes Docker base images, Go modules, npm packages, GitHub Actions, and
|
|
||||||
anything else fetched from a remote source. Version tags (`@v4`, `@latest`,
|
|
||||||
`:3.21`, etc.) are server-mutable and therefore remote code execution
|
|
||||||
vulnerabilities. The ONLY acceptable way to reference an external dependency
|
|
||||||
is by its content hash (Docker `@sha256:...`, Go module hash in `go.sum`, npm
|
|
||||||
integrity hash in lockfile, GitHub Actions `@<commit-sha>`). No exceptions.
|
|
||||||
This also means never `curl | bash` to install tools like pyenv, nvm, rustup,
|
|
||||||
etc. Instead, download a specific release archive from GitHub, verify its hash
|
|
||||||
(hardcoded in the Dockerfile or script), and only then install. Unverified
|
|
||||||
install scripts are arbitrary remote code execution. This is the single most
|
|
||||||
important rule in this document. Double-check every external reference in
|
|
||||||
every file before committing. There are zero exceptions to this rule.
|
|
||||||
|
|
||||||
- Every repo with software must have a root `Makefile` with these targets:
|
|
||||||
`make bootstrap`, `make setup`, `make test`, `make lint`, `make fmt` (writes),
|
|
||||||
`make fmt-check` (read-only), `make check` (runs `test`, `lint`, `fmt-check`),
|
|
||||||
`make docker`, and `make hooks` (installs pre-commit hook). A model Makefile
|
|
||||||
is at `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`.
|
|
||||||
|
|
||||||
- Repos follow the
|
|
||||||
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
|
||||||
pattern: the implementation of each Makefile target lives in an executable
|
|
||||||
script in `script/` (`script/bootstrap`, `script/setup`, `script/test`,
|
|
||||||
`script/lint`, `script/fmt`, `script/fmt-check`, `script/check`,
|
|
||||||
`script/docker`), and the Makefile targets are thin shims that call them. The
|
|
||||||
scripts must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run in
|
|
||||||
minimal containers (e.g. alpine images have no bash); locate the repo root
|
|
||||||
with `$(cd "$(dirname "$0")/.." && pwd -P)` and `cd` there before acting. From
|
|
||||||
the standard's canonical set we use `bootstrap`, `setup` (make the repo ready
|
|
||||||
for development after a fresh clone: runs `bootstrap`, then
|
|
||||||
`install-precommit`, plus any repo-specific initialization), `test`, and
|
|
||||||
`cibuild`. `script/bootstrap` installs all dependencies idempotently and
|
|
||||||
assumes nothing is present: base tools come from nix, apt, brew, or apk
|
|
||||||
(detected in that order; apt runs noninteractive). For node it uses the
|
|
||||||
installed node if present; otherwise it installs a PINNED node version via
|
|
||||||
nvm, first installing nvm itself if missing — from a hash-verified GitHub
|
|
||||||
release archive (never `curl | sh`), with bash installed as an explicit
|
|
||||||
prerequisite since nvm requires bash. yarn is then pinned via
|
|
||||||
`corepack prepare yarn@<version> --activate`. Never install "latest" or "lts";
|
|
||||||
always exact versions. `script/cibuild` runs the CI build: it changes to the
|
|
||||||
repo root and runs `docker build .`; the Gitea workflow calls it. Four further
|
|
||||||
scripts are our own extensions to the standard: `script/check` runs
|
|
||||||
`script/test`, `script/lint`, and `script/fmt-check`; `script/precommit` is
|
|
||||||
what the git pre-commit hook runs, and it calls `script/check`;
|
|
||||||
`script/install-precommit` installs the git pre-commit hook (the `make hooks`
|
|
||||||
target shims to it); and `script/projectname` (literally that filename) simply
|
|
||||||
outputs the project's name. Scripts that need the name call
|
|
||||||
`script/projectname` — e.g. `script/docker` assembles its image tag from it —
|
|
||||||
so those scripts stay byte-identical across all repos. Repo-type-specific
|
|
||||||
pre-commit extras (e.g. `go mod tidy` verification in Go repos) belong in
|
|
||||||
`script/precommit`, not in the hook itself. Model scripts are at
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`. The README
|
|
||||||
must document the provided scripts in an **Entrypoints** section (see the
|
|
||||||
README requirements below).
|
|
||||||
|
|
||||||
- Always use Makefile targets (`make fmt`, `make test`, `make lint`, etc.)
|
|
||||||
instead of invoking the underlying tools directly. The Makefile is the single
|
|
||||||
source of truth for how these operations are run.
|
|
||||||
|
|
||||||
- The Makefile is authoritative documentation for how the repo is used. Beyond
|
|
||||||
the required targets above, it should have targets for every common operation:
|
|
||||||
running a local development server (`make run`, `make dev`), re-initializing
|
|
||||||
or migrating the database (`make db-reset`, `make migrate`), building
|
|
||||||
artifacts (`make build`), generating code, seeding data, or anything else a
|
|
||||||
developer would do regularly. If someone checks out the repo and types
|
|
||||||
`make<tab>`, they should see every meaningful operation available. A new
|
|
||||||
contributor should be able to understand the entire development workflow by
|
|
||||||
reading the Makefile.
|
|
||||||
|
|
||||||
- Every repo should have a `Dockerfile`. All Dockerfiles must run `make check`
|
|
||||||
as a build step so the build fails if the branch is not green. For non-server
|
|
||||||
repos, the Dockerfile should bring up a development environment and run
|
|
||||||
`make check`. For server repos, `make check` should run as an early build
|
|
||||||
stage before the final image is assembled. Dockerfiles install development
|
|
||||||
prerequisites by running `script/bootstrap` rather than duplicating installs
|
|
||||||
inline; COPY `script/` and the dependency manifests (`package.json` +
|
|
||||||
`yarn.lock`, `go.mod` + `go.sum`, etc.) before running it so the bootstrap
|
|
||||||
layer stays cached until dependencies change.
|
|
||||||
|
|
||||||
- **Dockerfiles must use a separate lint stage for fail-fast feedback.** Go
|
|
||||||
repos use a multistage build where linting runs in an independent stage based
|
|
||||||
on the `golangci/golangci-lint` image (pinned by hash). This stage runs
|
|
||||||
`make fmt-check` and `make lint` before the full build begins. The build stage
|
|
||||||
then declares an explicit dependency on the lint stage via
|
|
||||||
`COPY --from=lint /src/go.sum /dev/null`, which forces BuildKit to complete
|
|
||||||
linting before proceeding to compilation and tests. This ensures lint failures
|
|
||||||
surface in seconds rather than minutes, without blocking on dependency
|
|
||||||
download or compilation in the build stage.
|
|
||||||
|
|
||||||
The standard pattern for a Go repo Dockerfile is:
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Lint stage — fast feedback on formatting and lint issues
|
|
||||||
# golangci/golangci-lint:v2.x.x, YYYY-MM-DD
|
|
||||||
FROM golangci/golangci-lint@sha256:... AS lint
|
|
||||||
WORKDIR /src
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
COPY . .
|
|
||||||
RUN make fmt-check
|
|
||||||
RUN make lint
|
|
||||||
|
|
||||||
# Build stage
|
|
||||||
# golang:1.x-alpine, YYYY-MM-DD
|
|
||||||
FROM golang@sha256:... AS builder
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
# Force BuildKit to run the lint stage before proceeding
|
|
||||||
COPY --from=lint /src/go.sum /dev/null
|
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
COPY . .
|
|
||||||
RUN make test
|
|
||||||
|
|
||||||
ARG VERSION=dev
|
|
||||||
RUN CGO_ENABLED=0 go build -trimpath \
|
|
||||||
-ldflags="-s -w -X main.Version=${VERSION}" \
|
|
||||||
-o /app ./cmd/app/
|
|
||||||
|
|
||||||
# Runtime stage
|
|
||||||
FROM alpine@sha256:...
|
|
||||||
COPY --from=builder /app /usr/local/bin/app
|
|
||||||
ENTRYPOINT ["app"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Key points:
|
|
||||||
- The lint stage uses the `golangci/golangci-lint` image directly (it
|
|
||||||
includes both Go and the linter), so there is no need to install the
|
|
||||||
linter separately.
|
|
||||||
- `COPY --from=lint /src/go.sum /dev/null` is a no-op file copy that creates
|
|
||||||
a stage dependency. BuildKit runs stages in parallel by default; without
|
|
||||||
this line, the build stage would not wait for lint to finish and a lint
|
|
||||||
failure might not fail the overall build.
|
|
||||||
- If the project uses `//go:embed` directives that reference build artifacts
|
|
||||||
(e.g. a web frontend compiled in a separate stage), the lint stage must
|
|
||||||
create placeholder files so the embed directives resolve. Example:
|
|
||||||
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
|
|
||||||
The lint stage should not depend on the actual build output — it exists to
|
|
||||||
fail fast.
|
|
||||||
- If the project requires CGO or system libraries for linting (e.g.
|
|
||||||
`vips-dev`), install them in the lint stage with `apk add`.
|
|
||||||
- The build stage runs `make test` after compilation setup. Tests run in the
|
|
||||||
build stage, not the lint stage, because they may require compiled
|
|
||||||
artifacts or heavier dependencies.
|
|
||||||
|
|
||||||
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
|
|
||||||
runs `script/cibuild` (which runs `docker build .`) on push. Since the
|
|
||||||
Dockerfile already runs `make check`, a successful build implies all checks
|
|
||||||
pass.
|
|
||||||
|
|
||||||
- Use platform-standard formatters: `black` for Python, `prettier` for
|
|
||||||
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
|
|
||||||
two exceptions: four-space indents (except Go), and `proseWrap: always` for
|
|
||||||
Markdown (hard-wrap at 80 columns). Documentation and writing repos (Markdown,
|
|
||||||
HTML, CSS) should also have `.prettierrc` and `.prettierignore`.
|
|
||||||
|
|
||||||
- Pre-commit hook: runs `script/precommit`, which calls `script/check`. If local
|
|
||||||
testing is not possible in the repo, `script/precommit` may skip `script/test`
|
|
||||||
and run only `script/lint` and `script/fmt-check`. The hook is installed by
|
|
||||||
`script/install-precommit`; the Makefile must provide a `make hooks` target
|
|
||||||
that shims to it.
|
|
||||||
|
|
||||||
- All repos with software must have tests that run via the platform-standard
|
|
||||||
test framework (`go test`, `pytest`, `jest`/`vitest`, etc.). If no meaningful
|
|
||||||
tests exist yet, add the most minimal test possible — e.g. importing the
|
|
||||||
module under test to verify it compiles/parses. There is no excuse for
|
|
||||||
`make test` to be a no-op.
|
|
||||||
|
|
||||||
- `make test` must complete in under 20 seconds. Add a 30-second timeout in the
|
|
||||||
Makefile.
|
|
||||||
|
|
||||||
- **`make test` should use the conditional verbose rerun pattern.** Run tests
|
|
||||||
without `-v` (verbose) first. If tests fail, automatically rerun with `-v` to
|
|
||||||
show full output. This keeps CI logs and `docker build` output clean on
|
|
||||||
success (just package/suite summaries) while providing full diagnostic detail
|
|
||||||
on failure (every test case, every assertion). The general shell pattern:
|
|
||||||
|
|
||||||
```makefile
|
|
||||||
test:
|
|
||||||
@<test-command> || \
|
|
||||||
{ echo "--- Rerunning with -v for details ---"; \
|
|
||||||
<test-command-with-v>; exit 1; }
|
|
||||||
```
|
|
||||||
|
|
||||||
Go example:
|
|
||||||
|
|
||||||
```makefile
|
|
||||||
test:
|
|
||||||
@go test -timeout 30s -race -cover ./... || \
|
|
||||||
{ echo "--- Rerunning with -v for details ---"; \
|
|
||||||
go test -timeout 30s -race -v ./...; exit 1; }
|
|
||||||
```
|
|
||||||
|
|
||||||
Python example:
|
|
||||||
|
|
||||||
```makefile
|
|
||||||
test:
|
|
||||||
@python -m pytest || \
|
|
||||||
{ echo "--- Rerunning with -v for details ---"; \
|
|
||||||
python -m pytest -v; exit 1; }
|
|
||||||
```
|
|
||||||
|
|
||||||
The `exit 1` ensures the target always fails after a rerun — the first run
|
|
||||||
already proved the tests are broken, so the build must not pass even if a
|
|
||||||
flaky test happens to succeed on the second attempt. The rerun exists solely
|
|
||||||
for diagnostic output.
|
|
||||||
|
|
||||||
- Docker builds must complete in under 5 minutes.
|
|
||||||
|
|
||||||
- `make check` must not modify any files in the repo. Tests may use temporary
|
|
||||||
directories.
|
|
||||||
|
|
||||||
- `main` must always pass `make check`, no exceptions.
|
|
||||||
|
|
||||||
- Never commit secrets. `.env` files, credentials, API keys, and private keys
|
|
||||||
must be in `.gitignore`. No exceptions.
|
|
||||||
|
|
||||||
- `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`),
|
|
||||||
editor files (`.swp`, `*~`), language build artifacts, and `node_modules/`.
|
|
||||||
Fetch the standard `.gitignore` from
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
|
|
||||||
a new repo.
|
|
||||||
|
|
||||||
- **No build artifacts in version control.** Code-derived data (compiled
|
|
||||||
bundles, minified output, generated assets) must never be committed to the
|
|
||||||
repository if it can be avoided. The build process (e.g. Dockerfile, Makefile)
|
|
||||||
should generate these at build time. Notable exception: Go protobuf generated
|
|
||||||
files (`.pb.go`) ARE committed because repos need to work with `go get`, which
|
|
||||||
downloads code but does not execute code generation.
|
|
||||||
|
|
||||||
- Never use `git add -A` or `git add .`. Always stage files explicitly by name.
|
|
||||||
|
|
||||||
- Never force-push to `main`.
|
|
||||||
|
|
||||||
- Make all changes on a feature branch. You can do whatever you want on a
|
|
||||||
feature branch.
|
|
||||||
|
|
||||||
- `.golangci.yml` is standardized and must _NEVER_ be modified by an agent, only
|
|
||||||
manually by the user. Fetch from
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`.
|
|
||||||
|
|
||||||
- When pinning images or packages by hash, add a comment above the reference
|
|
||||||
with the version and date (YYYY-MM-DD).
|
|
||||||
|
|
||||||
- Use `yarn`, not `npm`.
|
|
||||||
|
|
||||||
- Write all dates as YYYY-MM-DD (ISO 8601).
|
|
||||||
|
|
||||||
- Simple projects should be configured with environment variables.
|
|
||||||
|
|
||||||
- Dockerized web services listen on port 8080 by default, overridable with
|
|
||||||
`PORT`.
|
|
||||||
|
|
||||||
- **HTTP/web services must be hardened for production internet exposure before
|
|
||||||
tagging 1.0.** This means full compliance with security best practices
|
|
||||||
including, without limitation, all of the following:
|
|
||||||
- **Security headers** on every response:
|
|
||||||
- `Strict-Transport-Security` (HSTS) with `max-age` of at least one year
|
|
||||||
and `includeSubDomains`.
|
|
||||||
- `Content-Security-Policy` (CSP) with a restrictive default policy
|
|
||||||
(`default-src 'self'` as a baseline, tightened per-resource as
|
|
||||||
needed). Never use `unsafe-inline` or `unsafe-eval` unless
|
|
||||||
unavoidable, and document the reason.
|
|
||||||
- `X-Frame-Options: DENY` (or `SAMEORIGIN` if framing is required).
|
|
||||||
Prefer the `frame-ancestors` CSP directive as the primary control.
|
|
||||||
- `X-Content-Type-Options: nosniff`.
|
|
||||||
- `Referrer-Policy: strict-origin-when-cross-origin` (or stricter).
|
|
||||||
- `Permissions-Policy` restricting access to browser features the
|
|
||||||
application does not use (camera, microphone, geolocation, etc.).
|
|
||||||
- **Request and response limits:**
|
|
||||||
- Maximum request body size enforced on all endpoints (e.g. Go
|
|
||||||
`http.MaxBytesReader`). Choose a sane default per-route; never accept
|
|
||||||
unbounded input.
|
|
||||||
- Maximum response body size where applicable (e.g. paginated APIs).
|
|
||||||
- `ReadTimeout` and `ReadHeaderTimeout` on the `http.Server` to defend
|
|
||||||
against slowloris attacks.
|
|
||||||
- `WriteTimeout` on the `http.Server`.
|
|
||||||
- `IdleTimeout` on the `http.Server`.
|
|
||||||
- Per-handler execution time limits via `context.WithTimeout` or
|
|
||||||
chi/stdlib `middleware.Timeout`.
|
|
||||||
- **Authentication and session security:**
|
|
||||||
- Rate limiting on password-based authentication endpoints. API keys are
|
|
||||||
high-entropy and not susceptible to brute force, so they are exempt.
|
|
||||||
- CSRF tokens on all state-mutating HTML forms. API endpoints
|
|
||||||
authenticated via `Authorization` header (Bearer token, API key) are
|
|
||||||
exempt because the browser does not attach these automatically.
|
|
||||||
- Passwords stored using bcrypt, scrypt, or argon2 — never plain-text,
|
|
||||||
MD5, or SHA.
|
|
||||||
- Session cookies set with `HttpOnly`, `Secure`, and `SameSite=Lax` (or
|
|
||||||
`Strict`) attributes.
|
|
||||||
- **Reverse proxy awareness:**
|
|
||||||
- True client IP detection when behind a reverse proxy
|
|
||||||
(`X-Forwarded-For`, `X-Real-IP`). The application must accept
|
|
||||||
forwarded headers only from a configured set of trusted proxy
|
|
||||||
addresses — never trust `X-Forwarded-For` unconditionally.
|
|
||||||
- **CORS:**
|
|
||||||
- Authenticated endpoints must restrict `Access-Control-Allow-Origin` to
|
|
||||||
an explicit allowlist of known origins. Wildcard (`*`) is acceptable
|
|
||||||
only for public, unauthenticated read-only APIs.
|
|
||||||
- **Error handling:**
|
|
||||||
- Internal errors must never leak stack traces, SQL queries, file paths,
|
|
||||||
or other implementation details to the client. Return generic error
|
|
||||||
messages in production; detailed errors only when `DEBUG` is enabled.
|
|
||||||
- **TLS:**
|
|
||||||
- Services never terminate TLS directly. They are always deployed behind
|
|
||||||
a TLS-terminating reverse proxy. The service itself listens on plain
|
|
||||||
HTTP. However, HSTS headers and `Secure` cookie flags must still be
|
|
||||||
set by the application so that the browser enforces HTTPS end-to-end.
|
|
||||||
|
|
||||||
This list is non-exhaustive. Apply defense-in-depth: if a standard security
|
|
||||||
hardening measure exists for HTTP services and is not listed here, it is
|
|
||||||
still expected. When in doubt, harden.
|
|
||||||
|
|
||||||
- `README.md` is the primary documentation. Required sections:
|
|
||||||
- **Description**: First line must include the project name, purpose,
|
|
||||||
category (web server, SPA, CLI tool, etc.), license, and author. Example:
|
|
||||||
"µPaaS is an MIT-licensed Go web application by @sneak that receives
|
|
||||||
git-frontend webhooks and deploys applications via Docker in realtime."
|
|
||||||
- **Getting Started**: Copy-pasteable install/usage code block.
|
|
||||||
- **Entrypoints**: Opens by stating that the repo adheres to the
|
|
||||||
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
|
||||||
standard (with that link), then documents each provided `script/`
|
|
||||||
entrypoint and its purpose.
|
|
||||||
- **Rationale**: Why does this exist?
|
|
||||||
- **Design**: How is the program structured?
|
|
||||||
- **TODO**: Update meticulously, even between commits. When planning, put
|
|
||||||
the todo list in the README so a new agent can pick up where the last one
|
|
||||||
left off.
|
|
||||||
- **License**: MIT, GPL, or WTFPL. Ask the user for new projects. Include a
|
|
||||||
`LICENSE` file in the repo root and a License section in the README.
|
|
||||||
- **Author**: [@sneak](https://sneak.berlin).
|
|
||||||
|
|
||||||
- First commit of a new repo should contain only `README.md`.
|
|
||||||
|
|
||||||
- Go module root: `sneak.berlin/go/<name>`. Always run `go mod tidy` before
|
|
||||||
committing.
|
|
||||||
|
|
||||||
- Use SemVer.
|
|
||||||
|
|
||||||
- Database migrations live in `internal/db/migrations/` and must be embedded in
|
|
||||||
the binary.
|
|
||||||
- `000_migration.sql` — contains ONLY the creation of the migrations
|
|
||||||
tracking table itself. Nothing else.
|
|
||||||
- `001_schema.sql` — the full application schema.
|
|
||||||
- **Pre-1.0.0:** never add additional migration files (002, 003, etc.).
|
|
||||||
There is no installed base to migrate. Edit `001_schema.sql` directly.
|
|
||||||
- **Post-1.0.0:** add new numbered migration files for each schema change.
|
|
||||||
Never edit existing migrations after release.
|
|
||||||
|
|
||||||
- All repos should have an `.editorconfig` enforcing the project's indentation
|
|
||||||
settings.
|
|
||||||
|
|
||||||
- Avoid putting files in the repo root unless necessary. Root should contain
|
|
||||||
only project-level config files (`README.md`, `Makefile`, `Dockerfile`,
|
|
||||||
`LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and
|
|
||||||
language-specific config). Everything else goes in a subdirectory. Canonical
|
|
||||||
subdirectory names:
|
|
||||||
- `bin/` — executable scripts and tools
|
|
||||||
- `cmd/` — Go command entrypoints
|
|
||||||
- `configs/` — configuration templates and examples
|
|
||||||
- `deploy/` — deployment manifests (k8s, compose, terraform)
|
|
||||||
- `docs/` — documentation and markdown (README.md stays in root)
|
|
||||||
- `internal/` — Go internal packages
|
|
||||||
- `internal/db/migrations/` — database migrations
|
|
||||||
- `pkg/` — Go library packages
|
|
||||||
- `share/` — systemd units, data files
|
|
||||||
- `static/` — static assets (images, fonts, etc.)
|
|
||||||
- `web/` — web frontend source
|
|
||||||
|
|
||||||
- When setting up a new repo, files from the `prompts` repo may be used as
|
|
||||||
templates. Fetch them from
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/<path>`.
|
|
||||||
|
|
||||||
- New repos must contain at minimum:
|
|
||||||
- `README.md`, `.git`, `.gitignore`, `.editorconfig`
|
|
||||||
- `LICENSE`, `REPO_POLICIES.md` (copy from the `prompts` repo)
|
|
||||||
- `Makefile`
|
|
||||||
- `script/` entrypoints (`bootstrap`, `setup`, `projectname`, `test`,
|
|
||||||
`lint`, `fmt`, `fmt-check`, `check`, `docker`, `cibuild`, `precommit`,
|
|
||||||
`install-precommit`)
|
|
||||||
- `Dockerfile`, `.dockerignore`
|
|
||||||
- `.gitea/workflows/check.yml`
|
|
||||||
- Go: `go.mod`, `go.sum`, `.golangci.yml`
|
|
||||||
- JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
|
|
||||||
- Python: `pyproject.toml`
|
|
||||||
211
TODO.md
211
TODO.md
@@ -1,107 +1,122 @@
|
|||||||
# Workflow
|
# TODO: mfer 1.0
|
||||||
|
|
||||||
- branch (from `main`)
|
## Design Questions
|
||||||
- do the work in Next Step
|
|
||||||
- move Next Step to the top of Completed Steps
|
|
||||||
- move the top item of Future Steps into Next Step
|
|
||||||
- commit (`TODO.md` changes in the same commit as the work)
|
|
||||||
- merge to `main` if the branch is not protected, otherwise open a PR
|
|
||||||
- push
|
|
||||||
|
|
||||||
# Status
|
*sneak: please answer inline below each question. These are preserved for posterity.*
|
||||||
|
|
||||||
pre-1.0. No git tags. README section "TODO: Remaining Work for 1.0" lists
|
### Format Design
|
||||||
open design questions and implementation tasks; policy compliance work is
|
|
||||||
in flight and unmerged.
|
|
||||||
|
|
||||||
# Next Step
|
**1. Should `MFFileChecksum` be simplified?**
|
||||||
|
Currently it's a separate message wrapping a single `bytes multiHash` field. Since multihash already self-describes the algorithm, `repeated bytes hashes` directly on `MFFilePath` would be simpler and reduce per-file protobuf overhead. Is the extra message layer intentional (e.g. planning to add per-hash metadata like `verified_at`)?
|
||||||
|
|
||||||
Land the in-flight compliance branch chore/align-repo-policies: finish and
|
> *answer:*
|
||||||
commit the uncommitted work (32 modified Go files, new untracked
|
|
||||||
.golangci.yml and TODO.md), confirm `make check` is green, merge the branch
|
|
||||||
(one commit ahead of main as of 2026-07-03) to main, and push.
|
|
||||||
|
|
||||||
# Completed Steps
|
**2. Should file permissions/mode be stored?**
|
||||||
|
The format stores mtime/ctime but not Unix file permissions. For archival use (ExFAT, filesystem-independent checksums) this may not matter, but for software distribution or filesystem restoration it's a gap. Should we reserve a field now (e.g. `optional uint32 mode = 305`) even if we don't populate it yet?
|
||||||
|
|
||||||
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
|
> *answer:*
|
||||||
Makefile shims, README Entrypoints section
|
|
||||||
- 2026-07-03: aligned repo tooling, docs, and config with standardized
|
|
||||||
policies (7d9a138, on chore/align-repo-policies, unmerged)
|
|
||||||
- 2026-06-28: moved to standardized repo policies (#56, on main)
|
|
||||||
- 2026-04-07: added 1.0 roadmap as README TODO section, removed old
|
|
||||||
TODO.md (#54)
|
|
||||||
- 2026-03-20: added Gitea Actions CI workflow (#53)
|
|
||||||
- 2026-03-17: added REPO_POLICIES.md, renamed CLAUDE.md to AGENTS.md (#51);
|
|
||||||
removed committed .index.mf (#52)
|
|
||||||
- 2026-03-15: split Dockerfile with pre-built golangci-lint stage for
|
|
||||||
faster CI (#45)
|
|
||||||
- 2026-03-01: 1.0 quality polish: code review, tests, bug fixes, docs (#32)
|
|
||||||
- 2026-02-20: deterministic file ordering in Builder.Build() (#28);
|
|
||||||
removed committed vendor/modcache archives (#35)
|
|
||||||
- 2026-02-08: added --seed flag for deterministic manifest UUID
|
|
||||||
|
|
||||||
# Future Steps
|
**3. Should `atime` be removed from the schema?**
|
||||||
|
Access time is volatile, non-deterministic, and often disabled (`noatime`). Including it means two manifests of the same directory at different times will differ, which conflicts with the determinism goal. Remove it, or document it as "never set by default"?
|
||||||
|
|
||||||
- Compliance (fold of TODO.md audit 2026-07-02; verify which items the
|
> *answer:*
|
||||||
in-flight branch already closes, then check off):
|
|
||||||
- Add .editorconfig (canonical copy from sneak/prompts)
|
**4. What are the path normalization rules?**
|
||||||
- Add standardized .golangci.yml (present untracked on the branch;
|
The proto has `string path` with no specification about: always forward-slash? Must be relative? No `..` components allowed? UTF-8 NFC vs NFD normalization (macOS vs Linux)? Max path length? This is a security issue (path traversal) and a cross-platform compatibility issue. What rules should the spec mandate?
|
||||||
user-owned, copy verbatim)
|
|
||||||
- Make .gitignore cover secrets (.env, _.key, _.pem), OS files
|
> *answer:*
|
||||||
(.DS_Store), and editor files (_.swp, _~)
|
|
||||||
- Make fmt-check/lint verify with gofumpt, not gofmt -l, so
|
**5. Should we add a version byte after the magic?**
|
||||||
`make check` matches what `make fmt` writes
|
Currently `ZNAVSRFG` is followed immediately by protobuf. Adding a version byte (`ZNAVSRFG\x01`) would allow future framing changes without requiring protobuf parsing to detect the version. `MFFileOuter.Version` serves this purpose but requires successful deserialization to read. Worth the extra byte?
|
||||||
- Add README "Getting Started" section with copy-pasteable
|
|
||||||
install/usage block
|
> *answer:*
|
||||||
- Move FORMAT.md from repo root to docs/ and update the AGENTS.md
|
|
||||||
reference
|
**6. Should we add a length-prefix after the magic?**
|
||||||
- Pin Makefile-installed Go tools (protoc-gen-go@v1.28.1,
|
Protobuf is not self-delimiting. If we ever want to concatenate manifests or append data after the protobuf, the current framing is insufficient. Add a varint or fixed-width length-prefix?
|
||||||
golangci-lint@v2.0.2) by module hash, not mutable tag
|
|
||||||
- Set `make test` timeout to 30s (currently 10s)
|
> *answer:*
|
||||||
- Add explicit README "Rationale" heading (content exists under other
|
|
||||||
names); name the author in the README Description first line
|
### Signature Design
|
||||||
- Reconcile root-level AGENTS.md with directory-hygiene policy (keep
|
|
||||||
or relocate)
|
**7. What does the outer SHA-256 hash cover — compressed or uncompressed data?**
|
||||||
- Add a `make build` target
|
The review notes it currently hashes compressed data (good for verifying before decompression), but this should be explicitly documented. Which is the intended behavior?
|
||||||
- Rewrite `make hooks` to use printf or a heredoc instead of
|
|
||||||
non-portable `echo '...\n...'`
|
> *answer:*
|
||||||
- Answer the 14 owner design questions in the README 1.0 roadmap:
|
|
||||||
- Format: simplify MFFileChecksum; store file mode; drop atime;
|
**8. Should `signatureString()` sign raw bytes instead of a hex-encoded string?**
|
||||||
specify path normalization rules; version byte after magic;
|
Currently the canonical string is `MAGIC-UUID-MULTIHASH` with hex encoding, which adds a transformation layer. Signing the raw `sha256` bytes (or compressed `innerMessage` directly) would be simpler. Keep the string format or switch to raw bytes?
|
||||||
length-prefix after magic
|
|
||||||
- Signatures: hash covers compressed or uncompressed data; sign raw
|
> *answer:*
|
||||||
bytes vs hex canonical string; detached .mf.sig support; GPG
|
|
||||||
subprocess vs pure-Go crypto
|
**9. Should we support detached signature files (`.mf.sig`)?**
|
||||||
- Implementation: deterministic manifests by default; consolidate
|
Embedded signatures are better for single-file distribution. Detached `.mf.sig` files follow the familiar `SHASUMS`/`SHASUMS.asc` pattern and are simpler for HTTP serving. Support both modes?
|
||||||
duplicate scanner/checker implementations; export the manifest
|
|
||||||
type; canonical Go module path for 1.0
|
> *answer:*
|
||||||
- Format and correctness:
|
|
||||||
- Resolve proto go_package vs go.mod module path inconsistency
|
**10. GPG vs pure-Go crypto for signatures?**
|
||||||
- Specify and validate path invariants (UTF-8, forward-slash,
|
Shelling out to `gpg` is fragile (may not be installed, version-dependent output). `github.com/ProtonMail/go-crypto` provides pure-Go OpenPGP, or we could go Ed25519/signify (simpler, no key management). Which direction?
|
||||||
relative, no .., no leading /)
|
|
||||||
- Remove or deprecate atime; reserve mode field; add version byte
|
> *answer:*
|
||||||
(all pending design answers)
|
|
||||||
- Write a standalone format specification document
|
### Implementation Design
|
||||||
- Library:
|
|
||||||
- Delete internal/scanner and internal/checker; consolidate on the
|
**11. Should manifests be deterministic by default?**
|
||||||
mfer/ package versions (pending design answer)
|
This means: sort file entries by path, omit `createdAt` timestamp (or make it opt-in), no `atime`. Should determinism be the default, with a `--include-timestamps` flag to opt in?
|
||||||
- Add decompression size limit via io.LimitReader in
|
|
||||||
deserializeInner()
|
> *answer:*
|
||||||
- Fix errors.Is dead code in checker; make AddFile verify
|
|
||||||
totalRead == size
|
**12. Should we consolidate or keep both scanner/checker implementations?**
|
||||||
- Export manifest type or define a public interface (pending)
|
There are two parallel implementations: `mfer/scanner.go` + `mfer/checker.go` (typed with `FileSize`, `RelFilePath`) and `internal/scanner/` + `internal/checker/` (raw `int64`, `string`). The `mfer/` versions are superior. Delete the `internal/` versions?
|
||||||
- Replace GPG subprocess with pure-Go crypto (pending); add timeouts
|
|
||||||
to remaining subprocess calls
|
> *answer:*
|
||||||
- CLI:
|
|
||||||
- Kebab-case primary flag names; fix fetch URL construction with
|
**13. Should the `manifest` type be exported?**
|
||||||
url.JoinPath; add http.Client timeout and retry with backoff to
|
Currently unexported with exported constructors (`New`, `NewFromPaths`, etc.). Consumers can't declare `var m *mfer.manifest`. Export the type, or define an interface?
|
||||||
fetch; rate-limit Checker progress output; add --deterministic
|
|
||||||
flag or default; wire top-level --version properly
|
> *answer:*
|
||||||
- Testing:
|
|
||||||
- Fuzz NewManifestFromReader; end-to-end tests for freshen and fetch
|
**14. What should the Go module path be for 1.0?**
|
||||||
- Documentation:
|
Currently mixed between `sneak.berlin/go/mfer` and `git.eeqj.de/sneak/mfer`. Which is canonical?
|
||||||
- Promote docs/FORMAT.md as primary spec reference; audit error
|
|
||||||
messages; document the signature scheme fully
|
> *answer:*
|
||||||
- Release:
|
|
||||||
- Finalize module path, bump version constant, SemVer --version
|
---
|
||||||
output, tag v1.0.0
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Foundation (format correctness)
|
||||||
|
|
||||||
|
- [ ] Delete `internal/scanner/` and `internal/checker/` — consolidate on `mfer/` package versions; update CLI code
|
||||||
|
- [ ] Add deterministic file ordering — sort entries by path (lexicographic, byte-order) in `Builder.Build()`; add test asserting byte-identical output from two runs
|
||||||
|
- [ ] Add decompression size limit — `io.LimitReader` in `deserializeInner()` with `m.pbOuter.Size` as bound
|
||||||
|
- [ ] Fix `errors.Is` dead code in checker — replace with `os.IsNotExist(err)` or `errors.Is(err, fs.ErrNotExist)`
|
||||||
|
- [ ] Fix `AddFile` to verify size — check `totalRead == size` after reading, return error on mismatch
|
||||||
|
- [ ] Specify path invariants — add proto comments (UTF-8, forward-slash, relative, no `..`, no leading `/`); validate in `Builder.AddFile` and `Builder.AddFileWithHash`
|
||||||
|
|
||||||
|
### Phase 2: CLI polish
|
||||||
|
|
||||||
|
- [ ] Fix flag naming — all CLI flags use kebab-case as primary (`--include-dotfiles`, `--follow-symlinks`)
|
||||||
|
- [ ] Fix URL construction in fetch — use `BaseURL.JoinPath()` or `url.JoinPath()` instead of string concatenation
|
||||||
|
- [ ] Add progress rate-limiting to Checker — throttle to once per second, matching Scanner
|
||||||
|
- [ ] Add `--deterministic` flag (or make it default) — omit `createdAt`, sort files
|
||||||
|
|
||||||
|
### Phase 3: Robustness
|
||||||
|
|
||||||
|
- [ ] Replace GPG subprocess with pure-Go crypto — `github.com/ProtonMail/go-crypto` or Ed25519/signify
|
||||||
|
- [ ] Add timeout to any remaining subprocess calls
|
||||||
|
- [ ] Add fuzzing tests for `NewManifestFromReader`
|
||||||
|
- [ ] Add retry logic to fetch — exponential backoff for transient HTTP errors
|
||||||
|
|
||||||
|
### Phase 4: Format finalization
|
||||||
|
|
||||||
|
- [ ] Remove or deprecate `atime` from proto (pending design question answer)
|
||||||
|
- [ ] Reserve `optional uint32 mode = 305` in `MFFilePath` for future file permissions
|
||||||
|
- [ ] Add version byte after magic — `ZNAVSRFG\x01` for format version 1
|
||||||
|
- [ ] Write format specification document — separate from README: magic, outer structure, compression, inner structure, path invariants, signature scheme, canonical serialization
|
||||||
|
|
||||||
|
### Phase 5: Release prep
|
||||||
|
|
||||||
|
- [ ] Finalize Go module path
|
||||||
|
- [ ] Audit all error messages for consistency and helpfulness
|
||||||
|
- [ ] Add `--version` output matching SemVer
|
||||||
|
- [ ] Tag v1.0.0
|
||||||
|
|||||||
155
script/bootstrap
155
script/bootstrap
@@ -1,155 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/bootstrap: install all dependencies needed to build and develop
|
|
||||||
# this repo. Idempotent: every install is guarded by a check so already
|
|
||||||
# installed tools are skipped. Base tooling comes from nix, apt, brew,
|
|
||||||
# or apk (detected in that order); assumes NOTHING is present (not git,
|
|
||||||
# make, node, yarn, go, or python). Node is used directly if installed;
|
|
||||||
# otherwise a pinned version is installed via nvm (installing nvm
|
|
||||||
# itself first, from a hash-verified release archive, never curl | sh).
|
|
||||||
#
|
|
||||||
# Uncomment the language sections in main() that apply to this repo.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
# Pinned versions, 2026-07-06. Never "latest" or "lts"; exact versions.
|
|
||||||
NODE_VERSION="22.17.0"
|
|
||||||
NVM_VERSION="0.40.3"
|
|
||||||
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
|
|
||||||
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
|
|
||||||
YARN_VERSION="1.22.22"
|
|
||||||
|
|
||||||
PKGMGR=""
|
|
||||||
SUDO=""
|
|
||||||
|
|
||||||
detect_pkgmgr() {
|
|
||||||
[ -n "$PKGMGR" ] && return 0
|
|
||||||
if command -v nix-env >/dev/null 2>&1; then
|
|
||||||
PKGMGR="nix"
|
|
||||||
elif command -v apt-get >/dev/null 2>&1; then
|
|
||||||
PKGMGR="apt"
|
|
||||||
elif command -v brew >/dev/null 2>&1; then
|
|
||||||
PKGMGR="brew"
|
|
||||||
elif command -v apk >/dev/null 2>&1; then
|
|
||||||
PKGMGR="apk"
|
|
||||||
else
|
|
||||||
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ "$PKGMGR" = "apt" ]; then
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
if [ "$(id -u)" != "0" ]; then
|
|
||||||
SUDO="sudo"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
|
|
||||||
pkg_install() {
|
|
||||||
detect_pkgmgr
|
|
||||||
case "$PKGMGR" in
|
|
||||||
nix) nix-env -iA "nixpkgs.$1" ;;
|
|
||||||
apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
|
|
||||||
brew) brew install "$3" ;;
|
|
||||||
apk) apk add --no-cache "$4" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
missing() {
|
|
||||||
! command -v "$1" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# verify_sha256 <file> <expected-hash>
|
|
||||||
verify_sha256() {
|
|
||||||
if command -v sha256sum >/dev/null 2>&1; then
|
|
||||||
actual="$(sha256sum "$1" | cut -d' ' -f1)"
|
|
||||||
else
|
|
||||||
actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
|
|
||||||
fi
|
|
||||||
if [ "$actual" != "$2" ]; then
|
|
||||||
echo "bootstrap: sha256 mismatch for $1" >&2
|
|
||||||
echo " expected: $2" >&2
|
|
||||||
echo " actual: $actual" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# nvm is a bash script; run a command in a bash with nvm loaded
|
|
||||||
nvm_sh() {
|
|
||||||
bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_nvm() {
|
|
||||||
[ -s "$HOME/.nvm/nvm.sh" ] && return 0
|
|
||||||
# nvm prerequisites; nvm itself requires bash, so install it too
|
|
||||||
if missing bash; then pkg_install bash bash bash bash; fi
|
|
||||||
if missing curl; then pkg_install curl curl curl curl; fi
|
|
||||||
if missing git; then pkg_install git git git git; fi
|
|
||||||
tmp="$(mktemp -d)"
|
|
||||||
curl -fsSL -o "$tmp/nvm.tar.gz" \
|
|
||||||
"https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz"
|
|
||||||
verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256"
|
|
||||||
mkdir -p "$HOME/.nvm"
|
|
||||||
tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
|
|
||||||
rm -rf "$tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_node() {
|
|
||||||
if ! missing node; then return 0; fi
|
|
||||||
ensure_nvm
|
|
||||||
nvm_sh "nvm install $NODE_VERSION"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_yarn() {
|
|
||||||
if ! missing yarn; then return 0; fi
|
|
||||||
if ! missing corepack; then
|
|
||||||
corepack enable
|
|
||||||
corepack prepare "yarn@$YARN_VERSION" --activate
|
|
||||||
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
|
|
||||||
nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \
|
|
||||||
corepack prepare yarn@$YARN_VERSION --activate"
|
|
||||||
else
|
|
||||||
npm install -g "yarn@$YARN_VERSION"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_js_deps() {
|
|
||||||
if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then
|
|
||||||
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
|
|
||||||
yarn install --frozen-lockfile"
|
|
||||||
else
|
|
||||||
yarn install --frozen-lockfile
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
# Base tooling (every repo)
|
|
||||||
if missing git; then pkg_install git git git git; fi
|
|
||||||
if missing make; then pkg_install gnumake make make make; fi
|
|
||||||
|
|
||||||
# ---- JS / docs repos ----
|
|
||||||
# ensure_node
|
|
||||||
# ensure_yarn
|
|
||||||
# install_js_deps
|
|
||||||
|
|
||||||
# ---- Go repos ----
|
|
||||||
if missing go; then pkg_install go golang go go; fi
|
|
||||||
# golangci-lint: packaged in nix, brew, and apk. On apt there is no
|
|
||||||
# package: download a specific release archive from GitHub and
|
|
||||||
# verify its hash (verify_sha256), never curl | sh.
|
|
||||||
if missing golangci-lint; then
|
|
||||||
pkg_install golangci-lint golangci-lint golangci-lint golangci-lint
|
|
||||||
fi
|
|
||||||
go mod download
|
|
||||||
|
|
||||||
# ---- Python repos ----
|
|
||||||
# if missing python3; then pkg_install python3 python3 python3 python3; fi
|
|
||||||
# python3 -m venv .venv
|
|
||||||
# ./.venv/bin/pip install -e '.[dev]'
|
|
||||||
|
|
||||||
echo "bootstrap complete"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
15
script/check
15
script/check
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/check: run all checks (test, lint, fmt-check). Our own
|
|
||||||
# extension to scripts-to-rule-them-all. Must not modify any files.
|
|
||||||
# Generic: usually needs no adaptation.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
"$SCRIPT_DIR/test"
|
|
||||||
"$SCRIPT_DIR/lint"
|
|
||||||
"$SCRIPT_DIR/fmt-check"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/cibuild: run the CI build. The Dockerfile runs script/check
|
|
||||||
# (via make check), so a successful build implies all checks pass.
|
|
||||||
# Generic: needs no adaptation. The Gitea workflow runs this on push.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
docker build .
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/docker: build the Docker image tagged with the project name.
|
|
||||||
# Identical in all repos; the tag comes from script/projectname.
|
|
||||||
# Generic: needs no adaptation.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
docker build -t "$("$SCRIPT_DIR/projectname")" .
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
27
script/fmt
27
script/fmt
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/fmt: format all files (writes).
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
|
|
||||||
# (mirrors the old Makefile prerequisite; the generated file is
|
|
||||||
# committed, so this is normally a no-op).
|
|
||||||
ensure_pb() {
|
|
||||||
if [ ! -f mfer/mf.pb.go ] ||
|
|
||||||
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
|
|
||||||
(cd mfer && go generate .)
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
ensure_pb
|
|
||||||
gofumpt -l -w mfer internal cmd
|
|
||||||
golangci-lint run --fix
|
|
||||||
# prettier is best-effort, as in the old Makefile (- prefix)
|
|
||||||
prettier -w *.json || true
|
|
||||||
prettier -w *.md || true
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/fmt-check: check formatting (read-only). Same scope as
|
|
||||||
# script/fmt, but fails instead of writing.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
|
|
||||||
# (mirrors the old Makefile prerequisite; the generated file is
|
|
||||||
# committed, so this is normally a no-op).
|
|
||||||
ensure_pb() {
|
|
||||||
if [ ! -f mfer/mf.pb.go ] ||
|
|
||||||
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
|
|
||||||
(cd mfer && go generate .)
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
ensure_pb
|
|
||||||
if [ -n "$(gofmt -l .)" ]; then
|
|
||||||
echo "gofmt: files need formatting:" >&2
|
|
||||||
gofmt -l . >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/install-precommit: install the git pre-commit hook that runs
|
|
||||||
# script/precommit. Our own extension to scripts-to-rule-them-all.
|
|
||||||
# Generic: needs no adaptation.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
hook=".git/hooks/pre-commit"
|
|
||||||
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
|
|
||||||
chmod +x .git/hooks/pre-commit
|
|
||||||
echo "pre-commit hook installed: runs script/precommit"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
17
script/lint
17
script/lint
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/lint: run the linter.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
golangci-lint run
|
|
||||||
if [ -n "$(gofmt -l .)" ]; then
|
|
||||||
echo "gofmt: files need formatting:" >&2
|
|
||||||
gofmt -l . >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/precommit: run by the git pre-commit hook; fails the commit if
|
|
||||||
# checks fail. Our own extension to scripts-to-rule-them-all. Go repo
|
|
||||||
# extras run first: go mod tidy and go fmt, failing the commit if they
|
|
||||||
# change go.mod or go.sum.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
go mod tidy
|
|
||||||
go fmt ./...
|
|
||||||
git diff --exit-code -- go.mod go.sum ||
|
|
||||||
{ echo "go mod tidy changed files; stage and retry" >&2; exit 1; }
|
|
||||||
"$SCRIPT_DIR/check"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/projectname: output the name of this project. Our own
|
|
||||||
# extension to scripts-to-rule-them-all. Other scripts that need the
|
|
||||||
# name (e.g. script/docker) call this, so they can stay identical
|
|
||||||
# across all repos.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
main() {
|
|
||||||
echo "mfer"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
14
script/setup
14
script/setup
@@ -1,14 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/setup: set up the repo for development after a fresh clone:
|
|
||||||
# installs dependencies (script/bootstrap) and the git pre-commit hook.
|
|
||||||
# Add any repo-specific initialization (db init, .env template) here.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
"$SCRIPT_DIR/bootstrap"
|
|
||||||
"$SCRIPT_DIR/install-precommit"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
23
script/test
23
script/test
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/test: run the test suite.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
|
|
||||||
# (mirrors the old Makefile prerequisite; the generated file is
|
|
||||||
# committed, so this is normally a no-op).
|
|
||||||
ensure_pb() {
|
|
||||||
if [ ! -f mfer/mf.pb.go ] ||
|
|
||||||
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
|
|
||||||
(cd mfer && go generate .)
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
ensure_pb
|
|
||||||
go test -v --timeout 10s ./...
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
Reference in New Issue
Block a user