-
Notifications
You must be signed in to change notification settings - Fork 394
fix: allow Copilot bot in mattpocock-skills-reviewer; handle GitHub App "not a user" in checkBotStatus #33451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0a5cff4
8c7baa5
5adad1b
e8dfd14
5620e50
35ccd30
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,6 +154,25 @@ function isConfusedDeputyAttack(actor, eventName, payload) { | |
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Returns true when a GitHub API 404 error indicates the username refers to a | ||
| * GitHub App rather than a regular user account. | ||
| * | ||
| * The collaborators permission endpoint (`GET /repos/{owner}/{repo}/collaborators/{username}/permission`) | ||
| * responds with 404 and a message of the form "<login> is not a user" when called | ||
| * with a GitHub App actor name (e.g. "Copilot"). A plain "Not Found" 404 means | ||
| * the actor simply doesn't have repository access or doesn't exist. | ||
| * | ||
| * @param {unknown} error - The error to inspect | ||
| * @returns {boolean} | ||
| */ | ||
| function isGitHubAppNotUserError(error) { | ||
| if (!error || typeof error !== "object") return false; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] The regex Suggestion: Add a comment linking to the GitHub API documentation URL that appears in the error message (the one in the test: // Regex matches GitHub API error message format documented at:
// https://docs.github.com/rest/collaborators/collaborators#get-repository-permissions-for-a-user
return typeof msg === "string" && /\bis not a user\b/.test(msg); |
||
| if (!("status" in error) || error.status !== 404) return false; | ||
| const msg = getErrorMessage(error); | ||
| return typeof msg === "string" && /\bis not a user\b/.test(msg); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential security issue: The regex Suggestion: Make the pattern more specific to match the exact GitHub API response format: return typeof msg === "string" && /^[\w-]+ is not a user\b/.test(msg);This ensures we're matching "(login) is not a user" and not other unrelated messages. |
||
| } | ||
|
|
||
| /** | ||
| * Check if the actor is a bot and if it's active on the repository. | ||
| * Accepts both <slug> and <slug>[bot] actor forms, since GitHub Apps | ||
|
|
@@ -185,7 +204,16 @@ async function checkBotStatus(actor, owner, repo) { | |
| core.info(`Bot '${actor}' is active with permission level: ${botPermission.data.permission}`); | ||
| return { isBot: true, isActive: true }; | ||
| } catch (botError) { | ||
| // If we get a 404, the [bot]-suffixed form may not be listed as a collaborator. | ||
| // If we get a 404 "is not a user" response, GitHub is telling us this is a GitHub | ||
| // App identity (not a regular user). GitHub Apps trigger events via their installation, | ||
| // so if an app actor appears in an event payload it must already have repository access. | ||
| // Treat it as active to avoid false negatives for apps like Copilot. | ||
| if (isGitHubAppNotUserError(botError)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] Excellent regression test coverage! The new Minor observation: The slug-form fallback test creates a scenario where the first lookup returns plain Not blocking — current coverage is already strong. |
||
| core.info(`Bot '${actor}' is a GitHub App; treating as active based on event trigger`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic concern: This assumes that any GitHub App appearing in an event payload "must already have repository access." While this may be true for installation events, it's not necessarily true for all event types. For example:
Suggestion: Consider adding a comment explaining which specific event types this assumption applies to, or add a fallback verification step. At minimum, log the event name to aid debugging: if (isGitHubAppNotUserError(botError)) {
core.info(`Bot '${actor}' is a GitHub App (event: ${github.context.eventName}); treating as active based on event trigger`);
return { isBot: true, isActive: true };
} |
||
| return { isBot: true, isActive: true }; | ||
| } | ||
|
|
||
| // If we get a plain 404, the [bot]-suffixed form may not be listed as a collaborator. | ||
| // Fall back to checking the non-[bot] (slug) form, as some GitHub Apps appear | ||
| // under their plain slug name rather than the [bot]-suffixed form. | ||
| if (botError?.status === 404) { | ||
|
|
@@ -199,6 +227,11 @@ async function checkBotStatus(actor, owner, repo) { | |
| return { isBot: true, isActive: true }; | ||
| } catch (slugError) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical bug: The slug-form fallback loses When the The problem: If the slug-form lookup at line 228 also encounters the "is not a user" error, the function returns Why this matters: The entire point of this PR is to provide helpful error messages when GitHub Apps are blocked. This fallback path silently loses that context. Suggested fix: Refactor the slug-form fallback to preserve } catch (slugError) {
if (isGitHubAppNotUserError(slugError)) {
core.info(`Bot '${actor}' is a GitHub App (slug form); treating as active`);
return { isBot: true, isActive: true };
}
if (slugError?.status === 404) {
core.info(`Bot '${actor}' not found in repository collaborators (neither [bot] nor slug form)`);
return { isBot: true, isActive: false };
}
// Other errors
return { isBot: false, isActive: false, error: getErrorMessage(slugError) };
}This ensures the GitHub App detection works consistently regardless of which API call triggers it. |
||
| if (slugError?.status === 404) { | ||
| // Same "is not a user" check for the slug form fallback. | ||
| if (isGitHubAppNotUserError(slugError)) { | ||
| core.info(`Bot '${actor}' is a GitHub App; treating as active based on event trigger`); | ||
| return { isBot: true, isActive: true }; | ||
| } | ||
| core.warning(`Bot '${actor}' is not active/installed on ${owner}/${repo}`); | ||
| return { isBot: true, isActive: false }; | ||
| } | ||
|
|
@@ -225,7 +258,7 @@ async function checkBotStatus(actor, owner, repo) { | |
| * @param {string} owner - Repository owner | ||
| * @param {string} repo - Repository name | ||
| * @param {string[]} requiredPermissions - Array of required permission levels | ||
| * @returns {Promise<{authorized: boolean, permission?: string, error?: string}>} | ||
| * @returns {Promise<{authorized: boolean, permission?: string, error?: string, isGitHubApp?: boolean}>} | ||
| */ | ||
| async function checkRepositoryPermission(actor, owner, repo, requiredPermissions) { | ||
| try { | ||
|
|
@@ -254,7 +287,7 @@ async function checkRepositoryPermission(actor, owner, repo, requiredPermissions | |
| } catch (repoError) { | ||
| const errorMessage = getErrorMessage(repoError); | ||
| core.warning(`Repository permission check failed: ${errorMessage}`); | ||
| return { authorized: false, error: errorMessage }; | ||
| return { authorized: false, error: errorMessage, isGitHubApp: isGitHubAppNotUserError(repoError) }; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -263,6 +296,7 @@ module.exports = { | |
| parseAllowedBots, | ||
| canonicalizeBotIdentifier, | ||
| isAllowedBot, | ||
| isGitHubAppNotUserError, | ||
| readAllowBotAuthoredTriggerComment, | ||
| isConfusedDeputyAttack, | ||
| checkRepositoryPermission, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/diagnose] Great actionable error message! The conditional
isGitHubAppcheck provides workflow authors with precise guidance ("add it toon.bots:") instead of a generic API error.Positive highlight: The fallback path (line 215) is preserved for non-GitHub-App errors, maintaining backwards compatibility.