6d54c1e96c
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.
179 lines
9.6 KiB
Markdown
179 lines
9.6 KiB
Markdown
# ADR-0056: crates.io publish-readiness + `cargo binstall` metadata (D3)
|
||
|
||
## Status
|
||
|
||
Accepted — **prepared 2026-06-17** (plan:
|
||
`docs/plans/20260616-public-availability.md`, step 3a). The crate is made
|
||
**ready to publish** and carries `cargo-binstall` metadata. The actual
|
||
`cargo publish` is a gated maintainer step (see Ordering). First D3
|
||
package-manager mechanism; builds on ADR-0054 (versioned releases),
|
||
ADR-0055 (installer), ADR-ci-003 (release assets). Tracked by plan + ADR
|
||
(no Gitea issue — user decision).
|
||
|
||
## Context
|
||
|
||
`cargo binstall rdbms-playground` (and `cargo install`) need the crate on
|
||
**crates.io** (user decision, 2026-06-17). The manifest had
|
||
`publish = false`, a `readme = "README.md"` pointing at a **missing**
|
||
file, and no `keywords`/`categories`. Our release assets are **bare
|
||
binaries** (not archives) named `rdbms-playground-v<version>-<target>`
|
||
(`.exe` on Windows) with `.sha256` sidecars (ADR-ci-003); critically the
|
||
**release target triples differ from users' host triples** — we ship the
|
||
static `*-linux-musl` build (hosts are `*-linux-gnu`) and
|
||
`*-windows-gnu`/`-gnullvm` (hosts are `*-msvc`); only macOS matches.
|
||
|
||
## Decision
|
||
|
||
**Publish-readiness (this change):**
|
||
- Drop `publish = false`; add `homepage = "https://relplay.org"`,
|
||
`keywords`, `categories = ["command-line-utilities", "database"]`, and
|
||
an `exclude` (`/website`, `/docs`, `/.gitea`, `/.codegraph`) so the
|
||
published crate is code-only (585 files/8.3 MiB → 353/913 KiB
|
||
compressed).
|
||
- Author **`README.md`** (the `readme` target + crates.io front page;
|
||
engine-neutral and "simple/advanced mode" wording per ADR-0002 / the
|
||
website copy rules), with install instructions (curl|sh, binstall,
|
||
source, prebuilt).
|
||
- Add **`LICENSE-MIT`** and **`LICENSE-APACHE`** (the latter the verbatim
|
||
canonical text, added by the maintainer; both © Lazy Evaluation Ltd —
|
||
the publication entity), and a **`CONTRIBUTING.md`** stating the
|
||
"inbound = outbound" dual-license arrangement (so Apache-2.0 §5 makes
|
||
the §3 patent grant explicit on the self-hosted forge). Dual license
|
||
kept (not MIT-only) — user decision after reviewing the patent-grant
|
||
rationale.
|
||
|
||
**`cargo binstall` metadata** (`[package.metadata.binstall]`, syntax
|
||
verified against cargo-binstall SUPPORT.md):
|
||
- `pkg-fmt = "bin"` (bare binary), `bin-dir = "{ bin }{ binary-ext }"`,
|
||
and a base `pkg-url` using `v{ version }` (the `{ version }` placeholder
|
||
excludes the leading `v`).
|
||
- **Per-target `overrides`** mapping the common host triples to the asset
|
||
we actually publish: `x86_64`/`aarch64-unknown-linux-gnu` → the `-musl`
|
||
asset; `x86_64`/`aarch64-pc-windows-msvc` → the `-gnu`/`-gnullvm`
|
||
`.exe`. macOS needs no override (host triple == asset triple). The docs
|
||
do **not** promise automatic musl/gnu or msvc/gnu fallback, hence
|
||
explicit overrides.
|
||
|
||
**Ordering / gating (important):**
|
||
- `cargo publish` is **irreversible** (needs the crates.io token; a
|
||
version can't be un-published, only yanked) — a deliberate **maintainer
|
||
step**, not done here.
|
||
- binstall's `pkg-url` resolves to a **tagged release's** assets, so
|
||
publish **at a new tagged version whose release already exists**, and
|
||
publish **after** that release is built. **Do not publish `0.1.0`** — it
|
||
would diverge from the already-released `0.1.0` binaries (which predate
|
||
`--version`, ADR-0054). The clean path: bump → tag → release builds →
|
||
`cargo publish`.
|
||
|
||
## Verification
|
||
|
||
- `cargo publish --dry-run --allow-dirty` packages + verify-builds cleanly
|
||
(353 files, 913 KiB compressed; no metadata errors).
|
||
- `cargo metadata` confirms the `binstall` block + all four `overrides`
|
||
parse.
|
||
- **Unverified:** an actual `cargo binstall` run — cargo-binstall isn't a
|
||
dependency and nothing is on crates.io yet. **Validate at the first
|
||
publish + matching release** (especially the windows-msvc→gnu and
|
||
linux-gnu→musl overrides).
|
||
|
||
## Consequences
|
||
|
||
- The crate can be published at the next tagged release with `cargo
|
||
publish` (+ the token); `cargo install rdbms-playground` and `cargo
|
||
binstall rdbms-playground` then work.
|
||
- Remaining D3: Scoop, Homebrew (`lazyeval` tap), winget (komac/manual) —
|
||
each a manifest + a per-release bump, tracked in the plan.
|
||
- 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).
|