Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/cli-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ----------
Expand Down Expand Up @@ -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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
12 changes: 5 additions & 7 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <shell>`. Version
# tracks the clap major/minor (4.5). No default features — we only use
Expand Down
85 changes: 85 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -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 <https://crates.io/settings/tokens> 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<version> --pattern '*x86_64-linux.tar.gz' --pattern SHA256SUMS
grep x86_64-linux SHA256SUMS | sha256sum -c -
tar xzf bootintel-v<version>-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.
Loading