Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
60 changes: 15 additions & 45 deletions actions/setup/js/validate_lockdown_requirements.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,19 @@
* @param {any} core - GitHub Actions core library
* @returns {void}
*/
const { ERR_VALIDATION } = require("./error_codes.cjs");
const { renderLockdownTokenErrorMessage, renderPublicStrictModeErrorMessage, renderPullRequestTargetErrorMessage } = require("./validate_lockdown_requirements_templates.cjs");

function validateLockdownRequirements(core) {
/**
* @param {string} message
* @returns {never}
*/
function failWithError(message) {
core.setOutput("lockdown_check_failed", "true");
core.setFailed(message);
throw new Error(message);
}

// Check if lockdown mode is explicitly enabled (set to "true" in frontmatter)
const lockdownEnabled = process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT === "true";

Expand All @@ -46,22 +57,7 @@ function validateLockdownRequirements(core) {
core.info(`Custom github-token configured: ${hasCustomToken}`);

if (!hasAnyCustomToken) {
const errorMessage =
"Lockdown mode is enabled (lockdown: true) but no custom GitHub token is configured.\\n" +
"\\n" +
"Please configure one of the following as a repository secret:\\n" +
" - GH_AW_GITHUB_TOKEN (recommended)\\n" +
" - GH_AW_GITHUB_MCP_SERVER_TOKEN (alternative)\\n" +
" - Custom github-token in your workflow frontmatter\\n" +
"\\n" +
"See: https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/auth.mdx\\n" +
"\\n" +
"To set a token:\\n" +
' gh aw secrets set GH_AW_GITHUB_TOKEN --value "YOUR_FINE_GRAINED_PAT"';

core.setOutput("lockdown_check_failed", "true");
core.setFailed(errorMessage);
throw new Error(errorMessage);
failWithError(renderLockdownTokenErrorMessage());
}

core.info("✓ Lockdown mode requirements validated: Custom GitHub token is configured");
Expand All @@ -77,20 +73,7 @@ function validateLockdownRequirements(core) {
core.info(`Compiled with strict mode: ${isStrict}`);

if (isPublic && !isStrict) {
const errorMessage =
"This workflow is running on a public repository but was not compiled with strict mode.\\n" +
"\\n" +
"Public repository workflows must be compiled with strict mode enabled to meet\\n" +
"the security requirements for public exposure.\\n" +
"\\n" +
"To fix this, recompile the workflow with strict mode:\\n" +
" gh aw compile --strict\\n" +
"\\n" +
"See: https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/security.mdx";

core.setOutput("lockdown_check_failed", "true");
core.setFailed(errorMessage);
throw new Error(errorMessage);
failWithError(renderPublicStrictModeErrorMessage());
}

if (isPublic && isStrict) {
Expand All @@ -104,20 +87,7 @@ function validateLockdownRequirements(core) {
// and potentially exfiltrate secrets or cause unintended side effects.
const eventName = process.env.GITHUB_EVENT_NAME;
if (isPublic && eventName === "pull_request_target") {
const errorMessage =
"This workflow is triggered by the pull_request_target event on a public repository.\\n" +
"\\n" +
"The pull_request_target event is not allowed on public repositories because it runs\\n" +
"workflows with access to repository secrets even when triggered from a fork, which\\n" +
'creates a significant security risk (known as a "pwn request").\\n' +
"\\n" +
"To fix this, use the pull_request event instead, or migrate to a private repository.\\n" +
"\\n" +
"See: https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/security.mdx";

core.setOutput("lockdown_check_failed", "true");
core.setFailed(errorMessage);
throw new Error(errorMessage);
failWithError(renderPullRequestTargetErrorMessage());
}
}

Expand Down
82 changes: 82 additions & 0 deletions actions/setup/js/validate_lockdown_requirements.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,86 @@ describe("validate_lockdown_requirements", () => {
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("not compiled with strict mode"));
});
});

describe("setOutput side effects", () => {
it("should not set lockdown_check_failed output on a fully successful run", () => {
// All conditions pass: no lockdown, private repo, no special event
process.env.GITHUB_REPOSITORY_VISIBILITY = "private";
process.env.GITHUB_EVENT_NAME = "push";

validateLockdownRequirements(mockCore);

expect(mockCore.setOutput).not.toHaveBeenCalled();
expect(mockCore.setFailed).not.toHaveBeenCalled();
});

it("should not set lockdown_check_failed when lockdown is enabled with token and repo is private", () => {
process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT = "true";
process.env.GH_AW_GITHUB_TOKEN = "ghp_test";
process.env.GITHUB_REPOSITORY_VISIBILITY = "private";

validateLockdownRequirements(mockCore);

expect(mockCore.setOutput).not.toHaveBeenCalled();
});
});

describe("multiple tokens", () => {
it("should pass when all three tokens are configured simultaneously", () => {
process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT = "true";
process.env.GH_AW_GITHUB_TOKEN = "ghp_token1";
process.env.GH_AW_GITHUB_MCP_SERVER_TOKEN = "ghp_token2";
process.env.CUSTOM_GITHUB_TOKEN = "ghp_token3";

validateLockdownRequirements(mockCore);

expect(mockCore.setFailed).not.toHaveBeenCalled();
expect(mockCore.info).toHaveBeenCalledWith("GH_AW_GITHUB_TOKEN configured: true");
expect(mockCore.info).toHaveBeenCalledWith("GH_AW_GITHUB_MCP_SERVER_TOKEN configured: true");
expect(mockCore.info).toHaveBeenCalledWith("Custom github-token configured: true");
expect(mockCore.info).toHaveBeenCalledWith("✓ Lockdown mode requirements validated: Custom GitHub token is configured");
});
});

describe("error message formatting", () => {
it("should include newlines in lockdown error message for readability", () => {
process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT = "true";

expect(() => {
validateLockdownRequirements(mockCore);
}).toThrow();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] .toThrow() without an argument does not verify which error is thrown — inconsistent with the existing suite (line 301 uses .toThrow("not compiled with strict mode")).

💡 Match the specificity of existing tests

The pre-existing test block uses:

}).toThrow("not compiled with strict mode");

Applying the same pattern here makes all three new formatting tests consistent and catches regressions in the thrown message, not just the fact that something was thrown:

// lockdown test
}).toThrow("no custom GitHub token is configured");

// strict mode test
}).toThrow("not compiled with strict mode");

// pull_request_target test
}).toThrow("pwn request");


const errorMsg = mockCore.setFailed.mock.calls[0][0];
expect(errorMsg).toContain("\n");
expect(errorMsg).toContain("GH_AW_GITHUB_TOKEN (recommended)");
expect(errorMsg).toContain("GH_AW_GITHUB_MCP_SERVER_TOKEN (alternative)");
});

it("should include newlines in strict mode error message for readability", () => {
process.env.GITHUB_REPOSITORY_VISIBILITY = "public";
process.env.GH_AW_COMPILED_STRICT = "false";

expect(() => {
validateLockdownRequirements(mockCore);
}).toThrow();

const errorMsg = mockCore.setFailed.mock.calls[0][0];
expect(errorMsg).toContain("\n");
expect(errorMsg).toContain("gh aw compile --strict");
});

it("should include newlines in pull_request_target error message for readability", () => {
process.env.GITHUB_REPOSITORY_VISIBILITY = "public";
process.env.GH_AW_COMPILED_STRICT = "true";
process.env.GITHUB_EVENT_NAME = "pull_request_target";

expect(() => {
validateLockdownRequirements(mockCore);
}).toThrow();

const errorMsg = mockCore.setFailed.mock.calls[0][0];
expect(errorMsg).toContain("\n");
expect(errorMsg).toContain("pwn request");
});
});
});
59 changes: 59 additions & 0 deletions actions/setup/js/validate_lockdown_requirements_templates.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// @ts-check

const { renderTemplate } = require("./messages_core.cjs");

const LOCKDOWN_TOKEN_ERROR_TEMPLATE = `Lockdown mode is enabled (lockdown: true) but no custom GitHub token is configured.

Please configure one of the following as a repository secret:
- GH_AW_GITHUB_TOKEN (recommended)
- GH_AW_GITHUB_MCP_SERVER_TOKEN (alternative)
- Custom github-token in your workflow frontmatter

See: {auth_docs_url}

To set a token:
gh aw secrets set GH_AW_GITHUB_TOKEN --value "YOUR_FINE_GRAINED_PAT"`;

const PUBLIC_STRICT_MODE_ERROR_TEMPLATE = `This workflow is running on a public repository but was not compiled with strict mode.

Public repository workflows must be compiled with strict mode enabled to meet
the security requirements for public exposure.

To fix this, recompile the workflow with strict mode:
{strict_compile_command}

See: {security_docs_url}`;

const PULL_REQUEST_TARGET_ERROR_TEMPLATE = `This workflow is triggered by the pull_request_target event on a public repository.

The pull_request_target event is not allowed on public repositories because it runs
workflows with access to repository secrets even when triggered from a fork, which
creates a significant security risk (known as a "pwn request").

To fix this, use the pull_request event instead, or migrate to a private repository.

See: {security_docs_url}`;

const TEMPLATE_CONTEXT = {
auth_docs_url: "https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/auth.mdx",
security_docs_url: "https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/security.mdx",
strict_compile_command: "gh aw compile --strict",
};

function renderLockdownTokenErrorMessage() {
return renderTemplate(LOCKDOWN_TOKEN_ERROR_TEMPLATE, TEMPLATE_CONTEXT);
}

function renderPublicStrictModeErrorMessage() {
return renderTemplate(PUBLIC_STRICT_MODE_ERROR_TEMPLATE, TEMPLATE_CONTEXT);
}

function renderPullRequestTargetErrorMessage() {
return renderTemplate(PULL_REQUEST_TARGET_ERROR_TEMPLATE, TEMPLATE_CONTEXT);
}

module.exports = {
renderLockdownTokenErrorMessage,
renderPublicStrictModeErrorMessage,
renderPullRequestTargetErrorMessage,
};