fix: classify transient GitHub connectivity
Merge PR #102 after rebasing onto current master and validating focused GitHub transient/Code Queue preflight behavior. GitHub DNS/API failures now classify as retryable github-transient with retry/backoff or keep-fresh-heartbeat-task guidance.
This commit is contained in:
+108
-2
@@ -77,6 +77,7 @@ type GitHubDegradedReason =
|
||||
| "missing-token"
|
||||
| "auth-failed"
|
||||
| "egress-failed"
|
||||
| "github-transient"
|
||||
| "network-proxy-failed"
|
||||
| "permission-denied"
|
||||
| "repo-not-found"
|
||||
@@ -359,6 +360,8 @@ interface GitHubErrorPayload {
|
||||
runnerDisposition: RunnerDisposition;
|
||||
status?: number;
|
||||
message: string;
|
||||
retryable?: boolean;
|
||||
commanderAction?: string;
|
||||
details?: unknown;
|
||||
scopes?: {
|
||||
accepted: string | null;
|
||||
@@ -1347,6 +1350,97 @@ function sanitizedErrorDetails(parsed: unknown): unknown {
|
||||
return details;
|
||||
}
|
||||
|
||||
function errorProperty(value: unknown, key: string): string | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const item = value[key];
|
||||
return typeof item === "string" && item.length > 0 ? item : null;
|
||||
}
|
||||
|
||||
function errorChainText(error: unknown): string {
|
||||
const parts: string[] = [];
|
||||
let current: unknown = error;
|
||||
const seen = new Set<unknown>();
|
||||
while (current !== null && current !== undefined && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
if (current instanceof Error) {
|
||||
parts.push(current.name, current.message);
|
||||
current = (current as Error & { cause?: unknown }).cause;
|
||||
continue;
|
||||
}
|
||||
if (isRecord(current)) {
|
||||
for (const key of ["name", "message", "code", "errno", "syscall", "hostname", "host"]) {
|
||||
const value = errorProperty(current, key);
|
||||
if (value !== null) parts.push(value);
|
||||
}
|
||||
current = current.cause;
|
||||
continue;
|
||||
}
|
||||
parts.push(String(current));
|
||||
break;
|
||||
}
|
||||
return parts.join(" ").replace(/\s+/gu, " ").trim();
|
||||
}
|
||||
|
||||
function firstErrorProperty(error: unknown, key: string): string | null {
|
||||
let current: unknown = error;
|
||||
const seen = new Set<unknown>();
|
||||
while (current !== null && current !== undefined && !seen.has(current)) {
|
||||
seen.add(current);
|
||||
const value = errorProperty(current, key);
|
||||
if (value !== null) return value;
|
||||
current = isRecord(current) ? current.cause : current instanceof Error ? (current as Error & { cause?: unknown }).cause : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function classifyGitHubFetchFailure(error: unknown): {
|
||||
reason: GitHubDegradedReason;
|
||||
message: string;
|
||||
retryable?: boolean;
|
||||
commanderAction?: string;
|
||||
details: Record<string, unknown>;
|
||||
} {
|
||||
const text = errorChainText(error);
|
||||
const lower = text.toLowerCase();
|
||||
const isProxyFailure = /\bproxy\b/u.test(lower) && /could not resolve|connect|tunnel|proxy/i.test(text);
|
||||
const isGitHubTransient = !isProxyFailure && (
|
||||
/temporary failure in name resolution/iu.test(text)
|
||||
|| /\b(eai_again|enotfound|etimedout|econnreset|econnrefused|ehostunreach|enetunreach)\b/iu.test(text)
|
||||
|| /getaddrinfo|could not resolve host|name or service not known|failed to connect|connection timed out|connection reset|connection closed|other side closed|network is unreachable|socket connection/iu.test(text)
|
||||
);
|
||||
if (!isGitHubTransient) {
|
||||
return {
|
||||
reason: "network-proxy-failed",
|
||||
message: text.length > 0 ? text : "GitHub request failed before receiving an HTTP response",
|
||||
details: {
|
||||
scope: "github-api",
|
||||
transient: false,
|
||||
errorName: error instanceof Error ? error.name : firstErrorProperty(error, "name"),
|
||||
errorCode: firstErrorProperty(error, "code"),
|
||||
host: firstErrorProperty(error, "hostname") ?? firstErrorProperty(error, "host"),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
reason: "github-transient",
|
||||
message: text.length > 0 ? text : "GitHub DNS/API connection failed before receiving an HTTP response",
|
||||
retryable: true,
|
||||
commanderAction: "retry-backoff-or-keep-running-if-heartbeat-fresh",
|
||||
details: {
|
||||
scope: "github-api",
|
||||
transient: true,
|
||||
retryable: true,
|
||||
authFailure: false,
|
||||
semanticFailure: false,
|
||||
errorName: error instanceof Error ? error.name : firstErrorProperty(error, "name"),
|
||||
errorCode: firstErrorProperty(error, "code"),
|
||||
host: firstErrorProperty(error, "hostname") ?? firstErrorProperty(error, "host"),
|
||||
commanderAction: "retry/backoff; if the Code Queue task heartbeat or trace is fresh, keep supervising instead of closing or requeueing business work",
|
||||
valuesPrinted: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function errorPayload(reason: GitHubDegradedReason, message: string, extra: Omit<GitHubErrorPayload, "ok" | "degradedReason" | "runnerDisposition" | "message"> = {}): GitHubErrorPayload {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -1431,7 +1525,13 @@ async function githubRequest<T>(
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorPayload("network-proxy-failed", error instanceof Error ? error.message : String(error), { request: { method, path } });
|
||||
const classified = classifyGitHubFetchFailure(error);
|
||||
return errorPayload(classified.reason, classified.message, {
|
||||
request: { method, path },
|
||||
details: classified.details,
|
||||
retryable: classified.retryable,
|
||||
commanderAction: classified.commanderAction,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
@@ -1473,7 +1573,13 @@ async function githubGraphqlRequest<T>(
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorPayload("network-proxy-failed", error instanceof Error ? error.message : String(error), { request: { method: "POST", path: "/graphql" } });
|
||||
const classified = classifyGitHubFetchFailure(error);
|
||||
return errorPayload(classified.reason, classified.message, {
|
||||
request: { method: "POST", path: "/graphql" },
|
||||
details: classified.details,
|
||||
retryable: classified.retryable,
|
||||
commanderAction: classified.commanderAction,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user