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
40 changes: 30 additions & 10 deletions actions/setup/js/handle_agent_failure.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1869,6 +1869,33 @@ function buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilo
return "\n" + renderTemplateFromFile(templatePath, { issues: issueList });
}

/**
* Build the secret verification failure context for the agent failure issue/comment.
* For the Copilot engine, adds a suggestion to use `permissions.copilot-requests: write`
* to enable Copilot inference through the org without a personal access token.
* @param {string} secretVerificationResult - The secret verification result ("failed" or other)
* @param {string} engineId - The engine ID (e.g. "copilot")
* @returns {string} Formatted context string, or empty string if verification did not fail
*/
function buildSecretVerificationContext(secretVerificationResult, engineId) {
if (secretVerificationResult !== "failed") {
return "";
}

let context =
buildWarningAlertLine("Secret Verification Failed", "The workflow's secret validation step failed. Please check that the required secrets are configured in your repository settings.") +
"\nFor more information on configuring tokens, see: https://github.github.com/gh-aw/reference/engines/\n";

if ((engineId || "").toLowerCase() === "copilot") {
context +=
"\n**Alternative**: If your organization has a Copilot subscription, you can avoid the need for a personal access token by adding a top-level `permissions` block to your workflow file. This enables Copilot inference through the org using the built-in GitHub Actions token.\n" +
"\n```yaml\npermissions:\n copilot-requests: write\n```\n" +
"\nSee: https://github.github.com/gh-aw/reference/engines/#github-copilot-default\n";
}

return context;
}

/**
* Check whether agent-stdio.log contains a terminal_reason: "completed" result entry,
* indicating the agent finished its task successfully despite a non-zero job exit code.
Expand Down Expand Up @@ -2870,11 +2897,7 @@ async function main() {
workflow_source: workflowSource,
workflow_source_url: workflowSourceURL,
secret_verification_failed: String(secretVerificationResult === "failed"),
secret_verification_context:
secretVerificationResult === "failed"
? buildWarningAlertLine("Secret Verification Failed", "The workflow's secret validation step failed. Please check that the required secrets are configured in your repository settings.") +
"\nFor more information on configuring tokens, see: https://github.github.com/gh-aw/reference/engines/\n"
: "",
secret_verification_context: buildSecretVerificationContext(secretVerificationResult, engineId),
credential_auth_error_context: credentialAuthErrorContext,
assignment_errors_context: assignmentErrorsContext,
assign_copilot_failure_context: assignCopilotFailureContext,
Expand Down Expand Up @@ -3099,11 +3122,7 @@ async function main() {
branch: currentBranch,
pull_request_info: pullRequest ? ` \n**Pull Request:** [#${pullRequest.number}](${pullRequest.html_url})` : "",
secret_verification_failed: String(secretVerificationResult === "failed"),
secret_verification_context:
secretVerificationResult === "failed"
? buildWarningAlertLine("Secret Verification Failed", "The workflow's secret validation step failed. Please check that the required secrets are configured in your repository settings.") +
"\nFor more information on configuring tokens, see: https://github.github.com/gh-aw/reference/engines/\n"
: "",
secret_verification_context: buildSecretVerificationContext(secretVerificationResult, engineId),
credential_auth_error_context: credentialAuthErrorContext,
assignment_errors_context: assignmentErrorsContext,
assign_copilot_failure_context: assignCopilotFailureContext,
Expand Down Expand Up @@ -3240,6 +3259,7 @@ module.exports = {
hasAgentTerminalReasonCompleted,
detectAndHandleFailureCascade,
findRecentFailureIssues,
buildSecretVerificationContext,
CASCADE_WINDOW_MINUTES,
CASCADE_WINDOW_MS,
CASCADE_THRESHOLD,
Expand Down
29 changes: 28 additions & 1 deletion actions/setup/js/handle_agent_failure.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe("handle_agent_failure", () => {
let buildPushRepoMemoryFailureContext;
let buildReportIncompleteContext;
let buildFailureIssueTitle;
let buildSecretVerificationContext;
let getActionFailureIssueExpiresHours;
const ENGINE_RATE_LIMIT_TEMPLATE = "> [!WARNING]\n> **Engine Rate Limited (HTTP 429)**\n> OTLP telemetry\n> {engine_label}\n";

Expand All @@ -29,7 +30,7 @@ describe("handle_agent_failure", () => {

// Reset module registry so each test gets a fresh require
vi.resetModules();
({ main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildReportIncompleteContext, buildFailureIssueTitle, getActionFailureIssueExpiresHours } = require("./handle_agent_failure.cjs"));
({ main, buildCodePushFailureContext, buildPushRepoMemoryFailureContext, buildReportIncompleteContext, buildFailureIssueTitle, buildSecretVerificationContext, getActionFailureIssueExpiresHours } = require("./handle_agent_failure.cjs"));
});

afterEach(() => {
Expand Down Expand Up @@ -1299,6 +1300,32 @@ describe("handle_agent_failure", () => {
});
});

describe("buildSecretVerificationContext", () => {
it("returns empty string when verification did not fail", () => {
expect(buildSecretVerificationContext("", "copilot")).toBe("");
expect(buildSecretVerificationContext("success", "copilot")).toBe("");
expect(buildSecretVerificationContext("", "")).toBe("");
});

it("returns generic warning for non-copilot engines when verification failed", () => {
const result = buildSecretVerificationContext("failed", "claude");
expect(result).toContain("Secret Verification Failed");
expect(result).toContain("required secrets are configured");
expect(result).toContain("https://github.github.com/gh-aw/reference/engines/");
expect(result).not.toContain("copilot-requests");
});

it("returns copilot-specific message with copilot-requests: write permissions suggestion when verification failed", () => {
const result = buildSecretVerificationContext("failed", "copilot");
const mixedCaseResult = buildSecretVerificationContext("failed", "Copilot");
expect(result).toContain("Secret Verification Failed");
expect(result).toContain("required secrets are configured");
expect(result).toContain("```yaml\npermissions:\n copilot-requests: write\n```");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Copilot-specific reference URL is untested — if it were accidentally removed, all tests would still pass. The implementation appends a See: https://github.github.com/gh-aw/reference/engines/#github-copilot-default line that is unique to the Copilot branch, but no assertion checks for it.

💡 Suggested addition
expect(result).toContain("https://github.github.com/gh-aw/reference/engines/#github-copilot-default");

The generic URL (/engines/) is already covered by the claude test above; the Copilot-specific anchor (#github-copilot-default) is not. Adding this closes the coverage gap and locks in the exact link that users are expected to follow.

expect(result).toContain("https://github.github.com/gh-aw/reference/engines/#github-copilot-default");
expect(mixedCaseResult).toContain("copilot-requests: write");
});
});

describe("buildCodePushFailureContext", () => {
it("returns empty string when no errors", () => {
expect(buildCodePushFailureContext("")).toBe("");
Expand Down
Loading