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
14 changes: 10 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
cockpit_smoke: ${{ steps.scope.outputs.cockpit_smoke }}
cockpit_deploy_smoke: ${{ steps.scope.outputs.cockpit_deploy_smoke }}
examples_chat: ${{ steps.scope.outputs.examples_chat }}
examples_ag_ui: ${{ steps.scope.outputs.examples_ag_ui }}
cockpit_e2e: ${{ steps.scope.outputs.cockpit_e2e }}
website_e2e: ${{ steps.scope.outputs.website_e2e }}
posthog: ${{ steps.scope.outputs.posthog }}
Expand Down Expand Up @@ -399,10 +400,7 @@ jobs:
examples-ag-ui-e2e:
name: 'examples/ag-ui — e2e'
needs: ci-scope
# No dedicated ci-scope output exists for examples/ag-ui yet, so this job
# runs on every push and pull_request rather than being scope-gated. It is
# deliberately NOT wired into the require_scoped aggregation in
# `required-pr-checks` (see Step 1 report) to avoid breaking that logic.
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_ag_ui == 'true'
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
Expand Down Expand Up @@ -581,6 +579,7 @@ jobs:
- cockpit-deploy-smoke
- examples-chat-smoke
- examples-chat-e2e
- examples-ag-ui-e2e
- cockpit-e2e-summary
- website-e2e
- posthog-sync-plan
Expand All @@ -600,6 +599,7 @@ jobs:
RESULT_COCKPIT_DEPLOY_SMOKE: ${{ needs.cockpit-deploy-smoke.result }}
RESULT_EXAMPLES_CHAT_SMOKE: ${{ needs.examples-chat-smoke.result }}
RESULT_EXAMPLES_CHAT_E2E: ${{ needs.examples-chat-e2e.result }}
RESULT_EXAMPLES_AG_UI_E2E: ${{ needs.examples-ag-ui-e2e.result }}
RESULT_COCKPIT_E2E: ${{ needs.cockpit-e2e-summary.result }}
RESULT_WEBSITE_E2E: ${{ needs.website-e2e.result }}
RESULT_POSTHOG: ${{ needs.posthog-sync-plan.result }}
Expand All @@ -612,6 +612,7 @@ jobs:
SCOPE_COCKPIT_SMOKE: ${{ needs.ci-scope.outputs.cockpit_smoke }}
SCOPE_COCKPIT_DEPLOY_SMOKE: ${{ needs.ci-scope.outputs.cockpit_deploy_smoke }}
SCOPE_EXAMPLES_CHAT: ${{ needs.ci-scope.outputs.examples_chat }}
SCOPE_EXAMPLES_AG_UI: ${{ needs.ci-scope.outputs.examples_ag_ui }}
SCOPE_COCKPIT_E2E: ${{ needs.ci-scope.outputs.cockpit_e2e }}
SCOPE_WEBSITE_E2E: ${{ needs.ci-scope.outputs.website_e2e }}
SCOPE_POSTHOG: ${{ needs.ci-scope.outputs.posthog }}
Expand Down Expand Up @@ -665,6 +666,11 @@ jobs:
require_scoped "cockpit_deploy_smoke" "Cockpit — deploy smoke dry-run" "$RESULT_COCKPIT_DEPLOY_SMOKE" "$SCOPE_COCKPIT_DEPLOY_SMOKE"
require_scoped "examples_chat" "examples/chat — python smoke" "$RESULT_EXAMPLES_CHAT_SMOKE" "$SCOPE_EXAMPLES_CHAT"
require_scoped "examples_chat" "examples/chat — e2e" "$RESULT_EXAMPLES_CHAT_E2E" "$SCOPE_EXAMPLES_CHAT"
require_scoped \
"examples_ag_ui" \
"examples/ag-ui — e2e" \
"$RESULT_EXAMPLES_AG_UI_E2E" \
"$SCOPE_EXAMPLES_AG_UI"
require_scoped "cockpit_e2e" "Cockpit — e2e" "$RESULT_COCKPIT_E2E" "$SCOPE_COCKPIT_E2E"
require_scoped "website_e2e" "Website — e2e" "$RESULT_WEBSITE_E2E" "$SCOPE_WEBSITE_E2E"
require_scoped "posthog" "PostHog — dashboards-as-code drift check" "$RESULT_POSTHOG" "$SCOPE_POSTHOG"
Expand Down
1 change: 1 addition & 0 deletions examples/ag-ui/python/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "examples-ag-ui-python",
"tags": ["scope:examples-ag-ui"],
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "examples/ag-ui/python/src",
Expand Down
1 change: 1 addition & 0 deletions scripts/ci-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const SCOPE_KEYS = [
'cockpit_deploy_smoke',
'cockpit_e2e',
'examples_chat',
'examples_ag_ui',
'posthog',
'scripts_tests',
];
Expand Down
54 changes: 53 additions & 1 deletion scripts/ci-scope.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,59 @@ describe('classifyFromAffected — tag isolation', () => {
});
});

describe('classifyFromAffected — examples/ag-ui', () => {
// examples/ag-ui/{,angular/}project.json already carried
// `scope:examples-ag-ui`, but `examples_ag_ui` was missing from SCOPE_KEYS,
// so classifyFromAffected read the tag and dropped it on the floor. The
// examples-ag-ui-e2e job therefore had no scope to gate on and ran on every
// PR — a 35-minute-timeout Playwright job on docs-only changes.
it('an affected examples/ag-ui project selects examples_ag_ui only', async () => {
const project = JSON.parse(
await readFile('examples/ag-ui/angular/project.json', 'utf8')
);
const scope = classifyFromAffected(
['examples/ag-ui/angular/src/app/app.ts'],
[{ name: project.name, tags: project.tags }]
);

assert.equal(scope.examples_ag_ui, true);
assert.equal(scope.examples_chat, false);
assert.equal(scope.cockpit_e2e, false);
assert.equal(scope.website, false);
});

it('the python backend the e2e job uv-syncs owns the scope too', async () => {
// The examples-ag-ui-e2e job runs `uv sync` in examples/ag-ui/python and
// then drives the Angular app against it. An untagged backend would mean
// a python-only change silently skips the suite — scoping that buys speed
// by dropping coverage.
const project = JSON.parse(
await readFile('examples/ag-ui/python/project.json', 'utf8')
);

assert.ok(
project.tags?.includes('scope:examples-ag-ui'),
'examples/ag-ui/python must select the ag-ui e2e suite'
);

const scope = classifyFromAffected(
['examples/ag-ui/python/src/agent.py'],
[{ name: project.name, tags: project.tags }]
);
assert.equal(scope.examples_ag_ui, true);
});

it('a website-only change leaves examples_ag_ui false', () => {
const scope = classifyFromAffected(
['apps/website/src/app/layout.tsx'],
[{ name: 'website', tags: WEBSITE_TAGS }]
);
assert.equal(scope.examples_ag_ui, false);
});
});

describe('SCOPE_KEYS export', () => {
it('contains the 12 documented scope keys', () => {
it('contains the 13 documented scope keys', () => {
assert.deepEqual(SCOPE_KEYS, [
'library',
'angular_compatibility',
Expand All @@ -408,6 +459,7 @@ describe('SCOPE_KEYS export', () => {
'cockpit_deploy_smoke',
'cockpit_e2e',
'examples_chat',
'examples_ag_ui',
'posthog',
'scripts_tests',
]);
Expand Down
24 changes: 24 additions & 0 deletions scripts/ci-workflow.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,29 @@ describe('CI workflow', () => {
);
});

it('scope-gates the examples/ag-ui e2e job and requires it', async () => {
// This job had no `if:` at all — it ran on every push and pull_request,
// including docs-only ones, at a 35-minute timeout. It was also left out
// of required-pr-checks, so an ag-ui e2e failure did not block a merge.
// Both halves are fixed together: gating it without requiring it would
// leave a suite that is skipped often and ignored when it fails.
const workflow = await readWorkflow();
const job = readJobBlock(workflow, 'examples-ag-ui-e2e');

assert.match(job, /needs\.ci-scope\.outputs\.examples_ag_ui == 'true'/);

const required = readJobBlock(workflow, 'required-pr-checks');
assert.match(
required,
/require_scoped \\\n\s*"examples_ag_ui"/,
'required-pr-checks should aggregate the ag-ui e2e result'
);
assert.ok(
readJobNeeds(required).includes('examples-ag-ui-e2e'),
'required-pr-checks should depend on examples-ag-ui-e2e'
);
});

it('lets the cockpit e2e summary inspect CI scope outputs', async () => {
const cockpitE2eSummaryJob = await readCockpitE2eSummaryJob();

Expand Down Expand Up @@ -520,6 +543,7 @@ describe('CI workflow', () => {
'cockpit-deploy-smoke',
'examples-chat-smoke',
'examples-chat-e2e',
'examples-ag-ui-e2e',
'cockpit-e2e-summary',
'website-e2e',
'posthog-sync-plan',
Expand Down
Loading