feat(release): release-only runner label + post-push rollback #115

Merged
claude-desktop merged 4 commits from boss/26 into main 2026-04-20 08:53:27 +00:00
Collaborator

Closes #26.

Summary

  • publish-image now gates on runs-on: [docker, release]. PR CI
    (qa.yml) stays on plain docker, so it can never land on a
    socket-mounted runner by accident. Only the dedicated release runner
    advertises the release label.
  • New Rollback on smoke-test failure step on publish-image. Fires
    via if: failure() && steps.push.outcome == 'success', deletes the
    :${VERSION} and :latest container package versions from the
    Forgejo registry, and PATCHes the release to prerelease=true with a
    "DO NOT USE" banner. Both API calls are idempotent (404 = already gone
    = ok).
  • docs/runner-setup.md covers runner registration, the
    container.options: -v /var/run/docker.sock:/var/run/docker.sock
    config.yml snippet, the security rationale (socket ≡ root-on-host →
    repo-scoped token + release-label gating), and a release-candidate
    dry-run procedure (v0.0.0-rc1) for smoking the pipeline end-to-end
    before cutting a real release.
  • README.md — brief "Release pipeline" section that points at
    docs/runner-setup.md.

Acceptance-criteria map (issue #26)

  • Runner
    • Dedicated runner with docker socket documented (Option A —
      container.options: -v /var/run/docker.sock:...). See
      docs/runner-setup.md.
    • Labelled so only release.yml jobs pick it up
      (runs-on: [docker, release] on publish-image; every other
      job stays on plain docker).
    • Runner deployment documented in a dedicated
      docs/runner-setup.md + README pointer.
  • release.yml runtime verification — these were already in place
    from the earlier pass, preserved here:
    • bun --version / git --version / forgejo-mcp --help /
      claude --version against the pushed image.
    • Credential-file audit against the pushed image.
    • Rollback on failure — delete image tags from registry, flip
      release to prerelease with DO NOT USE banner (new).
  • PR CI
    • qa.yml's static dockerfile job untouched.
  • Tests
    • Dry-run procedure for v0.0.0-rc1 documented in
      docs/runner-setup.md so the first real release doesn't land
      blind.

Test plan

  • Register the release runner per docs/runner-setup.md (labels
    docker,release, socket bind-mount in config.yml).
  • git tag v0.0.0-rc1 && git push origin v0.0.0-rc1; watch
    release.yml exercise publish-image end-to-end on the release
    runner.
  • Verify the :v0.0.0-rc1 container package appears in the Forgejo
    registry and the release notes get a "Container image" section
    with a sha256 digest.
  • Force a smoke-test failure on a second rc tag (e.g. by
    temporarily renaming claude in the Dockerfile) and confirm the
    rollback step deletes :v0.0.0-rc2 + :latest and flips the
    release to prerelease with the DO NOT USE banner.
  • Clean up the rc tags, releases, and package versions in the UI.
Closes #26. ## Summary - `publish-image` now gates on `runs-on: [docker, release]`. PR CI (`qa.yml`) stays on plain `docker`, so it can never land on a socket-mounted runner by accident. Only the dedicated release runner advertises the `release` label. - New `Rollback on smoke-test failure` step on `publish-image`. Fires via `if: failure() && steps.push.outcome == 'success'`, deletes the `:${VERSION}` and `:latest` container package versions from the Forgejo registry, and PATCHes the release to `prerelease=true` with a "DO NOT USE" banner. Both API calls are idempotent (404 = already gone = ok). - `docs/runner-setup.md` covers runner registration, the `container.options: -v /var/run/docker.sock:/var/run/docker.sock` config.yml snippet, the security rationale (socket ≡ root-on-host → repo-scoped token + release-label gating), and a release-candidate dry-run procedure (`v0.0.0-rc1`) for smoking the pipeline end-to-end before cutting a real release. - `README.md` — brief "Release pipeline" section that points at `docs/runner-setup.md`. ## Acceptance-criteria map (issue #26) - **Runner** - [x] Dedicated runner with docker socket documented (Option A — `container.options: -v /var/run/docker.sock:...`). See `docs/runner-setup.md`. - [x] Labelled so only `release.yml` jobs pick it up (`runs-on: [docker, release]` on `publish-image`; every other job stays on plain `docker`). - [x] Runner deployment documented in a dedicated `docs/runner-setup.md` + README pointer. - **release.yml runtime verification** — these were already in place from the earlier pass, preserved here: - [x] `bun --version` / `git --version` / `forgejo-mcp --help` / `claude --version` against the pushed image. - [x] Credential-file audit against the pushed image. - [x] Rollback on failure — delete image tags from registry, flip release to prerelease with DO NOT USE banner (**new**). - **PR CI** - [x] `qa.yml`'s static `dockerfile` job untouched. - **Tests** - [x] Dry-run procedure for `v0.0.0-rc1` documented in `docs/runner-setup.md` so the first real release doesn't land blind. ## Test plan - [ ] Register the release runner per `docs/runner-setup.md` (labels `docker,release`, socket bind-mount in `config.yml`). - [ ] `git tag v0.0.0-rc1 && git push origin v0.0.0-rc1`; watch `release.yml` exercise `publish-image` end-to-end on the release runner. - [ ] Verify the `:v0.0.0-rc1` container package appears in the Forgejo registry and the release notes get a "Container image" section with a sha256 digest. - [ ] Force a smoke-test failure on a second rc tag (e.g. by temporarily renaming `claude` in the Dockerfile) and confirm the rollback step deletes `:v0.0.0-rc2` + `:latest` and flips the release to prerelease with the DO NOT USE banner. - [ ] Clean up the rc tags, releases, and package versions in the UI.
feat(release): release-only runner label + post-push rollback
All checks were successful
qa / qa (pull_request) Successful in 2m41s
qa / dockerfile (pull_request) Successful in 9s
0a6ca06547
publish-image now gates on `runs-on: [docker, release]` so PR CI (which
stays on plain `docker`) can never land on a socket-mounted runner.
Only the dedicated release runner — registered with both labels and
`container.options: -v /var/run/docker.sock:/var/run/docker.sock` in
its config.yml — advertises `release`.

Add a `Rollback on smoke-test failure` step that fires via
`if: failure() && steps.push.outcome == 'success'`: deletes the
`:${VERSION}` and `:latest` container package versions from the Forgejo
registry and PATCHes the release to prerelease=true with a DO NOT USE
banner. Both calls are best-effort — a 404 on package delete is
treated as success so re-runs are idempotent.

docs/runner-setup.md covers registration, config.yml, security posture
(socket = root-on-host, therefore repo-scoped + release-label-gated),
and the rc-tag dry-run procedure for exercising the pipeline before
cutting a real release.

Closes #26.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer requested changes 2026-04-20 00:26:50 +00:00
Dismissed
reviewer left a comment

Review of PR #115

See detailed findings below.

CI: Green (run #1671)

Rollback fires on release-notes annotation failure

File: .forgejo/workflows/release.ymlRollback on smoke-test failure step

The condition if: failure() && steps.push.outcome == "success" fires whenever any prior step fails, including Update release notes with image digest. That step uses curl -fsSL which exits non-zero on any 4xx/5xx API response (transient outage, rate-limit).

Result: a temporary Forgejo API error in the annotation step triggers the rollback, deleting a perfectly valid image and marking the release DO NOT USE — even though smoke tests and the credential audit both passed.

Fix: add continue-on-error: true to Update release notes with image digest:

      - name: Update release notes with image digest
        continue-on-error: true   # annotation failure must not trigger rollback
        env:
          VERSION: ...

⚠️ Minor — forgejo-mcp --help 2>&1 || true masks missing binary

File: .forgejo/workflows/release.ymlSmoke test image step

|| true swallows exit 127 (command not found). In practice low-risk (missing binary would be caught at build time), but the forgejo-mcp check is weaker than the other three smoke-test lines. Consider docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && forgejo-mcp --help; :".


Everything else

  • runs-on: [docker, release] on publish-image only; all other jobs stay on plain docker. ✓
  • Rollback steps.push.outcome == "success" guard is correct. ✓
  • set -u + case-based 404 tolerance in rollback script is appropriate. ✓
  • RELEASE_ID || echo "" fallback + empty-check with exit 0 is correct. ✓
  • :latest deletion trade-off acknowledged and documented in docs/runner-setup.md. ✓
  • docs/runner-setup.md is comprehensive: registration, config.yml snippet, security rationale, dry-run procedure, troubleshooting. ✓
  • README Release pipeline section accurate and links the doc. ✓
  • All acceptance criteria from issue #26 are met.
## Review of PR #115 See detailed findings below. **CI:** ✅ Green (run #1671) ### ❌ Rollback fires on release-notes annotation failure **File:** `.forgejo/workflows/release.yml` — `Rollback on smoke-test failure` step The condition `if: failure() && steps.push.outcome == "success"` fires whenever **any** prior step fails, including `Update release notes with image digest`. That step uses `curl -fsSL` which exits non-zero on any 4xx/5xx API response (transient outage, rate-limit). Result: a temporary Forgejo API error in the annotation step triggers the rollback, deleting a perfectly valid image and marking the release DO NOT USE — even though smoke tests and the credential audit both passed. **Fix:** add `continue-on-error: true` to `Update release notes with image digest`: ```yaml - name: Update release notes with image digest continue-on-error: true # annotation failure must not trigger rollback env: VERSION: ... ``` --- ### ⚠️ Minor — `forgejo-mcp --help 2>&1 || true` masks missing binary **File:** `.forgejo/workflows/release.yml` — `Smoke test image` step `|| true` swallows exit 127 (command not found). In practice low-risk (missing binary would be caught at build time), but the forgejo-mcp check is weaker than the other three smoke-test lines. Consider `docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && forgejo-mcp --help; :"`. --- ### ✅ Everything else - `runs-on: [docker, release]` on `publish-image` only; all other jobs stay on plain `docker`. ✓ - Rollback `steps.push.outcome == "success"` guard is correct. ✓ - `set -u` + case-based 404 tolerance in rollback script is appropriate. ✓ - RELEASE_ID `|| echo ""` fallback + empty-check with `exit 0` is correct. ✓ - `:latest` deletion trade-off acknowledged and documented in `docs/runner-setup.md`. ✓ - `docs/runner-setup.md` is comprehensive: registration, `config.yml` snippet, security rationale, dry-run procedure, troubleshooting. ✓ - README Release pipeline section accurate and links the doc. ✓ - All acceptance criteria from issue #26 are met.
fix(release): isolate annotation failures from rollback; sharpen smoke test
All checks were successful
qa / qa (pull_request) Successful in 2m29s
qa / dockerfile (pull_request) Successful in 7s
fd1d624bc4
Two reviewer findings on PR #115:

- `Update release notes with image digest` previously fell under the
  `failure() && steps.push.outcome == 'success'` rollback guard, so a
  transient Forgejo API blip on the annotation step would delete a good
  image and flag a passing release as DO NOT USE. Add
  `continue-on-error: true` — annotation is cosmetic, not rollback-worthy.

- The forgejo-mcp smoke check used `--help 2>&1 || true`, swallowing
  exit 127 (missing binary) the same as a successful help. Replace with
  `sh -c "command -v forgejo-mcp && forgejo-mcp --help; :"` so the
  binary's path lands in the log when present, leaving a visible gap if
  it ever goes missing without breaking the step on a non-zero --help.
Author
Collaborator

Both findings addressed in fd1d624.

Rollback fires on release-notes annotation failure

Agreed — the annotation is cosmetic, the image is already pushed, and the rollback should not be on the hook for a Forgejo API blip on PATCH /releases/{id}. Added continue-on-error: true to the Update release notes with image digest step with an inline comment explaining the rationale.

forgejo-mcp --help 2>&1 || true masks missing binary

Agreed — applied your suggestion verbatim:

docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && forgejo-mcp --help; :"

command -v prints the binary's PATH location to the log on success, so a missing binary leaves a visible gap rather than being silently swallowed by || true. The trailing ; : keeps the step green if --help itself returns non-zero (some help implementations do).

Both findings addressed in fd1d624. ### Rollback fires on release-notes annotation failure Agreed — the annotation is cosmetic, the image is already pushed, and the rollback should not be on the hook for a Forgejo API blip on `PATCH /releases/{id}`. Added `continue-on-error: true` to the `Update release notes with image digest` step with an inline comment explaining the rationale. ### `forgejo-mcp --help 2>&1 || true` masks missing binary Agreed — applied your suggestion verbatim: ```yaml docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && forgejo-mcp --help; :" ``` `command -v` prints the binary's PATH location to the log on success, so a missing binary leaves a visible gap rather than being silently swallowed by `|| true`. The trailing `; :` keeps the step green if `--help` itself returns non-zero (some help implementations do).
reviewer requested changes 2026-04-20 00:39:39 +00:00
Dismissed
reviewer left a comment

Review — PR #115: release-only runner label + post-push rollback

CI

Run #1673 (fd1d624) — success (2m38s).

Acceptance-criteria coverage

All items from issue #26 are met in principle — runner label isolation, runtime smoke tests, credential audit, rollback, documentation. Good work overall. One correctness issue blocks approval.


forgejo-mcp binary-absent case is silently swallowed

File: .forgejo/workflows/release.ymlSmoke test image step

docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && forgejo-mcp --help; :"

In POSIX sh the trailing ; : is a separate command that always exits 0. The exit status of the whole shell string is therefore always 0, regardless of whether command -v forgejo-mcp fails. Concretely:

  • forgejo-mcp presentcommand -v prints path, --help runs (exit code ignored by ; :), step green. ✓
  • forgejo-mcp absentcommand -v exits 1, && short-circuits, ; : runs, step exits 0 and the release proceeds with a broken image. ✗

The in-code comment says "a missing binary still leaves a visible 'no path printed' trail in the log instead of being silently swallowed by || true" — that is true for observability, but the job does not fail. This contradicts issue #26 AC: "Fail the release if any command exits non-zero."

Fix — separate the "binary exists" check (must be fatal) from the "--help exit code" tolerance:

docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp"
docker run --rm "$IMAGE" sh -c "forgejo-mcp --help; :"

Or keep it as a single docker run but scope the ; : to the --help invocation only:

docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && (forgejo-mcp --help; :)"

Either form lets command -v failure propagate and fail the step, while still tolerating a non-zero exit from --help.


Everything else

  • runs-on: [docker, release] isolation is correct; qa.yml stays on plain docker and is not changed in this PR.
  • continue-on-error: true on the annotation step correctly prevents a transient Forgejo API blip from triggering the rollback (the rollback gates on failure(), which continue-on-error suppresses for that step). Logic is sound.
  • Rollback order and best-effort 404 handling are correct.
  • Deleting :latest on rollback is documented as a conscious trade-off in docs/runner-setup.md — acceptable.
  • docs/runner-setup.md is thorough: labels table, config.yml snippet, security rationale, dry-run procedure, troubleshooting.
  • README "Release pipeline" section correctly points at the doc.
  • FORGEJO_TOKEN and REGISTRY_TOKEN scopes documented as repo-scoped — good security posture.
## Review — PR #115: release-only runner label + post-push rollback ### CI Run #1673 (`fd1d624`) — ✅ **success** (2m38s). ### Acceptance-criteria coverage All items from issue #26 are met in principle — runner label isolation, runtime smoke tests, credential audit, rollback, documentation. Good work overall. One correctness issue blocks approval. --- ### ❌ `forgejo-mcp` binary-absent case is silently swallowed **File**: `.forgejo/workflows/release.yml` — `Smoke test image` step ```yaml docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && forgejo-mcp --help; :" ``` In POSIX sh the trailing `; :` is a separate command that always exits 0. The exit status of the whole shell string is therefore **always 0**, regardless of whether `command -v forgejo-mcp` fails. Concretely: - `forgejo-mcp` **present** → `command -v` prints path, `--help` runs (exit code ignored by `; :`), step green. ✓ - `forgejo-mcp` **absent** → `command -v` exits 1, `&&` short-circuits, `; :` runs, step exits 0 and **the release proceeds with a broken image**. ✗ The in-code comment says _"a missing binary still leaves a visible 'no path printed' trail in the log instead of being silently swallowed by `|| true`"_ — that is true for observability, but the job does **not** fail. This contradicts issue #26 AC: _"Fail the release if any command exits non-zero."_ **Fix** — separate the "binary exists" check (must be fatal) from the "--help exit code" tolerance: ```yaml docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp" docker run --rm "$IMAGE" sh -c "forgejo-mcp --help; :" ``` Or keep it as a single `docker run` but scope the `; :` to the `--help` invocation only: ```yaml docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && (forgejo-mcp --help; :)" ``` Either form lets `command -v` failure propagate and fail the step, while still tolerating a non-zero exit from `--help`. --- ### Everything else ✅ - `runs-on: [docker, release]` isolation is correct; `qa.yml` stays on plain `docker` and is not changed in this PR. - `continue-on-error: true` on the annotation step correctly prevents a transient Forgejo API blip from triggering the rollback (the rollback gates on `failure()`, which `continue-on-error` suppresses for that step). Logic is sound. - Rollback order and best-effort 404 handling are correct. - Deleting `:latest` on rollback is documented as a conscious trade-off in `docs/runner-setup.md` — acceptable. - `docs/runner-setup.md` is thorough: labels table, `config.yml` snippet, security rationale, dry-run procedure, troubleshooting. - README "Release pipeline" section correctly points at the doc. - `FORGEJO_TOKEN` and `REGISTRY_TOKEN` scopes documented as repo-scoped — good security posture.
fix(release): fail step on missing forgejo-mcp binary
All checks were successful
qa / qa (pull_request) Successful in 2m30s
qa / dockerfile (pull_request) Successful in 8s
36623adbae
Scope the `; :` help-exit-code tolerance to a subshell around `--help`
only. Missing-binary failures from `command -v` now propagate out of
`sh -c` and fail the step, while a non-zero exit from `--help` itself
still stays green.

Addresses review feedback on #115.
Author
Collaborator

Addressed in 36623ad.

forgejo-mcp binary-absent case silently swallowed

Agreed — you're right, ; : was sibling to command -v, not subordinate to --help. Scoped it to a subshell around --help only, as you suggested:

docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && (forgejo-mcp --help; :)"

Now command -v failure (exit 1) propagates out of sh -c and fails the step, while a non-zero --help exit stays tolerated inside the subshell. Updated the inline comment to match the new semantics.

Addressed in 36623ad. ### `forgejo-mcp` binary-absent case silently swallowed Agreed — you're right, `; :` was sibling to `command -v`, not subordinate to `--help`. Scoped it to a subshell around `--help` only, as you suggested: ```yaml docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && (forgejo-mcp --help; :)" ``` Now `command -v` failure (exit 1) propagates out of `sh -c` and fails the step, while a non-zero `--help` exit stays tolerated inside the subshell. Updated the inline comment to match the new semantics.
reviewer requested changes 2026-04-20 00:44:05 +00:00
Dismissed
reviewer left a comment

Review: feat(release): release-only runner label + post-push rollback

CI: Green (fix(release): isolate annotation failures from rollback; sharpen smoke test, run #1673, 2m38s, success)

Acceptance criteria coverage: All criteria from #26 are addressed architecturally, but one smoke-test check has a logic bug that causes it to never fail even when the binary is absent — violating the AC requirement to "fail the release if any command exits non-zero."


🔴 Bug: forgejo-mcp smoke test never fails — binary absence is undetectable

File: .forgejo/workflows/release.yml, Smoke test image step

The problem:

docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && forgejo-mcp --help; :"

The shell grammar here is:

(command -v forgejo-mcp && forgejo-mcp --help) ; (:)

The ; has lower precedence than &&, so : (a no-op that always exits 0) runs unconditionally as a separate statement regardless of what the && chain returned. When forgejo-mcp is absent from the image:

  1. command -v forgejo-mcp → exits 1
  2. && short-circuits — forgejo-mcp --help skipped
  3. ; separates next statement
  4. : runs → exits 0
  5. docker run exits 0 → step is green

A Docker image with no forgejo-mcp binary passes the smoke test. The rollback is never triggered. The acceptance criterion says: "Fail the release if any command exits non-zero." A missing binary is clearly the highest-severity failure mode.

The comment says "a missing binary still leaves a visible trail in the log" — but the trail is just a blank line from command -v printing nothing. That does not constitute failure.

Fix: wrap the --help invocation in a subshell so && short-circuits the entire right-hand side when the binary is absent:

docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && (forgejo-mcp --help; :)"

With this fix:

  • Binary absent: command -v exits 1 → && skips the subshell → sh exits 1 → step FAILS → rollback triggered ✓
  • Binary present but --help exits non-zero: command -v prints path → subshell runs → ; : exits 0 → step SUCCEEDS ✓

The other three checks (bun --version, git --version, claude --version) correctly fail when their binary is missing because they invoke the binary directly without a ; : guard. The forgejo-mcp check should behave consistently.


Everything else looks solid

  • runs-on: [docker, release] correctly gates publish-image to the dedicated runner; qa.yml stays on plain docker
  • continue-on-error: true on the annotation step is correct — a transient Forgejo API blip on the cosmetic digest update must not trigger the rollback ✓
  • Rollback condition if: failure() && steps.push.outcome == 'success' is the right guard ✓
  • Rollback gracefully handles 404 on package delete (re-run or manual cleanup already did it) ✓
  • set -u in rollback catches undefined variable expansions ✓
  • RELEASE_ID fetch uses || echo '' + empty-string guard before the PATCH — correct ✓
  • :latest rollback tradeoff (could delete a good :latest from a prior real release) is explicitly documented in docs/runner-setup.md — accepted ✓
  • docs/runner-setup.md is thorough: registration steps, config.yml snippet, security rationale (socket = root on host), repo-scoped token guidance, dry-run procedure ✓
  • README "Release pipeline" section correctly points to the doc ✓
## Review: feat(release): release-only runner label + post-push rollback **CI**: ✅ Green (`fix(release): isolate annotation failures from rollback; sharpen smoke test`, run #1673, 2m38s, success) **Acceptance criteria coverage**: All criteria from #26 are addressed architecturally, but one smoke-test check has a logic bug that causes it to never fail even when the binary is absent — violating the AC requirement to "fail the release if any command exits non-zero." --- ### 🔴 Bug: `forgejo-mcp` smoke test never fails — binary absence is undetectable **File**: `.forgejo/workflows/release.yml`, `Smoke test image` step **The problem:** ```yaml docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && forgejo-mcp --help; :" ``` The shell grammar here is: ``` (command -v forgejo-mcp && forgejo-mcp --help) ; (:) ``` The `;` has **lower precedence than `&&`**, so `:` (a no-op that always exits 0) runs unconditionally as a separate statement regardless of what the `&&` chain returned. When `forgejo-mcp` is absent from the image: 1. `command -v forgejo-mcp` → exits 1 2. `&&` short-circuits — `forgejo-mcp --help` skipped 3. `;` separates next statement 4. `:` runs → exits 0 5. `docker run` exits 0 → **step is green** A Docker image with no `forgejo-mcp` binary passes the smoke test. The rollback is never triggered. The acceptance criterion says: _"Fail the release if any command exits non-zero."_ A missing binary is clearly the highest-severity failure mode. The comment says "a missing binary still leaves a visible trail in the log" — but the trail is just a blank line from `command -v` printing nothing. That does not constitute failure. **Fix**: wrap the `--help` invocation in a subshell so `&&` short-circuits the _entire right-hand side_ when the binary is absent: ```bash docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && (forgejo-mcp --help; :)" ``` With this fix: - Binary **absent**: `command -v` exits 1 → `&&` skips the subshell → `sh` exits 1 → step FAILS → rollback triggered ✓ - Binary **present** but `--help` exits non-zero: `command -v` prints path → subshell runs → `; :` exits 0 → step SUCCEEDS ✓ The other three checks (`bun --version`, `git --version`, `claude --version`) correctly fail when their binary is missing because they invoke the binary directly without a `; :` guard. The `forgejo-mcp` check should behave consistently. --- ### Everything else looks solid - `runs-on: [docker, release]` correctly gates `publish-image` to the dedicated runner; `qa.yml` stays on plain `docker` ✓ - `continue-on-error: true` on the annotation step is correct — a transient Forgejo API blip on the cosmetic digest update must not trigger the rollback ✓ - Rollback condition `if: failure() && steps.push.outcome == 'success'` is the right guard ✓ - Rollback gracefully handles 404 on package delete (re-run or manual cleanup already did it) ✓ - `set -u` in rollback catches undefined variable expansions ✓ - `RELEASE_ID` fetch uses `|| echo ''` + empty-string guard before the PATCH — correct ✓ - `:latest` rollback tradeoff (could delete a good `:latest` from a prior real release) is explicitly documented in `docs/runner-setup.md` — accepted ✓ - `docs/runner-setup.md` is thorough: registration steps, `config.yml` snippet, security rationale (socket = root on host), repo-scoped token guidance, dry-run procedure ✓ - README "Release pipeline" section correctly points to the doc ✓
Author
Collaborator

Looks like this review raced the fix — commit 36623ad (tagged on the review itself) already contains the subshell form you're asking for. The quoted buggy line is from fd1d624 (the previous commit).

Current state of .forgejo/workflows/release.yml line 197 on 36623ad:

docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && (forgejo-mcp --help; :)"

Which matches your recommended fix verbatim:

  • Binary absent: command -v exits 1 → && short-circuits the subshell → sh -c exits 1 → step fails → rollback triggers ✓
  • Binary present, --help non-zero: command -v prints path → subshell runs forgejo-mcp --help; : → exits 0 → step passes ✓

Inline comment above the line was also rewritten to describe the new semantics ("must fail the step if the binary is missing … ; : is scoped inside a subshell around --help only").

Nothing to push — could you re-review 36623ad directly?

Looks like this review raced the fix — commit `36623ad` (tagged on the review itself) already contains the subshell form you're asking for. The quoted buggy line is from `fd1d624` (the previous commit). Current state of `.forgejo/workflows/release.yml` line 197 on `36623ad`: ```yaml docker run --rm "$IMAGE" sh -c "command -v forgejo-mcp && (forgejo-mcp --help; :)" ``` Which matches your recommended fix verbatim: - Binary **absent**: `command -v` exits 1 → `&&` short-circuits the subshell → `sh -c` exits 1 → step fails → rollback triggers ✓ - Binary **present**, `--help` non-zero: `command -v` prints path → subshell runs `forgejo-mcp --help; :` → exits 0 → step passes ✓ Inline comment above the line was also rewritten to describe the new semantics ("must fail the step if the binary is missing … `; :` is scoped inside a subshell around `--help` only"). Nothing to push — could you re-review `36623ad` directly?
code-lead force-pushed boss/26 from 36623adbae
All checks were successful
qa / qa (pull_request) Successful in 2m30s
qa / dockerfile (pull_request) Successful in 8s
to 17e621d12b
All checks were successful
qa / qa (pull_request) Successful in 2m30s
qa / dockerfile (pull_request) Successful in 9s
2026-04-20 01:05:14 +00:00
Compare
reviewer requested changes 2026-04-20 01:11:26 +00:00
Dismissed
reviewer left a comment

Review — REQUEST_CHANGES (CI pending)

CI was still running at review time — push any trivial change (or wait) and I will re-review when it completes.


Code review findings (independent of CI)

The implementation is well-structured and I found no correctness bugs. All acceptance criteria from issue #26 are met.

.forgejo/workflows/release.yml

publish-image runner labelruns-on: [docker, release] is the right mechanism. Every other job keeps plain docker, so PR CI can never land on the socket-mounted runner even if they share the same host.

Rollback trigger conditionif: failure() && steps.push.outcome == 'success' is precise: it only fires when there is something to roll back (push succeeded) and something went wrong after the push.

continue-on-error: true on digest annotation — Correct design. A transient API blip on the cosmetic digest annotation must not trigger rollback of a good image. The comment explains the reasoning clearly.

Package DELETE loop — Avoids -f on curl, captures HTTP status, treats 2xx and 404 as success. Non-fatal for other codes. The original failure is never masked.

Empty RELEASE_ID guard in rollbackset -u active, but || echo '' fallback + [ -z "${RELEASE_ID}" ] early-exit handle the missing-release case cleanly.

Python JSON body encodingjson.dumps(sys.stdin.read()) properly escapes the PATCH body, no injection risk from release body content.

docs/runner-setup.md

Clear, complete, operator-friendly. Covers registration, config.yml snippet, security rationale (socket ≡ root on host → repo-scoped token + label gating), and the rc dry-run procedure. The :latest rollback caveat is called out explicitly.

README.md

Brief "Release pipeline" section with a pointer to docs/runner-setup.md. Appropriate level of detail.

Acceptance-criteria map

Criterion Status
Dedicated runner with socket access (Option A) documented
release label gates publish-image; PR CI stays on plain docker
Runner setup in docs/runner-setup.md + README pointer
bun/git/forgejo-mcp/claude smoke tests after push
Credential-file audit against pushed image
Rollback on failure (delete tags + flip release to prerelease + DO NOT USE banner)
qa.yml static dockerfile job untouched
Dry-run procedure for v0.0.0-rc1 documented

Ready to approve once CI goes green.

## Review — REQUEST_CHANGES (CI pending) CI was still running at review time — push any trivial change (or wait) and I will re-review when it completes. --- ### Code review findings (independent of CI) The implementation is well-structured and I found **no correctness bugs**. All acceptance criteria from issue #26 are met. #### `.forgejo/workflows/release.yml` **`publish-image` runner label** — `runs-on: [docker, release]` is the right mechanism. Every other job keeps plain `docker`, so PR CI can never land on the socket-mounted runner even if they share the same host. ✅ **Rollback trigger condition** — `if: failure() && steps.push.outcome == 'success'` is precise: it only fires when there is something to roll back (push succeeded) and something went wrong after the push. ✅ **`continue-on-error: true` on digest annotation** — Correct design. A transient API blip on the cosmetic digest annotation must not trigger rollback of a good image. The comment explains the reasoning clearly. ✅ **Package DELETE loop** — Avoids `-f` on curl, captures HTTP status, treats 2xx and 404 as success. Non-fatal for other codes. The original failure is never masked. ✅ **Empty RELEASE_ID guard in rollback** — `set -u` active, but `|| echo ''` fallback + `[ -z "${RELEASE_ID}" ]` early-exit handle the missing-release case cleanly. ✅ **Python JSON body encoding** — `json.dumps(sys.stdin.read())` properly escapes the PATCH body, no injection risk from release body content. ✅ #### `docs/runner-setup.md` Clear, complete, operator-friendly. Covers registration, `config.yml` snippet, security rationale (socket ≡ root on host → repo-scoped token + label gating), and the rc dry-run procedure. The `:latest` rollback caveat is called out explicitly. ✅ #### `README.md` Brief "Release pipeline" section with a pointer to `docs/runner-setup.md`. Appropriate level of detail. ✅ #### Acceptance-criteria map | Criterion | Status | |---|---| | Dedicated runner with socket access (Option A) documented | ✅ | | `release` label gates `publish-image`; PR CI stays on plain `docker` | ✅ | | Runner setup in `docs/runner-setup.md` + README pointer | ✅ | | `bun`/`git`/`forgejo-mcp`/`claude` smoke tests after push | ✅ | | Credential-file audit against pushed image | ✅ | | Rollback on failure (delete tags + flip release to prerelease + DO NOT USE banner) | ✅ | | `qa.yml` static `dockerfile` job untouched | ✅ | | Dry-run procedure for `v0.0.0-rc1` documented | ✅ | **Ready to approve once CI goes green.**
reviewer requested changes 2026-04-20 01:19:07 +00:00
Dismissed
reviewer left a comment

Review: REQUEST_CHANGES

CI is green (run #1678, 2m39s ). All acceptance criteria from issue #26 are met. One correctness bug in the rollback step needs fixing before merge.


🐛 Bug — rollback package DELETE will silently fail with a 403

File: .forgejo/workflows/release.ymlRollback on smoke-test failure step

The rollback deletes the bad image from the registry using FORGEJO_TOKEN:

curl -sS -o /tmp/rollback.out -w '%{http_code}' -X DELETE \
  -H "Authorization: token ${FORGEJO_TOKEN}" \
  "https://forge.jacquin.app/api/v1/packages/charles/container/claude-hooks/${tag}"

But docs/runner-setup.md says FORGEJO_TOKEN only needs write:repository scope. The Forgejo packages DELETE endpoint (/api/v1/packages/…) requires write:package scope — which is what REGISTRY_TOKEN has per the same doc. A token with only write:repository will get a 403, which the case statement treats as * (non-fatal): it logs "non-fatal" and continues to patch the release. The image stays in the registry. The rollback's primary safety measure — preventing operators from pulling a busted image — silently fails.

Fix: add REGISTRY_TOKEN to the rollback step's env: and use it for the DELETE calls:

- name: Rollback on smoke-test failure
  if: failure() && steps.push.outcome == 'success'
  env:
    VERSION: ${{ github.ref_name }}
    FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
    REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}   # ← add this
  run: |
    ...
    status=$(curl -sS -o /tmp/rollback.out -w '%{http_code}' -X DELETE \
      -H "Authorization: token ${REGISTRY_TOKEN}" \   # ← use REGISTRY_TOKEN here
      "https://forge.jacquin.app/api/v1/packages/charles/container/claude-hooks/${tag}" || echo '000')

Keep FORGEJO_TOKEN for the release PATCH call (which needs write:repository). This preserves least-privilege for both tokens.


📝 Minor typo (non-blocking, fix while you're in there)

File: .forgejo/workflows/release.yml — comment in the Build and push step:

and its absence fails the build immedialtely.

Should be immediately.


Acceptance criteria checklist

Criterion Status
publish-image gates on [docker, release]
All other jobs stay on plain docker
docs/runner-setup.md — runner registration, container.options snippet, security rationale
Dry-run procedure for v0.0.0-rc1 documented
README pointer to docs/runner-setup.md
bun, git, forgejo-mcp, claude smoke tests after push
Credential-file audit after push
Rollback implemented (image delete + prerelease banner) (logic correct; token fix needed)
qa.yml untouched
## Review: REQUEST_CHANGES CI is green (run #1678, 2m39s ✅). All acceptance criteria from issue #26 are met. One correctness bug in the rollback step needs fixing before merge. --- ### 🐛 Bug — rollback package DELETE will silently fail with a 403 **File:** `.forgejo/workflows/release.yml` — `Rollback on smoke-test failure` step The rollback deletes the bad image from the registry using `FORGEJO_TOKEN`: ```sh curl -sS -o /tmp/rollback.out -w '%{http_code}' -X DELETE \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "https://forge.jacquin.app/api/v1/packages/charles/container/claude-hooks/${tag}" ``` But `docs/runner-setup.md` says `FORGEJO_TOKEN` only needs `write:repository` scope. The Forgejo packages DELETE endpoint (`/api/v1/packages/…`) requires `write:package` scope — which is what `REGISTRY_TOKEN` has per the same doc. A token with only `write:repository` will get a 403, which the `case` statement treats as `*` (non-fatal): it logs "non-fatal" and continues to patch the release. The image stays in the registry. The rollback's primary safety measure — preventing operators from pulling a busted image — silently fails. **Fix:** add `REGISTRY_TOKEN` to the rollback step's `env:` and use it for the DELETE calls: ```yaml - name: Rollback on smoke-test failure if: failure() && steps.push.outcome == 'success' env: VERSION: ${{ github.ref_name }} FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} # ← add this run: | ... status=$(curl -sS -o /tmp/rollback.out -w '%{http_code}' -X DELETE \ -H "Authorization: token ${REGISTRY_TOKEN}" \ # ← use REGISTRY_TOKEN here "https://forge.jacquin.app/api/v1/packages/charles/container/claude-hooks/${tag}" || echo '000') ``` Keep `FORGEJO_TOKEN` for the release PATCH call (which needs `write:repository`). This preserves least-privilege for both tokens. --- ### 📝 Minor typo (non-blocking, fix while you're in there) **File:** `.forgejo/workflows/release.yml` — comment in the `Build and push` step: > `and its absence fails the build immedialtely.` Should be `immediately`. --- ### ✅ Acceptance criteria checklist | Criterion | Status | |---|---| | `publish-image` gates on `[docker, release]` | ✅ | | All other jobs stay on plain `docker` | ✅ | | `docs/runner-setup.md` — runner registration, `container.options` snippet, security rationale | ✅ | | Dry-run procedure for `v0.0.0-rc1` documented | ✅ | | README pointer to `docs/runner-setup.md` | ✅ | | `bun`, `git`, `forgejo-mcp`, `claude` smoke tests after push | ✅ | | Credential-file audit after push | ✅ | | Rollback implemented (image delete + prerelease banner) | ✅ (logic correct; token fix needed) | | `qa.yml` untouched | ✅ |
reviewer requested changes 2026-04-20 01:19:19 +00:00
Dismissed
reviewer left a comment

Review: REQUEST_CHANGES

CI is green (run #1678, 2m39s ). All acceptance criteria from issue #26 are met. One correctness bug in the rollback step needs fixing before merge.


🐛 Bug — rollback package DELETE will silently fail with a 403

File: .forgejo/workflows/release.ymlRollback on smoke-test failure step

The rollback deletes the bad image from the registry using FORGEJO_TOKEN:

curl -sS -o /tmp/rollback.out -w '%{http_code}' -X DELETE \
  -H "Authorization: token ${FORGEJO_TOKEN}" \
  "https://forge.jacquin.app/api/v1/packages/charles/container/claude-hooks/${tag}"

But docs/runner-setup.md says FORGEJO_TOKEN only needs write:repository scope. The Forgejo packages DELETE endpoint (/api/v1/packages/…) requires write:package scope — which is what REGISTRY_TOKEN has per the same doc. A token with only write:repository will get a 403, which the case statement treats as * (non-fatal): it logs "non-fatal" and continues to patch the release. The image stays in the registry. The rollback's primary safety measure — preventing operators from pulling a busted image — silently fails.

Fix: add REGISTRY_TOKEN to the rollback step's env: and use it for the DELETE calls:

- name: Rollback on smoke-test failure
  if: failure() && steps.push.outcome == 'success'
  env:
    VERSION: ${{ github.ref_name }}
    FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
    REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}   # ← add this
  run: |
    ...
    status=$(curl -sS -o /tmp/rollback.out -w '%{http_code}' -X DELETE \
      -H "Authorization: token ${REGISTRY_TOKEN}" \   # ← use REGISTRY_TOKEN here
      "https://forge.jacquin.app/api/v1/packages/charles/container/claude-hooks/${tag}" || echo '000')

Keep FORGEJO_TOKEN for the release PATCH calls (which need write:repository). This preserves least-privilege for both tokens.


📝 Minor typo (non-blocking, fix while you're in there)

File: .forgejo/workflows/release.yml — comment in the Build and push step:

and its absence fails the build immedialtely.

Should be immediately.


Acceptance criteria checklist

Criterion Status
publish-image gates on [docker, release]
All other jobs stay on plain docker
docs/runner-setup.md — runner registration, container.options snippet, security rationale
Dry-run procedure for v0.0.0-rc1 documented
README pointer to docs/runner-setup.md
bun, git, forgejo-mcp, claude smoke tests after push
Credential-file audit after push
Rollback implemented (image delete + prerelease banner) (logic correct; token fix needed)
qa.yml untouched
## Review: REQUEST_CHANGES CI is green (run #1678, 2m39s ✅). All acceptance criteria from issue #26 are met. One correctness bug in the rollback step needs fixing before merge. --- ### 🐛 Bug — rollback package DELETE will silently fail with a 403 **File:** `.forgejo/workflows/release.yml` — `Rollback on smoke-test failure` step The rollback deletes the bad image from the registry using `FORGEJO_TOKEN`: ```sh curl -sS -o /tmp/rollback.out -w '%{http_code}' -X DELETE \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "https://forge.jacquin.app/api/v1/packages/charles/container/claude-hooks/${tag}" ``` But `docs/runner-setup.md` says `FORGEJO_TOKEN` only needs `write:repository` scope. The Forgejo packages DELETE endpoint (`/api/v1/packages/…`) requires `write:package` scope — which is what `REGISTRY_TOKEN` has per the same doc. A token with only `write:repository` will get a 403, which the `case` statement treats as `*` (non-fatal): it logs "non-fatal" and continues to patch the release. The image stays in the registry. The rollback's primary safety measure — preventing operators from pulling a busted image — silently fails. **Fix:** add `REGISTRY_TOKEN` to the rollback step's `env:` and use it for the DELETE calls: ```yaml - name: Rollback on smoke-test failure if: failure() && steps.push.outcome == 'success' env: VERSION: ${{ github.ref_name }} FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} # ← add this run: | ... status=$(curl -sS -o /tmp/rollback.out -w '%{http_code}' -X DELETE \ -H "Authorization: token ${REGISTRY_TOKEN}" \ # ← use REGISTRY_TOKEN here "https://forge.jacquin.app/api/v1/packages/charles/container/claude-hooks/${tag}" || echo '000') ``` Keep `FORGEJO_TOKEN` for the release PATCH calls (which need `write:repository`). This preserves least-privilege for both tokens. --- ### 📝 Minor typo (non-blocking, fix while you're in there) **File:** `.forgejo/workflows/release.yml` — comment in the `Build and push` step: > `and its absence fails the build immedialtely.` Should be `immediately`. --- ### ✅ Acceptance criteria checklist | Criterion | Status | |---|---| | `publish-image` gates on `[docker, release]` | ✅ | | All other jobs stay on plain `docker` | ✅ | | `docs/runner-setup.md` — runner registration, `container.options` snippet, security rationale | ✅ | | Dry-run procedure for `v0.0.0-rc1` documented | ✅ | | README pointer to `docs/runner-setup.md` | ✅ | | `bun`, `git`, `forgejo-mcp`, `claude` smoke tests after push | ✅ | | Credential-file audit after push | ✅ | | Rollback implemented (image delete + prerelease banner) | ✅ (logic correct; token fix needed) | | `qa.yml` untouched | ✅ |
fix(release): rollback package DELETE uses REGISTRY_TOKEN (write:package)
All checks were successful
qa / qa (pull_request) Successful in 2m43s
qa / dockerfile (pull_request) Successful in 10s
d7f91dbe08
Reviewer R5 finding on PR #115: the rollback's DELETE on the packages
API was using FORGEJO_TOKEN (write:repository), which the packages
endpoint rejects with 403. The case handler treats 403 as non-fatal
("continues to patch the release"), so the rollback's primary safety
measure — removing a busted image from the registry — silently fails.

Swap the DELETE header to REGISTRY_TOKEN (write:package) to match the
endpoint's scope. Keep FORGEJO_TOKEN for the release PATCH calls
(write:repository is what they need). Preserves least-privilege for
both tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator

Manual push on boss/26 (d7f91db) applying R5's token-scope fix — boss was stuck in address-review retries on a stale worktree and the fix is small enough that pushing directly was faster than unsticking the loop.

  • REGISTRY_TOKEN added to the rollback step's env; package-DELETE swapped from FORGEJO_TOKEN (write:repository) to REGISTRY_TOKEN (write:package). Release PATCH calls keep FORGEJO_TOKEN. Least-privilege preserved.
  • R5's immedialtely typo was already fixed on the branch before this commit (R5 must have been reading a stale view).

Re-review when CI goes green.

Manual push on `boss/26` (`d7f91db`) applying R5's token-scope fix — boss was stuck in address-review retries on a stale worktree and the fix is small enough that pushing directly was faster than unsticking the loop. - `REGISTRY_TOKEN` added to the rollback step's env; package-DELETE swapped from `FORGEJO_TOKEN` (write:repository) to `REGISTRY_TOKEN` (write:package). Release PATCH calls keep `FORGEJO_TOKEN`. Least-privilege preserved. - R5's `immedialtely` typo was already fixed on the branch before this commit (R5 must have been reading a stale view). Re-review when CI goes green.
reviewer approved these changes 2026-04-20 08:46:19 +00:00
reviewer left a comment

Review — PR #115: feat(release): release-only runner label + post-push rollback

CI: green on head commit d7f91db (run #1679, 2m54s).


Acceptance criteria (issue #26) — all met

Criterion Status
Dedicated runner with docker socket (Option A) docs/runner-setup.md §3 — container.options: -v /var/run/docker.sock:/var/run/docker.sock
publish-image gated on [docker, release]; all other jobs stay on plain docker
Runner deployment documented docs/runner-setup.md + README "Release pipeline" section
Post-push runtime checks: bun --version, git --version, forgejo-mcp --help, claude --version Smoke test step, including the (forgejo-mcp --help; :) pattern to tolerate non-zero --help exit
Credential-file audit against all 5 required paths Verify step covers /home/claude/.claude.json, .credentials.json, .config/claude-hooks, /root/.claude.json, /root/.credentials.json
Rollback on failure Deletes :${VERSION} + :latest from registry; flips release to prerelease=true with DO NOT USE banner
qa.yml static dockerfile job untouched (not in diff)
Dry-run procedure documented docs/runner-setup.md §5

What looks good

  • Label-gating logic is airtight. publish-image requires both docker AND release; qa.yml only ever requests docker, so PR CI can never land on the privileged runner even if the same host runs both.
  • Rollback token scoping is correctly split: REGISTRY_TOKEN (write:package) deletes the container images; FORGEJO_TOKEN (write:repository) patches the release. The comment in the rollback env: block explains exactly why — Packages API silently 403s on write:repository. The final commit fixing this is the right call.
  • continue-on-error: true on "Update release notes" correctly keeps the job green for a transient API blip so the rollback step stays dormant. Well-commented.
  • if: failure() && steps.push.outcome == 'success' correctly limits rollback to the case where there is actually something to clean up.
  • set -u in the rollback script and || echo '000' on the DELETE curl call guard against the most likely failure modes.
  • :latest rollback trade-off is honestly documented in docs/runner-setup.md (the "⚠️ :latest and rollback" callout).
  • Security rationale in docs/runner-setup.md is clear and actionable (socket = root on host, repo-scoped token, don't register org-wide).

One non-blocking observation

release.yml, rollback step — CURRENT_BODY fetch has no error fallback.

CURRENT_BODY=$(curl -fsSL \
  -H "Authorization: token ${FORGEJO_TOKEN}" \
  "https://forge.jacquin.app/api/v1/repos/charles/claude-hooks/releases/${RELEASE_ID}" \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['body'])")

Actions runs with -eo pipefail by default. If the Forgejo API is unreachable at this point (after RELEASE_ID was fetched successfully), curl -f exits non-zero, -e kills the script before the PATCH, and the release doesn't get the DO NOT USE banner. The primary safety measure — image deletion — runs earlier and IS protected with || echo '000', so the acceptance criterion is met. Adding || echo '' as a fallback here would make the failure mode consistent with the rest of the script. Suggestion only, not a blocker.


Verdict: APPROVED. All acceptance criteria met, code is correct and well-commented, CI is green.

## Review — PR #115: `feat(release): release-only runner label + post-push rollback` **CI:** ✅ green on head commit `d7f91db` (run #1679, 2m54s). --- ### Acceptance criteria (issue #26) — all met | Criterion | Status | |---|---| | Dedicated runner with docker socket (Option A) | ✅ `docs/runner-setup.md` §3 — `container.options: -v /var/run/docker.sock:/var/run/docker.sock` | | `publish-image` gated on `[docker, release]`; all other jobs stay on plain `docker` | ✅ | | Runner deployment documented | ✅ `docs/runner-setup.md` + README "Release pipeline" section | | Post-push runtime checks: `bun --version`, `git --version`, `forgejo-mcp --help`, `claude --version` | ✅ Smoke test step, including the `(forgejo-mcp --help; :)` pattern to tolerate non-zero `--help` exit | | Credential-file audit against all 5 required paths | ✅ Verify step covers `/home/claude/.claude.json`, `.credentials.json`, `.config/claude-hooks`, `/root/.claude.json`, `/root/.credentials.json` | | Rollback on failure | ✅ Deletes `:${VERSION}` + `:latest` from registry; flips release to `prerelease=true` with DO NOT USE banner | | `qa.yml` static `dockerfile` job untouched | ✅ (not in diff) | | Dry-run procedure documented | ✅ `docs/runner-setup.md` §5 | --- ### What looks good - **Label-gating logic is airtight.** `publish-image` requires both `docker` AND `release`; `qa.yml` only ever requests `docker`, so PR CI can never land on the privileged runner even if the same host runs both. - **Rollback token scoping** is correctly split: `REGISTRY_TOKEN` (`write:package`) deletes the container images; `FORGEJO_TOKEN` (`write:repository`) patches the release. The comment in the rollback `env:` block explains exactly why — Packages API silently 403s on `write:repository`. The final commit fixing this is the right call. - **`continue-on-error: true` on "Update release notes"** correctly keeps the job green for a transient API blip so the rollback step stays dormant. Well-commented. - **`if: failure() && steps.push.outcome == 'success'`** correctly limits rollback to the case where there is actually something to clean up. - **`set -u`** in the rollback script and `|| echo '000'` on the DELETE curl call guard against the most likely failure modes. - **`:latest` rollback trade-off** is honestly documented in `docs/runner-setup.md` (the "⚠️ :latest and rollback" callout). - **Security rationale** in `docs/runner-setup.md` is clear and actionable (socket = root on host, repo-scoped token, don't register org-wide). --- ### One non-blocking observation **`release.yml`, rollback step — `CURRENT_BODY` fetch has no error fallback.** ```bash CURRENT_BODY=$(curl -fsSL \ -H "Authorization: token ${FORGEJO_TOKEN}" \ "https://forge.jacquin.app/api/v1/repos/charles/claude-hooks/releases/${RELEASE_ID}" \ | python3 -c "import json,sys; print(json.load(sys.stdin)['body'])") ``` Actions runs with `-eo pipefail` by default. If the Forgejo API is unreachable at this point (after `RELEASE_ID` was fetched successfully), `curl -f` exits non-zero, `-e` kills the script before the PATCH, and the release doesn't get the DO NOT USE banner. The primary safety measure — image deletion — runs earlier and IS protected with `|| echo '000'`, so the acceptance criterion is met. Adding `|| echo ''` as a fallback here would make the failure mode consistent with the rest of the script. Suggestion only, not a blocker. --- **Verdict: APPROVED.** All acceptance criteria met, code is correct and well-commented, CI is green.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!115
No description provided.