ci: reserve release-grade testing for milestones (#1228)

This commit is contained in:
Brad Groux 2026-08-24 00:34:36 -05:00 committed by GitHub
parent 415f4077b9
commit 49e25838e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 204 additions and 408 deletions

View file

@ -28,7 +28,7 @@ Describe the tests you ran to verify your changes. Provide instructions so revie
**Verification tier:**
- [ ] Documentation or static checks only
- [ ] Focused changed-package tests
- [ ] Explicit focused diagnostic (manual workflow dispatch)
- [ ] Full milestone gate (`ci:full`, critical security, integration, or release)
**Why this tier is sufficient:** Explain the changed behavior, covered failure
@ -46,7 +46,7 @@ modes, and why broader gates are or are not required.
- [ ] Separable follow-up work is linked instead of folded into this PR
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added or updated coverage for the next declared test milestone
- [ ] I have updated the documentation accordingly
- [ ] My changes generate no new warnings
- [ ] Any breaking changes have been documented in the PR description

View file

@ -13,7 +13,7 @@ on:
test_scope:
description: Unit-test verification tier
required: true
default: focused
default: full
type: choice
options:
- focused
@ -24,10 +24,7 @@ on:
type: string
permissions:
actions: read
contents: read
checks: read
pull-requests: read
concurrency:
group: >-
@ -81,7 +78,6 @@ jobs:
scripts/check-security-gates.test.mjs
scripts/check-tracked-ignore.test.mjs
scripts/select-ci-test-scope.test.mjs
scripts/verify-full-suite-job-evidence.test.mjs
- name: Guard delivery cadence
run: node scripts/check-delivery-cadence.mjs
@ -95,126 +91,6 @@ jobs:
- name: Reject tracked files covered by ignore rules
run: node scripts/check-tracked-ignore.mjs
- name: Find reviewed full-suite evidence
id: reviewed_full
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
reviewed_full=false
reviewed_pr=
reviewed_head=
reviewed_mode=
associated_prs="$(
gh api \
-H 'Accept: application/vnd.github+json' \
"repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" \
2>/dev/null ||
printf '[]'
)"
merged_pr="$(
jq -c \
--arg merge_sha "$GITHUB_SHA" \
'[.[] | select(
.merged_at != null and
.base.ref == "main" and
.merge_commit_sha == $merge_sha
)] | first // empty' \
<<<"$associated_prs"
)"
if [[ -n "$merged_pr" ]]; then
reviewed_pr="$(jq -r '.number' <<<"$merged_pr")"
reviewed_head="$(jq -r '.head.sha' <<<"$merged_pr")"
check_runs="$(
gh api \
-H 'Accept: application/vnd.github+json' \
"repos/${GITHUB_REPOSITORY}/commits/${reviewed_head}/check-runs?per_page=100" \
2>/dev/null ||
printf '{"check_runs":[]}'
)"
full_suite_check_id="$(
jq -r '
[
.check_runs[] |
select(
.name == "Workspace Unit Tests" and
.status == "completed" and
.conclusion == "success" and
.app.slug == "github-actions"
)
] |
sort_by(.completed_at) |
last |
.id // empty
' <<<"$check_runs"
)"
full_suite_job="$(
if [[ -n "$full_suite_check_id" ]]; then
gh api \
-H 'Accept: application/vnd.github+json' \
"repos/${GITHUB_REPOSITORY}/actions/jobs/${full_suite_check_id}" \
2>/dev/null ||
printf '{}'
else
printf '{}'
fi
)"
coverage_check_id="$(
jq -r '
[
.check_runs[] |
select(
.name == "Critical Path Coverage" and
.status == "completed" and
.conclusion == "success" and
.app.slug == "github-actions"
)
] |
sort_by(.completed_at) |
last |
.id // empty
' <<<"$check_runs"
)"
coverage_job="$(
if [[ -n "$coverage_check_id" ]]; then
gh api \
-H 'Accept: application/vnd.github+json' \
"repos/${GITHUB_REPOSITORY}/actions/jobs/${coverage_check_id}" \
2>/dev/null ||
printf '{}'
else
printf '{}'
fi
)"
if node scripts/verify-full-suite-job-evidence.mjs <<<"$full_suite_job" &&
node scripts/verify-full-suite-job-evidence.mjs coverage <<<"$coverage_job"; then
if git cat-file -e "${reviewed_head}^{commit}" 2>/dev/null ||
git fetch --no-tags origin "$reviewed_head"; then
if git merge-base --is-ancestor "$reviewed_head" "$GITHUB_SHA"; then
reviewed_full=true
reviewed_mode=ancestor
elif [[ "$(git rev-parse "${reviewed_head}^{tree}")" == \
"$(git rev-parse "${GITHUB_SHA}^{tree}")" ]]; then
reviewed_full=true
reviewed_mode=identical-tree
fi
fi
fi
fi
{
echo "reviewed_full=$reviewed_full"
echo "reviewed_pr=$reviewed_pr"
echo "reviewed_head=$reviewed_head"
echo "reviewed_mode=$reviewed_mode"
} >> "$GITHUB_OUTPUT"
- name: Select verification tier
id: scope
shell: bash
@ -222,9 +98,6 @@ jobs:
CI_EVENT_NAME: ${{ github.event_name }}
CI_MANUAL_SCOPE: ${{ inputs.test_scope || '' }}
CI_PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
CI_REVIEWED_FULL: ${{ steps.reviewed_full.outputs.reviewed_full || 'false' }}
CI_REVIEWED_PR: ${{ steps.reviewed_full.outputs.reviewed_pr || '' }}
CI_REVIEWED_MODE: ${{ steps.reviewed_full.outputs.reviewed_mode || '' }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha || '' }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || '' }}
PUSH_BEFORE_SHA: ${{ github.event.before || '' }}

View file

@ -1,31 +1,9 @@
name: Desktop Artifacts
on:
push:
branches: [main]
paths:
- 'desktop/**'
- 'server/**'
- 'web/**'
- 'shared/**'
- 'docs/DESKTOP-RELEASE.md'
- 'scripts/desktop-after-pack.mjs'
- 'scripts/prepare-desktop-release.mjs'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/workflows/desktop-artifacts.yml'
pull_request:
branches: [main]
paths:
- 'desktop/**'
- 'docs/DESKTOP-RELEASE.md'
- 'scripts/desktop-after-pack.mjs'
- 'scripts/prepare-desktop-release.mjs'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/workflows/desktop-artifacts.yml'
types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_dispatch:
concurrency:
@ -38,8 +16,20 @@ env:
jobs:
mac-unsigned:
name: Unsigned macOS Artifact
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.pull_request.labels.*.name, 'ci:full')
runs-on: macos-15
steps:
- name: Record milestone selection
run: |
{
echo "### Unsigned macOS artifact milestone"
echo
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
echo "- Reason: explicit \`ci:full\` or manual milestone"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
@ -78,8 +68,20 @@ jobs:
linux-unsigned:
name: Unsigned Linux Artifacts
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.pull_request.labels.*.name, 'ci:full')
runs-on: ubuntu-24.04
steps:
- name: Record milestone selection
run: |
{
echo "### Unsigned Linux artifact milestone"
echo
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
echo "- Reason: explicit \`ci:full\` or manual milestone"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
@ -122,8 +124,21 @@ jobs:
windows-unsigned:
name: Unsigned Windows Artifacts
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.pull_request.labels.*.name, 'ci:full')
runs-on: windows-2025
steps:
- name: Record milestone selection
shell: bash
run: |
{
echo "### Unsigned Windows artifact milestone"
echo
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
echo "- Reason: explicit \`ci:full\` or manual milestone"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10

View file

@ -1,32 +1,9 @@
name: Docker Image Contract
on:
push:
branches: [main]
paths:
- Dockerfile
- .dockerignore
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- server/**
- shared/**
- web/**
- scripts/check-docker-image.mjs
- .github/workflows/docker-image.yml
pull_request:
branches: [main]
paths:
- Dockerfile
- .dockerignore
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- server/**
- shared/**
- web/**
- scripts/check-docker-image.mjs
- .github/workflows/docker-image.yml
types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_dispatch:
permissions:
@ -39,8 +16,20 @@ concurrency:
jobs:
contract:
name: Build, Size, and Runtime Contract
if: >-
github.event_name == 'workflow_dispatch' ||
contains(github.event.pull_request.labels.*.name, 'ci:full')
runs-on: ubuntu-latest
steps:
- name: Record milestone selection
run: |
{
echo "### Docker image milestone"
echo
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
echo "- Reason: explicit \`ci:full\` or manual milestone"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build production image

View file

@ -1,6 +1,9 @@
name: Scheduled QA
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled, unlabeled]
schedule:
- cron: '17 8 * * 1'
workflow_dispatch:
@ -36,9 +39,21 @@ env:
jobs:
playwright:
name: Playwright E2E
if: >-
github.event_name != 'pull_request' ||
contains(github.event.pull_request.labels.*.name, 'ci:full')
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Record milestone selection
run: |
{
echo "### Playwright E2E milestone"
echo
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
echo "- Reason: explicit \`ci:full\`, scheduled, or manual milestone"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
@ -81,11 +96,24 @@ jobs:
k6:
name: k6 Load Smoke
if: >-
github.event_name != 'pull_request' ||
contains(github.event.pull_request.labels.*.name, 'ci:full')
runs-on: ubuntu-latest
timeout-minutes: 30
env:
K6_PROFILE: ${{ github.event_name == 'workflow_dispatch' && inputs.load_profile || 'smoke' }}
steps:
- name: Record milestone selection
run: |
{
echo "### k6 milestone"
echo
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
echo "- Profile: \`$K6_PROFILE\`"
echo "- Reason: explicit \`ci:full\`, scheduled, or manual milestone"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10

View file

@ -126,9 +126,11 @@ Do not run `npm install`, `yarn`, or `bun install`. If lockfile conflicts arise,
- At the 45-minute delivery checkpoint, if the issue is not pull-request ready, stop adding scope
and report the concrete cause. Split independent remaining work into linked issues, or continue
only when the next step is required to preserve correctness of the current behavior.
- During implementation, run the narrowest useful loop: type-check touched packages, lint changed
files, and run focused tests for changed behavior and high-risk edges.
- Run focused Vitest slices with
- During ordinary implementation, use source inspection, changed-file formatting/linting, and
touched-package type checking. Do not run workspace unit, coverage, E2E, desktop packaging, or
Docker contract tests between implementation PRs.
- When a maintainer explicitly declares a focused diagnostic milestone, run the exact Vitest slice
once with
`pnpm --filter <package> exec vitest run <exact-test-files>`. Do not use
`pnpm --filter <package> test -- <test-files>` or
`pnpm --filter <package> test -- --run <test-files>`; package wrappers can ignore that file
@ -137,9 +139,11 @@ Do not run `npm install`, `yarn`, or `bun install`. If lockfile conflicts arise,
Rerun only the checks affected by the later change.
- Use the complete workspace suite once at an explicit integration, critical-security, or release
milestone. Pull-request label `ci:full`, scheduled CI, and manual full dispatch are the
authoritative broad gates.
- Trust `scripts/select-ci-test-scope.mjs` and the `Select Test Scope` job to choose the required
CI tier. Do not add broader local gates merely to duplicate CI.
authoritative broad gates. Critical coverage, unsigned desktop artifacts, and the Docker image
contract run only at those milestones.
- Trust `scripts/select-ci-test-scope.mjs` and the `Select Test Scope` job to record the required
CI tier. Ordinary pull requests and `main` pushes select no workspace tests. Do not add local
test gates merely to duplicate a future milestone.
- Do not wait for optional desktop packaging, artifact previews, or release workflows when the
change does not touch their product boundary. They are evidence only when declared relevant.
- Add enough regression coverage to prove the behavior and its meaningful failure modes. Test

View file

@ -73,16 +73,17 @@ veritas-kanban/
2. Make your changes — write code, add tests, update docs.
3. Run touched-package type checking, changed-file linting, and focused tests
before committing:
3. Run touched-package type checking and changed-file linting before
committing. Ordinary implementation pull requests do not run workspace
tests:
```bash
pnpm --filter @veritas-kanban/server typecheck
pnpm exec eslint server/src/path/to/changed.ts
pnpm --filter @veritas-kanban/server exec vitest run src/path/to/changed.test.ts
```
Use direct `exec vitest run` invocation for exact-file slices. Do not use
At an explicitly declared focused diagnostic milestone, use direct
`exec vitest run` invocation for one exact-file slice. Do not use
`pnpm --filter <package> test -- <test-files>` or
`pnpm --filter <package> test -- --run <test-files>` as a focused command.
Package wrappers can ignore that file boundary and expand into the entire
@ -91,7 +92,8 @@ veritas-kanban/
Build `@veritas-kanban/shared` first and type-check its known consumers when
a shared contract changes. Use `pnpm test` at an explicit integration,
critical-security, or release milestone, or when a maintainer explicitly
selects the `ci:full` gate.
selects the `ci:full` gate. Critical coverage, E2E, desktop packaging, and
Docker contracts follow the same milestone boundary.
4. Commit using [conventional commits](#commit-conventions).
@ -109,8 +111,9 @@ This cadence extends the deterministic CI selector delivered in
verification effort becomes larger than the changed behavior.
- Do not rerun an unchanged passing check after documentation, comments, or
formatting-only edits.
- Treat `Select Test Scope` as the CI authority. Focused, full, and no-test
selections are recorded in the job summary.
- Treat `Select Test Scope` as the CI authority. Ordinary pull requests and
`main` pushes select no workspace tests; manual focused diagnostics and full
milestone selections are recorded in the job summary.
- Do not wait for optional desktop artifacts, packaging previews, or release
workflows unless the pull request changes that product boundary.
- Test the behavior and meaningful failure modes. Do not use raw test count as
@ -129,14 +132,16 @@ can rebase on the exact result.
1. Merge first branch to `main`
2. Confirm the required GitHub checks for that pull request
3. Rebase the next branch on the updated `main`
4. Run only the focused checks affected by conflict resolution
4. Inspect conflict resolution and run changed-file static checks
5. Merge the next branch
The complete build, workspace suite, integration suite, and applicable E2E or
artifact gates run once at the declared milestone. They are not repeated after
every unrelated merge.
The complete workspace suite, coverage, integration, E2E, desktop artifact,
and Docker gates run once at the declared milestone. They are not repeated
after every unrelated merge.
**Why:** Parallel branches often introduce integration issues that are hidden when batch-merging. Sequential merges with testing between each merge catch these immediately.
**Why:** Sequential merges keep conflicts attributable without paying the
release-certification cost after every independent change. The declared
milestone verifies the integrated candidate once.
### One Agent Per File Rule
@ -192,7 +197,7 @@ owner explicitly requires it.
Before merging, verify the checks selected for the changed product boundary:
- [ ] **Selected CI tier:** Every required check started for the pull request is green.
- [ ] **Focused local evidence:** Changed behavior and meaningful failure modes are covered.
- [ ] **Implementation evidence:** The diff and applicable static checks support the changed behavior.
- [ ] **Shared contracts, when changed:** New types are exported and known consumers type-check.
- [ ] **Configuration, when changed:** Ports, URLs, timeouts, environment variables, CSP, and CORS behave in the affected modes.
- [ ] **Frontend integration, when changed:** HTTP calls use shared helpers and location-sensitive behavior avoids hardcoded hosts.
@ -211,8 +216,9 @@ Before merging, verify the checks selected for the changed product boundary:
### Testing Requirements
Run browser or API smoke tests only when the change affects that product
boundary. Choose the smallest runtime check that proves the behavior:
Run browser or API smoke tests only at an explicit integration or release
milestone when the change affects that product boundary. Choose the smallest
runtime check that proves the behavior:
- **Server or API changes:** Exercise the changed endpoint and its meaningful auth or failure path. Add a health check only when startup or routing changed.
- **Web changes:** Open the changed route and verify its primary interaction, keyboard flow, and failure state.
@ -279,10 +285,10 @@ docs: update README with deployment instructions
3. **Open a PR** against `main`.
4. **Fill out the PR template** — describe changes, link related issues, include screenshots for UI changes.
5. **Ensure the selected PR CI tier passes** — all checks started for the pull
request must be green. The scope selector runs related tests for affected
workspaces and records its base/head evidence in the job summary. Use
`ci:full` for release candidates, critical integration/security boundaries,
or other changes that require an explicit complete-suite gate.
request must be green. The scope selector records affected workspaces but
defers their tests on ordinary pull requests. Use `ci:full` for release
candidates, critical integration/security boundaries, or other changes that
require an explicit complete-suite gate.
6. **Request review** — a maintainer will review and may request changes.
7. **Address feedback** — push additional commits as needed.
8. **Merge** — once approved, a maintainer will merge.
@ -336,32 +342,24 @@ Follow the existing conventions in `.eslintrc.*`, `.prettierrc`, and `tsconfig.j
### CI tiers
| Trigger | Stable checks | Scope |
| ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Documentation-only pull request or merge | Static gates; unit-test jobs record skip decisions | No workspace unit suite |
| Ordinary code pull request | `Lint & Type Check`, `Changed Tests`, `Build`, `Security Audit` | Vitest `related` coverage for affected server, web, CLI, or MCP workspaces |
| Ordinary code merge to `main` | Default static gates plus `Changed Tests` | Related coverage limited to affected workspaces |
| Pull request with `ci:full`, or a CI selector/workflow control change | Default checks plus `Workspace Unit Tests` | Complete workspace, desktop readiness regressions, and exact dual-storage parity |
| Merge whose reviewed head already passed `Workspace Unit Tests` | Static gates; both unit-test tiers record skip decisions | Reuses exact successful head evidence when that head is an ancestor of the merge commit |
| Nightly 08:00 UTC or manual `CI` dispatch with `test_scope=full` | Static gates, `Workspace Unit Tests`, `Build`, `Security Audit` | Complete authoritative workspace suite |
| Manual `CI` dispatch with `test_scope=focused` and optional `base_sha` | Static gates plus the selected unit-test tier | Classifies `base_sha...HEAD` (or `HEAD^...HEAD`) and stays focused unless CI controls changed |
| Desktop/package/release-workflow pull request, relevant `main` push, or manual `Desktop Artifacts` dispatch | Unsigned macOS, Linux, and Windows artifact jobs | Cross-platform packaging |
| Trigger | Stable checks | Scope |
| ---------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Documentation-only pull request or merge | Static gates; test jobs record skip decisions | No workspace tests |
| Ordinary code pull request or merge to `main` | `Lint & Type Check`, `Build`, `Security Audit`, scope recording | No workspace tests or coverage; affected packages remain visible |
| Pull request with `ci:full` | Default checks plus every milestone test and artifact gate | Complete unit, coverage, desktop, Docker, and applicable integration gates |
| Nightly 08:00 UTC or manual `CI` dispatch with `test_scope=full` | Static gates plus complete workspace and coverage gates | Authoritative recurring or operator-selected milestone |
| Manual `CI` dispatch with `test_scope=focused` and optional `base_sha` | Static gates plus `Changed Tests` | Explicit diagnostic slice for affected workspaces; no coverage ratchet |
| Manual `Desktop Artifacts` or `Docker Image Contract` dispatch | Selected artifact or container contract | Explicit operator milestone outside a pull request |
`Select Test Scope` is the decision record for each run. Its summary names the
event, exact base/head range, changed-path count, selected tier, affected
workspaces, and why `Changed Tests` or `Workspace Unit Tests` ran or skipped.
The selector fails safe to the complete suite for unknown non-documentation
paths and for changes to:
- GitHub Actions workflows and the selector itself
- full-suite evidence validation
Shared contracts, package manifests, lockfiles, storage implementations,
desktop source, and known-workspace deletions select focused affected
workspaces. Build and typecheck remain whole-repository gates on every ordinary
code pull request. The full workspace suite runs at scheduled, explicit
`ci:full`, critical integration/security, and release milestones instead of
being repeated for every source slice.
desktop source, and known-workspace deletions are recorded as affected
workspaces without launching tests. Build and typecheck remain
whole-repository gates on every ordinary code pull request. The full workspace
suite and release-grade artifact gates run at scheduled, explicit `ci:full`,
critical integration/security, and release milestones.
Run the selector contract locally with:
@ -372,11 +370,11 @@ pnpm test:ci-scope
Release validation remains the final authority: clean-clone build, full unit
and integration suites, applicable E2E, and signed artifact verification.
The operational target for the default pull-request tier is under 15 minutes,
with no desktop packaging. This is a target rather than an SLA; dependency
installation and hosted-runner availability still vary. Behavior changes
should include coverage reachable from the changed source so Vitest's related
test selection can execute it.
The operational target for the default pull-request tier is under 10 minutes,
with no workspace tests, coverage, or desktop/container packaging. This is a
target rather than an SLA; dependency installation and hosted-runner
availability still vary. Behavior changes should include coverage that the
next declared milestone can exercise.
Optional `Desktop Artifacts`, packaging previews, and release workflows are not
merge blockers outside their path boundary. If one starts without providing

View file

@ -50,9 +50,11 @@ live repository install into a production-only dependency state.
## GitHub Workflows
`Desktop Artifacts` runs on desktop/package/release-workflow pull requests,
after server/web/shared/desktop changes merge to `main`, and on manual
dispatch. It builds unsigned artifacts on:
`Desktop Artifacts` runs only for a pull request carrying `ci:full` or through
manual dispatch. Ordinary pull requests and `main` pushes do not package
desktop applications. A release candidate keeps `ci:full` applied through its
final synchronization so the artifacts correspond to the reviewed head. The
workflow builds unsigned artifacts on:
- `macos-15`: DMG, ZIP, blockmap, and update YAML.
- `ubuntu-24.04`: x64 AppImage, deb, rpm, blockmap, and update YAML.
@ -187,7 +189,8 @@ policy is tracked in
- Run `pnpm desktop:package:windows:unsigned` on Windows or the
`Desktop Artifacts` Windows job and inspect preview artifact names. This is
not a v6 GA release gate.
- Run `Desktop Artifacts` and download the uploaded DMG/ZIP/update metadata.
- Apply `ci:full` to the release-candidate pull request, then download the
uploaded DMG/ZIP/update metadata from its `Desktop Artifacts` run.
- Edit `docs/releases/vX.Y.Z.md`, run
`pnpm validate:release -- --version X.Y.Z`, and publish that exact file with
`gh release create --notes-file` or `gh release edit --notes-file`. Do not

View file

@ -55,7 +55,11 @@ Documentation freshness: 2026-08-22 for Veritas Kanban 6.1.1.
## Final Release Validation Commands
Run once from the clean 6.1.1 release candidate:
Apply `ci:full` to the release pull request and keep it applied through the
final candidate synchronization. That single milestone runs the complete
workspace suite, critical-path coverage, unsigned desktop artifacts, and
Docker image contract. Run the following commands once from the clean 6.1.1
release candidate:
```bash
pnpm install --frozen-lockfile
@ -128,8 +132,9 @@ the Homebrew cask uses the independently verified published ZIP checksum.
- [x] Native About, copied support information, the desktop bridge, and updater
fallback consume one authoritative version/build/channel/OS/architecture
record (#1005).
- [x] Pull-request verification uses documentation-only, focused, or full test
scope, while explicit and milestone release gates remain full (#1000).
- [x] Ordinary pull-request verification records affected workspaces without
running tests; manual focused diagnostics and explicit `ci:full`,
scheduled, or release milestones own the test suites (#1000, #1227).
- [x] Published release notes are sourced from
`docs/releases/vX.Y.Z.md`, use one full-width Markdown line per paragraph
or list item, reject blockquotes and overlong prose blocks, and are

View file

@ -3,9 +3,10 @@
Review date: 2026-06-04
The scheduled QA workflow runs heavier browser and load-test coverage outside
the fast pull-request path. Pull requests stay limited to lint, typecheck,
workspace unit tests, build, production dependency audit, and the desktop
artifact gate when relevant.
the fast pull-request path. Ordinary pull requests stay limited to lint,
typecheck, build, production dependency audit, security gates, and test-scope
recording. Workspace tests, coverage, desktop artifacts, and Docker contracts
run only at scheduled, manual, or `ci:full` milestones.
The 2026-06-04 audit found the workflow failing before job creation because
job-level `env` used the `runner.temp` context. GitHub does not expose the
@ -30,6 +31,7 @@ Workflow file:
Triggers:
- Pull request carrying `ci:full`, rerun on each synchronized candidate head.
- Weekly schedule: Monday at 08:17 UTC.
- Manual dispatch: `workflow_dispatch`.

View file

@ -14,8 +14,9 @@ export const CADENCE_CONTRACTS = {
pattern: /one independently shippable behavior/i,
},
{
description: 'narrowest useful implementation loop',
pattern: /run the narrowest useful loop/i,
description: 'ordinary implementation defers workspace tests',
pattern:
/Do not run workspace unit, coverage, E2E, desktop packaging, or Docker contract tests between implementation PRs/i,
},
{
description: '45-minute split or escalate delivery checkpoint',
@ -62,7 +63,8 @@ export const CADENCE_CONTRACTS = {
},
{
description: 'affected-boundary runtime smoke tests',
pattern: /Run browser or API smoke tests only when the change affects/i,
pattern:
/Run browser or API smoke tests only at an explicit integration or release milestone/i,
},
],
'.github/PULL_REQUEST_TEMPLATE.md': [
@ -75,8 +77,8 @@ export const CADENCE_CONTRACTS = {
pattern: /\*\*Verification tier:\*\*/i,
},
{
description: 'focused changed-package test tier',
pattern: /Focused changed-package tests/i,
description: 'explicit focused diagnostic tier',
pattern: /Explicit focused diagnostic/i,
},
{
description: 'full milestone gate tier',
@ -271,8 +273,7 @@ export function findUnsafeCanonicalCadenceStatements(files) {
export function findAmbiguousFocusedTestCommands(files) {
const violations = [];
const pattern =
/\bpnpm\s+(?:--filter(?:=|\s+)|-F\s+)\S+\s+(?:run\s+)?test\s+--(?=\s)/gi;
const pattern = /\bpnpm\s+(?:--filter(?:=|\s+)|-F\s+)\S+\s+(?:run\s+)?test\s+--(?=\s)/gi;
for (const [file, content] of Object.entries(files)) {
const normalized = normalizeWhitespace(content);

View file

@ -2,7 +2,7 @@
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { appendFile } from 'node:fs/promises';
import path, { matchesGlob } from 'node:path';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const COVERAGE_POLICY = JSON.parse(
@ -13,15 +13,6 @@ const COVERAGE_POLICY = JSON.parse(
);
const WORKSPACE_NAMES = COVERAGE_POLICY.packages.map(({ id }) => id);
const FULL_SUITE_PATH_PATTERNS = [
/^\.github\/workflows\//,
/^scripts\/select-ci-test-scope(?:\.test)?\.mjs$/,
/^scripts\/verify-full-suite-job-evidence(?:\.test)?\.mjs$/,
/^scripts\/(?:run-coverage|check-coverage-(?:policy|ratchets))(?:\.test)?\.mjs$/,
/^docs\/testing\/critical-path-coverage\.json$/,
/^(?:server|web|cli|mcp|desktop)\/vitest\.config\.ts$/,
];
const ALL_WORKSPACE_PATH_PATTERNS = [
/^shared\//,
/^package\.json$/,
@ -32,19 +23,6 @@ const ALL_WORKSPACE_PATH_PATTERNS = [
/^tsconfig(?:\.[^/]+)?\.json$/,
];
const CRITICAL_COVERAGE_PATHS = Object.fromEntries(
COVERAGE_POLICY.packages.map((packagePolicy) => [
packagePolicy.id,
[
...packagePolicy.boundaries.flatMap(({ include }) => include),
...(packagePolicy.runner?.testFiles ?? []).map(
(testFile) => `${packagePolicy.id}/${testFile}`
),
...(packagePolicy.runner?.triggerPatterns ?? []),
],
])
);
const DOCUMENTATION_PATH_PATTERNS = [
/\.md$/i,
/^docs\//,
@ -64,12 +42,6 @@ function normalizeFiles(files) {
return [...new Set(files.map((file) => file.trim()).filter(Boolean))].sort();
}
function summarizePaths(files) {
const visible = files.slice(0, 5);
const remainder = files.length - visible.length;
return `${visible.join(', ')}${remainder > 0 ? `, and ${remainder} more` : ''}`;
}
export function isDocumentationPath(file) {
return DOCUMENTATION_PATH_PATTERNS.some((pattern) => pattern.test(file));
}
@ -78,10 +50,6 @@ export function isDependencyFreeScopeControlPath(file) {
return DEPENDENCY_FREE_SCOPE_CONTROL_PATH_PATTERNS.some((pattern) => pattern.test(file));
}
export function requiresFullSuite(file) {
return FULL_SUITE_PATH_PATTERNS.some((pattern) => pattern.test(file));
}
export function affectedWorkspaces(files) {
const selected = new Set();
@ -96,15 +64,9 @@ export function affectedWorkspaces(files) {
return WORKSPACE_NAMES.filter((name) => selected.has(name));
}
export function coverageWorkspaces(files, scope = 'focused') {
export function coverageWorkspaces(_files, scope = 'focused') {
if (scope === 'full') return [...WORKSPACE_NAMES];
if (scope === 'none') return [];
return WORKSPACE_NAMES.filter((workspace) =>
files.some((file) =>
CRITICAL_COVERAGE_PATHS[workspace].some((pattern) => matchesGlob(file, pattern))
)
);
return [];
}
export function classifyCiTestScope({
@ -113,9 +75,6 @@ export function classifyCiTestScope({
labels = [],
changedFiles = [],
deletedFiles = [],
reviewedFullSuite = false,
reviewedPullRequest = '',
reviewedFullSuiteMode = '',
}) {
const files = normalizeFiles(changedFiles);
const deleted = normalizeFiles(deletedFiles);
@ -148,46 +107,20 @@ export function classifyCiTestScope({
};
}
if (eventName === 'push' && reviewedFullSuite) {
const prSuffix = reviewedPullRequest ? ` for PR #${reviewedPullRequest}` : '';
const evidence =
reviewedFullSuiteMode === 'identical-tree'
? 'has the exact Git tree published by this squash merge'
: 'is an ancestor of this merge commit';
if (eventName === 'workflow_dispatch' && manualScope === 'focused') {
if (packages.length === 0) {
return {
scope: 'none',
packages: [],
files,
reason: 'The manually selected range does not affect a testable workspace.',
};
}
return {
scope: 'none',
packages: [],
scope: 'focused',
packages,
files,
reason: `The reviewed head${prSuffix} already passed Workspace Unit Tests and Critical Path Coverage, and ${evidence}.`,
};
}
const unclassifiedDeletedCodePaths = deleted.filter(
(file) =>
!isDocumentationPath(file) &&
!isDependencyFreeScopeControlPath(file) &&
affectedWorkspaces([file]).length === 0
);
if (unclassifiedDeletedCodePaths.length > 0) {
return {
scope: 'full',
packages: WORKSPACE_NAMES,
files,
reason: `Deleted non-documentation paths outside a known workspace fail safe to the full suite: ${summarizePaths(
unclassifiedDeletedCodePaths
)}`,
};
}
const fullSuitePaths = files.filter(requiresFullSuite);
if (fullSuitePaths.length > 0) {
return {
scope: 'full',
packages: WORKSPACE_NAMES,
files,
reason: `CI control paths require the full suite before the selector change can take effect: ${summarizePaths(
fullSuitePaths
)}`,
reason: `Manual dispatch explicitly requested focused diagnostics for: ${packages.join(', ')}.`,
};
}
@ -207,41 +140,18 @@ export function classifyCiTestScope({
packages: [],
files,
reason:
'Only documentation or dependency-free delivery-cadence controls checked by the selector changed.',
};
}
const unclassifiedCodePaths = files.filter(
(file) =>
!isDocumentationPath(file) &&
!isDependencyFreeScopeControlPath(file) &&
affectedWorkspaces([file]).length === 0
);
if (unclassifiedCodePaths.length > 0) {
return {
scope: 'full',
packages: WORKSPACE_NAMES,
files,
reason: `Non-documentation paths outside a known test workspace fail safe to the full suite: ${summarizePaths(
unclassifiedCodePaths
)}`,
};
}
if (packages.length === 0) {
return {
scope: 'none',
packages: [],
files,
reason: 'No testable workspace changed.',
'Only documentation or dependency-free policy controls changed; workspace tests are reserved for explicit milestones.',
};
}
return {
scope: 'focused',
scope: 'none',
packages,
files,
reason: `Run Vitest related coverage for affected workspace packages: ${packages.join(', ')}.`,
reason:
packages.length > 0
? `Ordinary changes defer workspace tests and coverage to an explicit milestone. Affected packages: ${packages.join(', ')}.`
: 'This ordinary change is not an explicit test milestone; workspace tests and coverage are deferred.',
};
}
@ -337,9 +247,6 @@ async function main() {
headSha: process.env.CI_HEAD_SHA || '',
manualScope: process.env.CI_MANUAL_SCOPE || '',
labels: parseLabels(process.env.CI_PR_LABELS || '[]'),
reviewedFullSuite: process.env.CI_REVIEWED_FULL === 'true',
reviewedPullRequest: process.env.CI_REVIEWED_PR || '',
reviewedFullSuiteMode: process.env.CI_REVIEWED_MODE || '',
};
if (!input.eventName) {

View file

@ -9,7 +9,6 @@ import {
githubOutputLines,
isDependencyFreeScopeControlPath,
isDocumentationPath,
requiresFullSuite,
} from './select-ci-test-scope.mjs';
test('classifies documentation-only pull requests without unit tests', () => {
@ -37,39 +36,40 @@ test('dependency-free cadence controls do not trigger workspace unit tests', ()
assert.equal(isDependencyFreeScopeControlPath('scripts/check-delivery-cadence.mjs'), true);
});
test('cadence controls do not widen a focused workspace change', () => {
test('cadence controls do not turn an ordinary workspace change into a test milestone', () => {
const result = classifyCiTestScope({
eventName: 'pull_request',
changedFiles: ['scripts/check-delivery-cadence.mjs', 'server/src/routes/tasks.ts'],
});
assert.equal(result.scope, 'focused');
assert.equal(result.scope, 'none');
assert.deepEqual(result.packages, ['server']);
});
test('selects affected workspaces for ordinary code changes', () => {
test('records affected workspaces without testing an ordinary code change', () => {
const result = classifyCiTestScope({
eventName: 'pull_request',
changedFiles: ['web/src/App.tsx', 'server/src/routes/tasks.ts', 'README.md'],
});
assert.equal(result.scope, 'focused');
assert.equal(result.scope, 'none');
assert.deepEqual(result.packages, ['server', 'web']);
});
test('selects coverage only for changed critical boundaries and all packages for full scope', () => {
test('selects critical coverage only for the full milestone scope', () => {
const files = [
'server/src/storage/file-storage.ts',
'web/src/components/Board.tsx',
'mcp/src/tools/tasks.ts',
];
assert.deepEqual(coverageWorkspaces(files), ['server', 'mcp']);
assert.deepEqual(coverageWorkspaces(files), []);
assert.deepEqual(coverageWorkspaces(files, 'focused'), []);
assert.deepEqual(coverageWorkspaces(files, 'none'), []);
assert.deepEqual(coverageWorkspaces(files, 'full'), ['server', 'web', 'cli', 'mcp', 'desktop']);
});
test('reruns coverage when governed tests, schemas, or shared permissions change', () => {
test('defers governed critical-path coverage until a milestone', () => {
assert.deepEqual(
coverageWorkspaces([
'server/src/__tests__/provider-completion-service.test.ts',
@ -78,7 +78,7 @@ test('reruns coverage when governed tests, schemas, or shared permissions change
'web/src/__tests__/useWebSocket.test.ts',
'mcp/src/__tests__/task-tools.test.ts',
]),
['server', 'web', 'mcp']
[]
);
});
@ -92,7 +92,7 @@ test('ci:full overrides a documentation-only pull request', () => {
assert.equal(result.scope, 'full');
});
test('selects the full suite only for CI control paths', () => {
test('CI control paths require an explicit milestone instead of auto-running the full suite', () => {
const paths = [
'.github/workflows/ci.yml',
'scripts/select-ci-test-scope.mjs',
@ -108,12 +108,11 @@ test('selects the full suite only for CI control paths', () => {
eventName: 'pull_request',
changedFiles: [file],
});
assert.equal(result.scope, 'full', file);
assert.equal(requiresFullSuite(file), true, file);
assert.equal(result.scope, 'none', file);
}
});
test('keeps shared, storage, desktop, and manifest changes focused by workspace', () => {
test('records shared, storage, desktop, and manifest workspaces without automatic tests', () => {
const cases = [
{
file: 'shared/src/types/task.types.ts',
@ -131,9 +130,8 @@ test('keeps shared, storage, desktop, and manifest changes focused by workspace'
eventName: 'pull_request',
changedFiles: [file],
});
assert.equal(result.scope, 'focused', file);
assert.equal(result.scope, 'none', file);
assert.deepEqual(result.packages, packages, file);
assert.equal(requiresFullSuite(file), false, file);
}
});
@ -168,60 +166,33 @@ test('focused manual runs still classify the selected range by risk', () => {
);
});
test('a successful reviewed full suite suppresses duplicate post-merge tests', () => {
const result = classifyCiTestScope({
eventName: 'push',
reviewedFullSuite: true,
reviewedPullRequest: '1000',
changedFiles: ['.github/workflows/ci.yml'],
});
assert.equal(result.scope, 'none');
assert.match(result.reason, /PR #1000/);
assert.match(result.reason, /ancestor/);
});
test('an exact reviewed tree suppresses duplicate tests after a squash merge', () => {
const result = classifyCiTestScope({
eventName: 'push',
reviewedFullSuite: true,
reviewedPullRequest: '1011',
reviewedFullSuiteMode: 'identical-tree',
changedFiles: ['.github/workflows/ci.yml'],
});
assert.equal(result.scope, 'none');
assert.match(result.reason, /PR #1011/);
assert.match(result.reason, /exact Git tree/);
});
test('ordinary post-merge pushes remain limited to affected packages', () => {
test('ordinary post-merge pushes defer tests while recording affected packages', () => {
const result = classifyCiTestScope({
eventName: 'push',
changedFiles: ['cli/src/commands/doctor.ts'],
});
assert.equal(result.scope, 'focused');
assert.equal(result.scope, 'none');
assert.deepEqual(result.packages, ['cli']);
});
test('unknown non-documentation paths fail safe to the full suite', () => {
test('unknown non-documentation paths wait for an explicit milestone', () => {
const result = classifyCiTestScope({
eventName: 'pull_request',
changedFiles: ['site/src/runtime.ts'],
});
assert.equal(result.scope, 'full');
assert.equal(result.scope, 'none');
});
test('deleted known-workspace source stays focused while unknown source fails safe', () => {
test('deleted source paths are recorded without automatic test escalation', () => {
assert.equal(
classifyCiTestScope({
eventName: 'pull_request',
changedFiles: ['server/src/obsolete.ts'],
deletedFiles: ['server/src/obsolete.ts'],
}).scope,
'focused'
'none'
);
assert.equal(
classifyCiTestScope({
@ -229,7 +200,7 @@ test('deleted known-workspace source stays focused while unknown source fails sa
changedFiles: ['site/src/obsolete.ts'],
deletedFiles: ['site/src/obsolete.ts'],
}).scope,
'full'
'none'
);
assert.equal(
classifyCiTestScope({