From 30baa9da0b80d94dd8bdf0b6a932c46506664857 Mon Sep 17 00:00:00 2001 From: Pavel Dvorkin Date: Wed, 26 Nov 2025 14:06:42 -0500 Subject: [PATCH 1/3] Infra-2925: add merging GitHub action (#172) * INFRA-2925: Moved merge action logic to github-tools Signed-off-by: Pavel Dvorkin * Testing without CR approved * INFRA-2925: Improved logging on merge action * INFRA-2925-Changed stable_test to stable in merge action * INFRA-2925-Changed stable_test to stable in merge action * INFRA-2925-Fixed AI code review issues * INFRA-2925: stable_test merging allowed * INFRA-2925: Undo stable_test change * INFRA-2925: Linting fix * INFRA-2925: Linting fix * INFRA-2925: Code review changes * INFRA-2925:Added temporary release team for testing * INFRA-2925-Fixed code review suggestions --------- Signed-off-by: Pavel Dvorkin --- .github/workflows/merge-approved-pr.yml | 139 ++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 .github/workflows/merge-approved-pr.yml diff --git a/.github/workflows/merge-approved-pr.yml b/.github/workflows/merge-approved-pr.yml new file mode 100644 index 00000000..27f2cb7b --- /dev/null +++ b/.github/workflows/merge-approved-pr.yml @@ -0,0 +1,139 @@ +name: Merge Approved PR + +on: + workflow_call: + inputs: + pr-number: + required: true + type: number + description: 'The number of the pull request to process' + secrets: + github-token: + required: true + description: 'GitHub token with permissions to merge' + +jobs: + merge-pr: + runs-on: ubuntu-latest + steps: + # Fetch PR metadata (head and base branches) using the GitHub API + - name: Get PR Details + id: get-pr + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.github-token }} + script: | + // Fetch full details of the pull request associated with the comment + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: ${{ inputs.pr-number }} + }); + + // Output the base and head branch names for subsequent steps + core.setOutput('base', pr.base.ref); + core.setOutput('head', pr.head.ref); + + # Verify that the PR targets 'main' from 'stable-main-x.y.z' + - name: Verify Branch Names + id: verify-branches + run: | + # Define the required branch pattern + REQUIRED_BASE="main" + # Head branch must match pattern: stable-main-X.Y.Z where X, Y, Z are integers + HEAD_PATTERN="^stable-main-[0-9]+\.[0-9]+\.[0-9]+$" + + # Get actual values from the previous step + ACTUAL_BASE="${{ steps.get-pr.outputs.base }}" + ACTUAL_HEAD="${{ steps.get-pr.outputs.head }}" + + # Compare actual branches against requirements + if [[ "$ACTUAL_BASE" != "$REQUIRED_BASE" ]] || ! [[ "$ACTUAL_HEAD" =~ $HEAD_PATTERN ]]; then + echo "Skipping: PR must be from 'stable-main-X.Y.Z' to '$REQUIRED_BASE'. Found $ACTUAL_HEAD -> $ACTUAL_BASE" + echo "should_skip=true" >> "$GITHUB_OUTPUT" + else + echo "Branches match requirements: Source '$ACTUAL_HEAD' -> Target '$ACTUAL_BASE'." + echo "should_skip=false" >> "$GITHUB_OUTPUT" + fi + + # Check if the PR has the required approval status + - name: Verify Approval + id: verify-approval + if: steps.verify-branches.outputs.should_skip != 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.github-token }} + script: | + // Fetch all reviews for the PR + const { data: reviews } = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: ${{ inputs.pr-number }} + }); + + // Fetch members of the release team + let teamMembers = []; + try { + // Note: This requires a token with 'read:org' scope if the team is in an organization. + // GITHUB_TOKEN typically does not have this scope. Use a PAT if this fails. + const { data: members } = await github.rest.teams.listMembersInOrg({ + org: context.repo.owner, + team_slug: 'release-team', + per_page: 100 + }); + teamMembers = members.map(m => m.login); + } catch (error) { + // Fallback: If we can't fetch team members (e.g. due to token permissions), + // we can fail or fallback to author_association. + // Given the strict requirement for "Release Team", we must fail if we can't verify it. + console.log(`Error fetching release-team members for org '${context.repo.owner}': ${error.message}`); + console.log('Verify that the token has read:org permissions and the team exists.'); + core.setFailed(`Failed to fetch release-team members: ${error.message}`); + return; + } + + // Process reviews to find the latest state for each reviewer + const reviewerStates = {}; + for (const review of reviews) { + if (review.state === 'APPROVED' || review.state === 'CHANGES_REQUESTED') { + reviewerStates[review.user.login] = review.state; + } else if (review.state === 'DISMISSED') { + delete reviewerStates[review.user.login]; + } + } + + // Check for approval from a release-team member and no outstanding change requests + const states = Object.entries(reviewerStates); + const hasTeamApproval = states.some(([user, state]) => state === 'APPROVED' && teamMembers.includes(user)); + const hasChangesRequested = states.some(([, state]) => state === 'CHANGES_REQUESTED'); + + if (!hasTeamApproval) { + core.setFailed('Skipping: PR is not approved by a member of the release-team.'); + } else if (hasChangesRequested) { + core.setFailed('Skipping: PR has changes requested.'); + } else { + console.log('PR approval check passed.'); + } + + # Execute the merge if all checks pass + - name: Merge PR + if: steps.verify-branches.outputs.should_skip != 'true' + uses: actions/github-script@v7 + env: + BASE_REF: ${{ steps.get-pr.outputs.base }} + HEAD_REF: ${{ steps.get-pr.outputs.head }} + with: + github-token: ${{ secrets.github-token }} + script: | + try { + // Perform the merge using the 'merge' method (creates a merge commit, does not squash) + await github.rest.pulls.merge({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: ${{ inputs.pr-number }}, + merge_method: 'merge' + }); + console.log(`PR merged successfully: Source '${process.env.HEAD_REF}' -> Target '${process.env.BASE_REF}'.`); + } catch (error) { + core.setFailed(`Merge failed: ${error.message}`); + } From b6c804cd0baee99c6789b58291b4b4fd7c9ffdbf Mon Sep 17 00:00:00 2001 From: Jorge Carrasco Date: Thu, 27 Nov 2025 09:35:46 +0100 Subject: [PATCH 2/3] ci: update @metamask/auto-changelog to v5.2.0 (#175) Update auto-changelog to version 5.2.0 which adds: - Deduplication of commits with no PR number in subject - Merge commits deduplication using commit body instead of generic merge subject Release: https://github.com/MetaMask/auto-changelog/releases/tag/v5.2.0 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f16c1d94..e5fbba1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@metamask/auto-changelog` from `^5.1.0` to `^5.2.0` + - Adds deduplication for commits with no PR number in subject (non-"Squash & Merge" commits) + - Merge commits are now deduplicated using commit body instead of the generic merge subject + ## [1.0.0] ### Added diff --git a/package.json b/package.json index e0440082..ef776f54 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "update-release-sheet": "node .github/scripts/update-release-sheet.mjs" }, "dependencies": { - "@metamask/auto-changelog": "^5.1.0", + "@metamask/auto-changelog": "^5.2.0", "@metamask/utils": "^7.1.0", "@octokit/graphql": "^7.0.1", "@octokit/request": "^8.1.1", diff --git a/yarn.lock b/yarn.lock index e2ff0012..1d0a9ce5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -941,9 +941,9 @@ __metadata: languageName: node linkType: hard -"@metamask/auto-changelog@npm:^5.1.0": - version: 5.1.0 - resolution: "@metamask/auto-changelog@npm:5.1.0" +"@metamask/auto-changelog@npm:^5.2.0": + version: 5.2.0 + resolution: "@metamask/auto-changelog@npm:5.2.0" dependencies: "@octokit/rest": "npm:^20.0.0" diff: "npm:^5.0.0" @@ -954,7 +954,7 @@ __metadata: prettier: ">=3.0.0" bin: auto-changelog: dist/cli.mjs - checksum: 10/ee9f651c313a2377a21a304383f56f1872b4ce6411a0b61cf0b70b23bff9d3204332cd5a052cdfcaf363ce15424a7a1fdbde99a6b4352f2472fbe6b6d27c5791 + checksum: 10/86ca4f56a0c86a1fabbe4b7b66a28bda39e59c51c996aadedeb5f341b3e93acfd7ca5dafebbd82852070f12324bbd83fc7cb979cb9f8b9ca8f5dded53f1fa5af languageName: node linkType: hard @@ -1014,7 +1014,7 @@ __metadata: dependencies: "@lavamoat/allow-scripts": "npm:^2.3.1" "@lavamoat/preinstall-always-fail": "npm:^1.0.0" - "@metamask/auto-changelog": "npm:^5.1.0" + "@metamask/auto-changelog": "npm:^5.2.0" "@metamask/eslint-config": "npm:^12.0.0" "@metamask/eslint-config-jest": "npm:^12.0.0" "@metamask/eslint-config-nodejs": "npm:^12.0.0" From 26bff79d1c862b8b3bb53c556cc6a4731420c291 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:04:22 +0100 Subject: [PATCH 3/3] 1.1.0 (#176) * 1.1.0 --------- Co-authored-by: github-actions Co-authored-by: Jorge Carrasco --- CHANGELOG.md | 11 +++++++++-- package.json | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5fbba1d..88ec4eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0] + +### Added + +- Add merging GitHub action ([#172](https://github.com/MetaMask/github-tools/pull/172)) + ### Changed -- Bump `@metamask/auto-changelog` from `^5.1.0` to `^5.2.0` +- Bump `@metamask/auto-changelog` from `^5.1.0` to `^5.2.0` ([#175](https://github.com/MetaMask/github-tools/pull/175)) - Adds deduplication for commits with no PR number in subject (non-"Squash & Merge" commits) - Merge commits are now deduplicated using commit body instead of the generic merge subject @@ -29,5 +35,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Some inputs were renamed for consistency across actions. - Bump `actions/checkout` and `actions/setup-node` to `v6` ([#173](https://github.com/MetaMask/github-tools/pull/173)) -[Unreleased]: https://github.com/MetaMask/github-tools/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/github-tools/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/MetaMask/github-tools/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/MetaMask/github-tools/releases/tag/v1.0.0 diff --git a/package.json b/package.json index ef776f54..23a45966 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/github-tools", - "version": "1.0.0", + "version": "1.1.0", "private": true, "description": "Tools for interacting with the GitHub API to do metrics gathering", "repository": {