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:
@@ -546,6 +546,69 @@ async function main(): Promise<void> {
|
||||
assertCondition(proxyGapRecord.failureKind === "proxy-gap", "proxy failures should classify as proxy-gap", proxyGapRecord);
|
||||
assertCondition(proxyGapRecord.degradedReason === "configured GitHub egress proxy host is not resolvable: missing-egress-proxy.unidesk.svc.cluster.local", "proxy degraded reason should point at the proxy", proxyGapRecord);
|
||||
|
||||
const githubTransientContract = await codexPrPreflightQueryForTest(["--remote"], {
|
||||
config: null,
|
||||
coreFetch: () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
runtimePreflight: rawRuntimePreflightFixture({
|
||||
ok: false,
|
||||
pullRequestDelivery: {
|
||||
...asRecord(rawRuntimePreflightFixture().pullRequestDelivery),
|
||||
ok: false,
|
||||
credentials: {
|
||||
ghTokenPresent: true,
|
||||
githubTokenPresent: false,
|
||||
ghHostsConfigPresent: false,
|
||||
gitCredentialsPresent: false,
|
||||
},
|
||||
egress: {
|
||||
proxy: {
|
||||
selectedProxyHost: "d601-provider-egress-proxy.unidesk.svc.cluster.local",
|
||||
selectedProxyPort: "18789",
|
||||
selectedProxyHostResolvable: true,
|
||||
},
|
||||
githubDefault: { command: "curl", args: ["-IsS", "https://github.com"], ok: false, exitCode: 6, signal: null, error: null, stdout: "", stderr: "curl: (6) Could not resolve host: github.com" },
|
||||
apiDefault: { command: "curl", args: ["-IsS", "https://api.github.com"], ok: false, exitCode: 6, signal: null, error: null, stdout: "", stderr: "curl: (6) Could not resolve host: api.github.com" },
|
||||
issueApi: null,
|
||||
},
|
||||
remote: {
|
||||
gitLsRemote: { command: "git", args: ["ls-remote", "--heads", "origin", "master"], ok: false, exitCode: 128, signal: null, error: null, stdout: "", stderr: "ssh: Could not resolve hostname github.com: Temporary failure in name resolution" },
|
||||
gitHttpsLsRemote: null,
|
||||
githubSshAuthenticated: false,
|
||||
ghAuthStatus: { command: "gh", args: ["auth", "status"], ok: false, exitCode: 1, signal: null, error: null, stdout: "", stderr: "error connecting to api.github.com" },
|
||||
ghRepoView: null,
|
||||
ghIssueView: null,
|
||||
ghPrReadOnly: null,
|
||||
},
|
||||
limitations: [
|
||||
"GitHub HTTPS probe failed with the default environment/proxy",
|
||||
"GitHub API probe failed with the default environment/proxy",
|
||||
"git ls-remote origin master failed",
|
||||
],
|
||||
risks: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
const githubTransientRecord = asRecord(githubTransientContract);
|
||||
assertCondition(githubTransientRecord.failureKind === "github-transient", "GitHub DNS/API failures should classify separately from auth and semantic failures", githubTransientRecord);
|
||||
assertCondition(githubTransientRecord.degradedReason === "github-dns-api-transient", "GitHub transient degraded reason should be stable", githubTransientRecord);
|
||||
assertCondition(githubTransientRecord.runnerDisposition === "infra-blocked", "GitHub transient keeps infra-blocked disposition for legacy callers", githubTransientRecord);
|
||||
assertCondition(githubTransientRecord.retryable === true, "GitHub transient should expose top-level retryable=true", githubTransientRecord);
|
||||
assertCondition(githubTransientRecord.commanderAction === "retry-backoff-or-keep-running-if-heartbeat-fresh", "GitHub transient should expose top-level commander action", githubTransientRecord);
|
||||
const githubTransientPreflight = asRecord(githubTransientRecord.preflight);
|
||||
assertCondition(githubTransientPreflight.retryable === true, "GitHub transient preflight should be retryable", githubTransientPreflight);
|
||||
assertCondition(githubTransientPreflight.commanderAction === "retry-backoff-or-keep-running-if-heartbeat-fresh", "GitHub transient should expose bounded commander action", githubTransientPreflight);
|
||||
const githubTransient = asRecord(githubTransientPreflight.githubTransient);
|
||||
assertCondition(githubTransient.kind === "github-transient", "GitHub transient evidence should identify kind", githubTransient);
|
||||
assertCondition(githubTransient.notAuthMissing === true, "GitHub transient must not be auth-missing", githubTransient);
|
||||
assertCondition(githubTransient.notPrSemanticFailure === true, "GitHub transient must not be PR semantic failure", githubTransient);
|
||||
assertCondition(Array.isArray(githubTransient.failedProbes) && githubTransient.failedProbes.length <= 4, "GitHub transient evidence should stay bounded", githubTransient);
|
||||
assertCondition(String(githubTransient.commanderAction ?? "").includes("keep the task running"), "GitHub transient action should preserve fresh-heartbeat tasks", githubTransient);
|
||||
|
||||
let observedDryRunPath = "";
|
||||
const dryRunContract = await codexPrPreflightQueryForTest([
|
||||
"--remote",
|
||||
@@ -820,6 +883,7 @@ async function main(): Promise<void> {
|
||||
"local backend-core absence returns runner-local-observation-gap",
|
||||
"auth missing returns auth-missing with broker/auth-broker-needed",
|
||||
"proxy failures return proxy-gap",
|
||||
"GitHub DNS/API failures return github-transient with retry/backoff guidance",
|
||||
"git remote failures return git-remote-gap",
|
||||
"combined push/PR create dry-run contract stays read-only and separates system gh from UniDesk gh CLI",
|
||||
"broker-issued token, env-token, and missing-token branches expose authBroker source/capability/nextAction",
|
||||
|
||||
@@ -202,6 +202,21 @@ async function startMockGitHub(): Promise<{ baseUrl: string; requests: MockReque
|
||||
};
|
||||
}
|
||||
|
||||
async function startResetGitHub(): Promise<{ baseUrl: string; close: () => Promise<void> }> {
|
||||
const server = createServer((req) => {
|
||||
req.socket.destroy();
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
assertCondition(typeof address === "object" && address !== null, "reset mock server should expose address");
|
||||
const port = (address as AddressInfo).port;
|
||||
assertCondition(typeof port === "number", "reset mock server should expose port");
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())),
|
||||
};
|
||||
}
|
||||
|
||||
function dataOf(response: JsonRecord): JsonRecord {
|
||||
assertCondition(response.ok === true, "CLI command should succeed", response);
|
||||
assertCondition(typeof response.data === "object" && response.data !== null && !Array.isArray(response.data), "response data should be object", response);
|
||||
@@ -383,6 +398,24 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
await mock.close();
|
||||
}
|
||||
|
||||
const resetMock = await startResetGitHub();
|
||||
try {
|
||||
const transient = await runCli(["gh", "auth", "status", "--repo", "pikasTech/unidesk"], {
|
||||
GH_TOKEN: "contract-token",
|
||||
UNIDESK_GITHUB_API_URL: resetMock.baseUrl,
|
||||
});
|
||||
assertCondition(transient.status !== 0, "GitHub DNS/API transient should fail structurally", transient.json ?? { stdout: transient.stdout });
|
||||
const transientData = failedDataOf(transient.json ?? {});
|
||||
assertCondition(transientData.degradedReason === "github-transient", "GitHub DNS/API transient should not be auth-missing or semantic failure", transientData);
|
||||
assertCondition(transientData.runnerDisposition === "infra-blocked", "GitHub transient should remain infra-blocked", transientData);
|
||||
const transientDetails = transientData.details as JsonRecord;
|
||||
assertCondition(transientDetails.retryable === true, "GitHub transient should be retryable", transientDetails);
|
||||
assertCondition(transientDetails.commanderAction === "retry-backoff-or-keep-running-if-heartbeat-fresh", "GitHub transient should expose bounded commander action", transientDetails);
|
||||
assertCondition(!transient.stdout.includes("contract-token"), "GitHub transient output must not print token values", { stdout: transient.stdout });
|
||||
} finally {
|
||||
await resetMock.close();
|
||||
}
|
||||
|
||||
const title = "contract pr create";
|
||||
const bodyFile = join(tmpdir(), `unidesk-gh-pr-contract-${process.pid}.md`);
|
||||
writeFileSync(bodyFile, "Line 1\n`code`\n| a | b |\n", "utf8");
|
||||
@@ -519,6 +552,7 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
"pr read/view accept owner/repo#number shorthand and reject conflicting --repo",
|
||||
"pr read/view --raw is explicit full disclosure",
|
||||
"pr read normalizes open and merged lifecycle fields from REST",
|
||||
"GitHub DNS/API transients are retryable and distinct from auth or PR semantic failures",
|
||||
"pr view closeout metadata fields are accepted and hydrated through GraphQL",
|
||||
"pr read unsupported fields fail structurally with supported closeout fields listed",
|
||||
"pr create dry-run exposes planned operation",
|
||||
|
||||
@@ -320,7 +320,7 @@ interface CodexSkillsSyncOptions {
|
||||
full: boolean;
|
||||
}
|
||||
|
||||
type CodeQueuePrPreflightFailureKind = "auth-missing" | "runner-skills-blocker" | "proxy-gap" | "git-remote-gap" | "control-plane-missing" | "target-stack-not-running";
|
||||
type CodeQueuePrPreflightFailureKind = "auth-missing" | "runner-skills-blocker" | "github-transient" | "proxy-gap" | "git-remote-gap" | "control-plane-missing" | "target-stack-not-running";
|
||||
type CodeQueueObservationGapKind = "runner-local-observation-gap" | "control-plane-observation-gap" | null;
|
||||
|
||||
interface CodeQueuePrPreflightTransport {
|
||||
@@ -3817,6 +3817,64 @@ function compactCommandProbe(value: unknown): Record<string, unknown> | null {
|
||||
};
|
||||
}
|
||||
|
||||
function commandProbeFailureText(value: unknown): string {
|
||||
const probe = asRecord(value);
|
||||
if (probe === null || probe.ok === true) return "";
|
||||
const args = Array.isArray(probe.args) ? probe.args.map(String).join(" ") : "";
|
||||
return [
|
||||
typeof probe.command === "string" ? probe.command : "",
|
||||
args,
|
||||
typeof probe.error === "string" ? probe.error : "",
|
||||
typeof probe.stderr === "string" ? probe.stderr : "",
|
||||
typeof probe.stdout === "string" ? probe.stdout : "",
|
||||
].filter((item) => item.length > 0).join("\n");
|
||||
}
|
||||
|
||||
function commandProbeLooksLikeGithubTransient(value: unknown): boolean {
|
||||
const text = commandProbeFailureText(value);
|
||||
if (text.length === 0) return false;
|
||||
if (/\bproxy\b/iu.test(text) && /could not resolve|connect|tunnel|proxy/iu.test(text)) return false;
|
||||
return /temporary failure in name resolution|error connecting to api\.github\.com|getaddrinfo|could not resolve host:? (github\.com|api\.github\.com)|name or service not known|eai_again|enotfound|etimedout|econnreset|ehostunreach|enetunreach|failed to connect to (github\.com|api\.github\.com)|network is unreachable/iu.test(text);
|
||||
}
|
||||
|
||||
function githubTransientEvidence(pull: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const egress = asRecord(pull.egress) ?? {};
|
||||
const remote = asRecord(pull.remote);
|
||||
const probes: Array<[string, unknown]> = [
|
||||
["egress.githubDefault", egress.githubDefault],
|
||||
["egress.apiDefault", egress.apiDefault],
|
||||
["egress.issueApi", egress.issueApi],
|
||||
];
|
||||
if (remote !== null) {
|
||||
probes.push(
|
||||
["remote.gitLsRemote", remote.gitLsRemote],
|
||||
["remote.gitHttpsLsRemote", remote.gitHttpsLsRemote],
|
||||
["remote.ghAuthStatus", remote.ghAuthStatus],
|
||||
["remote.ghRepoView", remote.ghRepoView],
|
||||
["remote.ghIssueView", remote.ghIssueView],
|
||||
["remote.ghPrReadOnly", remote.ghPrReadOnly],
|
||||
);
|
||||
}
|
||||
const failedProbes = probes
|
||||
.map(([name, probe]) => ({ name, probe, text: commandProbeFailureText(probe) }))
|
||||
.filter((item) => commandProbeLooksLikeGithubTransient(item.probe))
|
||||
.slice(0, 4);
|
||||
if (failedProbes.length === 0) return null;
|
||||
return {
|
||||
kind: "github-transient",
|
||||
retryable: true,
|
||||
scope: "github-dns-api",
|
||||
failedProbes: failedProbes.map((item) => ({
|
||||
name: item.name,
|
||||
preview: compactInlinePreview(item.text, 240),
|
||||
})),
|
||||
commanderAction: "retry/backoff; if Code Queue heartbeat or trace is fresh, keep the task running and continue supervision",
|
||||
notAuthMissing: true,
|
||||
notPrSemanticFailure: true,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactToolStatus(value: unknown): Record<string, unknown> {
|
||||
const tool = asRecord(value) ?? {};
|
||||
return {
|
||||
@@ -4183,21 +4241,26 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
const limitations = Array.isArray(pull.limitations) ? pull.limitations.map(String) : [];
|
||||
const risks = Array.isArray(pull.risks) ? pull.risks.map(String) : [];
|
||||
const ok = preflight.ok === true && tokenCoverage.ok === true;
|
||||
const githubTransient = githubTransientEvidence(pull);
|
||||
const skillsBlocked = skills !== null && skills.ok !== true;
|
||||
const skillsSyncBlocked = skillsSync !== null && skillsSync.ok !== true;
|
||||
const failureKind = !tokenCoverage.ok
|
||||
? "auth-missing"
|
||||
: skillsBlocked || skillsSyncBlocked
|
||||
? "runner-skills-blocker"
|
||||
: limitations.some((item) => item.includes("git ls-remote") || item.includes("git push --dry-run failed"))
|
||||
? "git-remote-gap"
|
||||
: !preflight.ok
|
||||
? "proxy-gap"
|
||||
: null;
|
||||
: githubTransient !== null
|
||||
? "github-transient"
|
||||
: limitations.some((item) => item.includes("git ls-remote") || item.includes("git push --dry-run failed"))
|
||||
? "git-remote-gap"
|
||||
: !preflight.ok
|
||||
? "proxy-gap"
|
||||
: null;
|
||||
const degradedReason = failureKind === "auth-missing"
|
||||
? "auth-broker-needed"
|
||||
: failureKind === "runner-skills-blocker"
|
||||
? typeof skillsSync?.blocker === "string" ? skillsSync.blocker : typeof skills?.blocker === "string" ? skills.blocker : "runner-skills-degraded"
|
||||
: failureKind === "github-transient"
|
||||
? "github-dns-api-transient"
|
||||
: failureKind === "git-remote-gap"
|
||||
? "git remote probe failed"
|
||||
: failureKind === "proxy-gap"
|
||||
@@ -4326,12 +4389,19 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
limitations,
|
||||
risks,
|
||||
runnerDisposition: ok ? "ready" : "infra-blocked",
|
||||
...(githubTransient === null ? {} : {
|
||||
retryable: true,
|
||||
commanderAction: "retry-backoff-or-keep-running-if-heartbeat-fresh",
|
||||
githubTransient,
|
||||
}),
|
||||
activeRunnerDevContainer,
|
||||
recoveryHint: ok
|
||||
? tokenCoverage.source === "auth-broker"
|
||||
? "Runner PR workflow has auth-broker coverage for GitHub REST preflight; real PR creation still requires commander authorization."
|
||||
: "Runner PR workflow has env-token coverage for the scheduler."
|
||||
: "Scheduler preflight lacks GitHub auth coverage for scheduler-scoped admission only. First verify the active task with repo-native gh auth status and PR create dry-run; long-term configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN into the scheduler runtime secret.",
|
||||
: githubTransient !== null
|
||||
? "GitHub DNS/API connectivity failed transiently. Retry with backoff; if the task heartbeat or trace is fresh, keep supervising the running task instead of closing or requeueing business work."
|
||||
: "Scheduler preflight lacks GitHub auth coverage for scheduler-scoped admission only. First verify the active task with repo-native gh auth status and PR create dry-run; long-term configure auth-broker or inject GH_TOKEN/GITHUB_TOKEN into the scheduler runtime secret.",
|
||||
commands: {
|
||||
local: "bun scripts/cli.ts gh auth status --repo pikasTech/unidesk",
|
||||
runner: "bun scripts/cli.ts codex pr-preflight --remote",
|
||||
@@ -4582,6 +4652,8 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP
|
||||
runnerDisposition: compact.runnerDisposition,
|
||||
failureKind: compact.failureKind ?? null,
|
||||
degradedReason: compact.degradedReason ?? null,
|
||||
...(compact.retryable === true ? { retryable: true } : {}),
|
||||
...(typeof compact.commanderAction === "string" ? { commanderAction: compact.commanderAction } : {}),
|
||||
authScopeSummary: compact.authScopeSummary,
|
||||
scopeBoundary: compact.scopeBoundary,
|
||||
activeRunnerDevContainer: compact.activeRunnerDevContainer,
|
||||
@@ -4638,6 +4710,8 @@ export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher:
|
||||
runnerDisposition: compact.runnerDisposition,
|
||||
failureKind: compact.failureKind ?? null,
|
||||
degradedReason: compact.degradedReason ?? null,
|
||||
...(compact.retryable === true ? { retryable: true } : {}),
|
||||
...(typeof compact.commanderAction === "string" ? { commanderAction: compact.commanderAction } : {}),
|
||||
authScopeSummary: compact.authScopeSummary,
|
||||
scopeBoundary: compact.scopeBoundary,
|
||||
activeRunnerDevContainer: compact.activeRunnerDevContainer,
|
||||
|
||||
+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