Skip to content

feat(release): curl|sh installer + SHA256SUMS + build provenance#506

Merged
DorianZheng merged 7 commits into
mainfrom
ci/cli-release-tag-checksums
May 12, 2026
Merged

feat(release): curl|sh installer + SHA256SUMS + build provenance#506
DorianZheng merged 7 commits into
mainfrom
ci/cli-release-tag-checksums

Conversation

@DorianZheng

@DorianZheng DorianZheng commented May 12, 2026

Copy link
Copy Markdown
Member

Summary

End-to-end story for installing the boxlite CLI with curl | sh, plus the supply-chain integrity tooling around it.

Release artifacts (build-runtime.yml):

  • Per-file .sha256 sidecars next to every tarball (matches cargo-dist / uv / ripgrep convention; unlocks cargo-binstall out of the box).
  • Combined SHA256SUMS for batch verification.
  • GitHub Artifact Attestations via actions/attest-build-provenance@v2 — sigstore-backed, OIDC, no secrets. Verify with gh attestation verify <file> --repo boxlite-ai/boxlite.
  • A rendered, version-locked install.sh published as a release asset.

scripts/release/install.sh.template — POSIX-sh installer modeled on mise (mise.run) and uv (astral.sh/uv/install.sh). CI substitutes __VERSION__ and the three __SHA_*__ placeholders from the per-file sidecars at release time, then uploads the rendered install.sh.

User-facing one-liner:

curl -fsSL https://github.com/boxlite-ai/boxlite/releases/latest/download/install.sh | sh

# Or pin a version / install dir (env vars MUST sit on the sh side of the pipe):
curl -fsSL https://github.com/boxlite-ai/boxlite/releases/latest/download/install.sh \
  | BOXLITE_VERSION=v0.9.4 BOXLITE_INSTALL_DIR=/usr/local/bin sh

How install.sh behaves

  • Defaults BOXLITE_INSTALL_DIR=$HOME/.local/bin (no sudo needed).
  • Detects target via uname -s / uname -m; rejects macOS Intel and any other arch.
  • Fast path: verifies against the checksum embedded at render time. Falls back to fetching <artifact>.sha256 from the release for BOXLITE_VERSION pinning.
  • Atomic install: download to mktemp -d, verify, extract with tar --no-same-owner, then install -m 0755 into place. Trap cleans the tmpdir.

Adversarial review history

Codex adversarial review surfaced three findings on an earlier revision; all were validated and fixed in this branch:

  • High — initial push.tags: ['v*'] trigger silently broke SDK fan-out because softprops/action-gh-release@v2 creates releases via GITHUB_TOKEN, and GitHub's anti-recursion guard blocks the resulting release.published event from triggering downstream workflows. Reverted (f095516). Release flow is now git push tag && gh release create ....
  • High — installer's tar -xzf … && mv && chmod could plant a /usr/local/bin/boxlite owned by the CI runner UID when run under sudo (privilege escalation on hosts where the user's UID matches the runner's). starship has the same bug; uv/mise dodge by never recommending the root path. Switched to tar --no-same-owner + install -m 0755 (61a8d07).
  • Medium — README pinning snippet placed env vars before curl, so they decorated curl, not sh. POSIX rule: VAR=val cmd1 | cmd2 only sets VAR for cmd1. Fixed in 61a8d07.

Test plan

  • Tag a throwaway release (git push origin v0.0.0-rc1 && gh release create v0.0.0-rc1 --generate-notes --verify-tag) on a fork.
  • Confirm release assets: three CLI tarballs + three runtime tarballs + matching .sha256 sidecars + SHA256SUMS + install.sh + provenance attestations.
  • sha256sum -c <tarball>.sha256 — expect OK.
  • gh attestation verify <tarball> --repo <fork> — expect a successful match.
  • Confirm SDK workflows (build-wheels, build-node, build-c) fired on the same release: published event.
  • macOS arm64: curl -fsSL <release>/install.sh | sh. Expect ~/.local/bin/boxlite --version matches the tag.
  • Linux x86_64 (docker run -it ubuntu:22.04 …): same.
  • Sad path: curl <release>/install.sh | BOXLITE_VERSION=v9.9.9 sh — must fail clearly on missing tarball.
  • Sad path: simulate macOS Intel (or run on one) — must reject with the documented error.
  • Root-install ownership: sudo BOXLITE_INSTALL_DIR=/usr/local/bin sh install.sh then ls -l /usr/local/bin/boxlite — owner root, mode 0755.

Consolidates the previously-stacked PR #507 into this branch.

@DorianZheng DorianZheng changed the title ci(release): publish boxlite-cli on v* tags with SHA256SUMS ci(release): add SHA256SUMS + per-file sidecars + build provenance attestation May 12, 2026
@DorianZheng

Copy link
Copy Markdown
Member Author

Adversarial review (Codex) caught a real fan-out bug in the original tag-trigger design — reverted in f095516. See updated PR body for full rationale.

@DorianZheng DorianZheng changed the title ci(release): add SHA256SUMS + per-file sidecars + build provenance attestation feat(release): curl|sh installer + SHA256SUMS + build provenance May 12, 2026
Triggers build-runtime on push of `v*` tags so a single git tag produces
the GitHub Release plus all artifacts, no manual `gh release create` step
needed. Adds a SHA256SUMS file covering every published tarball so a future
`curl | sh` installer can verify integrity. Updates the stale workflow
header comment (it mentioned only runtime tarballs and omitted the CLI
output) and documents the prebuilt-binary install path in README.md
alongside the existing `cargo install` instructions.
…tation

Aligns boxlite's release artifacts with the 2026 cargo-dist / uv / ripgrep
convention. Per-file `.sha256` sidecars are what `cargo-binstall` and most
shell-based installers look for by default, and GitHub Artifact Attestations
(`actions/attest-build-provenance@v2`, OIDC-signed, no secrets) give users a
provenance check via `gh attestation verify` without us having to manage GPG
keys. The combined `SHA256SUMS` stays for batch verification.

README adds a short verification snippet pointing at the new sidecar and
attestation.
Adversarial review (Codex) flagged: when softprops/action-gh-release@v2
creates a release using GITHUB_TOKEN, the resulting release.published event
does NOT trigger downstream workflows. This is GitHub's anti-recursion guard,
confirmed in both softprops' README and GitHub Actions docs. The push.tags
trigger added in d9a0625 therefore silently broke fan-out to build-wheels,
build-node, build-c, and build-go — a tag would publish a release with the
CLI/runtime but no Python wheels, npm packages, or C archives.

Revert to release.published-only gating. The two-command release flow still
works fine:

    git push origin v0.9.4
    gh release create v0.9.4 --generate-notes --verify-tag

(The user's own token, not GITHUB_TOKEN, creates that release event, so all
SDK workflows fan out correctly.)
Adds `scripts/install.sh.template`, a POSIX-sh installer modeled after mise
and uv. CI substitutes `__VERSION__` and per-target `__SHA_*__` placeholders
from the just-generated `.sha256` sidecars and uploads the rendered
`install.sh` to the release. Users get:

    curl -fsSL https://github.com/boxlite-ai/boxlite/releases/latest/download/install.sh | sh

The script auto-detects target via uname, defaults to `$HOME/.local/bin`
(overridable with `BOXLITE_INSTALL_DIR`), verifies the tarball against the
embedded checksum on the fast path or fetches the `.sha256` sidecar for
non-current versions (`BOXLITE_VERSION=v...`), and atomically moves the
binary into place. Rejects macOS Intel and unsupported archs with a clear
error.

README's install block becomes a one-liner; the manual verify recipe
(sha256sum + gh attestation verify) stays underneath.
`scripts/release/install.sh.template` reflects what the file actually is:
release-time infrastructure consumed only by CI, not a script users run from
the repo. Matches the existing scripts/{build,ci,deploy,setup,images}/
subdir-by-purpose layout and leaves room for future release-side tooling
(release-note generator, version bumpers) alongside it.
…r pipe

Two Codex adversarial review findings:

1. The installer extracted with default tar settings and then `mv`'d the
   binary into place. When run with sudo for /usr/local/bin, tar's
   ownership-restore can plant a /usr/local/bin/boxlite owned by the CI
   runner's UID (501 on macOS runners, 1001 on Ubuntu) — which on many
   Linux desktops happens to be the user's own UID, making a privileged
   PATH binary writable by an unprivileged process. starship's installer
   has the same bug; uv and mise dodge it by never recommending the root
   path. We can do better with one line. Switched to
   `tar --no-same-owner` + `install -m 0755`, both portable across macOS
   BSD tools and GNU coreutils.

2. README's "pin a version" snippet placed BOXLITE_VERSION/INSTALL_DIR
   before `curl`. POSIX rule: `VAR=val cmd1 | cmd2` decorates only cmd1,
   so the variables never reached the `sh` process actually running the
   installer. Moved the env-var prefix to the sh side of the pipe.
@DorianZheng DorianZheng force-pushed the ci/cli-release-tag-checksums branch from 61a8d07 to 273ddf0 Compare May 12, 2026 10:42
… coverage

Three adversarial-review iterations tightened the installer:

- Atomic replace via stage-in-target + mv -f (GNU install truncates in place;
  macOS BSD install is already atomic, so this normalizes Linux behavior).
- install.sh itself now ships in SHA256SUMS, has its own .sha256 sidecar, and
  is included in the actions/attest-build-provenance@v2 subject set. Render
  step moved before sidecar generation so a single loop covers every release
  asset; sh -n install.sh runs as a gate before upload.
- BOXLITE_EXPECTED_SHA256 lets callers pin an out-of-band digest that takes
  precedence over the embedded fast-path and the remote sidecar; pinned
  installs without it emit a stderr warning explicitly naming the weaker
  trust boundary.
- scripts/release/test_install.sh runs as a CI gate from lint.yml (path-
  filtered on scripts/release/** + build-runtime.yml) and as a pre-publish
  step inside build-runtime.yml against the freshly-rendered installer, so
  regressions on any of the above are caught before a release ships.
@DorianZheng DorianZheng merged commit e2039a4 into main May 12, 2026
20 checks passed
@DorianZheng DorianZheng deleted the ci/cli-release-tag-checksums branch May 12, 2026 11:29
Comment on lines +287 to +296
install_script:
name: Installer script smoke test
needs: changes
if: ${{ needs.changes.outputs.install_script == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Run installer script smoke test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants