15 Commits
Author SHA1 Message Date
claude@clouddev1 f2b4ed00f4 docs(website): add cookieless Umami analytics + a privacy page
website / deploy (push) Successful in 37s
Wire self-hosted Umami (umami.oliversturm.com) into the site head:
- data-domains=relplay.org so the preview/staging deploys don't pollute
  the production stats
- data-do-not-track so DNT visitors aren't counted
- the Website ID is public by design (ships in every page)

Add a short /privacy page (footer-linked, kept out of the sidebar)
explaining the no-cookies / no-personal-data / no-cross-site-tracking
posture and the DNT behaviour. No cookie banner is needed: the tracker
stores nothing on the device, and first-party statistical analytics is
exempt from PECR consent under the Data (Use and Access) Act 2025.
2026-06-22 13:27:10 +00:00
claude@clouddev1 c86df95a0f docs(website): tabbed per-OS install instructions with auto-detection
website / deploy (push) Successful in 33s
Split the install page into Linux / macOS / Windows tabs so each visitor
sees only the commands that apply to them. A small InstallOsDetect component
seeds Starlight's synced-tabs localStorage key with the detected OS before
the tabs render, so the matching tab is pre-selected with no flash — and
only when the visitor hasn't already picked a tab, so a manual choice wins.

- installation.md -> .mdx (needs the Tabs/TabItem components)
- detection covers navigator.userAgentData and the legacy platform/UA
  fallbacks; unknown platforms fall through to the default tab
- piggybacks on Starlight's own restore mechanism rather than re-implementing
  tab switching
2026-06-22 09:36:56 +00:00
claude@clouddev1 fb536b4245 docs(website): reconcile installation docs with v0.2.0 shipped reality
ci / gate (push) Successful in 1m55s
ci / manifests (push) Successful in 3s
website / deploy (push) Successful in 35s
installation.md predated the install/packaging work that landed on main:
- drop the "once published" placeholders now that v0.2.0 is live
- fix the Homebrew/Scoop commands (tap/bucket-add forms; the bare
  `brew install rdbms-playground` was wrong)
- add the curl|sh and PowerShell one-line installers (ADR-0055),
  `cargo install` / `cargo binstall` (ADR-0056), and prebuilt binaries
- present winget as coming-soon (PR awaiting the public catalogue)

command-line-options.md: add `-V`/`--version` and the in-app `version`
command (ADR-0054).
2026-06-21 22:21:26 +00:00
claude@clouddev1 1237aa59c1 Merge branch 'main' into website
Bring v0.2.0 + install/packaging surface (crates.io, curl|sh installer,
Windows install.ps1, Scoop/Homebrew/winget, --version/version command)
onto the website branch for documentation reconciliation.
2026-06-21 22:15:41 +00:00
claude@clouddev1 ba0872dbb9 docs: website-branch session handoff (website-3) 2026-06-21 22:12:52 +00:00
claude@clouddev1 56e3456cfc docs: handoff 75 — D3 package managers complete (Scoop/Homebrew/winget)
ci / gate (push) Successful in 1m58s
ci / manifests (push) Successful in 4s
Covers the session that finished the D3 rollout: install.ps1 Windows 5.1
fixes, Scoop + Homebrew wiring and validation, the Homebrew-403 presigned-URL
saga fixed via a server-side Caddy HEAD rule, and the winget job + komac
bootstrap (PR #391335). Records the infra created off-repo (Caddy rule, the
lazyeval Gitea/GitHub bots + tokens), the gotchas, and the parked signing
tracks.
2026-06-21 22:05:00 +00:00
claude@clouddev1 0208c67e59 ci(publish): wire winget job via komac (D3 §3d)
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.
2026-06-21 21:58:47 +00:00
claude@clouddev1 6bb2288470 docs(adr-0056): Scoop+Homebrew validated; record the Caddy HEAD fix
Amendment 3: scoop install and brew install both verified end-to-end on
real Windows + macOS (install + run), resolving Amendment 2's unverified
caveats (incl. the HEAD:main push).

Records the root cause + fix for the Homebrew 403: Gitea (>=1.25)
303-redirects release downloads to a method-bound SigV4 presigned S3 URL;
brew resolves with HEAD, captures the HEAD-signed URL, then GETs it -> 403
(GET-only tools are unaffected). Fixed by a server-side Caddy rule that
answers HEAD on release-download paths directly (200, no redirect) so the
download GET re-runs the redirect fresh. That rule lives in the Gitea edge
config, NOT this repo - documented so a server rebuild doesn't silently
break brew. Includes the reference Caddyfile block.

Also notes the ad-hoc mac signature runs fine via brew; Developer-ID +
notarization stays parked on the Apple org conversion. Remaining D3: winget.
2026-06-20 08:15:17 +00:00
claude@clouddev1 6d54c1e96c ci(publish): wire Scoop bucket + Homebrew tap jobs (D3 §3b/§3c)
ci / gate (push) Successful in 1m59s
ci / manifests (push) Successful in 4s
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.
2026-06-19 21:30:18 +00:00
claude@clouddev1 c0531aa048 fix(install): use install.ps1 immediately + honest PATH guidance
The persisted User PATH only reaches newly-started processes, so the old
"restart your shell" advice was wrong — a sign-out/in was actually needed
(observed on Windows 11). Update the current session's $env:Path as well
so the command works right away in the same window, and reword the notice.

Also refresh the header: verified on ARM64 Windows 11 under Windows
PowerShell 5.1 and PowerShell 7.6 against the live v0.2.0 release.
2026-06-19 14:57:25 +00:00
claude@clouddev1 42b40bc099 fix(install): make install.ps1 work on Windows PowerShell 5.1
ci / gate (push) Successful in 3m30s
The in-box Windows shell (5.1, .NET Framework) is the baseline we must
support; PowerShell 7 is an opt-in install most users don't have.

- arch detection: read PROCESSOR_ARCHITECTURE/ARCHITEW6432 from the
  environment instead of RuntimeInformation::OSArchitecture, which
  resolves from a .NET Framework facade lacking that property under 5.1
  and throws under StrictMode (the reported failure).
- force TLS 1.2 before any web request (5.1 may default to TLS 1.0/1.1).
- pass -UseBasicParsing to Invoke-WebRequest (5.1 otherwise uses the IE
  engine and can fail when it is absent).

All three are no-ops on PowerShell 7. Relates to ADR-0055.
2026-06-19 14:22:28 +00:00
claude@clouddev1 cabc8131a9 docs: handoff 74 — road to public availability; v0.2.0 live on crates.io
ci / gate (push) Successful in 3m21s
2026-06-18 22:10:41 +00:00
claude@clouddev1 8ebe213b5d ci: add the publish.yaml workflow file (completes d3af1c4)
d3af1c4 described the manual publish workflow and updated ADR-0056, but
`git commit -am` doesn't stage new untracked files, so publish.yaml
itself was left out. Add it here.
2026-06-18 22:10:21 +00:00
claude@clouddev1 d3af1c413a ci: add a manual publish workflow (crates.io, idempotent + expandable)
A workflow_dispatch publish.yaml (mirrors release-macos.yaml) with a `tag`
input, run by hand once the automated release builds exist. Publishing
stays manual and keeps the registry token off every tag push: it's
irreversible (yank-only), the split release (tag Linux/Windows +
dispatched macOS) makes a human the 'all assets up' gate, and crates.io
has no Gitea-Actions trusted-publishing path. The crates.io job is
idempotent (crates.io API pre-check + cargo publish as backstop) and
independent (no inter-job needs), so future Scoop/Homebrew/winget jobs can
be added alongside without interfering or breaking re-runs. Token via the
CARGO_REGISTRY_TOKEN secret. ADR-0056 Amendment 1 + README index also
record 0.2.0 published + binstall verified.
2026-06-18 22:03:47 +00:00
claude@clouddev1 3c87dbb391 fix(ci): create the profile dir so macOS nix prune keeps the toolchain warm
The prune step's profile path $HOME/.cache/rdbms-ci/toolchain had no
parent dir, so `nix develop --profile` errored ("cannot read directory")
and — swallowed by `|| true` — never created the profile/gc-root. With
nothing rooting the toolchain, nix-collect-garbage deleted the whole
closure every run (~3.8 GiB re-downloaded each dispatch; confirmed in the
run-74 log). Add `mkdir -p` for the parent and drop the `|| true` on the
profile realization so a future breakage fails loudly. The VM stays
bounded as before, but the toolchain now persists across runs.

release-macos.yaml is workflow_dispatch (runs from main's definition), so
this takes effect on the next dispatch — the already-published v0.2.0
macOS binaries are unaffected.
2026-06-18 21:24:34 +00:00
20 changed files with 1526 additions and 82 deletions
+23
View File
@@ -6,6 +6,11 @@
# 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
# 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
on:
push:
@@ -46,3 +51,21 @@ jobs:
run: nix develop -c cargo clippy --all-targets -- -D warnings
- name: test
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
+284
View File
@@ -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.
+11 -2
View File
@@ -84,12 +84,21 @@ jobs:
# The runner wipes the workspace each run, so cargo target/ never
# accumulates. Bound the persistent nix store by generation: record the
# 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()
run: |
echo "--- disk before ---"; df -h / | tail -1
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-collect-garbage || true
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
first publish + matching release (the license files, © holder, and
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
View File
File diff suppressed because one or more lines are too long
+137
View File
@@ -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).
+145
View File
@@ -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 24).
## §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 24** 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 14)** + 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).
+130
View File
@@ -0,0 +1,130 @@
# Website-branch handoff — 2026-06-21 (website-3)
Third handoff for the **website** work (separate sequence from `main`'s
`YYYYMMDD-handoff-NN.md`). Read **website-2** (2026-06-11) first for the
scaffolding, cast pipeline, and the no-advertising constraint; this note covers
everything since, and — importantly — the state has moved on (see §1).
## §1. State (read this first)
- **Branch:** `website`. **HEAD `dff7841`.** Working tree **clean**.
- **Pushed:** `origin/website` == `dff7841` (this session's work is pushed).
- **Already merged into `main`:** `dff7841` is an ancestor of `main` — every
website change below is in `main`. **`main` has since advanced ~20 commits**
(now `56e3456`, handoff 75) with **v0.2.0 + install/packaging** work. So
**`website` is ~20 commits behind `main`.**
- **Consequence for the next session:** start by **merging `main` into
`website` again** (or rebranching `website` off `main`). There is real new
user-facing surface to document — see §4.
- **Agent limitation:** this machine's SSH key for `git.lazyeval.net` was **not
available to the agent** — `git fetch`/`push` fail from inside the session.
**Push and fetch are the user's steps.** Local merges work fine.
## §2. What shipped this session (all in `main` now)
The website lives in **`website/`**; living style guide **`website/STYLE.md`**;
decisions **`docs/website/adr/20260604-adr-website-001.md`**.
**CI / deployment — the big one (ADR-website-001 §4):**
- **`.gitea/workflows/website.yaml`** — on a push to `main` or `website` that
touches `website/**`, build the Astro site with pnpm and `wrangler pages
deploy website/dist` to the Cloudflare **Pages project `relplay`** (Direct
Upload, no Git integration). `--branch` selects environment vs the project's
production branch (`main`): **`main` → production (`relplay.org`)**,
**`website` → preview (`website.relplay.pages.dev`)**.
- Pure-Node build (the `.cast` files are committed, so no cargo). Runs on the
bare `ci-public` runner (node present; pnpm via corepack, pinned by
`package.json`'s `packageManager: pnpm@10.30.3`).
- **Secrets (set in repo Actions settings):** `CLOUDFLARE_API_TOKEN` (Pages:
Edit) + `CLOUDFLARE_ACCOUNT_ID`. Pages project created with production branch
= `main`.
- **`ci.yaml` gate** now skips website-only pushes (`paths-ignore` += `website/**`,
`.gitea/workflows/website.yaml`).
- **`staging.relplay.org`** (optional): attach the custom domain to the `website`
branch alias — add it in the Pages dashboard, then point its **proxied**
CNAME at `website.relplay.pages.dev` (external/unproxied DNS falls through to
production). Cloudflare Pages maps branches to **subdomains, not sub-folders**.
**Content updates (from two `main` merges that landed seed/readline/hint/etc.):**
- **Seed page** (`reference/generating-sample-data.mdx`) refreshed for **#33/#34**:
year-as-int columns (`*_year`/`published` → plausible years), built-in value
sets (`priority`/`severity`/`rating`), advisory now **status-only**. All output
blocks re-captured; the seed cast reflects it.
- **`hint` feature** (ADR-0053) documented: `getting-help.md`**`.mdx`** with a
"Hints" section (F1 = tier-3 live-input hint; `hint` command = last-error hint)
+ the real rendered block; `the-assistive-editor.mdx` points at F1.
- **Readline keys (#29)** + **cross-mode history recall (#30)** documented on
`the-assistive-editor.mdx`; "Planned" narrowed to multi-line entry only.
- **Landing** (`index.mdx`): the redundant plain **`<h1>` "RDBMS Playground"** is
hidden (kept for SEO/a11y via a **title-only hero**, landing-scoped in
`global.css`); the **Wordmark + tagline + buttons** render in the body so the
lockup sits where the title was and content rises above the fold (it was nearly
hidden on an iPad).
- **All 10 casts re-recorded** + a **new `hint` cast** (11 total) against the
current app.
**Casts — the F1 trick (important for future hint/keybinding casts):**
- autocast **cannot send F1** (or arrows/function keys — escape sequences split
by the per-key delay). Solution shipped on `main`: **ADR-0047 Amendment 1**
in `--demo` mode **Ctrl-G aliases F1** and **badges AS `[F1]`**, so a cast looks
like a real F1 press. The cast generator gained `CtrlG: '^G'`.
- The **hint cast** uses it realistically: type `add column `, pause, **F1**
(Ctrl-G) mid-command to recall the syntax, finish the command from the example,
then later a failed command + `hint` for the error.
**Hint-vs-docs audit (user-requested):** one genuine conflict found —
`hint.cmd.create_table.example` declared a 3-column compound PK; **fixed upstream**
(`5a37437`, now `create table Customers with pk id(serial)`). Everything else
(export/import, indexes, constraints, DML, SQL forms, error hints, terminology)
was consistent.
## §3. Verification status
- `cd website && pnpm build`**clean, 27 pages**. Forbidden-terms grep clean
(`DSL`/`SQLite`/`STRICT`/`rusqlite`/`PRAGMA`). Output-table box-drawing intact;
anchors resolve; hint cast content verified (`[F1]` badge + both hint blocks).
- **NOT done — needs a human visual pass:** the new **landing spacing/aesthetics**
(couldn't be eyeballed by the agent) and the **11 re-recorded casts** (verified
programmatically only). Do this in staging/production before relying on them.
## §4. NEXT WORK — priority order
1. **Re-merge `main` into `website`** (it's ~20 behind) — or rebranch off `main`.
2. **Reconcile the installation / getting-started docs with what `main` actually
shipped (v0.2.0):** the website branch's `installation.md` predates several
real install methods now live —
- **crates.io v0.2.0** (`cargo install rdbms-playground`, `cargo binstall`),
- the **curl | sh installer** (ADR-0055),
- **Windows `install.ps1`** (`fix(install)` commits),
- **package managers**: Scoop / Homebrew / **winget** (ADR-0056; winget via
komac). `installation.md` mentions brew/scoop but **not** winget, the
curl|sh installer, or binstall — reconcile against the shipped reality
(check ADR-0055/0056 + handoffs 74/75 + the `publish.yaml` workflow).
- There's also `--version`/`-V` + an in-app `version` command (ADR-0054) — a
line on the command-line-options page may be warranted.
3. **Visual sweep** (§3): landing + casts; final caption review (website-2 §2).
4. **Carryover from website-2:** light/dark player theme (deferred), open STYLE
decisions (#7/#8 mostly resolved; recheck), output-pane-scroll cast still
blocked (PgUp/PgDn unsendable — would need a typeable scroll key like #24).
## §5. Process pins
- **Commits:** user-confirmed (show the message first), **no AI attribution**,
**append-only** (no amend/rebase/force-push). **Push/fetch are the user's
steps** (and the agent can't reach the remote anyway — §1).
- **Ground every page in source** (`src/dsl/*`, `src/friendly/strings/en-US.yaml`,
the ADRs) — not `requirements.md` markers. No engine name, no "DSL" in copy.
- **Output blocks captured from the real app**, never hand-drawn (STYLE.md). The
capture recipe (throwaway in-crate/`it` test → render → paste → delete) and the
cast pipeline (`pnpm casts [name]`, needs `../target/debug`) are in website-2.
- **Cast stance (revised 2026-06-12):** default to a cast; justify its *absence*.
- **Issues** via `tea` (repo `oli/rdbms-playground` on `git.lazyeval.net`;
append `< /dev/null` + `timeout 30`). This session filed **#33** (year ints)
and **#34** (enum value sets) — both **implemented** on `main` now.
## §6. Footnote — a git oddity in this branch's history
`c84a640 "docs(website): document the hint feature with a cast"` is an **empty
rename commit** (a `git add` tooling slip staged only the `.md``.mdx` rename);
its actual content is in the **next** commit `028d324`. Harmless, left in place
per the append-only rule (user-approved). No action needed.
+18 -5
View File
@@ -85,11 +85,24 @@ since ADR-0027.)
No target requires anything the user must install. ADR-ci-003.)*
- [ ] **D3** Released via prebuilt binaries plus Homebrew, Scoop,
`winget`, and `cargo binstall`.
*(Prebuilt binaries + checksums now published to Gitea releases
(D1); the package-manager manifests (Homebrew / Scoop / winget /
`cargo binstall`) remain to do. The asset naming
`rdbms-playground-<tag>-<target>` is already binstall-friendly.
Tracked under ADR-ci-003 "Deferred".)*
*(Prebuilt binaries + checksums on Gitea releases (D1); **`cargo
binstall` + crates.io live** (ADR-0056); **Scoop + Homebrew wired and
validated end-to-end** on real Windows + macOS (ADR-0056 Amendments
2 & 3) — `publish.yaml` `scoop-bucket` / `homebrew-tap` jobs render
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
+38 -10
View File
@@ -24,9 +24,11 @@
%LOCALAPPDATA%\Programs\rdbms-playground.
.NOTES
Written but NOT tested on Windows from this environment (no PowerShell
here) validate on a real Windows host. The verified sibling is
install.sh (Linux/macOS).
Verified end-to-end on ARM64 Windows 11 under both Windows PowerShell 5.1
and PowerShell 7.6, against the live v0.2.0 release. The x86_64 branch is
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()]
param(
@@ -37,15 +39,30 @@ param(
Set-StrictMode -Version Latest
$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'
$Api = 'https://git.lazyeval.net/api/v1/repos/oli/rdbms-playground'
$Bin = 'rdbms-playground'
# 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) {
'X64' { $target = 'x86_64-pc-windows-gnu' }
'Arm64' { $target = 'aarch64-pc-windows-gnullvm' }
'AMD64' { $target = 'x86_64-pc-windows-gnu' }
'ARM64' { $target = 'aarch64-pc-windows-gnullvm' }
default { throw "install: unsupported CPU architecture: $osArch" }
}
@@ -65,8 +82,11 @@ try {
$shaFile = "$exe.sha256"
Write-Host "downloading $asset ..."
Invoke-WebRequest -Uri $url -OutFile $exe
Invoke-WebRequest -Uri "$url.sha256" -OutFile $shaFile
# -UseBasicParsing: Windows PowerShell 5.1's Invoke-WebRequest otherwise
# 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.
$expected = ((Get-Content -Raw $shaFile) -split '\s+')[0].ToLower()
@@ -80,14 +100,22 @@ try {
Move-Item -Path $exe -Destination $dest -Force
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')
if (-not $userPath) { $userPath = '' }
if (($userPath -split ';') -notcontains $InstallDir) {
$newPath = if ($userPath) { "$userPath;$InstallDir" } else { $InstallDir }
[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 {
Remove-Item -Path $tmp -Recurse -Force -ErrorAction SilentlyContinue
+86
View File
@@ -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
+66
View File
@@ -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
+92
View File
@@ -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"
+19
View File
@@ -58,6 +58,25 @@ export default defineConfig({
},
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } },
{ tag: 'meta', attrs: { name: 'twitter:image', content: 'https://relplay.org/og-card.png' } },
// Privacy-friendly, cookieless analytics (self-hosted Umami). The
// Website ID is public by design (it ships in every page). The script
// loads from the Umami host, which also becomes the data destination,
// so no `data-host-url` is needed.
// - data-domains: only run on the production apex, so the
// `website.relplay.pages.dev` preview and `staging.relplay.org`
// don't pollute the stats.
// - data-do-not-track: honour the browser's Do-Not-Track signal
// (those visits are not counted at all). See /privacy.
{
tag: 'script',
attrs: {
defer: true,
src: 'https://umami.oliversturm.com/script.js',
'data-website-id': 'fd77cfa5-cffe-4fc8-addb-1c6d7b6d9939',
'data-domains': 'relplay.org',
'data-do-not-track': true,
},
},
],
// Register the simple-mode command grammar with Expressive Code (Shiki).
expressiveCode: { shiki: { langs: [rdbmsLang, rdbmsSyntax] } },
+2
View File
@@ -15,6 +15,8 @@ import Default from '@astrojs/starlight/components/Footer.astro';
Made by <a href="https://www.lazyevaluation.biz/">Lazy Evaluation Ltd</a>
<span class="sep">·</span>
<a href="https://git.lazyeval.net/oli/rdbms-playground">Source &amp; issues</a>
<span class="sep">·</span>
<a href="/privacy/">Privacy</a>
</p>
</div>
@@ -0,0 +1,39 @@
---
// Pre-selects the install-instructions tab that matches the visitor's OS.
//
// This piggybacks on Starlight's synced-tabs mechanism rather than fighting it.
// A `<Tabs syncKey="install-os">` restores its active tab from
// `localStorage["starlight-synced-tabs__install-os"]` via a script Starlight
// inlines *before* the tabs render (so there is no flash of the wrong tab). All
// we do here is seed that same key with the detected OS — and only when the
// visitor has not already chosen a tab themselves, so a manual choice always wins.
//
// Requirements (mirrors Starlight's own restore script):
// - Must be placed *before* the `<Tabs>` it targets, so this runs first.
// - The stored value must equal one of the TabItem labels exactly
// ("Linux" / "macOS" / "Windows").
// - Inlined, so it runs during parse before the restore script's
// connectedCallback and paint.
---
<script is:inline>
(() => {
const KEY = 'starlight-synced-tabs__install-os';
try {
// A previously stored value is a deliberate user choice — never override it.
if (localStorage.getItem(KEY)) return;
const data = navigator.userAgentData;
const platform = ((data && data.platform) || navigator.platform || '').toLowerCase();
const ua = (navigator.userAgent || '').toLowerCase();
let os = null;
if (platform.includes('win') || ua.includes('windows')) os = 'Windows';
else if (platform.includes('mac') || ua.includes('mac os x')) os = 'macOS';
else if (platform.includes('linux') || platform.includes('x11') || ua.includes('linux'))
os = 'Linux';
if (os) localStorage.setItem(KEY, os);
} catch (e) {
// localStorage unavailable (private mode, blocked cookies, …) — silently
// fall back to the default first tab. No detection, no harm.
}
})();
</script>
@@ -1,64 +0,0 @@
---
title: Installation
description: Install RDBMS Playground from a prebuilt binary or a package manager, and run it for the first time.
sidebar:
order: 1
---
RDBMS Playground is a single self-contained program. There is nothing to
configure and no separate database to install — everything it needs is
built in.
## Prebuilt binaries
Download the binary for your platform, make it executable if needed, and
put it somewhere on your `PATH`.
:::note
Download links are published with each release. They are added here when the
first public version ships.
:::
## Package managers
Once published, the playground will be installable through common package
managers:
```sh
# macOS / Linux (Homebrew)
brew install rdbms-playground
# Windows (Scoop)
scoop install rdbms-playground
```
:::note
Package-manager availability lands with the first public release; the exact
names are confirmed here at that time.
:::
## Run it
Start the playground with no arguments and it opens a fresh, automatically
named temporary project so you can start experimenting immediately:
```sh
rdbms-playground
```
To open an existing project, pass its path:
```sh
rdbms-playground path/to/project
```
Useful options (run `rdbms-playground --help` for the full list):
| Option | What it does |
|---|---|
| `--resume` | Reopen the most recently used project. |
| `--data-dir <PATH>` | Use a different location for stored projects. |
| `--theme <light\|dark>` | Force a theme instead of auto-detecting. |
| `--mode <simple\|advanced>` | Start in a specific input mode. |
Next: [create your first project](/getting-started/first-project/).
@@ -0,0 +1,148 @@
---
title: Installation
description: Install RDBMS Playground with a one-line installer, a package manager, cargo, or a prebuilt binary.
sidebar:
order: 1
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
import InstallOsDetect from '../../../components/InstallOsDetect.astro';
RDBMS Playground is a single self-contained program. There is nothing to
configure and no separate database to install — everything it needs is
built in.
Pick the tab for your platform — we try to select it automatically — then
use whichever method you prefer. The one-line installers are the quickest if
you just want to get going.
<InstallOsDetect />
<Tabs syncKey="install-os">
<TabItem label="Linux">
**One-line installer**
```sh
curl -fsSL https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh | sh
```
It detects your CPU, installs to `~/.local/bin`, and prints a hint if that
directory isn't on your `PATH`. Set `RDBMS_INSTALL_DIR` to install elsewhere,
or `RDBMS_VERSION=vX.Y.Z` to pin a version. Prefer to read a script before
running it? It lives at
[`scripts/install.sh`](https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh).
**Homebrew**
```sh
brew tap lazyeval/tap https://git.lazyeval.net/lazyeval/homebrew-tap
brew install lazyeval/tap/rdbms-playground
```
</TabItem>
<TabItem label="macOS">
**One-line installer**
```sh
curl -fsSL https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh | sh
```
It detects your CPU (Apple Silicon or Intel), installs to `~/.local/bin`, and
prints a hint if that directory isn't on your `PATH`. Set `RDBMS_INSTALL_DIR`
to install elsewhere, or `RDBMS_VERSION=vX.Y.Z` to pin a version. Prefer to
read a script before running it? It lives at
[`scripts/install.sh`](https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh).
**Homebrew**
```sh
brew tap lazyeval/tap https://git.lazyeval.net/lazyeval/homebrew-tap
brew install lazyeval/tap/rdbms-playground
```
</TabItem>
<TabItem label="Windows">
**One-line installer (PowerShell)**
```powershell
irm https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.ps1 | iex
```
It downloads the matching `.exe`, verifies its checksum, installs to
`%LOCALAPPDATA%\Programs\rdbms-playground`, and adds that folder to your user
`PATH`. Use `-InstallDir` to choose a different location or `-Version` to pin
a release.
**Scoop**
```sh
scoop bucket add lazyeval https://git.lazyeval.net/lazyeval/scoop-bucket
scoop install rdbms-playground
```
**winget**
A winget package (`winget install LazyEvaluation.RdbmsPlayground`) is on its
way and awaiting review in the public winget catalogue. Until it lands, use
Scoop or the PowerShell one-liner above.
</TabItem>
</Tabs>
## With cargo
These work on any platform with a Rust toolchain. Install from
[crates.io](https://crates.io/crates/rdbms-playground):
```sh
cargo install rdbms-playground
```
For a faster install that fetches a prebuilt binary instead of compiling, use
[`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall) (install it
first — it is not part of `cargo` itself):
```sh
cargo binstall rdbms-playground
```
## Prebuilt binaries
Every release publishes static Linux, standalone Windows, and macOS binaries
(x86_64 and aarch64), each with a `.sha256` checksum, on the
[releases page](https://git.lazyeval.net/oli/rdbms-playground/releases).
Download the one for your platform, make it executable if needed, and put it
somewhere on your `PATH`.
## Run it
Start the playground with no arguments and it opens a fresh, automatically
named temporary project so you can start experimenting immediately:
```sh
rdbms-playground
```
To open an existing project, pass its path:
```sh
rdbms-playground path/to/project
```
Useful options (run `rdbms-playground --help` for the full list):
| Option | What it does |
|---|---|
| `--resume` | Reopen the most recently used project. |
| `--data-dir <PATH>` | Use a different location for stored projects. |
| `--theme <light\|dark>` | Force a theme instead of auto-detecting. |
| `--mode <simple\|advanced>` | Start in a specific input mode. |
| `--version` | Print the version and exit. |
The full list, with examples, is on the
[command-line options](/using-the-playground/command-line-options/) page.
Next: [create your first project](/getting-started/first-project/).
+51
View File
@@ -0,0 +1,51 @@
---
title: Privacy
description: What we measure on this site, and what we don't. No cookies, no personal data.
tableOfContents: false
---
This site is run as a free teaching tool, not a business that profiles its
visitors. We keep the analytics deliberately minimal.
## What we collect
We measure aggregate usage with **[Umami](https://umami.is/)**, a
privacy-friendly analytics tool that we **host ourselves** (on
`umami.oliversturm.com`). Nothing is sent to a third-party advertising
network.
For each page view we record:
- the page you viewed and the site you arrived from (the referrer),
- your browser, operating system, and device type,
- an approximate **country**.
That's it. The numbers are aggregated into counts — how many people read a
page, which pages are popular — and are never combined into a profile of an
individual.
## What we don't do
- **No cookies, and nothing stored on your device.** The tracker sets no
cookies and writes nothing to local storage. You can confirm this in your
browser's developer tools — there are no entries to clear.
- **No personal data.** Your IP address is used for a moment on our own
server to derive the approximate country and to tell repeated page-loads
apart within a single day; it is **not stored**.
- **No cross-site tracking.** We cannot and do not follow you to other sites,
and we never sell or share what we measure.
## Your choices
If your browser sends a **Do Not Track** signal, we honour it: those visits
are not counted at all.
Because the analytics use no cookies and store nothing on your device, there
is no cookie banner to dismiss — there is simply nothing to consent to.
## Questions
This site and its analytics are operated by
[Lazy Evaluation Ltd](https://www.lazyevaluation.biz/). If you have any
questions about what we measure, get in touch through the contact details
there.
@@ -29,8 +29,13 @@ rdbms-playground path/to/project
| `--no-undo` | Disable the undo machinery for this run — no snapshot is taken before each change (see [Undo, redo & history](/using-the-playground/undo-and-history/)). |
| `--demo` | Turn on demonstration mode — a teaching aid that briefly shows an on-screen badge for keys that otherwise leave no visible mark (Tab, Enter, the arrows, and the like). Useful for screencasts and for demonstrating the playground to a class. Off by default. |
| `--log-file <PATH>` | Write diagnostic logging to `PATH`. |
| `-V`, `--version` | Print the version and exit. |
| `-h`, `--help` | Print the usage banner and exit. |
Once the playground is running, the `version` command prints the same
version line into the output panel — handy when you want to note which
version you're on without leaving the app.
## Examples
```sh