diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4e3407e..35162dfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -235,6 +235,11 @@ jobs: with: python-version: '3.12' - run: npm ci + - name: Cache examples-chat python venv + uses: actions/cache@v4 + with: + path: examples/chat/python/.venv + key: uv-venv-${{ runner.os }}-py3.12-${{ hashFiles('examples/chat/python/uv.lock') }} - working-directory: examples/chat/python run: uv sync - run: npx nx run examples-chat-python:smoke --skip-nx-cache @@ -260,8 +265,20 @@ jobs: with: python-version: '3.12' - run: npm ci + - name: Cache examples-chat python venv + uses: actions/cache@v4 + with: + path: examples/chat/python/.venv + key: uv-venv-${{ runner.os }}-py3.12-${{ hashFiles('examples/chat/python/uv.lock') }} - working-directory: examples/chat/python run: uv sync + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + playwright-${{ runner.os }}- - run: npx playwright install --with-deps chromium - run: npx nx e2e examples-chat-angular --skip-nx-cache -- --shard=${{ matrix.shard }}/4 - name: Upload Playwright trace on failure @@ -333,9 +350,21 @@ jobs: with: python-version: '3.12' - run: npm ci + - name: Cache cap python venv + uses: actions/cache@v4 + with: + path: ${{ matrix.cap.python }}/.venv + key: uv-venv-${{ runner.os }}-py3.12-${{ matrix.cap.python }}-${{ hashFiles(format('{0}/uv.lock', matrix.cap.python)) }} - name: uv sync per-cap python working-directory: ${{ matrix.cap.python }} run: uv sync + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + playwright-${{ runner.os }}- - run: npx playwright install --with-deps chromium - name: nx e2e ${{ matrix.cap.angular }} run: npx nx e2e "${{ matrix.cap.angular }}" --skip-nx-cache @@ -374,6 +403,13 @@ jobs: node-version: 22 cache: npm - run: npm ci + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + playwright-${{ runner.os }}- - run: npx playwright install --with-deps chromium - run: npx nx e2e website --skip-nx-cache @@ -598,6 +634,14 @@ jobs: echo "website=$website_changed" >> "$GITHUB_OUTPUT" echo "cockpit=$cockpit_changed" >> "$GITHUB_OUTPUT" + - name: Cache Playwright browsers + if: steps.affected.outputs.website == 'true' + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + playwright-${{ runner.os }}- - name: Install Playwright browsers if: steps.affected.outputs.website == 'true' run: npx playwright install --with-deps chromium @@ -786,6 +830,13 @@ jobs: run: npx tsx scripts/verify-shared-deployment.ts env: LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + restore-keys: | + playwright-${{ runner.os }}- - run: npx playwright install --with-deps chromium - name: Run production smoke tests run: npx playwright test apps/cockpit/e2e/production-smoke.spec.ts --reporter=list diff --git a/scripts/ci-scope.mjs b/scripts/ci-scope.mjs index 5dab8d1e..65339be3 100644 --- a/scripts/ci-scope.mjs +++ b/scripts/ci-scope.mjs @@ -18,9 +18,21 @@ const GLOBAL_CI_FILES = new Set([ 'nx.json', 'tsconfig.json', 'tsconfig.base.json', +]); + +/** Files whose changes only affect linting (not builds, not e2e tests). + * These flip the scopes that actually run `nx lint` (library, cockpit, + * website, examples_chat) but do NOT trigger e2e/smoke/deploy jobs. + * Avoids the ~50 CI-minute spike from a one-line lint-config tweak + * re-running the 24-cap cockpit-e2e matrix. */ +const LINT_ONLY_FILES = new Set([ 'eslint.config.mjs', ]); +/** Subset of SCOPE_KEYS that own jobs running `nx lint`. Flipped true + * when a LINT_ONLY_FILES entry changes. */ +const LINT_SCOPE_KEYS = ['library', 'cockpit', 'website', 'examples_chat']; + export function emptyScope() { return Object.fromEntries(SCOPE_KEYS.map((k) => [k, false])); } @@ -58,6 +70,9 @@ export function classifyFromAffected(changedFiles, affectedProjects) { if (SCOPE_KEYS.includes(key)) scope[key] = true; } } + if (changedFiles.some((f) => LINT_ONLY_FILES.has(f))) { + for (const key of LINT_SCOPE_KEYS) scope[key] = true; + } return scope; } diff --git a/scripts/ci-scope.spec.mjs b/scripts/ci-scope.spec.mjs index a1a2200a..47ece4b3 100644 --- a/scripts/ci-scope.spec.mjs +++ b/scripts/ci-scope.spec.mjs @@ -36,6 +36,36 @@ describe('classifyFromAffected — short-circuit', () => { }); }); +describe('classifyFromAffected — lint-only files', () => { + it('eslint.config.mjs flips only lint-running scopes, NOT e2e/smoke/deploy', () => { + const scope = classifyFromAffected(['eslint.config.mjs'], []); + // Lint-running scopes: true + assert.equal(scope.library, true); + assert.equal(scope.cockpit, true); + assert.equal(scope.website, true); + assert.equal(scope.examples_chat, true); + // E2e / smoke / deploy / secret / posthog scopes: false + assert.equal(scope.website_e2e, false); + assert.equal(scope.cockpit_e2e, false); + assert.equal(scope.cockpit_smoke, false); + assert.equal(scope.cockpit_examples, false); + assert.equal(scope.cockpit_secret, false); + assert.equal(scope.cockpit_deploy_smoke, false); + assert.equal(scope.posthog, false); + }); + + it('eslint.config.mjs alongside an affected project still ORs in the project scopes', () => { + const scope = classifyFromAffected( + ['eslint.config.mjs', 'cockpit/chat/messages/python/src/graph.py'], + [{ name: 'cockpit-chat-messages-python', tags: ['scope:cockpit-e2e', 'scope:cockpit-examples', 'scope:cockpit-smoke'] }], + ); + assert.equal(scope.library, true); + assert.equal(scope.cockpit_e2e, true); + assert.equal(scope.cockpit_examples, true); + assert.equal(scope.cockpit_smoke, true); + }); +}); + describe('classifyFromAffected — publishable lib broadcast', () => { it('publishable lib triggers library + website + website_e2e + cockpit_* + examples_chat', () => { const scope = classifyFromAffected(['libs/chat/src/foo.ts'], [