Merge pull request #17 from modelcontextprotocol/den/lgtm-accept-cleanup
slash-commands: label cleanup + project status on /lgtm accept
This commit is contained in:
commit
0d800ff744
3 changed files with 95 additions and 1 deletions
|
|
@ -11,7 +11,9 @@ Listens for PR comments and push events, then:
|
||||||
- **`/lgtm`** — **core maintainers only** (`always-allow-teams`). Adds an
|
- **`/lgtm`** — **core maintainers only** (`always-allow-teams`). Adds an
|
||||||
`accepted` label, submits an **APPROVE** review on behalf of the commenter,
|
`accepted` label, submits an **APPROVE** review on behalf of the commenter,
|
||||||
and **enables auto-merge**. The PR merges automatically once all required
|
and **enables auto-merge**. The PR merges automatically once all required
|
||||||
checks pass. CODEOWNERS does _not_ grant `/lgtm` authorization.
|
checks pass. CODEOWNERS does _not_ grant `/lgtm` authorization. Optionally
|
||||||
|
removes staging labels (`remove-labels-on-accept`) and moves the PR to an
|
||||||
|
_Accepted_ status in a configured org Project (`project-number`).
|
||||||
- **`/lgtm cancel`** — removes the `accepted` label, dismisses the approval,
|
- **`/lgtm cancel`** — removes the `accepted` label, dismisses the approval,
|
||||||
and disables auto-merge.
|
and disables auto-merge.
|
||||||
- **`/lgtm force`** — same as `/lgtm`, but **only** members of
|
- **`/lgtm force`** — same as `/lgtm`, but **only** members of
|
||||||
|
|
@ -215,6 +217,10 @@ jobs:
|
||||||
| `submit-review` | | `'true'` | Submit an APPROVE review alongside the label on `/lgtm`. Set `'false'` to use label only. |
|
| `submit-review` | | `'true'` | Submit an APPROVE review alongside the label on `/lgtm`. Set `'false'` to use label only. |
|
||||||
| `enable-auto-merge` | | `'true'` | Enable auto-merge after `/lgtm` (disabled on `/lgtm cancel` and push invalidation). Requires repo setting "Allow auto-merge". |
|
| `enable-auto-merge` | | `'true'` | Enable auto-merge after `/lgtm` (disabled on `/lgtm cancel` and push invalidation). Requires repo setting "Allow auto-merge". |
|
||||||
| `auto-merge-method` | | `squash` | Merge method for auto-merge: `squash`, `merge`, or `rebase` |
|
| `auto-merge-method` | | `squash` | Merge method for auto-merge: `squash`, `merge`, or `rebase` |
|
||||||
|
| `remove-labels-on-accept` | | _(empty)_ | Comma-separated label names to remove when `/lgtm` accepts (e.g. `in-review,draft,proposal`). Missing labels are ignored. |
|
||||||
|
| `project-number` | | _(empty)_ | Org-level Project (V2) number to update on accept. Empty = disabled. Adds the PR to the project if not already present. Requires the App to have **Organization Projects: write**. |
|
||||||
|
| `project-status-field` | | `Status` | Name of the single-select field to set on accept. |
|
||||||
|
| `project-accepted-option` | | `Accepted` | Name of the option to set in the status field on accept. |
|
||||||
| `stageblog-workflow` | | _(empty)_ | Workflow file name (e.g. `stage-blog.yml`) to dispatch when `/stageblog` is invoked. Empty = command disabled. The workflow must accept `pr_number` and `head_sha` string inputs. |
|
| `stageblog-workflow` | | _(empty)_ | Workflow file name (e.g. `stage-blog.yml`) to dispatch when `/stageblog` is invoked. Empty = command disabled. The workflow must accept `pr_number` and `head_sha` string inputs. |
|
||||||
| `stageblog-paths` | | `blog/**` | Comma-separated CODEOWNERS-style glob patterns. `/stageblog` is refused if no changed file matches. |
|
| `stageblog-paths` | | `blog/**` | Comma-separated CODEOWNERS-style glob patterns. `/stageblog` is refused if no changed file matches. |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,27 @@ inputs:
|
||||||
description: Merge method for auto-merge — one of squash, merge, rebase
|
description: Merge method for auto-merge — one of squash, merge, rebase
|
||||||
required: false
|
required: false
|
||||||
default: squash
|
default: squash
|
||||||
|
remove-labels-on-accept:
|
||||||
|
description: >-
|
||||||
|
Comma-separated label names to remove from the PR when /lgtm accepts it
|
||||||
|
(e.g. in-review,draft,proposal). Missing labels are ignored.
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
project-number:
|
||||||
|
description: >-
|
||||||
|
Org-level Project (V2) number to update when /lgtm accepts the PR. Leave
|
||||||
|
empty to disable project updates. The PR is added to the project if not
|
||||||
|
already present.
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
project-status-field:
|
||||||
|
description: Name of the single-select field to set on accept
|
||||||
|
required: false
|
||||||
|
default: Status
|
||||||
|
project-accepted-option:
|
||||||
|
description: Name of the option to set in the status field on accept
|
||||||
|
required: false
|
||||||
|
default: Accepted
|
||||||
stageblog-workflow:
|
stageblog-workflow:
|
||||||
description: >-
|
description: >-
|
||||||
Workflow file name (e.g. stage-blog.yml) to dispatch when /stageblog is
|
Workflow file name (e.g. stage-blog.yml) to dispatch when /stageblog is
|
||||||
|
|
@ -108,6 +129,10 @@ runs:
|
||||||
SUBMIT_REVIEW: ${{ inputs.submit-review }}
|
SUBMIT_REVIEW: ${{ inputs.submit-review }}
|
||||||
ENABLE_AUTO_MERGE: ${{ inputs.enable-auto-merge }}
|
ENABLE_AUTO_MERGE: ${{ inputs.enable-auto-merge }}
|
||||||
AUTO_MERGE_METHOD: ${{ inputs.auto-merge-method }}
|
AUTO_MERGE_METHOD: ${{ inputs.auto-merge-method }}
|
||||||
|
REMOVE_LABELS_ON_ACCEPT: ${{ inputs.remove-labels-on-accept }}
|
||||||
|
PROJECT_NUMBER: ${{ inputs.project-number }}
|
||||||
|
PROJECT_STATUS_FIELD: ${{ inputs.project-status-field }}
|
||||||
|
PROJECT_ACCEPTED_OPTION: ${{ inputs.project-accepted-option }}
|
||||||
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
|
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
|
||||||
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
|
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ module.exports = async ({ github, context, core }) => {
|
||||||
const submitReview = process.env.SUBMIT_REVIEW === 'true';
|
const submitReview = process.env.SUBMIT_REVIEW === 'true';
|
||||||
const autoMerge = process.env.ENABLE_AUTO_MERGE === 'true';
|
const autoMerge = process.env.ENABLE_AUTO_MERGE === 'true';
|
||||||
const autoMergeMethod = process.env.AUTO_MERGE_METHOD.toUpperCase();
|
const autoMergeMethod = process.env.AUTO_MERGE_METHOD.toUpperCase();
|
||||||
|
const removeLabelsOnAccept = process.env.REMOVE_LABELS_ON_ACCEPT
|
||||||
|
.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
const projectNumber = parseInt(process.env.PROJECT_NUMBER, 10) || 0;
|
||||||
|
const projectStatusField = process.env.PROJECT_STATUS_FIELD;
|
||||||
|
const projectAcceptedOption = process.env.PROJECT_ACCEPTED_OPTION;
|
||||||
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
||||||
|
|
||||||
function setResult(result, actor) {
|
function setResult(result, actor) {
|
||||||
|
|
@ -260,6 +265,62 @@ module.exports = async ({ github, context, core }) => {
|
||||||
core.info(`Auto-merge disable skipped: ${e.message}`);
|
core.info(`Auto-merge disable skipped: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function onAccept(prNumber, prNodeId) {
|
||||||
|
for (const label of removeLabelsOnAccept) {
|
||||||
|
await removeLabel(prNumber, label);
|
||||||
|
}
|
||||||
|
if (!projectNumber) return;
|
||||||
|
try {
|
||||||
|
const lookup = await github.graphql(
|
||||||
|
`query($org: String!, $num: Int!, $field: String!) {
|
||||||
|
organization(login: $org) {
|
||||||
|
projectV2(number: $num) {
|
||||||
|
id
|
||||||
|
field(name: $field) {
|
||||||
|
... on ProjectV2SingleSelectField { id options { id name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ org: owner, num: projectNumber, field: projectStatusField },
|
||||||
|
);
|
||||||
|
const project = lookup.organization?.projectV2;
|
||||||
|
if (!project) {
|
||||||
|
return core.warning(`Project #${projectNumber} not found in org ${owner}`);
|
||||||
|
}
|
||||||
|
const field = project.field;
|
||||||
|
if (!field?.id) {
|
||||||
|
return core.warning(`Field '${projectStatusField}' not found (or not single-select) in project #${projectNumber}`);
|
||||||
|
}
|
||||||
|
const option = field.options.find(o => o.name === projectAcceptedOption);
|
||||||
|
if (!option) {
|
||||||
|
return core.warning(`Option '${projectAcceptedOption}' not found in field '${projectStatusField}'`);
|
||||||
|
}
|
||||||
|
// addProjectV2ItemById is idempotent — returns the existing item if
|
||||||
|
// the PR is already in the project.
|
||||||
|
const add = await github.graphql(
|
||||||
|
`mutation($proj: ID!, $pr: ID!) {
|
||||||
|
addProjectV2ItemById(input: {projectId: $proj, contentId: $pr}) {
|
||||||
|
item { id }
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ proj: project.id, pr: prNodeId },
|
||||||
|
);
|
||||||
|
const itemId = add.addProjectV2ItemById.item.id;
|
||||||
|
await github.graphql(
|
||||||
|
`mutation($proj: ID!, $item: ID!, $field: ID!, $opt: String!) {
|
||||||
|
updateProjectV2ItemFieldValue(input: {
|
||||||
|
projectId: $proj, itemId: $item, fieldId: $field,
|
||||||
|
value: { singleSelectOptionId: $opt }
|
||||||
|
}) { projectV2Item { id } }
|
||||||
|
}`,
|
||||||
|
{ proj: project.id, item: itemId, field: field.id, opt: option.id },
|
||||||
|
);
|
||||||
|
core.info(`Moved PR to '${projectAcceptedOption}' in project #${projectNumber}`);
|
||||||
|
} catch (e) {
|
||||||
|
core.warning(`Project update failed: ${e.message}. Ensure the GitHub App has Organization Projects:write permission.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Dispatch ---------------------------------------------------
|
// --- Dispatch ---------------------------------------------------
|
||||||
const ev = context.eventName;
|
const ev = context.eventName;
|
||||||
|
|
@ -315,6 +376,7 @@ module.exports = async ({ github, context, core }) => {
|
||||||
return setResult('force-unauthorized', actor);
|
return setResult('force-unauthorized', actor);
|
||||||
}
|
}
|
||||||
await addLabel(prNumber, approvedLabel);
|
await addLabel(prNumber, approvedLabel);
|
||||||
|
await onAccept(prNumber, pr.node_id);
|
||||||
if (submitReview) await submitApproval(prNumber, actor, true);
|
if (submitReview) await submitApproval(prNumber, actor, true);
|
||||||
await react(commentId, '+1');
|
await react(commentId, '+1');
|
||||||
await enableAutoMerge(pr.node_id, prNumber);
|
await enableAutoMerge(pr.node_id, prNumber);
|
||||||
|
|
@ -385,6 +447,7 @@ module.exports = async ({ github, context, core }) => {
|
||||||
// Execute
|
// Execute
|
||||||
if (cmd === 'lgtm' && !cancel) {
|
if (cmd === 'lgtm' && !cancel) {
|
||||||
await addLabel(prNumber, approvedLabel);
|
await addLabel(prNumber, approvedLabel);
|
||||||
|
await onAccept(prNumber, pr.node_id);
|
||||||
if (submitReview) await submitApproval(prNumber, actor);
|
if (submitReview) await submitApproval(prNumber, actor);
|
||||||
await react(commentId, '+1');
|
await react(commentId, '+1');
|
||||||
await enableAutoMerge(pr.node_id, prNumber);
|
await enableAutoMerge(pr.node_id, prNumber);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue