mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
Lands the 0.10.3 program work that two Codex sessions left uncommitted on
canonical main (chats 01a176ff "Review latest MCP updates" / 01a176e0 "DSH
plugin compatibility"), on top of contributor PR #7086, which merged as itself (5c3d461d8a)
immediately before this commit. The pieces share compiler inputs
(the crate-split moves touch most of them), so they land as one verified
tree rather than as intermediate commits that would not build.
Contents:
- Crate split slices: `codewhale-net`, `codewhale-notify` and
`codewhale-sandbox` crates; runtime config, config persistence, oauth,
settings, catalog cache and Codex model roster moved from tui to runtime;
hooks, extension host config and instruction budget to config; callers
rewired, re-export facades removed (CW-6129).
- Follow-up repairs to #7086 instruction budgets (per-scope budgets,
canonical source labels, `instruction_assembly` session snapshot).
- MCP: official SDK 2.4 host backend by default with `server/discover`
and legacy fallback, metadata/output schema handling, OAuth issuer
binding, Engine/CU server modernization (CW-6713, CW-7543, CW-7544,
CW-7546).
- DSH plugin compatibility from the authorized exception lane `26ba80b06a`:
shared compatibility table, native command lifecycle, reviewed imports,
Native-only filesystem sandbox (CW-7054).
- Computer Use plugin-only cutover: vendored bundle removed, marketplace
install from the independent release (CW-6732).
- Image-heavy sessions: 256 KiB read budget, outbound image omission with
receipts, one smaller retry on HTTP 413 (CW-7550).
- Typed review operation (`core/engine/review.rs`, `runtime_api/review.rs`)
used by Platform structured reviews.
- Receipt-owned Unix uninstall (Refs #7091).
- Steering (CW-7552) and Linux Shift+Enter (CW-7551, Refs #7093) source
changes; runtime/manual proof still pending.
Fixes made during takeover verification:
- MCP: Rust now admits the legacy handshake whenever the SDK classifies the
discovery probe as legacy (any JSON-RPC error, or a result without the
modern supported version); before, servers answering the probe with a
result were refused after it.
- MCP: an admitted legacy handshake reports initialize's own failure (e.g.
401) instead of the probe's -32601.
- MCP: OAuth reactive refresh covers the side-effect-free probe.
- MCP: broker launch failures keep their diagnostic (missing command).
- Tool images must decode before delivery (truncated PNG was accepted).
- THIRD_PARTY_NOTICES lists MCP SDK 2.4.0 and DSH 0.2.1-rc.2 excerpts.
- Changelog entries for MCP, DSH, Computer Use and image validation.
- `#[cfg(test)]` on three test-only items; `cargo fmt --all`; tests updated
to post-cutover behavior (no built-in CU, `$schema` manifests, canonical
paths, lazy CU tools, SyncSession resets instruction assembly).
Verified on this exact tree (macOS, source sha 4353faf1 before changelog
edits; changelog/generated-doc edits only after):
- cargo check --workspace --all-targets --locked: exit 0, no warnings
- codewhale-config 806 passed; codewhale-runtime 850 passed;
codewhale-net 52 passed; codewhale-notify 31 passed; codewhale-cli 501
passed (nextest, 0 failed)
- codewhale-tui lib: 14515 passed, 0 failed, 21 skipped
- extension host npm test: 420 tests, 413 passed, 0 failed, 7 skipped
- web check-facts OK
Linux x86_64 (Boat VM, same source before the #7097 fold-in):
cargo check --workspace --all-targets --locked exit 0; codewhale-config
808, codewhale-runtime 849, codewhale-net 52, codewhale-notify 31,
codewhale-sandbox 100, codewhale-cli 501 passed, 0 failed. The tui lib
test binary was still compiling on the 16 GB VM at commit time.
#7097 (main) was three-way merged into approval_log.rs and clipboard.rs;
re-check after the fold-in: workspace check exit 0; tui approval_log,
clipboard, session_export, file_mention, pet_watch 213 passed.
Not run: hosted CI, clippy -D warnings, release packaging, live provider
or real MCP servers beyond local fixtures, Deepin run for #7093.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1461 lines
76 KiB
YAML
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/tui/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/tui/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/tui/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/tui/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/tui/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-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/tui/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/tui/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/tui/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
|