Skip to content

Commit 30baa9d

Browse files
authored
Infra-2925: add merging GitHub action (#172)
* INFRA-2925: Moved merge action logic to github-tools Signed-off-by: Pavel Dvorkin <[email protected]> * 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 <[email protected]>
1 parent 83fca83 commit 30baa9d

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
name: Merge Approved PR
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
pr-number:
7+
required: true
8+
type: number
9+
description: 'The number of the pull request to process'
10+
secrets:
11+
github-token:
12+
required: true
13+
description: 'GitHub token with permissions to merge'
14+
15+
jobs:
16+
merge-pr:
17+
runs-on: ubuntu-latest
18+
steps:
19+
# Fetch PR metadata (head and base branches) using the GitHub API
20+
- name: Get PR Details
21+
id: get-pr
22+
uses: actions/github-script@v7
23+
with:
24+
github-token: ${{ secrets.github-token }}
25+
script: |
26+
// Fetch full details of the pull request associated with the comment
27+
const { data: pr } = await github.rest.pulls.get({
28+
owner: context.repo.owner,
29+
repo: context.repo.repo,
30+
pull_number: ${{ inputs.pr-number }}
31+
});
32+
33+
// Output the base and head branch names for subsequent steps
34+
core.setOutput('base', pr.base.ref);
35+
core.setOutput('head', pr.head.ref);
36+
37+
# Verify that the PR targets 'main' from 'stable-main-x.y.z'
38+
- name: Verify Branch Names
39+
id: verify-branches
40+
run: |
41+
# Define the required branch pattern
42+
REQUIRED_BASE="main"
43+
# Head branch must match pattern: stable-main-X.Y.Z where X, Y, Z are integers
44+
HEAD_PATTERN="^stable-main-[0-9]+\.[0-9]+\.[0-9]+$"
45+
46+
# Get actual values from the previous step
47+
ACTUAL_BASE="${{ steps.get-pr.outputs.base }}"
48+
ACTUAL_HEAD="${{ steps.get-pr.outputs.head }}"
49+
50+
# Compare actual branches against requirements
51+
if [[ "$ACTUAL_BASE" != "$REQUIRED_BASE" ]] || ! [[ "$ACTUAL_HEAD" =~ $HEAD_PATTERN ]]; then
52+
echo "Skipping: PR must be from 'stable-main-X.Y.Z' to '$REQUIRED_BASE'. Found $ACTUAL_HEAD -> $ACTUAL_BASE"
53+
echo "should_skip=true" >> "$GITHUB_OUTPUT"
54+
else
55+
echo "Branches match requirements: Source '$ACTUAL_HEAD' -> Target '$ACTUAL_BASE'."
56+
echo "should_skip=false" >> "$GITHUB_OUTPUT"
57+
fi
58+
59+
# Check if the PR has the required approval status
60+
- name: Verify Approval
61+
id: verify-approval
62+
if: steps.verify-branches.outputs.should_skip != 'true'
63+
uses: actions/github-script@v7
64+
with:
65+
github-token: ${{ secrets.github-token }}
66+
script: |
67+
// Fetch all reviews for the PR
68+
const { data: reviews } = await github.rest.pulls.listReviews({
69+
owner: context.repo.owner,
70+
repo: context.repo.repo,
71+
pull_number: ${{ inputs.pr-number }}
72+
});
73+
74+
// Fetch members of the release team
75+
let teamMembers = [];
76+
try {
77+
// Note: This requires a token with 'read:org' scope if the team is in an organization.
78+
// GITHUB_TOKEN typically does not have this scope. Use a PAT if this fails.
79+
const { data: members } = await github.rest.teams.listMembersInOrg({
80+
org: context.repo.owner,
81+
team_slug: 'release-team',
82+
per_page: 100
83+
});
84+
teamMembers = members.map(m => m.login);
85+
} catch (error) {
86+
// Fallback: If we can't fetch team members (e.g. due to token permissions),
87+
// we can fail or fallback to author_association.
88+
// Given the strict requirement for "Release Team", we must fail if we can't verify it.
89+
console.log(`Error fetching release-team members for org '${context.repo.owner}': ${error.message}`);
90+
console.log('Verify that the token has read:org permissions and the team exists.');
91+
core.setFailed(`Failed to fetch release-team members: ${error.message}`);
92+
return;
93+
}
94+
95+
// Process reviews to find the latest state for each reviewer
96+
const reviewerStates = {};
97+
for (const review of reviews) {
98+
if (review.state === 'APPROVED' || review.state === 'CHANGES_REQUESTED') {
99+
reviewerStates[review.user.login] = review.state;
100+
} else if (review.state === 'DISMISSED') {
101+
delete reviewerStates[review.user.login];
102+
}
103+
}
104+
105+
// Check for approval from a release-team member and no outstanding change requests
106+
const states = Object.entries(reviewerStates);
107+
const hasTeamApproval = states.some(([user, state]) => state === 'APPROVED' && teamMembers.includes(user));
108+
const hasChangesRequested = states.some(([, state]) => state === 'CHANGES_REQUESTED');
109+
110+
if (!hasTeamApproval) {
111+
core.setFailed('Skipping: PR is not approved by a member of the release-team.');
112+
} else if (hasChangesRequested) {
113+
core.setFailed('Skipping: PR has changes requested.');
114+
} else {
115+
console.log('PR approval check passed.');
116+
}
117+
118+
# Execute the merge if all checks pass
119+
- name: Merge PR
120+
if: steps.verify-branches.outputs.should_skip != 'true'
121+
uses: actions/github-script@v7
122+
env:
123+
BASE_REF: ${{ steps.get-pr.outputs.base }}
124+
HEAD_REF: ${{ steps.get-pr.outputs.head }}
125+
with:
126+
github-token: ${{ secrets.github-token }}
127+
script: |
128+
try {
129+
// Perform the merge using the 'merge' method (creates a merge commit, does not squash)
130+
await github.rest.pulls.merge({
131+
owner: context.repo.owner,
132+
repo: context.repo.repo,
133+
pull_number: ${{ inputs.pr-number }},
134+
merge_method: 'merge'
135+
});
136+
console.log(`PR merged successfully: Source '${process.env.HEAD_REF}' -> Target '${process.env.BASE_REF}'.`);
137+
} catch (error) {
138+
core.setFailed(`Merge failed: ${error.message}`);
139+
}

0 commit comments

Comments
 (0)