From 439e1f742385dcd834e504b8e821b0d9ba2f388a Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Mar 2026 21:55:40 +0100 Subject: [PATCH 01/10] ci: skip unnecessary jobs based on change detection Add a `detect-changes` job that classifies files into categories and conditionally skips expensive CI jobs: - docs-only (.md, .txt, docs/, .claude/, LICENSE): skip all jobs - config/tooling-only (.eslintrc, .prettierrc, etc.): run lint only - CI-only (.github/): run actionlint only - code changes: run everything as before The `all-jobs-complete` gate is updated to verify the right jobs succeeded based on what actually changed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 132 +++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ed4a5ec3c..412bcbc108 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,11 +26,85 @@ jobs: if: github.event_name == 'merge_group' uses: MetaMask/github-tools/.github/actions/check-skip-merge-queue@v1 + detect-changes: + name: Detect changes + runs-on: ubuntu-latest + outputs: + has-code: ${{ steps.changes.outputs.has-code }} + has-ci: ${{ steps.changes.outputs.has-ci }} + has-lint-targets: ${{ steps.changes.outputs.has-lint-targets }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Detect change categories + id: changes + run: | + if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then + # On pushes to main, always run everything + echo "has-code=true" >> "$GITHUB_OUTPUT" + echo "has-ci=true" >> "$GITHUB_OUTPUT" + echo "has-lint-targets=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BASE="${{ github.event.pull_request.base.sha }}" + elif [[ "${{ github.event_name }}" == "merge_group" ]]; then + BASE="${{ github.event.merge_group.base_sha }}" + else + BASE="${{ github.event.before }}" + fi + + FILES=$(git diff --name-only "$BASE" HEAD) + + HAS_CODE=false + HAS_CI=false + HAS_LINT_TARGETS=false + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + + case "$file" in + # CI / workflow files + .github/workflows/*|.github/actions/*) + HAS_CI=true + ;; + # Documentation (skip everything) + *.md|*.txt|docs/*|.claude/*|LICENSE|AGENTS.md) + ;; + # Config/tooling (lint-worthy but not code) + .eslintrc*|.prettierrc*|.editorconfig|.gitignore|.gitattributes|.nvmrc|.yarnrc*) + HAS_LINT_TARGETS=true + ;; + # Everything else is code (source, tests, package.json, tsconfig, lockfile, etc.) + *) + HAS_CODE=true + HAS_LINT_TARGETS=true + ;; + esac + done <<< "$FILES" + + echo "has-code=$HAS_CODE" >> "$GITHUB_OUTPUT" + echo "has-ci=$HAS_CI" >> "$GITHUB_OUTPUT" + echo "has-lint-targets=$HAS_LINT_TARGETS" >> "$GITHUB_OUTPUT" + + echo "## Change detection results" >> "$GITHUB_STEP_SUMMARY" + echo "- has-code: $HAS_CODE" >> "$GITHUB_STEP_SUMMARY" + echo "- has-ci: $HAS_CI" >> "$GITHUB_STEP_SUMMARY" + echo "- has-lint-targets: $HAS_LINT_TARGETS" >> "$GITHUB_STEP_SUMMARY" + echo "### Changed files" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + echo "$FILES" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + check-workflows: name: Check workflows runs-on: ubuntu-latest - needs: check-skip-merge-queue - if: github.event_name != 'merge_group' || needs.check-skip-merge-queue.outputs.skip-merge-queue != 'true' + needs: [check-skip-merge-queue, detect-changes] + if: | + (github.event_name != 'merge_group' || needs.check-skip-merge-queue.outputs.skip-merge-queue != 'true') + && (needs.detect-changes.outputs.has-ci == 'true' || needs.detect-changes.outputs.has-code == 'true') steps: - uses: actions/checkout@v6 - name: Download actionlint @@ -43,7 +117,8 @@ jobs: analyse-code: name: Code scanner - needs: check-workflows + needs: [check-workflows, detect-changes] + if: needs.detect-changes.outputs.has-code == 'true' uses: ./.github/workflows/security-code-scanner.yml permissions: actions: read @@ -55,9 +130,33 @@ jobs: lint-build-test: name: Lint, build, and test - needs: check-workflows + needs: [check-workflows, detect-changes] + if: needs.detect-changes.outputs.has-code == 'true' uses: ./.github/workflows/lint-build-test.yml + lint-only: + name: Lint (no code changes) + runs-on: ubuntu-latest + needs: [detect-changes] + if: needs.detect-changes.outputs.has-code != 'true' && needs.detect-changes.outputs.has-lint-targets == 'true' + strategy: + matrix: + node-version: [24.x] + steps: + - name: Checkout and setup environment + uses: MetaMask/action-checkout-and-setup@v3 + with: + is-high-risk-environment: false + node-version: ${{ matrix.node-version }} + - run: yarn lint + - name: Require clean working directory + shell: bash + run: | + if ! git diff --exit-code; then + echo "Working tree dirty at end of job" + exit 1 + fi + coverage-report: name: Coverage report needs: lint-build-test @@ -107,15 +206,38 @@ jobs: all-jobs-complete: name: All jobs complete runs-on: ubuntu-latest + if: ${{ always() }} needs: + - detect-changes - analyse-code - lint-build-test + - lint-only outputs: passed: ${{ steps.set-output.outputs.passed }} steps: - name: Set passed output id: set-output - run: echo "passed=true" >> "$GITHUB_OUTPUT" + run: | + HAS_CODE="${{ needs.detect-changes.outputs.has-code }}" + HAS_LINT="${{ needs.detect-changes.outputs.has-lint-targets }}" + + # If code changed, analyse-code and lint-build-test must have succeeded + if [[ "$HAS_CODE" == "true" ]]; then + if [[ "${{ needs.analyse-code.result }}" != "success" || "${{ needs.lint-build-test.result }}" != "success" ]]; then + echo "Code jobs did not succeed" + exit 1 + fi + fi + + # If only lint targets changed (no code), lint-only must have succeeded + if [[ "$HAS_CODE" != "true" && "$HAS_LINT" == "true" ]]; then + if [[ "${{ needs.lint-only.result }}" != "success" ]]; then + echo "Lint-only job did not succeed" + exit 1 + fi + fi + + echo "passed=true" >> "$GITHUB_OUTPUT" all-jobs-pass: name: All jobs pass From 15f62119325f3b99bcbd6dfb3b334c6111a4489d Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Mar 2026 21:59:27 +0100 Subject: [PATCH 02/10] fix(ci): address shellcheck warnings in detect-changes script - Remove redundant AGENTS.md pattern (already matched by *.md) - Group redirects to $GITHUB_OUTPUT and $GITHUB_STEP_SUMMARY Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 412bcbc108..5ec4d767a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,9 +42,11 @@ jobs: run: | if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then # On pushes to main, always run everything - echo "has-code=true" >> "$GITHUB_OUTPUT" - echo "has-ci=true" >> "$GITHUB_OUTPUT" - echo "has-lint-targets=true" >> "$GITHUB_OUTPUT" + { + echo "has-code=true" + echo "has-ci=true" + echo "has-lint-targets=true" + } >> "$GITHUB_OUTPUT" exit 0 fi @@ -71,7 +73,7 @@ jobs: HAS_CI=true ;; # Documentation (skip everything) - *.md|*.txt|docs/*|.claude/*|LICENSE|AGENTS.md) + *.md|*.txt|docs/*|.claude/*|LICENSE) ;; # Config/tooling (lint-worthy but not code) .eslintrc*|.prettierrc*|.editorconfig|.gitignore|.gitattributes|.nvmrc|.yarnrc*) @@ -85,18 +87,22 @@ jobs: esac done <<< "$FILES" - echo "has-code=$HAS_CODE" >> "$GITHUB_OUTPUT" - echo "has-ci=$HAS_CI" >> "$GITHUB_OUTPUT" - echo "has-lint-targets=$HAS_LINT_TARGETS" >> "$GITHUB_OUTPUT" + { + echo "has-code=$HAS_CODE" + echo "has-ci=$HAS_CI" + echo "has-lint-targets=$HAS_LINT_TARGETS" + } >> "$GITHUB_OUTPUT" - echo "## Change detection results" >> "$GITHUB_STEP_SUMMARY" - echo "- has-code: $HAS_CODE" >> "$GITHUB_STEP_SUMMARY" - echo "- has-ci: $HAS_CI" >> "$GITHUB_STEP_SUMMARY" - echo "- has-lint-targets: $HAS_LINT_TARGETS" >> "$GITHUB_STEP_SUMMARY" - echo "### Changed files" >> "$GITHUB_STEP_SUMMARY" - echo '```' >> "$GITHUB_STEP_SUMMARY" - echo "$FILES" >> "$GITHUB_STEP_SUMMARY" - echo '```' >> "$GITHUB_STEP_SUMMARY" + { + echo "## Change detection results" + echo "- has-code: $HAS_CODE" + echo "- has-ci: $HAS_CI" + echo "- has-lint-targets: $HAS_LINT_TARGETS" + echo "### Changed files" + echo '```' + echo "$FILES" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" check-workflows: name: Check workflows From 91a761fdbdd23c689c5f2e36c9ae09f30599937d Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Mar 2026 22:08:55 +0100 Subject: [PATCH 03/10] fix(ci): verify check-workflows and detect-changes in merge gate Address two gaps in the all-jobs-complete gate: - Fail if detect-changes itself fails (prevents empty outputs from letting the gate pass with zero validation) - Verify check-workflows succeeded for CI-only PRs (has-ci=true) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ec4d767a6..85c659c0f6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -215,6 +215,7 @@ jobs: if: ${{ always() }} needs: - detect-changes + - check-workflows - analyse-code - lint-build-test - lint-only @@ -224,7 +225,14 @@ jobs: - name: Set passed output id: set-output run: | + # detect-changes must always succeed — without it we can't trust any outputs + if [[ "${{ needs.detect-changes.result }}" != "success" ]]; then + echo "detect-changes did not succeed" + exit 1 + fi + HAS_CODE="${{ needs.detect-changes.outputs.has-code }}" + HAS_CI="${{ needs.detect-changes.outputs.has-ci }}" HAS_LINT="${{ needs.detect-changes.outputs.has-lint-targets }}" # If code changed, analyse-code and lint-build-test must have succeeded @@ -235,6 +243,14 @@ jobs: fi fi + # If CI files changed, check-workflows must have succeeded + if [[ "$HAS_CI" == "true" ]]; then + if [[ "${{ needs.check-workflows.result }}" != "success" ]]; then + echo "check-workflows did not succeed" + exit 1 + fi + fi + # If only lint targets changed (no code), lint-only must have succeeded if [[ "$HAS_CODE" != "true" && "$HAS_LINT" == "true" ]]; then if [[ "${{ needs.lint-only.result }}" != "success" ]]; then From cc6aff511167033e39659d8958635588ed41158e Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Mar 2026 22:21:07 +0100 Subject: [PATCH 04/10] fix(ci): address review feedback and correctness issues - Docs files (.md, .txt, etc.) now set HAS_LINT_TARGETS=true since prettier runs on them via lint:misc (rekmarks review) - analyse-code and lint-build-test handle skipped check-workflows via explicit result guard to prevent cascade-skip issues - Gate now requires check-workflows for code changes too (not just CI) - Dead else branch now fails explicitly instead of using github.event.before Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 85c659c0f6..6057e8c890 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,7 +55,8 @@ jobs: elif [[ "${{ github.event_name }}" == "merge_group" ]]; then BASE="${{ github.event.merge_group.base_sha }}" else - BASE="${{ github.event.before }}" + echo "::error::Unexpected event type: ${{ github.event_name }}" + exit 1 fi FILES=$(git diff --name-only "$BASE" HEAD) @@ -72,8 +73,9 @@ jobs: .github/workflows/*|.github/actions/*) HAS_CI=true ;; - # Documentation (skip everything) + # Documentation (lint-worthy for prettier, but not code) *.md|*.txt|docs/*|.claude/*|LICENSE) + HAS_LINT_TARGETS=true ;; # Config/tooling (lint-worthy but not code) .eslintrc*|.prettierrc*|.editorconfig|.gitignore|.gitattributes|.nvmrc|.yarnrc*) @@ -124,7 +126,9 @@ jobs: analyse-code: name: Code scanner needs: [check-workflows, detect-changes] - if: needs.detect-changes.outputs.has-code == 'true' + if: | + needs.detect-changes.outputs.has-code == 'true' + && (needs.check-workflows.result == 'success' || needs.check-workflows.result == 'skipped') uses: ./.github/workflows/security-code-scanner.yml permissions: actions: read @@ -137,7 +141,9 @@ jobs: lint-build-test: name: Lint, build, and test needs: [check-workflows, detect-changes] - if: needs.detect-changes.outputs.has-code == 'true' + if: | + needs.detect-changes.outputs.has-code == 'true' + && (needs.check-workflows.result == 'success' || needs.check-workflows.result == 'skipped') uses: ./.github/workflows/lint-build-test.yml lint-only: @@ -243,8 +249,8 @@ jobs: fi fi - # If CI files changed, check-workflows must have succeeded - if [[ "$HAS_CI" == "true" ]]; then + # check-workflows runs for both CI and code changes; require it when either is present + if [[ "$HAS_CI" == "true" || "$HAS_CODE" == "true" ]]; then if [[ "${{ needs.check-workflows.result }}" != "success" ]]; then echo "check-workflows did not succeed" exit 1 From 09af5ff346349516234a2a03b8831f4de32775f5 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Mar 2026 22:46:52 +0100 Subject: [PATCH 05/10] fix(ci): use always() to properly handle skipped check-workflows The implicit success() in GitHub Actions if: conditions short-circuits when an upstream job is skipped, making the '== skipped' check dead code. Use always() with '!= failure' instead so analyse-code and lint-build-test can run when check-workflows is legitimately skipped. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6057e8c890..0d00815bcf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -127,8 +127,9 @@ jobs: name: Code scanner needs: [check-workflows, detect-changes] if: | - needs.detect-changes.outputs.has-code == 'true' - && (needs.check-workflows.result == 'success' || needs.check-workflows.result == 'skipped') + always() + && needs.detect-changes.outputs.has-code == 'true' + && needs.check-workflows.result != 'failure' uses: ./.github/workflows/security-code-scanner.yml permissions: actions: read @@ -142,8 +143,9 @@ jobs: name: Lint, build, and test needs: [check-workflows, detect-changes] if: | - needs.detect-changes.outputs.has-code == 'true' - && (needs.check-workflows.result == 'success' || needs.check-workflows.result == 'skipped') + always() + && needs.detect-changes.outputs.has-code == 'true' + && needs.check-workflows.result != 'failure' uses: ./.github/workflows/lint-build-test.yml lint-only: From 98d1d7d5d1c7aa10e39e5499bca8b9b676bb7b1b Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Mar 2026 22:54:40 +0100 Subject: [PATCH 06/10] fix(ci): set HAS_LINT_TARGETS for CI files CI workflow files (.yml) go through prettier via lint:misc, so they need HAS_LINT_TARGETS=true to trigger the lint-only job. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0d00815bcf..7ba7104b73 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,9 +69,10 @@ jobs: [[ -z "$file" ]] && continue case "$file" in - # CI / workflow files + # CI / workflow files (also lint-worthy for prettier on .yml) .github/workflows/*|.github/actions/*) HAS_CI=true + HAS_LINT_TARGETS=true ;; # Documentation (lint-worthy for prettier, but not code) *.md|*.txt|docs/*|.claude/*|LICENSE) From 0f3cdede6002adced22264b8bb107c991470a423 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Mar 2026 23:16:19 +0100 Subject: [PATCH 07/10] fix(ci): respect merge queue skip in always() conditions The always() pattern on analyse-code and lint-build-test bypassed the merge queue skip optimization. Add check-skip-merge-queue to their needs and condition so they don't run when the merge queue determines the PR is already up-to-date. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ba7104b73..c5b6106082 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -126,9 +126,10 @@ jobs: analyse-code: name: Code scanner - needs: [check-workflows, detect-changes] + needs: [check-skip-merge-queue, check-workflows, detect-changes] if: | always() + && needs.check-skip-merge-queue.outputs.skip-merge-queue != 'true' && needs.detect-changes.outputs.has-code == 'true' && needs.check-workflows.result != 'failure' uses: ./.github/workflows/security-code-scanner.yml @@ -142,9 +143,10 @@ jobs: lint-build-test: name: Lint, build, and test - needs: [check-workflows, detect-changes] + needs: [check-skip-merge-queue, check-workflows, detect-changes] if: | always() + && needs.check-skip-merge-queue.outputs.skip-merge-queue != 'true' && needs.detect-changes.outputs.has-code == 'true' && needs.check-workflows.result != 'failure' uses: ./.github/workflows/lint-build-test.yml From c9901ec1a330159d08fea86d104e71733b302ba9 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 26 Mar 2026 09:28:27 +0100 Subject: [PATCH 08/10] remove claude from doc only pattern --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c5b6106082..b8d3ec558c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -75,7 +75,7 @@ jobs: HAS_LINT_TARGETS=true ;; # Documentation (lint-worthy for prettier, but not code) - *.md|*.txt|docs/*|.claude/*|LICENSE) + *.md|*.txt|docs/*|LICENSE) HAS_LINT_TARGETS=true ;; # Config/tooling (lint-worthy but not code) From 6e8d0f37f3c6a61028ec784d899df47c163e8e71 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 26 Mar 2026 09:58:16 +0100 Subject: [PATCH 09/10] fix(ci): treat custom actions as code to run full test pipeline Changes to .github/actions/* (e.g. playwright-install) were classified as CI-only, skipping build/test validation. Split into its own case that also sets HAS_CODE=true so the lint-build-test pipeline runs. Also removes .claude/* from the doc-only pattern so it falls through to the code catch-all, future-proofing for executable skills/plugins. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8d3ec558c..3d3e3c562e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -69,11 +69,17 @@ jobs: [[ -z "$file" ]] && continue case "$file" in - # CI / workflow files (also lint-worthy for prettier on .yml) - .github/workflows/*|.github/actions/*) + # CI workflow files (also lint-worthy for prettier on .yml) + .github/workflows/*) HAS_CI=true HAS_LINT_TARGETS=true ;; + # Custom actions are test infrastructure — treat as code + .github/actions/*) + HAS_CI=true + HAS_CODE=true + HAS_LINT_TARGETS=true + ;; # Documentation (lint-worthy for prettier, but not code) *.md|*.txt|docs/*|LICENSE) HAS_LINT_TARGETS=true From 6ac945e6ab632f11d2482ac92a2ba4f3e7f69b8b Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 26 Mar 2026 10:11:40 +0100 Subject: [PATCH 10/10] fix(ci): match actual license filenames in doc-only pattern LICENSE only matched a bare LICENSE file, but the repo uses LICENSE.APACHE2 and LICENSE.MIT. Change to LICENSE* so license-only changes are correctly classified as documentation, not code. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d3e3c562e..b6682d29fd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -81,7 +81,7 @@ jobs: HAS_LINT_TARGETS=true ;; # Documentation (lint-worthy for prettier, but not code) - *.md|*.txt|docs/*|LICENSE) + *.md|*.txt|docs/*|LICENSE*) HAS_LINT_TARGETS=true ;; # Config/tooling (lint-worthy but not code)