Files
Codewhale/.github/workflows/ci.yml
T
CodeWhale Bot e34f207d21 refactor(engine): extract the shared runtime from the TUI [skip ci]
Move the Engine band, extension host, fixtures and shared test support into
codewhale-engine; wire TUI/CLI to that owner. Preserve human-decision
construction boundaries and lower reusable test helpers. Remove unused
copied dependencies and repair path-based guards, scripts and crate maps.

Correct the blocking scanner external-module scope leak; raised counts equal
the corrected pre-move source. Refresh embedded built-in hashes after path-only
bundle regeneration. Restore the provider-free prompt budget to the exact
pre-b984e31290 macOS bytes and representative identities; tool catalogs are
unchanged. Portable prompt-fixture pinning remains follow-up work.

Local validation: Engine/TUI/CLI clippy --tests --no-deps -D warnings passed
with the existing allow-list; production CLI check passed; fmt passed.
Focused native run: 705 passed / 83 failed initially; after hash/path repairs,
81 affected cases plus 2 existing build-relocation cases passed (83/83).
Thus 786/788 original cases pass; 2 compiled-release-image cases lack input
and are not qualified. Frozen input inventories unchanged. macOS test-link
unwind-table warnings remain. Four metric tests pass; all 55 budgets pass.
Host Node: 413 pass, 7 skip; Bun: 414 pass, 6 skip. Existing scanner checks:
34 + 33 pass. Compiled-delivery synthetic fixtures: 7 pass. Web: 42 pass.
Import, blocking, dead-code, coarse-map, version and facts checks pass.

CW-7701. No hosted CI, provider call, release build or publication.
Stage A part 2 and the old App Server/ratatui dependencies remain separate.
2026-10-31 18:55:14 -07:00

1461 lines
76 KiB
YAML

name: CI
on:
pull_request:
branches: [main]
workflow_dispatch:
inputs:
expected_sha:
description: Exact 40-character commit selected by --ref (manual runs force all CI gates)
required: true
type: string
workspace_test_mode:
description: Workspace runner (shared-process qualification uses Ubuntu only; other gates still run)
type: choice
default: nextest
options:
- nextest
- shared-process-twice
permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'pull_request' && format('ci-pr-{0}', github.event.pull_request.number) || format('ci-{0}-{1}', github.workflow, github.sha) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUSTFLAGS: -Dwarnings
# Test threads share a process and tokio/async frames run deep; the default
# 2 MiB stack overflowed sporadically in runtime_api::tests::start_turn_*
# under load and aborted the whole lib suite (signal 6). 8 MiB is the
# measured-safe floor; nextest's per-process runs are unaffected either way.
RUST_MIN_STACK: 8388608
jobs:
changes:
name: Change detection
timeout-minutes: 10
runs-on: ubuntu-latest
outputs:
heavy: ${{ steps.detect.outputs.heavy }}
workflow: ${{ steps.detect.outputs.workflow }}
mobile: ${{ steps.detect.outputs.mobile }}
actions: ${{ steps.detect.outputs.actions }}
trusted: ${{ steps.trust.outputs.trusted }}
steps:
- name: Classify event trust
id: trust
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
THIS_REPO: ${{ github.repository }}
run: |
set -euo pipefail
# "trusted" means the code came from this repository, not a fork.
# Only trusted events may run on the self-hosted macOS runner: this
# repo is public with thousands of forks, and a fork PR on a
# self-hosted runner is arbitrary code execution on that machine.
if [ "${EVENT_NAME}" != "pull_request" ] || [ "${HEAD_REPO}" = "${THIS_REPO}" ]; then
echo "trusted=true" >> "$GITHUB_OUTPUT"
else
echo "trusted=false" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Detect executable changes
id: detect
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
BEFORE_SHA: ${{ github.event.before }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
run: |
set -euo pipefail
if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
if [[ "${#EXPECTED_SHA}" -ne 40 || "${EXPECTED_SHA}" =~ [^0-9a-fA-F] ]]; then
echo "::error::expected_sha must be a full 40-character commit SHA." >&2
exit 1
fi
actual="$(git rev-parse HEAD)"
expected_normalized="$(printf '%s' "${EXPECTED_SHA}" | tr '[:upper:]' '[:lower:]')"
if [[ "${actual}" != "${expected_normalized}" ]]; then
echo "::error::Dispatch resolved to ${actual}, not requested ${EXPECTED_SHA}." >&2
exit 1
fi
echo "Manual exact-head dispatch: forcing heavy, workflow, mobile, and action gates."
echo "heavy=true" >> "${GITHUB_OUTPUT}"
echo "workflow=true" >> "${GITHUB_OUTPUT}"
echo "mobile=true" >> "${GITHUB_OUTPUT}"
echo "actions=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
if [[ "${EVENT_NAME}" == "schedule" ]]; then
echo "heavy=true" >> "${GITHUB_OUTPUT}"
echo "workflow=true" >> "${GITHUB_OUTPUT}"
echo "mobile=true" >> "${GITHUB_OUTPUT}"
echo "actions=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
base=""
if [[ "${EVENT_NAME}" == "pull_request" && -n "${BASE_REF}" ]]; then
git fetch --no-tags origin "+${BASE_REF}:refs/remotes/origin/${BASE_REF}" --depth=1
base="origin/${BASE_REF}"
elif [[ -n "${BEFORE_SHA}" && "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]]; then
base="${BEFORE_SHA}"
fi
if [[ -z "${base}" ]]; then
echo "heavy=true" >> "${GITHUB_OUTPUT}"
echo "workflow=true" >> "${GITHUB_OUTPUT}"
echo "mobile=true" >> "${GITHUB_OUTPUT}"
echo "actions=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
mapfile -t changed < <(git diff --name-only "${base}" "${GITHUB_SHA}" | sort)
heavy=false
workflow=false
mobile=false
actions=false
for path in "${changed[@]}"; do
# Heavy classification. ORDER MATTERS: must-stay-heavy inputs are
# matched BEFORE any light entry so a script that only a
# heavy-gated job exercises can never be misclassified as light.
# Anything unrecognized falls through to the default-heavy `*)`
# arm (fail-safe default-heavy). Light-classified scripts below
# are exercised by ALWAYS-on jobs/steps that run regardless of
# `heavy` (check-versions.sh / check-ohos-deps.sh via Version
# drift, dev-cache/dev-test
# self-checks via Version drift), so no coverage is lost.
#
# Rust reads non-.rs files too, so "docs-only" is decided by what
# the binary and its tests consume, not by file extension. Every
# path under crates/ is heavy (about 70 include_str! calls embed
# crate markdown: skill bodies, crates/tui/CHANGELOG.md,
# SURVIVAL_CONTRACT.md, export fixtures). The docs/ files below
# are embedded with include_str!/include_bytes! or read by Rust
# tests, so they must match before the docs/*|*.md light arm.
# Everything else Rust reads (config.example.toml, workflows/,
# fleets/, scripts/*.json, .codewhale/, .env.example) is already
# heavy by default. .github/scripts/release-workflows.test.js
# fails if an include_str!/include_bytes! target classifies light.
case "${path}" in
scripts/release/npm-wrapper-smoke.js|scripts/mobile-smoke.sh|scripts/check-provider-registry.py|scripts/check-config-example.py)
heavy=true
;;
crates/*|docs/HOOKS.md|docs/KEYBINDINGS.md|docs/TELEMETRY.md|docs/FLEET.md|docs/FLEET_WORKFLOW_TUTORIAL.md|docs/zh_hans/FLEET.md|docs/2512.24601v2.pdf|docs/cloud-facts/*|docs/examples/*)
heavy=true
;;
docs/*|*.md|packaging/aur/*|.github/PULL_REQUEST_TEMPLATE.md|.github/ISSUE_TEMPLATE/*|.github/scripts/agent-task-metadata.test.sh|.github/workflows/agent-task-labels.yml|.github/workflows/auto-tag.yml|.github/workflows/stale.yml|.github/workflows/triage.yml|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/install-dogfood.sh|scripts/release/install-dogfood.test.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/dev-cache.sh|scripts/dev-cache.test.sh|scripts/dev-cargo.sh|scripts/dev-test.sh)
;;
*)
heavy=true
;;
esac
case "${path}" in
crates/workflow/*|.github/workflows/ci.yml)
workflow=true
;;
esac
# Mobile runtime surface: the `codewhale serve --mobile`
# HTTP/SSE stack that scripts/mobile-smoke.sh exercises. Pull
# requests run the smoke only when one of these changes; every
# push to main still runs it unconditionally as the pre-release
# safety net for anything this filter misses.
case "${path}" in
crates/app-server/*|crates/tui/src/runtime_api*|crates/tui/src/runtime_mobile.html|crates/engine/src/runtime_threads*|crates/tui/src/main.rs|scripts/mobile-smoke.sh|.github/workflows/ci.yml|Cargo.lock|Cargo.toml)
mobile=true
;;
esac
case "${path}" in
.github/workflows/*|.github/actionlint.yml|action.yml|scripts/github-action/*)
actions=true
;;
esac
done
echo "heavy=${heavy}" >> "${GITHUB_OUTPUT}"
echo "workflow=${workflow}" >> "${GITHUB_OUTPUT}"
echo "mobile=${mobile}" >> "${GITHUB_OUTPUT}"
echo "actions=${actions}" >> "${GITHUB_OUTPUT}"
echo "Heavy Rust CI required: ${heavy}"
echo "Workflow RLM cache CI required: ${workflow}"
echo "Mobile runtime smoke required (PRs): ${mobile}"
echo "Workflow lint required: ${actions}"
printf 'Changed files:\n'
printf ' %s\n' "${changed[@]}"
versions:
name: Version drift
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: actions/[email protected]
with:
node-version: 22
- name: Check version drift
# Checks 7 and 12 audit the previous-tag..HEAD commit range, not this
# tree, so a receipt another merge forgot reddens every open PR. They
# report here and block on every release path (release-candidate.yml,
# auto-tag.yml, release.yml, prepare-release.sh), which is where a
# missing receipt actually matters.
run: ./scripts/release/check-versions.sh --range-audit-advisory
- name: Check this PR's feature release-note receipts
# The range audit above is advisory because previous-tag..HEAD blames
# every open PR for receipts other merges forgot. This is the same
# check scoped to the PR's own commits, so it blocks: a `feat:` commit
# that references #N must add #N to CHANGELOG.md in the same PR.
# Locally: scripts/preflight.sh.
if: github.event_name == 'pull_request'
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: ./scripts/release/check-feature-release-notes.sh "${PR_BASE_SHA}" HEAD
- name: Check contributor credit
# The three credit surfaces were only ever cross-checked against
# `requiredCandidateCredits`, a hand-maintained list -- so they proved
# each other consistent while a contributor nobody remembered stayed
# invisible. Nine were missing from 0.10.0. This derives the expected
# set from the commit range instead (authors, co-author trailers, and
# `Harvested from PR #N by @handle`), so forgetting someone is red in
# final validation. Pull requests only report it: credit is written
# on main when the PR merges.
id: credit
continue-on-error: ${{ github.event_name == 'pull_request' }}
run: python3 scripts/check-contributor-credit.py
- name: Explain contributor credit on a pull request
if: github.event_name == 'pull_request' && steps.credit.outcome == 'failure'
run: echo "::notice title=Contributor credit::The names above are credited by a maintainer when this pull request merges. You do not need to edit docs/CONTRIBUTORS.md, CHANGELOG.md or web/lib/release-credits.ts."
- name: Check bundled plugin claims
# The 0.10.0 section claimed Computer Use 0.4.0 at revision ca6be22
# while the tree shipped 0.11.2 at d8640b17f275 -- three upstream
# releases apart, with nothing comparing the prose to the assets.
run: python3 scripts/check-bundled-plugin-claims.py
- name: Check OHOS dependency graph
run: ./scripts/release/check-ohos-deps.sh
- name: Check release helper contracts
run: |
bash .github/scripts/agent-task-metadata.test.sh
bash scripts/release/check-feature-release-notes.test.sh
bash scripts/release/generate-release-body.test.sh
bash scripts/release/install-dogfood.test.sh
bash scripts/release/prepare-release.test.sh
bash scripts/release/prune-actions-caches.test.sh
bash scripts/release/require-rc-receipt.test.sh
bash scripts/release/require-release-tag-checkout.test.sh
bash scripts/release/validate-crate-publish-order.test.sh
python3 scripts/release/publish-crates.test.py
bash scripts/release/verify-remote-tag.test.sh
bash packaging/aur/render.test.sh
sh scripts/dev-cache.test.sh
sh scripts/with-hermetic-test-home.test.sh
bash .github/scripts/update-homebrew-tap.test.sh
node .github/scripts/release-workflows.test.js
node --test .github/scripts/d8-executable.test.js
node --test scripts/release/assemble-release-assets.test.js
node --test scripts/release/ensure-release-assets-absent.test.js
node --test scripts/release/verify-release-inventory.test.js
- name: Run runtime web client tests
# crates/tui/tests/runtime_web_client.test.mjs exercises the embedded
# web client's event/snapshot state machine; it ran nowhere before.
run: node --test crates/tui/tests/runtime_web_client.test.mjs
plugin-conversion:
name: Plugin conversion
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.12'
- uses: actions/[email protected]
with:
node-version: 22
- name: Install data parser
run: python3 -m pip install --disable-pip-version-check PyYAML==6.0.2
- name: Check offline plugin conversion
# Pinned upstream YAML is data only; only our synthetic Node fixtures run.
# Always on: scripts and fixture changes must not depend on Rust CI filters.
run: python3 -B scripts/test_convert_plugin.py -v
integrations:
name: Integrations
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check Lighthouse SSH policy
run: python3 scripts/tencent-lighthouse/test_bootstrap_ssh.py
- uses: actions/[email protected]
with:
node-version: 22
- name: Test reusable GitHub review action
run: node --test scripts/github-action/*.test.mjs
- name: Run Runtime SDK runtime and type tests
run: |
npm ci --ignore-scripts --workspace @codewhale/runtime-sdk
npm test --workspace @codewhale/runtime-sdk
- name: Run chat-bridge suites
# All four bridges + bridge-core ship dependency-free node --test
# suites that no workflow ran. weixin has no lockfile by design
# (zero deps); npm test works without npm ci everywhere here.
run: |
set -euo pipefail
for bridge in bridge-core feishu-bridge telegram-bridge wecom-bridge weixin-bridge; do
echo "== ${bridge}"
(cd "integrations/${bridge}" && npm test)
done
- name: Run extension host suites and check the committed bundle
# The TypeScript extension host (experimental, [features]
# extension_host). Tests exercise both the committed bundle and
# source services backed by locked Cordis dependencies. Install before
# testing; then reject any bundle drift. Node 22 matches the host floor.
run: |
set -euo pipefail
cd crates/engine/extension-host
npm ci --ignore-scripts
npm test
npm run typecheck
npm run build
git diff --exit-code -- dist
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
# The validated floor (`BUN_MIN_VERSION_FOR_EXTENSION_HOST`).
bun-version: 1.4.0
- name: Run extension host suites on Bun
# Bun is an opt-in host runtime (`runtime = "bun"` or `"auto"`);
# Node stays the default. The same suites spawn the committed bundle
# under `bun test`, with Bun's launch flags. This leg runs the JS
# suites only; the Rust host tests run on Node.
run: |
set -euo pipefail
cd crates/engine/extension-host
npm run test:bun
extension-host-runtimes:
name: Extension host runtimes (${{ matrix.os }})
needs: changes
if: needs.changes.outputs.heavy == 'true'
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: crates/engine/extension-host
shell: bash
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/[email protected]
with:
node-version: 22
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2
with:
bun-version: 1.4.0
- run: npm ci
- run: npm run typecheck && npm run build
- name: Required Node and Bun source-host receipts
run: npm test && npm run test:bun
- name: Compile the same entry with the installed runtime and test it
run: |
bun_binary="$(bun -p 'process.execPath')"
host_binary="$(node -p "require('path').join(process.env.RUNNER_TEMP, 'codewhale-extension-host' + (process.platform === 'win32' ? '.exe' : ''))")"
node compile-host.mjs --bun "$bun_binary" --output "$host_binary"
CODEWHALE_COMPILED_HOST_TEST_BINARY="$host_binary" CODEWHALE_BUN_TEST_BINARY="$bun_binary" node --test test/compiled-host.test.mjs
- name: Keep runtime receipts on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: extension-host-runtime-${{ matrix.os }}
path: crates/engine/extension-host/dist/builtin-modules.json
if-no-files-found: error
vscode-extension:
name: VS Code extension
timeout-minutes: 15
runs-on: ubuntu-latest
defaults:
run:
working-directory: extensions/vscode
steps:
- uses: actions/checkout@v7
- uses: actions/[email protected]
with:
# The extension targets VS Code >=1.90, whose extension host is
# Node 20, and its @types/node pin is ^20. Build and test on the
# runtime the extension actually ships against.
node-version: 20
- name: Install extension dependencies
run: npm ci
- name: Run VS Code extension suites
# extensions/vscode ships node --test suites (api, markdown, sse) that
# NO workflow ran: release.yml only reads package.json for a version
# string, so the whole client compiled and shipped without its tests or
# `tsc` ever running in CI. `npm test` compiles first (tsc -p ./), so
# this is the type-check gate for the extension too.
run: npm test
# @vscode/vsce 4 is a build-only packager that requires Node >=22. The
# extension itself still compiles and tests on Node 20 above.
- uses: actions/[email protected]
with:
node-version: 22
- name: Package VS Code extension
run: npm run package
safety-gate:
name: Safety gate
needs: changes
if: needs.changes.outputs.heavy == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- uses: mozilla-actions/[email protected]
id: sccache
# The GitHub Actions cache backend is main-only, mirroring
# rust-cache's save-if: PR runs wrote thousands of refs/pull/N
# entries that pushed the repo past its 10 GiB cache cap.
if: github.ref == 'refs/heads/main'
continue-on-error: true
- name: Enable sccache
if: steps.sccache.outcome == 'success'
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
- name: Install Linux system dependencies
run: |
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config bubblewrap apparmor-profiles
sh scripts/prepare-linux-test-sandbox.sh
- name: Ubuntu build headroom
shell: bash
run: bash scripts/prepare-ubuntu-build-headroom.sh
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: taiki-e/install-action@nextest
- uses: actions/[email protected]
with:
node-version: 24
- name: Hermetic safety and authorization tests
env:
RUST_MIN_STACK: "8388608"
CODEWHALE_EXT_HOST_TESTS: '1'
# nextest fails when the filter selects no test (`--no-tests=fail`),
# so moving these modules to another crate cannot turn this step into
# a silent 0-test pass. It does not catch a step that merely shrinks,
# so name every package that owns safety tests explicitly:
# codewhale-runtime owns `safe_label` (its hostile-authority test)
# since RS-2; add the next package when another safety module moves.
run: |
sh scripts/with-hermetic-test-home.sh cargo nextest run -p codewhale-engine -p codewhale-tui -p codewhale-runtime --lib --locked --no-tests=fail -E 'test(auto_review) | test(authority) | test(sandbox)'
sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-execpolicy --locked
lint:
name: Lint
needs: changes
# Measured on 89ee629e with runner headroom: clippy 10 min, runtime-contract
# budget 16 min, and persistence-backlog still running at the old 45-minute
# cancellation. Commands, filters and budgets are unchanged.
timeout-minutes: 75
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@master
if: needs.changes.outputs.heavy == 'true'
with:
toolchain: stable
components: rustfmt, clippy
- uses: mozilla-actions/[email protected]
id: sccache
# Cache bootstrap failures (e.g. GitHub 504s fetching the sccache
# binary) degrade to an uncached build instead of failing product CI.
continue-on-error: true
# Main-only GitHub Actions cache backend; see the Safety gate job.
if: needs.changes.outputs.heavy == 'true' && github.ref == 'refs/heads/main'
- name: Enable sccache
if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success'
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
- name: Install Linux system dependencies
if: needs.changes.outputs.heavy == 'true'
run: |
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config
- name: Ubuntu build headroom
if: needs.changes.outputs.heavy == 'true'
shell: bash
run: bash scripts/prepare-ubuntu-build-headroom.sh
- uses: Swatinem/rust-cache@v2
if: needs.changes.outputs.heavy == 'true'
with:
cache-bin: false
# PRs restore the cache seeded by main but skip the expensive
# post-job save; sccache covers PR-specific compilation deltas.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check formatting
if: needs.changes.outputs.heavy == 'true'
run: cargo fmt --all -- --check
- name: Run clippy
# --all-targets, because without it CI never lints test code at all.
# That gap is not theoretical: the v0.9.10 release gate opened with
# four clippy failures sitting on a green main, and every one of them
# was in a test target. crates/tui/AGENTS.md already documents the
# all-targets command as the release gate; this makes CI run the gate
# it points contributors at instead of a weaker subset.
#
# collapsible_if and assertions_on_constants are no longer allowed for
# the same reason — they were three of those four, so the allowances
# were hiding exactly the class of problem that reached the gate. The
# three that remain are deliberate project style, not oversights.
if: needs.changes.outputs.heavy == 'true'
run: |
cargo clippy --workspace --all-targets --all-features --locked -- \
-D warnings \
-A clippy::uninlined_format_args \
-A clippy::too_many_arguments \
-A clippy::unnecessary_map_or
- name: sccache stats
if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success'
continue-on-error: true
shell: bash
run: sccache --show-stats
- name: Check core navigation source and normal dependency boundary
if: needs.changes.outputs.heavy == 'true'
shell: bash
run: |
python3 -B scripts/test_check_command_core_navigation_proof.py
graph=$(mktemp)
trap 'rm -f "$graph"' EXIT
cargo tree --locked -p codewhale-portable-core-navigation --edges normal --all-features --target all --prefix none > "$graph"
python3 -B scripts/check-command-core-navigation-proof.py --graph "$graph"
- name: Check provider registry drift
if: needs.changes.outputs.heavy == 'true'
run: |
python3 scripts/check-provider-registry.py
python3 scripts/check-config-example.py
- name: Check the offline model seed is generated
if: needs.changes.outputs.heavy == 'true'
# crates/config/assets/models_dev.bundled.json is rendered from
# scripts/catalog/models_dev_seed.toml and its lock (#6396). A hand edit
# fails here; docs/CATALOG_REFRESH.md has the regenerate steps. The age
# check fails a pin that has silently rotted: render --check only proves
# the asset matches the lock, never that the lock is recent.
run: |
python3 scripts/catalog_models_dev.py seed render --check
python3 scripts/catalog_models_dev.py seed age --max-days 30
python3 -m unittest scripts/catalog_models_dev_test.py
- name: Check command-contract prototype boundary
if: needs.changes.outputs.heavy == 'true'
# The runtime -> UI ratchet baseline is compared with the one at the
# PR base too, so a hand-edited JSON cannot raise it.
shell: bash
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
python3 scripts/test_check_command_crate_boundaries.py
baseline="${PR_BASE_SHA:-${PUSH_BEFORE_SHA:-}}"
if [[ -n "${baseline}" && ! "${baseline}" =~ ^0+$ ]]; then
git fetch --no-tags origin "${baseline}"
python3 scripts/check-command-crate-boundaries.py --baseline-ref "${baseline}"
else
python3 scripts/check-command-crate-boundaries.py
fi
- name: Check command migration manifest
if: needs.changes.outputs.heavy == 'true'
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
python3 scripts/test_check_command_migration_manifest.py
baseline="${PR_BASE_SHA:-${PUSH_BEFORE_SHA:-}}"
if [[ -n "${baseline}" && ! "${baseline}" =~ ^0+$ ]]; then
git fetch --no-tags origin "${baseline}"
python3 scripts/check-command-migration-manifest.py --baseline-ref "${baseline}"
else
python3 scripts/check-command-migration-manifest.py
fi
- name: Check reqwest client constructors
if: needs.changes.outputs.heavy == 'true'
run: |
python3 scripts/test_check_reqwest_builders.py
python3 scripts/check-reqwest-builders.py
# Clippy above runs with `--all-targets` (see the clippy step), so the
# gap this ratchet covers is not "tests keep it alive" but suppression
# itself: it refuses to let dead-code suppression rise (#4785), counting
# both `#[allow(dead_code)]` and `#[expect(dead_code)]`, because counting
# one spelling let a sweep rewrite allows as expects and book it as
# progress (#6241).
- name: Test dead-code and blocking-calls budget scripts
if: needs.changes.outputs.heavy == 'true'
run: |
python3 scripts/test_check_dead_code_budget.py
python3 scripts/test_check_blocking_calls_budget.py
# The four budget ratchets below (dead-code, blocking-calls,
# runtime-contract, persistence-backlog) assert whole-repo properties.
# They used to be advisory on every pull request and fatal on push, so
# every PR looked green and main went red after merge (38 of 154
# main-push runs green, 2026-09-16..22). Now scripts/ratchet-gate.sh
# blocks a same-repo PR that adds debt and prints the `--update` receipt
# command that lands the fix in that PR. It stays advisory in exactly
# two cases: the PR's merge base fails the same check (inherited debt,
# re-checked on a throwaway checkout of the base), or the PR comes from
# a fork. Dispatch has no base and blocks. Runtime-contract and
# persistence-backlog measure for many minutes, so only final
# validation runs them.
- name: Resolve ratchet merge base
if: needs.changes.outputs.heavy == 'true' && github.event_name == 'pull_request'
shell: bash
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
# The PR checkout is the synthetic merge commit; its first parent is
# the base tip this PR actually merges into.
if git rev-parse -q --verify HEAD^2 >/dev/null; then
base="$(git rev-parse HEAD^1)"
else
base="${PR_BASE_SHA}"
fi
echo "Ratchet merge base: ${base}"
echo "RATCHET_BASE_SHA=${base}" >> "${GITHUB_ENV}"
- name: Check dead-code budget
if: needs.changes.outputs.heavy == 'true'
continue-on-error: ${{ github.event_name == 'pull_request' && needs.changes.outputs.trusted != 'true' }}
run: >-
bash scripts/ratchet-gate.sh --name dead-code
--update "python3 scripts/check-dead-code-budget.py --update"
-- python3 scripts/check-dead-code-budget.py
# Ratchet for blocking calls that could park Tokio workers: any new
# thread::sleep/std::fs site outside spawn_blocking, dedicated-thread,
# or test scopes must be isolated or budgeted (#6149).
- name: Check blocking-calls budget
if: needs.changes.outputs.heavy == 'true'
continue-on-error: ${{ github.event_name == 'pull_request' && needs.changes.outputs.trusted != 'true' }}
run: >-
bash scripts/ratchet-gate.sh --name blocking-calls
--update "python3 scripts/check-blocking-calls-budget.py --update"
-- python3 scripts/check-blocking-calls-budget.py
- name: Test runtime-contract measurement harness
if: needs.changes.outputs.heavy == 'true'
run: |
python3 scripts/test_measure_runtime_contract.py
python3 scripts/test_check_runtime_contract_budget.py
# The offline runtime-contract measurement needs the full locked graph,
# dev-dependencies included (e.g. wiremock -> assert-json-diff), but
# clippy above builds no test targets and the rust-cache registry key
# derives from Cargo.lock, so any lock-changing PR (every dependabot
# bump) restores an empty cache and the hermetic `cargo test --offline`
# dies with "failed to download ... --offline was specified" before a
# single budget is measured. Fetch the locked graph once here so the
# measurement below is deterministic on every branch.
- name: Fetch locked dependency graph for offline measurement
if: needs.changes.outputs.heavy == 'true' && github.event_name != 'pull_request'
run: cargo fetch --locked
# Provider-free local measurement. The checker forces Cargo offline and
# the measurement script runs only locked, ignored Rust metric tests.
- name: Check runtime-contract budget
if: needs.changes.outputs.heavy == 'true' && github.event_name != 'pull_request'
run: >-
bash scripts/ratchet-gate.sh --name runtime-contract
--update "python3 scripts/check-runtime-contract-budget.py --update --allow-increase"
-- python3 scripts/check-runtime-contract-budget.py
# Provider-free paused-consumer measurement of the production
# persistence request channel. RSS is sampled only on macOS; every host
# enforces the accepted/retained request and payload contract.
- name: Test persistence-backlog measurement and checker harnesses
if: needs.changes.outputs.heavy == 'true'
run: |
python3 scripts/test_measure_persistence_backlog.py
python3 scripts/test_check_persistence_backlog_budget.py
- name: Check persistence-backlog budget
if: needs.changes.outputs.heavy == 'true' && github.event_name != 'pull_request'
run: >-
bash scripts/ratchet-gate.sh --name persistence-backlog
--update "python3 scripts/check-persistence-backlog-budget.py --update"
-- python3 scripts/check-persistence-backlog-budget.py
- name: Check README translations stay in sync
if: github.event_name != 'schedule'
run: python3 scripts/check-readme-translations.py
- name: Check README locale link symmetry
if: github.event_name != 'schedule'
run: bash scripts/check-readme-locales.sh
- name: Check TUI locale pack parity
if: github.event_name != 'schedule'
run: python3 scripts/check-tui-locale-parity.py
- name: Check TUI product vocabulary
if: github.event_name != 'schedule'
run: sh scripts/check-tui-product-vocabulary.sh
- name: Check website locale dictionary parity
if: github.event_name != 'schedule'
run: node web/scripts/check-locales.mjs
- name: Skip Rust lint for light change
if: needs.changes.outputs.heavy != 'true'
run: echo "No executable Rust changes detected; preserving required Lint context."
msrv:
# The workspace must build on its minimum supported toolchain. A lint
# expectation that newer rustc fulfils can be unfulfilled on 1.89 and
# fail the build there while stable stays green (#6543).
name: MSRV check (1.89)
needs: changes
if: needs.changes.outputs.heavy == 'true'
timeout-minutes: 45
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.89"
- name: Install Linux system dependencies
run: |
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo +1.89 check
run: cargo +1.89 check --workspace --locked
workflow-rlm-cache:
name: Workflow RLM cache
needs: changes
if: needs.changes.outputs.workflow == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/[email protected]
id: sccache
# Main-only GitHub Actions cache backend; see the Safety gate job.
if: github.ref == 'refs/heads/main'
continue-on-error: true
- name: Enable sccache
if: steps.sccache.outcome == 'success'
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run workflow crate tests
run: sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-workflow --locked
test:
name: Test
needs: changes
# Contexts "Test (ubuntu-latest)" / "Test (macos-latest)" /
# "Test (windows-latest)" derive from job name + matrix.os and are
# independent of runs-on. For light changes the macOS/Windows legs only
# echo a skip line, so run them on ubuntu instead of queueing for scarce
# macOS/Windows runners. Pull requests run only the Linux leg here; the
# required "Test (windows-latest)" context comes from test-windows-pr.
# The ternary is safe: matrix.os is always a non-empty literal, so
# runs-on can never evaluate to empty.
# A cold GitHub-hosted Mac spent 77-80 minutes in Test on 2026-09-23
# (fork PRs #6431, #6417), too close to the old 90-minute limit.
# On 2026-10-04 a PR leg spent 111 minutes compiling with no cache and
# was cancelled at 120 with 12,356 tests passed and none failed. The
# limit stays a literal (release-workflows.test.js, #5496); 210 bounds
# cold compilation and the complete suite without disabling test deadlines.
timeout-minutes: 210
# macOS legs go to the self-hosted Mac ONLY when all three hold: the
# change is heavy, the event is trusted (not a fork PR), and the
# CW_SELF_HOSTED_MAC repo variable is 'true'. That variable is the kill
# switch: unset it and every leg falls back to GitHub-hosted runners
# immediately, with no commit — important because an offline
# self-hosted runner queues jobs forever, which is worse than a slow one.
runs-on: ${{ needs.changes.outputs.heavy != 'true' && 'ubuntu-latest' || (matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true' && fromJSON('["self-hosted","macOS","ARM64","codewhale-mac"]')) || matrix.os }}
strategy:
# A failure on one desktop platform must not erase evidence from the
# other one. We need both conclusions to diagnose and release safely.
fail-fast: false
matrix:
# Pull requests run the Linux workspace suite only; the macOS and
# Windows suites run in final validation. The explicit manual
# libtest qualification uses one hosted Ubuntu job, never a fork PR or
# self-hosted runner; normal dispatches keep all three legs.
os: ${{ fromJSON((github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')) && '["ubuntu-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }}
steps:
- name: Skip tests for light change
if: needs.changes.outputs.heavy != 'true'
run: echo "No executable Rust changes detected; preserving required Test context."
- uses: actions/checkout@v7
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
- name: Test Windows installer PATH helper
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
shell: pwsh
run: ./scripts/installer/update-user-path.tests.ps1
- name: Install NSIS for Windows installer regression
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
shell: pwsh
run: ./scripts/installer/install-nsis.ps1
- name: Test Windows installer PATH regression
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
shell: pwsh
run: ./scripts/installer/installer-path-regression.tests.ps1 -AllowUserPathMutation
- uses: dtolnay/rust-toolchain@stable
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
- name: Install macOS LLVM tools
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
run: rustup component add llvm-tools --toolchain stable
- name: Omit macOS build debug metadata
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
shell: bash
# Match the existing full-workspace CNB gate. Keep debug assertions and
# every test; only disposable build debug tables change on this lane.
run: printf '%s\n' 'CARGO_PROFILE_DEV_DEBUG=0' 'CARGO_PROFILE_TEST_DEBUG=0' >> "$GITHUB_ENV"
- uses: mozilla-actions/[email protected]
id: sccache
continue-on-error: true
# Main-only GitHub Actions cache backend; see the Safety gate job.
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && github.ref == 'refs/heads/main'
- name: Enable sccache
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
- name: Install Linux system dependencies
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
run: |
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config bubblewrap apparmor-profiles
sh scripts/prepare-linux-test-sandbox.sh
- name: Ubuntu build headroom
# Reuse the same fixed hosted-runner provisioning as Safety and Lint.
# Exit 143 is a shutdown observation, not proof of OOM or timeout.
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
shell: bash
run: bash scripts/prepare-ubuntu-build-headroom.sh
- uses: Swatinem/rust-cache@v2
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
with:
cache-bin: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: taiki-e/install-action@nextest
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
- name: Check portable config policy boundary
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
shell: bash
run: |
python3 -B scripts/test_check_command_config_proof.py
sh scripts/check-portable-config.sh
- name: Check portable core navigation boundary
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
shell: bash
run: |
python3 -B scripts/test_check_command_core_navigation_proof.py
sh scripts/check-portable-core-navigation.sh
- uses: actions/[email protected]
# The extension-host integration tests spawn the real bundle under
# Node >= 22.19; CODEWHALE_EXT_HOST_TESTS below makes a missing Node a
# failure instead of a silent skip.
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
with:
node-version: 22
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
with:
bun-version: 1.4.0
- name: Build matching compiled-host input for required Native containment receipts
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
shell: bash
run: |
set -euo pipefail
bun_binary="$(bun -p 'process.execPath')"
host_binary="$(node -p "require('path').join(process.env.RUNNER_TEMP, 'codewhale-extension-host' + (process.platform === 'win32' ? '.exe' : ''))")"
compiled_info="$(node crates/engine/extension-host/compile-host.mjs --bun "$bun_binary" --output "$host_binary")"
printf 'CODEWHALE_COMPILED_HOST_TEST_BINARY=%s\n' "$host_binary" >> "$GITHUB_ENV"
node - "$host_binary" "$bun_binary" "$compiled_info" >> "$GITHUB_ENV" <<'JS'
const fs = require('node:fs'), crypto = require('node:crypto');
const digest = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const info = JSON.parse(process.argv[4]);
if (!info.platform || !info.arch || info.platform !== info.compiler?.platform || info.arch !== info.compiler?.arch) throw new Error('image/compiler identity mismatch');
console.log('CODEWHALE_COMPILED_HOST_TEST_PLATFORM=' + info.platform);
console.log('CODEWHALE_COMPILED_HOST_TEST_ARCH=' + info.arch);
console.log('CODEWHALE_COMPILED_HOST_TEST_SHA256=' + digest(process.argv[2]));
console.log('CODEWHALE_COMPILED_HOST_TEST_BUN=' + process.argv[3]);
console.log('CODEWHALE_COMPILED_HOST_TEST_RUNTIME_SHA256=' + digest(process.argv[3]));
console.log('CODEWHALE_COMPILED_HOST_TEST_BUNDLE_SHA256=' + digest('crates/engine/extension-host/dist/codewhale-extension-host.mjs'));
JS
# The required hermetic full invocation executes the shared actual
# compiled Native secret/history/write scenario on each OS; Windows
# additionally requires Node/Bun/compiled LPAC owner/reparse probes.
# Missing requested compiled input or a failed sandbox is red.
- name: Hold this machine's build lock (self-hosted)
# The self-hosted Mac is also a developer machine: local agents build
# through scripts/dev-cargo.sh, which holds this lock, and two Cargo
# builds at once exhaust its memory. Opt-in: the runner's `.env` names
# the same file as CODEWHALE_BUILD_LOCK_FILE. The holder is a
# background process; the runner kills orphans when the job ends, so a
# cancelled or crashed job cannot leave the lock held.
# (`env.*` in `if:` cannot see the runner's `.env`, so the opt-in is
# checked in the script.)
if: needs.changes.outputs.heavy == 'true' && runner.environment == 'self-hosted'
shell: bash
run: |
if [ -z "${CODEWHALE_BUILD_LOCK_FILE:-}" ]; then
echo "CODEWHALE_BUILD_LOCK_FILE is not set on this runner; building without the machine lock."
exit 0
fi
hold="$RUNNER_TEMP/cw-build-lock.hold"
ready="$RUNNER_TEMP/cw-build-lock.ready"
log="$RUNNER_TEMP/cw-build-lock.log"
touch "$hold"
rm -f "$ready"
# The single quotes are deliberate: the inner sh expands $1/$2.
# shellcheck disable=SC2016
nohup python3 scripts/build-lock.py "$CODEWHALE_BUILD_LOCK_FILE" -- \
sh -c 'touch "$1"; while [ -e "$2" ]; do sleep 2; done' _ "$ready" "$hold" \
>"$log" 2>&1 &
shown=0
until [ -e "$ready" ]; do
if [ "$shown" -eq 0 ] && [ -s "$log" ]; then cat "$log"; shown=1; fi
sleep 2
done
cat "$log"
# Scripts inside this job already run under the lock.
echo "CODEWHALE_BUILD_LOCK_HELD=1" >> "$GITHUB_ENV"
- name: Build canonical executable for acceptance tests
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
shell: bash
run: cargo build -p codewhale-cli --bin codewhale --all-features --locked
env:
RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
- name: Windows Terminal clipboard acceptance
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
timeout-minutes: 5
shell: pwsh
run: node scripts/release/windows-terminal-clipboard.mjs target/debug/codewhale.exe "${{ runner.temp }}/windows-clipboard"
- name: Retain Windows Terminal clipboard evidence
if: ${{ !cancelled() && matrix.os == 'windows-latest' && needs.changes.outputs.heavy == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-clipboard-${{ github.sha }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/windows-clipboard/
if-no-files-found: error
retention-days: 7
- name: Run tests
# Same test binaries as `cargo test`, run by cargo-nextest: one
# process per test, all runner cores busy, slow tests named instead
# of stalling the binary. `.config/nextest.toml` serializes the PTY
# binary and bounds the integration binary that spawns the real
# executable; retries are off, so a flake is a red run, not a hidden
# one. nextest does not run doctests — the next step keeps them.
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && !(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
shell: bash
run: |
monitor=
if [ "$RUNNER_OS" = Linux ]; then
# Headroom trace: if the runner is terminated again, the last lines
# say whether memory or disk ran out.
( while sleep 30; do
echo "[headroom] $(free -m | awk '/^Mem:/{print "mem used " $3 "/" $2 " MiB"} /^Swap:/{print "swap used " $3 "/" $2 " MiB"}' | paste -sd ' ') disk free $(df -h / | awk 'NR==2{print $4}')"
done ) &
monitor=$!
fi
status=0
sh scripts/with-hermetic-test-home.sh cargo nextest run --workspace --all-features --locked --profile ci || status=$?
if [ -n "$monitor" ]; then kill "$monitor" 2>/dev/null || true; fi
exit "$status"
env:
CARGO_BUILD_JOBS: '1'
QA_PTY_DIAGNOSTICS_DIR: ${{ runner.temp }}/pty-failures
CODEWHALE_EXT_HOST_TESTS: '1'
CODEWHALE_EXT_HOST_BUN_TESTS: '1'
# sccache 0.17 panics resolving its config directory under the
# isolated Windows home before Cargo can compile or run any test.
# Bypass only that optional cache; keep the full suite and isolation.
RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
# Give test threads the stack the product gives itself. main.rs runs
# the owner thread and every tokio worker at
# CODEWHALE_MAIN_STACK_BYTES (32 MiB) because the engine and
# runtime-thread futures are genuinely deep. `#[tokio::test]` builds
# its own runtime and never sees that, so tests ran the same code on
# ~2 MiB (~1 MiB on Windows) — a configuration that never ships.
# That gap is what aborted the whole Windows test binary with
# STATUS_STACK_OVERFLOW in start_turn_accepts_dynamic_tools_and_
# environment_id, masking every other Windows result (78afd8d3d4
# Box::pin'd that one frame; the mismatch itself remained). std reads
# this for any thread spawned without an explicit size, which covers
# both libtest's per-test threads and tokio's workers. Test threads
# hold 16 MiB: the engine-only chains they run measured a ~2.5 MiB
# debug high-water, while the full ~16.5 MiB UI-loop chain that
# forced 32 MiB lives in spawned binaries, which size their own
# stacks explicitly and never read this variable.
RUST_MIN_STACK: '16777216'
- name: Retain sealed terminal failure diagnostics
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: terminal-failures-${{ runner.os }}-${{ github.sha }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/pty-failures/
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
- name: Shared-process workspace qualification
if: github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice' && matrix.os == 'ubuntu-latest'
shell: bash
env:
CARGO_BUILD_JOBS: '1'
EXPECTED_SHA: ${{ inputs.expected_sha }}
CODEWHALE_EXT_HOST_TESTS: '1'
CODEWHALE_EXT_HOST_BUN_TESTS: '1'
RUST_MIN_STACK: '16777216'
RUSTFLAGS: ''
CARGO_ENCODED_RUSTFLAGS: ''
RUSTC_WRAPPER: ''
CARGO_TERM_COLOR: never
run: |
set -euo pipefail
umask 077
evidence="$RUNNER_TEMP/shared-process-workspace"
mkdir -p "$evidence"
unset RUST_TEST_THREADS
if [[ ! "$EXPECTED_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::expected_sha must be a full 40-character commit SHA." | tee "$evidence/preflight-error.txt"
exit 1
fi
expected="$(printf '%s' "$EXPECTED_SHA" | tr '[:upper:]' '[:lower:]')"
if [[ "$(git rev-parse HEAD)" != "$expected" || "$GITHUB_SHA" != "$expected" ]]; then
echo "::error::Qualification checkout does not match the exact dispatch SHA." | tee "$evidence/preflight-error.txt"
exit 1
fi
{
printf 'commit=%s\ntree=%s\n' "$expected" "$(git rev-parse 'HEAD^{tree}')"
rustc -Vv
cargo -V
uname -a
printf 'available_cpus=%s\n' "$(getconf _NPROCESSORS_ONLN)"
printf 'runner_os=%s\nrunner_arch=%s\n' "$RUNNER_OS" "$RUNNER_ARCH"
printf 'image_os=%s\nimage_version=%s\n' "${ImageOS:-unknown}" "${ImageVersion:-unknown}"
printf 'RUST_MIN_STACK=%s\nCARGO_INCREMENTAL=%s\nCARGO_BUILD_JOBS=%s\n' \
"$RUST_MIN_STACK" "${CARGO_INCREMENTAL:-default}" "${CARGO_BUILD_JOBS:-default}"
printf '%s\n' 'RUST_TEST_THREADS=unset (default libtest concurrency)' \
'RUSTFLAGS=empty; CARGO_ENCODED_RUSTFLAGS=empty; RUSTC_WRAPPER=empty' \
'command=sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked -- --format=pretty'
} > "$evidence/environment.txt"
cat > "$evidence/required-tests.txt" <<'EOF'
remote_control::tests::classic_recovery_uses_persisted_seq_floor_and_ignores_older_terminal
remote_control::tests::actual_start_reclaims_runtime_chat_writer_before_worker_spawn
remote_control::tests::separate_predispatch_crashes_on_one_run_get_distinct_recovery_turn_ids
runtime_api::tests::events_endpoint_respects_since_seq_cursor
runtime_threads::tests::approval_required_awaits_external_decision_allow
runtime_threads::tests::approval_remember_grants_tool_class_without_changing_posture
tools::subagent::budget_handback_tests::budget_handback_inflight_wall_timeout_persists_unreported_usage
tools::subagent::tests::child_permission_gate::wall_deadline_ends_pending_wait_with_receipt
tools::subagent::tests::resume_keeps_recorded_reasoning_in_manifest_and_request_after_parent_changes
EOF
failed=0
# Two fixed attempts, not retry-until-green. Preserve both native
# outcomes even when the first run fails; missing identities fail too.
for run in 1 2; do
if [[ "$(git rev-parse HEAD)" != "$expected" ]] || \
! git diff --quiet || ! git diff --cached --quiet; then
printf 'run=%s not_run=source_changed\n' "$run" >> "$evidence/results.txt"
failed=1
continue
fi
qualification_tmp=$(mktemp -d /tmp/cw69.XXXXXX)
printf 'run=%s start=%s tmpdir=%s\n' "$run" "$(date -u +%FT%TZ)" "$qualification_tmp" >> "$evidence/results.txt"
set +e
TMPDIR="$qualification_tmp" sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked -- --format=pretty \
2>&1 | tee "$evidence/run-$run.log"
pipeline_status=("${PIPESTATUS[@]}")
set -e
printf 'run=%s cargo_exit=%s log_exit=%s end=%s\n' \
"$run" "${pipeline_status[0]}" "${pipeline_status[1]}" "$(date -u +%FT%TZ)" >> "$evidence/results.txt"
if [[ "${pipeline_status[0]}" != 0 || "${pipeline_status[1]}" != 0 ]]; then
failed=1
fi
while IFS= read -r identity; do
if grep -Fxq "test $identity ... ok" "$evidence/run-$run.log"; then
printf 'run=%s test=%s result=ok\n' "$run" "$identity" >> "$evidence/results.txt"
else
printf 'run=%s test=%s result=missing_pass\n' "$run" "$identity" >> "$evidence/results.txt"
failed=1
fi
done < "$evidence/required-tests.txt"
if [[ "$(git rev-parse HEAD)" != "$expected" ]] || \
! git diff --quiet || ! git diff --cached --quiet; then
printf 'run=%s source_changed=true\n' "$run" >> "$evidence/results.txt"
failed=1
fi
# Plugin fixtures can leave read-only directories outside HOME.
# Change only owned directories; never follow links or chmod files.
permission_exit=0
find -P "$qualification_tmp" -type d -exec chmod u+w {} + || permission_exit=$?
cleanup_exit=0
rm -rf -- "$qualification_tmp" || cleanup_exit=$?
printf 'run=%s permission_exit=%s cleanup_exit=%s\n' \
"$run" "$permission_exit" "$cleanup_exit" >> "$evidence/results.txt"
if [[ "$permission_exit" != 0 || "$cleanup_exit" != 0 ]]; then
failed=1
fi
done
cat "$evidence/results.txt"
exit "$failed"
- name: Retain shared-process qualification evidence
if: always() && github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice' && matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v7
with:
name: shared-process-workspace-${{ github.sha }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/shared-process-workspace/
if-no-files-found: error
retention-days: 30
- name: Export exact compiled-image Native containment receipt
# JUnit is from the same full invocation: no second compile/test run.
# The official entire CI run must be green before release can consume
# this artifact. Missing/changed inputs or silent test skips are red.
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && !(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
shell: bash
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
git diff --exit-code
git diff --cached --exit-code
python3 scripts/compiled-host-native-receipt.py \
--junit target/nextest/ci/junit.xml \
--compiled-image "$CODEWHALE_COMPILED_HOST_TEST_BINARY" \
--bundle crates/engine/extension-host/dist/codewhale-extension-host.mjs \
--bun "$CODEWHALE_COMPILED_HOST_TEST_BUN" --source-sha "$GITHUB_SHA" \
--image-platform "$CODEWHALE_COMPILED_HOST_TEST_PLATFORM" --image-arch "$CODEWHALE_COMPILED_HOST_TEST_ARCH" \
--host-sha256 "$CODEWHALE_COMPILED_HOST_TEST_SHA256" \
--runtime-sha256 "$CODEWHALE_COMPILED_HOST_TEST_RUNTIME_SHA256" \
--bundle-sha256 "$CODEWHALE_COMPILED_HOST_TEST_BUNDLE_SHA256" \
--output "$RUNNER_TEMP/native-compiled-host"
- name: Retain exact compiled image and Native receipt
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && !(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: native-compiled-host-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/native-compiled-host/
if-no-files-found: error
retention-days: 30
- name: Run doctests
# The shared-process command already includes every workspace doctest.
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && !(github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice')
shell: bash
run: sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked --doc
env:
RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
RUST_MIN_STACK: '16777216'
# The Ubuntu lint lane validates non-RSS backlog fields. Run the same
# source-bound measurement on macOS so loss or growth of RSS evidence
# fails closed instead of becoming an unsupported-field skip.
# Only on the self-hosted Mac: there it reuses the warm build. Every
# hosted Mac leg (fork PRs, or any PR with CW_SELF_HOSTED_MAC off) runs
# it in the separate `macos-budget` job with its own timeout, because
# on a cold hosted Mac it pushed Test past 90 minutes.
- name: Check persistence-backlog RSS budget
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true'
run: python3 scripts/check-persistence-backlog-budget.py
- name: Lockfile drift guard
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
run: git diff --exit-code -- Cargo.lock
- name: Run Offline Eval Harness
# The eval harness is OS-independent prompt/composition checking;
# running it once (on the faster macOS leg, warm from the test build)
# instead of once per desktop OS keeps the coverage while taking
# ~2min off the Windows critical path. Self-hosted Mac only; hosted
# Mac legs run it in `macos-budget` (see the RSS step above).
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true'
run: cargo run -p codewhale-cli --bin codewhale --all-features -- eval
- name: sccache stats
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
continue-on-error: true
shell: bash
run: sccache --show-stats
- name: Linux test location (CNB)
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request'
run: echo "Linux workspace tests run on CNB for non-PR release/main pushes; pull requests run directly on Ubuntu."
- name: Release this machine's build lock (self-hosted)
if: always() && runner.environment == 'self-hosted'
shell: bash
run: rm -f "$RUNNER_TEMP/cw-build-lock.hold"
test-windows-pr:
name: Test (windows-latest)
needs: changes
if: github.event_name == 'pull_request'
timeout-minutes: 60
runs-on: ${{ needs.changes.outputs.heavy == 'true' && 'windows-latest' || 'ubuntu-latest' }}
steps:
- name: Skip Windows check for light change
if: needs.changes.outputs.heavy != 'true'
run: echo "No executable Rust changes detected; preserving required Test context."
- uses: actions/checkout@v7
if: needs.changes.outputs.heavy == 'true'
- name: Test Windows installer PATH helper
if: needs.changes.outputs.heavy == 'true'
shell: pwsh
run: ./scripts/installer/update-user-path.tests.ps1
- uses: dtolnay/rust-toolchain@stable
if: needs.changes.outputs.heavy == 'true'
- name: Check every workspace target compiles on Windows
if: needs.changes.outputs.heavy == 'true'
shell: bash
run: cargo check --workspace --all-targets --all-features --locked
- name: Lockfile drift guard
if: needs.changes.outputs.heavy == 'true'
run: git diff --exit-code -- Cargo.lock
- name: Record what this pull request leg covers
if: needs.changes.outputs.heavy == 'true'
shell: bash
run: |
{
echo "## Test (windows-latest) on a pull request"
echo
echo "Compiled every workspace target for Windows with warnings denied and ran the installer PATH helper tests."
echo "The Windows and macOS test suites run in final validation, before a release, not on pull requests."
} >> "$GITHUB_STEP_SUMMARY"
macos-budget:
# Fork PRs build cold on a GitHub-hosted Mac, and the RSS budget and the
# offline eval each rebuild codewhale-tui there. Inside Test that cost
# cancelled fork PRs at the 90-minute limit (jobs 106749104684 and
# 106235312282) before Test could report. Running both here, in parallel
# with Test and under their own timeout, keeps the coverage without
# holding the required Test (macos-latest) context hostage. When Test's
# macOS leg runs on the self-hosted Mac (trusted event and
# CW_SELF_HOSTED_MAC == 'true'), it runs these two steps on the warm
# build instead. The name keeps "(fork PR)" so check contexts stay stable.
# Hosted macOS allows only five concurrent jobs per account, and this job
# doubled every pull request's claim on them, queueing the required Test
# (macos-latest) leg for hours. So pull requests skip it and every push to
# main still runs it; a regression shows up on the merge that caused it.
name: macOS budget and eval (fork PR)
needs: changes
# The explicit Ubuntu-only qualification has no warm Test macOS leg, so
# retain these gates on the hosted fallback even when self-hosting is on.
if: needs.changes.outputs.heavy == 'true' && github.event_name != 'pull_request' && ((github.event_name == 'workflow_dispatch' && inputs.workspace_test_mode == 'shared-process-twice') || !(needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true'))
# Cold hosted builds consumed 73 minutes before eval could start (F run
# 37497498829). Match Test's compile allowance; RSS/eval limits stay intact.
timeout-minutes: 210
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
save-if: false
- name: Fetch dependencies before offline measurement
run: cargo fetch --locked
- name: Check persistence-backlog RSS budget
run: python3 scripts/check-persistence-backlog-budget.py
- name: Run Offline Eval Harness
run: cargo run -p codewhale-cli --bin codewhale --all-features -- eval
npm-wrapper-smoke:
name: npm wrapper smoke
needs: changes
if: github.event_name != 'schedule'
# Same ternary rationale as the Test job: light legs only echo, so keep
# them off macOS/Windows runners. On pull_request the matrix is
# ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)"
# context is unaffected. Heavy pull requests execute the Ubuntu smoke
# here; their branches may not be mirrored to CNB.
# Pushes to main run Ubuntu (a CNB pointer) and Windows only. The macOS
# leg was a ~28-minute hosted-Mac build on every main push while hosted
# macOS is capped at 5 concurrent jobs; it stays in the manual
# workflow_dispatch (full CI) matrix and in the release pipeline.
# A cold Windows compile can take 42 minutes; leave room for cache save
# after the separately bounded five-minute install/entrypoint smoke test.
timeout-minutes: 60
runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }}
strategy:
matrix:
os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || github.event_name == 'workflow_dispatch' && '["ubuntu-latest","macos-latest","windows-latest"]' || '["ubuntu-latest","windows-latest"]') }}
steps:
- name: Skip npm wrapper smoke for light change
if: needs.changes.outputs.heavy != 'true'
run: echo "No executable Rust changes detected; preserving required npm wrapper smoke context."
- uses: actions/checkout@v7
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
- uses: dtolnay/rust-toolchain@stable
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
- uses: mozilla-actions/[email protected]
id: sccache
continue-on-error: true
# Main-only GitHub Actions cache backend; see the Safety gate job.
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && github.ref == 'refs/heads/main'
- name: Enable sccache
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
- uses: actions/[email protected]
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
with:
node-version: 22
- name: Install Linux system dependencies
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
run: |
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config
- uses: Swatinem/rust-cache@v2
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
with:
cache-bin: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build wrapper binaries
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
# The smoke validates wrapper install/delegation plumbing, not
# codegen quality, so skip fat LTO + codegen-units=1 for a much
# cheaper release build. Shipped binaries keep the real profile via
# the Release workflow.
env:
CARGO_PROFILE_RELEASE_LTO: 'off'
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16'
run: cargo build --release --locked -p codewhale-cli --bin codewhale
- name: Smoke wrapper install and delegated entrypoints
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request')
timeout-minutes: 5
run: node scripts/release/npm-wrapper-smoke.js
- name: sccache stats
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success'
continue-on-error: true
shell: bash
run: sccache --show-stats
- name: Linux smoke location
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request'
run: echo "Linux npm wrapper smoke runs on CNB for non-PR release/main pushes; pull requests run directly on Ubuntu."
mobile-smoke:
name: Mobile runtime smoke
needs: changes
# Not a required PR context. Pull requests run it only when the mobile
# runtime surface changed (see the `mobile` filter above); every push to
# main runs it unconditionally as the pre-release safety net.
if: >-
github.event_name != 'schedule' &&
needs.changes.outputs.heavy == 'true' &&
(github.event_name != 'pull_request' || needs.changes.outputs.mobile == 'true')
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/[email protected]
id: sccache
# Main-only GitHub Actions cache backend; see the Safety gate job.
if: github.ref == 'refs/heads/main'
continue-on-error: true
- name: Enable sccache
if: steps.sccache.outcome == 'success'
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
- name: Install Linux system dependencies
run: |
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run mobile smoke tests
# The smoke exercises HTTP/SSE runtime behaviour, not codegen
# quality; skipping fat LTO + codegen-units=1 cuts the in-script
# release build from ~12min to a fraction of that.
env:
CARGO_PROFILE_RELEASE_LTO: 'off'
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16'
run: ./scripts/mobile-smoke.sh
- name: sccache stats
if: steps.sccache.outcome == 'success'
continue-on-error: true
shell: bash
run: sccache --show-stats
actionlint:
name: Workflow lint
needs: changes
if: needs.changes.outputs.actions == 'true'
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run actionlint
uses: docker://rhysd/actionlint:1.7.12
with:
# SC2129 (grouped redirects) is style-only and endemic to the
# existing GITHUB_ENV/GITHUB_OUTPUT append pattern; SC2221/SC2222
# flag the long-standing `*.md` glob shadowing the PR-template
# entry in change detection, which is intentional.
args: -color -ignore SC2129 -ignore SC2221 -ignore SC2222
# Check documentation builds without warnings
docs:
name: Documentation
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: Install Linux system dependencies
if: runner.os == 'Linux'
run: |
bash scripts/ci-apt-install.sh libdbus-1-dev pkg-config
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
- name: Build docs
run: cargo doc --workspace --no-deps
env:
RUSTDOCFLAGS: -Dwarnings
cargo-deny:
name: cargo-deny
needs: changes
if: github.event_name == 'workflow_dispatch'
uses: ./.github/workflows/cargo-deny.yml
security-audit:
name: Security audit
needs: changes
if: github.event_name == 'workflow_dispatch'
uses: ./.github/workflows/security-audit.yml
ohos:
name: OpenHarmony
needs: changes
if: github.event_name == 'workflow_dispatch'
uses: ./.github/workflows/ohos.yml