Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ee8a40e
Build only changed packages and dependants in CI
Mrtenz Jul 2, 2026
704c084
Fix shallow clone preventing merge-base resolution
Mrtenz Jul 2, 2026
e6bcd9a
Unshallow the checkout before computing the merge base
Mrtenz Jul 2, 2026
2a22ab0
Use GitHub Compare API to fetch exact merge base
Mrtenz Jul 2, 2026
4b23152
Unshallow with blobless filter to confirm merge base ancestry
Mrtenz Jul 2, 2026
e3bd09d
Restrict unshallow fetch to HEAD only
Mrtenz Jul 2, 2026
9735938
Use ts-bridge with partial tsconfig for partial builds
Mrtenz Jul 3, 2026
c990f76
Write partial tsconfig to workspace root so relative paths resolve
Mrtenz Jul 3, 2026
7f5617d
Fix incremental build detecting wrong changed files and missing depen…
Mrtenz Jul 3, 2026
f6c3c90
Extend partial builds to tests and changelog validation
Mrtenz Jul 3, 2026
c4707e9
Remove fallback to all packages when no packages changed
Mrtenz Jul 3, 2026
187c0cd
Skip matrix jobs when no packages changed
Mrtenz Jul 3, 2026
aa75de9
Gate test-wallet-cli-e2e on wallet-cli being changed
Mrtenz Jul 3, 2026
1920d2f
Fall back to all packages when root files change
Mrtenz Jul 3, 2026
0da66a7
Exclude private packages from changed workspace list
Mrtenz Jul 3, 2026
1a77046
Fix lint error in getAllWorkspaces
Mrtenz Jul 3, 2026
b08230f
Scope ESLint to changed packages and their dependants
Mrtenz Jul 14, 2026
577771d
Use xargs to pass ESLint paths in lint-eslint job
Mrtenz Jul 14, 2026
557d141
Use compact jq output when writing to GITHUB_OUTPUT
Mrtenz Jul 14, 2026
09203aa
Fix several issues found in code review
Mrtenz Jul 14, 2026
906ed75
Move analyse-code into lint-build-test workflow
Mrtenz Jul 14, 2026
ac82eb1
Point scanner-ref at temporary commit with paths support
Mrtenz Jul 14, 2026
1602e19
Revert analyse-code move to lint-build-test workflow
Mrtenz Jul 15, 2026
7a72c41
Fix position of lint-build-test in main workflow
Mrtenz Jul 15, 2026
349b92a
Merge get-changed-packages into prepare job
Mrtenz Jul 15, 2026
9f890a5
Add logging to ESLint and build steps
Mrtenz Jul 15, 2026
1e218ac
Improve logging format in ESLint and build steps
Mrtenz Jul 15, 2026
4a89ee4
Use changed-paths to gate both ESLint and build scope
Mrtenz Jul 15, 2026
f80672d
Add blank line after full-run log messages for consistency
Mrtenz Jul 15, 2026
664902c
Add permissions block to lint-build-test reusable workflow
Mrtenz Jul 15, 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 @@ -35,7 +81,6 @@ jobs:
matrix:
node-version: [24.x]
script:
- lint:eslint
- lint:misc:check
- constraints
- lint:dependencies
Expand All @@ -61,14 +106,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 @@ -113,7 +196,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 @@ -151,10 +263,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 @@ -176,10 +289,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 @@ -201,10 +315,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 @@ -246,6 +361,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
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