Skip to content
Closed
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
119 changes: 119 additions & 0 deletions .github/actions/check-skip-merge-queue/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Check skip merge queue
description: >-
Report whether the pull request in a merge group is first in the queue and
already up to date with the base branch, in which case the checks about to run
would test a commit nothing has changed under.

Only the report is produced here. Nothing can make the merge queue skip an
entry from inside a workflow, so callers gate their own jobs on the output.

# Ported from MetaMask/github-tools/.github/actions/check-skip-merge-queue,
# which calls actions/github-script by mutable tag. See the note in
# ../checkout-and-setup/action.yml for why that cannot be pinned from outside.

inputs:
head-ref:
description: The merge group's head ref, which carries the pull request number.
required: true
default: ${{ github.event.merge_group.head_ref }}
base-ref:
description: The branch the pull request is merging into.
required: true
default: main
github-token:
description: The token used to read the pull request and compare branches.
required: true
default: ${{ github.token }}

outputs:
up-to-date:
description: >-
Whether the pull request is first in the queue and up to date with the
base branch. `false` whenever that cannot be established, so a failure
here costs a redundant run rather than an untested merge.
value: ${{ steps.up-to-date.outputs.up-to-date || 'false' }}

runs:
using: composite
steps:
# `continue-on-error` on both steps: this is an optimization, and the safe
# answer is always available. A throw here leaves `up-to-date` empty, which
# the output above reads as `false`.
- name: Find the pull request behind the merge group
continue-on-error: true
id: pr-details
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
HEAD_REF: ${{ inputs.head-ref }}
with:
github-token: ${{ inputs.github-token }}
script: |
const { HEAD_REF } = process.env;
const match = HEAD_REF.match(/\/pr-([0-9]+)-/u);
if (!match) {
return core.setFailed(`Could not extract pull request number from head ref: "${HEAD_REF}".`);
}

const number = parseInt(match[1], 10);
const result = await github.graphql(`
query($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
headRefName
mergeQueueEntry { position }
}
}
}
`, {
owner: context.repo.owner,
name: context.repo.repo,
number,
});

if (!result.repository.pullRequest) {
return core.setFailed(`Pull request #${number} not found in repository "${context.repo.owner}/${context.repo.repo}".`);
}

const position = result.repository.pullRequest.mergeQueueEntry?.position;
if (!position) {
return core.setFailed(`Pull request #${number} is not in the merge queue.`);
}

core.setOutput('pr-number', number);
core.setOutput('pr-branch', result.repository.pullRequest.headRefName);
core.setOutput('merge-queue-position', position);

- name: Check whether it is up to date with the base branch
continue-on-error: true
id: up-to-date
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BASE_REF: ${{ inputs.base-ref }}
PR_BRANCH: ${{ steps.pr-details.outputs.pr-branch }}
MERGE_QUEUE_POSITION: ${{ steps.pr-details.outputs.merge-queue-position }}
with:
github-token: ${{ inputs.github-token }}
script: |
const { BASE_REF, PR_BRANCH, MERGE_QUEUE_POSITION } = process.env;

// Only the first entry can be up to date with the base branch by
// definition; anything behind it is waiting on merges above it.
if (parseInt(MERGE_QUEUE_POSITION, 10) !== 1) {
core.info(`Pull request is not first in the merge queue (position: ${MERGE_QUEUE_POSITION}).`);
core.setOutput('up-to-date', 'false');
return;
}

const comparison = await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `${BASE_REF}...${PR_BRANCH}`,
});

if (comparison.data.status === 'identical' || comparison.data.status === 'ahead') {
core.info(`Pull request branch "${PR_BRANCH}" is up-to-date with base branch "${BASE_REF}".`);
core.setOutput('up-to-date', 'true');
} else {
core.info(`Pull request branch "${PR_BRANCH}" is not up-to-date with base branch "${BASE_REF}".`);
core.setOutput('up-to-date', 'false');
}
2 changes: 1 addition & 1 deletion .github/actions/playwright-install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ runs:
steps:
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
Expand Down
65 changes: 65 additions & 0 deletions .github/actions/setup-environment/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Set up environment
description: Set up Node.js and Yarn and install dependencies, in a repository already checked out.

# Replaces the setup half of MetaMask/action-checkout-and-setup. That action is
# pinned by SHA where we call it, but calls actions/checkout, actions/setup-node
# and actions/cache by mutable tag internally, which this organization refuses --
# rightly, since a pin one layer deep is no pin at all. Keeping the steps here is
# the only way to pin the whole chain, and the chain is short.
#
# Checkout is not part of this action and cannot be: `uses: ./...` resolves
# against the workspace, so the repository has to be on disk before the runner
# can find this file. Callers check out first, with their own pinned step.
#
# Deliberately narrower than the action it replaces: no node_modules cache and
# no lookup-only fast path, only Yarn's download cache. Those are worth real
# minutes on a monorepo this size, but they turn on a cache key that has to
# account for the native rebuilds `postinstall` runs, and getting that subtly
# wrong yields a job that passes against stale binaries. `yarn install` runs
# every time here, so what is on disk always matches the lockfile.

inputs:
node-version:
description: The Node.js version to use. Defaults to the one in `.nvmrc`.
required: false
default: ''
cache:
description: >-
Whether to restore and save Yarn's download cache. Pass `false` from jobs
holding publish credentials, where a poisoned entry buys an attacker more
than the cache saves us.
required: false
default: 'true'

runs:
using: composite
steps:
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ inputs.node-version }}
node-version-file: ${{ inputs.node-version == '' && '.nvmrc' || '' }}

- name: Enable Corepack
shell: bash
run: corepack enable

# `enableGlobalCache: false` puts Yarn's downloads in `.yarn/cache`, which is
# gitignored, so without this every job refetches the whole dependency tree.
# Restoring a partial cache is still worth it, hence `restore-keys`: a
# lockfile change invalidates the key but leaves most entries reusable.
- name: Restore Yarn's download cache
if: inputs.cache == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .yarn/cache
key: yarn-${{ runner.os }}-${{ inputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
yarn-${{ runner.os }}-${{ inputs.node-version }}-
yarn-${{ runner.os }}-

# The LavaMoat plugin in `.yarnrc.yml` runs the scripts `lavamoat.allowScripts`
# permits, so there is no separate `yarn allow-scripts` step to run.
- name: Install dependencies
shell: bash
run: yarn install --immutable
60 changes: 53 additions & 7 deletions .github/workflows/changelog-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,58 @@ jobs:
check-changelog:
name: Check changelog
runs-on: ubuntu-latest
# `no-changelog` is the escape hatch for a pull request that genuinely
# changes no published behaviour.
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }}
steps:
- name: Check changelog
uses: MetaMask/github-tools/.github/actions/check-changelog@v1
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
base-branch: ${{ github.event.pull_request.base.ref }}
head-ref: ${{ github.head_ref }}
labels: ${{ toJSON(github.event.pull_request.labels) }}
pr-number: ${{ github.event.pull_request.number }}
repo: ${{ github.repository }}
# The whole comparison is against the merge base, so the history back
# to it has to be here.
fetch-depth: 0

- name: Require a changelog entry for every package this changes
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail

# Against the merge base rather than the base tip, so that commits
# landing on the base branch after this pull request opened are not
# read as its own changes.
merge_base="$(git merge-base "$BASE_SHA" "$HEAD_SHA")"
changed="$(git diff --name-only "$merge_base" "$HEAD_SHA")"

missing=()
for package_dir in packages/*/; do
package="${package_dir%/}"
changelog="${package}/CHANGELOG.md"

# A package with no changelog is private and releases nothing, so
# there is no entry for it to be missing. This is the same set
# `yarn changelog:validate` covers, which runs `--no-private`.
[[ -f "$changelog" ]] || continue

# Changes to the changelog itself do not oblige a changelog entry.
if ! grep -qE "^${package}/" <<< "$changed" \
|| [[ "$(grep -E "^${package}/" <<< "$changed")" == "$changelog" ]]; then
continue
fi

grep -qxF "$changelog" <<< "$changed" || missing+=("$package")
done

if [[ ${#missing[@]} -gt 0 ]]; then
echo "::error::These packages changed without a changelog entry:"
printf ' %s\n' "${missing[@]}"
echo
echo "Add an entry under '## [Unreleased]' in each package's CHANGELOG.md,"
echo "or apply the 'no-changelog' label if this genuinely changes no"
echo "published behaviour."
exit 1
fi

echo "Every changed package has a changelog entry."
2 changes: 1 addition & 1 deletion .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/coverage-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Download coverage artifact
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage
path: coverage/
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/dependabot-dedupe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Check out repo
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
Expand All @@ -24,13 +24,13 @@ jobs:
git config user.email "actions@github.com"

- name: Set up Node
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: Install Yarn
run: corepack enable
- name: Restore Yarn cache
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: yarn
Expand Down
Loading
Loading