Skip to content
Draft
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
121 changes: 117 additions & 4 deletions .github/workflows/lint-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
on:
workflow_call:

permissions:
contents: read

jobs:
prepare:
name: Prepare
Expand All @@ -12,6 +15,9 @@
node-version: [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@v2
Expand All @@ -24,6 +30,46 @@
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 "$MERGE_BASE" --head-ref "$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"

Check failure

Code scanning / zizmor

unpinned action reference: action is not pinned to a hash (required by blanket policy) Error

unpinned action reference: action is not pinned to a hash (required by blanket policy)
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 @@ -33,7 +79,6 @@
matrix:
node-version: [24.x]
script:
- lint:eslint
- lint:misc:check
- constraints
- lint:dependencies
Expand All @@ -56,14 +101,52 @@
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@v2
Expand Down Expand Up @@ -102,7 +185,36 @@
with:
is-high-risk-environment: 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 @@ -136,11 +248,12 @@
test:
name: Test
runs-on: ubuntu-latest
if: needs.prepare.outputs.package-names != '[]'
needs: prepare
strategy:
matrix:
node-version: [20.x, 22.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@v2
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
"@types/semver": "^7",
"@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.48.0",
"@yarnpkg/cli": "^4.17.1",
"@yarnpkg/core": "^4.9.0",
"@yarnpkg/fslib": "^3.1.5",
"@yarnpkg/parsers": "^3.0.3",
"@yarnpkg/types": "^4.0.0",
"comment-json": "^4.5.1",
"depcheck": "^1.4.7",
Expand Down
50 changes: 50 additions & 0 deletions scripts/generate-partial-build-tsconfig.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
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(): Promise<void> {
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 typeScriptWorkspaces = await getTypeScriptWorkspaces();
const { workspaces: packagesToBuild } = await computeChangedWorkspaces({
includeDependencies: true,
mergeBase,
headRef,
});

const packagesToBuildNames = new Set(packagesToBuild.map(({ name }) => name));
const references = typeScriptWorkspaces
.filter(({ name }) => packagesToBuildNames.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();
50 changes: 50 additions & 0 deletions scripts/get-changed-workspaces.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import { computeChangedWorkspaces } 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-ref <ref>] [--include-dependencies]`
*/
const argv = await yargs(hideBin(process.argv))
.usage('$0 --merge-base <sha> [--head-ref <ref>] [--include-dependencies]')
.option('merge-base', {
type: 'string',
describe: 'Merge base SHA',
demandOption: true,
})
.option('head-ref', {
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)',
})
.help()
.parseAsync();

const { mergeBase, headRef = 'HEAD', includeDependencies } = argv;

const { workspaces, hasRootChange } = await computeChangedWorkspaces({
includeDependencies,
mergeBase,
headRef,
});

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