Skip to content

chore: harden the release publish path - #35

Open
tuliomir wants to merge 2 commits into
mainfrom
release-pipeline-followups
Open

chore: harden the release publish path#35
tuliomir wants to merge 2 commits into
mainfrom
release-pipeline-followups

Conversation

@tuliomir

@tuliomir tuliomir commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Three follow-ups from the v0.0.2 release (#24), plus one hazard they exposed.

How it works

  • .github/workflows/docker.yml — the publish job now skips any tag containing -. Preview snapshot tags (v0.0.1-20260728 and friends) shared the v* trigger with real releases and failed the ancestry or version gate, leaving five red runs with more accruing.
  • Makefile — the default registry was docker.io/hathor, which does not exist on DockerHub. It now has no default and make push refuses without an explicit DOCKER_REGISTRY; :latest is never pushed from here, and the guard runs before the image build rather than after.
  • .claude/settings.jsondocker push and make push join the tagging commands on the deny list.
  • CLAUDE.md — documents that a publish failing after the push is not retryable, and that the workflow is the only sanctioned publish path.

The trigger stays broad on purpose. A narrow tag filter would also silence a malformed release tag like vfoo, making a botched release indistinguishable from no release. SemVer uses - for pre-release identifiers and this workflow refuses pre-releases anyway (it always moves :latest), so the guard is strictly quieter and never more permissive. The version regex remains the authoritative check.

Behavioral change

make push no longer works without configuration, and that is deliberate. Correcting the registry to the real namespace would have converted a target that failed at auth into a one-word way to clobber production: it builds single-arch, so it would replace the published multi-arch index and break arm64 consumers; it always moved :latest; and it bypasses the tag/manifest match and the immutability check the workflow enforces.

This deviates from the issue's acceptance criterion, which asked for make push to target the same registry as the workflow. Satisfying that as written would have made the repository less safe.

Acceptance criteria

  • A preview snapshot tag produces a skipped job, not a failed one.
  • make push with no DOCKER_REGISTRY fails immediately with an explanatory message and builds nothing; make build and make help are unaffected.
  • The non-retryable-publish trade-off is documented in CLAUDE.md.

Closes #28

Summary by CodeRabbit

  • Chores
    • Improved container release safeguards by excluding pre-release tags from publication.
    • Added verification requirements for signed, GitHub-verified release tags and commits.
    • Publishing now requires an explicitly configured container registry and rejects attempts to publish the latest tag.
    • Container releases publish only versioned image tags, reducing the risk of unintended or unverifiable images.
    • Expanded release guidance for signed artifacts, snapshot tags, and recovery from failed publications.

Three follow-ups from the v0.0.2 release, plus one hazard they exposed.

Preview snapshot tags (v0.0.1-20260728 and friends) shared the `v*`
trigger with real releases and failed the ancestry or version gate,
leaving five red runs with more to come. The publish job now skips any
tag containing `-`. The trigger stays broad on purpose: a narrow tag
filter would also silence a malformed release tag like `vfoo`, making a
botched release indistinguishable from no release. SemVer uses `-` for
pre-release identifiers, and this workflow refuses pre-releases anyway
since it always moves `:latest`, so the guard is strictly quieter and
never more permissive.

The Makefile pointed at `docker.io/hathor`, which does not exist on
DockerHub. Correcting it to the real namespace would have turned a
target that failed at auth into a one-word way to clobber production:
`make push` builds single-arch, always moved `:latest`, and bypasses the
tag/manifest match and the immutability check the workflow enforces.
It now has no default registry and refuses to run without an explicit
DOCKER_REGISTRY, never pushes `:latest`, and checks before building
rather than after. `docker push` and `make push` join the deny list.

Documents that a publish failing after the push is not retryable, and
names the one unrecoverable state: `:<version>` lands, `:latest` does
not, and bump-and-retag leaves that version permanently non-latest.

Closes #28

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 4, 2026 18:18
@tuliomir tuliomir added the enhancement New feature or request label Aug 4, 2026
@tuliomir tuliomir self-assigned this Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f46fe6d3-a240-496c-953c-8218c263255e

📥 Commits

Reviewing files that changed from the base of the PR and between fa2faec and 298bd9d.

📒 Files selected for processing (4)
  • .claude/settings.json
  • .github/workflows/docker.yml
  • CLAUDE.md
  • Makefile

📝 Walkthrough

Walkthrough

The release pipeline now skips pre-release tags, verifies signed tag and commit provenance, and documents recovery rules. Docker publishing requires an explicit registry and versioned tag. Claude permissions deny destructive or unapproved push commands.

Changes

Release pipeline hardening

Layer / File(s) Summary
Release tag and provenance verification
.github/workflows/docker.yml, CLAUDE.md
The workflow skips tags containing - and rejects lightweight tags, unverified tag signatures, and unverified commit signatures. Release guidance documents signed release requirements and publish recovery rules.
Explicit Docker push controls
Makefile, .claude/settings.json
make push requires DOCKER_REGISTRY, rejects latest, and pushes only the versioned image. Claude permissions deny release deletion and unapproved Docker, Make, and Buildx push operations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubTagEvent
  participant docker_yml
  participant GitHubAPI
  participant DockerRegistry
  GitHubTagEvent->>docker_yml: start release workflow
  docker_yml->>GitHubAPI: verify tag and commit signatures
  GitHubAPI-->>docker_yml: return provenance status
  docker_yml->>DockerRegistry: publish verified release image
Loading

Possibly related PRs

Suggested labels: documentation

Suggested reviewers: pedroferreira1

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR handles snapshot tags and retry documentation, but it does not make make push target the workflow registry by default as required by issue #28. Set the Makefile registry to the workflow registry, or update issue #28 and its acceptance criteria to approve the explicit-registry design.
Out of Scope Changes check ⚠️ Warning Signed-release verification and Claude deny rules add significant changes that are not required by the objectives in issue #28. Move signed-release verification and deny-list changes to a separate linked issue, or document their required scope in issue #28.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: hardening the release publish path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release-pipeline-followups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens the release/publish workflow and local tooling to reduce the risk of accidental or unsafe Docker image publishing, especially around snapshot/pre-release tags and local make push behavior.

Changes:

  • Skip the Docker publish job entirely for tags containing - (snapshot/pre-release style), avoiding noisy failed runs.
  • Remove the Makefile’s default Docker registry and add an early guard so make push requires explicit DOCKER_REGISTRY (and avoids pushing :latest via explicit tagging).
  • Extend the agent deny list and document the non-retryable publish failure mode and the sanctioned release path.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
Makefile Removes default registry, adds push-time guard, and clarifies push semantics.
CLAUDE.md Documents publish irreversibility and reinforces the workflow-only publish path.
.github/workflows/docker.yml Skips publish job for tags containing - to avoid failed runs for snapshot tags.
.claude/settings.json Adds additional deny-list entries to prevent risky publish commands.
Suppressed comments (1)

Makefile:50

  • Even though make push no longer tags/pushes an explicit :latest, it will still push ...:latest if TAG falls back to latest (e.g., when bun/package.json version resolution fails). That contradicts the safety comment above and can unintentionally clobber a scratch registry’s latest tag. Consider refusing to push when TAG=latest.
	$(MAKE) build
	docker tag $(IMAGE_NAME):$(TAG) $(REGISTRY)/$(IMAGE_NAME):$(TAG)
	docker push $(REGISTRY)/$(IMAGE_NAME):$(TAG)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Makefile Outdated
@test -n "$(REGISTRY)" || { \
echo "DOCKER_REGISTRY is unset."; \
echo "Releases are published by .github/workflows/docker.yml from a"; \
echo "signed tag — see the Releases section of CLAUDE.md."; \

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the message was promising something the pipeline did not enforce. Fixed by making it true rather than by softening the wording.

298bd9d adds a Require a signed tag on a signed commit step that reads verification state from GitHub's API and refuses to publish unless both the tag object and the commit it points at are verified signatures. Lightweight tags are rejected outright, since they carry no signature at all.

Both objects are checked deliberately: a signed tag on an unsigned commit still leaves the artifact that actually ships without provenance. Verification comes from the API rather than git tag -v so trust stays anchored to the keys registered to the org's accounts, instead of a keyring the runner imports — which a workflow edit could quietly widen.

Simulated against real history before committing: v0.0.1 and v0.0.2 both pass, and the unsigned v0.0.1-20260728 preview snapshot is rejected as lightweight — so this is also defence-in-depth behind the pre-release skip added in this PR.

Your suppressed comment on line 50 was right too, and is fixed in the same commit: TAG falls back to latest when DOCKER_TAG is unset and the version cannot be read from package.json, so make push could still move a floating tag despite the comment above it saying otherwise. It now refuses when TAG resolves to latest.

The publish path checked that a tag was well-formed and pointed at a
reviewed commit, but never that anyone authorised the release. Anyone
able to push a tag could ship a public image from any commit on main.
The push guard already claimed releases came from a signed tag, which
made the message a promise the pipeline did not keep.

The workflow now reads verification state from GitHub's API and refuses
unless both the tag object and the commit it points at are verified
signatures. Reading it from the API rather than `git tag -v` keeps trust
anchored to the keys registered to the org's accounts, instead of a
keyring the runner imports and a workflow edit could quietly widen.

Both objects are checked deliberately: a signed tag on an unsigned
commit still leaves the thing actually shipping without provenance.
Lightweight tags are rejected outright since they carry no signature at
all — a release tag must be `git tag -s`.

Verified against real history: v0.0.1 and v0.0.2 both pass, and the
unsigned v0.0.1-20260728 preview snapshot is rejected as lightweight,
which makes this defence-in-depth behind the pre-release skip.

Also refuses `make push` when TAG resolves to `latest`. That happens
when DOCKER_TAG is unset and the version cannot be read from
package.json, and pushing then moves a floating tag rather than an
identifiable build — contradicting the comment directly above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tuliomir tuliomir moved this from Todo to In Progress (Done) in Hathor Network Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: In Progress (Done)

Development

Successfully merging this pull request may close these issues.

Release pipeline follow-ups from v0.0.2

2 participants