From 6d54c1e96c199199204c2434aa47599b61766974 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Fri, 19 Jun 2026 21:30:18 +0000 Subject: [PATCH] =?UTF-8?q?ci(publish):=20wire=20Scoop=20bucket=20+=20Home?= =?UTF-8?q?brew=20tap=20jobs=20(D3=20=C2=A73b/=C2=A73c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitea/workflows/ci.yaml | 23 +++ .gitea/workflows/publish.yaml | 136 +++++++++++++++++- docs/adr/0056-crates-io-and-cargo-binstall.md | 66 +++++++++ docs/adr/README.md | 2 +- docs/requirements.md | 15 +- scripts/render-homebrew-formula.sh | 86 +++++++++++ scripts/render-scoop-manifest.sh | 66 +++++++++ scripts/test-package-renders.sh | 92 ++++++++++++ 8 files changed, 474 insertions(+), 12 deletions(-) create mode 100755 scripts/render-homebrew-formula.sh create mode 100755 scripts/render-scoop-manifest.sh create mode 100755 scripts/test-package-renders.sh diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 232f564..45cb0a4 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -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 diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index 61bed46..3383b71 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -71,9 +71,133 @@ jobs: nix develop -c cargo publish --locked echo "published rdbms-playground $VER to crates.io." - # Future manual publication targets go here as independent, idempotent jobs, - # e.g.: - # scoop-bucket: { ... update the lazyeval Scoop bucket manifest ... } - # homebrew-tap: { ... update the lazyeval Homebrew formula ... } - # winget: { ... komac submit, or a manual PR helper ... } - # No `needs:` between them — each checks the target and skips if already done. + # Update the lazyeval Scoop bucket (Windows). Renders the manifest from the + # release's .sha256 sidecars and commits it to lazyeval/scoop-bucket. Pushes + # with the lazyeval-ci bot token (LAZYEVAL_PKG_TOKEN), which is scoped — via + # the bot's org-team membership — to the lazyeval package repos only, so a + # leak cannot touch oli/rdbms-playground. + scoop-bucket: + runs-on: ci-public + container: + image: git.lazyeval.net/oli/rdbms-playground-ci:latest + steps: + - uses: actions/checkout@v4 # default ref (main) — current render script + + - name: update the lazyeval Scoop bucket (idempotent) + shell: bash + env: + TAG: ${{ inputs.tag }} + # Passed via env, never inlined into the script, so the value stays + # masked in logs; it only materialises in the clone URL at runtime. + PKG_TOKEN: ${{ secrets.LAZYEVAL_PKG_TOKEN }} + run: | + set -euo pipefail + VER="${TAG#v}" + echo "scoop: targeting rdbms-playground $VER ($TAG)" + + base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG" + fetch_hash() { + local asset="$1" line + echo "scoop: fetching $asset.sha256" >&2 + line=$(curl -fsSL "$base/$asset.sha256") \ + || { echo "ERROR: cannot fetch $asset.sha256 — is $TAG released with assets?" >&2; exit 1; } + # First whitespace-delimited field is the hash. `read` is a bash + # builtin (no awk, which the slim CI image may lack). + local hash _ + read -r hash _ <<<"$line" + printf '%s' "$hash" + } + h_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-pc-windows-gnu.exe") + h_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-pc-windows-gnullvm.exe") + + echo "scoop: rendering manifest" + bash scripts/render-scoop-manifest.sh "$VER" "$h_x64" "$h_arm" > /tmp/rdbms-playground.json + node -e 'JSON.parse(require("fs").readFileSync("/tmp/rdbms-playground.json","utf8"))' \ + || { echo "ERROR: rendered Scoop manifest is not valid JSON" >&2; exit 1; } + + work=$(mktemp -d) + echo "scoop: cloning lazyeval/scoop-bucket" + git clone --depth 1 "https://lazyeval-ci:${PKG_TOKEN}@git.lazyeval.net/lazyeval/scoop-bucket.git" "$work" + cp /tmp/rdbms-playground.json "$work/rdbms-playground.json" + + cd "$work" + git config user.name "lazyeval-ci" + git config user.email "ci@lazyeval.net" + git add rdbms-playground.json + if git diff --cached --quiet; then + echo "scoop: manifest already at $VER — nothing to commit." + exit 0 + fi + git commit -m "rdbms-playground $VER" + # Push to main explicitly: a freshly-created (empty) repo clone may put + # the first commit on a differently-named local branch. Assumes the + # bucket/tap default branch is `main` (Gitea's default for new repos). + git push origin HEAD:main + echo "scoop: bucket updated to rdbms-playground $VER." + + # Update the lazyeval Homebrew tap (macOS + Linux). Same shape as scoop-bucket; + # writes Formula/rdbms-playground.rb into lazyeval/homebrew-tap. + homebrew-tap: + runs-on: ci-public + container: + image: git.lazyeval.net/oli/rdbms-playground-ci:latest + steps: + - uses: actions/checkout@v4 + + - name: update the lazyeval Homebrew tap (idempotent) + shell: bash + env: + TAG: ${{ inputs.tag }} + PKG_TOKEN: ${{ secrets.LAZYEVAL_PKG_TOKEN }} + run: | + set -euo pipefail + VER="${TAG#v}" + echo "homebrew: targeting rdbms-playground $VER ($TAG)" + + base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG" + fetch_hash() { + local asset="$1" line + echo "homebrew: fetching $asset.sha256" >&2 + line=$(curl -fsSL "$base/$asset.sha256") \ + || { echo "ERROR: cannot fetch $asset.sha256 — is $TAG released with assets?" >&2; exit 1; } + # First whitespace-delimited field is the hash. `read` is a bash + # builtin (no awk, which the slim CI image may lack). + local hash _ + read -r hash _ <<<"$line" + printf '%s' "$hash" + } + mac_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-apple-darwin") + mac_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-apple-darwin") + lin_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-unknown-linux-musl") + lin_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-unknown-linux-musl") + + echo "homebrew: rendering formula" + bash scripts/render-homebrew-formula.sh "$VER" "$mac_arm" "$mac_x64" "$lin_arm" "$lin_x64" \ + > /tmp/rdbms-playground.rb + grep -q '^class RdbmsPlayground < Formula$' /tmp/rdbms-playground.rb \ + || { echo "ERROR: rendered formula looks malformed" >&2; exit 1; } + + work=$(mktemp -d) + echo "homebrew: cloning lazyeval/homebrew-tap" + git clone --depth 1 "https://lazyeval-ci:${PKG_TOKEN}@git.lazyeval.net/lazyeval/homebrew-tap.git" "$work" + mkdir -p "$work/Formula" + cp /tmp/rdbms-playground.rb "$work/Formula/rdbms-playground.rb" + + cd "$work" + git config user.name "lazyeval-ci" + git config user.email "ci@lazyeval.net" + git add Formula/rdbms-playground.rb + if git diff --cached --quiet; then + echo "homebrew: formula already at $VER — nothing to commit." + exit 0 + fi + git commit -m "rdbms-playground $VER" + # Push to main explicitly: a freshly-created (empty) repo clone may put + # the first commit on a differently-named local branch. Assumes the + # bucket/tap default branch is `main` (Gitea's default for new repos). + git push origin HEAD:main + echo "homebrew: tap updated to rdbms-playground $VER." + + # winget remains a future sibling job here (komac on Linux CI, or a manual PR + # to microsoft/winget-pkgs). No `needs:` between jobs — each is independent and + # idempotent, so one failing or being added never breaks another. diff --git a/docs/adr/0056-crates-io-and-cargo-binstall.md b/docs/adr/0056-crates-io-and-cargo-binstall.md index 63dc06f..32ab580 100644 --- a/docs/adr/0056-crates-io-and-cargo-binstall.md +++ b/docs/adr/0056-crates-io-and-cargo-binstall.md @@ -110,3 +110,69 @@ 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 ` (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). diff --git a/docs/adr/README.md b/docs/adr/README.md index 37160c7..d9ca677 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -68,4 +68,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0053 — Contextual `hint` command and keybinding](0053-contextual-hint-command-and-keybinding.md) — **Accepted, implemented 2026-06-15** (Phases A–D; closes **A1** + requirements **H2**). Settles the `hint` slot ADR-0003 left "ADR pending"; closes the last open piece of **A1** and tracks requirements **H2**. **Two surfaces:** an **F1 keybinding** that renders a deep hint for the *live* partial input without submitting (the primary path — a submitted `hint` command can't see the buffer it would help with, since Enter empties it), and a submitted **`hint` command** that expands on the *most recent error*. **No topic argument** (contextual only — `help ` already owns explicit reference). Introduces a **tier-3 teaching layer**, deeper than the existing tier-1 (colour / error headline) and tier-2 (ambient one-liner; and the error `hint:`, which is shown **by default** since `Verbosity::Verbose` is the default — `messages short` is the opt-*out*); without it `hint` would just duplicate what's already on screen. Tier-3 content lives in the catalogue under `hint.cmd.` (per command form) and `hint.err.` (per error/diagnostic class), each a structured `what`/`example`/`concept` block rendered via a new `note_hint*` family with `OutputStyleClass::Hint`. **Keyed per-form via a new `hint_ids: &[&str]` field on `CommandNode` mirroring `usage_ids`** (revised in Phase B): a per-*node* key proved too coarse — `add`/`drop`/`show`/`create` are each one node spanning many forms, and a live-input hint for `add 1:n relationship` must be specific to relationships; `hint_key_for_input_in_mode` reuses `usage_key_for_input_in_mode`'s form-word disambiguation, and covers the advanced-SQL forms whose `usage_ids` are empty. Not keyed off `help_id` (it is `None` on the advanced-SQL nodes purely to dedup the `help` list; that parallel gap is issue **#36**). **Clause-concept hints** (`on delete` actions, constraint slots, `with pk`, cardinality) are a recorded **deferred extension** (`hint.concept.`, issue **#37**) — per-form is the right tier-3 granularity, with position-awareness owned by tier-2 + the live `Next:` line. Runtime `translate_error` classes resolve via stored `last_error_hint_key` (`hint` command / empty-F1). (The second route — pre-submit `diagnostic.*` read live from the walker on the F1 path — is **deferred**, issue **#38**: `Diagnostic` carries no class key.) Adds `AppCommand::Hint`, a `HINT` grammar node + REGISTRY entry, the `hint_ids` field, and `last_error_hint_key`; F1 is a read-only overlay (buffer + completion memo untouched). **Content is the bulk of the work** (the mechanism is ~a day): v1 scope = ~37 command forms + 9 runtime error classes (comprehensive for those, ~57 blocks), authored **exemplars-first** (voice approved in this ADR's `/runda` review, then mass-authored in batches), enforced by a **comprehensiveness coverage test**, with graceful fall-back to tier-2 if a key is ever missing. The **pre-submit-diagnostic route + ~33 `diagnostic.*` blocks were deferred** (issue **#38**) — `Diagnostic` carries no class key, so the route needs a broad change for marginal value (tier-2 already surfaces diagnostics; many duplicate runtime classes). Forks user-chosen: two-surface model; **F1** (vs `?` / a chord); no-arg; comprehensive-for-commands-and-errors scope; exemplars-first; diagnostics deferred. OOS: per-topic `hint ` (rejected — overlaps `help`); always-on tier-3 (rejected — keeps ambient terse); non-`en-US` locales + success-command teaching (deferred); clause-concept hints (issue #37); the diagnostic route (issue #38); the `help`-side advanced-SQL gap (issue #36) - [ADR-0054 — Release versioning policy + version surfaces (`--version` / `version`)](0054-release-versioning-and-version-surfaces.md) — **Accepted + implemented 2026-06-16** (plan: `docs/plans/20260616-public-availability.md`, step 1 on the road to public availability; no prior issue/`requirements.md` item — an untracked gap). Fixes the **tag↔crate-version decoupling**: `Cargo.toml` built `0.1.0` while `release.yaml` named assets from the git tag, so a binary could report a version different from the asset it shipped in. **Decision:** `Cargo.toml` `version` is the **single source of truth** (read via `env!("CARGO_PKG_VERSION")`, no tag-injection); two surfaces report it through one `cli::version_text()` → catalog `cli.version_line` — a **`--version` / `-V`** CLI flag (mirrors `--help`, prints+exits in `main.rs`) and an in-app **`version`** command (REGISTRY node `app::VERSION`, `AppCommand::Version`, emits via `note_system`); and a **release-CI version guard** (`release.yaml` `test` job reads the `[package]` version from `Cargo.toml` and **fails the release** unless the `v*` tag equals `v`; the guard's parse was later switched from `cargo metadata | node` to a `grep` on Cargo.toml after the former broke on the flake devShell's stdout banner). Release ritual: bump `Cargo.toml` → commit → tag → push. New keys `cli.version_line` + `help.app.version` + `parse.usage.version` + `hint.cmd.version.{what,example}` (the new REGISTRY command pulls in the comprehensiveness coverage gate). Rejected: tag-as-source (makes Cargo.toml lie). Deferred: git-hash/build-date enrichment (behind the same `version_text()` seam); UI placement beyond the command. Tested test-first: CLI parse (`--version`/`-V`/default-off), `version_text()` carries `CARGO_PKG_VERSION`, the in-app command parses + emits. Also corrected a stale `release.yaml` header comment ("macOS is deferred" → built by the dispatched `release-macos.yaml`). - [ADR-0055 — `curl | sh` install script (`scripts/install.sh`)](0055-curl-sh-install-script.md) — **Accepted + implemented 2026-06-17** (plan: `docs/plans/20260616-public-availability.md`, step 2; tracked by plan + ADR, no Gitea issue — user decision). A one-line installer (`curl -fsSL /scripts/install.sh | sh`) so beginners don't hand-pick an asset + `chmod +x`. **POSIX `sh`** (shellcheck-clean), detects `uname` OS/arch → target triple (**Linux → the fully-static `*-musl`** build, macOS → `*-apple-darwin`; `amd64`/`arm64` aliased; **Windows rejected** → Scoop/winget/releases page), resolves the version from the **`releases/latest`** API (or `RDBMS_VERSION` to pin), downloads the asset **and its `.sha256` and verifies it** (mismatch aborts), installs to `~/.local/bin` (`RDBMS_INSTALL_DIR` override) with a PATH hint. Testing seams: `RDBMS_OS`/`RDBMS_ARCH` + `--print-target`. macOS note: `curl` downloads aren't Gatekeeper-quarantined so the ad-hoc binary runs as-is (Developer-ID + notarization is the postponed signing task). **Verified end-to-end against the live public `v0.1.0`** (all platform mappings, pinned + latest, checksum incl. tamper-rejection, install + run). Rejected: website-domain hosting (extra moving part; Gitea raw is simplest); deferred: uploading the script as a release asset, and a **shellcheck CI gate** (shellcheck isn't in the flake — touches ADR-ci-002). **Amendment 1 (2026-06-17):** added a Windows **`scripts/install.ps1`** (`irm | iex`; maps host CPU → our `*-windows-gnu`/`-gnullvm` `.exe`, SHA-256-verifies, installs to `%LOCALAPPDATA%\Programs\…` + user PATH) — user chose both a one-liner *and* Scoop/winget; **written but untested from this env** (no PowerShell — validate on Windows). -- [ADR-0056 — crates.io publish-readiness + `cargo binstall` metadata (D3)](0056-crates-io-and-cargo-binstall.md) — **Prepared 2026-06-17** (plan step 3a; tracked by plan + ADR). Makes the crate **ready to publish** to crates.io (user decision) and adds `cargo-binstall` metadata; the actual `cargo publish` is a **gated, irreversible maintainer step**. Manifest: drops `publish = false`; adds `homepage` (relplay.org), `keywords`, `categories`, and an `exclude` (`/website`,`/docs`,`/.gitea`,`/.codegraph`) trimming the crate from 585 files/8.3 MiB → **353/913 KiB compressed** (code-only). Authors **`README.md`** (engine-neutral, simple/advanced-mode wording; install via curl|sh/binstall/source/prebuilt) and **`LICENSE-MIT`** (© Lazy Evaluation Ltd — *confirm holder*); the canonical **`LICENSE-APACHE`** is deferred to the maintainer (don't ship retyped legal text) — the SPDX `license` field already satisfies crates.io. **binstall** (syntax verified vs cargo-binstall SUPPORT.md): `pkg-fmt = "bin"` (bare binaries), `pkg-url` spelled `v{ version }` (the placeholder omits the `v`), plus per-target **`overrides`** mapping the common host triples to the assets we ship — `*-linux-gnu` → the static `*-linux-musl` build, `*-pc-windows-msvc` → `*-gnu`/`-gnullvm` `.exe` (macOS matches directly; the docs promise no automatic fallback). **Ordering:** publish at a **new tagged version whose release exists**, after the release — **not `0.1.0`** (diverges from the already-released 0.1.0 binaries that predate `--version`). Verified: `cargo publish --dry-run` packages + verify-builds; `cargo metadata` confirms the binstall block + 4 overrides. **Unverified:** a real `cargo binstall` run (not a dep; nothing on crates.io yet) — validate at first publish. Rejected: cargo-dist (GitHub-centric). Maintainer follow-ups: confirm © holder, add canonical `LICENSE-APACHE`, real binstall validation. **Amendment 1 (2026-06-18):** `0.2.0` **published live** (crates.io; `cargo install` + `cargo binstall` verified — the unverified-overrides caveat is resolved), via a new **manual `workflow_dispatch`** workflow `.gitea/workflows/publish.yaml` (mirrors `release-macos.yaml`; `tag` input; `cargo publish` with a crate-scoped `CARGO_REGISTRY_TOKEN` secret). Publish stays **manual** by decision — irreversible (keeps the token off every tag push), 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. Each registry is its **own idempotent job** (crates.io job no-ops if the version exists) so Scoop/Homebrew/winget can be added as sibling jobs without interfering. +- [ADR-0056 — crates.io publish-readiness + `cargo binstall` metadata (D3)](0056-crates-io-and-cargo-binstall.md) — **Prepared 2026-06-17** (plan step 3a; tracked by plan + ADR). Makes the crate **ready to publish** to crates.io (user decision) and adds `cargo-binstall` metadata; the actual `cargo publish` is a **gated, irreversible maintainer step**. Manifest: drops `publish = false`; adds `homepage` (relplay.org), `keywords`, `categories`, and an `exclude` (`/website`,`/docs`,`/.gitea`,`/.codegraph`) trimming the crate from 585 files/8.3 MiB → **353/913 KiB compressed** (code-only). Authors **`README.md`** (engine-neutral, simple/advanced-mode wording; install via curl|sh/binstall/source/prebuilt) and **`LICENSE-MIT`** (© Lazy Evaluation Ltd — *confirm holder*); the canonical **`LICENSE-APACHE`** is deferred to the maintainer (don't ship retyped legal text) — the SPDX `license` field already satisfies crates.io. **binstall** (syntax verified vs cargo-binstall SUPPORT.md): `pkg-fmt = "bin"` (bare binaries), `pkg-url` spelled `v{ version }` (the placeholder omits the `v`), plus per-target **`overrides`** mapping the common host triples to the assets we ship — `*-linux-gnu` → the static `*-linux-musl` build, `*-pc-windows-msvc` → `*-gnu`/`-gnullvm` `.exe` (macOS matches directly; the docs promise no automatic fallback). **Ordering:** publish at a **new tagged version whose release exists**, after the release — **not `0.1.0`** (diverges from the already-released 0.1.0 binaries that predate `--version`). Verified: `cargo publish --dry-run` packages + verify-builds; `cargo metadata` confirms the binstall block + 4 overrides. **Unverified:** a real `cargo binstall` run (not a dep; nothing on crates.io yet) — validate at first publish. Rejected: cargo-dist (GitHub-centric). Maintainer follow-ups: confirm © holder, add canonical `LICENSE-APACHE`, real binstall validation. **Amendment 1 (2026-06-18):** `0.2.0` **published live** (crates.io; `cargo install` + `cargo binstall` verified — the unverified-overrides caveat is resolved), via a new **manual `workflow_dispatch`** workflow `.gitea/workflows/publish.yaml` (mirrors `release-macos.yaml`; `tag` input; `cargo publish` with a crate-scoped `CARGO_REGISTRY_TOKEN` secret). Publish stays **manual** by decision — irreversible (keeps the token off every tag push), 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. Each registry is its **own idempotent job** (crates.io job no-ops if the version exists) so Scoop/Homebrew/winget can be added as sibling jobs without interfering. **Amendment 2 (2026-06-19):** **Scoop + Homebrew wired** (D3 §3b/§3c) as sibling `publish.yaml` jobs (`scoop-bucket`, `homebrew-tap`) that render manifests from the release `.sha256` sidecars and push to **org-level, multi-package** repos `lazyeval/scoop-bucket` + `lazyeval/homebrew-tap`. Credential: a scoped bot user **`lazyeval-ci`** (Gitea PATs scope by permission-category, not per-repo, so an `oli` token would over-reach to the main repo) on a `lazyeval` org team with Write to the package repos only; its PAT is the `LAZYEVAL_PKG_TOKEN` secret on `oli/rdbms-playground`. Render scripts (`scripts/render-{scoop-manifest,homebrew-formula}.sh`) are **dependency-free bash** (CI image `node:22-slim` has no jq/ruby), tested by `scripts/test-package-renders.sh`. Scoop: `#/`-rename fragment + `checkver`, no `autoupdate`. Homebrew: `on_macos`/`on_linux`×arch bare-binary formula, no Windows. **Unverified:** real `scoop`/`brew install`, the `HEAD:main` branch assumption, macOS Gatekeeper-via-brew (ad-hoc sign). **Remaining D3:** winget. diff --git a/docs/requirements.md b/docs/requirements.md index a240981..2f35b28 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -85,11 +85,16 @@ 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--` 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** + (ADR-0056 Amendment 2) — `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`, end-to-end + install still to be user-verified. **Remaining: winget** (komac on + Linux CI, or a manual PR). Asset naming + `rdbms-playground--` is binstall-friendly.)* ## TUI shell diff --git a/scripts/render-homebrew-formula.sh b/scripts/render-homebrew-formula.sh new file mode 100755 index 0000000..fcd8644 --- /dev/null +++ b/scripts/render-homebrew-formula.sh @@ -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, with or without a leading 'v' +# sha256 of aarch64-apple-darwin +# sha256 of x86_64-apple-darwin +# sha256 of aarch64-unknown-linux-musl +# sha256 of x86_64-unknown-linux-musl +set -euo pipefail + +if [ "$#" -ne 5 ]; then + echo "usage: $0 " >&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 < "rdbms-playground" + end + + test do + assert_match "rdbms-playground #{version}", shell_output("#{bin}/rdbms-playground --version") + end +end +EOF diff --git a/scripts/render-scoop-manifest.sh b/scripts/render-scoop-manifest.sh new file mode 100755 index 0000000..ae0e4f3 --- /dev/null +++ b/scripts/render-scoop-manifest.sh @@ -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, with or without a leading 'v' (e.g. 0.2.0 or v0.2.0) +# sha256 of the x86_64-pc-windows-gnu.exe asset +# sha256 of the aarch64-pc-windows-gnullvm.exe asset +set -euo pipefail + +if [ "$#" -ne 3 ]; then + echo "usage: $0 " >&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 <&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() { # + 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"