release #34
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: release | |
| # Publish to crates.io. | |
| # | |
| # Triggered by: | |
| # - a GitHub Release published event (release-please drives this via | |
| # PAT, so this fires automatically on merge of the release PR), or | |
| # - a manual workflow_dispatch with an explicit existing tag (for | |
| # re-runs or emergency releases). | |
| # | |
| # Note: the `push: tags:` trigger was removed because release-please | |
| # now uses a PAT (RELEASE_PLEASE_TOKEN) that fires both `release: | |
| # published` AND `push: tags:` events. Keeping both caused duplicate | |
| # runs. For emergency releases that bypass release-please, use | |
| # workflow_dispatch. | |
| # | |
| # Stable tags (v0.1.0) mark the GitHub release as latest; pre-release | |
| # tags (v0.1.0-rc.1) mark it as prerelease so it doesn't show as | |
| # "Latest release" on the repo home. | |
| # | |
| # Publishing is destructive — version numbers on crates.io are | |
| # permanent. The verify job confirms CI already passed on the tagged | |
| # SHA via the check-runs API (rather than re-running the full test | |
| # matrix), then publishes crates strictly in dependency order with a | |
| # small settle delay between each so the index is consistent for | |
| # downstream crates. See npm-build-publish.yml for the same polling | |
| # pattern. | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Tag to release (e.g. v0.1.0-rc.1). Must already exist." | |
| required: true | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| verify: | |
| if: github.repository == 'tableau/hyper-api-rust' | |
| name: verify | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 35 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.inputs.tag || github.event.release.tag_name || github.ref }} | |
| - name: Wait for CI to pass | |
| # The full test matrix already ran on the merge commit before | |
| # release-please tagged it. Re-running it here was ~20 minutes | |
| # of redundant CI per release. Instead, poll the check-runs | |
| # API for the tag's SHA and verify all required checks are | |
| # green. Mirrors the verify-ci pattern in npm-build-publish.yml. | |
| # | |
| # Risk: a maintainer who runs workflow_dispatch with a tag on | |
| # a SHA that never had CI would skip all test coverage. Guard | |
| # against that by requiring TOTAL > 0 — if no required check | |
| # ever ran on this SHA, abort. | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| SHA=$(git rev-parse HEAD) | |
| echo "Waiting for required CI check-runs on $SHA" | |
| MAX_ATTEMPTS=30 | |
| SLEEP_SECONDS=60 | |
| # Exact-name membership against the canonical check-run list. | |
| # We previously used a regex via `test()` here, but the names | |
| # contain `(` and `+` which both have regex meaning — escaping | |
| # them through bash → jq → Oniguruma was fragile and broke | |
| # silently on the v0.2.2 release. Exact-string match avoids | |
| # the whole class of regex-escaping bugs. | |
| REQUIRED_JSON='[ | |
| "rustfmt","clippy","cargo-audit","cargo-deny", | |
| "version consistency","publish dry-run", | |
| "test (ubuntu-latest)","test (macos-14)","test (windows-latest)", | |
| "hyperdb-api-node (build + smoke)" | |
| ]' | |
| for i in $(seq 1 "$MAX_ATTEMPTS"); do | |
| RUNS=$(gh api "repos/$REPO/commits/$SHA/check-runs?per_page=100" \ | |
| --jq "[.check_runs[] | select(.name as \$n | $REQUIRED_JSON | index(\$n))]") | |
| TOTAL=$(echo "$RUNS" | jq 'length') | |
| COMPLETED=$(echo "$RUNS" | jq '[.[] | select(.status=="completed")] | length') | |
| FAILED=$(echo "$RUNS" | jq '[.[] | select(.status=="completed" and (.conclusion!="success" and .conclusion!="skipped" and .conclusion!="neutral"))] | length') | |
| echo "Attempt $i/$MAX_ATTEMPTS: total=$TOTAL completed=$COMPLETED failed=$FAILED" | |
| if [[ "$FAILED" -gt 0 ]]; then | |
| echo "::error::CI failed for $SHA. Failing check-runs:" | |
| echo "$RUNS" | jq -r '.[] | select(.status=="completed" and (.conclusion!="success" and .conclusion!="skipped" and .conclusion!="neutral")) | " - \(.name): \(.conclusion)"' | |
| exit 1 | |
| fi | |
| if [[ "$TOTAL" -gt 0 && "$COMPLETED" == "$TOTAL" ]]; then | |
| echo "CI passed ($TOTAL required check-runs completed successfully)." | |
| exit 0 | |
| fi | |
| echo "CI still pending, waiting ${SLEEP_SECONDS}s..." | |
| sleep "$SLEEP_SECONDS" | |
| done | |
| echo "::error::Timed out waiting for CI to pass ($SHA)." | |
| exit 1 | |
| - name: Install system libraries | |
| # `mold` is required because `.cargo/config.toml` pins | |
| # `linker = "clang"` + `link-arg=-fuse-ld=mold` for | |
| # `x86_64-unknown-linux-gnu`. The `publish` job below already | |
| # installs it; the `verify` job missed it, which broke the | |
| # v0.2.3 release.yml run with `clang: error: invalid linker | |
| # name in argument '-fuse-ld=mold'` while the verify-step's | |
| # `cargo run --release -p hyperdb-bootstrap -- verify` | |
| # tried to compile build scripts. Keeping the package list | |
| # in sync with the publish job below. | |
| run: sudo apt-get update -q && sudo apt-get install -y mold protobuf-compiler | |
| - uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| cache-key: release-verify | |
| rustflags: "" | |
| - name: Verify pinned hyperd release URLs still resolve | |
| # Standalone safety check: HEAD each platform's pinned hyperd | |
| # download URL right before publishing, in case Tableau rotated | |
| # something between merge time and tag time. Cheap (~1 min). | |
| run: cargo run --release -p hyperdb-bootstrap --bin hyperdb-bootstrap -- verify | |
| publish: | |
| name: publish to crates.io | |
| needs: verify | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.inputs.tag || github.event.release.tag_name || github.ref }} | |
| - name: Install system libraries | |
| run: sudo apt-get update -q && sudo apt-get install -y libfontconfig1-dev mold protobuf-compiler | |
| - uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| cache-key: release-publish | |
| rustflags: "" | |
| - name: Resolve tag name | |
| id: tag | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| INPUT_TAG: ${{ github.event.inputs.tag }} | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${INPUT_TAG:-${RELEASE_TAG:-$REF_NAME}}" | |
| # Defense in depth: tags coming from workflow_dispatch are | |
| # user-supplied. Enforce a strict `vX.Y.Z` / `vX.Y.Z-rc.N` | |
| # shape before letting the name flow into cargo/git commands. | |
| if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(rc|alpha|beta)\.[0-9]+)?$ ]]; then | |
| echo "::error::Invalid tag name: $TAG (expected vX.Y.Z or vX.Y.Z-rc.N)" >&2 | |
| exit 1 | |
| fi | |
| echo "name=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" | |
| - name: Confirm tag matches workspace version | |
| # All publishable crates are in lockstep. Use hyperdb-api-core as | |
| # the bellwether (it's the foundation every other crate depends on). | |
| # hyperdb-compile-check is outside the workspace but must also be | |
| # in lockstep — check its version separately via its own Cargo.toml. | |
| env: | |
| EXPECTED: ${{ steps.tag.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| ACTUAL=$(cargo metadata --no-deps --format-version 1 \ | |
| | jq -r '.packages[] | select(.name=="hyperdb-api-core") | .version') | |
| if [[ "$EXPECTED" != "$ACTUAL" ]]; then | |
| echo "::error::Tag version ($EXPECTED) does not match hyperdb-api-core Cargo.toml ($ACTUAL). Bump all workspace Cargo.tomls to match the tag before releasing." >&2 | |
| exit 1 | |
| fi | |
| # hyperdb-compile-check lives outside the workspace; check its | |
| # version via cargo metadata targeted at its own Cargo.toml. | |
| CC_ACTUAL=$(cargo metadata --no-deps --format-version 1 \ | |
| --manifest-path hyperdb-compile-check/Cargo.toml \ | |
| | jq -r '.packages[] | select(.name=="hyperdb-compile-check") | .version') | |
| if [[ "$EXPECTED" != "$CC_ACTUAL" ]]; then | |
| echo "::error::Tag version ($EXPECTED) does not match hyperdb-compile-check/Cargo.toml ($CC_ACTUAL). Bump it to match before releasing." >&2 | |
| exit 1 | |
| fi | |
| - name: Publish in dependency order | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| publish() { | |
| local crate="$1" | |
| echo "::group::Publishing $crate" | |
| if ! cargo publish -p "$crate" 2>&1 | tee /tmp/publish_out; then | |
| if grep -q "already exists on" /tmp/publish_out; then | |
| echo "::warning::$crate already published — skipping" | |
| else | |
| echo "::endgroup::" | |
| return 1 | |
| fi | |
| fi | |
| echo "::endgroup::" | |
| # crates.io index propagation: downstream crates' own | |
| # `cargo publish` verification step resolves their deps | |
| # against the live index. 45s is the empirically-safe | |
| # window for small crates. | |
| sleep 45 | |
| } | |
| # Publish order — strict topological sort of the version-pinned | |
| # sibling deps. `cargo publish` resolves EVERY sibling dep that | |
| # carries a version requirement against the live crates.io index | |
| # (in any section, including [dev-dependencies] and optional deps), | |
| # so each crate must be published only after the crates it pins are | |
| # already on the index. Path-only deps (no `version = `) are resolved | |
| # locally and impose no ordering — that's why hyperdb-api's dev-dep on | |
| # hyperdb-api-derive (and vice-versa) is path-only, which is what lets | |
| # this be a DAG rather than a cycle. | |
| # | |
| # Version-pinned sibling edges (X needs Y published first): | |
| # hyperdb-api-core → hyperdb-api-salesforce (optional) | |
| # hyperdb-api → hyperdb-api-core | |
| # hyperdb-compile-check → hyperdb-api | |
| # hyperdb-api-derive → hyperdb-compile-check (optional) | |
| # hyperdb-mcp → hyperdb-api | |
| # (hyperdb-api-salesforce, hyperdb-bootstrap, sea-query-hyperdb: | |
| # no version-pinned sibling deps) | |
| # | |
| # Resulting order: | |
| # salesforce → core → api → compile-check → derive → mcp | |
| # → bootstrap → sea-query | |
| publish hyperdb-api-salesforce | |
| publish hyperdb-api-core | |
| publish hyperdb-api | |
| # hyperdb-compile-check depends on hyperdb-api (runtime) and is outside | |
| # the workspace; publish via manifest path after hyperdb-api is indexed. | |
| # Must come BEFORE hyperdb-api-derive, which optionally depends on it. | |
| # | |
| # --allow-dirty: hyperdb-compile-check is its OWN workspace with its own | |
| # committed Cargo.lock. Publishing hyperdb-api to crates.io moments | |
| # earlier means cargo re-resolves this crate's lockfile against the live | |
| # index during packaging, leaving hyperdb-compile-check/Cargo.lock | |
| # modified in the working tree. Without --allow-dirty, `cargo publish` | |
| # aborts on the dirty lockfile. (The root-workspace crates don't hit | |
| # this — they share the root Cargo.lock that release-please already | |
| # synced. Only this out-of-workspace crate regenerates its own lock.) | |
| echo "::group::Publishing hyperdb-compile-check" | |
| if ! cargo publish --allow-dirty --manifest-path hyperdb-compile-check/Cargo.toml 2>&1 | tee /tmp/publish_out; then | |
| if grep -q "already exists on" /tmp/publish_out; then | |
| echo "::warning::hyperdb-compile-check already published — skipping" | |
| else | |
| echo "::endgroup::" | |
| exit 1 | |
| fi | |
| fi | |
| echo "::endgroup::" | |
| sleep 45 | |
| # hyperdb-api-derive optionally depends on hyperdb-compile-check (now | |
| # indexed above), so it must publish after it. | |
| publish hyperdb-api-derive | |
| publish hyperdb-mcp | |
| publish hyperdb-bootstrap | |
| publish sea-query-hyperdb | |
| - name: Create GitHub release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.name }} | |
| prerelease: ${{ contains(steps.tag.outputs.name, '-rc.') || contains(steps.tag.outputs.name, '-alpha.') || contains(steps.tag.outputs.name, '-beta.') }} | |
| generate_release_notes: true |