Fix: require('@actions/github') not resolvable from external main.js
PR #5 broke the action in production — the require() fails with MODULE_NOT_FOUND because actions/github-script bundles @actions/github into dist/index.js; it's not in node_modules and not resolvable from a sibling require()'d file. Fix: flip the architecture. github-script's 'github' client now uses bot-token (GITHUB_TOKEN), so the single 'github' object serves all user-visible operations. The PAT is passed via env (ORG_TOKEN) and used ONLY for the team-membership call via github.request() with an explicit authorization header override. Side effect: PAT requirements shrink further. It now ONLY needs Organization Members:read — no repo perms, no Actions:write. All repo-level operations (including workflow dispatch) use GITHUB_TOKEN with the workflow's permissions: block.
This commit is contained in:
parent
658d01c6d7
commit
9fc2acf515
2 changed files with 32 additions and 31 deletions
|
|
@ -9,19 +9,19 @@ description: >-
|
||||||
inputs:
|
inputs:
|
||||||
github-token:
|
github-token:
|
||||||
description: >-
|
description: >-
|
||||||
Token for org team-membership checks and workflow dispatch. MUST have
|
Token used for org team-membership checks only. MUST have read:org
|
||||||
read:org scope — the default GITHUB_TOKEN cannot do this. Use a
|
scope — the default GITHUB_TOKEN cannot do this. Use a fine-grained PAT
|
||||||
fine-grained PAT with Organization Members:read (+ Actions:write if
|
with Organization Members:read, or a GitHub App installation token with
|
||||||
using /stageblog), or a GitHub App installation token with equivalent
|
the same.
|
||||||
perms.
|
|
||||||
required: true
|
required: true
|
||||||
bot-token:
|
bot-token:
|
||||||
description: >-
|
description: >-
|
||||||
Token used for user-visible side effects (comments, reactions, labels,
|
Token used for all repo API calls: comments, reactions, labels, reviews,
|
||||||
reviews). Defaults to the automatic GITHUB_TOKEN so these show as
|
CODEOWNERS fetch, listFiles, and workflow dispatch. Defaults to the
|
||||||
authored by github-actions[bot]. The caller workflow must grant this
|
automatic GITHUB_TOKEN so user-visible side effects show as authored by
|
||||||
token the necessary repo permissions (pull-requests: write,
|
github-actions[bot]. The caller workflow must grant this token the
|
||||||
issues: write).
|
necessary permissions (pull-requests: write, issues: write,
|
||||||
|
contents: read; plus actions: write for /stageblog).
|
||||||
required: false
|
required: false
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
approved-label:
|
approved-label:
|
||||||
|
|
@ -95,9 +95,9 @@ runs:
|
||||||
SUBMIT_REVIEW: ${{ inputs.submit-review }}
|
SUBMIT_REVIEW: ${{ inputs.submit-review }}
|
||||||
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
|
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
|
||||||
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
|
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
|
||||||
BOT_TOKEN: ${{ inputs.bot-token }}
|
ORG_TOKEN: ${{ inputs.github-token }}
|
||||||
with:
|
with:
|
||||||
github-token: ${{ inputs.github-token }}
|
github-token: ${{ inputs.bot-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 });
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,10 @@ module.exports = async ({ github, context, core }) => {
|
||||||
const invalidateOnPush = process.env.INVALIDATE_ON_PUSH === 'true';
|
const invalidateOnPush = process.env.INVALIDATE_ON_PUSH === 'true';
|
||||||
const submitReview = process.env.SUBMIT_REVIEW === 'true';
|
const submitReview = process.env.SUBMIT_REVIEW === 'true';
|
||||||
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
||||||
|
// `github` is authed with bot-token (defaults to GITHUB_TOKEN) so comments,
|
||||||
// Separate client for user-visible side effects (comments, reactions,
|
// reactions, labels, and reviews show as github-actions[bot]. The PAT is
|
||||||
// labels, reviews). Uses github.token so everything shows as authored by
|
// passed separately and used ONLY for team-membership checks (read:org).
|
||||||
// github-actions[bot] instead of the PAT's identity. The PAT (`github`)
|
const orgToken = process.env.ORG_TOKEN;
|
||||||
// is still used for team-membership checks (needs read:org), CODEOWNERS
|
|
||||||
// fetch, listFiles, pulls.get, and workflow dispatch.
|
|
||||||
const bot = require('@actions/github').getOctokit(process.env.BOT_TOKEN);
|
|
||||||
|
|
||||||
function setResult(result, actor) {
|
function setResult(result, actor) {
|
||||||
core.setOutput('result', result);
|
core.setOutput('result', result);
|
||||||
|
|
@ -83,9 +80,13 @@ module.exports = async ({ github, context, core }) => {
|
||||||
const key = `${teamSlug}:${user}`;
|
const key = `${teamSlug}:${user}`;
|
||||||
if (teamMembershipCache.has(key)) return teamMembershipCache.get(key);
|
if (teamMembershipCache.has(key)) return teamMembershipCache.get(key);
|
||||||
try {
|
try {
|
||||||
const { data } = await github.rest.teams.getMembershipForUserInOrg({
|
const { data } = await github.request(
|
||||||
org: owner, team_slug: teamSlug, username: user,
|
'GET /orgs/{org}/teams/{team_slug}/memberships/{username}',
|
||||||
});
|
{
|
||||||
|
org: owner, team_slug: teamSlug, username: user,
|
||||||
|
headers: { authorization: `token ${orgToken}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
const ok = data.state === 'active';
|
const ok = data.state === 'active';
|
||||||
teamMembershipCache.set(key, ok);
|
teamMembershipCache.set(key, ok);
|
||||||
return ok;
|
return ok;
|
||||||
|
|
@ -157,13 +158,13 @@ module.exports = async ({ github, context, core }) => {
|
||||||
|
|
||||||
// --- Label & review helpers -------------------------------------
|
// --- Label & review helpers -------------------------------------
|
||||||
async function addLabel(prNumber, label) {
|
async function addLabel(prNumber, label) {
|
||||||
await bot.rest.issues.addLabels({
|
await github.rest.issues.addLabels({
|
||||||
owner, repo, issue_number: prNumber, labels: [label],
|
owner, repo, issue_number: prNumber, labels: [label],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function removeLabel(prNumber, label) {
|
async function removeLabel(prNumber, label) {
|
||||||
try {
|
try {
|
||||||
await bot.rest.issues.removeLabel({
|
await github.rest.issues.removeLabel({
|
||||||
owner, repo, issue_number: prNumber, name: label,
|
owner, repo, issue_number: prNumber, name: label,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -171,25 +172,25 @@ module.exports = async ({ github, context, core }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function react(commentId, content) {
|
async function react(commentId, content) {
|
||||||
await bot.rest.reactions.createForIssueComment({
|
await github.rest.reactions.createForIssueComment({
|
||||||
owner, repo, comment_id: commentId, content,
|
owner, repo, comment_id: commentId, content,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function submitApproval(prNumber, onBehalfOf) {
|
async function submitApproval(prNumber, onBehalfOf) {
|
||||||
await bot.rest.pulls.createReview({
|
await github.rest.pulls.createReview({
|
||||||
owner, repo, pull_number: prNumber, event: 'APPROVE',
|
owner, repo, pull_number: prNumber, event: 'APPROVE',
|
||||||
body: `${reviewMarker}\nApproved on behalf of @${onBehalfOf} via \`/lgtm\`.`,
|
body: `${reviewMarker}\nApproved on behalf of @${onBehalfOf} via \`/lgtm\`.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function dismissBotApprovals(prNumber, reason) {
|
async function dismissBotApprovals(prNumber, reason) {
|
||||||
const reviews = await bot.paginate(bot.rest.pulls.listReviews, {
|
const reviews = await github.paginate(github.rest.pulls.listReviews, {
|
||||||
owner, repo, pull_number: prNumber, per_page: 100,
|
owner, repo, pull_number: prNumber, per_page: 100,
|
||||||
});
|
});
|
||||||
for (const r of reviews) {
|
for (const r of reviews) {
|
||||||
if (r.state !== 'APPROVED') continue;
|
if (r.state !== 'APPROVED') continue;
|
||||||
if (!r.body || !r.body.includes(reviewMarker)) continue;
|
if (!r.body || !r.body.includes(reviewMarker)) continue;
|
||||||
try {
|
try {
|
||||||
await bot.rest.pulls.dismissReview({
|
await github.rest.pulls.dismissReview({
|
||||||
owner, repo, pull_number: prNumber, review_id: r.id,
|
owner, repo, pull_number: prNumber, review_id: r.id,
|
||||||
message: reason,
|
message: reason,
|
||||||
});
|
});
|
||||||
|
|
@ -210,7 +211,7 @@ module.exports = async ({ github, context, core }) => {
|
||||||
if (!hasApproved) return setResult('noop');
|
if (!hasApproved) return setResult('noop');
|
||||||
await removeLabel(pr.number, approvedLabel);
|
await removeLabel(pr.number, approvedLabel);
|
||||||
await dismissBotApprovals(pr.number, 'New commits pushed — approval invalidated.');
|
await dismissBotApprovals(pr.number, 'New commits pushed — approval invalidated.');
|
||||||
await bot.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner, repo, issue_number: pr.number,
|
owner, repo, issue_number: pr.number,
|
||||||
body: `New commits were pushed — removed the \`${approvedLabel}\` label. Re-approve with \`/lgtm\`.`,
|
body: `New commits were pushed — removed the \`${approvedLabel}\` label. Re-approve with \`/lgtm\`.`,
|
||||||
});
|
});
|
||||||
|
|
@ -270,7 +271,7 @@ module.exports = async ({ github, context, core }) => {
|
||||||
const touchesBlog = files.some(f => blogRegexes.some(r => r.test(f.filename)));
|
const touchesBlog = files.some(f => blogRegexes.some(r => r.test(f.filename)));
|
||||||
if (!touchesBlog) {
|
if (!touchesBlog) {
|
||||||
await react(commentId, '-1');
|
await react(commentId, '-1');
|
||||||
await bot.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner, repo, issue_number: prNumber,
|
owner, repo, issue_number: prNumber,
|
||||||
body: `\`/stageblog\` refused — this PR does not touch any blog paths (\`${process.env.STAGEBLOG_PATHS}\`).`,
|
body: `\`/stageblog\` refused — this PR does not touch any blog paths (\`${process.env.STAGEBLOG_PATHS}\`).`,
|
||||||
});
|
});
|
||||||
|
|
@ -284,7 +285,7 @@ module.exports = async ({ github, context, core }) => {
|
||||||
inputs: { pr_number: String(prNumber), head_sha: headSha },
|
inputs: { pr_number: String(prNumber), head_sha: headSha },
|
||||||
});
|
});
|
||||||
await react(commentId, 'rocket');
|
await react(commentId, 'rocket');
|
||||||
await bot.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner, repo, issue_number: prNumber,
|
owner, repo, issue_number: prNumber,
|
||||||
body: `Blog staging triggered by @${actor} for \`${headSha.slice(0, 7)}\`. Watch the [**${stageblogWorkflow}** workflow](../actions/workflows/${stageblogWorkflow}) for the preview link.`,
|
body: `Blog staging triggered by @${actor} for \`${headSha.slice(0, 7)}\`. Watch the [**${stageblogWorkflow}** workflow](../actions/workflows/${stageblogWorkflow}) for the preview link.`,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue