Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c7b83b5
Build only changed packages and dependants in CI
Mrtenz Jul 2, 2026
9e83091
Fix shallow clone preventing merge-base resolution
Mrtenz Jul 2, 2026
9355d7b
Unshallow the checkout before computing the merge base
Mrtenz Jul 2, 2026
dd473c6
Use GitHub Compare API to fetch exact merge base
Mrtenz Jul 2, 2026
3d2e757
Unshallow with blobless filter to confirm merge base ancestry
Mrtenz Jul 2, 2026
5626cb5
Restrict unshallow fetch to HEAD only
Mrtenz Jul 2, 2026
8512b54
Use ts-bridge with partial tsconfig for partial builds
Mrtenz Jul 3, 2026
fe987dd
Write partial tsconfig to workspace root so relative paths resolve
Mrtenz Jul 3, 2026
94006aa
Fix incremental build detecting wrong changed files and missing depen…
Mrtenz Jul 3, 2026
37d7f09
Extend partial builds to tests and changelog validation
Mrtenz Jul 3, 2026
72b9e97
Remove fallback to all packages when no packages changed
Mrtenz Jul 3, 2026
22d0c65
Skip matrix jobs when no packages changed
Mrtenz Jul 3, 2026
49af95a
Gate test-wallet-cli-e2e on wallet-cli being changed
Mrtenz Jul 3, 2026
c1f80bc
Fall back to all packages when root files change
Mrtenz Jul 3, 2026
83faade
Exclude private packages from changed workspace list
Mrtenz Jul 3, 2026
ace9b22
Fix lint error in getAllWorkspaces
Mrtenz Jul 3, 2026
d346744
Scope ESLint to changed packages and their dependants
Mrtenz Jul 14, 2026
e23f824
Use xargs to pass ESLint paths in lint-eslint job
Mrtenz Jul 14, 2026
857afa9
Use compact jq output when writing to GITHUB_OUTPUT
Mrtenz Jul 14, 2026
96910e8
Fix several issues found in code review
Mrtenz Jul 14, 2026
b6ea09d
Move analyse-code into lint-build-test workflow
Mrtenz Jul 14, 2026
7f0e057
Point scanner-ref at temporary commit with paths support
Mrtenz Jul 14, 2026
bcfbc6f
Revert analyse-code move to lint-build-test workflow
Mrtenz Jul 15, 2026
f8aa718
Fix position of lint-build-test in main workflow
Mrtenz Jul 15, 2026
5edbf53
Merge get-changed-packages into prepare job
Mrtenz Jul 15, 2026
f4cfef4
Add logging to ESLint and build steps
Mrtenz Jul 15, 2026
3a6de8b
Improve logging format in ESLint and build steps
Mrtenz Jul 15, 2026
62e9740
Use changed-paths to gate both ESLint and build scope
Mrtenz Jul 15, 2026
da7f802
Add blank line after full-run log messages for consistency
Mrtenz Jul 15, 2026
a508a37
Add permissions block to lint-build-test reusable workflow
Mrtenz Jul 15, 2026
fe898a1
Benchmark: ignored root file (README.md) + change to logging-controller
Mrtenz Jul 13, 2026
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
128 changes: 122 additions & 6 deletions .github/workflows/lint-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: Lint, Build, and Test
on:
workflow_call:

permissions:
contents: read

jobs:
prepare:
name: Prepare
Expand All @@ -12,6 +15,9 @@ jobs:
node-version: [18.x, 20.x, 22.x, 24.x]
outputs:
child-workspace-package-names: ${{ steps.workspace-package-names.outputs.child-workspace-package-names }}
merge-base: ${{ steps.fetch-merge-base.outputs.merge-base }}
package-names: ${{ steps.packages.outputs.package-names }}
changed-paths: ${{ steps.packages.outputs.changed-paths }}
steps:
- name: Checkout and setup environment
uses: MetaMask/action-checkout-and-setup@v3
Expand All @@ -26,6 +32,46 @@ jobs:
run: |
echo "child-workspace-package-names=$(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map(.name) | @json')" >> "$GITHUB_OUTPUT"
shell: bash
- name: Fetch merge base
id: fetch-merge-base
if: matrix.node-version == '24.x' && (github.base_ref != '' || github.event.merge_group.base_ref != '')
run: |
set -euo pipefail

PREFIXED_REF_REGEX='refs/heads/(.+)'
if [[ "$BASE_REF" =~ $PREFIXED_REF_REGEX ]]; then
BASE_REF="${BASH_REMATCH[1]}"
fi

MERGE_BASE=$(gh api "repos/$GITHUB_REPOSITORY/compare/$BASE_REF...$HEAD_SHA" --jq '.merge_base_commit.sha')
git fetch --unshallow --filter=blob:none --no-tags origin HEAD

echo "merge-base=$MERGE_BASE" >> "$GITHUB_OUTPUT"
env:
BASE_REF: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
GH_TOKEN: ${{ github.token }}
- name: Get changed package names
id: packages
if: matrix.node-version == '24.x'
run: |
if [[ -n "$MERGE_BASE" ]]; then
OUTPUT=$(yarn tsx scripts/get-changed-workspaces.mts "$MERGE_BASE" "$HEAD_SHA")
PACKAGES=$(echo "$OUTPUT" | jq -c '.names')
if [[ $(echo "$OUTPUT" | jq '.hasRootChange') == "true" ]]; then
CHANGED_PATHS="full"
else
CHANGED_PATHS=$(echo "$OUTPUT" | jq -c '.locations')
fi
else
PACKAGES=$(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map(.name) | @json')
CHANGED_PATHS="full"
fi
echo "package-names=$PACKAGES" >> "$GITHUB_OUTPUT"
echo "changed-paths=$CHANGED_PATHS" >> "$GITHUB_OUTPUT"
env:
MERGE_BASE: ${{ steps.fetch-merge-base.outputs.merge-base }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}

lint:
name: Lint (${{ matrix.script }})
Expand All @@ -38,7 +84,6 @@ jobs:
- codeowners:check
- constraints
- lint:dependencies
- lint:eslint
- lint:misc:check
- lint:teams
- lint:tsconfigs:all
Expand All @@ -63,14 +108,52 @@ jobs:
exit 1
fi

lint-eslint:
name: Lint (lint:eslint)
runs-on: ubuntu-latest
if: needs.prepare.outputs.changed-paths != '[]'
needs: prepare
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
persist-credentials: false
node-version: ${{ matrix.node-version }}
- name: Lint
run: |
if [[ "$CHANGED_PATHS" == "full" ]]; then
echo "Running ESLint on all packages."
echo ""
yarn lint:eslint
else
echo "Running ESLint on:"
echo "$CHANGED_PATHS" | jq -r '"- " + .[]'
echo ""
echo "$CHANGED_PATHS" | jq -r '.[]' | xargs yarn lint:eslint
fi
env:
CHANGED_PATHS: ${{ needs.prepare.outputs.changed-paths }}
- 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

validate-changelog:
name: Validate changelog
runs-on: ubuntu-latest
if: needs.prepare.outputs.package-names != '[]'
needs: prepare
strategy:
matrix:
node-version: [24.x]
package-name: ${{ fromJson(needs.prepare.outputs.child-workspace-package-names) }}
package-name: ${{ fromJson(needs.prepare.outputs.package-names) }}
steps:
- name: Checkout and setup environment
uses: MetaMask/action-checkout-and-setup@v3
Expand Down Expand Up @@ -118,7 +201,36 @@ jobs:
is-high-risk-environment: false
persist-credentials: false
node-version: ${{ matrix.node-version }}
- run: yarn build
- name: Unshallow checkout
if: needs.prepare.outputs.merge-base != ''
run: |
# Unshallow so git can walk history back to the merge base for
# `git diff --name-only`. Using `--filter=blob:none` avoids
# downloading file content — only commit and tree objects are needed.
git fetch --unshallow --filter=blob:none --no-tags origin HEAD
- name: Build
run: |
if [[ -n "$MERGE_BASE" && "$CHANGED_PATHS" != "full" ]]; then
TSCONFIG=$(mktemp --tmpdir="$GITHUB_WORKSPACE" --suffix=.json)
yarn tsx scripts/generate-partial-build-tsconfig.mts "$MERGE_BASE" "$HEAD_SHA" > "$TSCONFIG"
if [[ -s "$TSCONFIG" ]]; then
echo "Building changed packages:"
jq -r '"- " + (.references[].path | ltrimstr("./") | rtrimstr("/tsconfig.build.json"))' "$TSCONFIG"
echo ""
yarn ts-bridge --project "$TSCONFIG" --verbose
else
echo "No packages to build."
fi
rm -f "$TSCONFIG"
else
echo "Building all packages."
echo ""
yarn build
fi
env:
MERGE_BASE: ${{ needs.prepare.outputs.merge-base }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
CHANGED_PATHS: ${{ needs.prepare.outputs.changed-paths }}
- name: Require clean working directory
shell: bash
run: |
Expand Down Expand Up @@ -157,10 +269,11 @@ jobs:
test-18:
name: Test (18.x)
runs-on: ubuntu-latest
if: needs.prepare.outputs.package-names != '[]'
needs: prepare
strategy:
matrix:
package-name: ${{ fromJson(needs.prepare.outputs.child-workspace-package-names) }}
package-name: ${{ fromJson(needs.prepare.outputs.package-names) }}
steps:
- name: Checkout and setup environment
uses: MetaMask/action-checkout-and-setup@v3
Expand All @@ -182,10 +295,11 @@ jobs:
test-20:
name: Test (20.x)
runs-on: ubuntu-latest
if: needs.prepare.outputs.package-names != '[]'
needs: prepare
strategy:
matrix:
package-name: ${{ fromJson(needs.prepare.outputs.child-workspace-package-names) }}
package-name: ${{ fromJson(needs.prepare.outputs.package-names) }}
steps:
- name: Checkout and setup environment
uses: MetaMask/action-checkout-and-setup@v3
Expand All @@ -207,10 +321,11 @@ jobs:
test-22:
name: Test (22.x)
runs-on: ubuntu-latest
if: needs.prepare.outputs.package-names != '[]'
needs: prepare
strategy:
matrix:
package-name: ${{ fromJson(needs.prepare.outputs.child-workspace-package-names) }}
package-name: ${{ fromJson(needs.prepare.outputs.package-names) }}
steps:
- name: Checkout and setup environment
uses: MetaMask/action-checkout-and-setup@v3
Expand Down Expand Up @@ -252,6 +367,7 @@ jobs:
test-wallet-cli-e2e:
name: Test wallet-cli daemon e2e (${{ matrix.node-version }})
runs-on: ubuntu-latest
if: needs.prepare.outputs.package-names == '[]' || contains(fromJson(needs.prepare.outputs.package-names), '@metamask/wallet-cli')
needs: prepare
strategy:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Core Monorepo
# Core Monorepo <!-- benchmark: ignored root file change -->

This monorepo is a collection of packages used across multiple MetaMask clients (e.g. [`metamask-extension`](https://github.com/MetaMask/metamask-extension/), [`metamask-mobile`](https://github.com/MetaMask/metamask-mobile/)).

Expand Down
1 change: 1 addition & 0 deletions packages/logging-controller/src/LoggingController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Benchmark: mixed change (ignored root + single package).
import type {
ControllerGetStateAction,
ControllerStateChangeEvent,
Expand Down
52 changes: 52 additions & 0 deletions scripts/generate-partial-build-tsconfig.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
getAllWorkspaces,
getTypeScriptWorkspaces,
computeChangedWorkspaces,
} from './lib/workspaces.mjs';

/**
* Generate a filtered tsconfig.build.json for partial CI builds.
*
* Given a merge base SHA, outputs a tsconfig that references only the
* TypeScript packages that changed since that commit plus their transitive
* dependants and dependencies. Pipe the output to a temp file and pass it
* to `ts-bridge --project`.
*
* Dependencies are always included because TypeScript project references
* require every referenced project's dist output to already exist on disk.
*
* Usage: `tsx scripts/generate-partial-build-tsconfig.mts <merge-base-sha> [<head-sha>]`
*/
async function main() {
const mergeBase = process.argv[2];
if (!mergeBase) {
console.error(
'Usage: generate-partial-build-tsconfig.mts <merge-base-sha> [<head-sha>]',
);
process.exitCode = 1;
return;
}

const headRef = process.argv[3] ?? 'HEAD';

const allWorkspaces = await getAllWorkspaces();
const typeScriptWorkspaces = await getTypeScriptWorkspaces();
const packagesToBuild = await computeChangedWorkspaces(
allWorkspaces,
mergeBase,
headRef,
true,
);

const references = typeScriptWorkspaces
.filter(({ name }) => packagesToBuild.has(name))
.map(({ location }) => ({ path: `./${location}/tsconfig.build.json` }));

if (references.length === 0) {
return;
}

console.log(JSON.stringify({ files: [], include: [], references }, null, 2));
}

await main();
64 changes: 64 additions & 0 deletions scripts/get-changed-workspaces.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import {
getAllWorkspaces,
checkRootChange,
computeChangedWorkspaces,
getChangedFiles,
} from './lib/workspaces.mjs';

/**
* List workspaces that need to be checked given a merge base.
*
* Outputs a JSON object to stdout:
* - `names`: package names of changed workspaces and their transitive dependants
* - `locations`: workspace-relative paths (e.g. `packages/foo`) for the same set
* - `hasRootChange`: true if any non-ignored root file changed (triggers a full run)
*
* Usage: `tsx scripts/get-changed-workspaces.mts <merge-base-sha> [<head-sha>] [options]`
*/
const argv = await yargs(hideBin(process.argv))
.usage('$0 <merge-base> [head-sha] [options]')
.positional('merge-base', {
type: 'string',
describe: 'Merge base SHA',
})
.positional('head-sha', {
type: 'string',
describe: 'PR branch tip SHA (defaults to HEAD)',
})
.option('include-dependencies', {
type: 'boolean',
default: false,
describe:
'Also expand to transitive dependencies (needed for TypeScript builds)',
})
.demandCommand(1, 'merge-base is required')
.help()
.parseAsync();

const mergeBase = argv._[0] as string;
const headRef = (argv._[1] as string | undefined) ?? 'HEAD';
const workspaces = await getAllWorkspaces();

const changedFiles = await getChangedFiles(mergeBase, headRef);
const hasRootChange = checkRootChange(workspaces, changedFiles);

const changed = await computeChangedWorkspaces(
workspaces,
mergeBase,
headRef,
argv['include-dependencies'],
changedFiles,
);

const changedWorkspaces = workspaces.filter(({ name }) => changed.has(name));

console.log(
JSON.stringify({
names: changedWorkspaces.map(({ name }) => name),
locations: changedWorkspaces.map(({ location }) => location),
hasRootChange,
}),
);
Loading
Loading