Use raw fetch for team membership check — bypass Octokit auth entirely
The previous attempt (constructing orgClient via require('@actions/github')
inside the inline script) fails with MODULE_NOT_FOUND — the package is
bundled into github-script's dist/index.js and not resolvable even from
the eval'd inline script.
Switch to a raw fetch() call for the team membership endpoint. Node 20
(github-script@v8) has global fetch, so this has zero dependencies and
is immune to Octokit's auth-hook override behavior.
This commit is contained in:
parent
bd238ed6e4
commit
5cfe4d9921
2 changed files with 37 additions and 32 deletions
|
|
@ -99,11 +99,5 @@ runs:
|
||||||
with:
|
with:
|
||||||
github-token: ${{ inputs.bot-token }}
|
github-token: ${{ inputs.bot-token }}
|
||||||
script: |
|
script: |
|
||||||
// Construct a second Octokit client authed with the PAT for
|
|
||||||
// org team-membership checks. This has to happen here, not in
|
|
||||||
// main.js, because @actions/github is only resolvable from
|
|
||||||
// inside the github-script bundle.
|
|
||||||
const { getOctokit } = require('@actions/github');
|
|
||||||
const orgClient = getOctokit(process.env.ORG_TOKEN);
|
|
||||||
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, orgClient });
|
await main({ github, context, core });
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// Handler for /lgtm, /hold, and /stageblog slash commands. Called by actions/github-script
|
// Handler for /lgtm, /hold, and /stageblog slash commands. Called by actions/github-script
|
||||||
// via `require(...)({ github, context, core, orgClient })`.
|
// via `require(github.action_path + '/main.js')({ github, context, core })`.
|
||||||
// See action.yml for input wiring via env vars.
|
// See action.yml for input wiring via env vars.
|
||||||
|
|
||||||
module.exports = async ({ github, context, core, orgClient }) => {
|
module.exports = async ({ github, context, core }) => {
|
||||||
const { owner, repo } = context.repo;
|
const { owner, repo } = context.repo;
|
||||||
const approvedLabel = process.env.APPROVED_LABEL;
|
const approvedLabel = process.env.APPROVED_LABEL;
|
||||||
const holdLabel = process.env.HOLD_LABEL;
|
const holdLabel = process.env.HOLD_LABEL;
|
||||||
|
|
@ -13,10 +13,13 @@ module.exports = async ({ github, context, core, orgClient }) => {
|
||||||
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,
|
// `github` is authed with bot-token (defaults to GITHUB_TOKEN) so comments,
|
||||||
// reactions, labels, and reviews show as github-actions[bot]. `orgClient`
|
// reactions, labels, and reviews show as github-actions[bot]. The PAT is
|
||||||
// is a separate Octokit instance authed with the PAT and used ONLY for
|
// passed via env and used ONLY for the raw-fetch team-membership check
|
||||||
// team-membership checks (read:org). It must be constructed by the caller
|
// below — we cannot construct a second Octokit client here because
|
||||||
// because @actions/github is not resolvable from this file.
|
// @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);
|
||||||
|
|
@ -80,26 +83,34 @@ module.exports = async ({ github, context, core, orgClient }) => {
|
||||||
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);
|
||||||
try {
|
const res = await fetch(
|
||||||
const { data } = await orgClient.rest.teams.getMembershipForUserInOrg({
|
`https://api.github.com/orgs/${encodeURIComponent(owner)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(user)}`,
|
||||||
org: owner, team_slug: teamSlug, username: user,
|
{
|
||||||
});
|
headers: {
|
||||||
const ok = data.state === 'active';
|
authorization: `Bearer ${orgToken}`,
|
||||||
teamMembershipCache.set(key, ok);
|
accept: 'application/vnd.github+json',
|
||||||
return ok;
|
'x-github-api-version': '2022-11-28',
|
||||||
} catch (e) {
|
},
|
||||||
if (e.status === 404) {
|
},
|
||||||
teamMembershipCache.set(key, false);
|
);
|
||||||
return false;
|
if (res.status === 404) {
|
||||||
}
|
teamMembershipCache.set(key, false);
|
||||||
// 403 here almost always means the token lacks read:org.
|
return false;
|
||||||
if (e.status === 403) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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);
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isAuthorized(user, pr) {
|
async function isAuthorized(user, pr) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue