Skip to content

fix(release): reuse an existing release instead of creating a duplica… #1637

fix(release): reuse an existing release instead of creating a duplica…

fix(release): reuse an existing release instead of creating a duplica… #1637

Workflow file for this run

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/