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
7 changes: 4 additions & 3 deletions actions/setup/js/redact_secrets.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ function findFiles(dir, extensions) {
const BUILT_IN_PATTERNS = [
// GitHub tokens
{ name: "GitHub Personal Access Token (classic)", pattern: /ghp_[0-9a-zA-Z]{36}/g },
// Match both legacy 36-character installation tokens and the newer long
// JWT-like stateless tokens with 3+ dot-separated base64url-ish segments.
{ name: "GitHub Server-to-Server Token", pattern: /ghs_(?:[0-9a-zA-Z]{36}(?![0-9A-Za-z._-])|[0-9A-Za-z_-]{10,}(?:\.[0-9A-Za-z_-]{10,}){2,})/g },
// New stateless ghs_ token format allows dots, underscores, and dashes
// and uses variable length (minimum 36 chars after prefix).
// https://github.blog/changelog/2026-05-15-github-app-installation-tokens-per-request-override-header/
{ name: "GitHub Server-to-Server Token", pattern: /ghs_[0-9A-Za-z._-]{36,}/g },
{ name: "GitHub OAuth Access Token", pattern: /gho_[0-9a-zA-Z]{36}/g },
{ name: "GitHub User Access Token", pattern: /ghu_[0-9a-zA-Z]{36}/g },
{ name: "GitHub Fine-grained PAT", pattern: /github_pat_[0-9a-zA-Z_]{82}/g },
Expand Down
30 changes: 24 additions & 6 deletions actions/setup/js/redact_secrets.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe("redact_secrets.cjs", () => {
`sig_${"A".repeat(40)}`,
];
const ghToken = `${["gh", "s_"].join("")}${tokenSegments.join(".")}`;
expect(ghToken).toMatch(/^ghs_[0-9A-Za-z_-]{10,}(?:\.[0-9A-Za-z_-]{10,}){2,}$/);
expect(ghToken).toMatch(/^ghs_[0-9A-Za-z._-]{36,}$/);
fs.writeFileSync(testFile, `Long server token: ${ghToken}`);
process.env.GH_AW_SECRET_NAMES = "";
const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`);
Expand All @@ -190,8 +190,9 @@ describe("redact_secrets.cjs", () => {

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.

[/tdd] Stale test name: "JWT-like" no longer reflects the new unified format — consider renaming to something like "should redact boundary-length GitHub Server-to-Server Token (ghs_)".

💡 Suggested rename
it("should redact boundary-length GitHub Server-to-Server Token (ghs_)", async () => {

The JWT-segment structure was intentional in the old test (it verified the dot-separated 3-segment shape). Since the token format is now just ghs_[A-Za-z0-9._-]{36,}, the "JWT-like" label is misleading and may confuse future readers about what invariant is actually being asserted.

it("should redact boundary-length JWT-like GitHub Server-to-Server Token (ghs_)", async () => {
const testFile = path.join(tempDir, "test.txt");
const ghToken = `${["gh", "s_"].join("")}${["abcdefghij", "klmnopqrst", "uvwxyzABCD"].join(".")}`;
expect(ghToken).toMatch(/^ghs_[0-9A-Za-z_-]{10,}(?:\.[0-9A-Za-z_-]{10,}){2,}$/);
const ghToken = "ghs_abcdefghijk.lmnopqrstuv.wxyzABCDEFGH";
expect(ghToken.slice(4)).toHaveLength(36);
expect(ghToken).toMatch(/^ghs_[0-9A-Za-z._-]{36,}$/);
fs.writeFileSync(testFile, `Boundary server token: ${ghToken}`);
process.env.GH_AW_SECRET_NAMES = "";
const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`);
Expand All @@ -201,6 +202,23 @@ describe("redact_secrets.cjs", () => {
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub Server-to-Server Token"));
});

it("should redact dash-containing GitHub Server-to-Server Token (ghs_)", async () => {
const testFile = path.join(tempDir, "test.txt");
const ghToken = "ghs_abcd-efghij.klmn_opqr.stuvwxyz012345";
expect(ghToken.slice(4)).toHaveLength(36);
expect(ghToken).toContain("-");
expect(ghToken).toContain(".");
expect(ghToken).toContain("_");
expect(ghToken).toMatch(/^ghs_[0-9A-Za-z._-]{36,}$/);
fs.writeFileSync(testFile, `Dashed server token: ${ghToken}`);
process.env.GH_AW_SECRET_NAMES = "";
const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`);
await eval(`(async () => { ${modifiedScript}; await main(); })()`);
const redacted = fs.readFileSync(testFile, "utf8");
expect(redacted).toBe("Dashed server token: ***REDACTED***");
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub Server-to-Server Token"));
});

it("should redact GitHub OAuth Access Token (gho_)", async () => {
const testFile = path.join(tempDir, "test.txt");
const ghToken = "gho_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz";
Expand Down Expand Up @@ -462,13 +480,13 @@ describe("redact_secrets.cjs", () => {
it("should not redact partial matches", async () => {
const testFile = path.join(tempDir, "test.txt");
// These should NOT be redacted (not valid token formats)
fs.writeFileSync(testFile, "ghp_short ghs_toolong_this_is_not_a_valid_token_because_its_way_too_long ghs_short.segment.other ghs_abcdefghij.klmnopqrst");
fs.writeFileSync(testFile, "ghp_short ghs_invalid*token_because_it_has_disallowed_characters_and_is_long_enough ghs_short.segment.other ghs_12345678901234567890123456789012345 ghs_abcdefghij.klmnopqrst");
process.env.GH_AW_SECRET_NAMES = "";
const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`);
await eval(`(async () => { ${modifiedScript}; await main(); })()`);
const content = fs.readFileSync(testFile, "utf8");
// These should remain unchanged since they don't match the exact pattern
expect(content).toBe("ghp_short ghs_toolong_this_is_not_a_valid_token_because_its_way_too_long ghs_short.segment.other ghs_abcdefghij.klmnopqrst");
// Includes a 35-char ghs_ token (below the 36-char minimum) and a short dot-separated variant.
expect(content).toBe("ghp_short ghs_invalid*token_because_it_has_disallowed_characters_and_is_long_enough ghs_short.segment.other ghs_12345678901234567890123456789012345 ghs_abcdefghij.klmnopqrst");
});

it("should handle URLs with secrets", async () => {
Expand Down
Loading