# Manual publication workflow (workflow_dispatch) — the outward, irreversible # release steps a human triggers AFTER the automated `release.yaml` build has # produced downloadable assets (and they've been eyeballed as good). # # Why manual + separate from release.yaml: # * Publishing to a public registry is irreversible (crates.io versions can # only be *yanked*, never deleted) — a human pulls this lever, and the # registry token never sits on every tag push. # * Our release is split (Linux/Windows on the tag, macOS dispatched), so a # human is the natural "all assets are up — go" gate. crates.io publish # reads SOURCE so it doesn't strictly need the release, but binstall's # metadata points at the release assets — hence run this once builds exist. # # Structure: each registry is its OWN job with NO inter-job `needs`, so jobs run # independently and one failing (or a newly-added one) never breaks another. # Every job is IDEMPOTENT — re-dispatching when a target is already published is # a clean no-op. Add Scoop / Homebrew / winget as sibling jobs here later. name: publish on: workflow_dispatch: inputs: tag: description: 'Release tag to publish (e.g. v0.2.0)' required: true jobs: crates-io: runs-on: ci-public container: image: git.lazyeval.net/oli/rdbms-playground-ci:latest steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.tag }} - name: publish to crates.io (idempotent) shell: bash env: TAG: ${{ inputs.tag }} # A crate-scoped, publish-update crates.io token, stored as a Gitea # Actions secret. `cargo publish` reads CARGO_REGISTRY_TOKEN from env. CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: | set -euo pipefail # Source of truth = the [package] version at the checked-out tag # (toolchain-free read; same approach as release.yaml's guard, which # avoids the flake devShell's stdout banner corrupting a parse). VER=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/^version = "(.*)"/\1/') [ -n "$VER" ] || { echo "ERROR: could not read version from Cargo.toml" >&2; exit 1; } if [ "$TAG" != "v$VER" ]; then echo "ERROR: dispatch tag '$TAG' != 'v$VER' (Cargo.toml at that tag)" >&2 exit 1 fi # Idempotency: if this version is already on crates.io, no-op. # (crates.io requires a descriptive User-Agent per its data policy; # without one the API returns 403.) Only an explicit 200 means # "already there" — anything else proceeds, and `cargo publish` is the # final backstop (it refuses to overwrite an existing version). UA="rdbms-playground-release-ci (oliver@sturmnet.org)" code=$(curl -sS -o /dev/null -w '%{http_code}' -A "$UA" \ "https://crates.io/api/v1/crates/rdbms-playground/$VER" || echo 000) if [ "$code" = "200" ]; then echo "rdbms-playground $VER is already on crates.io — nothing to do." exit 0 fi echo "crates.io returned HTTP $code for $VER (not 200) — proceeding to publish." echo "publishing rdbms-playground $VER to crates.io ..." nix develop -c cargo publish --locked echo "published rdbms-playground $VER to crates.io." # Update the lazyeval Scoop bucket (Windows). Renders the manifest from the # release's .sha256 sidecars and commits it to lazyeval/scoop-bucket. Pushes # with the lazyeval-ci bot token (LAZYEVAL_PKG_TOKEN), which is scoped — via # the bot's org-team membership — to the lazyeval package repos only, so a # leak cannot touch oli/rdbms-playground. scoop-bucket: runs-on: ci-public container: image: git.lazyeval.net/oli/rdbms-playground-ci:latest steps: - uses: actions/checkout@v4 # default ref (main) — current render script - name: update the lazyeval Scoop bucket (idempotent) shell: bash env: TAG: ${{ inputs.tag }} # Passed via env, never inlined into the script, so the value stays # masked in logs; it only materialises in the clone URL at runtime. PKG_TOKEN: ${{ secrets.LAZYEVAL_PKG_TOKEN }} run: | set -euo pipefail VER="${TAG#v}" echo "scoop: targeting rdbms-playground $VER ($TAG)" base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG" fetch_hash() { local asset="$1" line echo "scoop: fetching $asset.sha256" >&2 line=$(curl -fsSL "$base/$asset.sha256") \ || { echo "ERROR: cannot fetch $asset.sha256 — is $TAG released with assets?" >&2; exit 1; } # First whitespace-delimited field is the hash. `read` is a bash # builtin (no awk, which the slim CI image may lack). local hash _ read -r hash _ <<<"$line" printf '%s' "$hash" } h_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-pc-windows-gnu.exe") h_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-pc-windows-gnullvm.exe") echo "scoop: rendering manifest" bash scripts/render-scoop-manifest.sh "$VER" "$h_x64" "$h_arm" > /tmp/rdbms-playground.json node -e 'JSON.parse(require("fs").readFileSync("/tmp/rdbms-playground.json","utf8"))' \ || { echo "ERROR: rendered Scoop manifest is not valid JSON" >&2; exit 1; } work=$(mktemp -d) echo "scoop: cloning lazyeval/scoop-bucket" git clone --depth 1 "https://lazyeval-ci:${PKG_TOKEN}@git.lazyeval.net/lazyeval/scoop-bucket.git" "$work" cp /tmp/rdbms-playground.json "$work/rdbms-playground.json" cd "$work" git config user.name "lazyeval-ci" git config user.email "ci@lazyeval.net" git add rdbms-playground.json if git diff --cached --quiet; then echo "scoop: manifest already at $VER — nothing to commit." exit 0 fi git commit -m "rdbms-playground $VER" # Push to main explicitly: a freshly-created (empty) repo clone may put # the first commit on a differently-named local branch. Assumes the # bucket/tap default branch is `main` (Gitea's default for new repos). git push origin HEAD:main echo "scoop: bucket updated to rdbms-playground $VER." # Update the lazyeval Homebrew tap (macOS + Linux). Same shape as scoop-bucket; # writes Formula/rdbms-playground.rb into lazyeval/homebrew-tap. homebrew-tap: runs-on: ci-public container: image: git.lazyeval.net/oli/rdbms-playground-ci:latest steps: - uses: actions/checkout@v4 - name: update the lazyeval Homebrew tap (idempotent) shell: bash env: TAG: ${{ inputs.tag }} PKG_TOKEN: ${{ secrets.LAZYEVAL_PKG_TOKEN }} run: | set -euo pipefail VER="${TAG#v}" echo "homebrew: targeting rdbms-playground $VER ($TAG)" base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG" fetch_hash() { local asset="$1" line echo "homebrew: fetching $asset.sha256" >&2 line=$(curl -fsSL "$base/$asset.sha256") \ || { echo "ERROR: cannot fetch $asset.sha256 — is $TAG released with assets?" >&2; exit 1; } # First whitespace-delimited field is the hash. `read` is a bash # builtin (no awk, which the slim CI image may lack). local hash _ read -r hash _ <<<"$line" printf '%s' "$hash" } mac_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-apple-darwin") mac_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-apple-darwin") lin_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-unknown-linux-musl") lin_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-unknown-linux-musl") echo "homebrew: rendering formula" bash scripts/render-homebrew-formula.sh "$VER" "$mac_arm" "$mac_x64" "$lin_arm" "$lin_x64" \ > /tmp/rdbms-playground.rb grep -q '^class RdbmsPlayground < Formula$' /tmp/rdbms-playground.rb \ || { echo "ERROR: rendered formula looks malformed" >&2; exit 1; } work=$(mktemp -d) echo "homebrew: cloning lazyeval/homebrew-tap" git clone --depth 1 "https://lazyeval-ci:${PKG_TOKEN}@git.lazyeval.net/lazyeval/homebrew-tap.git" "$work" mkdir -p "$work/Formula" cp /tmp/rdbms-playground.rb "$work/Formula/rdbms-playground.rb" cd "$work" git config user.name "lazyeval-ci" git config user.email "ci@lazyeval.net" git add Formula/rdbms-playground.rb if git diff --cached --quiet; then echo "homebrew: formula already at $VER — nothing to commit." exit 0 fi git commit -m "rdbms-playground $VER" # Push to main explicitly: a freshly-created (empty) repo clone may put # the first commit on a differently-named local branch. Assumes the # bucket/tap default branch is `main` (Gitea's default for new repos). git push origin HEAD:main echo "homebrew: tap updated to rdbms-playground $VER." # winget remains a future sibling job here (komac on Linux CI, or a manual PR # to microsoft/winget-pkgs). No `needs:` between jobs — each is independent and # idempotent, so one failing or being added never breaks another.