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
16 changes: 16 additions & 0 deletions .github/workflows/ci-required.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,24 +151,35 @@ jobs:
dotnet-version: 8.0.x

backend-unit:
needs:
- backend-architecture
- release-workflow-contract
name: Backend Unit
uses: ./.github/workflows/reusable-backend-unit.yml
with:
dotnet-version: 8.0.x

api-integration:
needs:
- backend-architecture
- release-workflow-contract
name: API Integration
uses: ./.github/workflows/reusable-api-integration.yml
with:
dotnet-version: 8.0.x

migration-validation:
needs:
- backend-architecture
name: Migration Validation
uses: ./.github/workflows/reusable-migration-validation.yml
with:
dotnet-version: 8.0.x

frontend-unit:
needs:
- release-workflow-contract
- paper-color-audit
name: Frontend Unit
uses: ./.github/workflows/reusable-frontend-unit.yml
with:
Expand All @@ -179,6 +190,8 @@ jobs:
uses: ./.github/workflows/reusable-paper-color-audit.yml

container-images:
needs:
- release-workflow-contract
name: Container Images
uses: ./.github/workflows/reusable-container-images.yml
with:
Expand Down Expand Up @@ -245,6 +258,9 @@ jobs:
- backend-unit
- api-integration
- migration-validation
- frontend-unit
Comment thread
Chris0Jeky marked this conversation as resolved.
- release-workflow-contract
- paper-color-audit
uses: ./.github/workflows/reusable-e2e-smoke.yml
with:
dotnet-version: 8.0.x
Expand Down
37 changes: 37 additions & 0 deletions docs/ci/continuation/STAGING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Required-workflow staging

Date: 2026-09-10. Owners: #2327 and #2332. Parent: [engineering contract](README.md).

This slice applies the `minimal` transform to `.github/workflows/ci-required.yml`. It changes scheduling dependencies only. It does not select fewer checks on a healthy candidate, remove Windows coverage, alter security enforcement, change permissions, or reuse earlier test results.

| Work | New prerequisite |
| --- | --- |
| Backend Unit and API Integration | Backend Architecture and Release Workflow Contract |
| Migration Validation | Backend Architecture |
| Frontend Unit | Release Workflow Contract and Paper Color Audit |
| Container Images | Release Workflow Contract |
| E2E Smoke | Frontend Unit, Release Workflow Contract and Paper Color Audit, in addition to every existing dependency |

The short control checks can prevent launching dependent expensive work after a known failure. The API job deliberately does not wait for the entire Backend Unit matrix: the `compute` proposal is optional, not adopted here. Security jobs remain independent. In particular, Secret Scan only runs on PR events and must not become an unconditional dependency on push/merge-group events.

The original workflow was read at immutable base `6c51b09bcdcefbc7a852a7047aa9ab8ee5eb10b7`. Its source blob `f056d76a0c1b4ecabd95783d47131dfe096121c5` was reproduced byte-for-byte before transformation. The transform checks that removing `needs` from each changed job leaves identical bytes. It preserves the complete header/comments, existing check names, reusable callees, commands, action pins, matrices, event/concurrency rules and permissions. The historical header's advisory/enforcing shorthand is deliberately not edited by a dependency-only PR; executable settings remain the authority.

## Qualification

Run the checked-in topology regressions through the existing self-test bridge:

```sh
node --test scripts/ci/smart-ci/continuation.test.mjs
```

Local result for this slice: **257 passed, 0 failed/skipped/cancelled**, Node 22.16.0/Linux. Four regressions exercise the actual checked-in workflow; the existing transformer suite covers unknown shapes, cycles, duplicate dependencies, idempotence and preservation. Local PyYAML parsing is only a syntax sanity check, not GitHub workflow semantic validation.

Configured-Node hosted Smart CI, Actionlint, full required CI and independent/maintainer reviews remain required on this PR head. Do not equate a successful core fixture with product qualification. No cancellation API is called: the transform prevents dependent jobs from starting after failed prerequisites but cannot stop work already running.

## Measurement and rollback

Compare matched change/risk strata and include failed/cancelled attempts, not just green runs. Record total runner seconds and healthy-candidate latency separately; a dependency barrier can reduce failed-run cost while increasing green latency. Include queue/setup time and collector overhead when available; do not fill missing data with zeros.

The retained August baseline is historical and is not an after-measurement. Capture a fresh window using the existing estate measurement tool only after the topology is deployed. No numerical savings are claimed in this PR.

Rollback is the inverse of these `needs` additions. Keep every newer concurrent hardening change. Do not restore an entire old workflow file over subsequent edits. The canonical policy remains shadow and all human settings/review gates remain unchanged.
1 change: 1 addition & 0 deletions scripts/ci/smart-ci/continuation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ import './continuation/tests/execution.test.mjs';
import './continuation/tests/planner.test.mjs';
import './continuation/tests/adapter.test.mjs';
import './continuation/tests/repository.test.mjs';
import './continuation/tests/workflow.test.mjs';
24 changes: 24 additions & 0 deletions scripts/ci/smart-ci/continuation/tests/workflow.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { stageWorkflow } from '../tools/stage-taskdeck.mjs';

const workflow = () => readFileSync(new URL('../../../../../.github/workflows/ci-required.yml', import.meta.url), 'utf8').replaceAll('\r\n', '\n');
function block(text, id) {
const start = text.indexOf(`\n ${id}:\n`); assert.ok(start >= 0, `missing ${id}`);
return text.slice(start + 1).split(/\n [a-z][a-z0-9-]*:\n/)[0];
}
test('checked-in required workflow satisfies minimal dependency staging', () => {
const text = workflow(), staged = stageWorkflow(text); assert.deepEqual(staged.changes, []); assert.equal(staged.text, text);
});
test('E2E waits for frontend and existing backend prerequisites', () => {
const b = block(workflow(), 'e2e-smoke');
for (const id of ['frontend-unit', 'backend-unit', 'api-integration', 'migration-validation', 'backend-architecture', 'docs-governance']) assert.ok(b.includes(` - ${id}\n`));
});
test('API does not wait for the full backend unit platform matrix', () => {
const b = block(workflow(), 'api-integration'); assert.ok(b.includes(' - backend-architecture\n')); assert.ok(!b.includes(' - backend-unit\n'));
});
test('PR-only secret scan remains independent of push and merge-group barriers', () => {
const text = workflow(); assert.ok(block(text, 'secret-scan').includes("if: ${{ github.event_name == 'pull_request' }}"));
assert.ok(!text.includes(' - secret-scan\n')); assert.match(text, /^ push:/m); assert.match(text, /^ pull_request:/m); assert.match(text, /^ merge_group:/m);
});
48 changes: 13 additions & 35 deletions scripts/ci/smart-ci/required-e2e-dependencies.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,38 @@ function extractTopLevelJob(workflow, jobName) {
const lines = workflow.replaceAll('\r\n', '\n').split('\n')
const jobsIndex = lines.findIndex((line) => line.trim() === 'jobs:')
assert.notEqual(jobsIndex, -1, 'ci-required.yml is missing the top-level jobs mapping')

const jobPattern = new RegExp(`^ ${jobName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}:\\s*$`)
const jobIndex = lines.findIndex((line, index) => index > jobsIndex && jobPattern.test(line))
const jobIndex = lines.findIndex((line, index) => index > jobsIndex && line === ` ${jobName}:`)
assert.notEqual(jobIndex, -1, `ci-required.yml is missing the top-level ${jobName} job`)

const nextJobIndex = lines.findIndex(
(line, index) => index > jobIndex && /^ [A-Za-z0-9_-]+:\s*$/.test(line),
)
const nextJobIndex = lines.findIndex((line, index) => index > jobIndex && /^ [A-Za-z0-9_-]+:\s*$/.test(line))
return lines.slice(jobIndex, nextJobIndex === -1 ? lines.length : nextJobIndex).join('\n')
}

function extractNeeds(job) {
const lines = job.split('\n')
const needsIndex = lines.findIndex((line) => line.trim() === 'needs:')
const lines = job.split('\n'), needsIndex = lines.findIndex(line => line.trim() === 'needs:')
assert.notEqual(needsIndex, -1, 'e2e-smoke is missing its needs list')

const needsIndent = lines[needsIndex].search(/\S/)
const dependencies = []
for (const line of lines.slice(needsIndex + 1)) {
const item = line.match(new RegExp(`^\\s{${needsIndent + 2}}-\\s*([A-Za-z0-9_-]+)\\s*$`))
if (item) {
dependencies.push(item[1])
continue
}
if (line.trim() === '') continue
break
const item = line.match(/^ -\s*([A-Za-z0-9_-]+)\s*$/)
if (item) dependencies.push(item[1]); else if (line.trim()) break
}
return dependencies
}

test('required E2E waits on the independent required prerequisites', async () => {
const workflow = await readFile(requiredWorkflowUrl, 'utf8')
const e2eJob = extractTopLevelJob(workflow, 'e2e-smoke')

test('required E2E waits on semantic and cheap control prerequisites', async () => {
const e2eJob = extractTopLevelJob(await readFile(requiredWorkflowUrl, 'utf8'), 'e2e-smoke')
assert.deepEqual(extractNeeds(e2eJob).sort(), [
'api-integration',
'backend-architecture',
'backend-unit',
'docs-governance',
'migration-validation',
'api-integration', 'backend-architecture', 'backend-unit', 'docs-governance',
'frontend-unit', 'migration-validation', 'paper-color-audit', 'release-workflow-contract',
])
assert.doesNotMatch(e2eJob, /^ - frontend-unit\s*$/m)
// This is a failure barrier, not artifact reuse. The callee still owns setup below.
// PR-only secret scan must not block push/merge-group through an unconditional needs edge.
assert.doesNotMatch(e2eJob, /^ - secret-scan\s*$/m)
assert.match(e2eJob, /^ uses:\s*\.\/\.github\/workflows\/reusable-e2e-smoke\.yml\s*$/m)
})

test('reusable E2E owns its runtime setup and execution', async () => {
const workflow = await readFile(reusableE2eWorkflowUrl, 'utf8')

assert.match(workflow, /- name: Checkout[\s\S]*?uses: actions\/checkout@/)
assert.match(workflow, /- name: Setup \.NET[\s\S]*?uses: actions\/setup-dotnet@/)
assert.match(workflow, /- name: Setup Node[\s\S]*?uses: actions\/setup-node@/)
assert.match(
workflow,
/- name: Install frontend dependencies[\s\S]*?working-directory: frontend\/taskdeck-web[\s\S]*?run: npm ci/,
)
assert.match(workflow, /- name: Install frontend dependencies[\s\S]*?working-directory: frontend\/taskdeck-web[\s\S]*?run: npm ci/)
assert.match(workflow, /- name: Run Playwright smoke tests[\s\S]*?run: npx playwright test/)
})
Loading