mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-22 03:18:54 +08:00
fix: skip workflow validation token exchange failures (#1417)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import * as core from "@actions/core";
|
||||
import {
|
||||
setupGitHubToken,
|
||||
WorkflowValidationSkipError,
|
||||
} from "../src/github/token";
|
||||
|
||||
describe("setupGitHubToken", () => {
|
||||
let originalOverrideToken: string | undefined;
|
||||
let originalAdditionalPermissions: string | undefined;
|
||||
let getIDTokenSpy: any;
|
||||
let setSecretSpy: any;
|
||||
let warningSpy: any;
|
||||
let fetchSpy: any;
|
||||
let setTimeoutSpy: any;
|
||||
let consoleLogSpy: any;
|
||||
let consoleErrorSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
originalOverrideToken = process.env.OVERRIDE_GITHUB_TOKEN;
|
||||
originalAdditionalPermissions = process.env.ADDITIONAL_PERMISSIONS;
|
||||
delete process.env.OVERRIDE_GITHUB_TOKEN;
|
||||
delete process.env.ADDITIONAL_PERMISSIONS;
|
||||
|
||||
getIDTokenSpy = spyOn(core, "getIDToken").mockResolvedValue("oidc-token");
|
||||
setSecretSpy = spyOn(core, "setSecret").mockImplementation(() => {});
|
||||
warningSpy = spyOn(core, "warning").mockImplementation(() => {});
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ token: "app-token" }), {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
}),
|
||||
);
|
||||
setTimeoutSpy = spyOn(global, "setTimeout").mockImplementation(((
|
||||
handler: any,
|
||||
) => {
|
||||
handler();
|
||||
return 0 as any;
|
||||
}) as any);
|
||||
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
|
||||
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalOverrideToken === undefined) {
|
||||
delete process.env.OVERRIDE_GITHUB_TOKEN;
|
||||
} else {
|
||||
process.env.OVERRIDE_GITHUB_TOKEN = originalOverrideToken;
|
||||
}
|
||||
|
||||
if (originalAdditionalPermissions === undefined) {
|
||||
delete process.env.ADDITIONAL_PERMISSIONS;
|
||||
} else {
|
||||
process.env.ADDITIONAL_PERMISSIONS = originalAdditionalPermissions;
|
||||
}
|
||||
|
||||
getIDTokenSpy.mockRestore();
|
||||
setSecretSpy.mockRestore();
|
||||
warningSpy.mockRestore();
|
||||
fetchSpy.mockRestore();
|
||||
setTimeoutSpy.mockRestore();
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("returns app token from OIDC exchange", async () => {
|
||||
await expect(setupGitHubToken()).resolves.toBe("app-token");
|
||||
|
||||
expect(getIDTokenSpy).toHaveBeenCalledWith("claude-code-github-action");
|
||||
expect(setSecretSpy).toHaveBeenCalledWith("app-token");
|
||||
});
|
||||
|
||||
test("skips without retrying when workflow is missing from default branch", async () => {
|
||||
const message =
|
||||
"Workflow validation failed. The workflow file must exist and have identical content to the version on the repository's default branch.";
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message,
|
||||
details: {
|
||||
error_code: "workflow_not_found_on_default_branch",
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ status: 401, statusText: "Unauthorized" },
|
||||
),
|
||||
);
|
||||
|
||||
await expect(setupGitHubToken()).rejects.toBeInstanceOf(
|
||||
WorkflowValidationSkipError,
|
||||
);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(warningSpy).toHaveBeenCalledWith(
|
||||
`Skipping action due to workflow validation: ${message}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("skips without retrying when workflow validation message has no error code", async () => {
|
||||
const message =
|
||||
"Workflow validation failed. The workflow file must exist and have identical content to the version on the repository's default branch.";
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message,
|
||||
},
|
||||
}),
|
||||
{ status: 401, statusText: "Unauthorized" },
|
||||
),
|
||||
);
|
||||
|
||||
await expect(setupGitHubToken()).rejects.toBeInstanceOf(
|
||||
WorkflowValidationSkipError,
|
||||
);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(warningSpy).toHaveBeenCalledWith(
|
||||
`Skipping action due to workflow validation: ${message}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("retries ordinary token exchange errors instead of skipping", async () => {
|
||||
const message = "Bad credentials";
|
||||
fetchSpy.mockImplementation(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message,
|
||||
},
|
||||
}),
|
||||
{ status: 401, statusText: "Unauthorized" },
|
||||
),
|
||||
);
|
||||
|
||||
await expect(setupGitHubToken()).rejects.toThrow(message);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
expect(warningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not skip message-only workflow validation errors with unexpected status", async () => {
|
||||
const message =
|
||||
"Workflow validation failed. The workflow file must exist and have identical content to the version on the repository's default branch.";
|
||||
fetchSpy.mockImplementation(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message,
|
||||
},
|
||||
}),
|
||||
{ status: 500, statusText: "Internal Server Error" },
|
||||
),
|
||||
);
|
||||
|
||||
await expect(setupGitHubToken()).rejects.toThrow(message);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
||||
expect(warningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user