Add the 4th publish.yaml sibling job: opens a PR to microsoft/winget-pkgs with komac (pinned 2.16.0 prebuilt) for LazyEvaluation.RdbmsPlayground (portable, x64 + arm64). Unlike scoop/homebrew it's a PR into Microsoft's central, human-gated catalog - async and validated on their side. - Auth: a classic public_repo GitHub PAT on a dedicated bot account (lazyeval-ci; fine-grained tokens can't open the cross-fork PR - komac #310), as the WINGET_GITHUB_TOKEN secret, job-scoped and passed to the API guards via a 0600 curl config file (never argv). - Idempotent via two guards before submitting (already-merged version + already-open PR) so a repeated publish dispatch can't open a duplicate. - No actions/checkout (komac works off URLs + the GitHub API). Docs: ADR-0056 Amendment 4 - the model, the one-time manual `komac new` bootstrap recipe (flags verified vs komac 2.16.0), and first-run learnings: the fork must pre-exist, and a direct single-exe portable takes its PATH alias from Commands[0] (not PortableCommandAlias, which is nested-only). Plus README index + requirements D3. Wiring only; going live needs the bootstrap PR (#391335, submitted) to merge.
285 lines
14 KiB
YAML
285 lines
14 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."
|
|
|
|
# Update the winget package (Windows) by opening a PR to microsoft/winget-pkgs
|
|
# with komac. Unlike scoop-bucket/homebrew-tap (which push to OUR repos and are
|
|
# live at once), winget is a PR into Microsoft's central, human-gated catalog —
|
|
# asynchronous, and re-submitting the same version would open a DUPLICATE PR. So
|
|
# this job guards on both already-merged versions AND already-open PRs before
|
|
# submitting, which keeps a repeated `publish` dispatch safe.
|
|
#
|
|
# Auth: komac needs a CLASSIC GitHub PAT with `public_repo` (fine-grained tokens
|
|
# cannot open the cross-fork PR — komac #310). It is held on a dedicated GitHub
|
|
# bot account (a leak can't reach other repos — same reasoning as lazyeval-ci)
|
|
# and referenced ONLY in this job's env (job-level secret scoping keeps it out
|
|
# of the other publish jobs).
|
|
#
|
|
# PREREQUISITE: the package `LazyEvaluation.RdbmsPlayground` must already exist
|
|
# in winget-pkgs via a one-time `komac new` (interactive — run manually once;
|
|
# see ADR-0056 Amendment 4). This job only does the per-release `komac update`.
|
|
winget:
|
|
runs-on: ci-public
|
|
container:
|
|
image: git.lazyeval.net/oli/rdbms-playground-ci:latest
|
|
steps:
|
|
- name: submit the winget update PR (idempotent)
|
|
shell: bash
|
|
env:
|
|
TAG: ${{ inputs.tag }}
|
|
# Classic public_repo PAT for the winget bot account. komac reads it
|
|
# from GITHUB_TOKEN; the API guards below read it via a curl config file
|
|
# so the token never appears in a command line / process list.
|
|
GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
VER="${TAG#v}"
|
|
PKG="LazyEvaluation.RdbmsPlayground"
|
|
echo "winget: targeting $PKG $VER ($TAG)"
|
|
|
|
# Auth header in a 0600 curl config (keeps the token out of argv/logs).
|
|
umask 077
|
|
printf 'header = "Authorization: Bearer %s"\nheader = "Accept: application/vnd.github+json"\n' \
|
|
"$GITHUB_TOKEN" > /tmp/gh-curlrc
|
|
api="https://api.github.com"
|
|
|
|
# Guard 1 — already merged into winget-pkgs?
|
|
# (manifests/<first-letter>/<Publisher>/<Package>/<version>)
|
|
merged=$(curl -sS -o /dev/null -w '%{http_code}' --config /tmp/gh-curlrc \
|
|
"$api/repos/microsoft/winget-pkgs/contents/manifests/l/LazyEvaluation/RdbmsPlayground/$VER" || echo 000)
|
|
if [ "$merged" = "200" ]; then
|
|
echo "winget: $PKG $VER already in winget-pkgs — nothing to do."
|
|
exit 0
|
|
fi
|
|
|
|
# Guard 2 — an open PR for this exact id+version already? (avoid a dup)
|
|
# curl -G --data-urlencode does the URL-encoding (no jq in the image).
|
|
curl -sS -G --config /tmp/gh-curlrc \
|
|
--data-urlencode "q=repo:microsoft/winget-pkgs type:pr state:open in:title \"$PKG\" \"$VER\"" \
|
|
"$api/search/issues" -o /tmp/winget-search.json
|
|
open=$(node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync("/tmp/winget-search.json","utf8")).total_count||0))')
|
|
if [ "$open" != "0" ]; then
|
|
echo "winget: an open PR for $PKG $VER already exists — skipping."
|
|
exit 0
|
|
fi
|
|
|
|
# Install pinned komac (prebuilt glibc binary; the CI image has no
|
|
# cargo/komac). Pinned for reproducibility — bump deliberately.
|
|
KOMAC_VER=2.16.0
|
|
curl -fsSL -o /tmp/komac.tgz \
|
|
"https://github.com/russellbanks/Komac/releases/download/v$KOMAC_VER/komac-$KOMAC_VER-x86_64-unknown-linux-gnu.tar.gz"
|
|
tar -xzf /tmp/komac.tgz -C /tmp
|
|
komac_bin=$(find /tmp -maxdepth 2 -type f -name komac | head -1)
|
|
[ -n "$komac_bin" ] || { echo "ERROR: komac binary not found after extract" >&2; exit 1; }
|
|
|
|
base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG"
|
|
echo "winget: submitting update PR via komac $KOMAC_VER"
|
|
# NB: confirm flags against `komac update --help` on first run — komac
|
|
# evolves; --version/--urls/--submit are the stable core. komac infers
|
|
# architecture + the `portable` installer type from the binaries.
|
|
"$komac_bin" update "$PKG" \
|
|
--version "$VER" \
|
|
--urls "$base/rdbms-playground-$TAG-x86_64-pc-windows-gnu.exe" \
|
|
"$base/rdbms-playground-$TAG-aarch64-pc-windows-gnullvm.exe" \
|
|
--submit
|
|
echo "winget: update PR submitted for $PKG $VER (Microsoft review is async)."
|
|
|
|
# No `needs:` between jobs — each is independent and idempotent, so one failing
|
|
# or being added never breaks another.
|