diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml new file mode 100644 index 0000000..61bed46 --- /dev/null +++ b/.gitea/workflows/publish.yaml @@ -0,0 +1,79 @@ +# 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.