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 diff --git a/.github/actions/mbx/action.yml b/.github/actions/mbx/action.yml new file mode 100644 index 000000000..035160092 --- /dev/null +++ b/.github/actions/mbx/action.yml @@ -0,0 +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: ${{ 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/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-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 644867daa..037d5c29a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,275 +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: ubuntu-latest - # Without this, a hung step sits until GitHub's 6-hour job limit. - 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 - 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: 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: | - sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt - 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). - if ! command -v pwsh >/dev/null 2>&1; then - sudo snap install powershell --classic - 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: | - 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 - - 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 + id-token: write + uses: ./.github/workflows/test-impl.yml + with: + trusted: true + mbx-backend: server - # 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 + 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 - 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: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - shared-key: test-windows - - 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: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - 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: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - shared-key: msrv-${{ matrix.version }} - - name: they build at the version they claim - run: | - for crate in ${{ matrix.crates }}; do - cargo +${{ 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 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..423fc70bc 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.4.0" +backend = "github:jdx/mr-boxington" + +[tools."github:jdx/mr-boxington"."platforms.linux-arm64"] +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: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: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: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: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: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: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" backend = "github:jdx/tak" diff --git a/mise.toml b/mise.toml index 6fb39aff1..4db9f6235 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 --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 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 clippy --all --all-features --all-targets -- -D warnings' [tasks."lint:fmt"] run = 'cargo fmt --all -- --check' [tasks."lint:semver"]