Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 57 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
branches: [main]
pull_request:
branches: [main]
# Release acceptance must be able to exercise the complete current tree.
workflow_dispatch:

permissions: {}

Expand Down Expand Up @@ -192,7 +194,7 @@ jobs:
needs:
- early-gates
- scope
if: always() && needs.early-gates.result == 'success' && needs.scope.result == 'success'
if: always() && !cancelled() && needs.early-gates.result == 'success' && needs.scope.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
Expand Down Expand Up @@ -434,7 +436,7 @@ jobs:

mutation-tests:
name: Mutation / ${{ matrix.target }}
if: needs.prepare.outputs.mutation-targets != '[]'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.mutation-targets != '[]'
needs: prepare
runs-on: ubuntu-latest
# The governed air-gap codec target currently instruments 352 mutants and
Expand All @@ -443,7 +445,7 @@ jobs:
permissions:
contents: read
strategy:
fail-fast: true
fail-fast: false
max-parallel: 6
matrix:
target: ${{ fromJSON(needs.prepare.outputs.mutation-targets) }}
Expand Down Expand Up @@ -496,7 +498,7 @@ jobs:

standard-tests:
name: Tests / non-coverage packages
if: needs.prepare.outputs.standard-packages != '[]'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.standard-packages != '[]'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 25
Expand Down Expand Up @@ -539,14 +541,14 @@ jobs:

dependent-tests:
name: Tests / affected dependents (${{ matrix.shard }}/${{ matrix.total }})
if: needs.prepare.outputs.dependent-test-packages != '[]'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.dependent-test-packages != '[]'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: true
fail-fast: false
matrix: ${{ fromJSON(needs.prepare.outputs.dependent-test-matrix) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -596,21 +598,21 @@ jobs:
filters+=(--filter "$package")
done
if [ "${#filters[@]}" -gt 0 ]; then
pnpm -r --no-sort "${filters[@]}" exec node \
pnpm -r --workspace-concurrency=1 --no-sort "${filters[@]}" exec node \
"$GITHUB_WORKSPACE/scripts/run-prebuilt-package-script.mjs" \
--script test
fi

browser-packages:
name: Platform / browser packages (${{ matrix.shard }}/${{ matrix.total }})
if: needs.prepare.outputs.browser-packages != '[]'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.browser-packages != '[]'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
contents: read
strategy:
fail-fast: true
fail-fast: false
matrix: ${{ fromJSON(needs.prepare.outputs.browser-matrix) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -667,7 +669,7 @@ jobs:

wallet-browser-platform:
name: Platform / wallet browser
if: needs.prepare.outputs.wallet_client == 'true'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.wallet_client == 'true'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down Expand Up @@ -701,7 +703,7 @@ jobs:

wallet-mobile-platform:
name: Platform / wallet mobile
if: needs.prepare.outputs.wallet_mobile == 'true'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.wallet_mobile == 'true'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down Expand Up @@ -740,7 +742,7 @@ jobs:

coverage-sdk:
name: Coverage / SDK
if: needs.prepare.outputs.sdk == 'true'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.sdk == 'true'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down Expand Up @@ -791,7 +793,7 @@ jobs:

coverage-did:
name: Coverage / DID
if: needs.prepare.outputs.did == 'true'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.did == 'true'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 20
Expand Down Expand Up @@ -826,14 +828,14 @@ jobs:

coverage-wallet:
name: Coverage / wallet-toolbox (${{ matrix.shard }}/4)
if: needs.prepare.outputs.wallet == 'true'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.wallet == 'true'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 40
permissions:
contents: read
strategy:
fail-fast: true
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
Expand Down Expand Up @@ -875,7 +877,7 @@ jobs:

coverage-wallet-monitor:
name: Coverage / wallet-toolbox monitor
if: needs.prepare.outputs.wallet == 'true'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.wallet == 'true'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 25
Expand Down Expand Up @@ -920,7 +922,7 @@ jobs:

coverage-verifast:
name: Coverage / VeriFast
if: needs.prepare.outputs.verifast == 'true'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.verifast == 'true'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down Expand Up @@ -972,14 +974,14 @@ jobs:

coverage-other:
name: Coverage / other affected packages (${{ matrix.shard }}/${{ matrix.total }})
if: needs.prepare.outputs.coverage-other-packages != '[]'
if: always() && !cancelled() && needs.prepare.result == 'success' && needs.prepare.outputs.coverage-other-packages != '[]'
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: true
fail-fast: false
matrix: ${{ fromJSON(needs.prepare.outputs.coverage-other-matrix) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -1025,7 +1027,7 @@ jobs:
"import { MongoMemoryServer } from 'mongodb-memory-server'; const s = await MongoMemoryServer.create({ instance: { launchTimeout: 120000 } }); await s.stop(); console.log('mongodb-memory-server binary cache warmed');"
fi
if [ "${#filters[@]}" -gt 0 ]; then
pnpm -r --no-sort "${filters[@]}" exec node \
pnpm -r --workspace-concurrency=1 --no-sort "${filters[@]}" exec node \
"$GITHUB_WORKSPACE/scripts/run-prebuilt-package-script.mjs" \
--script test:coverage
else
Expand Down Expand Up @@ -1058,7 +1060,7 @@ jobs:
coverage-upload:
name: Coverage / aggregate upload
if: >-
always() &&
always() && !cancelled() &&
needs.prepare.result == 'success' &&
needs.prepare.outputs.coverage-required == 'true' &&
(needs.coverage-other.result == 'success' || needs.coverage-other.result == 'skipped') &&
Expand Down Expand Up @@ -1213,7 +1215,7 @@ jobs:
needs:
- early-gates
- scope
if: always() && needs.early-gates.result == 'success' && needs.scope.result == 'success'
if: always() && !cancelled() && needs.early-gates.result == 'success' && needs.scope.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
Expand All @@ -1229,14 +1231,14 @@ jobs:

infra:
name: Infra / ${{ matrix.display }}
if: needs.infra-scope.outputs.has-infra == 'true'
if: always() && !cancelled() && needs.infra-scope.result == 'success' && needs.infra-scope.outputs.has-infra == 'true'
needs: infra-scope
runs-on: ubuntu-latest
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: true
fail-fast: false
matrix: ${{ fromJson(needs.infra-scope.outputs.matrix) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -1320,7 +1322,7 @@ jobs:
# Run on every PR (cheap and protects the docs site) and on pushes that touch docs
# The build step inside will fail fast on frontmatter or link problems before they reach production
if: >-
always() &&
always() && !cancelled() &&
needs.early-gates.result == 'success' &&
needs.scope.result == 'success' &&
needs.scope.outputs.docs == 'true'
Expand Down Expand Up @@ -1366,7 +1368,7 @@ jobs:
- early-gates
- scope
if: >-
always() &&
always() && !cancelled() &&
needs.early-gates.result == 'success' &&
needs.scope.result == 'success' &&
needs.scope.outputs.conformance == 'true'
Expand Down Expand Up @@ -1434,6 +1436,22 @@ jobs:
name: merge-gate
if: always()
needs:
- scope
- prepare
- infra-scope
- standard-tests
- dependent-tests
- browser-packages
- wallet-browser-platform
- wallet-mobile-platform
- coverage-sdk
- coverage-did
- coverage-wallet
- coverage-wallet-monitor
- coverage-verifast
- coverage-other
- coverage-upload
- mutation-tests
- repository-health
- sonar-zero-findings
- build-and-test
Expand All @@ -1444,8 +1462,20 @@ jobs:
- dependency-review
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
- name: Reject missing or skipped selected work
env:
CI_NEEDS: ${{ toJSON(needs) }}
CI_EVENT: ${{ github.event_name }}
run: node scripts/ci-result-gate.mjs
- name: Verify every required CI result
env:
HEALTH_RESULT: ${{ needs.repository-health.result }}
Expand Down
35 changes: 26 additions & 9 deletions docs/reference/ci-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
id: ci-performance
title: 'CI Performance Governance'
kind: reference
version: '1.1.0'
last_updated: '2026-07-31'
last_verified: '2026-08-30'
version: '1.2.0'
last_updated: '2026-09-23'
last_verified: '2026-09-23'
review_cadence_days: 30
status: stable
tags: [reference, ci, performance, github-actions]
Expand All @@ -31,23 +31,25 @@ prerequisite. This preserves behavioral coverage of possible consumers without
paying to regenerate unchanged packages' coverage reports.
A lockfile-only change selects the importers whose lock snapshots actually
changed instead of treating the root lockfile as a global invalidation. Root
compiler and workspace controls still select the complete graph deliberately.
compiler, workspace and shared CI execution controls select the complete graph deliberately.

Mutation selection follows each target's exact implementation, property,
regression, configuration, and policy inputs. Package-wide mutation suites
expand only where their configuration really covers the whole package. Image
jobs follow changed build contexts: a CI-workflow-only change selects no
application image, while shared image/runtime contract inputs deliberately fan
out to the registered consumers.
jobs follow changed build contexts. Changes to the shared CI workflow validate
all infrastructure build lanes; unrelated documentation workflow changes do not.
Shared image/runtime contract inputs fan out to the registered consumers.

The main CI workflow builds the selected graph once and shares immutable
outputs with isolated test lanes, skips empty lanes, installs through the
setup-node pnpm cache, caches the immutable MongoDB test binary, and rebuilds
native/build tools only in jobs that execute them. Browser lanes retain exact
package-composition reports without rebuilding the workspace. The cheap
repository-health, scope, Sonar, and dependency-review gates complete before
dependency installation, and all expensive matrices cancel unfinished siblings
after the first failure.
dependency installation. Matrix siblings finish after a failure so acceptance
evidence includes every selected shard. Within a regression or coverage shard,
packages execute serially because individual test runners already use worker
pools; this prevents nested pools starving real-cryptography integration tests.
These controls reduce repeated CPU, network, and setup work without weakening
the tests selected by the dependency or registered trust-boundary graph.

Expand All @@ -71,3 +73,18 @@ GITHUB_TOKEN=... node scripts/ci-performance.mjs \
Review the 40 exact run links, classification threshold, sample summaries,
workflow or runner changes, and the stated median/p95 budget. Never loosen a
budget solely to make a red trend green.

## Complete release acceptance

Dispatch `CI` manually on the reviewed main commit to select the entire governed
workspace, all infrastructure build lanes and mutation targets. This deliberately
uses the workflow's all-scope path instead of comparing only the latest commit.
Source merges still validate their affected scope automatically.

Every execution lane uses an explicit successful-preparation condition that
survives intentionally skipped PR-only gates on a main push and honors explicit
workflow cancellation so obsolete runs cannot hold the concurrency slot. The final result
gate independently checks scope outputs against each job result: selected jobs
must succeed; missing, cancelled or skipped selected jobs fail the merge gate.
Only genuinely unselected lanes and main's PR-only checks may be skipped.
Do not infer full release acceptance from a green documentation-only push.
Loading
Loading