fix(release): reuse an existing release instead of creating a duplica… #1637
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| # Run on PRs against ANY base branch, not just main, so a stacked PR (based on | |
| # another in-flight feature branch) still gets CI instead of no checks until it | |
| # is retargeted to main. Affected-crate selection already diffs against | |
| # `github.base_ref`, so a non-main base works. push/merge_group stay main-only. | |
| pull_request: | |
| # The merge queue creates a temporary branch and fires `merge_group`; without | |
| # this trigger the required checks never start and queued PRs stall forever. | |
| merge_group: | |
| branches: [main] | |
| types: [checks_requested] | |
| # Manual trigger for an on-demand mock (GitHub-hosted) E2E run, without waiting | |
| # on a push/PR or the full gate. Dispatch against any ref | |
| # (`gh workflow run ci.yml --ref <branch> -f platform=mock`) — GitHub runs THAT | |
| # ref's copy of this file. A dispatch skips build-and-test (see the e2e `if:`) | |
| # for a fast loop. This trigger must exist on the default branch for the | |
| # workflow to be dispatchable at all. The self-hosted GPU lanes moved to their | |
| # own workflow (e2e-selfhosted.yml) — dispatch THAT for GPU/Strix runs. | |
| workflow_dispatch: | |
| inputs: | |
| platform: | |
| description: Which runner(s) to target | |
| type: choice | |
| default: all | |
| options: [all, mock] | |
| # No tier input: the harness resolves pass/xfail/skip per scenario, so a | |
| # platform runs one job covering everything applicable to it. | |
| concurrency: | |
| # Manual dispatches get a UNIQUE group (run_id) so a stuck run never holds the | |
| # shared group and blocks later dispatches. push / PR / merge_group keep the | |
| # shared per-ref group so a new commit still supersedes the previous in-flight | |
| # run. The offline-self-hosted-runner stall that motivated this is now | |
| # structurally avoided: the self-hosted GPU lanes live in e2e-selfhosted.yml | |
| # with their own group, so an uncancellable queued job can never stall THIS | |
| # workflow's merge-required checks. | |
| group: >- | |
| ${{ github.workflow }}-${{ github.ref }}-${{ | |
| github.event_name == 'workflow_dispatch' && github.run_id || 'shared' }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| # Every job sets `timeout-minutes`. GitHub's default is 360, so a step that hangs | |
| # rather than fails — an `apt-get` against an unresponsive mirror, a network call | |
| # with no timeout — holds a runner for six hours and, in the merge queue, stalls | |
| # every queued PR behind it with no signal that anything is wrong. The values here | |
| # are deliberately loose (roughly 4x the observed p100, which is under 3 min for | |
| # most jobs and ~13 min for the Windows build) so they never fire on a merely slow | |
| # run; they exist to convert a silent multi-hour stall into a prompt, legible | |
| # failure. Raise a value if a job legitimately grows into it. | |
| jobs: | |
| # Decide which file categories a change touches so unaffected jobs can skip | |
| # their expensive work. Filtering runs ONLY on pull_request; on push and in the | |
| # merge queue every category is forced true, so post-merge `main` and queued | |
| # PRs always run the full suite. That keeps the skip from ever starving a | |
| # required check in the merge queue (a never-produced required check stalls the | |
| # queue forever — see the `commit-signatures` note below). Each output folds in | |
| # `.github/workflows/**`, so any change to CI itself triggers a full run. | |
| changes: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # paths-filter reads the PR's changed-file list from the API on | |
| # pull_request events; that needs pull-requests:read on top of the | |
| # workflow-wide contents:read (job-level permissions replace the default | |
| # set, so contents:read is repeated here for checkout). | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| # `<filter> || <fallback>`: the filter result on PRs (empty string when the | |
| # filter step is skipped off-PR), otherwise the forced 'true'. | |
| rust: ${{ steps.filter.outputs.rust || steps.all.outputs.forced }} | |
| heavy: ${{ steps.filter.outputs.heavy || steps.all.outputs.forced }} | |
| lint: ${{ steps.filter.outputs.lint || steps.all.outputs.forced }} | |
| tpn: ${{ steps.filter.outputs.tpn || steps.all.outputs.forced }} | |
| docs: ${{ steps.filter.outputs.docs || steps.all.outputs.forced }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Filter changed paths (pull requests only) | |
| id: filter | |
| if: github.event_name == 'pull_request' | |
| uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 | |
| with: | |
| filters: | | |
| # Rust-only checks (fmt, clippy, coverage). | |
| rust: | |
| - '**/*.rs' | |
| - '**/Cargo.toml' | |
| - 'Cargo.lock' | |
| - 'rust-toolchain*' | |
| # clippy runs `cargo xtask manifest --check` against this file. | |
| - 'MANIFEST.md' | |
| # rocm-deps compiles the pins into constants, so a pin change is a | |
| # source change even though no *.rs file moved. | |
| - 'runtime-deps.toml' | |
| - '.github/workflows/**' | |
| # build-and-test runs cargo AND the python/shell smoke steps plus the | |
| # install-lifecycle E2E (`cargo xtask package` + the real installer), | |
| # so it depends on the Rust set, the packaging xtask, the lifecycle | |
| # feature/steps, and the installer inputs. | |
| heavy: | |
| - '**/*.rs' | |
| - '**/Cargo.toml' | |
| - 'Cargo.lock' | |
| - 'rust-toolchain*' | |
| - 'runtime-deps.toml' | |
| - 'scripts/**' | |
| - 'xtask/**' | |
| - 'engines/**' | |
| - '**/*.py' | |
| - '**/*.sh' | |
| - '**/*.ps1' | |
| - '**/*.feature' | |
| - 'tests/e2e-cucumber/**' | |
| - 'install*' | |
| # Pinned-key consistency compares docs/keys/* against the installers, | |
| # so a canonical-key-only change must trigger the heavy job that runs | |
| # `cargo xtask verify-pinned-keys`. | |
| - 'docs/keys/**' | |
| - '.github/workflows/**' | |
| # THIRD_PARTY_NOTICES.txt staleness gate: anything that changes the | |
| # dependency tree, the cargo-about config/template, the generator, or | |
| # the generated file itself. | |
| tpn: | |
| - 'Cargo.lock' | |
| - '**/Cargo.toml' | |
| - 'about.toml' | |
| - 'about.hbs' | |
| - 'THIRD_PARTY_NOTICES.txt' | |
| - 'xtask/**' | |
| - '.github/workflows/**' | |
| # Python / Shell / PowerShell lint job. | |
| lint: | |
| - 'scripts/**' | |
| - 'engines/**' | |
| - '**/*.py' | |
| - '**/*.sh' | |
| - '**/*.ps1' | |
| - 'ruff.toml' | |
| - 'PSScriptAnalyzerSettings.psd1' | |
| - '.github/workflows/**' | |
| # Sphinx docs site: the doc sources themselves, plus README.md and | |
| # CONTRIBUTING.md, which several pages single-source via MyST | |
| # `{include}` directives, plus the build config/toolchain pins. | |
| docs: | |
| - 'docs/rocm-docs/**' | |
| - 'README.md' | |
| - 'CONTRIBUTING.md' | |
| - '.readthedocs.yaml' | |
| - '.github/workflows/**' | |
| - name: Force full run off pull requests | |
| id: all | |
| if: github.event_name != 'pull_request' | |
| run: echo "forced=true" >> "$GITHUB_OUTPUT" | |
| # Advisory nudge: if a PR references a bug that still has an expected-failure | |
| # (xfail) row in expectations.toml, remind the author to clear it. Otherwise a | |
| # fix whose CI lane doesn't exercise that platform merges green and the stale | |
| # row later surfaces as a mis-attributed XPASS on an unrelated PR. Never blocks: | |
| # advisory only, and deliberately NOT a required check. | |
| xfail-hint: | |
| name: Stale xfail hint (advisory) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # Need the PR range to read its commit messages. | |
| fetch-depth: 0 | |
| - name: Warn on referenced bugs that still have xfail rows | |
| # Title/body are passed via env (never interpolated into the shell) so a | |
| # crafted PR description can't inject commands. | |
| env: | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| git log --format=%B "$BASE_SHA..$HEAD_SHA" > /tmp/pr-commits.txt || true | |
| python3 scripts/xfail_expectations_hint.py \ | |
| --title "$PR_TITLE" \ | |
| --body "$PR_BODY" \ | |
| --commits-file /tmp/pr-commits.txt >> "$GITHUB_STEP_SUMMARY" | |
| build-and-test: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Don't spend per-OS build/test cycles unless the cheap lint gate is green. | |
| needs: [changes, clippy, prek] | |
| # A manual E2E dispatch skips this heavy job for a fast loop; the E2E jobs | |
| # tolerate a skipped build-and-test in their own `if:` guards. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Install native build deps | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y pkg-config libcap-dev | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| if: needs.changes.outputs.heavy == 'true' | |
| # Build the runtime binaries the smoke + acceptance steps below need (every | |
| # engine binary; smoke hard-fails on a missing one). Unit/integration tests | |
| # run in the dedicated `test` job on only the crates a change can affect; | |
| # `clippy --all-targets` is the compile-check for the non-binary targets, so | |
| # dropping `--all-targets` here loses no coverage. | |
| - name: Build | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: cargo build --workspace | |
| - name: Local no-fallback smoke | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: python scripts/smoke_local.py --skip-build | |
| - name: Acceptance harness self-tests | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: | | |
| python scripts/comfyui_therock_gpu_test.py --self-test | |
| python scripts/local_assistant_therock_gpu_test.py --self-test | |
| python scripts/vllm_therock_gpu_test.py --self-test | |
| python scripts/wsl_preflight.py --self-test | |
| - name: Portable WSL build deps self-test | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: bash scripts/setup-wsl-portable-build-deps.sh --self-test | |
| - name: Release readiness self-test | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: python scripts/release_readiness.py --self-test | |
| - name: Pinned key consistency check | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: cargo xtask verify-pinned-keys | |
| - name: Acceptance install lifecycle | |
| if: needs.changes.outputs.heavy == 'true' | |
| # The install lifecycle (package + real installer + install/uninstall) now | |
| # lives in the opt-in @lifecycle E2E set; run the Linux scenarios here. | |
| env: | |
| E2E_INCLUDE_LIFECYCLE: "1" | |
| E2E_ONLY_LIFECYCLE: "1" | |
| run: cargo xtask e2e | |
| # Fine-grained test selection (the second half of smarter CI; the path-based | |
| # job skip is the `changes` job above). Runs cargo's unit + integration tests | |
| # for only the crates a change can reach — the changed crates plus their | |
| # transitive dependents, computed by `cargo xtask affected` from the workspace | |
| # dependency graph — instead of the whole workspace. | |
| # | |
| # Merge-queue safety: the narrowing happens ONLY on pull_request. On push and | |
| # in the merge queue the selection is forced to `--workspace`, so post-merge | |
| # `main` and every queued PR run the full suite — a required check is never | |
| # starved and the queue cannot stall. The full Windows suite | |
| # (windows-build-and-test) also runs the whole workspace on every change, so it | |
| # backstops any crate a graph-based selection could miss (e.g. an integration | |
| # test that drives another crate only at runtime). | |
| test: | |
| name: Test (affected crates) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Same lint gate as the OS build jobs: don't spend a test cycle until the | |
| # cheap checks are green. | |
| needs: [changes, clippy, prek] | |
| # A manual E2E dispatch only exercises the E2E jobs; skip the rest. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # `cargo xtask affected` diffs origin/<base>...HEAD, so it needs the | |
| # base history and the real PR head (not the synthetic merge ref). | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} | |
| - name: Install native build deps | |
| if: needs.changes.outputs.rust == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y pkg-config libcap-dev | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| if: needs.changes.outputs.rust == 'true' | |
| with: | |
| # Keep the PR (subset) and push/merge (`--workspace`) builds in separate | |
| # cache lanes. A subset build resolves Cargo features differently than | |
| # the whole workspace, so a shared cache would make the two evict and | |
| # recompile each other's artifacts on every alternation. | |
| cache-key: ${{ github.event_name == 'pull_request' && 'affected-pr' || 'full' }} | |
| - name: Install cargo-nextest | |
| if: needs.changes.outputs.rust == 'true' | |
| uses: taiki-e/install-action@82cd3e7658a6f96c86c0234aeeda1748937cb0a1 # v2.85.13 | |
| with: | |
| tool: nextest | |
| - name: Select affected crates | |
| id: sel | |
| if: needs.changes.outputs.rust == 'true' | |
| run: | | |
| # The `cargo xtask` alias builds xtask in release (target/release), so | |
| # computing the selection doesn't perturb the debug feature resolution | |
| # the nextest build below uses. | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| sel=$(cargo xtask affected --base "origin/${{ github.base_ref }}") | |
| else | |
| sel="--workspace" | |
| fi | |
| echo "affected selection: ${sel:-<no rust crates changed>}" | |
| echo "sel=$sel" >> "$GITHUB_OUTPUT" | |
| - name: Test (nextest) | |
| # Skip cleanly when nothing Rust-relevant resolved to a crate, so the | |
| # required check still reports success. | |
| if: needs.changes.outputs.rust == 'true' && steps.sel.outputs.sel != '' | |
| # `sel` is intentionally unquoted: it expands to either `--workspace` or | |
| # several `-p <crate>` flags that must word-split into separate argv | |
| # entries. The values are crate names from our own tool, not user input. | |
| run: cargo nextest run ${{ steps.sel.outputs.sel }} | |
| windows-build-and-test: | |
| runs-on: windows-latest | |
| timeout-minutes: 45 | |
| env: | |
| CARGO_INCREMENTAL: "0" | |
| # Don't spend per-OS build/test cycles unless the cheap lint gate is green. | |
| needs: [changes, clippy, prek] | |
| # A manual E2E dispatch only exercises the E2E jobs; skip the rest. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| if: needs.changes.outputs.heavy == 'true' | |
| with: | |
| cache-on-failure: true | |
| - name: Build | |
| if: needs.changes.outputs.heavy == 'true' | |
| shell: pwsh | |
| run: | | |
| cargo build --workspace --all-targets | |
| cargo build --release -p rocm -p rocmd -p rocm-engine-lemonade -p rocm-engine-vllm -p xtask | |
| - name: Test | |
| if: needs.changes.outputs.heavy == 'true' | |
| shell: pwsh | |
| run: cargo test --workspace --all-targets | |
| - name: Local no-fallback smoke | |
| if: needs.changes.outputs.heavy == 'true' | |
| shell: pwsh | |
| run: python .\scripts\smoke_local.py --skip-build | |
| - name: Acceptance harness self-tests | |
| if: needs.changes.outputs.heavy == 'true' | |
| shell: pwsh | |
| run: | | |
| python .\scripts\comfyui_therock_gpu_test.py --self-test | |
| python .\scripts\local_assistant_therock_gpu_test.py --self-test | |
| python .\scripts\vllm_therock_gpu_test.py --self-test | |
| python .\scripts\wsl_preflight.py --self-test | |
| - name: Release readiness self-test | |
| if: needs.changes.outputs.heavy == 'true' | |
| shell: pwsh | |
| run: python .\scripts\release_readiness.py --self-test | |
| - name: Pinned key consistency check | |
| if: needs.changes.outputs.heavy == 'true' | |
| shell: pwsh | |
| run: cargo xtask verify-pinned-keys | |
| - name: Check PowerShell installer syntax | |
| if: needs.changes.outputs.heavy == 'true' | |
| shell: pwsh | |
| run: | | |
| $null = [scriptblock]::Create((Get-Content .\install.ps1 -Raw)) | |
| - name: Acceptance install lifecycle | |
| if: needs.changes.outputs.heavy == 'true' | |
| # Windows install lifecycle (packaging + native-crypto verify + user-PATH | |
| # + loopback HTTP + isolated smoke + uninstall) as opt-in @lifecycle E2E. | |
| env: | |
| E2E_INCLUDE_LIFECYCLE: "1" | |
| E2E_ONLY_LIFECYCLE: "1" | |
| run: cargo xtask e2e | |
| prek: | |
| # The fast lint gate: every toolchain-light lint/hygiene check, run from the | |
| # single source of truth in .pre-commit-config.yaml — hygiene + Python (ruff) | |
| # + Shell (shellcheck) + cargo fmt (rustfmt only, no compile). | |
| # `--no-group local-tools` skips the hooks that need a full Rust compile, | |
| # hawkeye, or PowerShell — those have their own jobs (clippy, build-and-test, | |
| # license-headers, powershell-lint). | |
| name: prek (lint / hygiene) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Always-on: prek is the cheapest gate (no compile) and its hygiene hooks run | |
| # on every file type, so it is relevant to any change — including docs-only | |
| # PRs that match no `changes` category. Like license-headers and | |
| # commit-signatures, it is not gated. | |
| # Exception: a manual E2E dispatch only exercises the E2E jobs; skip the rest. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| # cargo-fmt hook needs rustfmt (no build). | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| with: | |
| components: rustfmt | |
| cache: false | |
| # Supply-chain hardening: the action is pinned to a commit SHA and prek to | |
| # an exact version. The action ships a hardcoded SHA-256 checksum map | |
| # (src/known-checksums.ts) and verifies the downloaded prek archive against | |
| # it, so a tampered release can't be installed. This commit is past the | |
| # v2.0.4 tag because it is the one that added prek 0.4.5 to that map — pin | |
| # an earlier commit and the action would not know 0.4.5 and skip the check. | |
| - uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # prek 0.4.5 checksums | |
| with: | |
| prek-version: 0.4.5 | |
| extra-args: --all-files --no-group local-tools | |
| clippy: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: changes | |
| # A manual E2E dispatch only exercises the E2E jobs; skip the rest. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| if: needs.changes.outputs.rust == 'true' | |
| - name: Clippy | |
| if: needs.changes.outputs.rust == 'true' | |
| run: cargo clippy --locked --workspace --all-targets -- -D warnings | |
| # The e2e-cucumber `e2e` target sets `test = false` (so nextest/`cargo test | |
| # --all-targets` skip the custom harness), which also excludes it from | |
| # `clippy --all-targets` above — leaving the ~1.6k lines of harness + step | |
| # code unlinted. Clippy honours an explicit `--test`, so lint it directly. | |
| - name: Clippy (e2e harness + steps) | |
| if: needs.changes.outputs.rust == 'true' | |
| run: cargo clippy --locked -p e2e-cucumber --test e2e -- -D warnings | |
| - name: MANIFEST.md dependency table is current | |
| if: needs.changes.outputs.rust == 'true' | |
| run: cargo xtask manifest --check | |
| third-party-notices: | |
| name: Third-party notices current | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: changes | |
| # A manual E2E dispatch only exercises the E2E jobs; skip the rest. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| if: needs.changes.outputs.tpn == 'true' | |
| # Pin the cargo-about version so the check compares against the exact same | |
| # generator contributors use; a different version can format the notices | |
| # differently and fail the byte-for-byte comparison. This version has no | |
| # prebuilt release binary and its `cargo install` needs the `cli` feature, | |
| # so build from source (cached by version); `--locked` uses cargo-about's | |
| # own lockfile for a reproducible generator. | |
| - name: Cache cargo-about | |
| if: needs.changes.outputs.tpn == 'true' | |
| id: cache-cargo-about | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cargo/bin/cargo-about | |
| key: ${{ runner.os }}-cargo-about-0.9.1 | |
| - name: Install cargo-about | |
| if: needs.changes.outputs.tpn == 'true' && steps.cache-cargo-about.outputs.cache-hit != 'true' | |
| run: cargo install cargo-about@0.9.1 --locked --features cli | |
| - name: THIRD_PARTY_NOTICES.txt is current | |
| if: needs.changes.outputs.tpn == 'true' | |
| run: cargo xtask tpn --check | |
| coverage: | |
| name: Coverage (rocm-dash crates, ratcheted) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: changes | |
| # A manual E2E dispatch only exercises the E2E jobs; skip the rest. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Install native build deps | |
| if: needs.changes.outputs.rust == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y pkg-config libcap-dev | |
| - uses: dtolnay/rust-toolchain@c0e9df88980754dd93e5833b8dcb1b304c1fe173 # 1.96.0 | |
| if: needs.changes.outputs.rust == 'true' | |
| with: | |
| components: llvm-tools-preview | |
| - name: Install cargo-llvm-cov | |
| if: needs.changes.outputs.rust == 'true' | |
| uses: taiki-e/install-action@82cd3e7658a6f96c86c0234aeeda1748937cb0a1 # v2.85.13 | |
| with: | |
| tool: cargo-llvm-cov | |
| - name: Cache cargo registry and build | |
| if: needs.changes.outputs.rust == 'true' | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-cov-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-cov- | |
| # Ratcheted, fail-on-regression floor scoped to the transplanted rocm-dash | |
| # crates (the clean anchor) — deliberately NOT the whole workspace, so the | |
| # gate does not over-claim coverage of the large untyped rocm-cli core | |
| # Baseline measured 2026-06-11: 73.7% lines across the four | |
| # crates. Floor set just below measured; ratchet upward as the larger TUI | |
| # tab files (bench/overview/modal) gain tests. | |
| - name: Coverage gate (rocm-dash crates, >= 70% lines) | |
| if: needs.changes.outputs.rust == 'true' | |
| run: | | |
| cargo llvm-cov --no-cfg-coverage \ | |
| -p rocm-dash-core -p rocm-dash-collectors \ | |
| -p rocm-dash-daemon -p rocm-dash-tui \ | |
| --fail-under-lines 70 | |
| powershell-lint: | |
| # Runs the prek `powershell-script-analyzer` hook's logic (cargo xtask | |
| # powershell-lint) under BOTH PowerShell editions. windows-latest is the only | |
| # runner with Windows PowerShell 5.1 (`powershell`); it also ships PowerShell | |
| # 7 (`pwsh`). Kept separate from windows-build-and-test so it runs in parallel | |
| # rather than behind that slow job. | |
| name: Lint (PowerShell) | |
| runs-on: windows-latest | |
| timeout-minutes: 20 | |
| needs: changes | |
| # A manual E2E dispatch only exercises the E2E jobs; skip the rest. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| if: needs.changes.outputs.lint == 'true' | |
| - name: Install PSScriptAnalyzer (PowerShell 7) | |
| if: needs.changes.outputs.lint == 'true' | |
| shell: pwsh | |
| run: Install-Module PSScriptAnalyzer -Scope CurrentUser -Force -RequiredVersion 1.25.0 | |
| - name: Install PSScriptAnalyzer (Windows PowerShell 5.1) | |
| if: needs.changes.outputs.lint == 'true' | |
| shell: powershell | |
| run: | | |
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
| Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null | |
| Install-Module PSScriptAnalyzer -Scope CurrentUser -Force -RequiredVersion 1.25.0 | |
| - name: PSScriptAnalyzer (PowerShell 7) | |
| if: needs.changes.outputs.lint == 'true' | |
| run: cargo xtask powershell-lint --shell pwsh | |
| - name: PSScriptAnalyzer (Windows PowerShell 5.1) | |
| if: needs.changes.outputs.lint == 'true' | |
| run: cargo xtask powershell-lint --shell powershell | |
| license-headers: | |
| name: License header check (hawkeye) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| # Install a pinned prebuilt hawkeye binary instead of pulling the | |
| # korandoru/hawkeye Docker image on every run, and verify it against a | |
| # sha256 recorded here. The hash lives in-repo, out of band from the | |
| # release: GitHub release assets are mutable, so a republished/tampered | |
| # artifact (and its co-published .sha256) would pass an upstream-only | |
| # check — pinning the hash here makes that fail instead. To upgrade, bump | |
| # the version and replace the hash with the new release's published | |
| # .sha256 (confirm the bytes first). | |
| HAWKEYE_VERSION: v7.0.0 | |
| HAWKEYE_SHA256: 2617b43ec6f871511f7b381e3d34544d6aa47c2468541ca67102d69ff7d0f541 | |
| HAWKEYE_INSTALL_DIR: ${{ github.workspace }}/.hawkeye-bin | |
| # A manual E2E dispatch only exercises the E2E jobs; skip the rest. | |
| if: github.event_name != 'workflow_dispatch' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Cache hawkeye binary | |
| id: cache-hawkeye | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ${{ env.HAWKEYE_INSTALL_DIR }} | |
| key: ${{ runner.os }}-hawkeye-${{ env.HAWKEYE_VERSION }}-${{ env.HAWKEYE_SHA256 }} | |
| - name: Install hawkeye | |
| if: steps.cache-hawkeye.outputs.cache-hit != 'true' | |
| run: | | |
| tarball="hawkeye-x86_64-unknown-linux-gnu.tar.xz" | |
| curl --proto '=https' --tlsv1.2 -LsSf \ | |
| "https://github.com/fast/hawkeye/releases/download/${HAWKEYE_VERSION}/${tarball}" \ | |
| -o "${tarball}" | |
| echo "${HAWKEYE_SHA256} ${tarball}" | sha256sum -c - | |
| mkdir -p "${HAWKEYE_INSTALL_DIR}" | |
| tar -xJf "${tarball}" -C "${HAWKEYE_INSTALL_DIR}" --strip-components=1 | |
| rm -f "${tarball}" | |
| - name: Check license headers | |
| run: | | |
| "${HAWKEYE_INSTALL_DIR}/hawkeye" check | |
| docs-build: | |
| name: Sphinx docs build (-W) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [changes] | |
| # A manual E2E dispatch only exercises the E2E jobs; skip the rest. Off a | |
| # dispatch, `needs.changes` never ran, so short-circuit on event_name first. | |
| if: github.event_name != 'workflow_dispatch' && needs.changes.outputs.docs == 'true' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Cache pip packages | |
| id: cache-pip | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-docs-pip-${{ hashFiles('docs/rocm-docs/sphinx/requirements.txt') }} | |
| - name: Install Sphinx toolchain | |
| run: | | |
| python3 -m venv .docs-venv | |
| .docs-venv/bin/pip install -q -r docs/rocm-docs/sphinx/requirements.txt | |
| # Same source Sphinx points at (docs/rocm-docs/conf.py) and the same | |
| # -W (warnings-as-errors) mode read-the-docs builds with, so a broken | |
| # MyST include, dead xref, or toctree gap fails CI instead of surfacing | |
| # as a silent RTD build failure after merge. | |
| - name: Build docs (warnings as errors) | |
| run: | | |
| .docs-venv/bin/sphinx-build -b html docs/rocm-docs /tmp/rocm-cli-docs -W | |
| commit-signatures: | |
| name: Commit signatures + sign-off | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Runs on pull requests and in the merge queue. Both define a base that | |
| # bounds the commit range. Handling `merge_group` matters once this is a | |
| # *required* check: the queue fires `merge_group` (not `pull_request`), so a | |
| # job gated to PRs only would never produce the required check and the queue | |
| # entry would stall forever. | |
| if: github.event_name == 'pull_request' || github.event_name == 'merge_group' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| # PR: check out the PR head, not the synthetic refs/pull/N/merge commit | |
| # (which is unsigned and lacks a sign-off, and would otherwise appear | |
| # in origin/<base>..HEAD and fail the gate on every PR). | |
| # merge_group: check out the queue head — the rebased/squashed commits | |
| # GitHub built for the group. Merge commits are disabled on this repo, | |
| # so these are normal commits that preserve their Signed-off-by trailer | |
| # and are GitHub "Verified" (web-flow signed). | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.event.merge_group.head_sha }} | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| - name: Verify commits are signed and signed-off | |
| # `--require-verified` shells out to the `gh` CLI; pass the job token so | |
| # `gh api` is authenticated (avoids the unauthenticated rate limit). | |
| # Base: the PR's base branch, or — in the queue — the exact commit the | |
| # merge group was built on (`base_sha`), so the range is just the queued | |
| # commits and not whatever else has since landed on the base branch. | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BASE: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || github.event.merge_group.base_sha }} | |
| run: cargo xtask verify-commits --base "$BASE" --require-verified | |
| # Hardware (real AMD GPU) smoke tests run on dedicated self-hosted runners in a | |
| # SEPARATE workflow, e2e-selfhosted.yml (jobs e2e-gpu, e2e-gpu-strix-ubuntu, | |
| # e2e-gpu-strix-windows). They live there — not here — so a job queued on an | |
| # offline self-hosted runner (which GitHub cannot cancel) can never hold this | |
| # workflow's concurrency group and stall its merge-required checks (EAI-7548). | |
| # They are non-blocking (continue-on-error) and fork-safe (self-hosted runners | |
| # do not execute untrusted fork PRs). See docs/ci-hardware-testing.md. | |
| # ── E2E tests (cucumber-rs) ─────────────────────────────────────────────── | |
| # | |
| # BDD scenarios in Gherkin (.feature files) backed by Rust step functions. ONE | |
| # job per platform: the harness resolves every scenario to pass / xfail / skip | |
| # per host from its @id + @requires-* tags, a capability probe, and | |
| # expectations.toml (no tier flag, no tag filter). The mock job (GitHub-hosted, | |
| # no GPU) gates the PR; the self-hosted GPU jobs are non-blocking while proven | |
| # out. Each job writes a platform.json sidecar the consolidated report joins by | |
| # scenario id to render the (scenario × platform) expectation grid. | |
| # Blocking mock job (GitHub-hosted, no GPU): must stay green. @requires-gpu | |
| # scenarios resolve to skip here; known bugs resolve to xfail from | |
| # expectations.toml. | |
| e2e: | |
| name: E2E tests | |
| timeout-minutes: 15 | |
| runs-on: ubuntu-latest | |
| needs: [changes, build-and-test] | |
| # This is a REQUIRED check, so — like every sibling required job — the job | |
| # itself must always run (except on a manual dispatch that didn't select the | |
| # mock platform), and the actual work is gated at the STEP level. Gating | |
| # `heavy` at the job level would SKIP the job on a non-heavy PR, and a required | |
| # check that is never produced stalls the merge queue. `merge_group` forces | |
| # heavy=true, so the mock gate always executes there as a backstop. | |
| if: >- | |
| always() | |
| && needs.changes.result == 'success' | |
| && (github.event_name != 'workflow_dispatch' | |
| || inputs.platform == 'all' || inputs.platform == 'mock') | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| # Whether the real work runs this time. On push/PR/merge_group: only when | |
| # build-and-test succeeded and the change is heavy. On workflow_dispatch: | |
| # build-and-test is skipped, so tolerate that and rely on the job-level | |
| # platform gate above. | |
| - name: Decide whether to run | |
| id: gate | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| elif [ "${{ needs.build-and-test.result }}" = "success" ] \ | |
| && [ "${{ needs.changes.outputs.heavy }}" = "true" ]; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Install native build deps | |
| if: steps.gate.outputs.run == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y pkg-config libcap-dev | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| if: steps.gate.outputs.run == 'true' | |
| # The workspace test job selects only affected crates and runs the cucumber | |
| # harness, not the library's own unit tests — run the capability/expectation | |
| # /report unit tests (resolver logic, grid reconciliation) here. | |
| - name: Unit tests (e2e-cucumber lib) | |
| if: steps.gate.outputs.run == 'true' | |
| run: cargo test -p e2e-cucumber --lib | |
| - name: Run E2E tests | |
| if: steps.gate.outputs.run == 'true' | |
| run: cargo xtask e2e | |
| - name: Upload E2E report | |
| if: always() && steps.gate.outputs.run == 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: e2e-report | |
| path: tests/e2e-cucumber/results/ | |
| # Consolidate the mock (GitHub-hosted) platform's report into the cross-platform | |
| # report: a platform × tier matrix in the run Summary plus a single merged HTML | |
| # artifact. `if: always()` so a failing platform still appears; runs on | |
| # GitHub-hosted ubuntu (no GPU needed — it only parses report.json). | |
| # | |
| # The self-hosted GPU platforms are consolidated by e2e-selfhosted.yml's own | |
| # report job (they moved there so an offline runner can't stall this workflow's | |
| # required checks). This job only depends on the mock `e2e` lane; the `*-report` | |
| # glob still auto-includes any new GitHub-hosted platform added here. | |
| e2e-report: | |
| name: E2E consolidated report | |
| runs-on: ubuntu-latest | |
| # Bound the job (like every E2E job) so a stalled artifact download can't hang | |
| # a hosted runner indefinitely. | |
| timeout-minutes: 15 | |
| needs: | |
| - changes | |
| - e2e | |
| # Consolidate whatever ran. On dispatch `heavy` is unset, so also run when the | |
| # trigger was manual; `always()` still lets it collect partial/failed tiers. | |
| if: >- | |
| always() | |
| && (needs.changes.outputs.heavy == 'true' | |
| || github.event_name == 'workflow_dispatch') | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 | |
| # Pull every e2e artifact matching the glob. `xtask e2e-report` turns each | |
| # into one labeled platform and the `*-report` glob makes new platforms | |
| # appear automatically. Layout note: download-artifact@v8 extracts a match | |
| # into e2e-artifacts/<artifact-name>/ when several match, but flattens | |
| # straight into e2e-artifacts/ when EXACTLY ONE matches (its source picks | |
| # the root path when `artifacts.length === 1`). After the ci.yml ⇄ | |
| # e2e-selfhosted.yml split this job has one artifact, so the flattened | |
| # layout is the norm here; `discover()` handles both (labeling the root | |
| # file from platform.json's slug). | |
| - name: Download all E2E reports | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: '*-report' | |
| path: e2e-artifacts | |
| - name: Build consolidated report + step summary | |
| run: | | |
| mkdir -p consolidated | |
| cargo xtask e2e-report \ | |
| --artifacts-dir e2e-artifacts \ | |
| --html-out consolidated/index.html >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload consolidated report | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: e2e-consolidated-report | |
| path: consolidated/ |