diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index 3383b71..f58dd92 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -198,6 +198,87 @@ jobs: 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. + # 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////) + 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. diff --git a/docs/adr/0056-crates-io-and-cargo-binstall.md b/docs/adr/0056-crates-io-and-cargo-binstall.md index 3985650..b4784a1 100644 --- a/docs/adr/0056-crates-io-and-cargo-binstall.md +++ b/docs/adr/0056-crates-io-and-cargo-binstall.md @@ -238,3 +238,82 @@ 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/`) 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**. diff --git a/docs/adr/README.md b/docs/adr/README.md index 7658481..7d48835 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. **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. **Amendment 3 (2026-06-19):** **Scoop + Homebrew validated end-to-end** on real Windows + macOS (install + run). Found + fixed a **presigned-URL/HEAD gotcha**: Gitea (≥1.25) 303-redirects release downloads to a **method-bound SigV4 presigned S3 URL**; Homebrew resolves via HEAD, captures the HEAD-signed URL, then GETs it → 403 (GET-only tools unaffected). Fix is a **server-side Caddy rule** (lives in the Gitea edge config, NOT this repo — record it: lost on a rebuild → brew 403s again) answering HEAD on `…/releases/download/…` directly (200, no redirect) so brew's download GET re-runs the redirect fresh. ad-hoc mac signature **runs** via brew; Developer-ID + notarization still parked on the Apple org conversion. **Remaining D3:** winget only. +- [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. **Amendment 3 (2026-06-19):** **Scoop + Homebrew validated end-to-end** on real Windows + macOS (install + run). Found + fixed a **presigned-URL/HEAD gotcha**: Gitea (≥1.25) 303-redirects release downloads to a **method-bound SigV4 presigned S3 URL**; Homebrew resolves via HEAD, captures the HEAD-signed URL, then GETs it → 403 (GET-only tools unaffected). Fix is a **server-side Caddy rule** (lives in the Gitea edge config, NOT this repo — record it: lost on a rebuild → brew 403s again) answering HEAD on `…/releases/download/…` directly (200, no redirect) so brew's download GET re-runs the redirect fresh. ad-hoc mac signature **runs** via brew; Developer-ID + notarization still parked on the Apple org conversion. **Remaining D3:** winget only. **Amendment 4 (2026-06-20):** **winget wired** (D3 §3d) — the 4th `publish.yaml` sibling job opens a PR to the central, human-gated `microsoft/winget-pkgs` via **komac** (pinned 2.16.0; `LazyEvaluation.RdbmsPlayground`, `portable`, x64+arm64). Async + Microsoft-validated, unlike the bucket/tap. Auth: a **classic `public_repo` PAT** (fine-grained can't open the cross-fork PR, komac #310) on a **dedicated GitHub bot** → `WINGET_GITHUB_TOKEN`, job-scoped, passed via curl config file (not argv). Idempotent via **two guards** (already-merged version + already-open PR) so re-dispatch is safe. Unsigned is fine to submit (portable; only MSIX needs signing) — may earn an AV/SmartScreen manual-review label first time. **One-time `komac new` bootstrap is manual** (interactive); CI `komac update` handles releases after. Completes the D3 package-manager set. diff --git a/docs/requirements.md b/docs/requirements.md index 5beb6ee..82b0b12 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -96,9 +96,13 @@ since ADR-0027.) 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. **Remaining: - winget** (komac on Linux CI, or a manual PR). Asset naming - `rdbms-playground--` is binstall-friendly.)* + 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--` is + binstall-friendly.)* ## TUI shell