# 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." # Future manual publication targets go here as independent, idempotent jobs, # e.g.: # scoop-bucket: { ... update the lazyeval Scoop bucket manifest ... } # homebrew-tap: { ... update the lazyeval Homebrew formula ... } # winget: { ... komac submit, or a manual PR helper ... } # No `needs:` between them — each checks the target and skips if already done.