Repository navigation
ci: add a release dry run, so the release job can be executed without a tag - #214
Conversation
… a tag The release job is gated on `refs/tags/v`, so everything in it (packaging the standalone archives, and especially `download-artifact`) runs for the first time on a real release. That is how a dependency bump like download-artifact 4 to 8 can sit merged with no coverage until the next tag, where a failure blocks a release. Adds a `workflow_dispatch` trigger with a `dry_run` input, default true: - build pushes nothing on a dispatch (`push` is now `event_name == 'push'`, so pushes and tags still publish and PRs and dispatches do not); - the release job also runs on a dry-run dispatch, so its packaging and download steps execute against a real artifact; - `Create GitHub Release` stays gated on a version tag, so a dry run cannot publish anything. Rehearsing a release is now a manual dispatch, not a throwaway tag.
There was a problem hiding this comment.
Important
The dry-run path can't go green as written: arm64-smoke still requires an image that a dispatch never pushes. The documented rehearsal also collides with the main push it is meant to follow.
Reviewed changes
- New
workflow_dispatchtrigger — booleandry_runinput (defaulttrue) so the release job can be exercised without a tag. - Conditional image push —
push:narrowed fromgithub.event_name != 'pull_request'togithub.event_name == 'push', so a dispatch builds without publishing while PR behaviour is unchanged. - Release job gating —
release.ifnow also fires on adry_rundispatch;Create GitHub Releasestays gated on a version tag.
deepseek-v4.1-flash | 𝕏
| context: . | ||
| platforms: ${{ env.BUILD_PLATFORMS }} | ||
| push: ${{ github.event_name != 'pull_request' }} | ||
| push: ${{ github.event_name == 'push' }} |
There was a problem hiding this comment.
On a workflow_dispatch, push is false, but arm64-smoke still takes its "pull the image this build just published" path because every gate there tests github.event_name != 'pull_request' (lines 168, 180, 187), which is true for a dispatch. With nothing pushed, needs.build.outputs.digest is empty — buildx only emits containerimage.digest when a result is exported — so the step fails its explicit empty-digest check, and a non-empty digest would still docker pull an image that was never pushed. A dry run therefore always ends red, which is the opposite of the green rehearsal this PR adds.
Technical details
# Dry-run dispatch fails arm64-smoke
## Affected sites
- `.github/workflows/docker-publish.yml:100` — `push: ${{ github.event_name == 'push' }}` leaves a dispatch with no pushed image.
- `.github/workflows/docker-publish.yml:168` — `if: github.event_name != 'pull_request'` selects the pull path on dispatch.
- `.github/workflows/docker-publish.yml:173-176` — explicit `::error::build produced no digest` exit.
- `.github/workflows/docker-publish.yml:187-191` — `Resolve the image under test` chooses `calnode-pr:arm64` only for `pull_request`.
## Required outcome
- A `workflow_dispatch` run must not depend on an image this run pushed. Either skip `arm64-smoke` on dispatch, or let dispatch take the native-build path.
- If dispatch is routed through the native path, `Resolve the image under test` must select the locally built `calnode-pr:arm64` for dispatch too, not just `pull_request`.| # steps, then stop before publishing a GitHub Release. It exists so the release | ||
| # path can be executed without cutting a tag, which is otherwise the only way to | ||
| # find out whether it still works. | ||
| workflow_dispatch: |
There was a problem hiding this comment.
The trigger inherits concurrency: docker-publish-${{ github.ref }} with cancel-in-progress: true, and a dispatch on main — the usage documented in the PR body — lands in the same group as the merge commit's own main push build. Running the rehearsal right after merging therefore cancels that real :edge publish; a dispatch against a release tag cancels the tag build the same way.
Technical details
# Rehearsal cancels a real publish via shared concurrency group
## Affected sites
- `.github/workflows/docker-publish.yml:26` — new trigger joins an existing ref-keyed concurrency group.
- `.github/workflows/docker-publish.yml:33-35` — `group: docker-publish-${{ github.ref }}`, `cancel-in-progress: true`.
## Required outcome
- A dry-run dispatch must not cancel a concurrent push build for the same ref.
## Suggested approach
- Add the event (or the dry-run flag) to the group, e.g. `docker-publish-${{ github.ref }}-${{ github.event_name }}`, or set `cancel-in-progress` false for dispatch.| # Preinstalled gh CLI + GITHUB_TOKEN (no third-party action — keeps the | ||
| # supply-chain surface small). | ||
| - name: Create GitHub Release | ||
| if: startsWith(github.ref, 'refs/tags/v') |
There was a problem hiding this comment.
This guard checks only the ref, and a dispatch can target a tag (ref accepts a branch or tag name). On a tag ref startsWith(github.ref, 'refs/tags/v') is true, so gh release create runs even with dry_run: true, contradicting the input description, and would publish a release whose image this run never pushed. Require github.event_name == 'push' (or negate inputs.dry_run) in this condition.

The
releasejob indocker-publish.ymlis gated onrefs/tags/v, so every step inside it runs for the first time on an actual release. That includesdownload-artifact, which is why merging the4 → 8bump (#209) left a change with no CI coverage at all: a PR run skips the job entirely.That is a bad place to discover a breaking input rename.
Change
A
workflow_dispatchtrigger with adry_runinput, default true.gh release createConcretely:
push:becomes${{ github.event_name == 'push' }}, so a dispatch does not publish an image.release.ifbecomesstartsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.dry_run).Create GitHub Releasegainsif: startsWith(github.ref, 'refs/tags/v'), so a dry run stops before publishing.How it is used
That builds, packages, uploads, and runs
download-artifact@v8against the real artifact, then stops. It verifies #209 without a tag.Verification
This PR's own CI exercises the build and upload path on
pull_requestunchanged. The dispatch is run onmainimmediately after merge, which is the first real execution ofdownload-artifact@v8and the thing #209 currently lacks coverage for.