Skip to content

ci(release): make a tag the single source of truth for releases - #6

Closed
LeadcodeDev wants to merge 2 commits into
mainfrom
ci/release-automation
Closed

ci(release): make a tag the single source of truth for releases#6
LeadcodeDev wants to merge 2 commits into
mainfrom
ci/release-automation

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Why

Releasing 0.5.9 failed: the tag was pushed before Cargo.toml was bumped, and the workflow's guard rejected the mismatch. The bump and the tag were two manual steps that could desynchronize — this closes that failure mode rather than the instance.

Releasing is now a single action: push a X.Y.Z tag.

What

The tag becomes the single source of truth. The step that used to verify the manifest against the tag now applies the tag version to it, and the release is recorded back on main once it has actually shipped.

  • [workspace.package] version is rewritten from the tag; both crates inherit it via version.workspace = true.
  • The internal sqlx-gen-macros pin moves in lockstep — otherwise sqlx-gen would ship requiring a macros version that was never published.
  • Cargo.lock is refreshed with cargo update --workspace.
  • cargo publish gains --allow-dirty: the version is applied in the runner and is not committed until the release succeeds.
  • A final step commits the version on main as deploy: release X.Y.Z, pushed as github-actions[bot].

The rewrite runs at two call sites, so it lives in .github/scripts/set-workspace-version.sh rather than being duplicated across two YAML steps — which also makes it directly testable.

Ordering and safety

The commit-back runs last, after both cargo publish calls. A commit on main must never announce a release that failed to publish.

The job now needs permissions: contents: write, and fetch-depth: 0 so main is available from a tag checkout. Pushes made with GITHUB_TOKEN do not trigger further workflow runs, so the commit cannot loop back into a publish.

Re-running a release is safe: if main already records the version, the step exits without creating an empty commit.

Guards

Neither sed fails loudly if its pattern stops matching, so the script asserts on the resolved cargo metadata:

Drift Caught by
Version not applied, or pin outside its caret range cargo update (exit 101)
Pin left behind but still inside the caret range the req assertion (exit 1)

The second row is the one that matters in practice: a pin left at ^0.5.9 while releasing 0.5.10 resolves perfectly well, both package versions read correct, and the drift would ship unnoticed. Cargo does not catch it.

Verification

Run locally against the real manifest, restoring it afterwards:

  • bash -n passes on the script and on the commit-back step, extracted from the committed YAML.
  • Applying an arbitrary version yields that version for both crates in cargo metadata.
  • Both branches of the idempotence guard: a changed version produces a diff to commit, an unchanged one exits cleanly.
  • Adversarial: swapping the keys of the pin line — a plain reformatting that leaves the TOML valid and equivalent — makes the second sed miss. Releasing 0.5.10 then exits 1 on the req assertion.

sed > tmp && mv is used rather than sed -i, whose syntax differs between GNU and BSD, so the command exercised locally is the one that runs on the runner.

Known trade-off

The tagged commit does not contain the version bump — the bump lands on main as a separate commit created afterwards. Checking out tag X.Y.Z therefore shows the previous version in Cargo.toml, while the published crate carries X.Y.Z. Moving the tag onto the bump commit was rejected: force-updating a tag from the workflow that the tag itself triggered would re-enter the publish job.

The version rewrite now runs twice — once on the tagged checkout before
publishing, once on main afterwards — so it moves into a versioned script
instead of being duplicated across two YAML steps.

The commit-back deliberately runs last: a commit on main must never
announce a release that failed to publish.

The script also asserts the internal sqlx-gen-macros requirement, not just
the package versions. Cargo already refuses to resolve a pin that falls
outside its caret range, but a pin left at ^0.5.9 while releasing 0.5.10
resolves fine and would ship silently drifted.
@LeadcodeDev LeadcodeDev closed this Aug 2, 2026
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