Retire the GitHub workflow: amadan CI is the gate now
`.github/workflows/ci.yml` is deleted, and every sentence in the tree that pointed at it as the authority — or simply pointed at it — is rewritten to say what is now true. It was safe to delete because `.amadan/ci.d/` had become a superset of it, not a copy: the run on cd47f38 was green on the real arm64 worker with all four sections in the log, and the last remaining gap, gofmt, closed by widening (repo-wide `gofmt -l .` via `make test`, against the workflow's changed-files-only job). Separately, GitHub Actions on the carlosframework account had stopped starting jobs at all over billing, so the "required check" had been gating nothing for some time. The dangling pointers this removes: 10-gate and 30-minio-contract both opened by naming the workflow the definition of green; .amadan/README.md led with the same claim; internal/objstore/contract_test.go's doc comment and its user-facing skip message sent readers to a file that no longer exists; internal/host/statfs_unix.go cited the workflow header for the cross-compile bug that 20-cross@5 now carries. AGENTS.md gains the fact that falls out of this: CI is triggered by a push to the `amadan` remote, so a branch pushed only to `origin` is now gated by nothing at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7 files changed,
+136
−253
.amadan/README.md+40 −18.amadan/ci.d/10-gate+28 −4.amadan/ci.d/30-minio-contract+37 −25.github/workflows/ci.yml+0 −186AGENTS.md+24 −14internal/host/statfs_unix.go+1 −1internal/objstore/contract_test.go+6 −5
diff --git a/.amadan/README.md b/.amadan/README.md| index 2f411b85f..8d81510a1 100644 |
| --- a/.amadan/README.md |
| +++ b/.amadan/README.md |
| @@ -1,12 +1,32 @@ |
| # platform's CI on amadan |
| -`ci.d/` is a second home for this repo's gate, running on a self-hosted |
| -amadan runner. **`.github/workflows/ci.yml` remains the required check |
| -and remains the definition of green.** Where the two disagree, the |
| -workflow is the one that gates a merge, and the difference is a bug |
| -here. |
| +`ci.d/` is this repo's gate, running on a self-hosted amadan runner. |
| +**It is the only one.** `.github/workflows/ci.yml` was deleted on |
| +2026-08-24; nothing stands behind these three steps. A green run here |
| +is the whole of what "green" means for this repo, and a red one is not |
| +a discrepancy to reconcile against a more official run somewhere else. |
| -| workflow job | here | | |
| +The workflow went for two reasons that arrived together. The steps here |
| +had caught up with it and then gone past it — the last gap, `gofmt`, |
| +closed by *widening*, so this gate is now strictly the tighter of the |
| +two (see below). And GitHub Actions on the `carlosframework` account |
| +had stopped starting jobs at all over billing, so the "required check" |
| +had been gating nothing for some time before anyone removed it. The |
| +evidence for the first half is the run on `cd47f38`, green on the real |
| +arm64 worker with all four sections in the log: `=== gate ===` |
| +including `gofmt -l .: clean`, `=== cross ===` with all five targets, |
| +and `=== minio-contract ===` with `--- PASS: TestDeliveredContractMinio` |
| +and its five subtests. |
| + |
| +## Where each step came from |
| + |
| +This table is provenance, not a checklist against a live file. Every |
| +step here is shaped the way it is because of the job it descends from, |
| +and dropping that history makes the scripts read as arbitrary choices |
| +rather than as ported ones. The left column names a job in a file that |
| +no longer exists. |
| + |
| +| descended from | here | | |
| |---|---|---| |
| | `gate` | `10-gate` | build · vet · `go test ./... -count=1` | |
| | `cross` | `20-cross@5` | one target per shard, all five | |
| @@ -15,23 +35,25 @@ here. |
| ## `gofmt`: ported by widening it, not by reproducing it |
| -Nothing is missing now. Writing that down is still the point of this |
| -file: a `ci.d/` directory that quietly covers less than the workflow |
| -next to it is worth less than no `ci.d/` at all, because it invites |
| -people to read a green run as meaning more than it does. This file is |
| -the check on that, so it has to stay true. |
| +This section is the reason it was safe to delete the workflow, so it is |
| +worth keeping even now that the comparison is with a dead file. The |
| +whole risk in a `ci.d/` that covers *less* than what it replaces is |
| +that it invites people to read a green run as meaning more than it |
| +does — and that risk did not end when the other gate did. It moved: |
| +there is nothing left to compare against, so a gap here is now a gap |
| +full stop. This file is the check on that, and it has to stay true. |
| **`gofmt`** was the last gap, and it closed by widening rather than by |
| -imitation. The workflow checks only the files a PR changed, because it |
| -needs a **base SHA** — a GitHub concept. amadan CI hands a job its |
| +imitation. The workflow checked only the files a PR changed, because it |
| +needed a **base SHA** — a GitHub concept. amadan CI hands a job its |
| commit and its branch and has no notion of what it is a pull request |
| -against, so that job cannot be reproduced faithfully here. |
| +against, so that job could not be reproduced faithfully here. |
| -It does not need to be. The scoping existed to protect three |
| +It did not need to be. The scoping existed to protect three |
| pre-existing unformatted files, and those are gone: `gofmt -l .` over |
| the whole tree is clean today. So `10-gate` runs `make test`, whose |
| -recipe already contains a **repo-wide** gofmt check — which is stricter |
| -than the job it replaces, and one definition shared with what a |
| +recipe already contains a **repo-wide** gofmt check — strictly stronger |
| +than the job it replaced, and one definition shared with what a |
| developer runs locally rather than a second list that can drift. |
| The rule that survives from the old scoping is the one worth keeping: |
| @@ -41,7 +63,7 @@ is not a gate. |
| ## `minio-contract`, and why it turned out to be portable |
| -It was listed here as impossible: it starts MinIO in Docker, and amadan |
| +It was listed here as impossible: it started MinIO in Docker, and amadan |
| jobs get no container runtime. That conflated the job with its |
| packaging. MinIO and `mc` are single static Go binaries, and |
| `TestDeliveredContractMinio` only wants an endpoint and credentials over |
diff --git a/.amadan/ci.d/10-gate b/.amadan/ci.d/10-gate| index abb3ada43..434170661 100755 |
| --- a/.amadan/ci.d/10-gate |
| +++ b/.amadan/ci.d/10-gate |
| @@ -3,10 +3,34 @@ |
| # ./... -count=1 — all clean, every package, before any commit lands" — |
| # as amadan CI runs it. |
| # |
| -# A SECOND home for that gate, not a replacement. |
| -# .github/workflows/ci.yml stays the required check and stays the |
| -# definition of green; where the two disagree, the workflow is the one |
| -# that gates a merge and the difference is a bug in this file. |
| +# THIS IS THE GATE. Not a second home for one, not a copy kept beside a |
| +# more authoritative run somewhere else: the definition of green for this |
| +# repo is what these three steps say it is. |
| +# |
| +# It did not start that way, and the change is recent enough to be worth |
| +# writing down. Until 2026-08-24 the authority was |
| +# .github/workflows/ci.yml, and this file said so in as many words — |
| +# where the two disagreed, the workflow won and the difference was a bug |
| +# here. That workflow has been deleted. Two things made it safe to |
| +# delete, and they arrived together: |
| +# |
| +# 1. This directory caught up, and then went past it. `10-gate`, |
| +# `20-cross@5` and `30-minio-contract` cover every job the workflow |
| +# ran — build/vet/test, all five cross-compile targets, the live |
| +# MinIO contract — and the gofmt check here is STRICTER: `make |
| +# test` runs `gofmt -l .` over the whole tree, where the workflow |
| +# only ever checked the files a PR happened to touch. There is no |
| +# direction in which the deleted file was the tighter gate. |
| +# |
| +# 2. It had stopped running anyway. GitHub Actions on the |
| +# carlosframework account has been refusing to start jobs over |
| +# billing, so the "required check" was gating nothing well before |
| +# anyone removed it — which is the worse of the two failure modes, |
| +# because a gate that is merely absent is at least honest about it. |
| +# |
| +# So: a red step here is not a discrepancy to reconcile against a more |
| +# official run. It is the answer, and nothing behind it is going to catch |
| +# what it misses. Keep it true. |
| # |
| # One definition, not two. Everything past the build is `make test` — the |
| # same target a maintainer runs on a laptop — so CI and the local gate |
diff --git a/.amadan/ci.d/30-minio-contract b/.amadan/ci.d/30-minio-contract| index 48ed815ee..37cac5ee0 100755 |
| --- a/.amadan/ci.d/30-minio-contract |
| +++ b/.amadan/ci.d/30-minio-contract |
| @@ -3,10 +3,17 @@ |
| # TestDeliveredContractMinio, run against a real MinIO, with no container |
| # runtime anywhere in sight. |
| # |
| -# A SECOND home for the workflow's `minio-contract` job, not a |
| -# replacement. .github/workflows/ci.yml stays the required check and |
| -# stays the definition of green; where the two disagree, the workflow is |
| -# the one that gates a merge and the difference is a bug in this file. |
| +# THIS IS THE ONLY PLACE THIS TEST RUNS LIVE. It began as a port of the |
| +# `minio-contract` job in .github/workflows/ci.yml, with that workflow |
| +# holding the final say; the workflow has since been deleted (10-gate's |
| +# header records why), so there is no longer a second run of this |
| +# anywhere. If this step does not exercise the contract against a real |
| +# server, nothing does — `TestDeliveredContractMinio` skips itself |
| +# without CARLOS_MINIO_TEST, and a skipped test is a green test. |
| +# |
| +# References to "the workflow" below are provenance: they say where a |
| +# choice came from and where this deliberately diverged from it. They |
| +# describe a file that no longer exists. |
| # |
| # WHY THIS CAN EXIST HERE AT ALL |
| # |
| @@ -20,14 +27,15 @@ |
| # to hand you a MinIO. |
| # |
| # Running the server as a plain process inside the job's existing |
| -# bubblewrap sandbox is, if anything, tighter than the workflow: it binds |
| +# bubblewrap sandbox was, if anything, tighter than the container the |
| +# workflow used, and still is the tighter of the two shapes: it binds |
| # loopback only, its data directory is the job's private /tmp tmpfs, and |
| # it dies with the job's PID namespace. Nothing persists on the box. |
| set -eu |
| # Fail on a missing tool by name rather than by a confusing error forty |
| -# lines down. Both are present on the runner and in the workflow's image; |
| -# this is here so that a stripped-down box says which one it is missing. |
| +# lines down. Both are present on the runner; this is here so that a |
| +# stripped-down box says which one it is missing. |
| for tool in curl sha256sum; do |
| command -v "$tool" >/dev/null 2>&1 || { |
| echo "$tool is required by this step but was not found on PATH" >&2 |
| @@ -38,15 +46,15 @@ done |
| # --------------------------------------------------------------------- |
| # The pins. |
| # |
| -# The workflow pins its MinIO image and its mc download, and says why: |
| +# The workflow pinned its MinIO image and its mc download, and said why: |
| # a floating minio tag has already broken this exact job class once, when |
| -# mc's admin-policy syntax churned across versions. This is a port of |
| -# that pin, not an upgrade — RELEASE.2025-09-07T16-13-09Z is the same |
| -# server release the workflow's image digest resolves to, and the mc |
| -# release is the same one the workflow downloads. |
| +# mc's admin-policy syntax churned across versions. This started as a |
| +# port of that pin, not an upgrade — RELEASE.2025-09-07T16-13-09Z is the |
| +# same server release that image digest resolved to, and the mc release |
| +# is the same one it downloaded. |
| # |
| -# But a version string is a weaker pin than the workflow's, because the |
| -# workflow gets a container digest for free and a plain URL does not: |
| +# But a version string is a weaker pin than a container digest, which the |
| +# workflow got for free and a plain URL does not: |
| # dl.min.io could serve different bytes at the same archive path and |
| # nothing would notice. So each binary carries its sha256, verified after |
| # download and again on every cache hit, and a mismatch refuses to run |
| @@ -60,10 +68,11 @@ done |
| MINIO_RELEASE=RELEASE.2025-09-07T16-13-09Z |
| MC_RELEASE=RELEASE.2025-08-13T08-35-41Z |
| -# Arch-aware on purpose. The amadan runner is arm64 and the workflow's |
| -# ubuntu-latest is amd64, so a hardwired path works in exactly one of the |
| -# two places. The workflow in fact hardwires linux-amd64 for its mc |
| +# Arch-aware on purpose. The amadan runner is arm64; the workflow's |
| +# ubuntu-latest was amd64, and it hardwired linux-amd64 for its mc |
| # download — harmless there, wrong here, and a bug not to carry across. |
| +# The runner fleet is also the kind of thing that changes under you, so |
| +# this reads uname rather than trusting a note about what the box is. |
| case "$(uname -m)" in |
| x86_64 | amd64) |
| arch=amd64 |
| @@ -178,9 +187,10 @@ root_pass=$(head -c 18 /dev/urandom | od -An -tx1 | tr -d ' \n') |
| # --------------------------------------------------------------------- |
| # Start the server. |
| # |
| -# The workflow uses a fixed port 9000 because its MinIO is in a container |
| -# with its own network. amadan jobs deliberately do NOT get --unshare-net |
| -# (they need the Go module proxy), so a job shares the host's network |
| +# The workflow used a fixed port 9000 because its MinIO was in a |
| +# container with its own network. amadan jobs deliberately do NOT get |
| +# --unshare-net (they need the Go module proxy), so a job shares the |
| +# host's network |
| # namespace with the worker and with anything else on it. 9000 is a |
| # popular port; assuming it is free is assuming something this step does |
| # not know. |
| @@ -239,11 +249,13 @@ echo " minio healthy on 127.0.0.1:$port" |
| # --------------------------------------------------------------------- |
| # The fixture: two buckets and a scoped user granted exactly one of them. |
| # |
| -# Kept identical to the workflow's `mint scoped user` step — same bucket |
| -# names, same policy, same user — because the whole value of the test is |
| -# that cross-bucket denial is enforced by real IAM rather than by the |
| -# client politely not asking. Drift between this policy and the |
| -# workflow's is drift in what the two gates mean. |
| +# Carried over verbatim from the workflow's `mint scoped user` step — |
| +# same bucket names, same policy, same user — because the whole value of |
| +# the test is that cross-bucket denial is enforced by real IAM rather |
| +# than by the client politely not asking. Nothing checks this policy |
| +# against a second copy any more, so loosening it here is invisible: |
| +# grant `denied-bucket` by accident and the test still passes while |
| +# proving nothing. Change it only on purpose. |
| "$mc" --quiet alias set local "http://127.0.0.1:$port" "$root_user" "$root_pass" |
| "$mc" --quiet mb local/contract-bucket local/denied-bucket |
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml| deleted file mode 100644 |
| index b2d1ff026..000000000 |
| --- a/.github/workflows/ci.yml |
| +++ /dev/null |
| @@ -1,186 +0,0 @@ |
| -# The build gate from CLAUDE.md, enforced by a machine rather than by |
| -# whoever happens to be at a keyboard. The gate itself is not new; what is |
| -# new is that it now runs somewhere other than a maintainer's Linux laptop. |
| -# |
| -# The cross-compile job exists because of a specific bug: readDiskFreeBytes |
| -# in internal/host/probes.go multiplied an int64 by syscall.Statfs_t.Bsize, |
| -# which is int64 on Linux and uint32 on darwin. That is a compile error on |
| -# macOS for every GOARCH, and internal/host is imported by the carlos CLI — |
| -# the laptop tool the README's developer story is built around. It sat in |
| -# main undetected because every person who built it was on Linux. A |
| -# `GOOS=darwin go build ./...` costs seconds on a Linux runner and would |
| -# have caught it at the commit that introduced it. |
| -name: CI |
| - |
| -on: |
| - push: |
| - branches: [main] |
| - pull_request: |
| - |
| -permissions: |
| - contents: read |
| - |
| -# A second push to a PR makes the first run's answer irrelevant. |
| -concurrency: |
| - group: ci-${{ github.workflow }}-${{ github.ref }} |
| - cancel-in-progress: true |
| - |
| -jobs: |
| - gate: |
| - name: build · vet · test (linux) |
| - runs-on: ubuntu-latest |
| - steps: |
| - - uses: actions/checkout@v4 |
| - - uses: actions/setup-go@v5 |
| - with: |
| - go-version-file: go.mod |
| - cache: true |
| - - run: go build ./... |
| - - run: go vet ./... |
| - # -count=1 to match CLAUDE.md: never satisfy the gate from a cached |
| - # pass. The explicit timeout is above the 10m default because the |
| - # root package's e2e tests compile the carlos binary many times over. |
| - - run: go test ./... -count=1 -timeout 30m |
| - |
| - cross: |
| - name: compile ${{ matrix.goos }}/${{ matrix.goarch }} |
| - runs-on: ubuntu-latest |
| - strategy: |
| - # One platform breaking must not hide the state of the others. |
| - fail-fast: false |
| - matrix: |
| - include: |
| - - {goos: darwin, goarch: arm64} |
| - - {goos: darwin, goarch: amd64} |
| - - {goos: linux, goarch: arm64} |
| - # Windows is a CLIENT target: `carlos deploy` from a Windows |
| - # workstation against a Linux fleet. The box-side packages still |
| - # have to COMPILE there because the CLI is the same binary as the |
| - # agent, so unix-only syscalls live behind per-OS files |
| - # (internal/edge/purge_listen_*.go, internal/host/{statfs,flock}_*.go). |
| - # This job is what stops the next syscall.Foo from silently |
| - # un-shipping the Windows build. |
| - - {goos: windows, goarch: amd64} |
| - - {goos: windows, goarch: arm64} |
| - steps: |
| - - uses: actions/checkout@v4 |
| - - uses: actions/setup-go@v5 |
| - with: |
| - go-version-file: go.mod |
| - cache: true |
| - # Compile only — these binaries are never run, so no runner for the |
| - # target OS is needed. CGO_ENABLED=0 is already the default when |
| - # cross-compiling; it is explicit here so that adding a cgo dependency |
| - # fails loudly in this job rather than silently changing what is |
| - # covered. The tree is pure Go today (modernc.org/sqlite, no import "C"). |
| - - run: go build ./... |
| - env: |
| - GOOS: ${{ matrix.goos }} |
| - GOARCH: ${{ matrix.goarch }} |
| - CGO_ENABLED: "0" |
| - |
| - minio-contract: |
| - # Exercises internal/objstore's TestDeliveredContractMinio: the S3 |
| - # contract a member app actually gets from delivered grant env (static |
| - # creds, path-style, us-east-1, checksum WhenRequired), against a real |
| - # S3-compatible server plus a scoped user IAM never granted the second |
| - # bucket. This is the only place in the gate that runs that test live — |
| - # see internal/objstore/contract_test.go's doc comment. |
| - name: minio contract |
| - runs-on: ubuntu-latest |
| - steps: |
| - - uses: actions/checkout@v4 |
| - - uses: actions/setup-go@v5 |
| - with: |
| - go-version-file: go.mod |
| - cache: true |
| - - name: start minio |
| - # Pinned rather than :latest — everything else in this file pins |
| - # (go-version-file, action majors), and a floating minio tag has |
| - # already broken this exact job class once (mc's own admin-policy |
| - # syntax churned across versions). Verified live against the quay.io |
| - # registry API on 2026-08-10: RELEASE.2025-09-07T16-13-09Z is the |
| - # image quay.io/minio/minio:latest currently resolves to (identical |
| - # manifest digest sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e). |
| - # Bump deliberately, not silently — re-verify against |
| - # https://quay.io/api/v1/repository/minio/minio/tag/ before moving it. |
| - run: | |
| - docker run -d --name minio -p 9000:9000 \ |
| - -e MINIO_ROOT_USER=root -e MINIO_ROOT_PASSWORD=rootsecret \ |
| - quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z server /data |
| - for i in $(seq 1 30); do |
| - if curl -sf http://127.0.0.1:9000/minio/health/live; then |
| - break |
| - fi |
| - if [ "$i" -eq 30 ]; then |
| - echo "minio never became healthy after 30s" >&2 |
| - docker logs minio || true |
| - exit 1 |
| - fi |
| - sleep 1 |
| - done |
| - - name: mint scoped user |
| - run: | |
| - # Pinned to the mc release published alongside the minio server |
| - # image above (verified live against the minio/mc GitHub releases |
| - # API and dl.min.io's archive endpoint on 2026-08-10) rather than |
| - # the floating linux-amd64/mc path, for the same reproducibility |
| - # reason. Bump together with the minio image pin above. |
| - curl -sfL https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2025-08-13T08-35-41Z -o mc && chmod +x mc |
| - ./mc --version |
| - ./mc alias set local http://127.0.0.1:9000 root rootsecret |
| - ./mc mb local/contract-bucket local/denied-bucket |
| - cat > /tmp/policy.json <<'EOF' |
| - {"Version": "2012-10-17", "Statement": [ |
| - {"Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::contract-bucket"]}, |
| - {"Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": ["arn:aws:s3:::contract-bucket/*"]} |
| - ]} |
| - EOF |
| - ./mc admin policy create local contract-only /tmp/policy.json |
| - ./mc admin user add local scopeduser scopedsecret1 |
| - ./mc admin policy attach local contract-only --user scopeduser |
| - - name: contract test |
| - env: |
| - CARLOS_MINIO_TEST: "1" |
| - MINIO_ENDPOINT: http://127.0.0.1:9000 |
| - MINIO_SCOPED_KEY: scopeduser |
| - MINIO_SCOPED_SECRET: scopedsecret1 |
| - MINIO_BUCKET: contract-bucket |
| - MINIO_DENIED_BUCKET: denied-bucket |
| - run: go test ./internal/objstore/ -run TestDeliveredContractMinio -count=1 -v |
| - |
| - gofmt: |
| - name: gofmt (changed files) |
| - runs-on: ubuntu-latest |
| - # Only meaningful against a base to diff from. |
| - if: github.event_name == 'pull_request' |
| - steps: |
| - - uses: actions/checkout@v4 |
| - with: |
| - fetch-depth: 0 |
| - - uses: actions/setup-go@v5 |
| - with: |
| - go-version-file: go.mod |
| - # CLAUDE.md's rule is "gofmt -l clean for files you touch", not for the |
| - # tree: three pre-existing unformatted files are deliberately left |
| - # alone, so a repo-wide check would fail on arrival and teach everyone |
| - # to ignore this job. Scoped to the files the PR actually changes. |
| - - name: gofmt changed files |
| - env: |
| - BASE_SHA: ${{ github.event.pull_request.base.sha }} |
| - run: | |
| - set -euo pipefail |
| - files=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- '*.go') |
| - if [ -z "$files" ]; then |
| - echo "no Go files changed" |
| - exit 0 |
| - fi |
| - echo "checking:" |
| - echo "$files" | sed 's/^/ /' |
| - unformatted=$(gofmt -l $files) |
| - if [ -n "$unformatted" ]; then |
| - echo |
| - echo "needs gofmt -w:" |
| - echo "$unformatted" | sed 's/^/ /' |
| - exit 1 |
| - fi |
diff --git a/AGENTS.md b/AGENTS.md| index efe0aa0ee..78b2f8c32 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -74,26 +74,36 @@ Use the judgement the rest of this file asks for: a PR that is still |
| collecting review, that is gated on something (`#62`), or whose diff you |
| would want a second pair of eyes on, waits for a person. |
| -The reason is timing, not ceremony. `.github/workflows/ci.yml` triggers on |
| -`pull_request` *and* on `push` to `main`, so a locally merged change does |
| -still meet the gate — but only **after** it is on `main`, where a red run |
| -is an incident to clean up rather than a merge that simply doesn't happen. |
| -The PR is the one place the gate runs against the merge result while the |
| -change can still be turned away, and the one place the branch's diff is |
| -read as a single piece rather than commit by commit. |
| +The reason is timing, not ceremony. A locally merged change does still |
| +meet the gate — but only **after** it is on `main`, where a red run is an |
| +incident to clean up rather than a merge that simply doesn't happen. The |
| +PR is the one place the branch's diff is read as a single piece rather |
| +than commit by commit, while the change can still be turned away. |
| ## Build & test |
| - `go build ./...`, `go vet ./...`, `go test ./... -count=1` — all clean, |
| every package (21 as of 2026-08-08; the count grows), before any commit |
| lands. |
| -- `gofmt -l` clean for files you touch (three pre-existing unformatted files |
| - elsewhere are left alone). |
| -- `.github/workflows/ci.yml` runs that same gate on every PR, plus a |
| - `GOOS=darwin` compile. **The gate is not Linux-only**: `internal/host` is |
| - linked into the CLI, which people run on Macs, so a Linux-only type |
| - assumption breaks `go build .` for them. Cross-compiling is enough to catch |
| - it — no Mac required. |
| +- `gofmt -l .` clean over the whole tree — `make test` checks it, and the |
| + three pre-existing unformatted files that used to be exempted are gone. |
| +- **`.amadan/ci.d/` is the only CI this repo has.** The GitHub workflow |
| + that used to run this gate was deleted on 2026-08-24, once the amadan |
| + steps covered everything it did and more; `.amadan/README.md` records |
| + what each step descends from, and `.amadan/ci.d/10-gate`'s header |
| + records why the switch was safe. `10-gate` is `go build ./...` then |
| + `make test`, so CI and what you run locally are one definition rather |
| + than two that can drift. |
| +- **CI is triggered by a push to the `amadan` remote, not by GitHub.** |
| + `origin` is still where PRs live and where `main` is source of truth, |
| + but nothing on GitHub runs a build any more. A branch pushed only to |
| + `origin` has been gated by exactly nothing — push it to `amadan` too, |
| + and read the run, before calling it green. |
| +- `20-cross@5` adds `GOOS=darwin`/`windows`/`linux-arm64` compiles, one |
| + per shard. **The gate is not Linux-only**: `internal/host` is linked |
| + into the CLI, which people run on Macs, so a Linux-only type assumption |
| + breaks `go build .` for them. Cross-compiling is enough to catch it — |
| + no Mac required. |
| - Sandbox note: use `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod |
| GOPROXY=off`; go commands may need the sandbox disabled ("read-only file |
| system" from the toolchain is the tell). |
diff --git a/internal/host/statfs_unix.go b/internal/host/statfs_unix.go| index 4c9916994..a57f9ceba 100644 |
| --- a/internal/host/statfs_unix.go |
| +++ b/internal/host/statfs_unix.go |
| @@ -9,7 +9,7 @@ import "syscall" |
| // |
| // It returns BYTES, not the raw statfs fields, and that is the point: Bsize |
| // is int64 on Linux and uint32 on darwin, a difference that already broke |
| -// the CI build once (see .github/workflows/ci.yml's header) because the |
| +// the CI build once (see .amadan/ci.d/20-cross@5's header) because the |
| // multiplication happened at every call site instead of once here. Callers |
| // now get two plain uint64s and no per-OS arithmetic. |
| func diskBytes(dir string) (total, avail uint64, err error) { |
diff --git a/internal/objstore/contract_test.go b/internal/objstore/contract_test.go| index 6a9489a57..ae328d283 100644 |
| --- a/internal/objstore/contract_test.go |
| +++ b/internal/objstore/contract_test.go |
| @@ -28,13 +28,14 @@ import ( |
| // been exercised anywhere in this tree is the actual wire contract a member |
| // app's client.Options() produces from delivered env, against a real |
| // S3-compatible implementation. Skipped by default because it needs a |
| -// running Minio; CI's minio-contract job (.github/workflows/ci.yml) sets |
| -// CARLOS_MINIO_TEST=1 and stands one up. There is no docker in this |
| -// sandbox, so the live path here has only ever been verified in CI — see |
| -// the commit body. |
| +// running Minio; CI's 30-minio-contract step (.amadan/ci.d/30-minio-contract) |
| +// sets CARLOS_MINIO_TEST=1 and stands one up, as a plain process on |
| +// loopback rather than a container. That step is now the only place this |
| +// test runs live anywhere, so treat a change to it as a change to what |
| +// this test is worth. |
| func TestDeliveredContractMinio(t *testing.T) { |
| if os.Getenv("CARLOS_MINIO_TEST") != "1" { |
| - t.Skip("set CARLOS_MINIO_TEST=1 (with a running Minio and the MINIO_* env vars) to run the delivered-contract test; see .github/workflows/ci.yml's minio-contract job") |
| + t.Skip("set CARLOS_MINIO_TEST=1 (with a running Minio and the MINIO_* env vars) to run the delivered-contract test; see .amadan/ci.d/30-minio-contract, which stands one up") |
| } |
| endpoint := requireEnv(t, "MINIO_ENDPOINT") |