slash-commands: collapse github-token and bot-token into one input
The two-token split existed because the old design used GITHUB_TOKEN for user-visible writes (so they show as github-actions[bot]) and a separate PAT with read:org for the team check. With the GitHub App flow, one App installation token covers both roles. - Drop the bot-token input; github-token now handles all API calls - Replace the raw-fetch team check with github.rest.teams.getMembershipForUserInOrg — the main Octokit client is now authed with the same token - Remove the ORG_TOKEN env plumbing and the explanatory comment about why we couldn't use Octokit before
This commit is contained in:
parent
63db292169
commit
e518d5be36
3 changed files with 24 additions and 53 deletions
|
|
@ -103,7 +103,7 @@ After creating:
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Triggers** | `issue_comment` (types: `created`) + `pull_request_target` (types: `synchronize`) |
|
| **Triggers** | `issue_comment` (types: `created`) + `pull_request_target` (types: `synchronize`) |
|
||||||
| **Permissions** | `contents: read` is sufficient — the App token handles all writes. If falling back to `GITHUB_TOKEN` for `bot-token`, add `pull-requests: write`, `issues: write`. |
|
| **Permissions** | `contents: read` is sufficient — the App token handles all writes. |
|
||||||
| **Fork-PR safety** | No special guard needed — `issue_comment` and `pull_request_target` both run in the **base** repo's context with the base workflow definition, so fork authors cannot modify the logic. CODEOWNERS is also fetched from the PR's **base** ref, never the head. |
|
| **Fork-PR safety** | No special guard needed — `issue_comment` and `pull_request_target` both run in the **base** repo's context with the base workflow definition, so fork authors cannot modify the logic. CODEOWNERS is also fetched from the PR's **base** ref, never the head. |
|
||||||
| **No checkout** | The action calls GitHub API only. Do not `actions/checkout` PR code. |
|
| **No checkout** | The action calls GitHub API only. Do not `actions/checkout` PR code. |
|
||||||
|
|
||||||
|
|
@ -137,7 +137,6 @@ jobs:
|
||||||
- uses: modelcontextprotocol/actions/slash-commands@main
|
- uses: modelcontextprotocol/actions/slash-commands@main
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.app-token.outputs.token }}
|
github-token: ${{ steps.app-token.outputs.token }}
|
||||||
bot-token: ${{ steps.app-token.outputs.token }}
|
|
||||||
# auto-merge-method: squash # default; also: merge, rebase
|
# auto-merge-method: squash # default; also: merge, rebase
|
||||||
# stageblog-workflow: stage-blog.yml # uncomment to enable /stageblog
|
# stageblog-workflow: stage-blog.yml # uncomment to enable /stageblog
|
||||||
```
|
```
|
||||||
|
|
@ -196,8 +195,7 @@ jobs:
|
||||||
|
|
||||||
| Input | Required | Default | Description |
|
| Input | Required | Default | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `github-token` | ✅ | — | Token with `read:org` (Organization Members: read) for the team-membership check. Pass the App installation token. **Default `GITHUB_TOKEN` will not work** — it lacks `read:org`. |
|
| `github-token` | ✅ | — | GitHub App installation token used for everything: team-membership checks, labels, comments, reactions, APPROVE review, auto-merge, CODEOWNERS fetch, workflow dispatch. Must have Organization Members:read + Repository Pull requests:write + Contents:read (+ Actions:write for `/stageblog`). **Default `GITHUB_TOKEN` will not work.** |
|
||||||
| `bot-token` | | `${{ github.token }}` | Token for all repo API calls: comments, reactions, labels, reviews, CODEOWNERS fetch, file listing, workflow dispatch, auto-merge. Pass the App installation token so approvals come from the App identity. |
|
|
||||||
| `approved-label` | | `accepted` | Label added by `/lgtm` to mark the PR as accepted |
|
| `approved-label` | | `accepted` | Label added by `/lgtm` to mark the PR as accepted |
|
||||||
| `hold-label` | | `do-not-merge/hold` | Label added by `/hold` |
|
| `hold-label` | | `do-not-merge/hold` | Label added by `/hold` |
|
||||||
| `always-allow-teams` | | `core-maintainers` | Comma-separated team slugs (in the repo's org) whose members can `/lgtm` any PR. **Only these teams can `/lgtm`** — CODEOWNERS does not grant it. Members may also `/hold`/`/stageblog` any PR. |
|
| `always-allow-teams` | | `core-maintainers` | Comma-separated team slugs (in the repo's org) whose members can `/lgtm` any PR. **Only these teams can `/lgtm`** — CODEOWNERS does not grant it. Members may also `/hold`/`/stageblog` any PR. |
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,13 @@ description: >-
|
||||||
inputs:
|
inputs:
|
||||||
github-token:
|
github-token:
|
||||||
description: >-
|
description: >-
|
||||||
Token used for org team-membership checks only. MUST have read:org
|
GitHub App installation token used for all API calls: team-membership
|
||||||
scope — the default GITHUB_TOKEN cannot do this. Use a fine-grained PAT
|
checks, labels, comments, reactions, reviews, auto-merge, CODEOWNERS
|
||||||
with Organization Members:read, or a GitHub App installation token with
|
fetch, and workflow dispatch. Must have Organization Members:read plus
|
||||||
the same.
|
Repository Pull requests:write and Contents:read (and Actions:write if
|
||||||
|
/stageblog is enabled). The default GITHUB_TOKEN will NOT work — it
|
||||||
|
lacks read:org and its approvals may not count toward required reviews.
|
||||||
required: true
|
required: true
|
||||||
bot-token:
|
|
||||||
description: >-
|
|
||||||
Token used for all repo API calls: comments, reactions, labels, reviews,
|
|
||||||
CODEOWNERS fetch, listFiles, and workflow dispatch. Defaults to the
|
|
||||||
automatic GITHUB_TOKEN so user-visible side effects show as authored by
|
|
||||||
github-actions[bot]. The caller workflow must grant this token the
|
|
||||||
necessary permissions (pull-requests: write, issues: write,
|
|
||||||
contents: read; plus actions: write for /stageblog).
|
|
||||||
required: false
|
|
||||||
default: ${{ github.token }}
|
|
||||||
approved-label:
|
approved-label:
|
||||||
description: Label added by /lgtm to mark the PR as accepted
|
description: Label added by /lgtm to mark the PR as accepted
|
||||||
required: false
|
required: false
|
||||||
|
|
@ -118,9 +110,8 @@ runs:
|
||||||
AUTO_MERGE_METHOD: ${{ inputs.auto-merge-method }}
|
AUTO_MERGE_METHOD: ${{ inputs.auto-merge-method }}
|
||||||
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
|
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
|
||||||
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
|
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
|
||||||
ORG_TOKEN: ${{ inputs.github-token }}
|
|
||||||
with:
|
with:
|
||||||
github-token: ${{ inputs.bot-token }}
|
github-token: ${{ inputs.github-token }}
|
||||||
script: |
|
script: |
|
||||||
const main = require(process.env.GITHUB_ACTION_PATH + '/main.js');
|
const main = require(process.env.GITHUB_ACTION_PATH + '/main.js');
|
||||||
await main({ github, context, core });
|
await main({ github, context, core });
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,6 @@ module.exports = async ({ github, context, core }) => {
|
||||||
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 reviewMarker = '<!-- slash-commands-lgtm -->';
|
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
||||||
// `github` is authed with bot-token (defaults to GITHUB_TOKEN) so comments,
|
|
||||||
// reactions, labels, and reviews show as github-actions[bot]. The PAT is
|
|
||||||
// passed via env and used ONLY for the raw-fetch team-membership check
|
|
||||||
// below — we cannot construct a second Octokit client here because
|
|
||||||
// @actions/github is bundled into github-script's dist/index.js and not
|
|
||||||
// resolvable, nor does a per-request `headers.authorization` override work
|
|
||||||
// (Octokit's auth hook overwrites it).
|
|
||||||
const orgToken = process.env.ORG_TOKEN;
|
|
||||||
|
|
||||||
function setResult(result, actor) {
|
function setResult(result, actor) {
|
||||||
core.setOutput('result', result);
|
core.setOutput('result', result);
|
||||||
|
|
@ -87,32 +79,22 @@ module.exports = async ({ github, context, core }) => {
|
||||||
async function isTeamMember(user, teamSlug) {
|
async function isTeamMember(user, teamSlug) {
|
||||||
const key = `${teamSlug}:${user}`;
|
const key = `${teamSlug}:${user}`;
|
||||||
if (teamMembershipCache.has(key)) return teamMembershipCache.get(key);
|
if (teamMembershipCache.has(key)) return teamMembershipCache.get(key);
|
||||||
const res = await fetch(
|
let ok;
|
||||||
`https://api.github.com/orgs/${encodeURIComponent(owner)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(user)}`,
|
try {
|
||||||
{
|
const { data } = await github.rest.teams.getMembershipForUserInOrg({
|
||||||
headers: {
|
org: owner, team_slug: teamSlug, username: user,
|
||||||
authorization: `Bearer ${orgToken}`,
|
});
|
||||||
accept: 'application/vnd.github+json',
|
ok = data.state === 'active';
|
||||||
'x-github-api-version': '2022-11-28',
|
} catch (e) {
|
||||||
},
|
if (e.status === 404) {
|
||||||
},
|
ok = false;
|
||||||
);
|
} else if (e.status === 403) {
|
||||||
if (res.status === 404) {
|
core.warning(`Team membership check for @${owner}/${teamSlug} returned 403 — the provided github-token likely lacks Organization Members:read. Treating as not-a-member.`);
|
||||||
teamMembershipCache.set(key, false);
|
ok = false;
|
||||||
return false;
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (res.status === 403) {
|
|
||||||
// Almost always means the token lacks read:org.
|
|
||||||
core.warning(`Team membership check for @${owner}/${teamSlug} returned 403 — the provided github-token likely lacks read:org (Organization Members:read) scope. Treating as not-a-member.`);
|
|
||||||
teamMembershipCache.set(key, false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!res.ok) {
|
|
||||||
const body = await res.text();
|
|
||||||
throw new Error(`Team membership check failed: ${res.status} ${body}`);
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
const ok = data.state === 'active';
|
|
||||||
teamMembershipCache.set(key, ok);
|
teamMembershipCache.set(key, ok);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue