From 303691b2092d6af5061903650abfc172b3dcc30a Mon Sep 17 00:00:00 2001 From: BootIntel Agent Date: Sat, 26 Sep 2026 08:17:06 +0000 Subject: [PATCH] Make the crates publishable, and document where the token goes cargo install bootintel-cli was impossible for two reasons that had nothing to do with credentials, so handing over a token alone would not have worked. crates/cli carried publish = false, with a comment explaining it existed to satisfy cargo-deny's wildcard-dep check because bootintel-detectors was declared as a bare path dependency. The actual fix is to give that dependency both a path and a version: cargo uses the path locally and the version when packaging. Verified that cargo deny still reports advisories, bans, licenses and sources all ok afterwards, so the workaround is no longer earning its keep. The ordering is not optional and is easy to get wrong once: the library must be on the index before the binary crate can be packaged, otherwise cargo fails with "no matching package named bootintel-detectors". Reproduced here. The new publish-crates job does them in order and waits for the index to become consistent in between. The job is opt-in behind a publish_crates dispatch input and refuses to start without CARGO_REGISTRY_TOKEN, with an error naming the exact settings page. That check exists because the failure it prevents is unrecoverable: a crates.io version can be yanked but never reused, so a half-publish would burn 0.4.0 permanently. docs/releasing.md records the whole process, including the token scopes and the reason the Homebrew line must not be advertised before the tap exists. No install claim was added that does not work yet, which is the mistake that made three advertised channels dead ends in the first place. Also refreshed four stale cli-v0.3.1 references in the README, including the GitHub Action pin and the attestation-verify example. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/cli-release.yml | 60 ++++++++++++++++++++++ README.md | 10 ++-- crates/cli/Cargo.toml | 12 ++--- docs/releasing.md | 85 +++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 docs/releasing.md diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index d18da79..0db2aa8 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -46,6 +46,11 @@ on: required: false type: boolean default: false + publish_crates: + description: 'Also publish bootintel-detectors + bootintel-cli to crates.io (needs CARGO_REGISTRY_TOKEN)' + required: false + type: boolean + default: false # Provenance # ---------- @@ -308,3 +313,58 @@ jobs: subject-name: ghcr.io/bootintel/cli subject-digest: ${{ steps.docker_build.outputs.digest }} push-to-registry: true + + publish-crates: + name: publish to crates.io + needs: release + if: inputs.publish_crates && !inputs.dry_run + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install libudev + run: sudo apt-get update && sudo apt-get install -y libudev-dev + + # Fail loudly rather than half-publishing. A missing token here would + # otherwise surface as an opaque cargo auth error after the library + # had already gone out, and a crates.io version can never be reused. + - name: Require the registry token + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then + echo "::error::CARGO_REGISTRY_TOKEN is not set. Add it at" \ + "Settings > Secrets and variables > Actions > New repository secret." \ + "Create the token at https://crates.io/settings/tokens with publish-new + publish-update scope." + exit 1 + fi + + # Order matters and cannot be parallelised: bootintel-cli declares + # bootintel-detectors with both a path and a version, so the library + # must exist on the index before the binary crate can be packaged. + - name: Publish bootintel-detectors + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p bootintel-detectors + + # The index is eventually consistent. Without this, the next publish + # fails with "no matching package named bootintel-detectors". + - name: Wait for the index to carry the new library + run: | + for i in $(seq 1 30); do + if cargo info bootintel-detectors@${{ inputs.version }} >/dev/null 2>&1; then + echo "index has bootintel-detectors ${{ inputs.version }}" + exit 0 + fi + sleep 10 + done + echo "::error::bootintel-detectors ${{ inputs.version }} never appeared on the index." + exit 1 + + - name: Publish bootintel-cli + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p bootintel-cli diff --git a/README.md b/README.md index f1721fe..512498e 100644 --- a/README.md +++ b/README.md @@ -99,19 +99,21 @@ cargo install --path crates/cli --features tui **GitHub Actions** (reusable composite action): ```yaml -- uses: bootintel/cli/.github/actions/bootintel-scan@cli-v0.3.1 +- uses: bootintel/cli/.github/actions/bootintel-scan@cli-v0.4.0 with: log-file: artifacts/boot.log format: sarif output-file: bootintel.sarif gate-critical: true - version: 0.3.1 # pin the binary too + version: 0.4.0 # pin the binary too ``` -Pin to a release tag (`@cli-v0.3.1`) or a commit SHA — **never `@main`** (a compromised `main` would execute arbitrary shell in every consumer's pipeline). +Pin to a release tag (`@cli-v0.4.0`) or a commit SHA — **never `@main`** (a compromised `main` would execute arbitrary shell in every consumer's pipeline). **Homebrew:** the formula template lives at `packaging/homebrew/bootintel.rb`. A public `bootintel/homebrew-tap` for `brew install bootintel` is planned. +**crates.io:** `cargo install bootintel-cli` is not available yet. The crates are publishable and the release workflow has the job wired, pending a registry token. See [docs/releasing.md](docs/releasing.md). + **Windows:** the one-liner above downloads + SHA256-verifies the latest release, extracts `bootintel.exe` into `$env:USERPROFILE\.local\bin`, and prints a `setx PATH` line if that dir isn't already on your PATH. Override with `$env:BOOTINTEL_VERSION` / `$env:BOOTINTEL_INSTALL_DIR`, or use `$env:BOOTINTEL_TARBALL` for offline installs. Currently x86_64 only — ARM64 users need `cargo install --path crates/cli --features tui`. ### Verify a download @@ -125,7 +127,7 @@ sha256sum -c SHA256SUMS --ignore-missing Every archive also carries a signed [build-provenance attestation](https://docs.github.com/actions/security-for-github-actions/using-artifact-attestations). That binds the artifact to the workflow, repository and commit that built it — a stronger statement than a code-signing certificate, which only says an organisation paid a CA: ```sh -gh attestation verify bootintel-v0.3.1-x86_64-linux.tar.gz --repo BootIntel/cli +gh attestation verify bootintel-v0.4.0-x86_64-linux.tar.gz --repo BootIntel/cli ``` Container images are attested the same way: diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 55d9785..1d3da46 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -9,19 +9,17 @@ authors.workspace = true repository.workspace = true homepage.workspace = true readme = "README.md" -# Not published to crates.io — bootintel is distributed via binary -# releases (install.sh / install.ps1 / Homebrew / Nix). Marked here -# so cargo-deny's wildcard-dep check accepts the path dep on -# bootintel-detectors (crates.io rejects wildcard path deps for -# published crates; that's not our distribution path). -publish = false [[bin]] name = "bootintel" path = "src/main.rs" [dependencies] -bootintel-detectors = { path = "../detectors" } +# Both path AND version. cargo uses the path for local builds and the +# version when packaging for crates.io, which rejects a bare path dep. +# This is what publish = false was working around; publishing +# bootintel-detectors first makes the workaround unnecessary. +bootintel-detectors = { path = "../detectors", version = "0.4.0" } clap = { version = "4.5", features = ["derive", "wrap_help", "env"] } # Shell-completion emitters for `bootintel completions `. Version # tracks the clap major/minor (4.5). No default features — we only use diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..af4ab7d --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,85 @@ +# Releasing bootintel-cli + +The release workflow is `workflow_dispatch` only. Nothing ships on a push, and +no tag is created automatically, so a stray tag can never publish a build. + +## One-time setup + +### crates.io token + +`cargo install bootintel-cli` needs the crates published, and publishing needs +a registry token. It is NOT in the repository and cannot be, so it lives as a +GitHub Actions secret. + +1. Go to and create a token. + Scopes: `publish-new` and `publish-update`. Nothing else. + Name it something traceable, for example `github-actions-bootintel-cli`. +2. In this repository, go to + **Settings > Secrets and variables > Actions > New repository secret**. +3. Name it exactly `CARGO_REGISTRY_TOKEN`. Paste the token as the value. + +That name is what `.github/workflows/cli-release.yml` reads. Until the secret +exists, the `publish-crates` job fails immediately with a message pointing +here, rather than part-publishing and leaving a version that can never be +reused. crates.io versions are permanent: a wrong publish cannot be deleted, +only yanked. + +The token is never echoed by the workflow and is only passed as an environment +variable to the two `cargo publish` steps. + +### Homebrew tap + +`brew install bootintel` needs a public `BootIntel/homebrew-tap` repository. +The formula template is already written at `packaging/homebrew/bootintel.rb`. +Until that repository exists, do not advertise the brew line anywhere: an +install command that fails reads as an abandoned project, which is worse than +having no brew line at all. + +## Cutting a release + +1. Bump `version` in the workspace `Cargo.toml`, run `cargo update -w` so the + lockfile follows, and promote the `[Unreleased]` changelog section. + Versioning policy is documented at the top of `CHANGELOG.md`. The crate is + pre-1.0, so the leading zero is the major component: a breaking change + moves the minor, not the major. +2. Merge that through a pull request with CI green. +3. Dispatch `cli-release` from `main` with the version, no leading `v`. + Leave `publish_crates` off for a first pass if you want to inspect the + binaries before anything reaches crates.io. +4. The workflow builds five targets, attaches `SHA256SUMS`, attests build + provenance, and creates a **draft** release. +5. Verify before publishing the draft. At minimum, download one archive and + check it against the published checksum, then run the binary: + + ```sh + gh release download cli-v --pattern '*x86_64-linux.tar.gz' --pattern SHA256SUMS + grep x86_64-linux SHA256SUMS | sha256sum -c - + tar xzf bootintel-v-x86_64-linux.tar.gz && ./bootintel --version + ``` + + Checking the checksum matters more than it looks: it proves the artifact and + the checksum came from the same build, which is the whole point of shipping + both. +6. Publish the draft and mark it latest. That creates the tag on the released + commit. + +## Publishing to crates.io + +Order is not optional. `bootintel-cli` declares `bootintel-detectors` with +both a path and a version, so the library must be on the index before the +binary crate can be packaged. `cargo package -p bootintel-cli` fails with +`no matching package named bootintel-detectors` otherwise. + +The `publish-crates` job does this in order, waits for the index to become +consistent, and then publishes the binary crate. Dispatch the workflow with +`publish_crates` checked, or run it by hand: + +```sh +cargo publish -p bootintel-detectors +# wait for the index +cargo publish -p bootintel-cli +``` + +After the first successful publish, update the Install section of `README.md` +to offer `cargo install bootintel-cli` instead of the from-source line. Do not +add it before then.