Skip to content
Open
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
79 changes: 79 additions & 0 deletions .github/actions/is-release-pr/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Is release PR
description: >
Determine whether a pull request is a release PR using the same check as
update-changelogs (checkout head, merge-base, MetaMask/action-is-release).

inputs:
pr-number:
description: Pull request number to evaluate.
required: true
commit-starts-with:
description: >
Comma-separated commit/PR title prefixes. Use `[version]` for the new root
package version (same as vars.RELEASE_COMMIT_PREFIX).
required: true
token:
description: GitHub token for gh/API access.
required: false
default: ${{ github.token }}

outputs:
IS_RELEASE:
description: '"true" when the PR is a release PR, otherwise "false".'
value: ${{ steps.is-release.outputs.IS_RELEASE }}
head-sha:
description: Head commit SHA of the pull request.
value: ${{ steps.pr-info.outputs.pr-head-sha }}
head-ref:
description: Head branch name of the pull request.
value: ${{ steps.pr-info.outputs.pr-head-ref }}
base-ref:
description: Base branch name of the pull request.
value: ${{ steps.pr-info.outputs.pr-base-ref }}
merge-base:
description: Merge-base SHA between the PR head and base branch.
value: ${{ steps.merge-base.outputs.merge-base }}

runs:
using: composite
steps:
- name: Get pull request info
id: pr-info
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
PR_NUMBER: ${{ inputs.pr-number }}
run: |
set -euo pipefail
gh pr view "$PR_NUMBER" \
--repo "$GITHUB_REPOSITORY" \
--json baseRefName,headRefOid,headRefName,title \
--jq '"pr-base-ref=\(.baseRefName)\npr-head-sha=\(.headRefOid)\npr-head-ref=\(.headRefName)\npr-title=\(.title)"' \
>> "$GITHUB_OUTPUT"

- name: Checkout repository
uses: actions/checkout@v7
with:
token: ${{ inputs.token }}
fetch-depth: 0
persist-credentials: false
ref: ${{ steps.pr-info.outputs.pr-head-sha }}

- name: Get merge base
id: merge-base
shell: bash
env:
BASE_REF: ${{ steps.pr-info.outputs.pr-base-ref }}
run: |
set -euo pipefail
MERGE_BASE=$(git merge-base HEAD "refs/remotes/origin/$BASE_REF")
echo "merge-base=$MERGE_BASE" >> "$GITHUB_OUTPUT"

- name: Check if the pull request is a release
id: is-release
uses: MetaMask/action-is-release@v2
with:
commit-starts-with: ${{ inputs.commit-starts-with }}
commit-message: ${{ steps.pr-info.outputs.pr-title }}
before: ${{ steps.merge-base.outputs.merge-base }}
skip-checkout: true
111 changes: 108 additions & 3 deletions .github/workflows/close-stale-release-prs.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
name: Close Stale Release PRs

# Release PRs on `release/*` branches are expected to merge quickly. Abandoned
# ones block other engineers from starting a new release. This workflow closes
# inactive release PRs, leaves a comment, and deletes the branch.
# Release PRs are expected to merge quickly. Abandoned ones block other engineers
# from starting a new release. This workflow closes inactive release PRs, leaves a
# comment, and deletes the branch.
#
# Release PR detection is shared with update-changelogs via
# `.github/actions/is-release-pr` (root version bump + RELEASE_COMMIT_PREFIX).
# `release/*` is only a cheap prefilter before that shared check.
on:
schedule:
# Check twice an hour so the 3h window is reasonably precise.
Expand All @@ -17,8 +21,82 @@ concurrency:
cancel-in-progress: false

jobs:
find-candidates:
name: Find release/* PR candidates
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
prs: ${{ steps.list.outputs.prs }}
steps:
- name: List same-repo open release/* PRs
id: list
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# Paginate all open PRs; --limit 100 would miss older release/* heads
# once the repo has many open PRs.
PRS="$(
gh api --paginate \
"/repos/${GITHUB_REPOSITORY}/pulls?state=open&per_page=100" \
--jq '.[] | select(.head.repo != null and .head.repo.fork == false and (.head.ref | startswith("release/"))) | .number' \
| jq --compact-output --slurp '.'
)"
Comment thread
cursor[bot] marked this conversation as resolved.
echo "prs=$PRS" >> "$GITHUB_OUTPUT"
echo "Candidate PR numbers: $PRS"

filter-release-prs:
name: Confirm release PR #${{ matrix.pr }}
needs: find-candidates
if: needs.find-candidates.outputs.prs != '[]'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
strategy:
fail-fast: false
matrix:
pr: ${{ fromJson(needs.find-candidates.outputs.prs) }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Check if the pull request is a release
id: is-release
uses: ./.github/actions/is-release-pr
with:
pr-number: ${{ matrix.pr }}
commit-starts-with: ${{ vars.RELEASE_COMMIT_PREFIX }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: Record confirmed release PR
if: steps.is-release.outputs.IS_RELEASE == 'true'
env:
# Pass via env so the value is not expanded into the script text
# (zizmor template-injection). PR numbers come from open same-repo
# release/* PRs; write access is enough to create those.
PR_NUMBER: ${{ matrix.pr }}
run: |
set -euo pipefail
# Unique filename per matrix leg so merge-multiple keeps every PR.
echo "$PR_NUMBER" > "release-pr-${PR_NUMBER}.txt"

- name: Upload confirmed release PR artifact
if: steps.is-release.outputs.IS_RELEASE == 'true'
uses: actions/upload-artifact@v4
with:
name: release-pr-${{ matrix.pr }}
path: release-pr-${{ matrix.pr }}.txt
if-no-files-found: ignore
Comment thread
cursor[bot] marked this conversation as resolved.

close-stale-release-prs:
name: Close stale release PRs
needs: [find-candidates, filter-release-prs]
# Still close any PRs that were confirmed even if some matrix legs failed.
if: always() && needs.find-candidates.result == 'success' && needs.find-candidates.outputs.prs != '[]' && needs.filter-release-prs.result != 'cancelled'
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -30,7 +108,34 @@ jobs:
is-high-risk-environment: false
persist-credentials: false
cache-node-modules: true

- name: Download confirmed release PR numbers
uses: actions/download-artifact@v4
continue-on-error: true
with:
pattern: release-pr-*
merge-multiple: true
path: release-pr-numbers

- name: Build RELEASE_PR_NUMBERS
id: numbers
run: |
set -euo pipefail
if [ -d release-pr-numbers ]; then
NUMBERS="$(find release-pr-numbers -type f -print0 | xargs -0 cat | sort -n -u | paste -sd, -)"
else
NUMBERS=''
fi
echo "numbers=$NUMBERS" >> "$GITHUB_OUTPUT"
echo "Confirmed release PR numbers: ${NUMBERS:-<none>}"

- name: Close inactive release PRs
if: steps.numbers.outputs.numbers != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_PR_NUMBERS: ${{ steps.numbers.outputs.numbers }}
run: yarn tsx scripts/close-stale-release-prs.mts

- name: No confirmed release PRs
if: steps.numbers.outputs.numbers == ''
run: echo 'No confirmed release PRs to evaluate.'
41 changes: 7 additions & 34 deletions .github/workflows/update-changelogs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,50 +44,23 @@ jobs:
pull-requests: read
outputs:
is-release: ${{ steps.is-release.outputs.IS_RELEASE }}
head-sha: ${{ steps.pr-info.outputs.pr-head-sha }}
head-ref: ${{ steps.pr-info.outputs.pr-head-ref }}
base-ref: ${{ steps.pr-info.outputs.pr-base-ref }}
merge-base: ${{ steps.merge-base.outputs.merge-base }}
head-sha: ${{ steps.is-release.outputs.head-sha }}
head-ref: ${{ steps.is-release.outputs.head-ref }}
base-ref: ${{ steps.is-release.outputs.base-ref }}
merge-base: ${{ steps.is-release.outputs.merge-base }}
steps:
- name: Get pull request info
id: pr-info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
run: |
gh pr view "$PR_NUMBER" \
--repo "$GITHUB_REPOSITORY" \
--json baseRefName,headRefOid,headRefName,title \
--jq '"pr-base-ref=\(.baseRefName)\npr-head-sha=\(.headRefOid)\npr-head-ref=\(.headRefName)\npr-title=\(.title)"' \
>> "$GITHUB_OUTPUT"

- name: Checkout repository
uses: actions/checkout@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
persist-credentials: false
ref: ${{ steps.pr-info.outputs.pr-head-sha }}

- name: Get merge base
id: merge-base
shell: bash
env:
BASE_REF: ${{ steps.pr-info.outputs.pr-base-ref }}
run: |
set -euo pipefail

MERGE_BASE=$(git merge-base HEAD "refs/remotes/origin/$BASE_REF")
echo "merge-base=$MERGE_BASE" >> "$GITHUB_OUTPUT"

- name: Check if the pull request is a release
id: is-release
uses: MetaMask/action-is-release@v2
uses: ./.github/actions/is-release-pr
with:
pr-number: ${{ github.event.issue.number || github.event.pull_request.number }}
commit-starts-with: ${{ vars.RELEASE_COMMIT_PREFIX }}
commit-message: ${{ steps.pr-info.outputs.pr-title }}
before: ${{ steps.merge-base.outputs.merge-base }}
skip-checkout: true
token: ${{ secrets.GITHUB_TOKEN }}

react-to-comment:
name: React to the comment
Expand Down
2 changes: 1 addition & 1 deletion docs/processes/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Have changes that you need to release? There are a few things to understand:

- The responsibility of maintenance is not the only thing shared among multiple teams at MetaMask; releases are as well. That means **if you work on a team that has codeownership over a package, you are free to create a new release without needing the Wallet Framework team to do so.**
- Unlike clients, releases are not issued on a schedule; **anyone may create a release at any time**. Because of this, you may wish to review the Pull Requests tab on GitHub and ensure that no one else has a release candidate already in progress. If not, then you are free to start the process.
- Release PRs on `release/*` branches that sit inactive for **3 hours** are automatically closed, with the branch deleted, so abandoned releases do not block others. Add the `release:keep-open` label if you need a longer-lived release PR in exceptional cases.
- Release PRs that sit inactive for **3 hours** are automatically closed, with the branch deleted, so abandoned releases do not block others. Detection uses the same release check as changelog updates (`MetaMask/action-is-release` via `.github/actions/is-release-pr`), after a cheap `release/*` branch prefilter. Add the `release:keep-open` label if you need a longer-lived release PR in exceptional cases.
- The release process is a work in progress. Further improvements to simplify the process are planned, but in the meantime, if you encounter any issues, please reach out to the Wallet Framework team.
- Breaking changes take special consideration. [Read the guide](./breaking-changes.md) on how to prepare and handle them effectively.

Expand Down
Loading
Loading