Skip to content

Commit e454935

Browse files
bloveclaude
andauthored
ci: scope-gate the examples/ag-ui e2e job and make it block merges (#944)
`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 cd502f1 commit e454935

5 files changed

Lines changed: 89 additions & 5 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 }}
@@ -399,10 +400,7 @@ jobs:
399400
examples-ag-ui-e2e:
400401
name: 'examples/ag-ui — e2e'
401402
needs: ci-scope
402-
# No dedicated ci-scope output exists for examples/ag-ui yet, so this job
403-
# runs on every push and pull_request rather than being scope-gated. It is
404-
# deliberately NOT wired into the require_scoped aggregation in
405-
# `required-pr-checks` (see Step 1 report) to avoid breaking that logic.
403+
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_ag_ui == 'true'
406404
runs-on: ubuntu-latest
407405
timeout-minutes: 35
408406
steps:
@@ -581,6 +579,7 @@ jobs:
581579
- cockpit-deploy-smoke
582580
- examples-chat-smoke
583581
- examples-chat-e2e
582+
- examples-ag-ui-e2e
584583
- cockpit-e2e-summary
585584
- website-e2e
586585
- posthog-sync-plan
@@ -600,6 +599,7 @@ jobs:
600599
RESULT_COCKPIT_DEPLOY_SMOKE: ${{ needs.cockpit-deploy-smoke.result }}
601600
RESULT_EXAMPLES_CHAT_SMOKE: ${{ needs.examples-chat-smoke.result }}
602601
RESULT_EXAMPLES_CHAT_E2E: ${{ needs.examples-chat-e2e.result }}
602+
RESULT_EXAMPLES_AG_UI_E2E: ${{ needs.examples-ag-ui-e2e.result }}
603603
RESULT_COCKPIT_E2E: ${{ needs.cockpit-e2e-summary.result }}
604604
RESULT_WEBSITE_E2E: ${{ needs.website-e2e.result }}
605605
RESULT_POSTHOG: ${{ needs.posthog-sync-plan.result }}
@@ -612,6 +612,7 @@ jobs:
612612
SCOPE_COCKPIT_SMOKE: ${{ needs.ci-scope.outputs.cockpit_smoke }}
613613
SCOPE_COCKPIT_DEPLOY_SMOKE: ${{ needs.ci-scope.outputs.cockpit_deploy_smoke }}
614614
SCOPE_EXAMPLES_CHAT: ${{ needs.ci-scope.outputs.examples_chat }}
615+
SCOPE_EXAMPLES_AG_UI: ${{ needs.ci-scope.outputs.examples_ag_ui }}
615616
SCOPE_COCKPIT_E2E: ${{ needs.ci-scope.outputs.cockpit_e2e }}
616617
SCOPE_WEBSITE_E2E: ${{ needs.ci-scope.outputs.website_e2e }}
617618
SCOPE_POSTHOG: ${{ needs.ci-scope.outputs.posthog }}
@@ -665,6 +666,11 @@ jobs:
665666
require_scoped "cockpit_deploy_smoke" "Cockpit — deploy smoke dry-run" "$RESULT_COCKPIT_DEPLOY_SMOKE" "$SCOPE_COCKPIT_DEPLOY_SMOKE"
666667
require_scoped "examples_chat" "examples/chat — python smoke" "$RESULT_EXAMPLES_CHAT_SMOKE" "$SCOPE_EXAMPLES_CHAT"
667668
require_scoped "examples_chat" "examples/chat — e2e" "$RESULT_EXAMPLES_CHAT_E2E" "$SCOPE_EXAMPLES_CHAT"
669+
require_scoped \
670+
"examples_ag_ui" \
671+
"examples/ag-ui — e2e" \
672+
"$RESULT_EXAMPLES_AG_UI_E2E" \
673+
"$SCOPE_EXAMPLES_AG_UI"
668674
require_scoped "cockpit_e2e" "Cockpit — e2e" "$RESULT_COCKPIT_E2E" "$SCOPE_COCKPIT_E2E"
669675
require_scoped "website_e2e" "Website — e2e" "$RESULT_WEBSITE_E2E" "$SCOPE_WEBSITE_E2E"
670676
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,29 @@ describe('CI workflow', () => {
409409
);
410410
});
411411

412+
it('scope-gates the examples/ag-ui e2e job and requires it', async () => {
413+
// This job had no `if:` at all — it ran on every push and pull_request,
414+
// including docs-only ones, at a 35-minute timeout. It was also left out
415+
// of required-pr-checks, so an ag-ui e2e failure did not block a merge.
416+
// Both halves are fixed together: gating it without requiring it would
417+
// leave a suite that is skipped often and ignored when it fails.
418+
const workflow = await readWorkflow();
419+
const job = readJobBlock(workflow, 'examples-ag-ui-e2e');
420+
421+
assert.match(job, /needs\.ci-scope\.outputs\.examples_ag_ui == 'true'/);
422+
423+
const required = readJobBlock(workflow, 'required-pr-checks');
424+
assert.match(
425+
required,
426+
/require_scoped \\\n\s*"examples_ag_ui"/,
427+
'required-pr-checks should aggregate the ag-ui e2e result'
428+
);
429+
assert.ok(
430+
readJobNeeds(required).includes('examples-ag-ui-e2e'),
431+
'required-pr-checks should depend on examples-ag-ui-e2e'
432+
);
433+
});
434+
412435
it('lets the cockpit e2e summary inspect CI scope outputs', async () => {
413436
const cockpitE2eSummaryJob = await readCockpitE2eSummaryJob();
414437

@@ -520,6 +543,7 @@ describe('CI workflow', () => {
520543
'cockpit-deploy-smoke',
521544
'examples-chat-smoke',
522545
'examples-chat-e2e',
546+
'examples-ag-ui-e2e',
523547
'cockpit-e2e-summary',
524548
'website-e2e',
525549
'posthog-sync-plan',

0 commit comments

Comments
 (0)