Skip to content

Commit 401860a

Browse files
bloveclaude
andcommitted
ci: scope-gate the examples/ag-ui e2e job and make it block merges
`examples-ag-ui-e2e` had no `if:` at all. It ran on every push and every pull request — a Playwright suite with a 35-minute timeout, executing on docs-only and website-only PRs. On #932 (three files under apps/website/src) it ran to completion for nothing. It was also absent from `required-pr-checks`, so an ag-ui e2e failure never blocked a merge. Both halves are fixed here deliberately: gating it without requiring it would leave a suite that is skipped often and ignored when it fails. The plumbing was half-built rather than missing. examples/ag-ui/project.json and examples/ag-ui/angular/project.json already carried `scope:examples-ag-ui`, but `examples_ag_ui` was never added to SCOPE_KEYS, so classifyFromAffected read the tag and discarded it — there was no output for the job to gate on. Also tags examples/ag-ui/python, which had no tags at all. That is the backend the e2e job `uv sync`s and drives the Angular app against, so leaving it untagged would have made a python-only change silently skip the suite — buying speed by dropping coverage. examples/chat/python already carries its `scope:examples-chat` tag the same way. Verified against real commits: website-only #932 base..head → false; an ag-ui angular change → true; an isolated python-only change → true, with examples_chat and website both false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 574a0c0 commit 401860a

5 files changed

Lines changed: 92 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
cockpit_smoke: ${{ steps.scope.outputs.cockpit_smoke }}
3030
cockpit_deploy_smoke: ${{ steps.scope.outputs.cockpit_deploy_smoke }}
3131
examples_chat: ${{ steps.scope.outputs.examples_chat }}
32+
examples_ag_ui: ${{ steps.scope.outputs.examples_ag_ui }}
3233
cockpit_e2e: ${{ steps.scope.outputs.cockpit_e2e }}
3334
website_e2e: ${{ steps.scope.outputs.website_e2e }}
3435
posthog: ${{ steps.scope.outputs.posthog }}
@@ -382,10 +383,7 @@ jobs:
382383
examples-ag-ui-e2e:
383384
name: 'examples/ag-ui — e2e'
384385
needs: ci-scope
385-
# No dedicated ci-scope output exists for examples/ag-ui yet, so this job
386-
# runs on every push and pull_request rather than being scope-gated. It is
387-
# deliberately NOT wired into the require_scoped aggregation in
388-
# `required-pr-checks` (see Step 1 report) to avoid breaking that logic.
386+
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_ag_ui == 'true'
389387
runs-on: ubuntu-latest
390388
timeout-minutes: 35
391389
steps:
@@ -564,6 +562,7 @@ jobs:
564562
- cockpit-deploy-smoke
565563
- examples-chat-smoke
566564
- examples-chat-e2e
565+
- examples-ag-ui-e2e
567566
- cockpit-e2e-summary
568567
- website-e2e
569568
- posthog-sync-plan
@@ -583,6 +582,7 @@ jobs:
583582
RESULT_COCKPIT_DEPLOY_SMOKE: ${{ needs.cockpit-deploy-smoke.result }}
584583
RESULT_EXAMPLES_CHAT_SMOKE: ${{ needs.examples-chat-smoke.result }}
585584
RESULT_EXAMPLES_CHAT_E2E: ${{ needs.examples-chat-e2e.result }}
585+
RESULT_EXAMPLES_AG_UI_E2E: ${{ needs.examples-ag-ui-e2e.result }}
586586
RESULT_COCKPIT_E2E: ${{ needs.cockpit-e2e-summary.result }}
587587
RESULT_WEBSITE_E2E: ${{ needs.website-e2e.result }}
588588
RESULT_POSTHOG: ${{ needs.posthog-sync-plan.result }}
@@ -595,6 +595,7 @@ jobs:
595595
SCOPE_COCKPIT_SMOKE: ${{ needs.ci-scope.outputs.cockpit_smoke }}
596596
SCOPE_COCKPIT_DEPLOY_SMOKE: ${{ needs.ci-scope.outputs.cockpit_deploy_smoke }}
597597
SCOPE_EXAMPLES_CHAT: ${{ needs.ci-scope.outputs.examples_chat }}
598+
SCOPE_EXAMPLES_AG_UI: ${{ needs.ci-scope.outputs.examples_ag_ui }}
598599
SCOPE_COCKPIT_E2E: ${{ needs.ci-scope.outputs.cockpit_e2e }}
599600
SCOPE_WEBSITE_E2E: ${{ needs.ci-scope.outputs.website_e2e }}
600601
SCOPE_POSTHOG: ${{ needs.ci-scope.outputs.posthog }}
@@ -648,6 +649,11 @@ jobs:
648649
require_scoped "cockpit_deploy_smoke" "Cockpit — deploy smoke dry-run" "$RESULT_COCKPIT_DEPLOY_SMOKE" "$SCOPE_COCKPIT_DEPLOY_SMOKE"
649650
require_scoped "examples_chat" "examples/chat — python smoke" "$RESULT_EXAMPLES_CHAT_SMOKE" "$SCOPE_EXAMPLES_CHAT"
650651
require_scoped "examples_chat" "examples/chat — e2e" "$RESULT_EXAMPLES_CHAT_E2E" "$SCOPE_EXAMPLES_CHAT"
652+
require_scoped \
653+
"examples_ag_ui" \
654+
"examples/ag-ui — e2e" \
655+
"$RESULT_EXAMPLES_AG_UI_E2E" \
656+
"$SCOPE_EXAMPLES_AG_UI"
651657
require_scoped "cockpit_e2e" "Cockpit — e2e" "$RESULT_COCKPIT_E2E" "$SCOPE_COCKPIT_E2E"
652658
require_scoped "website_e2e" "Website — e2e" "$RESULT_WEBSITE_E2E" "$SCOPE_WEBSITE_E2E"
653659
require_scoped "posthog" "PostHog — dashboards-as-code drift check" "$RESULT_POSTHOG" "$SCOPE_POSTHOG"

examples/ag-ui/python/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "examples-ag-ui-python",
3+
"tags": ["scope:examples-ag-ui"],
34
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
45
"projectType": "application",
56
"sourceRoot": "examples/ag-ui/python/src",

scripts/ci-scope.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const SCOPE_KEYS = [
1515
'cockpit_deploy_smoke',
1616
'cockpit_e2e',
1717
'examples_chat',
18+
'examples_ag_ui',
1819
'posthog',
1920
'scripts_tests',
2021
];

scripts/ci-scope.spec.mjs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,59 @@ describe('classifyFromAffected — tag isolation', () => {
395395
});
396396
});
397397

398+
describe('classifyFromAffected — examples/ag-ui', () => {
399+
// examples/ag-ui/{,angular/}project.json already carried
400+
// `scope:examples-ag-ui`, but `examples_ag_ui` was missing from SCOPE_KEYS,
401+
// so classifyFromAffected read the tag and dropped it on the floor. The
402+
// examples-ag-ui-e2e job therefore had no scope to gate on and ran on every
403+
// PR — a 35-minute-timeout Playwright job on docs-only changes.
404+
it('an affected examples/ag-ui project selects examples_ag_ui only', async () => {
405+
const project = JSON.parse(
406+
await readFile('examples/ag-ui/angular/project.json', 'utf8')
407+
);
408+
const scope = classifyFromAffected(
409+
['examples/ag-ui/angular/src/app/app.ts'],
410+
[{ name: project.name, tags: project.tags }]
411+
);
412+
413+
assert.equal(scope.examples_ag_ui, true);
414+
assert.equal(scope.examples_chat, false);
415+
assert.equal(scope.cockpit_e2e, false);
416+
assert.equal(scope.website, false);
417+
});
418+
419+
it('the python backend the e2e job uv-syncs owns the scope too', async () => {
420+
// The examples-ag-ui-e2e job runs `uv sync` in examples/ag-ui/python and
421+
// then drives the Angular app against it. An untagged backend would mean
422+
// a python-only change silently skips the suite — scoping that buys speed
423+
// by dropping coverage.
424+
const project = JSON.parse(
425+
await readFile('examples/ag-ui/python/project.json', 'utf8')
426+
);
427+
428+
assert.ok(
429+
project.tags?.includes('scope:examples-ag-ui'),
430+
'examples/ag-ui/python must select the ag-ui e2e suite'
431+
);
432+
433+
const scope = classifyFromAffected(
434+
['examples/ag-ui/python/src/agent.py'],
435+
[{ name: project.name, tags: project.tags }]
436+
);
437+
assert.equal(scope.examples_ag_ui, true);
438+
});
439+
440+
it('a website-only change leaves examples_ag_ui false', () => {
441+
const scope = classifyFromAffected(
442+
['apps/website/src/app/layout.tsx'],
443+
[{ name: 'website', tags: WEBSITE_TAGS }]
444+
);
445+
assert.equal(scope.examples_ag_ui, false);
446+
});
447+
});
448+
398449
describe('SCOPE_KEYS export', () => {
399-
it('contains the 12 documented scope keys', () => {
450+
it('contains the 13 documented scope keys', () => {
400451
assert.deepEqual(SCOPE_KEYS, [
401452
'library',
402453
'angular_compatibility',
@@ -408,6 +459,7 @@ describe('SCOPE_KEYS export', () => {
408459
'cockpit_deploy_smoke',
409460
'cockpit_e2e',
410461
'examples_chat',
462+
'examples_ag_ui',
411463
'posthog',
412464
'scripts_tests',
413465
]);

scripts/ci-workflow.spec.mjs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,9 @@ describe('CI workflow', () => {
373373
// hardcoded LIBS list that excludes both of these. If they are dropped
374374
// from this run-many their specs stop executing silently.
375375
const cockpitJob = readJobBlock(await readWorkflow(), 'cockpit');
376-
const runMany = cockpitJob.match(/npx nx run-many -t test --projects=(\S+)/);
376+
const runMany = cockpitJob.match(
377+
/npx nx run-many -t test --projects=(\S+)/
378+
);
377379

378380
assert.ok(runMany, 'cockpit job should run tests via nx run-many');
379381

@@ -386,6 +388,29 @@ describe('CI workflow', () => {
386388
}
387389
});
388390

391+
it('scope-gates the examples/ag-ui e2e job and requires it', async () => {
392+
// This job had no `if:` at all — it ran on every push and pull_request,
393+
// including docs-only ones, at a 35-minute timeout. It was also left out
394+
// of required-pr-checks, so an ag-ui e2e failure did not block a merge.
395+
// Both halves are fixed together: gating it without requiring it would
396+
// leave a suite that is skipped often and ignored when it fails.
397+
const workflow = await readWorkflow();
398+
const job = readJobBlock(workflow, 'examples-ag-ui-e2e');
399+
400+
assert.match(job, /needs\.ci-scope\.outputs\.examples_ag_ui == 'true'/);
401+
402+
const required = readJobBlock(workflow, 'required-pr-checks');
403+
assert.match(
404+
required,
405+
/require_scoped \\\n\s*"examples_ag_ui"/,
406+
'required-pr-checks should aggregate the ag-ui e2e result'
407+
);
408+
assert.ok(
409+
readJobNeeds(required).includes('examples-ag-ui-e2e'),
410+
'required-pr-checks should depend on examples-ag-ui-e2e'
411+
);
412+
});
413+
389414
it('lets the cockpit e2e summary inspect CI scope outputs', async () => {
390415
const cockpitE2eSummaryJob = await readCockpitE2eSummaryJob();
391416

@@ -497,6 +522,7 @@ describe('CI workflow', () => {
497522
'cockpit-deploy-smoke',
498523
'examples-chat-smoke',
499524
'examples-chat-e2e',
525+
'examples-ag-ui-e2e',
500526
'cockpit-e2e-summary',
501527
'website-e2e',
502528
'posthog-sync-plan',

0 commit comments

Comments
 (0)