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.