From 0f17fcc255ecb53c11410fa2a2831dcec663ceba Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:37:30 +0000 Subject: [PATCH 01/10] chore(ci): adopt mbx for Rust builds --- .github/actions/mbx/action.yml | 13 +++ .github/workflows/mbx-dogfood.yml | 130 ------------------------------ .github/workflows/test.yml | 27 +++---- AGENTS.md | 11 +++ CONTRIBUTING.md | 19 +++++ mise.lock | 39 +++++++++ mise.toml | 7 +- 7 files changed, 98 insertions(+), 148 deletions(-) create mode 100644 .github/actions/mbx/action.yml delete mode 100644 .github/workflows/mbx-dogfood.yml diff --git a/.github/actions/mbx/action.yml b/.github/actions/mbx/action.yml new file mode 100644 index 000000000..2c086a7e8 --- /dev/null +++ b/.github/actions/mbx/action.yml @@ -0,0 +1,13 @@ +name: Set up mbx +description: Select the trusted jdx server or fork-safe GitHub cache backend + +runs: + using: composite + steps: + - uses: jdx/mr-boxington-action@2bbb8f141e40e388b509cc68fe0bfbe7bc4b6818 # v1 + with: + version: 0.3.0 + backend: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'github' || 'server' }} + server-url: https://cache.mise.jdx.dev + namespace: jdx/usage + oidc-audience: https://cache.mise.jdx.dev diff --git a/.github/workflows/mbx-dogfood.yml b/.github/workflows/mbx-dogfood.yml deleted file mode 100644 index 80a8ae082..000000000 --- a/.github/workflows/mbx-dogfood.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: mbx-dogfood - -# Measures jdx/mr-boxington's build cache against this workspace, which is a -# real 26-member graph rather than a fixture. -# -# Deliberately additive and self-contained: it shares no cache, no key and no -# job with `test`, so nothing here can slow or break the real pipeline, and -# deleting this one file stops it entirely. It is an experiment on someone -# else's build, and it should stay that cheap to abandon. - -on: - push: - branches: [main] - # So a change to this file is exercised by the PR that makes it. - pull_request: - paths: [".github/workflows/mbx-dogfood.yml"] - workflow_dispatch: - -permissions: {} - -concurrency: - group: mbx-dogfood-${{ github.ref }} - cancel-in-progress: true - -env: - # Pinned: an unannounced mbx release changing its hit rate would land here as - # a step change indistinguishable from something this repository did. - MBX_VERSION: v0.3.0 - CARGO_TERM_COLOR: always - -jobs: - dogfood: - runs-on: ubuntu-latest - timeout-minutes: 45 - permissions: - contents: read - # Exchanged for a short-lived cache-server credential. mbx only publishes - # from a protected-branch push, so pull requests read the remote and never - # write to it — a fork cannot poison this cache. - id-token: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: recursive - persist-credentials: false - - - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable - - # No Swatinem/rust-cache, on purpose. It restores the whole target - # directory, so cargo would recompile nothing and mbx would have nothing - # to do: the run would look fast and measure zero. Absence here is the - # experiment, not an oversight. - - - name: Set up mbx with the remote cache - uses: jdx/mr-boxington-action@2bbb8f141e40e388b509cc68fe0bfbe7bc4b6818 # v1.0.0 - with: - backend: server - version: ${{ env.MBX_VERSION }} - server-url: https://cache.mise.jdx.dev - namespace: jdx/usage - oidc-audience: https://cache.mise.jdx.dev - - # `--no-run` compiles every crate and every test binary without running - # them, which is the whole dependency graph and none of the shell, - # submodule and fixture setup the real suite needs. This job measures - # compilation, so it should fail only when compilation fails. - - name: Cold build - env: - MBX_STATS_REPORT: ${{ runner.temp }}/cold.json - run: | - start=$SECONDS - mbx build test --all --all-features --no-run - echo "COLD_SECONDS=$((SECONDS - start))" >> "$GITHUB_ENV" - - # The deletion is the assertion, not cleanup. A second build that kept - # its target directory would be measuring cargo's freshness checks - # instead of the cache. - - name: Discard the target directory - run: rm -rf target - - - name: Warm build - env: - MBX_STATS_REPORT: ${{ runner.temp }}/warm.json - run: | - start=$SECONDS - mbx build test --all --all-features --no-run - echo "WARM_SECONDS=$((SECONDS - start))" >> "$GITHUB_ENV" - - - name: Summarize - if: always() - run: | - set -euo pipefail - row() { - local label="$1" file="$2" seconds="${3:-}" - [ -f "$file" ] || return 0 - jq -r --arg label "$label" --arg seconds "$seconds" ' - "| \($label) | \($seconds)s | \(.lookups) | \(.hits) | \(.misses) | " + - "\(if .lookups > 0 then (100 * .hits / .lookups | floor | tostring) + "%" else "n/a" end) | " + - "\((.bypasses | add) // 0) | \(.downloaded_bytes) | \(.uploaded_bytes) |" - ' "$file" - } - { - echo "### mbx $MBX_VERSION on this workspace" - echo - echo "| build | wall | lookups | hits | misses | hit rate | bypassed | down | up |" - echo "| --- | --- | --- | --- | --- | --- | --- | --- | --- |" - row cold "${RUNNER_TEMP}/cold.json" "${COLD_SECONDS:-?}" - row warm "${RUNNER_TEMP}/warm.json" "${WARM_SECONDS:-?}" - echo - if [ -f "${RUNNER_TEMP}/warm.json" ]; then - echo "Bypasses by reason, warm build:" - echo '```' - jq -r '.bypasses | to_entries[] | "\(.value)\t\(.key)"' "${RUNNER_TEMP}/warm.json" - echo '```' - fi - } >> "$GITHUB_STEP_SUMMARY" - - # Without this the job goes green whether or not the cache did anything, - # which is the one outcome worth being told about. The warm build restores - # from the local store this job just populated, so this holds even on a - # pull request that is forbidden from writing to the remote. - - name: Assert the warm build restored something - run: | - set -euo pipefail - hits=$(jq -r '.hits' "${RUNNER_TEMP}/warm.json") - echo "warm build restored $hits actions" - if [ "$hits" -le 0 ]; then - echo "::error::the warm build restored nothing; the cache did not work" - exit 1 - fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 762310453..11e3cb912 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,19 +16,18 @@ env: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} # Without this, a hung step sits until GitHub's 6-hour job limit. timeout-minutes: 30 permissions: contents: read + id-token: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - shared-key: test + - uses: ./.github/actions/mbx - uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4 # Retries and a longer wait, rather than being allowed to fail. `cli`'s completion # tests refuse to skip a missing shell when `CI` is set — see @@ -101,9 +100,9 @@ jobs: # defaults — are compiled nowhere else, and rotted unnoticed until they were measured. - name: check the trimmed feature shapes still compile run: | - cargo clippy -p usage-lib --no-default-features -- -D warnings - cargo clippy -p usage-rs --no-default-features -- -D warnings - cargo clippy -p usage-config --no-default-features -- -D warnings + mbx build clippy -p usage-lib --no-default-features -- -D warnings + mbx build clippy -p usage-rs --no-default-features -- -D warnings + mbx build clippy -p usage-config --no-default-features -- -D warnings - run: mise r render # Same reasoning as `render`: the shadow is checked in, so a change to the derive's # vocabulary that would alter it has to be committed rather than discovered later. @@ -131,6 +130,7 @@ jobs: timeout-minutes: 30 permissions: contents: read + id-token: write steps: # Before the checkout, and not optional: there is no `.gitattributes`, and Windows git # defaults to `autocrlf=true`. The fixtures would arrive with CRLF and the help snapshots @@ -141,9 +141,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - shared-key: test-windows + - uses: ./.github/actions/mbx - uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4 - run: mise r build # `usage bash` runs whatever Windows resolves `bash` to, and `CreateProcess` searches the @@ -187,10 +185,11 @@ jobs: # `cargo check` rather than `cargo test`: dev-dependencies are not part of what an adopter # compiles, and holding them to the MSRV would pin the toolchain past what the library needs. msrv: - runs-on: ubuntu-latest + runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} timeout-minutes: 15 permissions: contents: read + id-token: write strategy: fail-fast: false matrix: @@ -245,13 +244,11 @@ jobs: # thing to keep current for no gain. - name: install the toolchain under test run: rustup toolchain install ${{ matrix.version }} --profile minimal - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - shared-key: msrv-${{ matrix.version }} + - uses: ./.github/actions/mbx - name: they build at the version they claim run: | for crate in ${{ matrix.crates }}; do - cargo +${{ matrix.version }} check --locked -p "$crate" --all-features + mbx build +${{ matrix.version }} check --locked -p "$crate" --all-features done # Single fan-in gate, so branch protection can require one stable check diff --git a/AGENTS.md b/AGENTS.md index 04f679bc7..66f5b9346 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,16 @@ # AGENTS.md +## mbx build cache + +Compilation-heavy mise tasks use `mbx`. If an mbx command fails or creates a +development papercut, rerun the exact equivalent `cargo` command from +`CONTRIBUTING.md`; this unblocks work without weakening the check. If Cargo +succeeds, surface the mismatch and recommend a +[mr-boxington Discussion](https://github.com/jdx/mr-boxington/discussions) with +the repository and commit, OS, `mbx --version`, both commands and outputs, the +cache summary, and `MBX_BYPASS_LOG` details when relevant. Do not silently make +Cargo the permanent path, and do not post externally without user authorization. + This file provides guidance to coding agents working in this repository. ## Conventional Commits diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5499d0f3b..b6bd9bc88 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,22 @@ # Contributing See the [contributing guide](https://usage.jdx.dev/contributing). + +## mbx build cache + +The normal `mise run build`, `mise run test`, and `mise run lint:clippy` +workflows use [mbx](https://mr-boxington.jdx.dev) for compilation-heavy Cargo +work. If mbx appears to be the problem, use the equivalent Cargo commands to +unblock yourself without skipping or weakening the check: + +```sh +cargo build --all +cargo test --all --all-features +cargo clippy --all --all-features --all-targets -- -D warnings +``` + +If Cargo succeeds where mbx fails, or mbx introduces a papercut, please start a +[mr-boxington Discussion](https://github.com/jdx/mr-boxington/discussions). +Include the repository and commit, operating system, `mbx --version`, both +commands and their output, the mbx cache summary, and an `MBX_BYPASS_LOG` when +relevant (for example, `MBX_BYPASS_LOG=mbx-bypasses.log mise run build`). diff --git a/mise.lock b/mise.lock index 7709b88b2..c3e87b4d3 100644 --- a/mise.lock +++ b/mise.lock @@ -345,6 +345,45 @@ url = "https://github.com/crate-ci/cargo-release/releases/download/v1.1.5/cargo- url_api = "https://api.github.com/repos/crate-ci/cargo-release/releases/assets/510347954" provenance = "github-attestations" +[[tools."github:jdx/mr-boxington"]] +version = "0.3.0" +backend = "github:jdx/mr-boxington" + +[tools."github:jdx/mr-boxington"."platforms.linux-arm64"] +checksum = "sha256:4be02935fb0c1892b659c8146e07fe092fb12f6c4e86c6ebbc3b63449d5dcb25" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704880" + +[tools."github:jdx/mr-boxington"."platforms.linux-arm64-musl"] +checksum = "sha256:4be02935fb0c1892b659c8146e07fe092fb12f6c4e86c6ebbc3b63449d5dcb25" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704880" + +[tools."github:jdx/mr-boxington"."platforms.linux-x64"] +checksum = "sha256:65ef46c20761ffd1d930638c3ff2d237535a98f28968929f04f195bfdd258e77" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704890" + +[tools."github:jdx/mr-boxington"."platforms.linux-x64-musl"] +checksum = "sha256:65ef46c20761ffd1d930638c3ff2d237535a98f28968929f04f195bfdd258e77" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704890" + +[tools."github:jdx/mr-boxington"."platforms.macos-arm64"] +checksum = "sha256:1803cec79be0b753fc3af23044edc46fd81ffdf9d66fcb7b6637cfb22cc6db4a" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704881" + +[tools."github:jdx/mr-boxington"."platforms.macos-x64"] +checksum = "sha256:72a98d019b83859e465d30a91422e7b2eb72cf53fb42e116a277865310392c0c" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-x86_64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704883" + +[tools."github:jdx/mr-boxington"."platforms.windows-x64"] +checksum = "sha256:9d99f1f57789e88f4870ac61fd6c4e2ceec5ed532f4c9c8e9be4f32663a1fd16" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-x86_64-pc-windows-msvc.zip" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704879" + [[tools."github:jdx/tak"]] version = "0.0.5" backend = "github:jdx/tak" diff --git a/mise.toml b/mise.toml index 6fb39aff1..161dde2d8 100644 --- a/mise.toml +++ b/mise.toml @@ -3,6 +3,7 @@ CARGO_TERM_COLOR = 'always' _.path = ["./target/debug"] [tools] +"github:jdx/mr-boxington" = "latest" # Pinned rather than "latest" because tak is the measuring instrument: an # upgrade that changes how it samples would land in the series as a step change # in usage's numbers, indistinguishable from a real regression. Bump deliberately. @@ -30,7 +31,7 @@ depends = ["render", "lint-fix", "snapshots"] [tasks.build] sources = ['{cli/,}src/**/*.rs', '{cli/,}Cargo.toml'] outputs = ['target/debug/rtx'] -run = 'cargo build --all' +run = 'mbx build build --all' [tasks.cli] alias = ['x'] @@ -61,7 +62,7 @@ run = 'aube install && aube run docs:dev' [tasks.test] alias = 't' -run = 'cargo test --all --all-features' +run = 'mbx build test --all --all-features' # The Go parser answers the same corpus the Rust one does, so its suite needs the # `usage` CLI: a vector's spec is KDL, and lowering it is the CLI's job rather than @@ -85,7 +86,7 @@ run = 'actionlint' [tasks."lint:prettier"] run = "prettier -c ." [tasks."lint:clippy"] -run = 'cargo clippy --all --all-features --all-targets -- -D warnings' +run = 'mbx build clippy --all --all-features --all-targets -- -D warnings' [tasks."lint:fmt"] run = 'cargo fmt --all -- --check' [tasks."lint:semver"] From 3e34c6aa2cd6827081cbda618597c778a6248313 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:56:10 +0000 Subject: [PATCH 02/10] fix: use direct mbx command syntax --- .github/workflows/test.yml | 8 ++++---- mise.toml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11e3cb912..0099a826d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -100,9 +100,9 @@ jobs: # defaults — are compiled nowhere else, and rotted unnoticed until they were measured. - name: check the trimmed feature shapes still compile run: | - mbx build clippy -p usage-lib --no-default-features -- -D warnings - mbx build clippy -p usage-rs --no-default-features -- -D warnings - mbx build clippy -p usage-config --no-default-features -- -D warnings + mbx clippy -p usage-lib --no-default-features -- -D warnings + mbx clippy -p usage-rs --no-default-features -- -D warnings + mbx clippy -p usage-config --no-default-features -- -D warnings - run: mise r render # Same reasoning as `render`: the shadow is checked in, so a change to the derive's # vocabulary that would alter it has to be committed rather than discovered later. @@ -248,7 +248,7 @@ jobs: - name: they build at the version they claim run: | for crate in ${{ matrix.crates }}; do - mbx build +${{ matrix.version }} check --locked -p "$crate" --all-features + mbx +${{ matrix.version }} check --locked -p "$crate" --all-features done # Single fan-in gate, so branch protection can require one stable check diff --git a/mise.toml b/mise.toml index 161dde2d8..4db9f6235 100644 --- a/mise.toml +++ b/mise.toml @@ -31,7 +31,7 @@ depends = ["render", "lint-fix", "snapshots"] [tasks.build] sources = ['{cli/,}src/**/*.rs', '{cli/,}Cargo.toml'] outputs = ['target/debug/rtx'] -run = 'mbx build build --all' +run = 'mbx build --all' [tasks.cli] alias = ['x'] @@ -62,7 +62,7 @@ run = 'aube install && aube run docs:dev' [tasks.test] alias = 't' -run = 'mbx build test --all --all-features' +run = 'mbx test --all --all-features' # The Go parser answers the same corpus the Rust one does, so its suite needs the # `usage` CLI: a vector's spec is KDL, and lowering it is the CLI's job rather than @@ -86,7 +86,7 @@ run = 'actionlint' [tasks."lint:prettier"] run = "prettier -c ." [tasks."lint:clippy"] -run = 'mbx build clippy --all --all-features --all-targets -- -D warnings' +run = 'mbx clippy --all --all-features --all-targets -- -D warnings' [tasks."lint:fmt"] run = 'cargo fmt --all -- --check' [tasks."lint:semver"] From 91e0a20c96963ad01352a5ed50671aef2a9c9265 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:11:56 +0000 Subject: [PATCH 03/10] fix: update mbx to 0.4.0 --- .github/actions/mbx/action.yml | 2 +- mise.lock | 44 +++++++++++++++++----------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/actions/mbx/action.yml b/.github/actions/mbx/action.yml index 2c086a7e8..d938511d6 100644 --- a/.github/actions/mbx/action.yml +++ b/.github/actions/mbx/action.yml @@ -6,7 +6,7 @@ runs: steps: - uses: jdx/mr-boxington-action@2bbb8f141e40e388b509cc68fe0bfbe7bc4b6818 # v1 with: - version: 0.3.0 + version: 0.4.0 backend: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'github' || 'server' }} server-url: https://cache.mise.jdx.dev namespace: jdx/usage diff --git a/mise.lock b/mise.lock index c3e87b4d3..423fc70bc 100644 --- a/mise.lock +++ b/mise.lock @@ -346,43 +346,43 @@ url_api = "https://api.github.com/repos/crate-ci/cargo-release/releases/assets/5 provenance = "github-attestations" [[tools."github:jdx/mr-boxington"]] -version = "0.3.0" +version = "0.4.0" backend = "github:jdx/mr-boxington" [tools."github:jdx/mr-boxington"."platforms.linux-arm64"] -checksum = "sha256:4be02935fb0c1892b659c8146e07fe092fb12f6c4e86c6ebbc3b63449d5dcb25" -url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-aarch64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704880" +checksum = "sha256:ed81dab87775bcc8764c7257d8bea0dd8b7f811d6263b71bc7cd83a879661593" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.4.0/mbx-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/529719396" [tools."github:jdx/mr-boxington"."platforms.linux-arm64-musl"] -checksum = "sha256:4be02935fb0c1892b659c8146e07fe092fb12f6c4e86c6ebbc3b63449d5dcb25" -url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-aarch64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704880" +checksum = "sha256:ed81dab87775bcc8764c7257d8bea0dd8b7f811d6263b71bc7cd83a879661593" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.4.0/mbx-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/529719396" [tools."github:jdx/mr-boxington"."platforms.linux-x64"] -checksum = "sha256:65ef46c20761ffd1d930638c3ff2d237535a98f28968929f04f195bfdd258e77" -url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-x86_64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704890" +checksum = "sha256:b288265404b8fa4620ea1d082ba9b33a0a1695212d88a385e76aa07a743da250" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.4.0/mbx-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/529719409" [tools."github:jdx/mr-boxington"."platforms.linux-x64-musl"] -checksum = "sha256:65ef46c20761ffd1d930638c3ff2d237535a98f28968929f04f195bfdd258e77" -url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-x86_64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704890" +checksum = "sha256:b288265404b8fa4620ea1d082ba9b33a0a1695212d88a385e76aa07a743da250" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.4.0/mbx-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/529719409" [tools."github:jdx/mr-boxington"."platforms.macos-arm64"] -checksum = "sha256:1803cec79be0b753fc3af23044edc46fd81ffdf9d66fcb7b6637cfb22cc6db4a" -url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-aarch64-apple-darwin.tar.gz" -url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704881" +checksum = "sha256:416cab92e23c4652183e4a794af3fdcbc50b56296d8c09f8b6548e7577416307" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.4.0/mbx-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/529719398" [tools."github:jdx/mr-boxington"."platforms.macos-x64"] -checksum = "sha256:72a98d019b83859e465d30a91422e7b2eb72cf53fb42e116a277865310392c0c" -url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-x86_64-apple-darwin.tar.gz" -url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704883" +checksum = "sha256:9f1016b0592ffd3b4b4000640223e10a2100cc6136d722fac5c4829cb89fc7ed" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.4.0/mbx-x86_64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/529719400" [tools."github:jdx/mr-boxington"."platforms.windows-x64"] -checksum = "sha256:9d99f1f57789e88f4870ac61fd6c4e2ceec5ed532f4c9c8e9be4f32663a1fd16" -url = "https://github.com/jdx/mr-boxington/releases/download/v0.3.0/mbx-x86_64-pc-windows-msvc.zip" -url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/526704879" +checksum = "sha256:f841b1907cf86c54db4d3d2d1e88f87303ccc8f720efff90b6331d7d5592bf4e" +url = "https://github.com/jdx/mr-boxington/releases/download/v0.4.0/mbx-x86_64-pc-windows-msvc.zip" +url_api = "https://api.github.com/repos/jdx/mr-boxington/releases/assets/529719397" [[tools."github:jdx/tak"]] version = "0.0.5" From d94b4d826670f16ce817f5ad6101ff00eb150503 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:03:40 +0000 Subject: [PATCH 04/10] fix(ci): support namespace and dependabot runners --- .github/actions/mbx/action.yml | 2 +- .github/workflows/test.yml | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/actions/mbx/action.yml b/.github/actions/mbx/action.yml index d938511d6..133a66022 100644 --- a/.github/actions/mbx/action.yml +++ b/.github/actions/mbx/action.yml @@ -7,7 +7,7 @@ runs: - uses: jdx/mr-boxington-action@2bbb8f141e40e388b509cc68fe0bfbe7bc4b6818 # v1 with: version: 0.4.0 - backend: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'github' || 'server' }} + backend: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'github' || 'server' }} server-url: https://cache.mise.jdx.dev namespace: jdx/usage oidc-audience: https://cache.mise.jdx.dev diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b55d0fca..ec0bc16bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ env: jobs: test: - runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} + runs-on: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} # Without this, a hung step sits until GitHub's 6-hour job limit. timeout-minutes: 30 permissions: @@ -54,7 +54,9 @@ jobs: - name: Install shells and bash-completion for completion integration tests timeout-minutes: 5 run: | - sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt + if [[ -f /etc/apt/apt-mirrors.txt ]]; then + sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt + fi sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y zsh fish bash-completion # pwsh is pre-installed on GitHub ubuntu-latest images. Self-heal @@ -183,7 +185,7 @@ jobs: # `cargo check` rather than `cargo test`: dev-dependencies are not part of what an adopter # compiles, and holding them to the MSRV would pin the toolchain past what the library needs. msrv: - runs-on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} + runs-on: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} timeout-minutes: 15 permissions: contents: read From 546180f823b58d042928f11f1474c57353a1404b Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:10:25 +0000 Subject: [PATCH 05/10] fix(ci): install powershell without snap --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec0bc16bc..b9affd0e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,11 +59,11 @@ jobs: fi sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y zsh fish bash-completion - # pwsh is pre-installed on GitHub ubuntu-latest images. Self-heal - # if a future image drops it so the integration test still runs - # (rather than panicking under CI=1). + # pwsh is pre-installed on GitHub ubuntu-latest images. Namespace + # images have Microsoft's apt repository but not snap, so install + # from apt when it is absent. if ! command -v pwsh >/dev/null 2>&1; then - sudo snap install powershell --classic + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y powershell fi # A step of its own, and `always()`, because the interesting time to hear which # shells are present is the run where the one above did not finish — and a `set -e` From 717de7ddf8b32e826e3f4efd61feb961db66418e Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:50:08 +0000 Subject: [PATCH 06/10] fix: simplify required-unless predicates --- lib/src/parse.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/parse.rs b/lib/src/parse.rs index 8e8142c53..e4cf4a7cd 100644 --- a/lib/src/parse.rs +++ b/lib/src/parse.rs @@ -2434,9 +2434,9 @@ fn parse_partial_traced( && arg.required_unless_all.iter().all(|selector| { selector_is_explicit(selector, &out, &overridden_flags, custom_env) }); - let required_unless = (!arg.required_unless.is_empty() - || !arg.required_unless_all.is_empty()) - && !(unless_any || unless_all); + let required_unless = !(unless_any + || unless_all + || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty())); if (arg.required || required_if || required_if_eq @@ -2720,9 +2720,9 @@ fn parse_partial_traced( && flag.required_unless_all.iter().all(|selector| { selector_is_explicit(selector, &out, &overridden_flags, custom_env) }); - let required_unless = (!flag.required_unless.is_empty() - || !flag.required_unless_all.is_empty()) - && !(unless_any || unless_all); + let required_unless = !(unless_any + || unless_all + || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty())); if (flag.required || required_if || required_if_eq From 808241742b44e089630b9e79d23c2b4b1723a07f Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:59:27 +0000 Subject: [PATCH 07/10] chore: split clippy cleanup from mbx adoption --- lib/src/parse.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/parse.rs b/lib/src/parse.rs index e4cf4a7cd..8e8142c53 100644 --- a/lib/src/parse.rs +++ b/lib/src/parse.rs @@ -2434,9 +2434,9 @@ fn parse_partial_traced( && arg.required_unless_all.iter().all(|selector| { selector_is_explicit(selector, &out, &overridden_flags, custom_env) }); - let required_unless = !(unless_any - || unless_all - || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty())); + let required_unless = (!arg.required_unless.is_empty() + || !arg.required_unless_all.is_empty()) + && !(unless_any || unless_all); if (arg.required || required_if || required_if_eq @@ -2720,9 +2720,9 @@ fn parse_partial_traced( && flag.required_unless_all.iter().all(|selector| { selector_is_explicit(selector, &out, &overridden_flags, custom_env) }); - let required_unless = !(unless_any - || unless_all - || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty())); + let required_unless = (!flag.required_unless.is_empty() + || !flag.required_unless_all.is_empty()) + && !(unless_any || unless_all); if (flag.required || required_if || required_if_eq From d04658c8392894dd8a58891efc6f0b1f908d351b Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:07:33 +0000 Subject: [PATCH 08/10] security(ci): restrict trusted mbx runs to jdx --- .github/actions/mbx/action.yml | 2 +- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/mbx/action.yml b/.github/actions/mbx/action.yml index 133a66022..839b5796b 100644 --- a/.github/actions/mbx/action.yml +++ b/.github/actions/mbx/action.yml @@ -7,7 +7,7 @@ runs: - uses: jdx/mr-boxington-action@2bbb8f141e40e388b509cc68fe0bfbe7bc4b6818 # v1 with: version: 0.4.0 - backend: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'github' || 'server' }} + backend: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login != 'jdx') && 'github' || 'server' }} server-url: https://cache.mise.jdx.dev namespace: jdx/usage oidc-audience: https://cache.mise.jdx.dev diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9affd0e5..1050d1170 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ env: jobs: test: - runs-on: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} + runs-on: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login != 'jdx') && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} # Without this, a hung step sits until GitHub's 6-hour job limit. timeout-minutes: 30 permissions: @@ -185,7 +185,7 @@ jobs: # `cargo check` rather than `cargo test`: dev-dependencies are not part of what an adopter # compiles, and holding them to the MSRV would pin the toolchain past what the library needs. msrv: - runs-on: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} + runs-on: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login != 'jdx') && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} timeout-minutes: 15 permissions: contents: read From 9ffc46168f8668716cfa2100dd54fe050b071ad3 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:37:41 +0000 Subject: [PATCH 09/10] security(ci): isolate mbx OIDC permissions --- .github/actions/mbx/action.yml | 7 +- .github/workflows/test-impl.yml | 279 +++++++++++++++++++++++++++++++ .github/workflows/test.yml | 280 +++----------------------------- 3 files changed, 312 insertions(+), 254 deletions(-) create mode 100644 .github/workflows/test-impl.yml diff --git a/.github/actions/mbx/action.yml b/.github/actions/mbx/action.yml index 839b5796b..035160092 100644 --- a/.github/actions/mbx/action.yml +++ b/.github/actions/mbx/action.yml @@ -1,13 +1,18 @@ name: Set up mbx description: Select the trusted jdx server or fork-safe GitHub cache backend +inputs: + backend: + description: Explicit backend for structurally trusted or untrusted callers + default: auto + runs: using: composite steps: - uses: jdx/mr-boxington-action@2bbb8f141e40e388b509cc68fe0bfbe7bc4b6818 # v1 with: version: 0.4.0 - backend: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login != 'jdx') && 'github' || 'server' }} + backend: ${{ inputs.backend != 'auto' && inputs.backend || ((github.actor != 'jdx' || (github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login != 'jdx'))) && 'github' || 'server') }} server-url: https://cache.mise.jdx.dev namespace: jdx/usage oidc-audience: https://cache.mise.jdx.dev diff --git a/.github/workflows/test-impl.yml b/.github/workflows/test-impl.yml new file mode 100644 index 000000000..9d40ddd04 --- /dev/null +++ b/.github/workflows/test-impl.yml @@ -0,0 +1,279 @@ +name: test implementation +on: + workflow_call: + inputs: + trusted: + required: true + type: boolean + mbx-backend: + required: true + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + MISE_EXPERIMENTAL: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + test: + runs-on: ${{ !inputs.trusted && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} + # Without this, a hung step sits until GitHub's 6-hour job limit. + timeout-minutes: 30 + steps: + - name: Assert OIDC is unavailable to untrusted CI + if: ${{ !inputs.trusted }} + run: | + test -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" + test -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + persist-credentials: false + - uses: ./.github/actions/mbx + with: + backend: ${{ inputs.mbx-backend }} + - uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5 + # Retries and a longer wait, rather than being allowed to fail. `cli`'s completion + # tests refuse to skip a missing shell when `CI` is set — see + # `skip_if_shell_missing` — which is a deliberate policy this step exists to keep: + # a run that quietly tested one shell instead of four is worse than a run that + # stopped. So the flakiness is what gets fixed, not the consequence of it. + # + # The failure mode, seen three times in an afternoon, is the azure mirror that + # GitHub's runners list first *stalling* rather than refusing. Retries alone did not + # help — an eight-minute attempt with them timed out the same way — because retrying + # a connection that hangs just hangs again. What was missing is a per-connection + # timeout: with one, apt gives up on the stalled mirror in seconds and moves to the + # next entry in `/etc/apt/apt-mirrors.txt`, which is the archive.ubuntu.com that was + # answering fine throughout. + # + # `bash-completion` is in the list for the same reason: usage no longer embeds a copy of + # it, so the generated bash completion needs the system one loaded, and + # `bash_completion_or_skip` panics under CI rather than skipping without it. The image + # already ships it; naming it here is what keeps that true if a future one stops. + # + # Deliberately the distro package rather than a newer build from source: `ubuntu-latest` + # is 24.04, whose 2.11 is the oldest version the generated script supports. Testing + # against the floor is the point — 26.04 ships 2.16, which would pass either way. + - name: Install shells and bash-completion for completion integration tests + timeout-minutes: 5 + run: | + if [[ -f /etc/apt/apt-mirrors.txt ]]; then + sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt + fi + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y zsh fish bash-completion + # pwsh is pre-installed on GitHub ubuntu-latest images. Namespace + # images have Microsoft's apt repository but not snap, so install + # from apt when it is absent. + if ! command -v pwsh >/dev/null 2>&1; then + sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y powershell + fi + # A step of its own, and `always()`, because the interesting time to hear which + # shells are present is the run where the one above did not finish — and a `set -e` + # script says nothing after the command that failed it. + - name: Report which shells are available + if: always() + run: | + probe_dir=$(mktemp -d) + trap 'rm -rf "$probe_dir"' EXIT + for shell in bash zsh fish pwsh; do + if [[ "$shell" == pwsh ]]; then + probe="$probe_dir/$shell.ps1" + else + probe="$probe_dir/$shell" + fi + printf 'echo ok\n' > "$probe" + if [[ "$shell" == pwsh ]]; then + output=$(pwsh -NoProfile -NonInteractive -File "$probe" 2>/dev/null) && usable=true || usable=false + else + output=$("$shell" "$probe" 2>/dev/null) && usable=true || usable=false + fi + if [[ "$usable" == true && "$output" == ok ]]; then + echo "$shell: $(command -v "$shell") (script probe passed)" + else + echo "::warning::$shell cannot run a script, so its completion tests cannot run" + fi + done + - run: mise r build + - run: mise r test + # Run here rather than in a job of its own: the corpus vectors carry KDL + # specs, and lowering them needs the `usage` CLI this job has already built. + - run: mise r test:go + # Everything else in this pipeline builds with `--all-features`, and resolver-v2 unification + # means a workspace build turns a feature on for every member as soon as one member wants it. + # So the shapes an adopter actually gets — usage-lib without `docs`, usage-rs without its + # defaults — are compiled nowhere else, and rotted unnoticed until they were measured. + - name: check the trimmed feature shapes still compile + run: | + mbx clippy -p usage-lib --no-default-features -- -D warnings + mbx clippy -p usage-rs --no-default-features -- -D warnings + mbx clippy -p usage-config --no-default-features -- -D warnings + - run: mise r render + # Same reasoning as `render`: the shadow is checked in, so a change to the derive's + # vocabulary that would alter it has to be committed rather than discovered later. + - run: mise r gen-shadow + # And the Go tables, for the same reason: they are checked in, so a change to + # the emitter that nobody meant has to show up as a diff here. + - run: mise r gen-go + - name: assert render, gen-shadow and gen-go produce no diff + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "::error::'mise run render', 'mise run gen-shadow' or 'mise run gen-go' produced changes. Run them locally and commit." + git status + git diff + exit 1 + fi + - run: mise r lint + + # The only job that is not Linux, and for several `cfg` branches the only place they are + # compiled at all. The argv path planner, the config path normaliser and + # `os_string_from_bytes` all exist for Windows and had never been built on it — each of + # #1232, #1233 and #1234 fixed something no other job could see. This is the job that keeps + # that true going forward rather than once. + test-windows: + runs-on: windows-latest + timeout-minutes: 30 + steps: + # Before the checkout, and not optional: there is no `.gitattributes`, and Windows git + # defaults to `autocrlf=true`. The fixtures would arrive with CRLF and the help snapshots + # in `lib/tests/parse.rs` and `cli/tests/markdown.rs` would fail on a difference + # `pretty_assertions` prints as two identical strings. + - run: git config --global core.autocrlf false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + persist-credentials: false + - uses: ./.github/actions/mbx + with: + backend: ${{ inputs.mbx-backend }} + - uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5 + - run: mise r build + # `usage bash` runs whatever Windows resolves `bash` to, and `CreateProcess` searches the + # system directory before `PATH`. On this runner that is `C:\Windows\System32\bash.exe` — + # the WSL launcher, with no distribution installed — so the mount fixtures cannot run. + # Naming Git Bash is what makes them run rather than skip. + # + # `USAGECLI_`, not `USAGE_`: mise strips `usage_*` from a task's environment by comparing + # six characters case-insensitively, and Windows variable names are case-insensitive, so + # `USAGE_SHELL_BASH` would not survive `mise r`. That collision is why the `USAGECLI_` + # spelling exists. + # + # Unquoted on purpose. In double quotes YAML reads `\b` as a backspace. + # + # If this ever stops arriving, the job fails rather than passing quietly: + # `skip_if_posix_shell_missing` panics under CI on every platform, so a run cannot skip + # its way to green. + - env: + USAGECLI_SHELL_BASH: C:\Program Files\Git\bin\bash.exe + run: mise r test + # No zsh, fish or bash-completion, and nothing to install them with: they have no Windows + # equivalent, which is why `skip_if_shell_missing` and `bash_completion_or_skip` limit + # their CI panic to Unix. Three tests skip here and the Linux job covers them. + # + # Linting here is not duplication of the Linux job. `#[cfg]` decides whether code exists, + # so a warning can be real on one platform and absent on the other — both of the ones + # #1234 fixed were invisible to every other job in this file. `lint:clippy` denies + # warnings and passes `--all-targets`, which makes those a build failure rather than noise. + - run: mise r lint:clippy + + # The declared `rust-version` is a promise to anyone depending on these crates, and nothing + # was checking it. It said 1.80 — set in 2024 when `LazyLock` landed and never revisited — + # and usage-argv had not built at 1.80 for a long time. A promise nobody checks is a guess. + # + # All published crates hold mise's own 1.91, which is the floor that matters for the fleet. + # Each crate is checked at the version it declares rather than at one shared guess, which is + # how a lower floor stays real instead of aspirational. + # + # `cargo check` rather than `cargo test`: dev-dependencies are not part of what an adopter + # compiles, and holding them to the MSRV would pin the toolchain past what the library needs. + msrv: + runs-on: ${{ !inputs.trusted && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - version: "1.91" + crates: >- + usage-argv usage-derive usage-config usage-validation usage-rs usage-test + usage-lib usage-dynamic clap_usage usage-cli + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + persist-credentials: false + - name: the matrix and the manifests agree + run: | + # Both directions, because one of them alone is how a false floor survives. + # + # Every crate this row checks declares this row's version. Or the job checks a version + # nobody promised: a crate that changes its floor without touching this file would + # otherwise be tested at the old one and pass. + for crate in ${{ matrix.crates }}; do + manifest="$(git ls-files '*Cargo.toml' | xargs grep -l "^name = \"$crate\"$" | head -1)" + declared="$(sed -n 's/^rust-version *= *"\(.*\)".*/\1/p' "$manifest" | head -1)" + if [ "$declared" != "${{ matrix.version }}" ]; then + echo "::error::$crate declares $declared, checked here at ${{ matrix.version }}" + exit 1 + fi + done + # And every published crate declaring this row's version is in this row. Without this + # half, a new crate — or one that was simply never listed — makes a floor promise that + # no toolchain ever tries. `usage-conformance` and `xtask` promised 1.91 for as long as + # this job existed while depending on a crate that needs 1.95; nothing noticed, because + # nothing looked from the manifests back to the matrix. + # + # Published only: a floor is a promise to whoever depends on the crate, so a + # `publish = false` member has nobody to make one to and should not declare one. + for manifest in $(git ls-files '*Cargo.toml'); do + declared="$(sed -n 's/^rust-version *= *"\(.*\)".*/\1/p' "$manifest" | head -1)" + [ "$declared" = "${{ matrix.version }}" ] || continue + grep -q '^publish *= *false' "$manifest" && continue + name="$(sed -n 's/^name *= *"\(.*\)".*/\1/p' "$manifest" | head -1)" + case " ${{ matrix.crates }} " in + *" $name "*) ;; + *) + echo "::error::$name declares $declared and is published, but no row checks it" + exit 1 + ;; + esac + done + # `rustup` rather than an action: it is already on the runner, and this needs one + # toolchain and no other behaviour — a pinned third-party action would be one more + # thing to keep current for no gain. + - name: install the toolchain under test + run: rustup toolchain install ${{ matrix.version }} --profile minimal + - uses: ./.github/actions/mbx + with: + backend: ${{ inputs.mbx-backend }} + - name: they build at the version they claim + run: | + for crate in ${{ matrix.crates }}; do + mbx +${{ matrix.version }} check --locked -p "$crate" --all-features + done + + # Single fan-in gate, so branch protection can require one stable check + # instead of an enumerated list that goes stale when jobs are added or renamed. + final: + permissions: {} + needs: + - test + - test-windows + - msrv + runs-on: ubuntu-latest + timeout-minutes: 1 + # Run on success or upstream failure but skip when the workflow is cancelled + # — `always()` would override `cancel-in-progress` and waste a runner. + if: ${{ !cancelled() }} + steps: + - name: Check job results + if: | + needs.test.result != 'success' || + needs.test-windows.result != 'success' || + needs.msrv.result != 'success' + run: exit 1 + - run: echo "All CI jobs completed successfully" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1050d1170..037d5c29a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,274 +1,48 @@ name: test + on: push: branches: ["main"] pull_request: -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - permissions: {} -env: - MISE_EXPERIMENTAL: 1 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jobs: - test: - runs-on: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login != 'jdx') && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} - # Without this, a hung step sits until GitHub's 6-hour job limit. - timeout-minutes: 30 - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: recursive - persist-credentials: false - - uses: ./.github/actions/mbx - - uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5 - # Retries and a longer wait, rather than being allowed to fail. `cli`'s completion - # tests refuse to skip a missing shell when `CI` is set — see - # `skip_if_shell_missing` — which is a deliberate policy this step exists to keep: - # a run that quietly tested one shell instead of four is worse than a run that - # stopped. So the flakiness is what gets fixed, not the consequence of it. - # - # The failure mode, seen three times in an afternoon, is the azure mirror that - # GitHub's runners list first *stalling* rather than refusing. Retries alone did not - # help — an eight-minute attempt with them timed out the same way — because retrying - # a connection that hangs just hangs again. What was missing is a per-connection - # timeout: with one, apt gives up on the stalled mirror in seconds and moves to the - # next entry in `/etc/apt/apt-mirrors.txt`, which is the archive.ubuntu.com that was - # answering fine throughout. - # - # `bash-completion` is in the list for the same reason: usage no longer embeds a copy of - # it, so the generated bash completion needs the system one loaded, and - # `bash_completion_or_skip` panics under CI rather than skipping without it. The image - # already ships it; naming it here is what keeps that true if a future one stops. - # - # Deliberately the distro package rather than a newer build from source: `ubuntu-latest` - # is 24.04, whose 2.11 is the oldest version the generated script supports. Testing - # against the floor is the point — 26.04 ships 2.16, which would pass either way. - - name: Install shells and bash-completion for completion integration tests - timeout-minutes: 5 - run: | - if [[ -f /etc/apt/apt-mirrors.txt ]]; then - sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt - fi - sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update - sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y zsh fish bash-completion - # pwsh is pre-installed on GitHub ubuntu-latest images. Namespace - # images have Microsoft's apt repository but not snap, so install - # from apt when it is absent. - if ! command -v pwsh >/dev/null 2>&1; then - sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 install -y powershell - fi - # A step of its own, and `always()`, because the interesting time to hear which - # shells are present is the run where the one above did not finish — and a `set -e` - # script says nothing after the command that failed it. - - name: Report which shells are available - if: always() - run: | - probe_dir=$(mktemp -d) - trap 'rm -rf "$probe_dir"' EXIT - for shell in bash zsh fish pwsh; do - if [[ "$shell" == pwsh ]]; then - probe="$probe_dir/$shell.ps1" - else - probe="$probe_dir/$shell" - fi - printf 'echo ok\n' > "$probe" - if [[ "$shell" == pwsh ]]; then - output=$(pwsh -NoProfile -NonInteractive -File "$probe" 2>/dev/null) && usable=true || usable=false - else - output=$("$shell" "$probe" 2>/dev/null) && usable=true || usable=false - fi - if [[ "$usable" == true && "$output" == ok ]]; then - echo "$shell: $(command -v "$shell") (script probe passed)" - else - echo "::warning::$shell cannot run a script, so its completion tests cannot run" - fi - done - - run: mise r build - - run: mise r test - # Run here rather than in a job of its own: the corpus vectors carry KDL - # specs, and lowering them needs the `usage` CLI this job has already built. - - run: mise r test:go - # Everything else in this pipeline builds with `--all-features`, and resolver-v2 unification - # means a workspace build turns a feature on for every member as soon as one member wants it. - # So the shapes an adopter actually gets — usage-lib without `docs`, usage-rs without its - # defaults — are compiled nowhere else, and rotted unnoticed until they were measured. - - name: check the trimmed feature shapes still compile - run: | - mbx clippy -p usage-lib --no-default-features -- -D warnings - mbx clippy -p usage-rs --no-default-features -- -D warnings - mbx clippy -p usage-config --no-default-features -- -D warnings - - run: mise r render - # Same reasoning as `render`: the shadow is checked in, so a change to the derive's - # vocabulary that would alter it has to be committed rather than discovered later. - - run: mise r gen-shadow - # And the Go tables, for the same reason: they are checked in, so a change to - # the emitter that nobody meant has to show up as a diff here. - - run: mise r gen-go - - name: assert render, gen-shadow and gen-go produce no diff - run: | - if [ -n "$(git status --porcelain)" ]; then - echo "::error::'mise run render', 'mise run gen-shadow' or 'mise run gen-go' produced changes. Run them locally and commit." - git status - git diff - exit 1 - fi - - run: mise r lint - - # The only job that is not Linux, and for several `cfg` branches the only place they are - # compiled at all. The argv path planner, the config path normaliser and - # `os_string_from_bytes` all exist for Windows and had never been built on it — each of - # #1232, #1233 and #1234 fixed something no other job could see. This is the job that keeps - # that true going forward rather than once. - test-windows: - runs-on: windows-latest - timeout-minutes: 30 + trusted: + if: ${{ github.actor == 'jdx' && (github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login == 'jdx')) }} permissions: contents: read id-token: write - steps: - # Before the checkout, and not optional: there is no `.gitattributes`, and Windows git - # defaults to `autocrlf=true`. The fixtures would arrive with CRLF and the help snapshots - # in `lib/tests/parse.rs` and `cli/tests/markdown.rs` would fail on a difference - # `pretty_assertions` prints as two identical strings. - - run: git config --global core.autocrlf false - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: recursive - persist-credentials: false - - uses: ./.github/actions/mbx - - uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5 - - run: mise r build - # `usage bash` runs whatever Windows resolves `bash` to, and `CreateProcess` searches the - # system directory before `PATH`. On this runner that is `C:\Windows\System32\bash.exe` — - # the WSL launcher, with no distribution installed — so the mount fixtures cannot run. - # Naming Git Bash is what makes them run rather than skip. - # - # `USAGECLI_`, not `USAGE_`: mise strips `usage_*` from a task's environment by comparing - # six characters case-insensitively, and Windows variable names are case-insensitive, so - # `USAGE_SHELL_BASH` would not survive `mise r`. That collision is why the `USAGECLI_` - # spelling exists. - # - # Unquoted on purpose. In double quotes YAML reads `\b` as a backspace. - # - # If this ever stops arriving, the job fails rather than passing quietly: - # `skip_if_posix_shell_missing` panics under CI on every platform, so a run cannot skip - # its way to green. - - env: - USAGECLI_SHELL_BASH: C:\Program Files\Git\bin\bash.exe - run: mise r test - # No zsh, fish or bash-completion, and nothing to install them with: they have no Windows - # equivalent, which is why `skip_if_shell_missing` and `bash_completion_or_skip` limit - # their CI panic to Unix. Three tests skip here and the Linux job covers them. - # - # Linting here is not duplication of the Linux job. `#[cfg]` decides whether code exists, - # so a warning can be real on one platform and absent on the other — both of the ones - # #1234 fixed were invisible to every other job in this file. `lint:clippy` denies - # warnings and passes `--all-targets`, which makes those a build failure rather than noise. - - run: mise r lint:clippy + uses: ./.github/workflows/test-impl.yml + with: + trusted: true + mbx-backend: server - # The declared `rust-version` is a promise to anyone depending on these crates, and nothing - # was checking it. It said 1.80 — set in 2024 when `LazyLock` landed and never revisited — - # and usage-argv had not built at 1.80 for a long time. A promise nobody checks is a guess. - # - # All published crates hold mise's own 1.91, which is the floor that matters for the fleet. - # Each crate is checked at the version it declares rather than at one shared guess, which is - # how a lower floor stays real instead of aspirational. - # - # `cargo check` rather than `cargo test`: dev-dependencies are not part of what an adopter - # compiles, and holding them to the MSRV would pin the toolchain past what the library needs. - msrv: - runs-on: ${{ github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login != 'jdx') && 'ubuntu-latest' || 'namespace-profile-endev-linux-amd64' }} - timeout-minutes: 15 + untrusted: + if: ${{ github.actor != 'jdx' || (github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login != 'jdx')) }} permissions: contents: read - id-token: write - strategy: - fail-fast: false - matrix: - include: - - version: "1.91" - crates: >- - usage-argv usage-derive usage-config usage-validation usage-rs usage-test - usage-lib usage-dynamic clap_usage usage-cli - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - submodules: recursive - persist-credentials: false - - name: the matrix and the manifests agree - run: | - # Both directions, because one of them alone is how a false floor survives. - # - # Every crate this row checks declares this row's version. Or the job checks a version - # nobody promised: a crate that changes its floor without touching this file would - # otherwise be tested at the old one and pass. - for crate in ${{ matrix.crates }}; do - manifest="$(git ls-files '*Cargo.toml' | xargs grep -l "^name = \"$crate\"$" | head -1)" - declared="$(sed -n 's/^rust-version *= *"\(.*\)".*/\1/p' "$manifest" | head -1)" - if [ "$declared" != "${{ matrix.version }}" ]; then - echo "::error::$crate declares $declared, checked here at ${{ matrix.version }}" - exit 1 - fi - done - # And every published crate declaring this row's version is in this row. Without this - # half, a new crate — or one that was simply never listed — makes a floor promise that - # no toolchain ever tries. `usage-conformance` and `xtask` promised 1.91 for as long as - # this job existed while depending on a crate that needs 1.95; nothing noticed, because - # nothing looked from the manifests back to the matrix. - # - # Published only: a floor is a promise to whoever depends on the crate, so a - # `publish = false` member has nobody to make one to and should not declare one. - for manifest in $(git ls-files '*Cargo.toml'); do - declared="$(sed -n 's/^rust-version *= *"\(.*\)".*/\1/p' "$manifest" | head -1)" - [ "$declared" = "${{ matrix.version }}" ] || continue - grep -q '^publish *= *false' "$manifest" && continue - name="$(sed -n 's/^name *= *"\(.*\)".*/\1/p' "$manifest" | head -1)" - case " ${{ matrix.crates }} " in - *" $name "*) ;; - *) - echo "::error::$name declares $declared and is published, but no row checks it" - exit 1 - ;; - esac - done - # `rustup` rather than an action: it is already on the runner, and this needs one - # toolchain and no other behaviour — a pinned third-party action would be one more - # thing to keep current for no gain. - - name: install the toolchain under test - run: rustup toolchain install ${{ matrix.version }} --profile minimal - - uses: ./.github/actions/mbx - - name: they build at the version they claim - run: | - for crate in ${{ matrix.crates }}; do - mbx +${{ matrix.version }} check --locked -p "$crate" --all-features - done + uses: ./.github/workflows/test-impl.yml + with: + trusted: false + mbx-backend: github - # Single fan-in gate, so branch protection can require one stable check - # instead of an enumerated list that goes stale when jobs are added or renamed. final: + name: final + if: ${{ always() && !cancelled() && needs.trusted.result != 'cancelled' && needs.untrusted.result != 'cancelled' }} permissions: {} - needs: - - test - - test-windows - - msrv + needs: [trusted, untrusted] runs-on: ubuntu-latest timeout-minutes: 1 - # Run on success or upstream failure but skip when the workflow is cancelled - # — `always()` would override `cancel-in-progress` and waste a runner. - if: ${{ !cancelled() }} steps: - - name: Check job results - if: | - needs.test.result != 'success' || - needs.test-windows.result != 'success' || - needs.msrv.result != 'success' - run: exit 1 - - run: echo "All CI jobs completed successfully" + - name: Check selected workflow result + env: + TRUSTED_RESULT: ${{ needs.trusted.result }} + UNTRUSTED_RESULT: ${{ needs.untrusted.result }} + run: | + if [[ "$TRUSTED_RESULT" == success && "$UNTRUSTED_RESULT" == skipped ]] || + [[ "$TRUSTED_RESULT" == skipped && "$UNTRUSTED_RESULT" == success ]]; then + exit 0 + fi + echo "trusted=$TRUSTED_RESULT untrusted=$UNTRUSTED_RESULT" + exit 1 From 2fab6d42fdf03f886bb1615e71ec842e8b35a463 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Wed, 26 Aug 2026 03:40:50 +0000 Subject: [PATCH 10/10] ci: register Namespace runner with actionlint --- .github/actionlint.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index aef3e9618..659f46944 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -3,6 +3,7 @@ self-hosted-runner: labels: - bamboo-perf - macos-14 + - namespace-profile-endev-linux-amd64 - buildjet-32vcpu-ubuntu-2204-arm - buildjet-16vcpu-ubuntu-2204-arm - buildjet-8vcpu-ubuntu-2204-arm