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
3 changes: 2 additions & 1 deletion .github/workflows/contribution-check.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions .github/workflows/smoke-copilot-aoai-apikey.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions .github/workflows/smoke-copilot-aoai-entra.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions .github/workflows/smoke-copilot-arm.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions .github/workflows/smoke-copilot.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions .github/workflows/squad-implement-worker.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions .github/workflows/squad.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 20 additions & 1 deletion actions/setup/js/generate_safe_outputs_tools.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,25 @@ function applyAssignMilestoneAlternativeRequirements(tool) {
schema.anyOf = [{ required: ["milestone_number"] }, { required: ["milestone_title"] }];
}

/**
* Resolve ${ENV_VAR} placeholders inside a JSON string from process.env.
* Replacement values are escaped as JSON string content so quotes, backslashes, and
* newlines in the resolved value do not corrupt the surrounding JSON document.
* Unresolved placeholders are left unchanged.
* @param {string} value
* @returns {string}
*/
function resolveEnvStringPlaceholders(value) {
return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, envName) => {
const envValue = process.env[envName];
if (envValue === undefined) {
return match;
}
// JSON.stringify wraps the value in quotes; strip them to get escaped string content.
return JSON.stringify(envValue).slice(1, -1);
});
}

async function main() {
const toolsSourcePath = process.env.GH_AW_SAFE_OUTPUTS_TOOLS_SOURCE_PATH || `${process.env.RUNNER_TEMP}/gh-aw/actions/safe_outputs_tools.json`;
const configPath = process.env.GH_AW_SAFE_OUTPUTS_CONFIG_PATH || `${process.env.RUNNER_TEMP}/gh-aw/safeoutputs/config.json`;
Expand All @@ -210,7 +229,7 @@ async function main() {
// Write JSON payloads from env vars if provided (replaces heredoc-based file writing)
if (process.env.GH_AW_TOOLS_META_JSON) {
try {
fs.writeFileSync(toolsMetaPath, process.env.GH_AW_TOOLS_META_JSON);
fs.writeFileSync(toolsMetaPath, resolveEnvStringPlaceholders(process.env.GH_AW_TOOLS_META_JSON));
} catch (err) {
throw new Error(`${ERR_SYSTEM}: Failed to write file ${toolsMetaPath}: ${getErrorMessage(err)}`, { cause: err });
}
Expand Down
89 changes: 89 additions & 0 deletions actions/setup/js/generate_safe_outputs_tools.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,95 @@ describe("generate_safe_outputs_tools", () => {
expect(result).toHaveLength(0);
});

it("resolves env placeholders in GH_AW_TOOLS_META_JSON", () => {
fs.writeFileSync(configPath, JSON.stringify({ create_issue: { max: 1 } }));
const metaFromEnv = JSON.stringify({
description_suffixes: {
create_issue: " TARGET: ${GH_AW_INPUT_TARGET_REPO}",
},
repo_params: {},
dynamic_tools: [],
});

runScript({
GH_AW_TOOLS_META_JSON: metaFromEnv,
GH_AW_INPUT_TARGET_REPO: "github/gh-aw",
});

const result = JSON.parse(fs.readFileSync(outputPath, "utf8"));
const createIssueTool = result.find((/** @type {{name: string, description: string}} */ t) => t.name === "create_issue");
expect(createIssueTool).toBeDefined();
expect(createIssueTool.description).toContain("TARGET: github/gh-aw");
});

it("resolves multiple distinct env placeholders in GH_AW_TOOLS_META_JSON", () => {
fs.writeFileSync(configPath, JSON.stringify({ create_issue: { max: 1 } }));
const metaFromEnv = JSON.stringify({
description_suffixes: {
create_issue: " TARGET: ${GH_AW_INPUT_TARGET_REPO} OWNER: ${GH_AW_GITHUB_REPOSITORY_OWNER}",
},
repo_params: {},
dynamic_tools: [],
});

runScript({
GH_AW_TOOLS_META_JSON: metaFromEnv,
GH_AW_INPUT_TARGET_REPO: "github/gh-aw",
GH_AW_GITHUB_REPOSITORY_OWNER: "github",
});

const result = JSON.parse(fs.readFileSync(outputPath, "utf8"));
const createIssueTool = result.find((/** @type {{name: string, description: string}} */ t) => t.name === "create_issue");
expect(createIssueTool).toBeDefined();
expect(createIssueTool.description).toContain("TARGET: github/gh-aw");
expect(createIssueTool.description).toContain("OWNER: github");
});

it("leaves unresolved placeholders in GH_AW_TOOLS_META_JSON unchanged", () => {
fs.writeFileSync(configPath, JSON.stringify({ create_issue: { max: 1 } }));
const metaFromEnv = JSON.stringify({
description_suffixes: {
create_issue: " TARGET: ${GH_AW_INPUT_MISSING}",
},
repo_params: {},
dynamic_tools: [],
});

runScript({
GH_AW_TOOLS_META_JSON: metaFromEnv,
});

const result = JSON.parse(fs.readFileSync(outputPath, "utf8"));
const createIssueTool = result.find((/** @type {{name: string, description: string}} */ t) => t.name === "create_issue");
expect(createIssueTool).toBeDefined();
expect(createIssueTool.description).toContain("TARGET: ${GH_AW_INPUT_MISSING}");
});

it("escapes quotes, backslashes, and newlines when resolving GH_AW_TOOLS_META_JSON placeholders", () => {
fs.writeFileSync(configPath, JSON.stringify({ create_issue: { max: 1 } }));
const metaFromEnv = JSON.stringify({
description_suffixes: {
create_issue: " TARGET: ${GH_AW_INPUT_TARGET_REPO}",
},
repo_params: {},
dynamic_tools: [],
});

runScript({
GH_AW_TOOLS_META_JSON: metaFromEnv,
GH_AW_INPUT_TARGET_REPO: 'a"b\\c\nd',
});

// The written tools_meta.json must remain valid JSON despite the unsafe characters.
const writtenMeta = JSON.parse(fs.readFileSync(toolsMetaPath, "utf8"));
expect(writtenMeta.description_suffixes.create_issue).toContain('a"b\\c\nd');

const result = JSON.parse(fs.readFileSync(outputPath, "utf8"));
const createIssueTool = result.find((/** @type {{name: string, description: string}} */ t) => t.name === "create_issue");
expect(createIssueTool).toBeDefined();
expect(createIssueTool.description).toContain('TARGET: a"b\\c\nd');
});

it("ignores non-tool config keys when filtering", () => {
// dispatch_workflow and max_bot_mentions are not tool names in source file
fs.writeFileSync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ describe("no-exec-interpolated-command", () => {
},
// execApi parameter-alias with identifier args (still array-shaped by convention) — flagged
{
code: "function run(execApi, branchName) { execApi.exec(\"git checkout \" + branchName, args); }",
code: 'function run(execApi, branchName) { execApi.exec("git checkout " + branchName, args); }',
errors: [{ messageId: "interpolatedCommand", data: { kind: "dynamic string concatenation", method: "exec" } }],
},
],
Expand Down
Loading
Loading