Compare commits
10
Commits
v0.2.0
...
56e3456cfc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56e3456cfc | ||
|
|
0208c67e59 | ||
|
|
6bb2288470 | ||
|
|
6d54c1e96c | ||
|
|
c0531aa048 | ||
|
|
42b40bc099 | ||
|
|
cabc8131a9 | ||
|
|
8ebe213b5d | ||
|
|
d3af1c413a | ||
|
|
3c87dbb391 |
@@ -6,6 +6,11 @@
|
|||||||
# was enabled once the tree was reformatted on main (ADR-ci-002 Amendment 1 /
|
# was enabled once the tree was reformatted on main (ADR-ci-002 Amendment 1 /
|
||||||
# issue #35). The release job (static binary for D2) and the platform matrix
|
# issue #35). The release job (static binary for D2) and the platform matrix
|
||||||
# layer on later, step by step.
|
# layer on later, step by step.
|
||||||
|
#
|
||||||
|
# A separate, lightweight `manifests` job logic-tests the package-manifest
|
||||||
|
# render scripts (Scoop/Homebrew) used by publish.yaml — bash + node only, no
|
||||||
|
# toolchain — so a render regression surfaces on the breaking push rather than
|
||||||
|
# weeks later at the next manual publish dispatch (ADR-0056 Amendment 2).
|
||||||
name: ci
|
name: ci
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -46,3 +51,21 @@ jobs:
|
|||||||
run: nix develop -c cargo clippy --all-targets -- -D warnings
|
run: nix develop -c cargo clippy --all-targets -- -D warnings
|
||||||
- name: test
|
- name: test
|
||||||
run: nix develop -c cargo test --no-fail-fast
|
run: nix develop -c cargo test --no-fail-fast
|
||||||
|
|
||||||
|
# Logic test for the package-manifest render scripts. Renders with DUMMY
|
||||||
|
# inputs and validates the output — it never publishes or touches the lazyeval
|
||||||
|
# repos (that is publish.yaml's manual job). Runs on the same image but skips
|
||||||
|
# nix: it needs only bash + node, both in the base image.
|
||||||
|
#
|
||||||
|
# NOTE: the CI image has no ruby, so the script's `ruby -c` formula syntax
|
||||||
|
# check is skipped here (it degrades gracefully); the Scoop JSON is still
|
||||||
|
# validated with node and both manifests' fields are asserted. Full formula
|
||||||
|
# syntax is checked dev-side (ruby present) on every pre-commit local run.
|
||||||
|
manifests:
|
||||||
|
runs-on: ci-public
|
||||||
|
container:
|
||||||
|
image: git.lazyeval.net/oli/rdbms-playground-ci:latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: render-script tests (Scoop + Homebrew)
|
||||||
|
run: bash scripts/test-package-renders.sh
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
# 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.
|
||||||
@@ -84,12 +84,21 @@ jobs:
|
|||||||
# The runner wipes the workspace each run, so cargo target/ never
|
# The runner wipes the workspace each run, so cargo target/ never
|
||||||
# accumulates. Bound the persistent nix store by generation: record the
|
# accumulates. Bound the persistent nix store by generation: record the
|
||||||
# current devShell as a generation of a persistent profile (in $HOME),
|
# current devShell as a generation of a persistent profile (in $HOME),
|
||||||
# keep the 2 newest, reclaim what older ones referenced.
|
# keep the 2 newest, reclaim what older ones referenced — so the
|
||||||
|
# toolchain stays *warm* across runs and only stale generations are GC'd.
|
||||||
|
#
|
||||||
|
# The profile's parent dir MUST exist first, or `nix develop --profile`
|
||||||
|
# errors ("cannot read directory …") and the profile/gc-root is never
|
||||||
|
# created — which made `nix-collect-garbage` delete the whole toolchain
|
||||||
|
# closure every run (re-downloaded ~3.8 GiB each time; the retention was
|
||||||
|
# silently broken by the swallowed `|| true`). No `|| true` on the
|
||||||
|
# profile realization now: a future breakage should fail loudly.
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "--- disk before ---"; df -h / | tail -1
|
echo "--- disk before ---"; df -h / | tail -1
|
||||||
P="$HOME/.cache/rdbms-ci/toolchain"
|
P="$HOME/.cache/rdbms-ci/toolchain"
|
||||||
nix develop --profile "$P" -c true || true
|
mkdir -p "$(dirname "$P")"
|
||||||
|
nix develop --profile "$P" -c true
|
||||||
nix-env -p "$P" --delete-generations +2 || true
|
nix-env -p "$P" --delete-generations +2 || true
|
||||||
nix-collect-garbage || true
|
nix-collect-garbage || true
|
||||||
echo "--- disk after ---"; df -h / | tail -1
|
echo "--- disk after ---"; df -h / | tail -1
|
||||||
|
|||||||
@@ -86,3 +86,234 @@ verified against cargo-binstall SUPPORT.md):
|
|||||||
- Remaining follow-up: run the real `cargo binstall` validation at the
|
- Remaining follow-up: run the real `cargo binstall` validation at the
|
||||||
first publish + matching release (the license files, © holder, and
|
first publish + matching release (the license files, © holder, and
|
||||||
CONTRIBUTING are now in place).
|
CONTRIBUTING are now in place).
|
||||||
|
|
||||||
|
## Amendment 1 — 2026-06-18: published live + a manual `publish` workflow
|
||||||
|
|
||||||
|
**`rdbms-playground 0.2.0` is published to crates.io** (`cargo install` and
|
||||||
|
`cargo binstall rdbms-playground` both verified working by the user). The
|
||||||
|
"unverified binstall" caveat is resolved — the per-target overrides
|
||||||
|
resolve correctly against the `v0.2.0` release assets.
|
||||||
|
|
||||||
|
**How publishing is wired:** a new **manual `workflow_dispatch` workflow**
|
||||||
|
(`.gitea/workflows/publish.yaml`), mirroring `release-macos.yaml`, takes a
|
||||||
|
`tag` input and runs `cargo publish` (token via the
|
||||||
|
`CARGO_REGISTRY_TOKEN` Gitea Actions secret — a crate-scoped,
|
||||||
|
publish-update token). **Not** automated on the tag, by decision: the
|
||||||
|
publish is irreversible (yank-only), keeping the registry token off every
|
||||||
|
tag push; the release is split (Linux/Windows on the tag, macOS
|
||||||
|
dispatched), so a human is the natural "all assets are up — go" gate; and
|
||||||
|
crates.io has no Gitea-Actions trusted-publishing path today, so a stored
|
||||||
|
token on the self-hosted runner would be the only automated option.
|
||||||
|
Each registry is its **own idempotent job** (no inter-job `needs`) — the
|
||||||
|
crates.io job skips cleanly if the version is already published (crates.io
|
||||||
|
API pre-check + `cargo publish` as the backstop) — so future
|
||||||
|
Scoop/Homebrew/winget jobs can be added alongside without breaking one
|
||||||
|
another or re-runs. The first such job's `tag`-vs-`Cargo.toml` guard
|
||||||
|
mirrors `release.yaml`.
|
||||||
|
|
||||||
|
## Amendment 2 — 2026-06-19: Scoop bucket + Homebrew tap (D3 §3b/§3c)
|
||||||
|
|
||||||
|
Two more package managers wired as **sibling `publish.yaml` jobs**
|
||||||
|
(`scoop-bucket`, `homebrew-tap`), following Amendment 1's independent +
|
||||||
|
idempotent pattern. Each fetches the release's `.sha256` sidecars, renders
|
||||||
|
a manifest, and commits it into a per-manager repo.
|
||||||
|
|
||||||
|
**Repos — org-level and multi-package.** Both live under a new **`lazyeval`
|
||||||
|
Gitea organisation** (created with the `oli` account, which gives the
|
||||||
|
`git.lazyeval.net/lazyeval/...` paths): `lazyeval/scoop-bucket` and
|
||||||
|
`lazyeval/homebrew-tap`. A Scoop *bucket* and a Homebrew *tap* are by
|
||||||
|
definition **collections of manifests**, so these are reusable for future
|
||||||
|
tools, not single-package repos. Homebrew's `homebrew-` repo-name prefix is
|
||||||
|
mandatory (→ referenced as `lazyeval/tap`); Scoop's bucket name is free.
|
||||||
|
Users: `scoop bucket add lazyeval <url>` (the label is local/arbitrary;
|
||||||
|
only the URL owner is real) then `scoop install rdbms-playground`; and
|
||||||
|
`brew tap lazyeval/tap https://git.lazyeval.net/lazyeval/homebrew-tap`
|
||||||
|
(the explicit-URL form — the `user/repo` shorthand assumes GitHub) then
|
||||||
|
`brew install lazyeval/tap/rdbms-playground`.
|
||||||
|
|
||||||
|
**Credential — a scoped bot user, not an `oli` PAT.** Gitea PATs scope by
|
||||||
|
**permission category, not per-repository** (`write:repository` grants
|
||||||
|
write to *every* repo the account can reach — there is no repo picker like
|
||||||
|
GitHub fine-grained PATs). So an `oli` token would also be able to push to
|
||||||
|
`oli/rdbms-playground` itself. Instead a dedicated bot user **`lazyeval-ci`**
|
||||||
|
is a member of a `lazyeval` org team with **Write** to the package repos
|
||||||
|
only; its `write:repository` PAT is therefore effectively scoped to those
|
||||||
|
repos and **cannot touch the main project repo**. Stored as the
|
||||||
|
`LAZYEVAL_PKG_TOKEN` Actions secret on `oli/rdbms-playground` (where the
|
||||||
|
workflow runs — *not* an org secret, which wouldn't reach a user-repo
|
||||||
|
workflow; *not* on the target repos, which only receive pushes). Passed via
|
||||||
|
`env:` (never inlined), so it stays masked and only materialises in the
|
||||||
|
clone URL at runtime; pushes go to `HEAD:main` (assumes the repos default
|
||||||
|
to `main`).
|
||||||
|
|
||||||
|
**Render scripts are dependency-free bash.** The CI job container is
|
||||||
|
`node:22-bookworm-slim` — **no jq, no ruby** — so
|
||||||
|
`scripts/render-{scoop-manifest,homebrew-formula}.sh` are pure bash
|
||||||
|
(heredocs, no external deps) taking a version + the relevant hashes and
|
||||||
|
emitting the manifest on stdout. `scripts/test-package-renders.sh` is their
|
||||||
|
test (JSON validated with `node` — present in the image — plus `jq`/`ruby`
|
||||||
|
when available; field-level assertions). The job validates the rendered
|
||||||
|
Scoop JSON with `node -e JSON.parse` before committing.
|
||||||
|
|
||||||
|
**Manifest specifics.**
|
||||||
|
- *Scoop* (`rdbms-playground.json` at bucket root): `64bit` =
|
||||||
|
`x86_64-pc-windows-gnu.exe`, `arm64` =
|
||||||
|
`aarch64-pc-windows-gnullvm.exe`; each URL carries a
|
||||||
|
`#/rdbms-playground.exe` rename fragment so the `bin` shim resolves
|
||||||
|
regardless of version. Carries `checkver` (lets `scoop status` / the
|
||||||
|
community excavator see lag) but **no `autoupdate`** — our pipeline is the
|
||||||
|
updater.
|
||||||
|
- *Homebrew* (`Formula/rdbms-playground.rb`): `on_macos`/`on_linux` ×
|
||||||
|
`on_arm`/`on_intel` selecting the four bare-binary assets (macOS direct;
|
||||||
|
Linux = the static `-musl` build). **Windows absent** — Homebrew has no
|
||||||
|
Windows port. `install` drops the single staged binary under a stable
|
||||||
|
name; the `test` block runs `--version`.
|
||||||
|
|
||||||
|
**Unverified (validate on first real use):** an actual `scoop install` and
|
||||||
|
`brew install`/`brew test`; the `HEAD:main` default-branch assumption; and
|
||||||
|
whether macOS Gatekeeper accepts the **ad-hoc-signed** mac binary via
|
||||||
|
`brew` (execution should be fine — ad-hoc satisfies arm64's signing
|
||||||
|
requirement and `brew`'s curl download sets no quarantine xattr, unlike a
|
||||||
|
browser download — but this rides on the still-parked Developer-ID signing
|
||||||
|
decision). **Remaining D3:** winget (komac on Linux CI, or a manual PR).
|
||||||
|
|
||||||
|
## Amendment 3 — 2026-06-19: validated end-to-end; a presigned-URL/HEAD gotcha + the Caddy fix it needs
|
||||||
|
|
||||||
|
**Scoop and Homebrew now install and run end-to-end** (`v0.2.0`,
|
||||||
|
user-verified on real Windows + macOS): `scoop install rdbms-playground`
|
||||||
|
and `brew install lazyeval/tap/rdbms-playground` both fetch, checksum,
|
||||||
|
install, and the installed binary launches. Amendment 2's "unverified"
|
||||||
|
caveats are resolved (the `HEAD:main` push populated both repos cleanly).
|
||||||
|
|
||||||
|
**Distribution depends on a server-side Caddy rule — RECORD THIS: it lives
|
||||||
|
in the Gitea edge config, NOT this repo, and if it is lost in a server
|
||||||
|
rebuild Homebrew silently 403s again.** Symptom: `brew install` failed with
|
||||||
|
`curl (22) … 403` on the asset URL. Root cause: Gitea (≥1.25; here 1.26.2)
|
||||||
|
serves release-asset downloads by **303-redirecting to a method-bound
|
||||||
|
AWS-SigV4 presigned S3 URL** (OVH), signed for the *incoming* request's HTTP
|
||||||
|
verb. Homebrew **resolves the URL with a HEAD, captures the returned
|
||||||
|
(HEAD-signed) presigned URL, then runs the download GET against that
|
||||||
|
captured URL** → GET-on-a-HEAD-signed-URL → 403. Reproduced precisely:
|
||||||
|
`GET on HEAD-resolved = 403`, `HEAD on HEAD-resolved = 200`, `GET on
|
||||||
|
GET-resolved = 200`, `HEAD on GET-resolved = 403`. GET-only tools
|
||||||
|
(`install.sh`, `install.ps1`, `cargo binstall`, curl) are unaffected — they
|
||||||
|
GET the original URL and let it redirect fresh to a GET-signed URL. This is
|
||||||
|
a **Homebrew defect** (it reuses an ephemeral, method-scoped credential as
|
||||||
|
if it were a durable resource locator); Gitea's per-request method-bound
|
||||||
|
signing is correct, and SigV4 *cannot* sign one URL for two verbs.
|
||||||
|
`SERVE_DIRECT=false` (Gitea proxies, no presign) would also fix it but was
|
||||||
|
declined — not reshaping storage for one client.
|
||||||
|
|
||||||
|
**Fix (deployed, "Tier A"): a Caddy rule that answers HEAD on
|
||||||
|
release-download paths directly** — 200, no redirect, no body — so brew's
|
||||||
|
resolve records the *original* URL (no presigned credential captured) and
|
||||||
|
its download GET runs through the redirect fresh → GET-signed → 200. Scoped
|
||||||
|
to `method HEAD` + the release-download path; **GET is untouched**, so every
|
||||||
|
working channel is unaffected, and a HEAD carries no payload so no client
|
||||||
|
can *break* (at most a HEAD-probing download manager loses a progress-bar
|
||||||
|
size — cosmetic). Reference Caddyfile (place before the Gitea
|
||||||
|
reverse_proxy):
|
||||||
|
|
||||||
|
```
|
||||||
|
@release_head {
|
||||||
|
method HEAD
|
||||||
|
path_regexp ^/[^/]+/[^/]+/releases/download/.+
|
||||||
|
}
|
||||||
|
handle @release_head {
|
||||||
|
header Accept-Ranges bytes
|
||||||
|
header Content-Type application/octet-stream
|
||||||
|
respond 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A "Tier B" variant (a sidecar that fetches and returns the *real*
|
||||||
|
Content-Length so even HEAD-probing clients stay fully faithful) was specced
|
||||||
|
but proved unnecessary — brew is happy with the bare 200.
|
||||||
|
|
||||||
|
**macOS signing:** the brew-installed binary **runs** under the current
|
||||||
|
**ad-hoc** signature (`codesign --sign -`) — confirmed on Apple Silicon.
|
||||||
|
**Developer-ID signing + notarization remains parked** pending the user's
|
||||||
|
Apple account conversion to an Organization (GA plan / ADR-ci-003); it is
|
||||||
|
needed for *browser-download* trust, not for the package-manager paths,
|
||||||
|
which are all working now.
|
||||||
|
|
||||||
|
**Remaining D3:** winget only.
|
||||||
|
|
||||||
|
## Amendment 4 — 2026-06-20: winget (D3 §3d) — the last package manager
|
||||||
|
|
||||||
|
winget wired as the fourth `publish.yaml` sibling job, **completing the D3
|
||||||
|
package-manager set** (crates.io/binstall, Scoop, Homebrew, winget; plus the
|
||||||
|
install scripts + direct binaries).
|
||||||
|
|
||||||
|
**Model — fundamentally unlike Scoop/Homebrew.** winget has no
|
||||||
|
self-hosted-source equivalent: its default catalog is the central,
|
||||||
|
**human-gated** GitHub repo `microsoft/winget-pkgs`, and you get in by
|
||||||
|
**opening a PR** of manifests into it. So the job submits a PR (via
|
||||||
|
**komac**, pinned `2.16.0`, prebuilt glibc binary — the CI image has no
|
||||||
|
cargo) and **Microsoft's pipeline validates async** (schema, SHA256,
|
||||||
|
AV/SmartScreen scan, sandbox install) + moderator review. It is *not* live
|
||||||
|
on dispatch like the bucket/tap.
|
||||||
|
|
||||||
|
**PackageIdentifier `LazyEvaluation.RdbmsPlayground`** (publisher segment =
|
||||||
|
the publishing entity / `lazyeval` org / license holder — consistent with
|
||||||
|
everything else). Bare-exe → winget **`portable`** installer type; x64
|
||||||
|
(`-pc-windows-gnu`) + arm64 (`-pc-windows-gnullvm`), komac infers arch +
|
||||||
|
type from the binaries.
|
||||||
|
|
||||||
|
**Auth — a dedicated GitHub bot, classic token.** komac needs a **classic
|
||||||
|
`public_repo` PAT**; **fine-grained tokens cannot open the cross-fork PR**
|
||||||
|
(komac #310 — the PR is created on the *target* repo you don't administer,
|
||||||
|
which the fine-grained model can't express). A classic `public_repo` token
|
||||||
|
can't be scoped to one repo, so it lives on a **dedicated GitHub bot
|
||||||
|
account** (a leak can't reach other repos — the lazyeval-ci reasoning), as
|
||||||
|
the `WINGET_GITHUB_TOKEN` secret, referenced **only** in the `winget` job
|
||||||
|
(job-level scoping keeps it away from the crates.io / lazyeval tokens) and
|
||||||
|
passed to the API guards via a 0600 curl config file (never argv).
|
||||||
|
|
||||||
|
**Idempotency — stronger than the others need.** A re-submitted version
|
||||||
|
would open a *duplicate* PR, so before submitting the job guards on **both**
|
||||||
|
(1) already-merged versions (`contents` API on
|
||||||
|
`manifests/l/LazyEvaluation/RdbmsPlayground/<ver>`) and (2) an already-open
|
||||||
|
PR for the id+version (`search/issues`). Either → clean skip, so a repeated
|
||||||
|
`publish` dispatch is safe even mid-review.
|
||||||
|
|
||||||
|
**Signing:** none required to *submit* (only MSIX needs it; ours is
|
||||||
|
portable). The unsigned binary may earn an AV/SmartScreen **manual-review
|
||||||
|
label** on the first PR — usually clears; a nudge toward Trusted Signing
|
||||||
|
(the UK Ltd clears the 3-year-history bar) but not a blocker. Continues the
|
||||||
|
parked Developer-ID/notarization posture.
|
||||||
|
|
||||||
|
**One-time bootstrap (manual — NOT in CI, because `komac new` is
|
||||||
|
interactive).** Run once to create the package in winget-pkgs, then CI
|
||||||
|
`komac update` handles every release after:
|
||||||
|
|
||||||
|
```
|
||||||
|
komac token update # paste the bot's classic public_repo PAT at the prompt (not in argv/history)
|
||||||
|
komac new LazyEvaluation.RdbmsPlayground \
|
||||||
|
-v 0.2.0 \
|
||||||
|
-u https://git.lazyeval.net/oli/rdbms-playground/releases/download/v0.2.0/rdbms-playground-v0.2.0-x86_64-pc-windows-gnu.exe \
|
||||||
|
https://git.lazyeval.net/oli/rdbms-playground/releases/download/v0.2.0/rdbms-playground-v0.2.0-aarch64-pc-windows-gnullvm.exe \
|
||||||
|
--publisher "Lazy Evaluation Ltd" --package-name "RDBMS Playground" \
|
||||||
|
--moniker rdbms-playground --license "MIT OR Apache-2.0" \
|
||||||
|
--package-url https://relplay.org -s
|
||||||
|
```
|
||||||
|
komac downloads the two exes, detects the bare binary as **`portable`**, and
|
||||||
|
**prompts for the command alias — enter `rdbms-playground`** so users get
|
||||||
|
that on PATH (not the long versioned filename). `-s` opens the PR; fill any
|
||||||
|
remaining prompts. (Flags verified against komac 2.16.0: `-v/--version`,
|
||||||
|
`-u/--urls`, `-s/--submit`; the alias has no flag and is prompted.) Once
|
||||||
|
that first PR merges, the CI job's `komac update` takes over.
|
||||||
|
|
||||||
|
**Bootstrap learnings (2026-06-21, first real run).** GitHub bot account =
|
||||||
|
`lazyeval-ci`. Its fork **`lazyeval-ci/winget-pkgs` must exist *before*
|
||||||
|
`komac new`** — komac's auto-fork races on winget-pkgs (one of GitHub's
|
||||||
|
largest repos) and fails with *"Could not resolve to a Repository
|
||||||
|
'lazyeval-ci/winget-pkgs'"*; fork it manually (web **Fork**, or `gh repo
|
||||||
|
fork microsoft/winget-pkgs --clone=false`), wait for it to populate, then
|
||||||
|
re-run. **Alias:** for a **direct single-exe portable**, winget derives the
|
||||||
|
on-PATH command from **`Commands[0]`** (the interactive "Commands" prompt →
|
||||||
|
`rdbms-playground`) — **not** `PortableCommandAlias`, which is
|
||||||
|
**archive/nested-portable-only** and must NOT be added to our manifest
|
||||||
|
(verified against winget-cli's portable-install logic + the 1.6.0 installer
|
||||||
|
schema; komac correctly omits it). So komac's generated manifest needs no
|
||||||
|
hand-editing. First PR: **#391335**.
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,137 @@
|
|||||||
|
# Session handoff — 2026-06-18 (74)
|
||||||
|
|
||||||
|
Large session. Continues from handoff-73 (Ctrl-G demo alias). This one ran
|
||||||
|
the **road to public availability** end to end: a post-merge doc
|
||||||
|
reconciliation, then versioning, installers, crates.io/binstall, the
|
||||||
|
`cargo fmt` gate, and an actual **`v0.2.0` release that is now live on
|
||||||
|
crates.io**. Three new main-sequence ADRs (0054/0055/0056), one CI-ADR
|
||||||
|
amendment, issue **#35 closed**.
|
||||||
|
|
||||||
|
## §1. State
|
||||||
|
|
||||||
|
**Branch `main`.** **2509 pass / 0 fail / 1 ignored** (the long-standing
|
||||||
|
`friendly` doctest); **clippy clean**; **`cargo fmt --check` clean** (the
|
||||||
|
tree is now stock-rustfmt formatted, and CI gates it). `rdbms-playground
|
||||||
|
--version` → `rdbms-playground 0.2.0`.
|
||||||
|
|
||||||
|
**Released:** **`v0.2.0`** — Gitea release with all six D1 targets
|
||||||
|
(Linux/Windows via `release.yaml` on the tag; macOS via the dispatched
|
||||||
|
`release-macos.yaml`, **ad-hoc-signed**). **Published to crates.io**
|
||||||
|
(`cargo install rdbms-playground` and `cargo binstall rdbms-playground`
|
||||||
|
both user-verified).
|
||||||
|
|
||||||
|
**Push state:** earlier commits were pushed (they triggered CI/releases).
|
||||||
|
The **most recent commits are unpushed** and matter:
|
||||||
|
- `3c87dbb` macOS nix-prune fix (takes effect next macOS dispatch).
|
||||||
|
- `d3af1c4` + `8ebe213` the manual `publish.yaml` workflow — ADR-0056 in
|
||||||
|
the first, the **workflow file itself in the second** (`git commit -am`
|
||||||
|
had skipped the new untracked file).
|
||||||
|
- this handoff.
|
||||||
|
Push `main` to land them. (Push is the user's step.)
|
||||||
|
|
||||||
|
## §2. What shipped (commits `628b250`→`d3af1c4`)
|
||||||
|
|
||||||
|
- **`628b250` doc reconciliation** after the CI + website branch merges:
|
||||||
|
rewrote CLAUDE.md's stale repo-layout tree; added a Website subproject
|
||||||
|
note; fixed the CI note; **gitignored `.wrangler/` + `.vscode/`** and
|
||||||
|
removed a tracked `website/.vscode/`; updated requirements (D1 macOS
|
||||||
|
runtime-verified, DOC1 canonical-docs-on-website) + ADR-ci-003.
|
||||||
|
- **`c30a611` ADR-0054 — version surfaces.** `--version`/`-V` + an in-app
|
||||||
|
**`version`** command, both reading `CARGO_PKG_VERSION` via one
|
||||||
|
`cli::version_text()`. A **release-CI guard** fails the release unless
|
||||||
|
the `v*` tag equals `v<Cargo.toml version>`.
|
||||||
|
- **`ef99e6c` ADR-0055 — `scripts/install.sh`** (curl|sh, POSIX,
|
||||||
|
shellcheck-clean, checksum-verified, `~/.local/bin`). Verified
|
||||||
|
end-to-end against the live release. **`install.ps1`** (Windows
|
||||||
|
`irm|iex`) added later (`e9606b5`) — **written but untested here** (no
|
||||||
|
PowerShell on this box; validate on Windows).
|
||||||
|
- **`e9606b5` ADR-0056 — crates.io + binstall prep.** Publish-ready
|
||||||
|
Cargo.toml (dropped `publish=false`; homepage/keywords/categories/
|
||||||
|
`exclude`); `README.md`; `LICENSE-MIT`/`LICENSE-APACHE` (dual, © Lazy
|
||||||
|
Evaluation Ltd) + `CONTRIBUTING.md` (inbound=outbound); the
|
||||||
|
`[package.metadata.binstall]` block with per-target overrides
|
||||||
|
(linux-gnu→musl, windows-msvc→gnu/gnullvm; macOS direct).
|
||||||
|
- **`41b7e9a` + `ec3c7c3` — #35 fmt gate.** One mechanical `cargo fmt`
|
||||||
|
(stock defaults, 102 files, behaviour-preserving) recorded in
|
||||||
|
`.git-blame-ignore-revs`; `ci.yaml` now gates `fmt --check` (ADR-ci-002
|
||||||
|
Amendment 1). **Closes #35.**
|
||||||
|
- **`88830ed`+`bd5be5e` — v0.2.0 bump + the guard bug.** The first
|
||||||
|
`release.yaml` run **failed** at the version guard: it piped `nix
|
||||||
|
develop -c cargo metadata` to node, but the **flake devShell prints a
|
||||||
|
banner to stdout**, corrupting the JSON. Fixed to a toolchain-free
|
||||||
|
`grep -m1 '^version = ' Cargo.toml`. The `v0.2.0` tag was re-pointed
|
||||||
|
(Option A) to the fix commit; re-run went green.
|
||||||
|
- **`3c87dbb` — macOS nix-prune fix.** The prune step's profile dir
|
||||||
|
(`~/.cache/rdbms-ci`) didn't exist, so `nix develop --profile` errored
|
||||||
|
(swallowed by `|| true`) → the gc-root was never created → the whole
|
||||||
|
toolchain (~3.8 GiB) was deleted **and re-downloaded every run**. Added
|
||||||
|
`mkdir -p` + dropped the `|| true`. Diagnosed from the run-74 log via
|
||||||
|
`tea actions runs logs 74`.
|
||||||
|
- **`d3af1c4` — manual `publish.yaml`.** `workflow_dispatch` + `tag`
|
||||||
|
input (mirrors `release-macos.yaml`). Idempotent `crates-io` job
|
||||||
|
(crates.io API pre-check + `cargo publish` backstop), independent jobs
|
||||||
|
so Scoop/Homebrew/winget slot in later. ADR-0056 Amendment 1.
|
||||||
|
|
||||||
|
## §3. Live vs manual vs parked
|
||||||
|
|
||||||
|
- **Automated on a `v*` tag:** `release.yaml` builds + publishes the four
|
||||||
|
Linux/Windows targets (+ fmt/clippy/test gate).
|
||||||
|
- **Manual `workflow_dispatch`:** `release-macos.yaml` (mac binaries —
|
||||||
|
intermittent runner) and `publish.yaml` (crates.io now; more registries
|
||||||
|
later). Run them once the tag's build is up.
|
||||||
|
- **Parked (user decisions):**
|
||||||
|
- **macOS Developer-ID signing.** The pipeline **ad-hoc-signs**
|
||||||
|
(`codesign --sign -`). The user's `Apple Development` cert is the
|
||||||
|
**wrong type** — distribution needs **`Developer ID Application`** +
|
||||||
|
**notarization** (App Store Connect API key recommended). Fine for
|
||||||
|
`curl|sh` (no quarantine); matters for browser downloads. Details in
|
||||||
|
`docs/plans/20260616-public-availability.md`.
|
||||||
|
- **Remaining D3:** Scoop (`lazyeval` bucket), Homebrew (`lazyeval`
|
||||||
|
tap), winget (komac on Linux CI, or manual PR) — each a sibling job
|
||||||
|
in `publish.yaml` + a manifest repo.
|
||||||
|
|
||||||
|
## §4. Immediate next steps
|
||||||
|
|
||||||
|
1. **Push `main`** (lands `3c87dbb` + `d3af1c4`).
|
||||||
|
2. **Add the `CARGO_REGISTRY_TOKEN` secret** (crate-scoped,
|
||||||
|
`publish-update`) so `publish.yaml` works: `tea actions secrets create
|
||||||
|
CARGO_REGISTRY_TOKEN` (paste at prompt) or the Gitea UI.
|
||||||
|
3. **Smoke-test `publish.yaml`:** dispatch it for `v0.2.0` — it should
|
||||||
|
**idempotently skip** ("already on crates.io"), exercising the path
|
||||||
|
risk-free.
|
||||||
|
4. The release ritual going forward (ADR-0054): bump `Cargo.toml` →
|
||||||
|
commit → tag `v<x.y.z>` → push tag (Linux/Windows release builds) →
|
||||||
|
dispatch `release-macos` → dispatch `publish`.
|
||||||
|
|
||||||
|
## §5. Gotchas learned (don't relearn the hard way)
|
||||||
|
|
||||||
|
- **The flake devShell prints a banner to stdout** — never pipe `nix
|
||||||
|
develop -c <cmd>` into a parser. Read Cargo.toml directly, etc.
|
||||||
|
- **Workflow-file source differs by trigger:** a **tag**-triggered run
|
||||||
|
(`release.yaml`) uses the workflow **at the tagged commit**; a
|
||||||
|
**`workflow_dispatch`** run (`release-macos`/`publish`) uses the
|
||||||
|
**default branch** (`main`). So fixing a dispatched workflow only needs
|
||||||
|
a `main` push; fixing a tag-triggered one needs the tag re-pointed.
|
||||||
|
- **Version vs tag:** `Cargo.toml` is bare `0.2.0`; the git tag is
|
||||||
|
`v0.2.0`; the guard checks `tag == "v" + version`; binstall `pkg-url`
|
||||||
|
spells `v{ version }`.
|
||||||
|
- **CI logs are reachable** via `tea actions runs logs <id>` (and `tea
|
||||||
|
actions runs list --output tsv`). Use it instead of guessing from a
|
||||||
|
step name.
|
||||||
|
- **crates.io API needs a descriptive User-Agent** (403 without one).
|
||||||
|
|
||||||
|
## §6. How to take over
|
||||||
|
|
||||||
|
1. Read handoffs 72 → 73 → 74, `CLAUDE.md`, `docs/requirements.md`, and
|
||||||
|
**`docs/plans/20260616-public-availability.md`** (the GA roadmap with
|
||||||
|
all decisions + parked items).
|
||||||
|
2. Confirm green: `cargo test` (**2509 / 1 ignored**), `cargo clippy
|
||||||
|
--all-targets`, `cargo fmt --check`.
|
||||||
|
3. ADRs for this arc: **0054** (versioning), **0055** (installer),
|
||||||
|
**0056** (crates.io/binstall + the publish workflow); CI side
|
||||||
|
**ADR-ci-002 Amendment 1** (fmt gate), **ADR-ci-003** (release matrix
|
||||||
|
+ macOS).
|
||||||
|
4. Workflow unchanged: phased, test-first, `/runda` + DA before commits,
|
||||||
|
ADR amendment + README index-upkeep for decided-area changes, confirm
|
||||||
|
commit messages, never push.
|
||||||
|
5. Consider a `cargo sweep` at this milestone (`target/` grows).
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Session handoff — 2026-06-21 (75)
|
||||||
|
|
||||||
|
Continues from handoff-74 (v0.2.0 live on crates.io). This session **finished
|
||||||
|
the D3 package-manager rollout** — Scoop, Homebrew, and winget — plus the
|
||||||
|
Windows `install.ps1` fixes, and included a deep **presigned-URL / Homebrew**
|
||||||
|
debugging saga that's fixed by a server-side Caddy rule. Five commits; one
|
||||||
|
new ADR amendment arc (ADR-0056 Amendments 2–4).
|
||||||
|
|
||||||
|
## §1. State
|
||||||
|
|
||||||
|
**Branch `main`.** **No crate (Rust) code changed this session** — only
|
||||||
|
`.gitea/workflows/`, `scripts/`, and `docs/`. So the `cargo test` baseline
|
||||||
|
from handoff-74 (**2509 pass / 0 fail / 1 ignored**, clippy + `fmt --check`
|
||||||
|
clean) stands unchanged by construction; not re-run (nothing in the build
|
||||||
|
graph moved). New coverage this session is **`scripts/test-package-renders.sh`**
|
||||||
|
(shellcheck-clean, green), now gated on every push by a new **`ci.yaml`
|
||||||
|
`manifests` job** (bash + node; ruby-absent in CI degrades gracefully).
|
||||||
|
|
||||||
|
**Commits this session** (`cabc813`→`0208c67`; the workflow-bearing ones were
|
||||||
|
pushed, since the live `publish` dispatches + raw-URL installs the user tested
|
||||||
|
needed them on `main`):
|
||||||
|
- `42b40bc` install.ps1 → Windows PowerShell 5.1 compat
|
||||||
|
- `c0531aa` install.ps1 → immediate-use PATH + honest messaging
|
||||||
|
- `6d54c1e` Scoop + Homebrew jobs (D3 §3b/§3c)
|
||||||
|
- `6bb2288` Scoop/Homebrew validated + Caddy fix recorded (Amendment 3)
|
||||||
|
- `0208c67` winget job via komac (D3 §3d, Amendment 4)
|
||||||
|
|
||||||
|
**External / infra state created this session (NOT all in the repo):**
|
||||||
|
- **Caddy Tier-A HEAD rule on the Gitea server** — *load-bearing for Homebrew*,
|
||||||
|
lives in the Gitea edge config, **not** this repo (see §4). If lost on a
|
||||||
|
server rebuild, brew 403s again.
|
||||||
|
- **Gitea:** `lazyeval` org with repos **`scoop-bucket`** + **`homebrew-tap`**;
|
||||||
|
**`lazyeval-ci`** Gitea bot user (org team, Write to those repos);
|
||||||
|
**`LAZYEVAL_PKG_TOKEN`** secret on `oli/rdbms-playground`.
|
||||||
|
- **GitHub:** **`lazyeval-ci`** bot account; classic `public_repo` PAT →
|
||||||
|
**`WINGET_GITHUB_TOKEN`** secret on `oli/rdbms-playground`; fork
|
||||||
|
**`lazyeval-ci/winget-pkgs`**.
|
||||||
|
- **winget bootstrap PR [#391335](https://github.com/microsoft/winget-pkgs/pull/391335)**
|
||||||
|
submitted, in Microsoft review.
|
||||||
|
- **komac 2.16.0** installed locally at `~/.local/bin/komac`.
|
||||||
|
|
||||||
|
## §2. D3 — the package-manager set (the session's throughline)
|
||||||
|
|
||||||
|
| Channel | State |
|
||||||
|
| --- | --- |
|
||||||
|
| crates.io / `cargo binstall` | **live** (handoff-74) |
|
||||||
|
| `install.sh` / `install.ps1` | **live** — PS1 validated on ARM Windows 11 (5.1 + 7.6) |
|
||||||
|
| Scoop (`lazyeval/scoop-bucket`) | **live + validated** (install + run) |
|
||||||
|
| Homebrew (`lazyeval/homebrew-tap`) | **live + validated** — needs the Caddy rule (§4) |
|
||||||
|
| winget (`LazyEvaluation.RdbmsPlayground`) | **wired**; PR #391335 awaiting Microsoft merge |
|
||||||
|
|
||||||
|
How Scoop/Homebrew/winget are wired: sibling jobs in
|
||||||
|
`.gitea/workflows/publish.yaml` (manual `workflow_dispatch`, `tag` input),
|
||||||
|
each idempotent + independent. Scoop/Homebrew render **dependency-free bash**
|
||||||
|
manifests (`scripts/render-{scoop-manifest,homebrew-formula}.sh`) from the
|
||||||
|
release `.sha256` sidecars and push to the org repos via `LAZYEVAL_PKG_TOKEN`.
|
||||||
|
winget runs `komac update --submit` to PR `microsoft/winget-pkgs`, guarded
|
||||||
|
against duplicate PRs. ADR-0056 **Amendments 2–4** carry the full design.
|
||||||
|
|
||||||
|
## §3. The Homebrew 403 saga + Caddy fix (don't lose this)
|
||||||
|
|
||||||
|
Homebrew `brew install` 403'd while every other tool worked. Root cause,
|
||||||
|
reproduced: Gitea (≥1.25; here 1.26.2) **303-redirects release downloads to a
|
||||||
|
method-bound AWS-SigV4 presigned S3 URL** (OVH), signed for the *incoming*
|
||||||
|
request's HTTP verb. Homebrew **resolves with a HEAD, captures the returned
|
||||||
|
HEAD-signed URL, then runs the download GET against it** → GET-on-a-HEAD-signed
|
||||||
|
URL → 403. (`GET on HEAD-resolved = 403`, `HEAD on HEAD-resolved = 200`,
|
||||||
|
`GET on GET-resolved = 200`.) GET-only tools (install.sh/ps1, binstall, curl)
|
||||||
|
are unaffected. This is a **Homebrew defect** (reusing an ephemeral
|
||||||
|
method-scoped credential as a durable locator); Gitea's per-request signing is
|
||||||
|
correct, and SigV4 *cannot* sign one URL for two verbs. `SERVE_DIRECT=false`
|
||||||
|
would fix it but was **declined** (don't reshape storage for one client).
|
||||||
|
|
||||||
|
**Fix (deployed, "Tier A"):** a Caddy rule answering **HEAD on
|
||||||
|
`…/releases/download/…` directly** (200, no redirect, no body) so brew's
|
||||||
|
resolve records the *original* URL and its download GET redirects fresh →
|
||||||
|
GET-signed → 200. GET untouched; a HEAD carries no payload so nothing can
|
||||||
|
break (a HEAD-probing download manager at most loses a progress-bar size).
|
||||||
|
Reference Caddyfile is in **ADR-0056 Amendment 3**.
|
||||||
|
|
||||||
|
## §4. Immediate next steps
|
||||||
|
|
||||||
|
1. **winget PR #391335:** watch validation — the one likely speed bump is an
|
||||||
|
**AV/SmartScreen manual-review label** (unsigned binary); usually clears.
|
||||||
|
Once a moderator merges, winget is live.
|
||||||
|
2. **Post-merge check (Windows):** `winget install LazyEvaluation.RdbmsPlayground`,
|
||||||
|
confirm the command is **`rdbms-playground`** (it is — a direct single-exe
|
||||||
|
portable takes its PATH alias from `Commands[0]`, which komac set; see §5).
|
||||||
|
3. After merge, future releases are hands-off: the CI `winget` job's
|
||||||
|
`komac update` carries each version (alias inherited from the merged
|
||||||
|
manifest). The release ritual is unchanged from ADR-0054, with the
|
||||||
|
`publish` dispatch now also doing Scoop/Homebrew/winget.
|
||||||
|
|
||||||
|
## §5. Gotchas learned (don't relearn the hard way)
|
||||||
|
|
||||||
|
- **The Caddy HEAD rule is server-side, not in this repo.** Homebrew (and any
|
||||||
|
HEAD-then-GET client) depends on it. Record/back it up.
|
||||||
|
- **GitHub tokens:** komac needs a **classic `public_repo`** PAT — **fine-grained
|
||||||
|
tokens can't open the cross-fork PR** (komac #310: the PR is created on the
|
||||||
|
*target* repo you don't administer, which the fine-grained model can't
|
||||||
|
express). A classic token can't be repo-scoped, so it lives on a **dedicated
|
||||||
|
bot** (`lazyeval-ci`) to bound the blast radius.
|
||||||
|
- **komac fork must pre-exist:** `komac new` auto-fork *races* on winget-pkgs
|
||||||
|
(huge repo) → *"Could not resolve to a Repository 'lazyeval-ci/winget-pkgs'"*.
|
||||||
|
Fork manually first (`gh repo fork microsoft/winget-pkgs --clone=false`),
|
||||||
|
wait, then re-run. Pre-fill `komac new` flags to skip the long interactive
|
||||||
|
prompt parade.
|
||||||
|
- **Direct portable alias = `Commands[0]`, NOT `PortableCommandAlias`.** The
|
||||||
|
latter is **archive/nested-portable-only**; adding it to a bare-exe portable
|
||||||
|
is wrong (komac correctly omits it). The "Commands" prompt → `rdbms-playground`
|
||||||
|
is the on-PATH command. Verified vs winget-cli logic + the 1.6.0 schema.
|
||||||
|
- **install.ps1 / Windows PowerShell 5.1** (the in-box shell — PS7 is opt-in):
|
||||||
|
arch via `PROCESSOR_ARCHITECTURE` env (not `RuntimeInformation::OSArchitecture`,
|
||||||
|
which is absent under 5.1's .NET-Framework facade + StrictMode); force TLS 1.2;
|
||||||
|
`-UseBasicParsing`. Also update `$env:Path` in-session (the persisted User PATH
|
||||||
|
only reaches *new* processes; "restart your shell" was wrong — sign-out/in or
|
||||||
|
the in-session update).
|
||||||
|
- **Gitea release downloads go through OVH S3 presigned URLs** — method-bound,
|
||||||
|
300 s expiry. Never assume HEAD-resolve-then-GET works against them.
|
||||||
|
|
||||||
|
## §6. Parked / deferred (user decisions)
|
||||||
|
|
||||||
|
- **macOS Developer-ID signing + notarization** — pending the Apple account →
|
||||||
|
Organization conversion. Ad-hoc signing covers all package-manager paths
|
||||||
|
(brew-installed binary *runs* on Apple Silicon); Developer-ID only matters for
|
||||||
|
browser-download Gatekeeper trust.
|
||||||
|
- **Windows code signing (Trusted Signing)** — not required to *ship* winget
|
||||||
|
(portable; only MSIX needs signing). Unsigned can earn an AV/SmartScreen
|
||||||
|
manual-review label + a user-run warning. **Lazy Evaluation Ltd (since 2012)
|
||||||
|
clears Trusted Signing's 3-year-org bar** when the user wants to remove the
|
||||||
|
warning (individual onboarding is currently paused; org path is open).
|
||||||
|
- **Release notes / CHANGELOG** — raised this session, not done. If wanted:
|
||||||
|
a `CHANGELOG.md` and/or populated Gitea release bodies, then the CI `winget`
|
||||||
|
job can pass komac `--release-notes-url …/releases/tag/$TAG`.
|
||||||
|
|
||||||
|
## §7. How to take over
|
||||||
|
|
||||||
|
1. Read handoffs 73 → 74 → 75, `CLAUDE.md`, `docs/requirements.md` (D1/D3),
|
||||||
|
and **ADR-0056 (esp. Amendments 1–4)** + the GA plan
|
||||||
|
`docs/plans/20260616-public-availability.md`.
|
||||||
|
2. Workflow unchanged: phased, test-first, `/runda` + DA before commits, ADR
|
||||||
|
amendment + README index-upkeep for decided-area changes, confirm commit
|
||||||
|
messages, never push.
|
||||||
|
3. If `cargo test` is needed, the baseline is **2509 / 1 ignored** (handoff-74).
|
||||||
|
4. Consider a `cargo sweep` at this milestone (`target/` grows).
|
||||||
+18
-5
@@ -85,11 +85,24 @@ since ADR-0027.)
|
|||||||
No target requires anything the user must install. ADR-ci-003.)*
|
No target requires anything the user must install. ADR-ci-003.)*
|
||||||
- [ ] **D3** Released via prebuilt binaries plus Homebrew, Scoop,
|
- [ ] **D3** Released via prebuilt binaries plus Homebrew, Scoop,
|
||||||
`winget`, and `cargo binstall`.
|
`winget`, and `cargo binstall`.
|
||||||
*(Prebuilt binaries + checksums now published to Gitea releases
|
*(Prebuilt binaries + checksums on Gitea releases (D1); **`cargo
|
||||||
(D1); the package-manager manifests (Homebrew / Scoop / winget /
|
binstall` + crates.io live** (ADR-0056); **Scoop + Homebrew wired and
|
||||||
`cargo binstall`) remain to do. The asset naming
|
validated end-to-end** on real Windows + macOS (ADR-0056 Amendments
|
||||||
`rdbms-playground-<tag>-<target>` is already binstall-friendly.
|
2 & 3) — `publish.yaml` `scoop-bucket` / `homebrew-tap` jobs render
|
||||||
Tracked under ADR-ci-003 "Deferred".)*
|
dependency-free manifests from the release `.sha256` sidecars and push
|
||||||
|
them, via the scoped `lazyeval-ci` bot token, to
|
||||||
|
`lazyeval/scoop-bucket` and `lazyeval/homebrew-tap`; rendering covered
|
||||||
|
by `scripts/test-package-renders.sh`. NB: the Homebrew path depends on
|
||||||
|
a **server-side Caddy rule** answering HEAD on release-download paths
|
||||||
|
directly (Gitea's presigned-S3 redirect is method-bound and brew
|
||||||
|
reuses a HEAD-signed URL for its GET) — see ADR-0056 Amendment 3;
|
||||||
|
that rule lives in the Gitea edge config, not this repo. **winget
|
||||||
|
wired** (ADR-0056 Amendment 4) — a `publish.yaml` `winget` job submits
|
||||||
|
a PR to `microsoft/winget-pkgs` via komac (`LazyEvaluation.RdbmsPlayground`,
|
||||||
|
portable, x64+arm64), guarded against duplicate PRs; pending the
|
||||||
|
**one-time manual `komac new` bootstrap** + Microsoft review before
|
||||||
|
it goes live. Asset naming `rdbms-playground-<tag>-<target>` is
|
||||||
|
binstall-friendly.)*
|
||||||
|
|
||||||
## TUI shell
|
## TUI shell
|
||||||
|
|
||||||
|
|||||||
+38
-10
@@ -24,9 +24,11 @@
|
|||||||
%LOCALAPPDATA%\Programs\rdbms-playground.
|
%LOCALAPPDATA%\Programs\rdbms-playground.
|
||||||
|
|
||||||
.NOTES
|
.NOTES
|
||||||
Written but NOT tested on Windows from this environment (no PowerShell
|
Verified end-to-end on ARM64 Windows 11 under both Windows PowerShell 5.1
|
||||||
here) — validate on a real Windows host. The verified sibling is
|
and PowerShell 7.6, against the live v0.2.0 release. The x86_64 branch is
|
||||||
install.sh (Linux/macOS).
|
symmetric (env-based arch detection + a confirmed matching release asset)
|
||||||
|
but has not been run directly. The sibling installer is install.sh
|
||||||
|
(Linux/macOS).
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
@@ -37,15 +39,30 @@ param(
|
|||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Windows PowerShell 5.1 (the in-box shell) can negotiate only TLS 1.0/1.1 by
|
||||||
|
# default, which modern hosts reject. Opt into TLS 1.2 without disturbing any
|
||||||
|
# protocols already enabled. (No-op on PowerShell 7.)
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol =
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||||
|
|
||||||
$Repo = 'https://git.lazyeval.net/oli/rdbms-playground'
|
$Repo = 'https://git.lazyeval.net/oli/rdbms-playground'
|
||||||
$Api = 'https://git.lazyeval.net/api/v1/repos/oli/rdbms-playground'
|
$Api = 'https://git.lazyeval.net/api/v1/repos/oli/rdbms-playground'
|
||||||
$Bin = 'rdbms-playground'
|
$Bin = 'rdbms-playground'
|
||||||
|
|
||||||
# Map the host CPU to the target triple we publish for Windows.
|
# Map the host CPU to the target triple we publish for Windows.
|
||||||
$osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
# Read the architecture from the environment rather than
|
||||||
|
# RuntimeInformation::OSArchitecture: under Windows PowerShell 5.1 that type
|
||||||
|
# resolves from a .NET Framework facade that lacks OSArchitecture, which (with
|
||||||
|
# StrictMode) throws "property cannot be found". PROCESSOR_ARCHITECTURE is set
|
||||||
|
# on every PowerShell version; PROCESSOR_ARCHITEW6432 reports the true OS
|
||||||
|
# architecture when a 32-bit shell runs under WOW64.
|
||||||
|
$osArch = [Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITEW6432')
|
||||||
|
if (-not $osArch) {
|
||||||
|
$osArch = [Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITECTURE')
|
||||||
|
}
|
||||||
switch ($osArch) {
|
switch ($osArch) {
|
||||||
'X64' { $target = 'x86_64-pc-windows-gnu' }
|
'AMD64' { $target = 'x86_64-pc-windows-gnu' }
|
||||||
'Arm64' { $target = 'aarch64-pc-windows-gnullvm' }
|
'ARM64' { $target = 'aarch64-pc-windows-gnullvm' }
|
||||||
default { throw "install: unsupported CPU architecture: $osArch" }
|
default { throw "install: unsupported CPU architecture: $osArch" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +82,11 @@ try {
|
|||||||
$shaFile = "$exe.sha256"
|
$shaFile = "$exe.sha256"
|
||||||
|
|
||||||
Write-Host "downloading $asset ..."
|
Write-Host "downloading $asset ..."
|
||||||
Invoke-WebRequest -Uri $url -OutFile $exe
|
# -UseBasicParsing: Windows PowerShell 5.1's Invoke-WebRequest otherwise
|
||||||
Invoke-WebRequest -Uri "$url.sha256" -OutFile $shaFile
|
# tries to use the Internet Explorer engine and can fail when it is absent.
|
||||||
|
# (No-op on PowerShell 7.)
|
||||||
|
Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $exe
|
||||||
|
Invoke-WebRequest -UseBasicParsing -Uri "$url.sha256" -OutFile $shaFile
|
||||||
|
|
||||||
# The sidecar is "<hash> <name>"; compare just the hash.
|
# The sidecar is "<hash> <name>"; compare just the hash.
|
||||||
$expected = ((Get-Content -Raw $shaFile) -split '\s+')[0].ToLower()
|
$expected = ((Get-Content -Raw $shaFile) -split '\s+')[0].ToLower()
|
||||||
@@ -80,14 +100,22 @@ try {
|
|||||||
Move-Item -Path $exe -Destination $dest -Force
|
Move-Item -Path $exe -Destination $dest -Force
|
||||||
Write-Host "installed $Bin $Version -> $dest"
|
Write-Host "installed $Bin $Version -> $dest"
|
||||||
|
|
||||||
# Add the install dir to the user PATH (persistent) if it's not there.
|
# Persist the install dir on the user PATH (for future shells) if missing.
|
||||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
if (-not $userPath) { $userPath = '' }
|
if (-not $userPath) { $userPath = '' }
|
||||||
if (($userPath -split ';') -notcontains $InstallDir) {
|
if (($userPath -split ';') -notcontains $InstallDir) {
|
||||||
$newPath = if ($userPath) { "$userPath;$InstallDir" } else { $InstallDir }
|
$newPath = if ($userPath) { "$userPath;$InstallDir" } else { $InstallDir }
|
||||||
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
|
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
|
||||||
Write-Host "added $InstallDir to your user PATH — restart your shell to pick it up"
|
|
||||||
}
|
}
|
||||||
|
# Also update THIS session's PATH so the command works immediately. The
|
||||||
|
# persisted change only reaches newly-started processes; an already-running
|
||||||
|
# shell (and, depending on how the terminal inherited its environment, even
|
||||||
|
# a freshly-opened one) won't see it until the next sign-out/in.
|
||||||
|
if (($env:Path -split ';') -notcontains $InstallDir) {
|
||||||
|
$env:Path = "$env:Path;$InstallDir"
|
||||||
|
}
|
||||||
|
Write-Host "added $InstallDir to your PATH — '$Bin' works in this window now."
|
||||||
|
Write-Host "for shells already open elsewhere, sign out and back in (or open a fresh one)."
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Remove-Item -Path $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -Path $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Render the Homebrew formula for rdbms-playground to stdout.
|
||||||
|
#
|
||||||
|
# Pure function of its inputs — NO network, NO jq/ruby — so it runs unchanged in
|
||||||
|
# the CI job container (node:22-bookworm-slim: bash + coreutils only). Given a
|
||||||
|
# version and the four macOS/Linux asset SHA-256 hashes it prints a complete
|
||||||
|
# formula. The publish.yaml `homebrew-tap` job fetches the hashes from the
|
||||||
|
# release .sha256 sidecars and commits the result into lazyeval/homebrew-tap as
|
||||||
|
# Formula/rdbms-playground.rb.
|
||||||
|
#
|
||||||
|
# The release assets are bare binaries (no archive), so Homebrew stages the
|
||||||
|
# single downloaded file in the build dir and `install` drops it under a stable
|
||||||
|
# name. Windows is intentionally absent — Homebrew has no Windows port (Scoop /
|
||||||
|
# winget cover Windows).
|
||||||
|
#
|
||||||
|
# Usage: render-homebrew-formula.sh <version> <mac-arm> <mac-intel> <linux-arm> <linux-intel>
|
||||||
|
# <version> version, with or without a leading 'v'
|
||||||
|
# <mac-arm> sha256 of aarch64-apple-darwin
|
||||||
|
# <mac-intel> sha256 of x86_64-apple-darwin
|
||||||
|
# <linux-arm> sha256 of aarch64-unknown-linux-musl
|
||||||
|
# <linux-intel> sha256 of x86_64-unknown-linux-musl
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -ne 5 ]; then
|
||||||
|
echo "usage: $0 <version> <mac-arm> <mac-intel> <linux-arm> <linux-intel>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
version=${1#v}
|
||||||
|
mac_arm=$2
|
||||||
|
mac_intel=$3
|
||||||
|
linux_arm=$4
|
||||||
|
linux_intel=$5
|
||||||
|
|
||||||
|
base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/v$version"
|
||||||
|
|
||||||
|
# Ruby interpolations (#{version}, #{bin}) must survive verbatim into the
|
||||||
|
# formula; they contain no '$', so this unquoted heredoc leaves them untouched
|
||||||
|
# and only expands the shell variables below.
|
||||||
|
cat <<EOF
|
||||||
|
# typed: false
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# rdbms-playground — installs the prebuilt release binary for the host
|
||||||
|
# platform. Regenerated for each release by scripts/render-homebrew-formula.sh;
|
||||||
|
# do not edit by hand.
|
||||||
|
class RdbmsPlayground < Formula
|
||||||
|
desc "Cross-platform TUI playground for learning relational databases"
|
||||||
|
homepage "https://relplay.org"
|
||||||
|
version "$version"
|
||||||
|
license any_of: ["MIT", "Apache-2.0"]
|
||||||
|
|
||||||
|
on_macos do
|
||||||
|
on_arm do
|
||||||
|
url "$base/rdbms-playground-v$version-aarch64-apple-darwin"
|
||||||
|
sha256 "$mac_arm"
|
||||||
|
end
|
||||||
|
on_intel do
|
||||||
|
url "$base/rdbms-playground-v$version-x86_64-apple-darwin"
|
||||||
|
sha256 "$mac_intel"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
on_linux do
|
||||||
|
on_arm do
|
||||||
|
url "$base/rdbms-playground-v$version-aarch64-unknown-linux-musl"
|
||||||
|
sha256 "$linux_arm"
|
||||||
|
end
|
||||||
|
on_intel do
|
||||||
|
url "$base/rdbms-playground-v$version-x86_64-unknown-linux-musl"
|
||||||
|
sha256 "$linux_intel"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def install
|
||||||
|
# The release asset is a single bare binary; Homebrew stages it in the
|
||||||
|
# build dir under its (versioned) basename. Install it as a stable name.
|
||||||
|
bin.install Dir["*"].first => "rdbms-playground"
|
||||||
|
end
|
||||||
|
|
||||||
|
test do
|
||||||
|
assert_match "rdbms-playground #{version}", shell_output("#{bin}/rdbms-playground --version")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
EOF
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Render the Scoop manifest for rdbms-playground to stdout.
|
||||||
|
#
|
||||||
|
# Pure function of its inputs — NO network, NO jq/ruby — so it runs unchanged in
|
||||||
|
# the CI job container (node:22-bookworm-slim: bash + coreutils only). Given a
|
||||||
|
# version and the two Windows asset SHA-256 hashes it prints a complete,
|
||||||
|
# schema-valid Scoop manifest. The publish.yaml `scoop-bucket` job fetches the
|
||||||
|
# hashes from the release's .sha256 sidecars and commits the result into the
|
||||||
|
# lazyeval/scoop-bucket repository as rdbms-playground.json.
|
||||||
|
#
|
||||||
|
# Manifest updates are CI-driven (this script, per release), so the manifest
|
||||||
|
# carries `checkver` (so `scoop status` / the community excavator can see when
|
||||||
|
# the bucket lags upstream) but deliberately NO `autoupdate` — our pipeline is
|
||||||
|
# the updater, not Scoop's maintainer tooling.
|
||||||
|
#
|
||||||
|
# Usage: render-scoop-manifest.sh <version> <hash-x64> <hash-arm64>
|
||||||
|
# <version> version, with or without a leading 'v' (e.g. 0.2.0 or v0.2.0)
|
||||||
|
# <hash-x64> sha256 of the x86_64-pc-windows-gnu.exe asset
|
||||||
|
# <hash-arm64> sha256 of the aarch64-pc-windows-gnullvm.exe asset
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -ne 3 ]; then
|
||||||
|
echo "usage: $0 <version> <hash-x64> <hash-arm64>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Accept either 0.2.0 or v0.2.0; the manifest 'version' field is bare.
|
||||||
|
version=${1#v}
|
||||||
|
hash_x64=$2
|
||||||
|
hash_arm64=$3
|
||||||
|
|
||||||
|
repo="https://git.lazyeval.net/oli/rdbms-playground"
|
||||||
|
base="$repo/releases/download/v$version"
|
||||||
|
|
||||||
|
# The `#/rdbms-playground.exe` fragment tells Scoop to save the versioned asset
|
||||||
|
# under a stable filename, so the `bin` shim resolves regardless of version.
|
||||||
|
url_x64="$base/rdbms-playground-v$version-x86_64-pc-windows-gnu.exe#/rdbms-playground.exe"
|
||||||
|
url_arm64="$base/rdbms-playground-v$version-aarch64-pc-windows-gnullvm.exe#/rdbms-playground.exe"
|
||||||
|
|
||||||
|
# Note: \$.tag_name emits a literal $ (Scoop's JSONPath); the regex uses [0-9.]
|
||||||
|
# rather than \d so the manifest contains no backslashes to escape.
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"version": "$version",
|
||||||
|
"description": "A cross-platform TUI playground for learning relational databases.",
|
||||||
|
"homepage": "https://relplay.org",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"architecture": {
|
||||||
|
"64bit": {
|
||||||
|
"url": "$url_x64",
|
||||||
|
"hash": "$hash_x64"
|
||||||
|
},
|
||||||
|
"arm64": {
|
||||||
|
"url": "$url_arm64",
|
||||||
|
"hash": "$hash_arm64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bin": "rdbms-playground.exe",
|
||||||
|
"checkver": {
|
||||||
|
"url": "https://git.lazyeval.net/api/v1/repos/oli/rdbms-playground/releases/latest",
|
||||||
|
"jsonpath": "\$.tag_name",
|
||||||
|
"regex": "v([0-9.]+)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Tests for the package-manifest render scripts (Scoop + Homebrew).
|
||||||
|
#
|
||||||
|
# Validates that each render script emits a well-formed manifest for given
|
||||||
|
# inputs, and that the inputs land in the right places. Dependency-light:
|
||||||
|
# requires node (always in the CI image) for JSON parsing; uses jq and ruby for
|
||||||
|
# extra checks when present (both available on the dev box). No network.
|
||||||
|
#
|
||||||
|
# Run: scripts/test-package-renders.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||||
|
pass() { echo "ok: $*"; }
|
||||||
|
|
||||||
|
# Distinct dummy hashes so we can assert each lands in the right slot.
|
||||||
|
VER=9.9.9
|
||||||
|
H_WIN_X64=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01
|
||||||
|
H_WIN_ARM=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa02
|
||||||
|
H_MAC_ARM=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa03
|
||||||
|
H_MAC_X64=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa04
|
||||||
|
H_LIN_ARM=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa05
|
||||||
|
H_LIN_X64=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa06
|
||||||
|
|
||||||
|
# ---- Scoop ----------------------------------------------------------------
|
||||||
|
scoop=$tmp/rdbms-playground.json
|
||||||
|
# Pass a leading 'v' to confirm it gets stripped.
|
||||||
|
"$here/render-scoop-manifest.sh" "v$VER" "$H_WIN_X64" "$H_WIN_ARM" > "$scoop"
|
||||||
|
|
||||||
|
node -e 'JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"))' "$scoop" \
|
||||||
|
|| fail "scoop manifest is not valid JSON"
|
||||||
|
pass "scoop manifest parses as JSON"
|
||||||
|
|
||||||
|
# Field-level assertions via node (no jq dependency).
|
||||||
|
check_json() { # <jsexpr> <expected>
|
||||||
|
local got
|
||||||
|
got=$(node -e '
|
||||||
|
const m = JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));
|
||||||
|
process.stdout.write(String(eval(process.argv[2])));
|
||||||
|
' "$scoop" "$1")
|
||||||
|
[ "$got" = "$2" ] || fail "scoop: $1 = '$got', expected '$2'"
|
||||||
|
}
|
||||||
|
check_json 'm.version' "$VER"
|
||||||
|
check_json 'm.bin' "rdbms-playground.exe"
|
||||||
|
check_json 'm.architecture["64bit"].hash' "$H_WIN_X64"
|
||||||
|
check_json 'm.architecture["arm64"].hash' "$H_WIN_ARM"
|
||||||
|
check_json 'm.checkver.jsonpath' '$.tag_name'
|
||||||
|
grep -q "rdbms-playground-v$VER-x86_64-pc-windows-gnu.exe#/rdbms-playground.exe" "$scoop" \
|
||||||
|
|| fail "scoop: x64 url/fragment missing"
|
||||||
|
grep -q "rdbms-playground-v$VER-aarch64-pc-windows-gnullvm.exe#/rdbms-playground.exe" "$scoop" \
|
||||||
|
|| fail "scoop: arm64 url/fragment missing"
|
||||||
|
pass "scoop manifest fields correct"
|
||||||
|
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
jq -e . "$scoop" >/dev/null || fail "scoop: jq rejected the manifest"
|
||||||
|
pass "scoop manifest valid per jq"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- Homebrew -------------------------------------------------------------
|
||||||
|
formula=$tmp/rdbms-playground.rb
|
||||||
|
"$here/render-homebrew-formula.sh" "$VER" "$H_MAC_ARM" "$H_MAC_X64" "$H_LIN_ARM" "$H_LIN_X64" > "$formula"
|
||||||
|
|
||||||
|
if command -v ruby >/dev/null 2>&1; then
|
||||||
|
ruby -c "$formula" >/dev/null || fail "homebrew formula is not valid Ruby"
|
||||||
|
pass "homebrew formula parses as Ruby"
|
||||||
|
else
|
||||||
|
echo "warn: ruby not present — skipping formula syntax check" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Each hash must appear exactly once (right asset → right slot), the version
|
||||||
|
# must be present, and #{version} must survive verbatim for brew's test block.
|
||||||
|
for pair in \
|
||||||
|
"aarch64-apple-darwin:$H_MAC_ARM" \
|
||||||
|
"x86_64-apple-darwin:$H_MAC_X64" \
|
||||||
|
"aarch64-unknown-linux-musl:$H_LIN_ARM" \
|
||||||
|
"x86_64-unknown-linux-musl:$H_LIN_X64"; do
|
||||||
|
target=${pair%%:*}; hash=${pair#*:}
|
||||||
|
grep -q "rdbms-playground-v$VER-$target\"" "$formula" || fail "homebrew: url for $target missing"
|
||||||
|
grep -q "sha256 \"$hash\"" "$formula" || fail "homebrew: sha256 for $target missing"
|
||||||
|
done
|
||||||
|
grep -q 'version "'"$VER"'"' "$formula" || fail "homebrew: version line missing"
|
||||||
|
grep -q 'assert_match "rdbms-playground #{version}"' "$formula" \
|
||||||
|
|| fail "homebrew: test block #{version} interpolation was mangled"
|
||||||
|
grep -q 'rdbms-playground-v9.9.9-x86_64-pc-windows' "$formula" \
|
||||||
|
&& fail "homebrew: formula unexpectedly references a Windows asset"
|
||||||
|
pass "homebrew formula fields correct"
|
||||||
|
|
||||||
|
echo "all render tests passed"
|
||||||
Reference in New Issue
Block a user