Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions actions/setup/js/compute_text.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,38 @@ async function main() {
const actor = context.actor;
const { owner, repo } = context.repo;

// Check if the actor has repository access (admin, maintain permissions)
const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({
owner: owner,
repo: repo,
username: actor,
});

const permission = repoPermission.data.permission;
core.info(`Repository permission level: ${permission}`);
// Check if the actor has repository access (admin, maintain, write permissions)
// Non-user actors (bots, GitHub Apps like "Copilot") may not have a user record,
// causing the API to throw an error (e.g., "Copilot is not a user").
// In that case, check the allowed bots list before returning empty outputs.
let permission;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot read the list of trusted bots from the front matter and treat them as "write" access.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d231e93. When the permission check fails for a non-user actor, compute_text.cjs now reads GH_AW_ALLOWED_BOTS (passed from the on.bots frontmatter) and treats listed bots as having "write" access, allowing them to proceed with text extraction.

try {
const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({
owner: owner,
repo: repo,
username: actor,
});
permission = repoPermission.data.permission;
core.info(`Repository permission level: ${permission}`);
} catch (permError) {
core.warning(`Permission check failed for actor '${actor}': ${getErrorMessage(permError)}`);
// Check if actor is in the allowed bots list (configured via on.bots in frontmatter)
const allowedBots =
process.env.GH_AW_ALLOWED_BOTS?.split(",")
.map(b => b.trim())
.filter(b => b) ?? [];
Comment on lines +36 to +39
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bot parsing logic here duplicates the implementation in check_permissions_utils.cjs which already provides a parseAllowedBots() function. Consider importing and using that utility function to avoid code duplication and ensure consistent behavior across the codebase.

For example:

const { parseAllowedBots } = require("./check_permissions_utils.cjs");
// ...
const allowedBots = parseAllowedBots();

Note: The current implementation (map then filter) is actually more correct than the existing utility (which only filters), but consolidating to one implementation would be better for maintainability. If you use the utility, you may want to fix the trimming issue there as well.

Copilot uses AI. Check for mistakes.
if (allowedBots.includes(actor)) {
core.info(`Actor '${actor}' is in the allowed bots list, treating as 'write' access`);
permission = "write";
} else {
core.setOutput("text", "");
core.setOutput("title", "");
core.setOutput("body", "");
return;
}
}

if (permission !== "admin" && permission !== "maintain") {
if (permission !== "admin" && permission !== "maintain" && permission !== "write") {
core.setOutput("text", "");
core.setOutput("title", "");
core.setOutput("body", "");
Expand Down
35 changes: 34 additions & 1 deletion actions/setup/js/compute_text.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const mockCore = {
describe("compute_text.cjs", () => {
let computeTextScript, sanitizeIncomingTextFunction;
(beforeEach(() => {
(vi.clearAllMocks(), (mockContext.eventName = "issues"), (mockContext.payload = {}), delete process.env.GH_AW_ALLOWED_DOMAINS);
(vi.clearAllMocks(), (mockContext.eventName = "issues"), (mockContext.payload = {}), (mockContext.actor = "test-user"), delete process.env.GH_AW_ALLOWED_DOMAINS, delete process.env.GH_AW_ALLOWED_BOTS);
const scriptPath = path.join(process.cwd(), "compute_text.cjs");
computeTextScript = fs.readFileSync(scriptPath, "utf8");
const scriptWithExport = computeTextScript.replace("module.exports = { main };", "global.testSanitizeIncomingText = sanitizeIncomingText; global.testMain = main;");
Expand Down Expand Up @@ -200,6 +200,39 @@ const mockCore = {
expect(mockCore.setOutput).toHaveBeenCalledWith("title", ""),
expect(mockCore.setOutput).toHaveBeenCalledWith("body", ""));
}),
it("should allow access for write permission users", async () => {
(mockGithub.rest.repos.getCollaboratorPermissionLevel.mockResolvedValue({ data: { permission: "write" } }),
(mockContext.eventName = "issues"),
(mockContext.payload = { issue: { title: "Test Issue", body: "Issue description" } }),
await testMain(),
expect(mockCore.setOutput).toHaveBeenCalledWith("text", "Test Issue\n\nIssue description"),
expect(mockCore.setOutput).toHaveBeenCalledWith("title", "Test Issue"),
expect(mockCore.setOutput).toHaveBeenCalledWith("body", "Issue description"));
}),
it("should return empty outputs when permission check fails for non-user actors like Copilot", async () => {
(mockGithub.rest.repos.getCollaboratorPermissionLevel.mockRejectedValue(new Error("Copilot is not a user")),
(mockContext.actor = "Copilot"),
(mockContext.eventName = "issues"),
(mockContext.payload = { issue: { title: "Test Issue", body: "Issue description" } }),
await testMain(),
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Permission check failed for actor 'Copilot'")),
expect(mockCore.setOutput).toHaveBeenCalledWith("text", ""),
expect(mockCore.setOutput).toHaveBeenCalledWith("title", ""),
expect(mockCore.setOutput).toHaveBeenCalledWith("body", ""));
}),
it("should extract text for allowed bot actors when permission check fails", async () => {
((process.env.GH_AW_ALLOWED_BOTS = "Copilot,dependabot[bot]"),
mockGithub.rest.repos.getCollaboratorPermissionLevel.mockRejectedValue(new Error("Copilot is not a user")),
(mockContext.actor = "Copilot"),
(mockContext.eventName = "issues"),
(mockContext.payload = { issue: { title: "Bot Issue", body: "Bot description" } }),
await testMain(),
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Permission check failed for actor 'Copilot'")),
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("allowed bots list")),
expect(mockCore.setOutput).toHaveBeenCalledWith("text", "Bot Issue\n\nBot description"),
expect(mockCore.setOutput).toHaveBeenCalledWith("title", "Bot Issue"),
expect(mockCore.setOutput).toHaveBeenCalledWith("body", "Bot description"));
}),
it("should sanitize extracted text before output", async () => {
((mockContext.eventName = "issues"), (mockContext.payload = { issue: { title: "Test @user fixes #123", body: "Visit https://evil.com" } }), await testMain());
const outputCall = mockCore.setOutput.mock.calls[0];
Expand Down
4 changes: 4 additions & 0 deletions pkg/workflow/compiler_activation_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,10 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate
steps = append(steps, " - name: Compute current body text\n")
steps = append(steps, " id: sanitized\n")
steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script")))
if len(data.Bots) > 0 {
steps = append(steps, " env:\n")
steps = append(steps, fmt.Sprintf(" GH_AW_ALLOWED_BOTS: %s\n", strings.Join(data.Bots, ",")))
}
steps = append(steps, " with:\n")
steps = append(steps, " script: |\n")
steps = append(steps, generateGitHubScriptWithRequire("compute_text.cjs"))
Expand Down
Loading