ci: split fast and deep static checks (#5634)

* ci: split fast and deep static checks

Fast PRs run typecheck, lint, and format before provider admission.
Knip, duplication, architecture guards, and Python stay on the full tier.
Shared-boundary fallbacks keep the Linux x64 baseline, and Turbo jobs
export remote-cache credentials when they are configured.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: keep pull requests from writing the remote cache

Trusted default-branch runs still receive TURBO_TOKEN. Pull requests get an
empty token and remote read-only mode. Knip saves a per-commit cache key so
the next trusted full run can refresh the restored snapshot.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: forward the release cache token and upload deep static reports

Release qualification now passes TURBO_TOKEN into the called workflow.
Labeled pull requests still do not. The deep static job records its task
context and uploads duplication reports after a failed gate.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* style: format the turbo cache policy test

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: keep fast checks exclusive and trusted cache writes explicit

Declare the static artifact name and context script so typecheck matches the
implementations. Lock check:ci:fast around dist, and publish the remote cache
only for release qualification or default-branch push, schedule, and dispatch.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: record static task graphs without nesting the artifacts lock

Keep check:ci:fast locked, and point graph recording at the unlocked
check:ci:fast:tasks script so check:pr and check:test can record hashes
while they already hold that lock.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* ci: keep unauthenticated Turbo runs on the local cache

Open pull requests do not receive TURBO_TOKEN, so a remote read mode cannot
authenticate. Those runs now use the local cache. Trusted runs still publish.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* test(ida): treat unchanged invalid requests as nonretryable

Main already stopped marking invalid_request retryable. The pull request
merge runs that projector, while the IDA lookup tests still expected
retryable true. Match the merged behavior.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
morluto
2026-10-21 03:29:31 +08:00
committed by GitHub
parent 0dbc046ca6
commit 0add6a5aae
40 changed files with 1042 additions and 166 deletions
+118 -8
View File
@@ -15,6 +15,15 @@ on:
description: Exact qualification source; must match the caller revision.
required: true
type: string
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted release qualification passes it.
required: false
permissions:
contents: read
@@ -29,6 +38,15 @@ concurrency:
env:
HUSKY: "0"
# Set Actions secret TURBO_TOKEN and variable TURBO_TEAM. OIDC can mint
# TURBO_TOKEN via vercel/setup-turborepo-remote-cache-action; do not commit a token.
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Release qualification sets remote_cache_write and passes the
# token. Open pull requests get an empty token and the local cache only.
# Remote reads need a credential, and pull requests do not receive one.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
jobs:
changes:
@@ -151,14 +169,10 @@ jobs:
if: needs.changes.outputs.code == 'true'
env:
CI_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CI_STATIC_CONTEXT_SCRIPT: check:ci:fast:tasks
run: npm run ci:static-context
- if: needs.changes.outputs.code == 'true'
run: npm run check:ci
- if: needs.changes.outputs.code == 'true'
run: npm run verify:test-discovery
- name: Check LLDB producer retention and launch contracts
if: needs.changes.outputs.code == 'true'
run: npm run test:python
run: npm run check:ci:fast
- if: needs.changes.outputs.code != 'true'
run: npm run format:check
- name: Save validated static task cache
@@ -180,6 +194,72 @@ jobs:
if-no-files-found: ignore
retention-days: 7
static-deep:
name: Deep static checks
needs: changes
# Full qualification only. Provider lanes wait on fast static, not this job.
if: needs.changes.outputs.tier == 'full' && needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- name: Restore deep static task cache
id: static-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .turbo/cache/
key: rea-static-deep-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-${{ github.sha }}
restore-keys: rea-static-deep-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-
- name: Restore Knip analysis cache
id: knip-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules/.cache/knip
# Each trusted commit publishes a new entry. Restore still matches the
# configuration prefix from the previous successful full run.
key: knip-meta-v1-${{ runner.os }}-${{ hashFiles('knip.json', 'tsconfig.json', 'package.json', 'package-lock.json') }}-${{ github.sha }}
restore-keys: knip-meta-v1-${{ runner.os }}-${{ hashFiles('knip.json', 'tsconfig.json', 'package.json', 'package-lock.json') }}-
- name: Record deep static task source and execution identity
continue-on-error: true
env:
CI_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CI_STATIC_CONTEXT_SCRIPT: check:ci:deep:tasks
run: npm run ci:static-context
- name: Run deep static checks
run: npm run check:ci:deep
- name: Save validated deep static task cache
if: >-
success() && steps.npm-cache.outputs.can-save == 'true' &&
steps.static-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .turbo/cache/
key: ${{ steps.static-cache.outputs.cache-primary-key }}
- name: Save Knip analysis cache
if: >-
success() && steps.npm-cache.outputs.can-save == 'true' &&
steps.knip-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules/.cache/knip
key: ${{ steps.knip-cache.outputs.cache-primary-key }}
- name: Upload complete deep static diagnostics
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: static-deep-check-reports
path: .cache/static-checks/
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
test-source:
name: Source tests
needs: changes
@@ -373,8 +453,9 @@ jobs:
retention-days: 7
sqlite-runtime:
# Preserve all selected coverage, but admit expensive host/provider work
# only after static validation. Portable tests still provide early feedback.
# Admit expensive host/provider work after fast static validation
# (typecheck, lint, format). Deep static does not gate these lanes.
# Portable tests still provide early feedback.
needs: [changes, static, build]
if: needs.changes.outputs.sqlite == 'true'
uses: ./.github/workflows/sqlite-runtime.yml
@@ -858,68 +939,96 @@ jobs:
skills/reverse-engineer-anything
if-no-files-found: error
# Provider lanes wait for fast static and the portable build, not deep static.
browser:
needs: [changes, static, build]
if: needs.changes.outputs.browser == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-browser.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
binary_layout:
needs: [changes, static, build]
if: needs.changes.outputs.binary_layout == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-binary-layout.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
evm:
needs: [changes, static, build]
if: needs.changes.outputs.evm == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-evm-interface.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
javascript_recovery:
needs: [changes, static, build]
if: needs.changes.outputs.javascript_recovery == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-javascript-recovery.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
recorded_crash:
needs: [changes, static, build]
if: needs.changes.outputs.recorded_crash == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-recorded-crash.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
web_network:
needs: [changes, static, build]
if: needs.changes.outputs.web_network == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-web-network-captures.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
web_runtime:
needs: [changes, static, build]
if: needs.changes.outputs.web_runtime == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-web-runtime.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
web_source_map:
needs: [changes, static, build]
if: needs.changes.outputs.web_source_map == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-web-source-map.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
agentenv:
needs: [changes, static, build]
if: needs.changes.outputs.agentenv == 'true'
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
uses: ./.github/workflows/real-agentenv.yml
with:
build_cache_artifact: rea-build-cache
remote_cache_write: ${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}
nativeaot:
needs: [changes, static]
@@ -944,6 +1053,7 @@ jobs:
[
changes,
static,
static-deep,
build,
reverse-tools-quality,
test-shard,
+5
View File
@@ -14,6 +14,11 @@ concurrency:
env:
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
jobs:
mutation:
+16
View File
@@ -9,12 +9,28 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
permissions:
contents: read
env:
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
NODE_OPTIONS: --max-old-space-size=768
PIP_DISABLE_PIP_VERSION_CHECK: "1"
+16
View File
@@ -14,6 +14,15 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
permissions:
contents: read
@@ -21,6 +30,13 @@ permissions:
env:
NODE_OPTIONS: --max-old-space-size=768
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
REA_VERIFY_LARGE_ELF_MCP: ${{ inputs.large_mcp && '1' || '0' }}
jobs:
+16
View File
@@ -8,10 +8,26 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
workflow_dispatch:
env:
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
permissions:
contents: read
+8
View File
@@ -8,6 +8,14 @@ on:
permissions:
contents: read
env:
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
jobs:
windows-native-artifact:
name: Build Windows bridge security boundary
+16
View File
@@ -9,6 +9,15 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
permissions:
contents: read
@@ -16,6 +25,13 @@ permissions:
env:
NODE_OPTIONS: --max-old-space-size=768
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
jobs:
real-evm-interface:
@@ -6,6 +6,11 @@ on:
env:
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
permissions:
contents: read
+5 -12
View File
@@ -13,20 +13,8 @@ on:
- "src/composition/go.ts"
- "src/cli/goCommands.ts"
- "src/server/registerGoTools.ts"
- "src/server/createServer.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/artifacts/readStableArtifact*.ts"
- "src/artifacts/ArtifactReader.ts"
- "src/process/PrivateRuntimeRoot.ts"
- "scripts/verify-go-binary.mjs"
- "scripts/lib/mcp-verifier-results.mjs"
- "scripts/rea.mjs"
- "package.json"
- "package-lock.json"
- ".nvmrc"
permissions:
contents: read
@@ -38,6 +26,11 @@ concurrency:
env:
NODE_OPTIONS: --max-old-space-size=768
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
jobs:
real-go-binary:
+5
View File
@@ -5,6 +5,11 @@ on:
env:
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
permissions:
contents: read
+5
View File
@@ -5,6 +5,11 @@ on:
env:
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
permissions:
contents: read
@@ -8,6 +8,15 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
workflow_dispatch:
permissions:
@@ -15,6 +24,13 @@ permissions:
env:
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
NODE_OPTIONS: "--max-old-space-size=768"
RAYON_NUM_THREADS: "1"
+7 -11
View File
@@ -20,21 +20,17 @@ on:
- "src/composition/captures.ts"
- "src/cli/captureCommands.ts"
- "src/server/registerCaptureTools.ts"
- "src/server/createServer.ts"
- "src/server/sessionAvailabilityPolicy.ts"
- "src/application/CapabilityInventory.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/process/**"
- "src/artifacts/readStableArtifact.ts"
- "scripts/verify-packet-capture.mjs"
- ".github/workflows/real-packet-capture.yml"
- "package*.json"
- ".nvmrc"
permissions:
contents: read
env:
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
concurrency:
group: real-packet-capture-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
+16
View File
@@ -9,6 +9,15 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
permissions:
contents: read
@@ -16,6 +25,13 @@ permissions:
env:
NODE_OPTIONS: --max-old-space-size=768
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
UV_CONCURRENT_BUILDS: "1"
UV_CONCURRENT_INSTALLS: "1"
UV_PYTHON_DOWNLOADS: never
+7 -11
View File
@@ -13,21 +13,17 @@ on:
- "src/composition/signals.ts"
- "src/cli/signalCommands.ts"
- "src/server/registerSignalTools.ts"
- "src/server/createServer.ts"
- "src/server/sessionAvailabilityPolicy.ts"
- "src/application/CapabilityInventory.ts"
- "src/contracts/toolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/cli.ts"
- "src/cliCommandNames.ts"
- "src/process/**"
- "src/artifacts/readStableArtifact.ts"
- "scripts/verify-signal-recording.mjs"
- ".github/workflows/real-signal-recording.yml"
- "package*.json"
- ".nvmrc"
permissions:
contents: read
env:
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
concurrency:
group: real-signal-recording-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
+5
View File
@@ -12,6 +12,11 @@ permissions:
env:
HUSKY: "0"
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
jobs:
real-termux:
@@ -9,12 +9,28 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
permissions:
contents: read
env:
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
NODE_OPTIONS: --max-old-space-size=768
jobs:
+16
View File
@@ -7,11 +7,27 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
workflow_dispatch:
permissions:
contents: read
env:
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
jobs:
real-web-runtime:
runs-on: ubuntu-latest
+16
View File
@@ -7,11 +7,27 @@ on:
required: false
type: string
default: ""
remote_cache_write:
description: Trusted release qualification may publish the remote cache.
required: false
type: boolean
default: false
secrets:
TURBO_TOKEN:
description: Remote cache token. Only trusted callers pass it.
required: false
workflow_dispatch:
permissions:
contents: read
env:
HUSKY: "0"
# A called workflow sees the caller event, so workflow_call is not an event
# name here. Writes when remote_cache_write is set, or on the default branch
# for push, schedule, and manual dispatch. Open pull requests stay empty-token
# and use the local cache only.
TURBO_TOKEN: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'))) && 'local:rw,remote:rw' || 'local:rw' }}"
jobs:
real-web-source-map:
runs-on: ubuntu-latest
+9
View File
@@ -21,6 +21,12 @@ on:
env:
HUSKY: "0"
# Remote cache stays local when either value is empty.
# Writes only on the default branch for push, schedule, manual dispatch,
# and workflow_call. Pull requests get an empty token and remote read-only mode.
TURBO_TOKEN: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && secrets.TURBO_TOKEN || '' }}"
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_CACHE: "${{ (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call')) && 'local:rw,remote:rw' || 'local:rw,remote:r' }}"
concurrency:
group: release-${{ inputs.release_branch || github.event.pull_request.merge_commit_sha || github.ref_name }}
@@ -301,8 +307,11 @@ jobs:
contents: read
actions: read
uses: ./.github/workflows/ci.yml
secrets:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
with:
source_sha: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
remote_cache_write: true
rehearsal:
name: Rehearse qualified release on a clean runner
+55 -19
View File
@@ -69,8 +69,14 @@ Source projects also contain large capacity regressions; `test:fast` promises
no compiled-runtime prerequisite, not a fixed time budget.
`npm run lint` prints errors and a warning summary; `npm run jscpd` prints
duplication statistics. Complete reports remain in `.cache/static-checks/` and
CI uploads them as `static-check-reports`, including after a failed static gate.
duplication statistics. Complete reports remain in `.cache/static-checks/`.
The fast gate uploads them as `static-check-reports`. Full qualification
uploads duplication diagnostics from the deep job as
`static-deep-check-reports`, including after that gate fails.
Task-graph recording runs `check:ci:fast:tasks` and `check:ci:deep:tasks`
without the artifacts lock, so it can record hashes while `check:pr` or
`check:test` already holds that lock. The CI jobs still run the locked
`check:ci:fast` and `check:ci:deep` commands.
Use `npm run lint:details` or `npm run jscpd:details` for all console diagnostics.
Both duplicate commands scan production and test code with the same 3% limit;
the summary commands preserve checker failures and restore reports from cached
@@ -481,14 +487,16 @@ real-system lanes remain separate jobs so one kind of evidence cannot stand in
for another. Workflow syntax and expression validation runs inside the required
static-check job when workflow changes select it, avoiding a separate runner.
Native, provider, fixture, SQLite compatibility, and website lanes wait for
static checks to succeed before allocating their runners. This avoids spending
those runner slots on revisions that already fail static validation. Portable
test shards and documentation checks remain independent of static results, so
they can still provide diagnostic feedback. The required gate rejects skipped
selected lanes when static checks fail; passing revisions retain all selected
coverage. This ordering reduces peak demand on invalid revisions but can add
static-check waiting to otherwise passing runs; it is not a measured reduction
in end-to-end latency.
the fast static checks (typecheck, built lint, and format) to succeed before
allocating their runners. Knip, duplication, architecture guards, test
discovery, and the Python producer run in the separate full-tier deep static
job and do not gate provider admission. Portable test shards and documentation
checks remain independent of static results, so they can still provide
diagnostic feedback. The required gate rejects skipped selected lanes when
static checks fail; on the full tier it also requires the deep static job.
Passing revisions retain all selected coverage. This ordering reduces peak
demand on invalid revisions but can add static-check waiting to otherwise
passing runs; it is not a measured reduction in end-to-end latency.
Core jobs have execution limits of five minutes for classification, ten minutes
for static checks, builds, docs, and report merging, and thirty minutes per
@@ -1167,13 +1175,16 @@ scripts also select it. Unrelated code edits retain the complete portable suite
on the pinned Node version without starting four additional compatibility jobs.
Full qualification retains every SQLite version.
Shared process/contract/CLI boundaries, dependencies, other packaging inputs,
unknown paths, and changes to the planner select the full baseline.
unknown paths, and changes to the planner select the fast Linux x64 baseline:
portable code checks and the installed-package runtime lane, without provider
lanes or the multi-host package matrix. A shared file with exactly one owner
still selects that owner. Full qualification still selects every lane.
CI timing collection, timing history, and test-report helper edits retain static
checks and the complete portable suite without selecting unrelated host/provider
lanes. Classification, qualification, gate, release receipt, and unknown CI
scripts keep the broad fallback.
scripts keep that fast baseline.
Package script-only edits select their owning verifier when its entrypoint is
identifiable; unknown or shared script changes select everything. Android
identifiable; unknown or shared script changes select the fast baseline. Android
build-script edits retain the Linux suite and workflow validation; real Android
verification stays manual. Renames account for both paths.
@@ -1197,7 +1208,8 @@ or provider effects outside its owners.
Ordinary main pushes use the same owner scopes as PRs. Their diff starts at a
successful ancestor push run of this CI workflow, rather than the preceding
push: changes from failed or cancelled intermediate runs remain in scope.
Missing, untrusted, or non-ancestor history selects every lane. The lookup is
Missing, untrusted, or non-ancestor history selects the fast Linux x64
baseline: portable checks and the Linux runtime lane. The lookup is
bounded to the last 100 successful push runs; lookup failures broaden selection.
Every code change still selects all six portable shards, without coverage
instrumentation in this fast tier. Ordinary PR matrices cancel remaining
@@ -1227,8 +1239,11 @@ immediately starts the separate `Full PR qualification` workflow against the
label event's merge revision; subsequent PR revisions also select the full tier
in normal CI while that label remains. Unrelated labels do not start validation
jobs or replace the main CI checks. Manual dispatch also requests full validation.
Fast-tier broad fallbacks retain all owner lanes and the three-host
package matrix; Intel macOS and coverage remain full-tier obligations. Each
Fast-tier broad fallbacks keep the portable suite and the Linux x64 runtime
baseline. They do not start every owner lane or the three-host package
matrix. Intel macOS and coverage remain full-tier obligations. Real provider
lanes stay path-gated, and a shared registration file listed by many lanes
does not start those lanes on a fast PR. Each
classification job records its selection and broad-fallback reasons in the run
summary. Representative Git-diff cases live in
`tests/boundary/filesystem/ciChangeScope.test.ts`.
@@ -1308,9 +1323,14 @@ report in each shard's snapshot. The bounded lookup verifies the artifact's
producer; newer schedules that reuse qualification without producing timings do
not hide the last full run's artifact.
CI assigns discovered files by historical duration, with a deterministic
fallback weight for new files, rather than dividing only by file count. Every
fallback weight for new files, rather than dividing only by file count.
`process-boundary` files are weighted at their full duration because that
project runs files serially. Other projects are weighted at half their file
duration, matching the two-worker cap, so a serial file does not look as cheap
as a file that overlaps with another. Every
discovered file still belongs to exactly one shard; worker limits and project
ordering remain unchanged. The merge job rejects successful reports with missing,
ordering remain unchanged. Fast qualification keeps five built shards without
coverage. The merge job rejects successful reports with missing,
unexpected, or duplicate portable files. Coverage instrumentation and aggregate
thresholds remain enforced in full qualification.
Missing or unusable history falls back to Vitest's default assignment. Only
@@ -1318,8 +1338,24 @@ timing data is reused; live provider results are never cached. Turbo outputs are
restored by task hash, with trusted default-branch runs as cache writers and PRs
as restore-only consumers. The static job also restores its Turbo snapshot;
each task revalidates its own input hash before reusing outputs or diagnostics.
The Node pin and npm install settings are global hash inputs. Knip includes its ignore rules and VitePress
The Node pin and npm install settings are global hash inputs. Knip runs only
in full qualification, with `--cache` and a GitHub cache of
`node_modules/.cache/knip`. The saved key includes the commit SHA. Restore uses
the configuration prefix, so the next trusted full run can publish a refreshed
snapshot. That cache is an AST cache keyed by file mtime and size, so a
fresh checkout often misses it; Turbo still hashes the Knip task inputs. Knip
includes its ignore rules and VitePress
entry, so changes to those inputs cannot reuse an older successful check.
Jobs that run Turbo export `TURBO_TEAM`. A called workflow sees the caller
event, so `workflow_call` is not an event name inside CI. `TURBO_TOKEN` is set
when release qualification passes `remote_cache_write`, or when the ref is the
default branch and the event is push, schedule, or workflow_dispatch, and only
when that secret is available to the run. Release qualification passes
`TURBO_TOKEN` and `remote_cache_write` into the called workflow. Labeled
pull-request qualification does not. Pull requests receive an empty token and
`TURBO_CACHE=local:rw`. Remote reads require `TURBO_TOKEN`, and this repository
does not pass a separate read-only credential into pull requests. An empty
token leaves the local Turbo cache in place.
The shared npm-cache action caches downloads, not `node_modules`, and every
consumer still runs `npm ci`. Keys cover the runner OS, actual Node architecture,
+8 -2
View File
@@ -119,7 +119,13 @@
"precheck:pr": "npm run deps:check",
"check:pr": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check",
"check:ci": "node scripts/run-exclusive.mjs artifacts npm run check:ci:tasks && npm run verify:architecture-guards",
"precheck:ci:fast": "npm run deps:check",
"precheck:ci:deep": "npm run deps:check",
"check:ci": "npm run check:ci:fast && npm run check:ci:deep",
"check:ci:fast": "node scripts/run-exclusive.mjs artifacts npm run check:ci:fast:tasks",
"check:ci:fast:tasks": "turbo run typecheck lint:built format:check",
"check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python",
"check:ci:deep:tasks": "turbo run knip metadata:check jscpd scan:todos",
"check:ci:tasks": "turbo run typecheck lint:built format:check knip metadata:check jscpd scan:todos",
"ci:static-context": "node scripts/ci/static-task-context.mjs",
"build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills",
@@ -131,7 +137,7 @@
"lint:details": "oxlint && npm run verify:module-boundaries",
"lint:built": "npm run lint",
"lint:fix": "oxlint --fix && npm run verify:module-boundaries",
"knip": "knip",
"knip": "knip --cache",
"jscpd": "node scripts/ci/static-check-report.mjs duplicates",
"jscpd:details": "jscpd src/ tests/ --no-tips",
"scan:todos": "node scripts/scan-todos.mjs",
+1
View File
@@ -49,6 +49,7 @@ export function selectStaticCheckArtifact(
page: unknown,
run: { id?: string | number; head_sha?: string },
runAttempt: number,
name?: string,
): unknown | null;
export function readStaticCheckArtifact(options: {
+14 -5
View File
@@ -7,6 +7,11 @@ import { promisify } from "node:util";
import { parseStaticTaskContext } from "./static-task-context.mjs";
const execute = promisify(execFile);
/** Job display name owns the artifact that contains that gate's diagnostics. */
export const STATIC_EVIDENCE_ARTIFACTS = {
"Static checks": "static-check-reports",
"Deep static checks": "static-deep-check-reports",
};
const MAX_ARCHIVE_BYTES = 32 * 1024 * 1024;
const MAX_MEMBER_BYTES = 4 * 1024 * 1024;
const MAX_DIAGNOSTICS = 10_000;
@@ -43,7 +48,8 @@ export function classifyFailure({
"run_event_or_repository_identity_unverified",
],
() => [
job?.name !== mainJob?.name || job?.name !== "Static checks",
job?.name !== mainJob?.name ||
!Object.hasOwn(STATIC_EVIDENCE_ARTIFACTS, job?.name ?? ""),
"failed_job_not_the_same_static_gate",
],
() => [
@@ -134,7 +140,12 @@ export function safeSha(value) {
? value.toLowerCase()
: null;
}
export function selectStaticCheckArtifact(page, run, runAttempt) {
export function selectStaticCheckArtifact(
page,
run,
runAttempt,
name = STATIC_EVIDENCE_ARTIFACTS["Static checks"],
) {
if (runAttempt !== 1) return null;
if (
!Array.isArray(page?.artifacts) ||
@@ -143,9 +154,7 @@ export function selectStaticCheckArtifact(page, run, runAttempt) {
page.artifacts.length >= 100
)
return null;
const matches = page.artifacts.filter(
(artifact) => artifact.name === "static-check-reports",
);
const matches = page.artifacts.filter((artifact) => artifact.name === name);
if (matches.length !== 1) return null;
const [artifact] = matches;
if (
+9 -2
View File
@@ -11,6 +11,7 @@ import {
readStaticCheckArtifact,
safeSha,
selectStaticCheckArtifact,
STATIC_EVIDENCE_ARTIFACTS,
validRepoPath,
} from "./artifacts.mjs";
import { createGhApiRequest } from "./github-api.mjs";
@@ -366,12 +367,18 @@ export function createGitHubApi({
return page.jobs;
},
async getStaticEvidence(repository, run, job) {
if (job.name !== "Static checks") return null;
const artifactName = STATIC_EVIDENCE_ARTIFACTS[job.name];
if (!artifactName) return null;
if (run.run_attempt !== 1) return null;
const page = await request(
`repos/${repository}/actions/runs/${positiveId(run.id)}/artifacts?per_page=100&page=1`,
);
const artifact = selectStaticCheckArtifact(page, run, run.run_attempt);
const artifact = selectStaticCheckArtifact(
page,
run,
run.run_attempt,
artifactName,
);
if (!artifact) return null;
return await readStaticCheckArtifact({
repository,
+33 -2
View File
@@ -42,8 +42,18 @@ const matchesAny = (file, patterns) =>
patterns.some((pattern) => matchesGlob(file, pattern));
const selected = Object.fromEntries(flags.map((flag) => [flag, false]));
const reasons = [];
// Shared process, contract, CLI, and CI-script edits are not a specific provider.
// Fast qualification keeps the portable suite and the Linux x64 runtime
// baseline. Full qualification still selects every lane.
const SHARED_BOUNDARY =
/^(src\/process\/|src\/filesystem\/|src\/contracts\/tool|src\/server\/createServer|src\/cli\.ts|scripts\/ci\/)/;
const full = (reason) => {
for (const flag of flags) selected[flag] = true;
if (tier === "full") {
for (const flag of flags) selected[flag] = true;
} else {
selected.code = true;
selected.runtime = true;
}
reasons.push(reason);
};
const git = (...args) =>
@@ -196,6 +206,28 @@ if (tier === "reused") {
selected.code = true;
continue;
}
// Timing and report helpers stay on the portable suite. They are not a
// runtime or provider change.
if (PORTABLE_CI_REPORTERS.has(file)) {
selected.code = true;
continue;
}
// A shared boundary is not evidence that every provider or host changed.
// One matching owner is path evidence. Several owners means the same
// registration file is listed by many lanes; fast qualification does not
// fan those out or start the multi-host package matrix.
if (tier !== "full" && SHARED_BOUNDARY.test(file)) {
selected.code = true;
selected.runtime = true;
const owners = Object.entries(SCOPES)
.filter(([, patterns]) =>
patterns.some((pattern) => matchesGlob(file, pattern)),
)
.map(([owner]) => owner);
if (owners.length === 1) selected[owners[0]] = true;
reasons.push(`Shared boundary on fast tier: ${file}`);
continue;
}
const owners = Object.entries(SCOPES)
.filter(([, patterns]) =>
patterns.some((pattern) => matchesGlob(file, pattern)),
@@ -226,7 +258,6 @@ if (tier === "reused") {
}
if (matchesAny(file, READINESS_SOURCES)) selected.readiness = true;
if (file.startsWith("tests/")) continue;
if (PORTABLE_CI_REPORTERS.has(file)) continue;
if (
/^(src\/process\/|src\/filesystem\/|src\/contracts\/tool|src\/server\/createServer|src\/cli\.ts|scripts\/ci\/)/.test(
file,
+1
View File
@@ -29,6 +29,7 @@ if (
if (outputs.tier === "full" && selectedScopes.length !== CI_FLAGS.length)
throw new Error("Full qualification requires every canonical CI scope");
const required = reused ? ["changes"] : ["changes", "static", "test"];
if (outputs.tier === "full") required.push("static-deep");
const lanes = {
code: [
"build",
+1
View File
@@ -15,4 +15,5 @@ export function recordStaticTaskContext(options?: {
root?: string;
npmCli?: string;
outputDirectory?: string;
script?: string;
}): Promise<StaticTaskContext>;
+8 -1
View File
@@ -6,6 +6,10 @@ import { promisify } from "node:util";
import { z } from "zod";
const execute = promisify(execFile);
const STATIC_CONTEXT_SCRIPTS = new Set([
"check:ci:fast:tasks",
"check:ci:deep:tasks",
]);
const contextSchema = z.object({
version: z.literal(1),
sourceSha: z.string().regex(/^[a-f0-9]{40}$/u),
@@ -30,7 +34,10 @@ export async function recordStaticTaskContext({
root = process.cwd(),
npmCli = process.env.npm_execpath,
outputDirectory = join(root, ".cache/static-checks"),
script = process.env.CI_STATIC_CONTEXT_SCRIPT ?? "check:ci:fast:tasks",
} = {}) {
if (!STATIC_CONTEXT_SCRIPTS.has(script))
throw new Error("Static context script is not a known check");
if (typeof npmCli !== "string" || !npmCli)
throw new Error(
"Run static context recording through npm run ci:static-context",
@@ -44,7 +51,7 @@ export async function recordStaticTaskContext({
const [graph, source, status, npmVersion, turbo] = await Promise.all([
execute(
process.execPath,
[npmCli, "run", "--silent", "check:ci:tasks", "--", "--dry=json"],
[npmCli, "run", "--silent", script, "--", "--dry=json"],
options,
),
execute("git", ["rev-parse", "HEAD"], options),
+4 -3
View File
@@ -479,9 +479,10 @@ describe("headless IDA procedure lookup failures", () => {
expect(projected.category, "the category is invalid_input").toBe(
"invalid_input",
);
expect(projected.retryable, "the caller can correct and retry").toBe(
true,
);
expect(
projected.retryable,
"an unchanged invalid request is not retryable",
).toBe(false);
expect(
projected.details,
"the operation and exactly one issue that names the procedure in its message",
+105 -1
View File
@@ -219,8 +219,45 @@ it("shares static task snapshots only after validation using the npm cache owner
key: "${{ steps.static-cache.outputs.cache-primary-key }}",
});
expect(steps.indexOf(save!)).toBeGreaterThan(
steps.findIndex((step) => step.run === "npm run test:python"),
steps.findIndex((step) => step.run === "npm run check:ci:fast"),
);
const deep = ci?.workflow.jobs?.["static-deep"]?.steps ?? [];
const deepSave = deep.find(
(step) => step.name === "Save validated deep static task cache",
);
const knipRestore = deep.find((step) => step.id === "knip-cache");
const knipSave = deep.find(
(step) => step.name === "Save Knip analysis cache",
);
expect(deepSave?.uses).toBe(
"actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830",
);
expect(deepSave?.if).toContain("success()");
expect(deepSave?.if).toContain("steps.npm-cache.outputs.can-save == 'true'");
expect(deepSave?.if).toContain(
"steps.static-cache.outputs.cache-hit != 'true'",
);
expect(deep.indexOf(deepSave!)).toBeGreaterThan(
deep.findIndex((step) => step.run === "npm run check:ci:deep"),
);
const knipWith = z
.object({
path: z.string(),
key: z.string(),
"restore-keys": z.string(),
})
.parse(knipRestore?.with);
expect(knipWith.path).toBe("node_modules/.cache/knip");
expect(knipWith.key).toContain("${{ github.sha }}");
expect(knipWith["restore-keys"]).not.toContain("github.sha");
expect(knipWith["restore-keys"].startsWith("knip-meta-v1-")).toBe(true);
expect(knipSave?.if).toContain(
"steps.knip-cache.outputs.cache-hit != 'true'",
);
expect(knipSave?.with).toEqual({
path: "node_modules/.cache/knip",
key: "${{ steps.knip-cache.outputs.cache-primary-key }}",
});
});
it("passes the npm cache owner's selected path to package installation", async () => {
@@ -241,6 +278,73 @@ it("passes the npm cache owner's selected path to package installation", async (
).toBe("${{ steps.npm-cache.outputs.path }}");
});
it("exports turbo remote-cache credentials wherever a workflow runs turbo", async () => {
const directory = join(repository, ".github/workflows");
const files = (await readdir(directory)).filter((file) =>
/\.ya?ml$/u.test(file),
);
const turboCommand =
/\bturbo\b|build:cached|check:ci|docs:check|docs:generate|docs:prepare|test:prepare/u;
const workflowSchema = z.object({
env: z.record(z.string(), z.unknown()).optional(),
jobs: z.record(
z.string(),
z
.object({
env: z.record(z.string(), z.unknown()).optional(),
steps: z.array(z.object({ run: z.string().optional() })).optional(),
})
.passthrough(),
),
});
for (const file of files) {
const workflow = workflowSchema.parse(
parse(await readFile(join(directory, file), "utf8")),
);
for (const [name, job] of Object.entries(workflow.jobs)) {
const runsTurbo = job.steps?.some(
(step) => step.run !== undefined && turboCommand.test(step.run),
);
if (!runsTurbo) continue;
const env = { ...workflow.env, ...job.env };
const token = z.string().parse(env.TURBO_TOKEN);
const cache = z.string().parse(env.TURBO_CACHE);
expect(token, `${file}:${name}`).toContain("secrets.TURBO_TOKEN");
expect(token, `${file}:${name}`).toContain("|| ''");
expect(token, `${file}:${name}`).not.toBe("${{ secrets.TURBO_TOKEN }}");
const grantsReleaseWrite = token.includes(
"inputs.remote_cache_write == true",
);
const grantsCalledEvent = token.includes(
"github.event_name == 'workflow_call'",
);
expect(grantsReleaseWrite || grantsCalledEvent, `${file}:${name}`).toBe(
true,
);
if (grantsReleaseWrite)
expect(token, `${file}:${name}`).not.toContain(
"github.event_name == 'workflow_call'",
);
for (const event of ["push", "schedule", "workflow_dispatch"])
expect(token, `${file}:${name}`).toContain(
`github.event_name == '${event}'`,
);
expect(token, `${file}:${name}`).not.toContain(
"github.event_name == 'pull_request'",
);
if (grantsReleaseWrite) {
expect(cache, `${file}:${name}`).toContain(
"inputs.remote_cache_write == true",
);
expect(cache, `${file}:${name}`).toContain("|| 'local:rw'");
expect(cache, `${file}:${name}`).not.toContain("local:rw,remote:r'");
} else expect(cache, `${file}:${name}`).toContain("local:rw,remote:r'");
expect(cache, `${file}:${name}`).toContain("local:rw,remote:rw");
expect(env.TURBO_TEAM, `${file}:${name}`).toBe("${{ vars.TURBO_TEAM }}");
}
}
});
it("bounds the rolling static snapshot with Turbo age and size eviction", async () => {
const turbo = z
.object({
+105 -24
View File
@@ -222,12 +222,19 @@ it("preserves a provider lane when a portable CI reporter changes with provider
});
});
it("keeps unknown CI scripts on complete qualification", async () => {
it("keeps unknown CI scripts on the fast Linux baseline", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["scripts/ci/new-qualification.mjs"]);
const plan = await classify(directory, base, head);
const { CI_FLAGS } = await loadScopes();
for (const flag of CI_FLAGS) expect(plan[flag]).toBe(true);
expect(plan).toMatchObject({ tier: "fast", code: true, runtime: true });
for (const flag of CI_FLAGS)
if (flag !== "code" && flag !== "runtime") expect(plan[flag]).toBe(false);
expect(plan.matrix).toEqual({
include: [{ os: "ubuntu-latest", platform: "linux", architecture: "x64" }],
});
const full = await classify(directory, base, head, "pull_request", true);
for (const flag of CI_FLAGS) expect(full[flag]).toBe(true);
});
it("emits an observational owner proposal without changing ordinary or full portable execution", async () => {
@@ -399,7 +406,7 @@ it("includes changes from earlier cancelled pushes since the last successful bas
});
it.each(["", "not-a-commit"])(
"fails closed to all lanes for a missing or invalid push baseline (%s)",
"fails closed to the fast Linux baseline for a missing or invalid push baseline (%s)",
async (base) => {
const { directory } = await fixture();
const head = await change(directory, ["README.md"]);
@@ -407,13 +414,20 @@ it.each(["", "not-a-commit"])(
expect(plan).toMatchObject({
tier: "fast",
code: true,
package: true,
evm: true,
browser: true,
runtime: true,
package: false,
evm: false,
browser: false,
reasons: expect.arrayContaining([expect.stringContaining("baseline")]),
});
const { CI_FLAGS } = await loadScopes();
for (const flag of CI_FLAGS) expect(plan[flag]).toBe(true);
for (const flag of CI_FLAGS)
if (flag !== "code" && flag !== "runtime") expect(plan[flag]).toBe(false);
expect(plan.matrix).toEqual({
include: [
{ os: "ubuntu-latest", platform: "linux", architecture: "x64" },
],
});
},
);
@@ -518,9 +532,10 @@ it("fails closed when the successful push baseline is not an ancestor", async ()
expect(await classify(directory, unrelated, head, "push")).toMatchObject({
tier: "fast",
code: true,
package: true,
evm: true,
browser: true,
runtime: true,
package: false,
evm: false,
browser: false,
});
expect(head).not.toBe(base);
});
@@ -631,17 +646,39 @@ it.each([
],
[
"src/process/ProviderProcess.ts",
{ code: true, package: true, evm: true, browser: true },
{
code: true,
runtime: true,
package: false,
evm: false,
browser: false,
sqlite: false,
},
],
[
"package-lock.json",
{ code: true, package: true, evm: true, browser: true },
{
code: true,
runtime: true,
package: false,
evm: false,
browser: false,
},
],
[
".github/workflows/real-evm-interface.yml",
{ code: false, evm: true, browser: false, package: false },
],
["unknown-input", { code: true, package: true, evm: true, browser: true }],
[
"unknown-input",
{
code: true,
runtime: true,
package: false,
evm: false,
browser: false,
},
],
[
"src/application/Example.ts",
{
@@ -830,11 +867,35 @@ it.each([
"tests/boundary/mcp/serverLifecycleMcp.test.ts",
{ code: true, sqlite: true },
],
["src/process/OwnedCommand.ts", { code: true, sqlite: true, runtime: true }],
["src/server/createServer.ts", { code: true, sqlite: true, runtime: true }],
[
"src/process/OwnedCommand.ts",
{
code: true,
sqlite: false,
runtime: true,
package: false,
browser: false,
},
],
[
"src/server/createServer.ts",
{
code: true,
sqlite: false,
runtime: true,
package: false,
browser: false,
},
],
[
"src/contracts/toolContracts.ts",
{ code: true, sqlite: true, runtime: true },
{
code: true,
sqlite: false,
runtime: true,
package: false,
browser: false,
},
],
[
"src/domain/analysisErrorProjection.ts",
@@ -927,8 +988,10 @@ it("scopes package script edits while retaining broad dependency checks", async
);
expect(await classify(directory, head, dependency)).toMatchObject({
code: true,
browser: true,
package: true,
runtime: true,
browser: false,
package: false,
evm: false,
});
});
@@ -1089,11 +1152,24 @@ it.each([
],
[
".nvmrc",
{ code: true, package: true, browser: true, evm: true, sqlite: true },
{
code: true,
runtime: true,
package: false,
browser: false,
evm: false,
sqlite: false,
},
],
[
"tsconfig.build.json",
{ code: true, package: true, browser: true, evm: true },
{
code: true,
runtime: true,
package: false,
browser: false,
evm: false,
},
],
["docs/.vitepress/config.ts", { code: true, docs: true, package: false }],
[
@@ -1117,7 +1193,10 @@ it.each([
"src/contracts/evm/tools.ts",
{ code: true, docs: true, evm: true, package: false },
],
["scripts/unknown-website-helper.mjs", { code: true, package: true }],
[
"scripts/unknown-website-helper.mjs",
{ code: true, runtime: true, package: false, browser: false },
],
[
".github/workflows/release.yml",
{ workflows: true, code: true, package: false, website: false },
@@ -1229,9 +1308,11 @@ it("retains a broad fallback when a provider script adds shared shell work", asy
};
const head = await change(directory, ["package.json"], JSON.stringify(after));
expect(await classify(directory, base, head)).toMatchObject({
evm: true,
browser: true,
package: true,
code: true,
runtime: true,
evm: false,
browser: false,
package: false,
});
});
@@ -87,6 +87,30 @@ const comparison = (): Parameters<typeof classifyFailure>[0] => ({
blobs: { "src/example.ts": true },
});
it("attributes a deep duplication failure when both gates record the same jscpd task", () => {
const duplicate = { kind: "jscpd" as const, paths: ["src/a.ts", "src/b.ts"] };
const deepContext = (sourceSha: string) => ({
...context(sourceSha),
taskHashes: { jscpd: "c".repeat(16) },
});
expect(
classifyFailure({
...comparison(),
job: job({ name: "Deep static checks" }),
mainJob: job({ name: "Deep static checks" }),
diagnostics: [duplicate],
mainDiagnostics: [duplicate],
context: deepContext(sha("a")),
mainContext: deepContext(sha("c")),
blobs: { "src/a.ts": true, "src/b.ts": true },
}),
).toEqual({
classification: "matching-main-diagnostics",
reason:
"supported_diagnostics_match_failed_task_and_source_on_verified_main_ancestor",
});
});
it("attributes only an identical structured lint failure with matching task, context, ancestry, and source blob", () => {
const result = classifyFailure(comparison());
expect(result).toEqual({
@@ -115,6 +139,10 @@ it.each([
["ancestry", { ancestry: { status: "diverged", mergeBaseSha: sha("e") } }],
["canceled job", { job: job({ conclusion: "cancelled" }) }],
["not a static job", { job: job({ name: "Test shard (1)" }) }],
[
"deep gate compared with the fast gate",
{ job: job({ name: "Deep static checks" }) },
],
] as const)("does not attribute on mismatched %s", (_label, overrides) => {
const result = classifyFailure({ ...comparison(), ...overrides });
expect(result.classification).toBe("unknown");
@@ -411,6 +439,76 @@ it("uses static artifacts only when listing is complete, unique, and tied to thi
2,
),
).toBeNull();
const deep = { ...artifact, id: 13, name: "static-deep-check-reports" };
expect(
selectStaticCheckArtifact(
{ total_count: 2, artifacts: [artifact, deep] },
runRecord,
1,
"static-deep-check-reports",
),
).toBe(deep);
expect(
selectStaticCheckArtifact(
{ total_count: 2, artifacts: [artifact, deep] },
runRecord,
1,
),
).toBe(artifact);
expect(
selectStaticCheckArtifact(
{ total_count: 2, artifacts: [deep, deep] },
runRecord,
1,
"static-deep-check-reports",
),
).toBeNull();
});
it("loads each static gate from its own artifact", async () => {
const downloads: string[] = [];
const api = createGitHubApi({
token: "fixture-token",
fetchImpl: async (url) => {
downloads.push(String(url));
throw new Error("stop-before-download");
},
apiRequest: async () => ({
total_count: 2,
artifacts: [
{
id: 1,
name: "static-check-reports",
expired: false,
workflow_run: { id: 10, head_sha: sha("a") },
},
{
id: 2,
name: "static-deep-check-reports",
expired: false,
workflow_run: { id: 10, head_sha: sha("a") },
},
],
}),
});
const runRecord = { id: 10, run_attempt: 1, head_sha: sha("a") };
await expect(
api.getStaticEvidence("owner/rea", runRecord, {
name: "Deep static checks",
}),
).rejects.toThrow("stop-before-download");
await expect(
api.getStaticEvidence("owner/rea", runRecord, { name: "Static checks" }),
).rejects.toThrow("stop-before-download");
expect(
await api.getStaticEvidence("owner/rea", runRecord, {
name: "Test shard (1)",
}),
).toBeNull();
expect(downloads).toEqual([
"https://api.github.com/repos/owner/rea/actions/artifacts/2/zip",
"https://api.github.com/repos/owner/rea/actions/artifacts/1/zip",
]);
});
it("rejects artifact redirects that could send the bearer token to a non-HTTPS host", async () => {
+58 -3
View File
@@ -201,8 +201,41 @@ it("starts source feedback before build and requires both groups without waiving
expect(
jobs.static?.steps?.find(
(step) => step.run === "npm run ci:static-context",
)?.["continue-on-error"],
).toBe(true);
),
).toMatchObject({
"continue-on-error": true,
env: { CI_STATIC_CONTEXT_SCRIPT: "check:ci:fast:tasks" },
});
expect(
jobs["static-deep"]?.steps?.find(
(step) => step.run === "npm run ci:static-context",
),
).toMatchObject({
"continue-on-error": true,
env: { CI_STATIC_CONTEXT_SCRIPT: "check:ci:deep:tasks" },
});
const deepUpload = jobs["static-deep"]?.steps?.find(
(step) => step.with?.name === "static-deep-check-reports",
);
expect(deepUpload?.if).toBe("always()");
expect(deepUpload?.with).toMatchObject({
path: ".cache/static-checks/",
"if-no-files-found": "ignore",
"retention-days": 7,
});
const deepSteps = jobs["static-deep"]?.steps ?? [];
const contextIndex = deepSteps.findIndex(
(step) => step.run === "npm run ci:static-context",
);
const checkIndex = deepSteps.findIndex(
(step) => step.run === "npm run check:ci:deep",
);
const uploadIndex = deepSteps.findIndex(
(step) => step.with?.name === "static-deep-check-reports",
);
expect(contextIndex).toBeGreaterThanOrEqual(0);
expect(checkIndex).toBeGreaterThan(contextIndex);
expect(uploadIndex).toBeGreaterThan(checkIndex);
expect(jobs["test-source"]?.needs).toBe("changes");
expect(jobs["test-shard"]?.needs).toEqual(["changes", "build"]);
expect(
@@ -268,7 +301,29 @@ it("records the actual static task graph with explicit source identity and no am
const head = await execute("git", ["rev-parse", "HEAD"]);
expect(context.sourceSha).toBe(head.stdout.trim());
expect(context.taskHashes["lint:built"]).toMatch(/^[a-f0-9]{16}$/u);
expect(context.taskHashes.jscpd).toMatch(/^[a-f0-9]{16}$/u);
expect(context.taskHashes.typecheck).toMatch(/^[a-f0-9]{16}$/u);
expect(context.taskHashes["format:check"]).toMatch(/^[a-f0-9]{16}$/u);
expect(context.taskHashes).not.toHaveProperty("jscpd");
expect(context.taskHashes).not.toHaveProperty("knip");
const deep = await recordStaticTaskContext({
outputDirectory: `${directory}/deep`,
script: "check:ci:deep:tasks",
});
expect(deep.taskHashes.jscpd).toMatch(/^[a-f0-9]{16}$/u);
expect(deep.taskHashes.knip).toMatch(/^[a-f0-9]{16}$/u);
expect(deep.taskHashes).not.toHaveProperty("lint:built");
await expect(
recordStaticTaskContext({
outputDirectory: directory,
script: "check:ci",
}),
).rejects.toThrow("Static context script is not a known check");
await expect(
recordStaticTaskContext({
outputDirectory: directory,
script: "check:ci:fast",
}),
).rejects.toThrow("Static context script is not a known check");
expect(context).not.toHaveProperty("environmentVariables");
expect(context).not.toHaveProperty("env");
expect(
+18 -15
View File
@@ -12,21 +12,23 @@ async function workflow(name: string) {
concurrency: z.object({ group: z.string() }).optional(),
jobs: z.record(
z.string(),
z.object({
name: z.string().optional(),
if: z.string().optional(),
uses: z.string().optional(),
with: z.record(z.string(), z.string()).optional(),
steps: z
.array(
z.object({
run: z.string().optional(),
uses: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
}),
)
.optional(),
}),
z
.object({
name: z.string().optional(),
if: z.string().optional(),
uses: z.string().optional(),
with: z.record(z.string(), z.string()).optional(),
steps: z
.array(
z.object({
run: z.string().optional(),
uses: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
}),
)
.optional(),
})
.passthrough(),
),
})
.parse(
@@ -92,6 +94,7 @@ it("qualifies only ci:full label additions at the caller's merge revision", asyn
with: { source_sha: "${{ github.sha }}" },
},
]);
expect(full.jobs.qualification).not.toHaveProperty("secrets");
// Metadata events must not create skipped replacements for the main CI checks.
expect(full.jobs.required).toBeUndefined();
expect(full.concurrency).toBeUndefined();
+114 -42
View File
@@ -156,6 +156,7 @@ it("holds native and provider fan-out behind static validation while portable fe
const earlyJobs = new Set([
"changes",
"static",
"static-deep",
"build",
"test-shard",
"test-source",
@@ -171,8 +172,112 @@ it("holds native and provider fan-out behind static validation while portable fe
expect(job.if).not.toMatch(/always\(|cancelled\(/u);
}
expect(jobs["test-shard"]?.needs).not.toContain("static");
const stepRuns = z.array(z.object({ run: z.string().optional() }));
expect(jobs["static-deep"]?.needs).toBe("changes");
expect(String(jobs["static-deep"]?.if)).toContain("tier == 'full'");
expect(String(jobs["static-deep"]?.if)).toContain("code == 'true'");
const staticRuns = stepRuns.parse(jobs.static?.steps);
const deepRuns = stepRuns.parse(jobs["static-deep"]?.steps);
expect(staticRuns.some((step) => step.run === "npm run check:ci:fast")).toBe(
true,
);
expect(
staticRuns.some(
(step) =>
step.run === "npm run check:ci:deep" ||
step.run === "npm run test:python",
),
).toBe(false);
expect(deepRuns.some((step) => step.run === "npm run check:ci:deep")).toBe(
true,
);
for (const [name, job] of Object.entries(jobs)) {
if (
name === "required" ||
name === "static-deep" ||
job.needs === undefined
)
continue;
expect(job.needs, `${name} must not wait for deep static`).not.toContain(
"static-deep",
);
}
});
const trustedRemoteCacheWrite =
"${{ inputs.remote_cache_write == true || (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')) }}";
async function assertProviderCacheCallers(
jobs: Awaited<ReturnType<typeof workflow>>["jobs"],
consumers: readonly string[],
) {
for (const name of consumers) {
const caller = jobs[name];
expect(caller?.needs).toContain("build");
expect(caller?.secrets).toEqual({
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}",
});
expect(caller?.with).toEqual({
build_cache_artifact: "rea-build-cache",
remote_cache_write: trustedRemoteCacheWrite,
});
const path = z.string().parse(caller?.uses);
const called = z
.object({
on: z.object({
workflow_call: z.object({
inputs: z.record(z.string(), z.unknown()),
secrets: z.object({
TURBO_TOKEN: z.object({ required: z.literal(false) }),
}),
}),
workflow_dispatch: z.unknown(),
}),
jobs: z.record(
z.string(),
z.object({
steps: z.array(
z.object({
uses: z.string().optional(),
with: z.record(z.string(), z.unknown()).optional(),
}),
),
}),
),
})
.parse(
parse(
await readFile(new URL(`../../../${path}`, import.meta.url), "utf8"),
),
);
expect(called.on.workflow_call.inputs.build_cache_artifact).toMatchObject({
type: "string",
default: "",
});
expect(called.on.workflow_call.inputs.remote_cache_write).toMatchObject({
type: "boolean",
required: false,
default: false,
});
expect(Object.hasOwn(called.on, "workflow_dispatch")).toBe(true);
for (const job of Object.values(called.jobs))
expect(
job.steps.find(
(step) => step.uses === "./.github/actions/portable-build-cache",
)?.with,
).toEqual({
build_cache_artifact: "${{ inputs.build_cache_artifact }}",
});
}
for (const name of [
"sqlite-runtime",
"nativeaot",
"ghidra_bridge",
"website",
])
expect(jobs[name]?.secrets, name).toBeUndefined();
}
it("binds provider build reuse to the current run while keeping standalone dispatches independent", async () => {
const { jobs } = await workflow();
const buildSteps = z
@@ -236,48 +341,7 @@ it("binds provider build reuse to the current run while keeping standalone dispa
),
);
expect(jobs["test-shard"]?.if).toBe("needs.changes.outputs.code == 'true'");
for (const name of consumers) {
const caller = jobs[name];
expect(caller?.needs).toContain("build");
expect(caller?.with).toEqual({ build_cache_artifact: "rea-build-cache" });
const path = z.string().parse(caller?.uses);
const called = z
.object({
on: z.object({
workflow_call: z.object({
inputs: z.record(z.string(), z.unknown()),
}),
workflow_dispatch: z.unknown(),
}),
jobs: z.record(
z.string(),
z.object({
steps: z.array(
z.object({
uses: z.string().optional(),
with: z.record(z.string(), z.unknown()).optional(),
}),
),
}),
),
})
.parse(
parse(
await readFile(new URL(`../../../${path}`, import.meta.url), "utf8"),
),
);
expect(called.on.workflow_call.inputs.build_cache_artifact).toMatchObject({
type: "string",
default: "",
});
expect(Object.hasOwn(called.on, "workflow_dispatch")).toBe(true);
for (const job of Object.values(called.jobs))
expect(
job.steps.find(
(step) => step.uses === "./.github/actions/portable-build-cache",
)?.with,
).toEqual({ build_cache_artifact: "${{ inputs.build_cache_artifact }}" });
}
await assertProviderCacheCallers(jobs, consumers);
const action = z
.object({
runs: z.object({
@@ -369,6 +433,14 @@ it("keeps fast feedback separate from complete qualification and release proof",
required: z.literal(true),
type: z.literal("string"),
}),
remote_cache_write: z.object({
required: z.literal(false),
type: z.literal("boolean"),
default: z.literal(false),
}),
}),
secrets: z.object({
TURBO_TOKEN: z.object({ required: z.literal(false) }),
}),
}),
}),
@@ -61,6 +61,10 @@ const workflow = async () =>
build: z.object({ steps: stepsSchema }),
docs: z.object({ steps: stepsSchema }),
static: z.object({ steps: stepsSchema }),
"static-deep": z.object({
if: z.string(),
steps: stepsSchema,
}),
}),
})
.parse(
@@ -327,10 +331,26 @@ it("runs the existing onboarding and Python producer guards from their selected
expect(scripts["docs:check"]).toContain(
"tests/conformance/readmeConsistency.test.ts",
);
const producer = jobs.static.steps.find(
(step) => step.run === "npm run test:python",
expect(
jobs.static.steps.some((step) => step.run === "npm run test:python"),
).toBe(false);
expect(
jobs.static.steps.some((step) => step.run === "npm run check:ci:fast"),
).toBe(true);
const deep = jobs["static-deep"];
expect(deep?.if).toBe(
"needs.changes.outputs.tier == 'full' && needs.changes.outputs.code == 'true'",
);
expect(deep?.steps.some((step) => step.run === "npm run check:ci:deep")).toBe(
true,
);
expect(scripts["check:ci:deep"]).toContain("npm run test:python");
expect(scripts["check:ci:fast"]).toBe(
"node scripts/run-exclusive.mjs artifacts npm run check:ci:fast:tasks",
);
expect(scripts["check:ci:fast:tasks"]).toBe(
"turbo run typecheck lint:built format:check",
);
expect(producer?.if).toBe("needs.changes.outputs.code == 'true'");
expect(scripts["test:python"]).toContain(
"bridge/native/test_rea_lldb_tracer.py",
);
@@ -8,8 +8,10 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
allocateByDuration,
estimatedShardCost,
parseTestDurations,
readTestDurations,
weightFileDurations,
} from "../../ciTestSequencer.js";
describe("CI test history", () => {
@@ -85,6 +87,27 @@ describe("CI test history", () => {
});
describe("duration shard allocation", () => {
it("charges serial process-boundary files their full duration and other projects one worker share", () => {
expect(estimatedShardCost("process-boundary", 400)).toBe(400);
expect(estimatedShardCost("domain", 400)).toBe(200);
expect(estimatedShardCost("adapters", 400)).toBe(200);
});
it("fills unknown files from the raw median before serial and shared weighting", () => {
expect(
weightFileDurations([
{ project: "domain", duration: 400 },
{ project: "process-boundary", duration: undefined },
{ project: "adapters", duration: undefined },
]),
).toEqual([200, 400, 200]);
expect(
weightFileDurations([
{ project: "process-boundary", duration: undefined },
]),
).toEqual([undefined]);
});
it.prop([
fc.uniqueArray(fc.integer({ min: 0, max: 10_000 }), {
minLength: 1,
@@ -41,7 +41,8 @@ async function readReleaseWorkflow() {
if: z.string(),
needs: z.array(z.string()),
uses: z.string(),
with: z.record(z.string(), z.string()),
secrets: z.record(z.string(), z.string()),
with: z.record(z.string(), z.union([z.string(), z.boolean()])),
}),
publish: jobSchema,
rehearsal: jobSchema.extend({
@@ -288,9 +289,13 @@ it("binds npm and MCP publication to the same immutable release SHA", async () =
"needs.release-please.outputs.release_created",
);
expect(workflow.jobs.qualification.uses).toBe("./.github/workflows/ci.yml");
expect(workflow.jobs.qualification.secrets).toEqual({
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}",
});
expect(workflow.jobs.qualification.with).toEqual({
source_sha:
"${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}",
remote_cache_write: true,
});
expect(workflow.jobs.publish.needs).toContain("qualification");
expect(workflow.jobs.publish.needs).toContain("rehearsal");
+35 -1
View File
@@ -62,6 +62,33 @@ export async function readTestDurations(
type TimedItem<T> = { item: T; key: string; duration: number | undefined };
// process-boundary runs its files one at a time. Other projects overlap up to
// the runner worker cap, so equal file-duration sums are not equal wall time.
const PARALLEL_TEST_WORKERS = 2;
export function estimatedShardCost(
project: string,
durationMs: number,
): number {
if (project === "process-boundary") return durationMs;
return durationMs / PARALLEL_TEST_WORKERS;
}
/** Weight raw durations, filling gaps from the raw median before worker sharing. */
export function weightFileDurations(
entries: readonly { project: string; duration: number | undefined }[],
): (number | undefined)[] {
const known = entries
.flatMap(({ duration }) => (duration === undefined ? [] : [duration]))
.sort((left, right) => left - right);
if (!known.length) return entries.map(() => undefined);
const lower = known[Math.floor((known.length - 1) / 2)]!;
const median = lower + (known[Math.floor(known.length / 2)]! - lower) / 2;
return entries.map(({ project, duration }) =>
estimatedShardCost(project, duration ?? median),
);
}
/** Distribute the longest files first, retaining every discovered specification. */
export function allocateByDuration<T>(
items: readonly TimedItem<T>[],
@@ -105,16 +132,23 @@ export class CiTestSequencer extends BaseSequencer {
const durations = await readTestDurations(historyPath, root);
if (!durations) return super.shard(files);
const weighted = files.map((item) => {
const timed = files.map((item) => {
const path = relative(root, resolve(root, item.moduleId))
.split(sep)
.join("/");
return {
item,
key: path + "\0" + item.project.name + "\0" + item.pool,
project: item.project.name,
duration: durations.get(path),
};
});
const costs = weightFileDurations(timed);
const weighted = timed.map((entry, index) => ({
item: entry.item,
key: entry.key,
duration: costs[index],
}));
const shards = allocateByDuration(weighted, shard.count);
return shards ? shards[shard.index - 1]! : super.shard(files);
}