name: Publish
on:
workflow_dispatch:
pull_request:
types: [closed]
branches: [main]
permissions:
contents: write
issues: write
pull-requests: write
actions: write
concurrency:
group: publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 15
if: >-
github.event_name == 'workflow_dispatch' || (
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'release') &&
github.event.pull_request.user.login == 'github-actions[bot]'
)
steps:
- name: Clone repository
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Verify CI passed on the release PR
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD="${{ github.event.pull_request.head.sha }}"
REPO="${{ github.repository }}"
THIS_RUN_ID="${{ github.run_id }}"
echo "Verifying CI on PR #$PR_NUMBER (head: $PR_HEAD)..."
# Pull check-runs for the PR head commit, excluding publish checks.
# A commit can have historical check-runs from previous attempts, so
# keep only the latest run for each check name.
# Returns one line per check: "<conclusion> <name>". Conclusions:
# success / failure / cancelled / timed_out / skipped / neutral /
# action_required / null (running).
CHECKS=$(gh api \
"repos/$REPO/commits/$PR_HEAD/check-runs?per_page=100" \
--jq ".check_runs
| map(select(.name != \"publish\"))
| map(select(((.details_url // \"\") | contains(\"/actions/runs/$THIS_RUN_ID/\")) | not))
| map({
name,
conclusion: (.conclusion // \"running\"),
timestamp: (.completed_at // .started_at // \"\")
})
| sort_by(.name, .timestamp)
| group_by(.name)
| map(last)
| .[]
| \"\(.conclusion) \(.name)\"")
if [[ -z "$CHECKS" ]]; then
echo "::error::No CI checks found for PR #$PR_NUMBER head $PR_HEAD."
echo "::error::CI must run on release PRs before merge."
exit 1
fi
echo "Latest checks (excluding publish runs):"
echo "$CHECKS" | sed 's/^/ /'
# Any non-green conclusion is a hard refusal. `skipped` is OK
# (some matrix jobs may legitimately skip); `neutral` is OK
# (advisory checks).
BAD=$(echo "$CHECKS" | awk '$1 != "success" && $1 != "skipped" && $1 != "neutral"' || true)
if [[ -n "$BAD" ]]; then
echo "::error::PR #$PR_NUMBER has non-green checks:"
echo "$BAD" | sed 's/^/::error:: /'
exit 1
fi
for required in "Lint" "Documentation" "MSRV Check"; do
if ! echo "$CHECKS" | awk -v required="$required" '$1 == "success" { $1=""; sub(/^ /, ""); if ($0 == required) found = 1 } END { exit !found }'; then
echo "::error::Missing successful required check: $required"
exit 1
fi
done
if ! echo "$CHECKS" | awk '$1 == "success" && $0 ~ /test::sqlite \(/ && $0 !~ /\$\{\{/ { found = 1 } END { exit !found }'; then
echo "::error::Missing successful SQLite test matrix check."
exit 1
fi
if ! echo "$CHECKS" | awk '$1 == "success" && $0 ~ /test::pg \(/ && $0 !~ /\$\{\{/ { found = 1 } END { exit !found }'; then
echo "::error::Missing successful PostgreSQL test matrix check."
exit 1
fi
echo "All checks on PR #$PR_NUMBER are green — proceeding to publish."
- name: Get Rust version
id: rust-version
run: |
RUST_VERSION=$(grep '^rust-version = ' Cargo.toml | sed 's/rust-version = "\(.*\)"/\1/')
echo "version=$RUST_VERSION" >> $GITHUB_OUTPUT
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ steps.rust-version.outputs.version }}
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: msrv
- name: Install release-plz
uses: taiki-e/install-action@v2
with:
tool: release-plz
- name: Capture pre-release tag
id: pre
run: |
PRE_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo none)
echo "tag=$PRE_TAG" >> $GITHUB_OUTPUT
echo "Pre-release latest tag: $PRE_TAG"
- name: Run release-plz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
release-plz release --git-token "$GITHUB_TOKEN" || {
echo "::error::release-plz release failed - some crates may have been partially published"
exit 1
}
- name: Trigger binary release
if: success()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Fetch tags release-plz just pushed
git fetch --tags --force
NEW_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo none)
echo "Post-release latest tag: $NEW_TAG"
if [[ "$NEW_TAG" == "${{ steps.pre.outputs.tag }}" ]]; then
echo "No new tag created — skipping binary release."
exit 0
fi
if [[ "$NEW_TAG" != v[0-9]* ]]; then
echo "Latest tag '$NEW_TAG' is not a versioned release — skipping."
exit 0
fi
echo "Dispatching release.yml for $NEW_TAG"
gh workflow run release.yml --ref "$NEW_TAG"
- name: Summary
run: echo "Successfully published crates to crates.io" >> $GITHUB_STEP_SUMMARY