6d54c1e96c
Add sibling publish.yaml jobs (scoop-bucket, homebrew-tap) that render a manifest from the release .sha256 sidecars and idempotently push it to the org-level lazyeval/scoop-bucket and lazyeval/homebrew-tap repos, using the scoped lazyeval-ci bot token (LAZYEVAL_PKG_TOKEN). Render logic lives in dependency-free bash (the CI image has no jq/ruby): scripts/render-scoop-manifest.sh and scripts/render-homebrew-formula.sh. scripts/test-package-renders.sh exercises both: it validates the Scoop JSON with node and asserts fields on both manifests, and additionally runs `ruby -c` on the formula where ruby is present (dev box), skipping it gracefully otherwise. A new ci.yaml `manifests` job runs that test on every push so a render regression surfaces immediately, not at the next manual publish dispatch. The CI image has no ruby, so in CI the gate covers the Scoop JSON (node) and field assertions for both manifests; the formula's Ruby syntax is checked dev-side only (the static heredoc's variable parts cannot introduce syntax errors). - Scoop: x64 (gnu) + arm64 (gnullvm); #/-rename fragment so the bin shim is version-stable; checkver, no autoupdate (the pipeline is the updater). - Homebrew: on_macos/on_linux x arch bare-binary formula; no Windows. Docs: ADR-0056 Amendment 2 (+ README index, requirements D3). Unverified pending real use: scoop/brew install, the HEAD:main branch assumption, macOS Gatekeeper-via-brew on the ad-hoc-signed binary.
204 lines
9.5 KiB
YAML
204 lines
9.5 KiB
YAML
# 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.
|