This is an automated email from the ASF dual-hosted git repository.
mfordjody pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git
The following commit(s) were added to refs/heads/master by this push:
new de62b3cd Require maintainer lgtm before merging (#972)
de62b3cd is described below
commit de62b3cd1bd2feb84c6796ca06d398288abf67ef
Author: mfordjody <[email protected]>
AuthorDate: Mon Jul 20 14:11:43 2026 +0800
Require maintainer lgtm before merging (#972)
---
.github/workflows/commands.yml | 188 +++++++++++++++++++++++++++++++-------
.github/workflows/review-gate.yml | 36 +-------
OWNERS | 4 +-
3 files changed, 159 insertions(+), 69 deletions(-)
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 15024dd3..d5db43db 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -18,13 +18,11 @@
# /assign [@user ...] assign the issue/PR (self-assign needs no
permission;
# assigning others requires write access)
# /unassign [@user ...] remove assignees
-# /lgtm (PR only, write access, not the PR author) label the
-# PR with `lgtm`; if the commenter is an approver in
-# the root OWNERS file, record Maintainer Approval and
-# request a squash merge (or auto-merge while checks
-# run). Required status checks still enforce CI;
-# non-approvers only apply the label
-# /lgtm cancel remove the `lgtm` label and disable auto-merge
+# /lgtm (PR only) only an approver in the root OWNERS file
+# can record Maintainer Approval. The PR is squash
+# merged immediately when CI is green, or by the
+# workflow-run handler when CI finishes.
+# /lgtm cancel remove the `lgtm` label and reset approval
# /close close the issue/PR (author or write access)
# /reopen reopen the issue/PR (author or write access)
# /retest (PR only, write access) re-run failed workflow runs
@@ -43,8 +41,20 @@ on:
issue_comment:
types:
- created
+ workflow_run:
+ workflows:
+ - Dubbo Kubernetes CI
+ types:
+ - completed
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: Pull request to reconsider for an approved merge
+ required: true
+ type: number
permissions:
+ checks: read
contents: write
issues: write
pull-requests: write
@@ -57,6 +67,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
if: |
+ github.event_name == 'issue_comment' &&
github.repository == 'apache/dubbo-kubernetes' &&
startsWith(github.event.comment.body, '/')
env:
@@ -74,7 +85,7 @@ jobs:
# Keep the required status tied to the built-in GitHub Actions App even
# when slash-command comments and merges use a custom bot identity.
- name: Record maintainer approval
- if: github.event.issue.pull_request && github.actor == 'mfordjody'
+ if: github.event.issue.pull_request
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
@@ -86,16 +97,29 @@ jobs:
const {owner, repo} = context.repo;
const pullNumber = context.payload.issue.number;
const {data: pr} = await github.rest.pulls.get({owner, repo,
pull_number: pullNumber});
- const authorIsMaintainer = pr.user.login.toLowerCase() ===
'mfordjody';
- if (!match[1] && authorIsMaintainer) return;
+ const cancelled = !!match[1];
+ if (!cancelled && context.actor.toLowerCase() !== 'mfordjody')
return;
+ if (cancelled && context.actor.toLowerCase() !==
pr.user.login.toLowerCase()) {
+ try {
+ const {data: permission} = await
github.rest.repos.getCollaboratorPermissionLevel({
+ owner,
+ repo,
+ username: context.actor,
+ });
+ if (!['admin', 'maintain',
'write'].includes(permission.permission)) return;
+ } catch (error) {
+ core.info(`Approval cancellation denied: ${error.message}`);
+ return;
+ }
+ }
await github.rest.repos.createCommitStatus({
owner,
repo,
sha: pr.head.sha,
- state: match[1] && !authorIsMaintainer ? 'pending' : 'success',
+ state: cancelled ? 'pending' : 'success',
context: 'Maintainer Approval Gate',
- description: match[1] && !authorIsMaintainer
+ description: cancelled
? 'Maintainer approval cancelled'
: 'Approved by @mfordjody via /lgtm',
target_url: pr.html_url,
@@ -212,48 +236,51 @@ jobs:
await ack('+1');
return;
}
- if (commenter === issue.user.login) {
- await reply(`@${commenter}: you cannot \`/lgtm\` your own
pull request.`);
- return;
- }
if (!(await hasWriteAccess(commenter))) {
await reply(`@${commenter}: only collaborators with write
access can \`/lgtm\` a pull request.`);
return;
}
- try {
- await github.rest.issues.createLabel({owner, repo, name:
'lgtm', color: '15dd18', description: 'Looks good to me, ready to merge'});
- } catch (e) {
- core.info(`label exists: ${e.message}`);
- }
- await github.rest.issues.addLabels({owner, repo, issue_number:
issueNumber, labels: ['lgtm']});
- await ack('+1');
const approvers = await getApprovers();
if (!approvers.includes(commenter.toLowerCase())) {
const who = approvers.length
? approvers.map((a) => '@' + a).join(', ')
: 'nobody (OWNERS file missing or empty)';
- await reply(`\`lgtm\` label added by @${commenter}. Merging
requires \`/lgtm\` from an approver in the OWNERS file: ${who}.`);
+ await reply(`@${commenter}: merging requires \`/lgtm\` from
an approver in the OWNERS file: ${who}.`);
return;
}
+ try {
+ await github.rest.issues.createLabel({owner, repo, name:
'lgtm', color: '15dd18', description: 'Looks good to me, ready to merge'});
+ } catch (e) {
+ core.info(`label exists: ${e.message}`);
+ }
+ await github.rest.issues.addLabels({owner, repo, issue_number:
issueNumber, labels: ['lgtm']});
+ await ack('+1');
+
const {data: pr} = await github.rest.pulls.get({owner, repo,
pull_number: issueNumber});
if (pr.merged) {
return;
}
try {
- await github.rest.pulls.merge({owner, repo, pull_number:
issueNumber, merge_method: 'squash'});
+ await github.rest.pulls.merge({
+ owner,
+ repo,
+ pull_number: issueNumber,
+ merge_method: 'squash',
+ sha: pr.head.sha,
+ });
await reply(`Merged on behalf of @${commenter} via
\`/lgtm\`.`);
} catch (mergeErr) {
- try {
- await github.graphql(
- `mutation($id: ID!) { enablePullRequestAutoMerge(input:
{pullRequestId: $id, mergeMethod: SQUASH}) { pullRequest { number } } }`,
- {id: pr.node_id},
- );
- await reply(`\`lgtm\` recorded by @${commenter}.
Auto-merge (squash) enabled — the PR will merge once required checks pass.`);
- } catch (autoErr) {
- await reply(`\`lgtm\` recorded by @${commenter}, but the
PR could not be merged automatically:\n\n- merge: ${mergeErr.message}\n-
auto-merge: ${autoErr.message}\n\nPlease merge manually once checks pass.`);
- }
+ core.info(`Merge deferred until CI finishes:
${mergeErr.message}`);
+ await github.rest.actions.createWorkflowDispatch({
+ owner,
+ repo,
+ workflow_id: 'commands.yml',
+ ref: pr.base.ref,
+ inputs: {pr_number: String(pr.number)},
+ });
+ await reply(`\`/lgtm\` recorded by @${commenter}. The PR
will be squash merged automatically after required CI passes.`);
}
return;
}
@@ -302,3 +329,96 @@ jobs:
return;
}
}
+
+ merge-approved:
+ name: Merge Approved Pull Request
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ if: |
+ github.repository == 'apache/dubbo-kubernetes' &&
+ (github.event_name == 'workflow_dispatch' ||
+ (github.event_name == 'workflow_run' &&
+ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.conclusion == 'success'))
+ steps:
+ - name: Merge current-head pull request approved via /lgtm
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const {owner, repo} = context.repo;
+ let headSha;
+ let candidates;
+ if (context.eventName === 'workflow_dispatch') {
+ const pullNumber = Number(context.payload.inputs.pr_number);
+ const {data: pr} = await github.rest.pulls.get({owner, repo,
pull_number: pullNumber});
+ headSha = pr.head.sha;
+ candidates = [pr];
+ } else {
+ headSha = context.payload.workflow_run.head_sha;
+ const {data: associated} = await
github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner,
+ repo,
+ commit_sha: headSha,
+ });
+ candidates = associated.filter((pr) =>
+ pr.state === 'open' &&
+ pr.head.sha === headSha &&
+ ['master', 'main'].includes(pr.base.ref),
+ );
+ }
+
+ for (const candidate of candidates) {
+ const {data: pr} = await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number: candidate.number,
+ });
+ const labels = new Set(pr.labels.map((label) => label.name));
+ if (pr.state !== 'open' || pr.head.sha !== headSha ||
+ !['master', 'main'].includes(pr.base.ref) ||
!labels.has('lgtm')) {
+ core.info(`#${pr.number}: missing lgtm label`);
+ continue;
+ }
+
+ const {data: combined} = await
github.rest.repos.getCombinedStatusForRef({
+ owner,
+ repo,
+ ref: headSha,
+ });
+ const approval = combined.statuses.find((status) =>
+ status.context === 'Maintainer Approval Gate',
+ );
+ if (approval?.state !== 'success') {
+ core.info(`#${pr.number}: maintainer approval is
${approval?.state || 'missing'}`);
+ continue;
+ }
+
+ const {data: checks} = await github.rest.checks.listForRef({
+ owner,
+ repo,
+ ref: headSha,
+ per_page: 100,
+ });
+ const requiredCI = checks.check_runs.find((check) =>
+ check.name === 'CI Required' && check.app?.slug ===
'github-actions',
+ );
+ if (requiredCI?.conclusion !== 'success') {
+ core.info(`#${pr.number}: CI Required is
${requiredCI?.conclusion || requiredCI?.status || 'missing'}`);
+ continue;
+ }
+
+ try {
+ const {data: result} = await github.rest.pulls.merge({
+ owner,
+ repo,
+ pull_number: pr.number,
+ merge_method: 'squash',
+ sha: headSha,
+ });
+ if (!result.merged) {
+ core.setFailed(`#${pr.number}: ${result.message}`);
+ }
+ } catch (error) {
+ core.setFailed(`#${pr.number}: ${error.message}`);
+ }
+ }
diff --git a/.github/workflows/review-gate.yml
b/.github/workflows/review-gate.yml
index be8f2885..855c8bab 100644
--- a/.github/workflows/review-gate.yml
+++ b/.github/workflows/review-gate.yml
@@ -22,10 +22,6 @@ on:
- reopened
- synchronize
- ready_for_review
- pull_request_review:
- types:
- - submitted
- - dismissed
push:
branches:
- master
@@ -46,17 +42,13 @@ jobs:
name: Publish Maintainer Approval
runs-on: ubuntu-latest
timeout-minutes: 5
- if: |
- github.repository == 'apache/dubbo-kubernetes' &&
- (github.event_name != 'pull_request_review' ||
- github.event.review.user.login == 'mfordjody')
+ if: github.repository == 'apache/dubbo-kubernetes'
steps:
- name: Publish approval status
uses: actions/github-script@v7
with:
script: |
const {owner, repo} = context.repo;
- const maintainer = 'mfordjody';
const publish = async (sha, state, description, targetUrl) => {
await github.rest.repos.createCommitStatus({
owner,
@@ -89,31 +81,9 @@ jobs:
return;
}
- const authorIsMaintainer = pr.user.login.toLowerCase() ===
maintainer;
- let approved = authorIsMaintainer;
- if (!approved) {
- const reviews = await github.paginate('GET
/repos/{owner}/{repo}/pulls/{pull_number}/reviews', {
- owner,
- repo,
- pull_number: pr.number,
- per_page: 100,
- });
- const currentApprovals = reviews.filter((review) =>
- review.user?.login?.toLowerCase() === maintainer &&
- String(review.state).toUpperCase() === 'APPROVED' &&
- review.commit_id === pr.head.sha,
- );
- core.info(`Found ${currentApprovals.length} current approval(s)
from @${maintainer} among ${reviews.length} review(s)`);
- approved = currentApprovals.length > 0;
- }
-
await publish(
pr.head.sha,
- approved ? 'success' : 'pending',
- authorIsMaintainer
- ? 'Maintainer-authored pull request'
- : approved
- ? `Approved by @${maintainer}`
- : `Waiting for @${maintainer} approval`,
+ 'pending',
+ 'Waiting for @mfordjody /lgtm',
pr.html_url,
);
diff --git a/OWNERS b/OWNERS
index 9e7261b5..6b9cf1b8 100644
--- a/OWNERS
+++ b/OWNERS
@@ -14,8 +14,8 @@
# limitations under the License.
# OWNERS drives the /lgtm merge command in .github/workflows/commands.yml.
-# The required Maintainer Approval status independently enforces approval by
-# an approver before another user's pull request can merge.
+# The required Maintainer Approval status independently enforces /lgtm from
+# an approver before any pull request can merge.
approvers:
- mfordjody