Skip to content

Commit df1aeec

Browse files
authored
fix(copilot-driver): handle auth failures in --continue attempts (#26146)
1 parent fc7a080 commit df1aeec

File tree

3 files changed

+128
-10
lines changed

3 files changed

+128
-10
lines changed

.changeset/patch-fix-resume-auth-failure.md

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/copilot_driver.cjs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
*
1010
* Retry policy:
1111
* - If the process produced any output (hasOutput) and exits with a non-zero code, the
12-
* session is considered partially executed. The driver retries with --resume so the
12+
* session is considered partially executed. The driver retries with --continue so the
1313
* Copilot CLI can continue from where it left off.
1414
* - CAPIError 400 is a well-known transient failure mode and is logged explicitly, but
1515
* any partial-execution failure is retried — not just CAPIError 400.
1616
* - If the process produced no output (failed to start / auth error before any work), the
1717
* driver does not retry because there is nothing to resume.
18+
* - "No authentication information found" errors are non-retryable: the absent token will
19+
* remain absent on every subsequent attempt, so all further retries will also fail.
1820
* - Retries use exponential backoff: 5s → 10s → 20s (capped at 60s).
1921
* - Maximum 3 retry attempts after the initial run.
2022
*
@@ -43,6 +45,11 @@ const CAPI_ERROR_400_PATTERN = /CAPIError:\s*400/;
4345
// This is a persistent policy configuration error — retrying will not help.
4446
const MCP_POLICY_BLOCKED_PATTERN = /MCP servers were blocked by policy:/;
4547

48+
// Pattern to detect missing authentication credentials.
49+
// This error means no auth token is available in the environment; retrying will not help
50+
// because the missing token will still be absent on every subsequent attempt.
51+
const NO_AUTH_INFO_PATTERN = /No authentication information found/;
52+
4653
/**
4754
* Emit a timestamped diagnostic log line to stderr.
4855
* All driver messages are prefixed with "[copilot-driver]" so they are easy to
@@ -73,6 +80,17 @@ function isMCPPolicyError(output) {
7380
return MCP_POLICY_BLOCKED_PATTERN.test(output);
7481
}
7582

83+
/**
84+
* Determines if the collected output contains a "No authentication information found" error.
85+
* This means no auth token (COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN) is available
86+
* in the environment. Retrying will not help because the absent token will remain absent.
87+
* @param {string} output - Collected stdout+stderr from the process
88+
* @returns {boolean}
89+
*/
90+
function isNoAuthInfoError(output) {
91+
return NO_AUTH_INFO_PATTERN.test(output);
92+
}
93+
7694
/**
7795
* Sleep for a specified duration
7896
* @param {number} ms - Duration in milliseconds
@@ -221,11 +239,11 @@ async function main() {
221239
const driverStartTime = Date.now();
222240

223241
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
224-
// Add --resume flag on retries so the copilot session resumes from where it left off
225-
const currentArgs = attempt > 0 ? [...args, "--resume"] : args;
242+
// Add --continue flag on retries so the copilot session continues from where it left off
243+
const currentArgs = attempt > 0 ? [...args, "--continue"] : args;
226244

227245
if (attempt > 0) {
228-
log(`retry ${attempt}/${MAX_RETRIES}: sleeping ${delay}ms before next attempt with --resume`);
246+
log(`retry ${attempt}/${MAX_RETRIES}: sleeping ${delay}ms before next attempt with --continue`);
229247
await sleep(delay);
230248
delay = Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS);
231249
log(`retry ${attempt}/${MAX_RETRIES}: woke up, next delay cap will be ${Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS)}ms`);
@@ -241,23 +259,40 @@ async function main() {
241259
}
242260

243261
// Determine whether to retry.
244-
// Retry whenever the session was partially executed (hasOutput), using --resume so that
262+
// Retry whenever the session was partially executed (hasOutput), using --continue so that
245263
// the Copilot CLI can continue from where it left off. CAPIError 400 is the well-known
246-
// transient case, but any partial-execution failure is eligible for a resume retry.
247-
// Exception: MCP policy errors are persistent configuration issues — never retry.
264+
// transient case, but any partial-execution failure is eligible for a continue retry.
265+
// Exceptions: MCP policy errors and auth errors are persistent — never retry.
248266
const isCAPIError = isTransientCAPIError(result.output);
249267
const isMCPPolicy = isMCPPolicyError(result.output);
250-
log(`attempt ${attempt + 1} failed:` + ` exitCode=${result.exitCode}` + ` isCAPIError400=${isCAPIError}` + ` isMCPPolicyError=${isMCPPolicy}` + ` hasOutput=${result.hasOutput}` + ` retriesRemaining=${MAX_RETRIES - attempt}`);
268+
const isAuthErr = isNoAuthInfoError(result.output);
269+
log(
270+
`attempt ${attempt + 1} failed:` +
271+
` exitCode=${result.exitCode}` +
272+
` isCAPIError400=${isCAPIError}` +
273+
` isMCPPolicyError=${isMCPPolicy}` +
274+
` isAuthError=${isAuthErr}` +
275+
` hasOutput=${result.hasOutput}` +
276+
` retriesRemaining=${MAX_RETRIES - attempt}`
277+
);
251278

252279
// MCP policy errors are persistent — retrying will not help.
253280
if (isMCPPolicy) {
254281
log(`attempt ${attempt + 1}: MCP servers blocked by policy — not retrying (this is a policy configuration issue, not a transient error)`);
255282
break;
256283
}
257284

285+
// Auth errors are persistent for the duration of the job — retrying will not help.
286+
// "No authentication information found" means COPILOT_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN
287+
// are all absent or invalid. Retrying with --continue will produce the same auth failure.
288+
if (isAuthErr) {
289+
log(`attempt ${attempt + 1}: no authentication information found — not retrying (COPILOT_GITHUB_TOKEN, GH_TOKEN, and GITHUB_TOKEN are all absent or invalid)`);
290+
break;
291+
}
292+
258293
if (attempt < MAX_RETRIES && result.hasOutput) {
259294
const reason = isCAPIError ? "CAPIError 400 (transient)" : "partial execution";
260-
log(`attempt ${attempt + 1}: ${reason} — will retry with --resume (attempt ${attempt + 2}/${MAX_RETRIES + 1})`);
295+
log(`attempt ${attempt + 1}: ${reason} — will retry with --continue (attempt ${attempt + 2}/${MAX_RETRIES + 1})`);
261296
continue;
262297
}
263298

actions/setup/js/copilot_driver.test.cjs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe("copilot_driver.cjs", () => {
3434
});
3535
});
3636

37-
describe("retry policy: resume on partial execution", () => {
37+
describe("retry policy: continue on partial execution", () => {
3838
// Inline the same retry-eligibility logic as the driver for unit testing.
3939
// The driver retries whenever the session produced output (hasOutput), regardless
4040
// of the specific error type. CAPIError 400 is just the well-known case.
@@ -141,6 +141,84 @@ describe("copilot_driver.cjs", () => {
141141
});
142142
});
143143

144+
describe("no-auth-info detection pattern", () => {
145+
const NO_AUTH_INFO_PATTERN = /No authentication information found/;
146+
147+
it("matches the exact error from the issue report", () => {
148+
const errorOutput =
149+
"Error: No authentication information found.\n" +
150+
"Copilot can be authenticated with GitHub using an OAuth Token or a Fine-Grained Personal Access Token.\n" +
151+
"To authenticate, you can use any of the following methods:\n" +
152+
" - Start 'copilot' and run the '/login' command\n" +
153+
" - Set the COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN environment variable\n" +
154+
" - Run 'gh auth login' to authenticate with the GitHub CLI";
155+
expect(NO_AUTH_INFO_PATTERN.test(errorOutput)).toBe(true);
156+
});
157+
158+
it("matches when embedded in larger output after a long run", () => {
159+
const output = "Some agent work output\nMore work\nNo authentication information found\nEnd";
160+
expect(NO_AUTH_INFO_PATTERN.test(output)).toBe(true);
161+
});
162+
163+
it("does not match unrelated auth errors", () => {
164+
expect(NO_AUTH_INFO_PATTERN.test("Access denied by policy settings")).toBe(false);
165+
expect(NO_AUTH_INFO_PATTERN.test("Error: 401 Unauthorized")).toBe(false);
166+
expect(NO_AUTH_INFO_PATTERN.test("Authentication failed")).toBe(false);
167+
expect(NO_AUTH_INFO_PATTERN.test("CAPIError: 400 Bad Request")).toBe(false);
168+
expect(NO_AUTH_INFO_PATTERN.test("")).toBe(false);
169+
});
170+
});
171+
172+
describe("auth error prevents retry", () => {
173+
// Inline the same retry logic as the driver, including auth error check
174+
const MCP_POLICY_BLOCKED_PATTERN = /MCP servers were blocked by policy:/;
175+
const NO_AUTH_INFO_PATTERN = /No authentication information found/;
176+
const MAX_RETRIES = 3;
177+
178+
/**
179+
* @param {{hasOutput: boolean, exitCode: number, output: string}} result
180+
* @param {number} attempt
181+
* @returns {boolean}
182+
*/
183+
function shouldRetry(result, attempt) {
184+
if (result.exitCode === 0) return false;
185+
// MCP policy errors are persistent — never retry
186+
if (MCP_POLICY_BLOCKED_PATTERN.test(result.output)) return false;
187+
// Auth errors are persistent — never retry
188+
if (NO_AUTH_INFO_PATTERN.test(result.output)) return false;
189+
return attempt < MAX_RETRIES && result.hasOutput;
190+
}
191+
192+
it("does not retry when auth fails on first attempt (no real work done)", () => {
193+
const result = { exitCode: 1, hasOutput: true, output: "Error: No authentication information found." };
194+
expect(shouldRetry(result, 0)).toBe(false);
195+
});
196+
197+
it("does not retry when auth fails on a --continue attempt (the reported bug scenario)", () => {
198+
// This replicates the issue: attempt 1 ran for 39 min then failed,
199+
// attempt 2 (--continue) fails with auth error — should not retry attempts 3 & 4.
200+
const resumeResult = { exitCode: 1, hasOutput: true, output: "Error: No authentication information found." };
201+
expect(shouldRetry(resumeResult, 1)).toBe(false);
202+
expect(shouldRetry(resumeResult, 2)).toBe(false);
203+
expect(shouldRetry(resumeResult, 3)).toBe(false);
204+
});
205+
206+
it("does not retry auth error even when output is mixed with other content", () => {
207+
const result = { exitCode: 1, hasOutput: true, output: "Some output\nError: No authentication information found.\nMore output" };
208+
expect(shouldRetry(result, 0)).toBe(false);
209+
});
210+
211+
it("still retries non-auth errors with output (CAPIError 400)", () => {
212+
const result = { exitCode: 1, hasOutput: true, output: "CAPIError: 400 Bad Request" };
213+
expect(shouldRetry(result, 0)).toBe(true);
214+
});
215+
216+
it("still retries generic partial-execution errors with output", () => {
217+
const result = { exitCode: 1, hasOutput: true, output: "Failed to get response from the AI model; retried 5 times" };
218+
expect(shouldRetry(result, 0)).toBe(true);
219+
});
220+
});
221+
144222
describe("retry configuration", () => {
145223
it("has sensible default values", () => {
146224
// These match the constants in copilot_driver.cjs

0 commit comments

Comments
 (0)