Skip to content

release

release #34

Workflow file for this run

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