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
15 changes: 14 additions & 1 deletion actions/setup/js/generate_aw_info.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { validateContextVariables } = require("./validate_context_variables.cjs")
const validateLockdownRequirements = require("./validate_lockdown_requirements.cjs");
const { writeMergedModelsJSON } = require("./merge_frontmatter_models.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { ERR_CONFIG } = require("./error_codes.cjs");

/**
* Generate aw_info.json with workflow run metadata.
Expand Down Expand Up @@ -52,11 +53,23 @@ async function main(core, ctx) {
}

// Build awInfo from env vars (compile-time) + context (runtime)
const model = process.env.GH_AW_INFO_MODEL || "";

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.

Raw model value interpolated into error message — risk of log injection/corruption: if the model string contains ANSI escape sequences or newlines, it can corrupt GitHub Actions log output or mislead log parsers.

💡 Suggested fix

Stringify the value before embedding it in the message:

const message = `${ERR_CONFIG}: GH_AW_INFO_MODEL contains an unresolved GitHub Actions expression: ${JSON.stringify(model)}`;

This ensures the full raw value is safely visible in logs without escaping into the surrounding log stream.

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.

Updated the failure path in 7b76783 to stringify GH_AW_INFO_MODEL before embedding it in the error message, and kept the assertion coverage aligned with that output.


// Reject model names that contain an unresolved GitHub Actions expression.
// This can happen when a vars.* expression was used for the model in the
// workflow frontmatter but the variable is not defined in the repository,
// causing GitHub Actions to pass the literal expression string at runtime.
if (/\$\{\{/.test(model)) {
const message = `${ERR_CONFIG}: GH_AW_INFO_MODEL contains an unresolved GitHub Actions expression: ${JSON.stringify(model)}`;
core.setFailed(message);
throw new Error(message);
}

/** @type {Record<string, unknown>} */
const awInfo = {
engine_id: process.env.GH_AW_INFO_ENGINE_ID || "",
engine_name: process.env.GH_AW_INFO_ENGINE_NAME || "",
model: process.env.GH_AW_INFO_MODEL || "",
model,
version: process.env.GH_AW_INFO_VERSION || "",
agent_version: process.env.GH_AW_INFO_AGENT_VERSION || "",
workflow_name: process.env.GH_AW_INFO_WORKFLOW_NAME || "",
Expand Down
51 changes: 51 additions & 0 deletions actions/setup/js/generate_aw_info.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,57 @@ describe("generate_aw_info.cjs", () => {
expect(awInfo.steps.firewall).toBe("squid");
});

it("should fail when model name contains an unresolved GitHub Actions expression", async () => {
process.env.GH_AW_INFO_MODEL = "${{ vars.COPILOT_MODEL }}";

await expect(main(mockCore, mockContext)).rejects.toThrow("ERR_CONFIG");
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("ERR_CONFIG"));
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("unresolved GitHub Actions expression"));
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(JSON.stringify("${{ vars.COPILOT_MODEL }}")));
});

it("should fail when model name contains a partial unresolved expression", async () => {
process.env.GH_AW_INFO_MODEL = "${{ vars.MODEL || 'gpt-4' }}";

await expect(main(mockCore, mockContext)).rejects.toThrow("ERR_CONFIG");
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("ERR_CONFIG"));
});

it("should not fail when model name does not contain unresolved expressions", async () => {
const cases = [
{ label: "plain string", model: "gpt-4o" },
{ label: "empty string", model: "" },
];

for (const { label, model } of cases) {
process.env.GH_AW_INFO_MODEL = model;

try {
await main(mockCore, mockContext);

expect(mockCore.setFailed).not.toHaveBeenCalled();
const awInfo = JSON.parse(fs.readFileSync(awInfoPath, "utf8"));
expect(awInfo.model).toBe(model);
} catch (error) {
throw new Error(`${label} case failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
});

it("should not fail when model is a resolved experiment variant string", async () => {
// Workflows using experiment variants set model to a GitHub Actions expression in frontmatter
// (e.g. model: "${{ needs.activation.outputs.model_size }}"). GitHub Actions evaluates
// that expression before passing the value as an environment variable, so by the time
// generate_aw_info.cjs runs, GH_AW_INFO_MODEL holds the resolved variant string.
process.env.GH_AW_INFO_MODEL = "claude-haiku-4.5";

await main(mockCore, mockContext);

expect(mockCore.setFailed).not.toHaveBeenCalled();
const awInfo = JSON.parse(fs.readFileSync(awInfoPath, "utf8"));
expect(awInfo.model).toBe("claude-haiku-4.5");
});

it("should fail when a numeric context field contains non-numeric data", async () => {
const maliciousContext = {
...mockContext,
Expand Down