From df38d7ad6be0bc92372d291e45066dfc1d4656db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:23:07 +0000 Subject: [PATCH 1/5] Add runtime check for unresolved GitHub Actions expressions in model name Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/generate_aw_info.cjs | 15 ++++++++- actions/setup/js/generate_aw_info.test.cjs | 36 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/generate_aw_info.cjs b/actions/setup/js/generate_aw_info.cjs index a8d266a0328..211b58d6dda 100644 --- a/actions/setup/js/generate_aw_info.cjs +++ b/actions/setup/js/generate_aw_info.cjs @@ -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. @@ -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 || ""; + + // 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: ${model}`; + core.setFailed(message); + throw new Error(message); + } + /** @type {Record} */ 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 || "", diff --git a/actions/setup/js/generate_aw_info.test.cjs b/actions/setup/js/generate_aw_info.test.cjs index 489590169fe..6649ce7fb4b 100644 --- a/actions/setup/js/generate_aw_info.test.cjs +++ b/actions/setup/js/generate_aw_info.test.cjs @@ -230,6 +230,42 @@ 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("${{ 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 is a plain string without expressions", async () => { + process.env.GH_AW_INFO_MODEL = "gpt-4o"; + + await main(mockCore, mockContext); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + const awInfo = JSON.parse(fs.readFileSync(awInfoPath, "utf8")); + expect(awInfo.model).toBe("gpt-4o"); + }); + + it("should not fail when model name is empty", async () => { + process.env.GH_AW_INFO_MODEL = ""; + + await main(mockCore, mockContext); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + const awInfo = JSON.parse(fs.readFileSync(awInfoPath, "utf8")); + expect(awInfo.model).toBe(""); + }); + it("should fail when a numeric context field contains non-numeric data", async () => { const maliciousContext = { ...mockContext, From 58ec11f713a158a74ceafbd8b96e2a10a060a511 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:44:29 +0000 Subject: [PATCH 2/5] test: add experiment-resolved model variant test case Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/generate_aw_info.test.cjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/actions/setup/js/generate_aw_info.test.cjs b/actions/setup/js/generate_aw_info.test.cjs index 6649ce7fb4b..968e2d246b3 100644 --- a/actions/setup/js/generate_aw_info.test.cjs +++ b/actions/setup/js/generate_aw_info.test.cjs @@ -266,6 +266,20 @@ describe("generate_aw_info.cjs", () => { expect(awInfo.model).toBe(""); }); + 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, From 66ad5623d135fe6692ef265b060e6b1dcbfdd0bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:02:08 +0000 Subject: [PATCH 3/5] Merge main into branch Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/agentdrain/data/default_weights.json | 50 ++++-------------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/pkg/agentdrain/data/default_weights.json b/pkg/agentdrain/data/default_weights.json index 3bd824cbe51..33a7fe6b8be 100644 --- a/pkg/agentdrain/data/default_weights.json +++ b/pkg/agentdrain/data/default_weights.json @@ -3,12 +3,7 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -54,21 +49,12 @@ "id": 1, "size": 500, "stage": "finish", - "template": [ - "stage=finish", - "\u003c*\u003e", - "tokens=\u003cNUM\u003e" - ] + "template": ["stage=finish", "\u003c*\u003e", "tokens=\u003cNUM\u003e"] } ], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -112,12 +98,7 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -161,12 +142,7 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -210,12 +186,7 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -259,12 +230,7 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": [ - "session_id", - "trace_id", - "span_id", - "timestamp" - ], + "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], "MaskRules": [ { "Name": "uuid", @@ -304,4 +270,4 @@ }, "next_id": 1 } -} \ No newline at end of file +} From af0b051ef4f38ae1a2ace1551d9dbb66247f8cc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:29:00 +0000 Subject: [PATCH 4/5] fix review feedback on model validation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/generate_aw_info.cjs | 2 +- actions/setup/js/generate_aw_info.test.cjs | 26 ++++------- pkg/agentdrain/data/default_weights.json | 50 ++++++++++++++++++---- 3 files changed, 52 insertions(+), 26 deletions(-) diff --git a/actions/setup/js/generate_aw_info.cjs b/actions/setup/js/generate_aw_info.cjs index 211b58d6dda..89b97a6ff5a 100644 --- a/actions/setup/js/generate_aw_info.cjs +++ b/actions/setup/js/generate_aw_info.cjs @@ -60,7 +60,7 @@ async function main(core, ctx) { // 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: ${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); } diff --git a/actions/setup/js/generate_aw_info.test.cjs b/actions/setup/js/generate_aw_info.test.cjs index 968e2d246b3..c4fe5418ac1 100644 --- a/actions/setup/js/generate_aw_info.test.cjs +++ b/actions/setup/js/generate_aw_info.test.cjs @@ -236,7 +236,7 @@ describe("generate_aw_info.cjs", () => { 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("${{ vars.COPILOT_MODEL }}")); + expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining(JSON.stringify("${{ vars.COPILOT_MODEL }}"))); }); it("should fail when model name contains a partial unresolved expression", async () => { @@ -246,24 +246,16 @@ describe("generate_aw_info.cjs", () => { expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("ERR_CONFIG")); }); - it("should not fail when model name is a plain string without expressions", async () => { - process.env.GH_AW_INFO_MODEL = "gpt-4o"; + it("should not fail when model name does not contain unresolved expressions", async () => { + for (const model of ["gpt-4o", ""]) { + process.env.GH_AW_INFO_MODEL = model; - await main(mockCore, mockContext); - - expect(mockCore.setFailed).not.toHaveBeenCalled(); - const awInfo = JSON.parse(fs.readFileSync(awInfoPath, "utf8")); - expect(awInfo.model).toBe("gpt-4o"); - }); - - it("should not fail when model name is empty", async () => { - process.env.GH_AW_INFO_MODEL = ""; + await main(mockCore, mockContext); - await main(mockCore, mockContext); - - expect(mockCore.setFailed).not.toHaveBeenCalled(); - const awInfo = JSON.parse(fs.readFileSync(awInfoPath, "utf8")); - expect(awInfo.model).toBe(""); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + const awInfo = JSON.parse(fs.readFileSync(awInfoPath, "utf8")); + expect(awInfo.model).toBe(model); + } }); it("should not fail when model is a resolved experiment variant string", async () => { diff --git a/pkg/agentdrain/data/default_weights.json b/pkg/agentdrain/data/default_weights.json index 33a7fe6b8be..3bd824cbe51 100644 --- a/pkg/agentdrain/data/default_weights.json +++ b/pkg/agentdrain/data/default_weights.json @@ -3,7 +3,12 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], + "ExcludeFields": [ + "session_id", + "trace_id", + "span_id", + "timestamp" + ], "MaskRules": [ { "Name": "uuid", @@ -49,12 +54,21 @@ "id": 1, "size": 500, "stage": "finish", - "template": ["stage=finish", "\u003c*\u003e", "tokens=\u003cNUM\u003e"] + "template": [ + "stage=finish", + "\u003c*\u003e", + "tokens=\u003cNUM\u003e" + ] } ], "config": { "Depth": 4, - "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], + "ExcludeFields": [ + "session_id", + "trace_id", + "span_id", + "timestamp" + ], "MaskRules": [ { "Name": "uuid", @@ -98,7 +112,12 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], + "ExcludeFields": [ + "session_id", + "trace_id", + "span_id", + "timestamp" + ], "MaskRules": [ { "Name": "uuid", @@ -142,7 +161,12 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], + "ExcludeFields": [ + "session_id", + "trace_id", + "span_id", + "timestamp" + ], "MaskRules": [ { "Name": "uuid", @@ -186,7 +210,12 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], + "ExcludeFields": [ + "session_id", + "trace_id", + "span_id", + "timestamp" + ], "MaskRules": [ { "Name": "uuid", @@ -230,7 +259,12 @@ "clusters": [], "config": { "Depth": 4, - "ExcludeFields": ["session_id", "trace_id", "span_id", "timestamp"], + "ExcludeFields": [ + "session_id", + "trace_id", + "span_id", + "timestamp" + ], "MaskRules": [ { "Name": "uuid", @@ -270,4 +304,4 @@ }, "next_id": 1 } -} +} \ No newline at end of file From 7b7678380448b603388f55087904d614b6bc7bcf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:48:37 +0000 Subject: [PATCH 5/5] test: clarify combined model happy-path case Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/generate_aw_info.test.cjs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/generate_aw_info.test.cjs b/actions/setup/js/generate_aw_info.test.cjs index c4fe5418ac1..c0721ecc26d 100644 --- a/actions/setup/js/generate_aw_info.test.cjs +++ b/actions/setup/js/generate_aw_info.test.cjs @@ -247,14 +247,23 @@ describe("generate_aw_info.cjs", () => { }); it("should not fail when model name does not contain unresolved expressions", async () => { - for (const model of ["gpt-4o", ""]) { + 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; - await main(mockCore, mockContext); + try { + await main(mockCore, mockContext); - expect(mockCore.setFailed).not.toHaveBeenCalled(); - const awInfo = JSON.parse(fs.readFileSync(awInfoPath, "utf8")); - expect(awInfo.model).toBe(model); + 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)}`); + } } });