mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix: target intended users in lgtm approvals (#7023)
Maintainers frequently direct lgtm at tagged users who volunteer on an issue, but approval previously applied to the issue or PR author. Mentioned users are now approval targets, with the author as the fallback. Prose and examples can also contain lgtm without intending approval. Only treat lgtm or lgtmi at the start, after leading mentions, or at the end as commands. Process PR comments too, where approvals previously had no effect.
This commit is contained in:
parent
37bedb7f0b
commit
249008144c
4 changed files with 93 additions and 49 deletions
118
.github/workflows/approve-contributor.yml
vendored
118
.github/workflows/approve-contributor.yml
vendored
|
|
@ -6,11 +6,11 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
approve:
|
approve:
|
||||||
if: ${{ !github.event.issue.pull_request }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
@ -30,17 +30,18 @@ jobs:
|
||||||
const commenter = context.payload.comment.user.login;
|
const commenter = context.payload.comment.user.login;
|
||||||
const commentBody = (context.payload.comment.body || '').trim();
|
const commentBody = (context.payload.comment.body || '').trim();
|
||||||
|
|
||||||
let targetCapability;
|
const approvalAtStartPattern = /^[\s.]*(?:@[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\s*,\s*|[.:]\s*|\s+))*(lgtmi|lgtm)(?=$|[\s.])/i;
|
||||||
if (/\blgtmi\b/i.test(commentBody)) {
|
const approvalAtEndPattern = /(?:^|[\s.])(lgtmi|lgtm)[\s.]*$/i;
|
||||||
targetCapability = 'issue';
|
const approvalMatch = commentBody.match(approvalAtStartPattern) ?? commentBody.match(approvalAtEndPattern);
|
||||||
} else if (/\blgtm\b/i.test(commentBody)) {
|
|
||||||
targetCapability = 'pr';
|
if (!approvalMatch) {
|
||||||
} else {
|
console.log('Comment does not start or end with lgtm or lgtmi');
|
||||||
console.log('Comment does not match lgtm or lgtmi');
|
|
||||||
core.setOutput('status', 'skipped');
|
core.setOutput('status', 'skipped');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetCapability = approvalMatch[1].toLowerCase() === 'lgtmi' ? 'issue' : 'pr';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
|
const { data: permissionLevel } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
|
|
@ -59,6 +60,24 @@ jobs:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseMentionedUsers(body) {
|
||||||
|
const users = [];
|
||||||
|
const seenUsers = new Set();
|
||||||
|
const mentionPattern = /(^|[^A-Za-z0-9_])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?)(?![A-Za-z0-9-]|\/)/g;
|
||||||
|
|
||||||
|
for (const match of body.matchAll(mentionPattern)) {
|
||||||
|
const username = match[2];
|
||||||
|
const normalizedUser = username.toLowerCase();
|
||||||
|
if (seenUsers.has(normalizedUser)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenUsers.add(normalizedUser);
|
||||||
|
users.push(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
function parseApprovedUsers(content) {
|
function parseApprovedUsers(content) {
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
const entries = [];
|
const entries = [];
|
||||||
|
|
@ -113,62 +132,87 @@ jobs:
|
||||||
|
|
||||||
const content = fs.readFileSync(APPROVED_FILE, 'utf8');
|
const content = fs.readFileSync(APPROVED_FILE, 'utf8');
|
||||||
const { entries, users } = parseApprovedUsers(content);
|
const { entries, users } = parseApprovedUsers(content);
|
||||||
const normalizedAuthor = issueAuthor.toLowerCase();
|
const mentionedUsers = parseMentionedUsers(commentBody);
|
||||||
const existingEntry = users.get(normalizedAuthor);
|
const approvalTargets = mentionedUsers.length > 0 ? mentionedUsers : [issueAuthor];
|
||||||
|
const changedTargets = [];
|
||||||
|
const alreadyTargets = [];
|
||||||
|
|
||||||
|
for (const username of approvalTargets) {
|
||||||
|
const normalizedUser = username.toLowerCase();
|
||||||
|
const existingEntry = users.get(normalizedUser);
|
||||||
const existingCapability = existingEntry?.capability ?? null;
|
const existingCapability = existingEntry?.capability ?? null;
|
||||||
|
|
||||||
if (existingCapability === 'pr' || existingCapability === targetCapability) {
|
if (existingCapability === 'pr' || existingCapability === targetCapability) {
|
||||||
core.setOutput('status', 'already');
|
alreadyTargets.push(existingEntry?.username ?? username);
|
||||||
core.setOutput('capability', existingCapability);
|
console.log(`${username} is already approved for ${existingCapability}`);
|
||||||
console.log(`${issueAuthor} is already approved for ${existingCapability}`);
|
continue;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingEntry) {
|
if (existingEntry) {
|
||||||
existingEntry.capability = targetCapability;
|
existingEntry.capability = targetCapability;
|
||||||
|
changedTargets.push(existingEntry.username);
|
||||||
} else {
|
} else {
|
||||||
entries.push({ type: 'user', username: issueAuthor, normalizedUser: normalizedAuthor, capability: targetCapability });
|
const entry = { type: 'user', username, normalizedUser, capability: targetCapability };
|
||||||
|
entries.push(entry);
|
||||||
|
users.set(normalizedUser, entry);
|
||||||
|
changedTargets.push(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Set ${username} capability to ${targetCapability}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput('capability', targetCapability);
|
||||||
|
core.setOutput('changed_targets', JSON.stringify(changedTargets));
|
||||||
|
core.setOutput('already_targets', JSON.stringify(alreadyTargets));
|
||||||
|
|
||||||
|
if (changedTargets.length === 0) {
|
||||||
|
core.setOutput('status', 'already');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(APPROVED_FILE, stringifyApprovedUsers(entries));
|
fs.writeFileSync(APPROVED_FILE, stringifyApprovedUsers(entries));
|
||||||
core.setOutput('status', existingCapability ? 'updated' : 'added');
|
core.setOutput('status', 'changed');
|
||||||
core.setOutput('capability', targetCapability);
|
|
||||||
console.log(`Set ${issueAuthor} capability to ${targetCapability}`);
|
|
||||||
|
|
||||||
- name: Commit and push
|
- name: Commit and push
|
||||||
if: steps.update.outputs.status == 'added' || steps.update.outputs.status == 'updated'
|
if: steps.update.outputs.status == 'changed'
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git add .github/APPROVED_CONTRIBUTORS
|
git add .github/APPROVED_CONTRIBUTORS
|
||||||
git diff --staged --quiet || git commit -m "chore: approve contributor ${{ github.event.issue.user.login }}"
|
git diff --staged --quiet || git commit -m "chore: approve contributors from issue #${{ github.event.issue.number }}"
|
||||||
git push
|
git push
|
||||||
|
|
||||||
- name: Comment on issue
|
- name: Comment on issue
|
||||||
if: steps.update.outputs.status == 'added' || steps.update.outputs.status == 'updated' || steps.update.outputs.status == 'already'
|
if: steps.update.outputs.status == 'changed' || steps.update.outputs.status == 'already'
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
env:
|
||||||
|
CAPABILITY: ${{ steps.update.outputs.capability }}
|
||||||
|
CHANGED_TARGETS: ${{ steps.update.outputs.changed_targets }}
|
||||||
|
ALREADY_TARGETS: ${{ steps.update.outputs.already_targets }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const issueAuthor = context.payload.issue.user.login;
|
const capability = process.env.CAPABILITY;
|
||||||
const capability = '${{ steps.update.outputs.capability }}';
|
const changedTargets = JSON.parse(process.env.CHANGED_TARGETS || '[]');
|
||||||
|
const alreadyTargets = JSON.parse(process.env.ALREADY_TARGETS || '[]');
|
||||||
const defaultBranch = context.payload.repository.default_branch;
|
const defaultBranch = context.payload.repository.default_branch;
|
||||||
let body;
|
const formatTargets = (targets) => targets.map((target) => `@${target}`).join(', ');
|
||||||
|
const bodyLines = [];
|
||||||
|
|
||||||
if ('${{ steps.update.outputs.status }}' === 'already') {
|
if (changedTargets.length > 0) {
|
||||||
body = `@${issueAuthor} is already approved.`;
|
if (capability === 'issue') {
|
||||||
} else if (capability === 'issue') {
|
bodyLines.push(`${formatTargets(changedTargets)} approved for issues. Future issues will not be auto-closed. PRs still require \`lgtm\` at the start of a maintainer reply (optionally after one or more \`@username\` mentions) or at the end.`);
|
||||||
body = [
|
|
||||||
`@${issueAuthor} approved for issues. Your future issues will not be auto-closed. PRs still require \`lgtm\`.`,
|
|
||||||
'',
|
|
||||||
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
|
|
||||||
].join('\n');
|
|
||||||
} else {
|
} else {
|
||||||
body = [
|
bodyLines.push(`${formatTargets(changedTargets)} approved for issues and PRs. Future issues and PRs will not be auto-closed.`);
|
||||||
`@${issueAuthor} approved for issues and PRs. Your future issues and PRs will not be auto-closed.`,
|
|
||||||
'',
|
|
||||||
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alreadyTargets.length > 0) {
|
||||||
|
const verb = alreadyTargets.length === 1 ? 'is' : 'are';
|
||||||
|
bodyLines.push(`${formatTargets(alreadyTargets)} ${verb} already approved.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyLines.push('', `See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`);
|
||||||
|
const body = bodyLines.join('\n');
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
|
|
|
||||||
2
.github/workflows/issue-gate.yml
vendored
2
.github/workflows/issue-gate.yml
vendored
|
|
@ -101,7 +101,7 @@ jobs:
|
||||||
'',
|
'',
|
||||||
`Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`,
|
`Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`,
|
||||||
'',
|
'',
|
||||||
'If a maintainer replies `lgtmi` on one of your issues, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open.',
|
'If a maintainer replies `lgtmi` on one of your issues, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open. The command must be at the start of the reply (optionally after one or more `@username` mentions) or at the end.',
|
||||||
'',
|
'',
|
||||||
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
|
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
|
||||||
4
.github/workflows/pr-gate.yml
vendored
4
.github/workflows/pr-gate.yml
vendored
|
|
@ -116,11 +116,11 @@ jobs:
|
||||||
console.log(`${prAuthor} is not approved, closing PR`);
|
console.log(`${prAuthor} is not approved, closing PR`);
|
||||||
|
|
||||||
const message = [
|
const message = [
|
||||||
'This PR was auto-closed. Only contributors approved with `lgtm` can open PRs. Open an issue first.',
|
'This PR was auto-closed. Only contributors approved with `lgtm` can open PRs. Open an issue first and ask a maintainer for approval.',
|
||||||
'',
|
'',
|
||||||
`Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`,
|
`Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`,
|
||||||
'',
|
'',
|
||||||
'If a maintainer replies `lgtmi`, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open.',
|
'If a maintainer replies `lgtmi`, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open. The command must be at the start of the reply (optionally after one or more `@username` mentions) or at the end.',
|
||||||
'',
|
'',
|
||||||
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
|
`See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ Approval happens through maintainer replies on issues:
|
||||||
- `lgtmi`: your future issues will not be auto-closed
|
- `lgtmi`: your future issues will not be auto-closed
|
||||||
- `lgtm`: your future issues and PRs will not be auto-closed
|
- `lgtm`: your future issues and PRs will not be auto-closed
|
||||||
|
|
||||||
`lgtmi` does not grant rights to submit PRs. Only `lgtm` grants rights to submit PRs.
|
The command must be at the start of the reply (optionally after one or more `@username` mentions) or at the end. `lgtmi` does not grant rights to submit PRs. Only `lgtm` grants rights to submit PRs.
|
||||||
|
|
||||||
## Quality Bar For Issues
|
## Quality Bar For Issues
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ If you open an issue, keep it short, concrete, and worth reading.
|
||||||
- Explain why it matters.
|
- Explain why it matters.
|
||||||
- If you want to implement the change yourself, say so.
|
- If you want to implement the change yourself, say so.
|
||||||
|
|
||||||
If the issue is real and written well, a maintainer may reopen it, reply `lgtmi`, or reply `lgtm`.
|
If the issue is real and written well, a maintainer may reopen it or reply with `lgtmi` or `lgtm` in the command position described above.
|
||||||
|
|
||||||
## Blocking
|
## Blocking
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ If you send a large volume of issues through automation, your GitHub account wil
|
||||||
|
|
||||||
## Before Submitting a PR
|
## Before Submitting a PR
|
||||||
|
|
||||||
Do not open a PR unless you have already been approved with `lgtm`.
|
Do not open a PR unless you have already been approved by a maintainer using `lgtm` in the command position described above.
|
||||||
|
|
||||||
Before submitting a PR:
|
Before submitting a PR:
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue