From f9f2d71a8d895dfcefcece42ee361de7c79029ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:05:57 +0000 Subject: [PATCH 01/10] fix: normalize upload-asset allowed-exts values Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d805e60c-20aa-4a2f-86e0-c69c378498d0 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/publish_assets.go | 22 ++++++++++++++++++- pkg/workflow/publish_assets_test.go | 22 +++++++++++++++++++ pkg/workflow/upload_assets_config_test.go | 26 +++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/pkg/workflow/publish_assets.go b/pkg/workflow/publish_assets.go index e35d3b808c6..bab67a34b3b 100644 --- a/pkg/workflow/publish_assets.go +++ b/pkg/workflow/publish_assets.go @@ -12,6 +12,17 @@ import ( var publishAssetsLog = logger.New("workflow:publish_assets") +func normalizeAllowedExtension(extension string) string { + normalized := strings.ToLower(strings.TrimSpace(extension)) + if normalized == "" { + return "" + } + if !strings.HasPrefix(normalized, ".") { + normalized = "." + normalized + } + return normalized +} + // UploadAssetsConfig holds configuration for publishing assets to an orphaned git branch type UploadAssetsConfig struct { BaseSafeOutputConfig `yaml:",inline"` @@ -59,9 +70,18 @@ func (c *Compiler) parseUploadAssetConfig(outputMap map[string]any) *UploadAsset if allowedExts, exists := configMap["allowed-exts"]; exists { if allowedExtsArray, ok := allowedExts.([]any); ok { var extStrings []string + seen := make(map[string]struct{}) for _, ext := range allowedExtsArray { if extStr, ok := ext.(string); ok { - extStrings = append(extStrings, extStr) + normalized := normalizeAllowedExtension(extStr) + if normalized == "" { + continue + } + if _, exists := seen[normalized]; exists { + continue + } + seen[normalized] = struct{}{} + extStrings = append(extStrings, normalized) } } if len(extStrings) > 0 { diff --git a/pkg/workflow/publish_assets_test.go b/pkg/workflow/publish_assets_test.go index d6cd0341a91..65289f43315 100644 --- a/pkg/workflow/publish_assets_test.go +++ b/pkg/workflow/publish_assets_test.go @@ -46,6 +46,20 @@ func TestParseUploadAssetConfig(t *testing.T) { BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("5")}, }, }, + { + name: "upload-asset config normalizes allowed-exts without dots", + input: map[string]any{ + "upload-asset": map[string]any{ + "allowed-exts": []any{"png", " SVG ", ".jpg", "png"}, + }, + }, + expected: &UploadAssetsConfig{ + BranchName: "assets/${{ github.workflow }}", + MaxSizeKB: 10240, + AllowedExts: []string{".png", ".svg", ".jpg"}, + BaseSafeOutputConfig: BaseSafeOutputConfig{}, + }, + }, { name: "no upload-asset config", input: map[string]any{}, @@ -88,6 +102,14 @@ func TestParseUploadAssetConfig(t *testing.T) { if len(result.AllowedExts) != len(tt.expected.AllowedExts) { t.Errorf("AllowedExts length: expected %d, got %d", len(tt.expected.AllowedExts), len(result.AllowedExts)) } + for i := range tt.expected.AllowedExts { + if i >= len(result.AllowedExts) { + break + } + if result.AllowedExts[i] != tt.expected.AllowedExts[i] { + t.Errorf("AllowedExts[%d]: expected %s, got %s", i, tt.expected.AllowedExts[i], result.AllowedExts[i]) + } + } }) } } diff --git a/pkg/workflow/upload_assets_config_test.go b/pkg/workflow/upload_assets_config_test.go index 9cacb1b6b8c..0be8919ec1d 100644 --- a/pkg/workflow/upload_assets_config_test.go +++ b/pkg/workflow/upload_assets_config_test.go @@ -68,3 +68,29 @@ func TestUploadAssetsConfigCustomExtensions(t *testing.T) { t.Errorf("Expected custom max size 1024, got %d", config.MaxSizeKB) } } + +func TestUploadAssetsConfigNormalizesExtensions(t *testing.T) { + compiler := NewCompiler() + + outputMap := map[string]any{ + "upload-asset": map[string]any{ + "allowed-exts": []any{"png", " SVG ", ".jpg", "png"}, + }, + } + + config := compiler.parseUploadAssetConfig(outputMap) + if config == nil { + t.Fatal("Expected config to be created") + } + + expectedExts := []string{".png", ".svg", ".jpg"} + if len(config.AllowedExts) != len(expectedExts) { + t.Fatalf("Expected %d normalized extensions, got %d", len(expectedExts), len(config.AllowedExts)) + } + + for i, ext := range expectedExts { + if config.AllowedExts[i] != ext { + t.Errorf("Expected extension %s at position %d, got %s", ext, i, config.AllowedExts[i]) + } + } +} From 853113975ce6291e2fa2bda7cdf661127233eea2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:15:18 +0000 Subject: [PATCH 02/10] fix: normalize asset extension parsing in JS handlers Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3ef72008-9f47-40a0-9fda-79ba057a572b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 27 ++++++++++++++++-- .../setup/js/safe_outputs_handlers.test.cjs | 28 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 710744b1bd7..4bbc263da07 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -27,6 +27,28 @@ const { getOrGenerateTemporaryId } = require("./temporary_id.cjs"); * @returns {Object} An object containing all handler functions */ function createHandlers(server, appendSafeOutput, config = {}) { + /** + * @param {string} value + * @returns {boolean} + */ + const isGitHubExpression = value => value.startsWith("${{") && value.endsWith("}}"); + + /** + * @param {string} extValue + * @returns {string} + */ + const normalizeAllowedExtension = extValue => { + const trimmed = extValue.trim(); + if (!trimmed) { + return ""; + } + if (isGitHubExpression(trimmed)) { + return trimmed; + } + const normalized = trimmed.toLowerCase(); + return normalized.startsWith(".") ? normalized : `.${normalized}`; + }; + /** * Default handler for safe output tools * @param {string} type - The tool type @@ -128,15 +150,16 @@ function createHandlers(server, appendSafeOutput, config = {}) { // Check file extension - read from environment variable if available const ext = path.extname(filePath).toLowerCase(); const allowedExts = process.env.GH_AW_ASSETS_ALLOWED_EXTS - ? process.env.GH_AW_ASSETS_ALLOWED_EXTS.split(",").map(ext => ext.trim()) + ? process.env.GH_AW_ASSETS_ALLOWED_EXTS.split(",").map(normalizeAllowedExtension).filter(Boolean) : [ // Default set as specified in problem statement ".png", ".jpg", ".jpeg", ]; + const hasExpressionExt = allowedExts.some(isGitHubExpression); - if (!allowedExts.includes(ext)) { + if (!allowedExts.includes(ext) && !hasExpressionExt) { throw new Error(`${ERR_VALIDATION}: File extension '${ext}' is not allowed. Allowed extensions: ${allowedExts.join(", ")}`); } diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index a45b7ef1f29..0d2a2ed5815 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -265,6 +265,34 @@ describe("safe_outputs_handlers", () => { expect(result.content[0].type).toBe("text"); }); + it("should normalize custom allowed extensions", () => { + process.env.GH_AW_ASSETS_BRANCH = "test-branch"; + process.env.GH_AW_ASSETS_ALLOWED_EXTS = "TXT, md"; + + const testFile = path.join(testWorkspaceDir, "test.txt"); + fs.writeFileSync(testFile, "test content"); + + const args = { path: testFile }; + const result = handlers.uploadAssetHandler(args); + + expect(mockAppendSafeOutput).toHaveBeenCalled(); + expect(result.content[0].type).toBe("text"); + }); + + it("should allow upload when allowed extensions include GitHub expression", () => { + process.env.GH_AW_ASSETS_BRANCH = "test-branch"; + process.env.GH_AW_ASSETS_ALLOWED_EXTS = "${{ inputs.allowed_exts }}"; + + const testFile = path.join(testWorkspaceDir, "test.txt"); + fs.writeFileSync(testFile, "test content"); + + const args = { path: testFile }; + const result = handlers.uploadAssetHandler(args); + + expect(mockAppendSafeOutput).toHaveBeenCalled(); + expect(result.content[0].type).toBe("text"); + }); + it("should reject file exceeding size limit", () => { process.env.GH_AW_ASSETS_BRANCH = "test-branch"; process.env.GH_AW_ASSETS_MAX_SIZE_KB = "1"; // 1 KB limit From 1cba0d690933f4384a3f73dfad0c631657ef2e15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:18:50 +0000 Subject: [PATCH 03/10] fix: harden JS extension expression handling Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3ef72008-9f47-40a0-9fda-79ba057a572b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 9 +++++---- actions/setup/js/safe_outputs_handlers.test.cjs | 7 ++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 4bbc263da07..d7b665c2bd4 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -31,7 +31,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { * @param {string} value * @returns {boolean} */ - const isGitHubExpression = value => value.startsWith("${{") && value.endsWith("}}"); + const isGitHubExpression = value => /^\$\{\{[\s\S]*\}\}$/.test(value.trim()); /** * @param {string} extValue @@ -157,9 +157,10 @@ function createHandlers(server, appendSafeOutput, config = {}) { ".jpg", ".jpeg", ]; - const hasExpressionExt = allowedExts.some(isGitHubExpression); - - if (!allowedExts.includes(ext) && !hasExpressionExt) { + if (!allowedExts.includes(ext)) { + if (allowedExts.some(isGitHubExpression)) { + throw new Error(`${ERR_CONFIG}: GH_AW_ASSETS_ALLOWED_EXTS contains unresolved GitHub Actions expression. Ensure expressions resolve before safe outputs validation.`); + } throw new Error(`${ERR_VALIDATION}: File extension '${ext}' is not allowed. Allowed extensions: ${allowedExts.join(", ")}`); } diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 0d2a2ed5815..36817bae2e0 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -279,7 +279,7 @@ describe("safe_outputs_handlers", () => { expect(result.content[0].type).toBe("text"); }); - it("should allow upload when allowed extensions include GitHub expression", () => { + it("should reject unresolved GitHub expression in allowed extensions", () => { process.env.GH_AW_ASSETS_BRANCH = "test-branch"; process.env.GH_AW_ASSETS_ALLOWED_EXTS = "${{ inputs.allowed_exts }}"; @@ -287,10 +287,7 @@ describe("safe_outputs_handlers", () => { fs.writeFileSync(testFile, "test content"); const args = { path: testFile }; - const result = handlers.uploadAssetHandler(args); - - expect(mockAppendSafeOutput).toHaveBeenCalled(); - expect(result.content[0].type).toBe("text"); + expect(() => handlers.uploadAssetHandler(args)).toThrow("contains unresolved GitHub Actions expression"); }); it("should reject file exceeding size limit", () => { From 4c5b845c7ff8c4b2c9fb91ca43d678cb10f3c027 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:21:51 +0000 Subject: [PATCH 04/10] fix: fail safely on unresolved extension expressions Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3ef72008-9f47-40a0-9fda-79ba057a572b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 6 +++--- actions/setup/js/safe_outputs_handlers.test.cjs | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index d7b665c2bd4..8f54b698ee0 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -157,10 +157,10 @@ function createHandlers(server, appendSafeOutput, config = {}) { ".jpg", ".jpeg", ]; + if (allowedExts.some(isGitHubExpression)) { + throw new Error(`${ERR_CONFIG}: GH_AW_ASSETS_ALLOWED_EXTS contains unresolved GitHub Actions expression. Ensure expressions resolve before safe outputs validation.`); + } if (!allowedExts.includes(ext)) { - if (allowedExts.some(isGitHubExpression)) { - throw new Error(`${ERR_CONFIG}: GH_AW_ASSETS_ALLOWED_EXTS contains unresolved GitHub Actions expression. Ensure expressions resolve before safe outputs validation.`); - } throw new Error(`${ERR_VALIDATION}: File extension '${ext}' is not allowed. Allowed extensions: ${allowedExts.join(", ")}`); } diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 36817bae2e0..95ab380f7df 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -290,6 +290,17 @@ describe("safe_outputs_handlers", () => { expect(() => handlers.uploadAssetHandler(args)).toThrow("contains unresolved GitHub Actions expression"); }); + it("should reject unresolved expression even when literal extension also matches", () => { + process.env.GH_AW_ASSETS_BRANCH = "test-branch"; + process.env.GH_AW_ASSETS_ALLOWED_EXTS = ".txt,${{ inputs.allowed_exts }}"; + + const testFile = path.join(testWorkspaceDir, "test.txt"); + fs.writeFileSync(testFile, "test content"); + + const args = { path: testFile }; + expect(() => handlers.uploadAssetHandler(args)).toThrow("contains unresolved GitHub Actions expression"); + }); + it("should reject file exceeding size limit", () => { process.env.GH_AW_ASSETS_BRANCH = "test-branch"; process.env.GH_AW_ASSETS_MAX_SIZE_KB = "1"; // 1 KB limit From ae578380c96f379ee750e0f87b3354942d8cc342 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:34:23 +0000 Subject: [PATCH 05/10] fix: reject unexpanded expressions in asset ext config Agent-Logs-Url: https://github.com/github/gh-aw/sessions/77050c4d-1c90-4348-8809-a8d29c6c1090 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 8f54b698ee0..2b588675922 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -31,7 +31,10 @@ function createHandlers(server, appendSafeOutput, config = {}) { * @param {string} value * @returns {boolean} */ - const isGitHubExpression = value => /^\$\{\{[\s\S]*\}\}$/.test(value.trim()); + const isGitHubExpression = value => { + const trimmed = value.trim(); + return trimmed.startsWith("${{") && trimmed.endsWith("}}"); + }; /** * @param {string} extValue @@ -42,9 +45,6 @@ function createHandlers(server, appendSafeOutput, config = {}) { if (!trimmed) { return ""; } - if (isGitHubExpression(trimmed)) { - return trimmed; - } const normalized = trimmed.toLowerCase(); return normalized.startsWith(".") ? normalized : `.${normalized}`; }; @@ -149,6 +149,14 @@ function createHandlers(server, appendSafeOutput, config = {}) { // Check file extension - read from environment variable if available const ext = path.extname(filePath).toLowerCase(); + if ( + process.env.GH_AW_ASSETS_ALLOWED_EXTS && + process.env.GH_AW_ASSETS_ALLOWED_EXTS.split(",") + .map(extValue => extValue.trim()) + .some(isGitHubExpression) + ) { + throw new Error(`${ERR_CONFIG}: GH_AW_ASSETS_ALLOWED_EXTS contains unresolved GitHub Actions expression. Ensure expressions resolve before safe outputs validation.`); + } const allowedExts = process.env.GH_AW_ASSETS_ALLOWED_EXTS ? process.env.GH_AW_ASSETS_ALLOWED_EXTS.split(",").map(normalizeAllowedExtension).filter(Boolean) : [ @@ -157,9 +165,6 @@ function createHandlers(server, appendSafeOutput, config = {}) { ".jpg", ".jpeg", ]; - if (allowedExts.some(isGitHubExpression)) { - throw new Error(`${ERR_CONFIG}: GH_AW_ASSETS_ALLOWED_EXTS contains unresolved GitHub Actions expression. Ensure expressions resolve before safe outputs validation.`); - } if (!allowedExts.includes(ext)) { throw new Error(`${ERR_VALIDATION}: File extension '${ext}' is not allowed. Allowed extensions: ${allowedExts.join(", ")}`); } From 4a004f3b3b2f0393c0a6a0fff40acf6289b3f365 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:36:39 +0000 Subject: [PATCH 06/10] refactor: avoid duplicate env parsing for extension checks Agent-Logs-Url: https://github.com/github/gh-aw/sessions/77050c4d-1c90-4348-8809-a8d29c6c1090 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 2b588675922..e407575a01f 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -149,16 +149,12 @@ function createHandlers(server, appendSafeOutput, config = {}) { // Check file extension - read from environment variable if available const ext = path.extname(filePath).toLowerCase(); - if ( - process.env.GH_AW_ASSETS_ALLOWED_EXTS && - process.env.GH_AW_ASSETS_ALLOWED_EXTS.split(",") - .map(extValue => extValue.trim()) - .some(isGitHubExpression) - ) { + const rawAllowedExtValues = process.env.GH_AW_ASSETS_ALLOWED_EXTS ? process.env.GH_AW_ASSETS_ALLOWED_EXTS.split(",").map(extValue => extValue.trim()) : null; + if (rawAllowedExtValues && rawAllowedExtValues.some(isGitHubExpression)) { throw new Error(`${ERR_CONFIG}: GH_AW_ASSETS_ALLOWED_EXTS contains unresolved GitHub Actions expression. Ensure expressions resolve before safe outputs validation.`); } - const allowedExts = process.env.GH_AW_ASSETS_ALLOWED_EXTS - ? process.env.GH_AW_ASSETS_ALLOWED_EXTS.split(",").map(normalizeAllowedExtension).filter(Boolean) + const allowedExts = rawAllowedExtValues + ? rawAllowedExtValues.map(normalizeAllowedExtension).filter(Boolean) : [ // Default set as specified in problem statement ".png", From 13dc34f101a6d0bdba2d0ef69d9696d808dffd81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:40:08 +0000 Subject: [PATCH 07/10] fix: tighten expression detection in allowed ext parsing Agent-Logs-Url: https://github.com/github/gh-aw/sessions/77050c4d-1c90-4348-8809-a8d29c6c1090 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index e407575a01f..fcd17db0670 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -33,7 +33,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { */ const isGitHubExpression = value => { const trimmed = value.trim(); - return trimmed.startsWith("${{") && trimmed.endsWith("}}"); + return /^\$\{\{[\s\S]*\}\}$/.test(trimmed); }; /** From cfc57d494b0f5942e1d2e09c3107004175a77254 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:55:36 +0000 Subject: [PATCH 08/10] refactor: share extension parsing helpers and preserve expressions Agent-Logs-Url: https://github.com/github/gh-aw/sessions/62a39b21-d83d-41b7-8ee0-77c94be0e2e3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/allowed_extensions_helpers.cjs | 46 +++++++++++++++++++ .../js/allowed_extensions_helpers.test.cjs | 46 +++++++++++++++++++ actions/setup/js/safe_outputs_handlers.cjs | 31 ++----------- pkg/workflow/publish_assets.go | 15 +++++- pkg/workflow/publish_assets_test.go | 14 ++++++ 5 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 actions/setup/js/allowed_extensions_helpers.cjs create mode 100644 actions/setup/js/allowed_extensions_helpers.test.cjs diff --git a/actions/setup/js/allowed_extensions_helpers.cjs b/actions/setup/js/allowed_extensions_helpers.cjs new file mode 100644 index 00000000000..2f970f2f690 --- /dev/null +++ b/actions/setup/js/allowed_extensions_helpers.cjs @@ -0,0 +1,46 @@ +// @ts-check + +/** + * @param {string} value + * @returns {boolean} + */ +function isGitHubExpression(value) { + const trimmed = value.trim(); + return /^\$\{\{[\s\S]*\}\}$/.test(trimmed); +} + +/** + * @param {string} extValue + * @returns {string} + */ +function normalizeAllowedExtension(extValue) { + const trimmed = extValue.trim(); + if (!trimmed) { + return ""; + } + const normalized = trimmed.toLowerCase(); + return normalized.startsWith(".") ? normalized : `.${normalized}`; +} + +/** + * @param {string | undefined} envValue + * @returns {{rawValues: string[], normalizedValues: string[], hasUnresolvedExpression: boolean} | null} + */ +function parseAllowedExtensionsEnv(envValue) { + if (!envValue) { + return null; + } + + const rawValues = envValue.split(",").map(extValue => extValue.trim()); + return { + rawValues, + normalizedValues: rawValues.map(normalizeAllowedExtension).filter(Boolean), + hasUnresolvedExpression: rawValues.some(isGitHubExpression), + }; +} + +module.exports = { + isGitHubExpression, + normalizeAllowedExtension, + parseAllowedExtensionsEnv, +}; diff --git a/actions/setup/js/allowed_extensions_helpers.test.cjs b/actions/setup/js/allowed_extensions_helpers.test.cjs new file mode 100644 index 00000000000..aa1203b0030 --- /dev/null +++ b/actions/setup/js/allowed_extensions_helpers.test.cjs @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { isGitHubExpression, normalizeAllowedExtension, parseAllowedExtensionsEnv } from "./allowed_extensions_helpers.cjs"; + +describe("allowed_extensions_helpers", () => { + describe("isGitHubExpression", () => { + it("returns true for full GitHub Actions expression", () => { + expect(isGitHubExpression("${{ inputs.allowed_exts }}")).toBe(true); + }); + + it("returns false for non-expression text", () => { + expect(isGitHubExpression("prefix ${{ inputs.allowed_exts }}")).toBe(false); + }); + }); + + describe("normalizeAllowedExtension", () => { + it("normalizes case, trims spaces, and adds missing dot", () => { + expect(normalizeAllowedExtension(" PNG ")).toBe(".png"); + }); + + it("returns empty string for blank input", () => { + expect(normalizeAllowedExtension(" ")).toBe(""); + }); + }); + + describe("parseAllowedExtensionsEnv", () => { + it("returns null when env value is undefined", () => { + expect(parseAllowedExtensionsEnv(undefined)).toBeNull(); + }); + + it("parses and normalizes literal extension values", () => { + expect(parseAllowedExtensionsEnv("TXT, md")).toEqual({ + rawValues: ["TXT", "md"], + normalizedValues: [".txt", ".md"], + hasUnresolvedExpression: false, + }); + }); + + it("detects unresolved GitHub Actions expressions", () => { + expect(parseAllowedExtensionsEnv(".txt,${{ inputs.allowed_exts }}")).toEqual({ + rawValues: [".txt", "${{ inputs.allowed_exts }}"], + normalizedValues: [".txt", ".${{ inputs.allowed_exts }}"], + hasUnresolvedExpression: true, + }); + }); + }); +}); diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index fcd17db0670..032909d62c9 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -18,6 +18,7 @@ const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); const { findRepoCheckout } = require("./find_repo_checkout.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { getOrGenerateTemporaryId } = require("./temporary_id.cjs"); +const { parseAllowedExtensionsEnv } = require("./allowed_extensions_helpers.cjs"); /** * Create handlers for safe output tools @@ -27,28 +28,6 @@ const { getOrGenerateTemporaryId } = require("./temporary_id.cjs"); * @returns {Object} An object containing all handler functions */ function createHandlers(server, appendSafeOutput, config = {}) { - /** - * @param {string} value - * @returns {boolean} - */ - const isGitHubExpression = value => { - const trimmed = value.trim(); - return /^\$\{\{[\s\S]*\}\}$/.test(trimmed); - }; - - /** - * @param {string} extValue - * @returns {string} - */ - const normalizeAllowedExtension = extValue => { - const trimmed = extValue.trim(); - if (!trimmed) { - return ""; - } - const normalized = trimmed.toLowerCase(); - return normalized.startsWith(".") ? normalized : `.${normalized}`; - }; - /** * Default handler for safe output tools * @param {string} type - The tool type @@ -149,12 +128,12 @@ function createHandlers(server, appendSafeOutput, config = {}) { // Check file extension - read from environment variable if available const ext = path.extname(filePath).toLowerCase(); - const rawAllowedExtValues = process.env.GH_AW_ASSETS_ALLOWED_EXTS ? process.env.GH_AW_ASSETS_ALLOWED_EXTS.split(",").map(extValue => extValue.trim()) : null; - if (rawAllowedExtValues && rawAllowedExtValues.some(isGitHubExpression)) { + const parsedAllowedExts = parseAllowedExtensionsEnv(process.env.GH_AW_ASSETS_ALLOWED_EXTS); + if (parsedAllowedExts?.hasUnresolvedExpression) { throw new Error(`${ERR_CONFIG}: GH_AW_ASSETS_ALLOWED_EXTS contains unresolved GitHub Actions expression. Ensure expressions resolve before safe outputs validation.`); } - const allowedExts = rawAllowedExtValues - ? rawAllowedExtValues.map(normalizeAllowedExtension).filter(Boolean) + const allowedExts = parsedAllowedExts + ? parsedAllowedExts.normalizedValues : [ // Default set as specified in problem statement ".png", diff --git a/pkg/workflow/publish_assets.go b/pkg/workflow/publish_assets.go index bab67a34b3b..26c6f9fd421 100644 --- a/pkg/workflow/publish_assets.go +++ b/pkg/workflow/publish_assets.go @@ -3,6 +3,7 @@ package workflow import ( "errors" "fmt" + "regexp" "strings" "github.com/github/gh-aw/pkg/constants" @@ -11,12 +12,22 @@ import ( ) var publishAssetsLog = logger.New("workflow:publish_assets") +var githubExpressionPattern = regexp.MustCompile(`(?s)^\$\{\{.*\}\}$`) + +func isGitHubExpression(value string) bool { + trimmed := strings.TrimSpace(value) + return githubExpressionPattern.MatchString(trimmed) +} func normalizeAllowedExtension(extension string) string { - normalized := strings.ToLower(strings.TrimSpace(extension)) - if normalized == "" { + trimmed := strings.TrimSpace(extension) + if trimmed == "" { return "" } + if isGitHubExpression(trimmed) { + return trimmed + } + normalized := strings.ToLower(trimmed) if !strings.HasPrefix(normalized, ".") { normalized = "." + normalized } diff --git a/pkg/workflow/publish_assets_test.go b/pkg/workflow/publish_assets_test.go index 65289f43315..80550bd3d0b 100644 --- a/pkg/workflow/publish_assets_test.go +++ b/pkg/workflow/publish_assets_test.go @@ -60,6 +60,20 @@ func TestParseUploadAssetConfig(t *testing.T) { BaseSafeOutputConfig: BaseSafeOutputConfig{}, }, }, + { + name: "upload-asset config preserves github actions expressions in allowed-exts", + input: map[string]any{ + "upload-asset": map[string]any{ + "allowed-exts": []any{"${{ inputs.allowed_exts }}", " PNG "}, + }, + }, + expected: &UploadAssetsConfig{ + BranchName: "assets/${{ github.workflow }}", + MaxSizeKB: 10240, + AllowedExts: []string{"${{ inputs.allowed_exts }}", ".png"}, + BaseSafeOutputConfig: BaseSafeOutputConfig{}, + }, + }, { name: "no upload-asset config", input: map[string]any{}, From 062f1edfbeee9a7206c82a8976815dd139ea9620 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:58:54 +0000 Subject: [PATCH 09/10] fix: preserve expressions in js extension normalization Agent-Logs-Url: https://github.com/github/gh-aw/sessions/62a39b21-d83d-41b7-8ee0-77c94be0e2e3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/allowed_extensions_helpers.cjs | 3 +++ actions/setup/js/allowed_extensions_helpers.test.cjs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/allowed_extensions_helpers.cjs b/actions/setup/js/allowed_extensions_helpers.cjs index 2f970f2f690..c721860fce8 100644 --- a/actions/setup/js/allowed_extensions_helpers.cjs +++ b/actions/setup/js/allowed_extensions_helpers.cjs @@ -18,6 +18,9 @@ function normalizeAllowedExtension(extValue) { if (!trimmed) { return ""; } + if (isGitHubExpression(trimmed)) { + return trimmed; + } const normalized = trimmed.toLowerCase(); return normalized.startsWith(".") ? normalized : `.${normalized}`; } diff --git a/actions/setup/js/allowed_extensions_helpers.test.cjs b/actions/setup/js/allowed_extensions_helpers.test.cjs index aa1203b0030..28fb1c016c5 100644 --- a/actions/setup/js/allowed_extensions_helpers.test.cjs +++ b/actions/setup/js/allowed_extensions_helpers.test.cjs @@ -38,7 +38,7 @@ describe("allowed_extensions_helpers", () => { it("detects unresolved GitHub Actions expressions", () => { expect(parseAllowedExtensionsEnv(".txt,${{ inputs.allowed_exts }}")).toEqual({ rawValues: [".txt", "${{ inputs.allowed_exts }}"], - normalizedValues: [".txt", ".${{ inputs.allowed_exts }}"], + normalizedValues: [".txt", "${{ inputs.allowed_exts }}"], hasUnresolvedExpression: true, }); }); From fb416b586f179a0a94917dfe16049d55ec5d9c34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:16:13 +0000 Subject: [PATCH 10/10] fix: copy allowed extension helper in setup script Agent-Logs-Url: https://github.com/github/gh-aw/sessions/80136222-62f9-48b4-9c3a-2409a9dc67d2 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/setup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index 8a28dc977b6..30082b5a01f 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -280,6 +280,7 @@ SAFE_OUTPUTS_FILES=( "safe_outputs_tools_loader.cjs" "safe_outputs_config.cjs" "safe_outputs_handlers.cjs" + "allowed_extensions_helpers.cjs" "safe_outputs_append.cjs" "mcp_server_core.cjs" "mcp_logger.cjs"