From 173747c3b909b9d4a67c20b734a9df48b3313b4e Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Sun, 2 Aug 2026 10:56:23 +0200 Subject: [PATCH 1/2] ci(release): derive workspace version from the pushed tag --- .github/workflows/publish.yml | 54 ++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0e515d0..67cc075 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,25 +13,53 @@ jobs: - uses: dtolnay/rust-toolchain@stable - - name: Verify tag matches workspace version + - name: Apply tag version to workspace run: | - # Version is centralized in the root [workspace.package] block; - # individual crates inherit via `version.workspace = true`. - CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 \ - | jq -r '.packages[] | select(.name=="sqlx-gen") | .version') + # The tag is the single source of truth for a release: the manifest is + # rewritten from it here rather than bumped by hand beforehand, so a + # release can no longer fail on a tag/manifest mismatch. TAG_VERSION=${GITHUB_REF#refs/tags/} - echo "Cargo workspace version: $CARGO_VERSION" - echo "Tag version: $TAG_VERSION" - if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then - echo "::error::Tag $TAG_VERSION does not match workspace version $CARGO_VERSION" - exit 1 - fi + echo "Releasing $TAG_VERSION" + # Version is centralized in [workspace.package]; both crates inherit it + # via `version.workspace = true`. Written as `sed > tmp && mv` rather + # than `sed -i`, whose syntax differs between GNU and BSD — this form + # is the one exercised by the local test. + sed -E '/^\[workspace\.package\]/,/^\[workspace\.dependencies\]/ s/^version = "[^"]*"/version = "'"$TAG_VERSION"'"/' \ + Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml + + # The internal pin must move in lockstep: sqlx-gen depends on + # sqlx-gen-macros by version, and both crates are always released + # together. Leaving it behind would publish a sqlx-gen requiring a + # macros version that never shipped. + sed -E 's|^(sqlx-gen-macros = \{ path = "crates/sqlx_gen_macros", version = )"[^"]*"|\1"'"$TAG_VERSION"'"|' \ + Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml + + cargo update --workspace + + - name: Verify the version was applied + run: | + # Neither sed fails loudly if its pattern stops matching, so assert on + # the resolved metadata: without this, a drifted manifest layout would + # silently republish the previous version. + TAG_VERSION=${GITHUB_REF#refs/tags/} + for pkg in sqlx-gen sqlx-gen-macros; do + GOT=$(cargo metadata --no-deps --format-version 1 \ + | jq -r --arg p "$pkg" '.packages[] | select(.name==$p) | .version') + echo "$pkg: $GOT" + if [ "$GOT" != "$TAG_VERSION" ]; then + echo "::error::$pkg is at $GOT, expected $TAG_VERSION" + exit 1 + fi + done + + # --allow-dirty: the version above is applied in the runner and never + # committed, so the working tree is intentionally dirty at this point. - name: Publish sqlx-gen-macros - run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} --package sqlx-gen-macros + run: cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }} --package sqlx-gen-macros - name: Wait for crates.io index update run: sleep 30 - name: Publish sqlx-gen - run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} --package sqlx-gen + run: cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }} --package sqlx-gen From ebdfae20b553dee4787eee7036f76eb698cc79a6 Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Sun, 2 Aug 2026 11:07:24 +0200 Subject: [PATCH 2/2] ci(release): record the released version on main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/scripts/set-workspace-version.sh | 52 +++++++++++++++ .github/workflows/publish.yml | 84 +++++++++++++----------- 2 files changed, 96 insertions(+), 40 deletions(-) create mode 100755 .github/scripts/set-workspace-version.sh diff --git a/.github/scripts/set-workspace-version.sh b/.github/scripts/set-workspace-version.sh new file mode 100755 index 0000000..e2eb27e --- /dev/null +++ b/.github/scripts/set-workspace-version.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Rewrites the workspace version and refreshes Cargo.lock, then asserts the +# result. Called twice by the publish workflow: once on the tagged checkout +# before publishing, once on main to record the released version. +# +# Usage: set-workspace-version.sh +set -euo pipefail + +VERSION="${1:?usage: set-workspace-version.sh }" + +# Version is centralized in [workspace.package]; both crates inherit it via +# `version.workspace = true`. Written as `sed > tmp && mv` rather than `sed -i`, +# whose syntax differs between GNU and BSD, so the command exercised locally on +# macOS is the one that runs on the Ubuntu runner. +sed -E '/^\[workspace\.package\]/,/^\[workspace\.dependencies\]/ s/^version = "[^"]*"/version = "'"$VERSION"'"/' \ + Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml + +# The internal pin must move in lockstep: sqlx-gen depends on sqlx-gen-macros by +# version and both crates are always released together, so leaving it behind +# would publish a sqlx-gen requiring a macros version that never shipped. +sed -E 's|^(sqlx-gen-macros = \{ path = "crates/sqlx_gen_macros", version = )"[^"]*"|\1"'"$VERSION"'"|' \ + Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml + +cargo update --workspace + +# Neither sed fails loudly if its pattern stops matching, so assert on the +# resolved metadata: without this, a drifted manifest layout would silently +# republish the previous version. +failed=0 +for pkg in sqlx-gen sqlx-gen-macros; do + got=$(cargo metadata --no-deps --format-version 1 \ + | jq -r --arg p "$pkg" '.packages[] | select(.name==$p) | .version') + echo "$pkg: $got" + if [ "$got" != "$VERSION" ]; then + echo "::error::$pkg is at $got, expected $VERSION" + failed=1 + fi +done + +# Both package versions above are inherited from [workspace.package], so they +# would still read correct if only the internal pin stopped being rewritten. +# That drift is invisible to the loop and must be asserted separately. +req=$(cargo metadata --no-deps --format-version 1 \ + | jq -r '.packages[] | select(.name=="sqlx-gen") | .dependencies[] + | select(.name=="sqlx-gen-macros") | .req') +echo "sqlx-gen -> sqlx-gen-macros: $req" +if [ "$req" != "^$VERSION" ]; then + echo "::error::sqlx-gen requires sqlx-gen-macros $req, expected ^$VERSION" + failed=1 +fi + +exit $failed diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 67cc075..d0a68ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,53 +8,26 @@ on: jobs: publish: runs-on: ubuntu-latest + # Needed by the final step, which records the released version on main. + permissions: + contents: write steps: + # fetch-depth: 0 so main is available locally for the commit-back step — + # a tag push otherwise checks out a detached, shallow history. - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: dtolnay/rust-toolchain@stable + # The tag is the single source of truth for a release: the manifest is + # rewritten from it rather than bumped by hand beforehand, so a release + # can no longer fail on a tag/manifest mismatch. - name: Apply tag version to workspace - run: | - # The tag is the single source of truth for a release: the manifest is - # rewritten from it here rather than bumped by hand beforehand, so a - # release can no longer fail on a tag/manifest mismatch. - TAG_VERSION=${GITHUB_REF#refs/tags/} - echo "Releasing $TAG_VERSION" - - # Version is centralized in [workspace.package]; both crates inherit it - # via `version.workspace = true`. Written as `sed > tmp && mv` rather - # than `sed -i`, whose syntax differs between GNU and BSD — this form - # is the one exercised by the local test. - sed -E '/^\[workspace\.package\]/,/^\[workspace\.dependencies\]/ s/^version = "[^"]*"/version = "'"$TAG_VERSION"'"/' \ - Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml - - # The internal pin must move in lockstep: sqlx-gen depends on - # sqlx-gen-macros by version, and both crates are always released - # together. Leaving it behind would publish a sqlx-gen requiring a - # macros version that never shipped. - sed -E 's|^(sqlx-gen-macros = \{ path = "crates/sqlx_gen_macros", version = )"[^"]*"|\1"'"$TAG_VERSION"'"|' \ - Cargo.toml > Cargo.toml.tmp && mv Cargo.toml.tmp Cargo.toml - - cargo update --workspace - - - name: Verify the version was applied - run: | - # Neither sed fails loudly if its pattern stops matching, so assert on - # the resolved metadata: without this, a drifted manifest layout would - # silently republish the previous version. - TAG_VERSION=${GITHUB_REF#refs/tags/} - for pkg in sqlx-gen sqlx-gen-macros; do - GOT=$(cargo metadata --no-deps --format-version 1 \ - | jq -r --arg p "$pkg" '.packages[] | select(.name==$p) | .version') - echo "$pkg: $GOT" - if [ "$GOT" != "$TAG_VERSION" ]; then - echo "::error::$pkg is at $GOT, expected $TAG_VERSION" - exit 1 - fi - done + run: .github/scripts/set-workspace-version.sh "${GITHUB_REF#refs/tags/}" - # --allow-dirty: the version above is applied in the runner and never - # committed, so the working tree is intentionally dirty at this point. + # --allow-dirty: the version above is applied in the runner and is not + # committed until the release has actually succeeded. - name: Publish sqlx-gen-macros run: cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }} --package sqlx-gen-macros @@ -63,3 +36,34 @@ jobs: - name: Publish sqlx-gen run: cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }} --package sqlx-gen + + # Deliberately last: a commit on main must never announce a release that + # failed to publish. Pushes made with GITHUB_TOKEN do not trigger further + # workflow runs, so this cannot loop back into a publish. + - name: Record the released version on main + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/} + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # The tag checkout is detached and may sit behind main, so re-apply the + # version on top of main's current state instead of reusing the dirty + # tree left by the publish steps (-f discards it). + git fetch origin main + git checkout -f -B main origin/main + .github/scripts/set-workspace-version.sh "$TAG_VERSION" + + # Re-running a release, or tagging a commit whose version already + # matches, must not fail on an empty commit. + if git diff --quiet -- Cargo.toml Cargo.lock; then + echo "main already records $TAG_VERSION, nothing to commit" + exit 0 + fi + + git add Cargo.toml Cargo.lock + git commit -m "deploy: release $TAG_VERSION" + + # If this push loses a race with a concurrent push to main, the crates + # are already published: re-run this step, or bump main by hand. + git push origin main