From ebc7b3306b675141570e9c6c211b1a4de83389cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:38:59 +0000 Subject: [PATCH 1/7] Initial plan From 655d44325a794c8c61b94aeb958abfdb404f7aef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:48:17 +0000 Subject: [PATCH 2/7] Fix runtime-imports path resolution to use workflows/ prefix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/runtime_import.cjs | 4 + actions/setup/js/runtime_import.test.cjs | 117 ++++++++++++----------- 2 files changed, 67 insertions(+), 54 deletions(-) diff --git a/actions/setup/js/runtime_import.cjs b/actions/setup/js/runtime_import.cjs index ec31047b8c..f367573dc7 100644 --- a/actions/setup/js/runtime_import.cjs +++ b/actions/setup/js/runtime_import.cjs @@ -694,6 +694,10 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start filepath = filepath.substring(8); // Remove ".github/" } else if (filepath.startsWith(".github\\")) { filepath = filepath.substring(8); // Remove ".github\" (Windows) + } else if (!filepath.startsWith(".actions/") && !filepath.startsWith(".actions\\")) { + // If path doesn't start with .github or .actions, prefix with workflows/ + // This makes imports like "a.md" resolve to ".github/workflows/a.md" + filepath = path.join("workflows", filepath); } // Remove leading ./ or ../ if present diff --git a/actions/setup/js/runtime_import.test.cjs b/actions/setup/js/runtime_import.test.cjs index fbc91e1af2..52a8b3c74f 100644 --- a/actions/setup/js/runtime_import.test.cjs +++ b/actions/setup/js/runtime_import.test.cjs @@ -8,8 +8,9 @@ const { processRuntimeImports, processRuntimeImport, hasFrontMatter, removeXMLCo describe("runtime_import", () => { let tempDir; let githubDir; + let workflowsDir; (beforeEach(() => { - ((tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "runtime-import-test-"))), (githubDir = path.join(tempDir, ".github")), fs.mkdirSync(githubDir, { recursive: true }), vi.clearAllMocks()); + ((tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "runtime-import-test-"))), (githubDir = path.join(tempDir, ".github")), (workflowsDir = path.join(githubDir, "workflows")), fs.mkdirSync(workflowsDir, { recursive: true }), vi.clearAllMocks()); }), afterEach(() => { tempDir && fs.existsSync(tempDir) && fs.rmSync(tempDir, { recursive: !0, force: !0 }); @@ -293,7 +294,7 @@ describe("runtime_import", () => { describe("processRuntimeImport", () => { (it("should read and return file content", async () => { const content = "# Test Content\n\nThis is a test."; - fs.writeFileSync(path.join(githubDir, "test.md"), content); + fs.writeFileSync(path.join(workflowsDir, "test.md"), content); const result = await processRuntimeImport("test.md", !1, tempDir); expect(result).toBe(content); }), @@ -306,7 +307,7 @@ describe("runtime_import", () => { }), it("should remove front matter and warn", async () => { const filepath = "with-frontmatter.md"; - fs.writeFileSync(path.join(githubDir, filepath), "---\ntitle: Test\nkey: value\n---\n\n# Content\n\nActual content."); + fs.writeFileSync(path.join(workflowsDir, filepath), "---\ntitle: Test\nkey: value\n---\n\n# Content\n\nActual content."); const result = await processRuntimeImport(filepath, !1, tempDir); (expect(result).toContain("# Content"), expect(result).toContain("Actual content."), @@ -314,7 +315,7 @@ describe("runtime_import", () => { expect(core.warning).toHaveBeenCalledWith(`File ${filepath} contains front matter which will be ignored in runtime import`)); }), it("should remove XML comments", async () => { - fs.writeFileSync(path.join(githubDir, "with-comments.md"), "# Title\n\n\x3c!-- This is a comment --\x3e\n\nContent here."); + fs.writeFileSync(path.join(workflowsDir, "with-comments.md"), "# Title\n\n\x3c!-- This is a comment --\x3e\n\nContent here."); const result = await processRuntimeImport("with-comments.md", !1, tempDir); (expect(result).toContain("# Title"), expect(result).toContain("Content here."), expect(result).not.toContain("\x3c!-- This is a comment --\x3e")); }), @@ -329,50 +330,58 @@ describe("runtime_import", () => { workflow: "test-workflow", payload: {}, }; - fs.writeFileSync(path.join(githubDir, "with-macros.md"), "# Title\n\nActor: ${{ github.actor }}\n"); + fs.writeFileSync(path.join(workflowsDir, "with-macros.md"), "# Title\n\nActor: ${{ github.actor }}\n"); const result = await processRuntimeImport("with-macros.md", !1, tempDir); expect(result).toContain("# Title"); expect(result).toContain("Actor: testuser"); delete global.context; }), it("should reject unsafe GitHub Actions expressions", async () => { - fs.writeFileSync(path.join(githubDir, "unsafe-macros.md"), "Secret: ${{ secrets.TOKEN }}\n"); + fs.writeFileSync(path.join(workflowsDir, "unsafe-macros.md"), "Secret: ${{ secrets.TOKEN }}\n"); await expect(processRuntimeImport("unsafe-macros.md", !1, tempDir)).rejects.toThrow("unauthorized GitHub Actions expressions"); }), it("should handle file in subdirectory", async () => { - const subdir = path.join(githubDir, "subdir"); - (fs.mkdirSync(subdir), fs.writeFileSync(path.join(githubDir, "subdir/test.md"), "Subdirectory content")); + const subdir = path.join(workflowsDir, "subdir"); + (fs.mkdirSync(subdir), fs.writeFileSync(path.join(workflowsDir, "subdir/test.md"), "Subdirectory content")); const result = await processRuntimeImport("subdir/test.md", !1, tempDir); expect(result).toBe("Subdirectory content"); }), it("should handle empty file", async () => { - fs.writeFileSync(path.join(githubDir, "empty.md"), ""); + fs.writeFileSync(path.join(workflowsDir, "empty.md"), ""); const result = await processRuntimeImport("empty.md", !1, tempDir); expect(result).toBe(""); }), it("should handle file with only front matter", async () => { - fs.writeFileSync(path.join(githubDir, "only-frontmatter.md"), "---\ntitle: Test\n---\n"); + fs.writeFileSync(path.join(workflowsDir, "only-frontmatter.md"), "---\ntitle: Test\n---\n"); const result = await processRuntimeImport("only-frontmatter.md", !1, tempDir); expect(result.trim()).toBe(""); }), it("should allow template conditionals", async () => { const content = "{{#if condition}}content{{/if}}"; - fs.writeFileSync(path.join(githubDir, "with-conditionals.md"), content); + fs.writeFileSync(path.join(workflowsDir, "with-conditionals.md"), content); const result = await processRuntimeImport("with-conditionals.md", !1, tempDir); expect(result).toBe(content); }), - it("should support .github/ prefix in path", async () => { - const content = "Test with .github prefix"; - fs.writeFileSync(path.join(githubDir, "test-prefix.md"), content); - const result = await processRuntimeImport(".github/test-prefix.md", !1, tempDir); + it("should support .github/workflows/ prefix in path", async () => { + const content = "Test with .github/workflows prefix"; + fs.writeFileSync(path.join(workflowsDir, "test-prefix.md"), content); + const result = await processRuntimeImport(".github/workflows/test-prefix.md", !1, tempDir); expect(result).toBe(content); }), - it("should work without .github/ prefix", async () => { + it("should work without prefix (resolves to workflows/)", async () => { const content = "Test without prefix"; - fs.writeFileSync(path.join(githubDir, "test-no-prefix.md"), content); + fs.writeFileSync(path.join(workflowsDir, "test-no-prefix.md"), content); const result = await processRuntimeImport("test-no-prefix.md", !1, tempDir); expect(result).toBe(content); }), + it("should support .actions/ prefix", async () => { + const actionsDir = path.join(githubDir, "actions"); + fs.mkdirSync(actionsDir, { recursive: true }); + const content = "Test with .actions prefix"; + fs.writeFileSync(path.join(actionsDir, "test-action.md"), content); + const result = await processRuntimeImport(".actions/test-action.md", !1, tempDir); + expect(result).toBe(content); + }), it("should reject paths outside .github folder", async () => { // Try to access a file in the root (not in .github) fs.writeFileSync(path.join(tempDir, "outside.md"), "Outside content"); @@ -381,17 +390,17 @@ describe("runtime_import", () => { }), describe("processRuntimeImports", () => { (it("should process single runtime-import macro", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "Imported content"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "Imported content"); const result = await processRuntimeImports("Before\n{{#runtime-import import.md}}\nAfter", tempDir); expect(result).toBe("Before\nImported content\nAfter"); }), it("should process optional runtime-import macro", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "Imported content"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "Imported content"); const result = await processRuntimeImports("Before\n{{#runtime-import? import.md}}\nAfter", tempDir); expect(result).toBe("Before\nImported content\nAfter"); }), it("should process multiple runtime-import macros", async () => { - (fs.writeFileSync(path.join(githubDir, "import1.md"), "Content 1"), fs.writeFileSync(path.join(githubDir, "import2.md"), "Content 2")); + (fs.writeFileSync(path.join(workflowsDir, "import1.md"), "Content 1"), fs.writeFileSync(path.join(workflowsDir, "import2.md"), "Content 2")); const result = await processRuntimeImports("{{#runtime-import import1.md}}\nMiddle\n{{#runtime-import import2.md}}", tempDir); expect(result).toBe("Content 1\nMiddle\nContent 2"); }), @@ -407,59 +416,59 @@ describe("runtime_import", () => { expect(result).toBe("No imports here"); }), it("should reuse cached content for duplicate imports", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "Content"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "Content"); const result = await processRuntimeImports("{{#runtime-import import.md}}\n{{#runtime-import import.md}}", tempDir); expect(result).toBe("Content\nContent"); expect(core.info).toHaveBeenCalledWith("Reusing cached content for import.md"); }), it("should handle macros with extra whitespace", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "Content"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "Content"); const result = await processRuntimeImports("{{#runtime-import import.md }}", tempDir); expect(result).toBe("Content"); }), it("should handle inline macros", async () => { - fs.writeFileSync(path.join(githubDir, "inline.md"), "inline content"); + fs.writeFileSync(path.join(workflowsDir, "inline.md"), "inline content"); const result = await processRuntimeImports("Before {{#runtime-import inline.md}} after", tempDir); expect(result).toBe("Before inline content after"); }), it("should process imports with files containing special characters", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "Content with $pecial ch@racters!"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "Content with $pecial ch@racters!"); const result = await processRuntimeImports("{{#runtime-import import.md}}", tempDir); expect(result).toBe("Content with $pecial ch@racters!"); }), it("should remove XML comments from imported content", async () => { - fs.writeFileSync(path.join(githubDir, "with-comment.md"), "Text \x3c!-- comment --\x3e more text"); + fs.writeFileSync(path.join(workflowsDir, "with-comment.md"), "Text \x3c!-- comment --\x3e more text"); const result = await processRuntimeImports("{{#runtime-import with-comment.md}}", tempDir); expect(result).toBe("Text more text"); }), it("should handle path with subdirectories", async () => { - const subdir = path.join(githubDir, "docs", "shared"); - (fs.mkdirSync(subdir, { recursive: !0 }), fs.writeFileSync(path.join(githubDir, "docs/shared/import.md"), "Subdir content")); + const subdir = path.join(workflowsDir, "docs", "shared"); + (fs.mkdirSync(subdir, { recursive: !0 }), fs.writeFileSync(path.join(workflowsDir, "docs/shared/import.md"), "Subdir content")); const result = await processRuntimeImports("{{#runtime-import docs/shared/import.md}}", tempDir); expect(result).toBe("Subdir content"); }), it("should preserve newlines around imports", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "Content"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "Content"); const result = await processRuntimeImports("Line 1\n\n{{#runtime-import import.md}}\n\nLine 2", tempDir); expect(result).toBe("Line 1\n\nContent\n\nLine 2"); }), it("should handle multiple consecutive imports", async () => { - (fs.writeFileSync(path.join(githubDir, "import1.md"), "Content 1"), fs.writeFileSync(path.join(githubDir, "import2.md"), "Content 2")); + (fs.writeFileSync(path.join(workflowsDir, "import1.md"), "Content 1"), fs.writeFileSync(path.join(workflowsDir, "import2.md"), "Content 2")); const result = await processRuntimeImports("{{#runtime-import import1.md}}{{#runtime-import import2.md}}", tempDir); expect(result).toBe("Content 1Content 2"); }), it("should handle imports at the start of content", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "Start content"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "Start content"); const result = await processRuntimeImports("{{#runtime-import import.md}}\nFollowing text", tempDir); expect(result).toBe("Start content\nFollowing text"); }), it("should handle imports at the end of content", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "End content"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "End content"); const result = await processRuntimeImports("Preceding text\n{{#runtime-import import.md}}", tempDir); expect(result).toBe("Preceding text\nEnd content"); }), it("should handle tab characters in macro", async () => { - fs.writeFileSync(path.join(githubDir, "import.md"), "Content"); + fs.writeFileSync(path.join(workflowsDir, "import.md"), "Content"); const result = await processRuntimeImports("{{#runtime-import\timport.md}}", tempDir); expect(result).toBe("Content"); })); @@ -467,18 +476,18 @@ describe("runtime_import", () => { describe("Edge Cases", () => { (it("should handle very large files", async () => { const largeContent = "x".repeat(1e5); - fs.writeFileSync(path.join(githubDir, "large.md"), largeContent); + fs.writeFileSync(path.join(workflowsDir, "large.md"), largeContent); const result = await processRuntimeImports("{{#runtime-import large.md}}", tempDir); expect(result).toBe(largeContent); }), it("should handle files with unicode characters", async () => { - fs.writeFileSync(path.join(githubDir, "unicode.md"), "Hello 世界 🌍 café", "utf8"); + fs.writeFileSync(path.join(workflowsDir, "unicode.md"), "Hello 世界 🌍 café", "utf8"); const result = await processRuntimeImports("{{#runtime-import unicode.md}}", tempDir); expect(result).toBe("Hello 世界 🌍 café"); }), it("should handle files with various line endings", async () => { const content = "Line 1\nLine 2\r\nLine 3\rLine 4"; - fs.writeFileSync(path.join(githubDir, "mixed-lines.md"), content); + fs.writeFileSync(path.join(workflowsDir, "mixed-lines.md"), content); const result = await processRuntimeImports("{{#runtime-import mixed-lines.md}}", tempDir); expect(result).toBe(content); }), @@ -488,14 +497,14 @@ describe("runtime_import", () => { expect(result).toBe(content); }), it("should handle front matter with varying formats", async () => { - fs.writeFileSync(path.join(githubDir, "yaml-frontmatter.md"), "---\ntitle: Test\narray:\n - item1\n - item2\n---\n\nBody content"); + fs.writeFileSync(path.join(workflowsDir, "yaml-frontmatter.md"), "---\ntitle: Test\narray:\n - item1\n - item2\n---\n\nBody content"); const result = await processRuntimeImport("yaml-frontmatter.md", !1, tempDir); (expect(result).toContain("Body content"), expect(result).not.toContain("array:"), expect(result).not.toContain("item1")); })); }), describe("Error Handling", () => { (it("should provide clear error for unsafe GitHub Actions expressions", async () => { - fs.writeFileSync(path.join(githubDir, "bad.md"), "${{ secrets.TOKEN }}"); + fs.writeFileSync(path.join(workflowsDir, "bad.md"), "${{ secrets.TOKEN }}"); await expect(processRuntimeImports("{{#runtime-import bad.md}}", tempDir)).rejects.toThrow("unauthorized GitHub Actions expressions"); }), it("should provide clear error for missing required files", async () => { @@ -512,8 +521,8 @@ describe("runtime_import", () => { await expect(processRuntimeImport("../../etc/passwd", !1, tempDir)).rejects.toThrow("Security: Path ../../etc/passwd must be within .github folder"); }), it("should allow valid path within .github folder", async () => { - // Create a subdirectory structure within .github - const subdir = path.join(githubDir, "subdir"); + // Create a subdirectory structure within workflows + const subdir = path.join(workflowsDir, "subdir"); fs.mkdirSync(subdir, { recursive: !0 }); fs.writeFileSync(path.join(subdir, "subfile.txt"), "Sub content"); @@ -521,19 +530,19 @@ describe("runtime_import", () => { const result = await processRuntimeImport("subdir/subfile.txt", !1, tempDir); expect(result).toBe("Sub content"); }), - it("should allow ./path within .github folder", async () => { - fs.writeFileSync(path.join(githubDir, "test.txt"), "Test content"); + it("should allow ./path within workflows folder", async () => { + fs.writeFileSync(path.join(workflowsDir, "test.txt"), "Test content"); const result = await processRuntimeImport("./test.txt", !1, tempDir); expect(result).toBe("Test content"); }), it("should normalize paths with redundant separators", async () => { - fs.writeFileSync(path.join(githubDir, "test.txt"), "Test content"); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), "Test content"); const result = await processRuntimeImport("./././test.txt", !1, tempDir); expect(result).toBe("Test content"); }), - it("should allow nested paths that stay within .github folder", async () => { - // Create nested directory structure within .github - const dirA = path.join(githubDir, "a"); + it("should allow nested paths that stay within workflows folder", async () => { + // Create nested directory structure within workflows + const dirA = path.join(workflowsDir, "a"); const dirB = path.join(dirA, "b"); fs.mkdirSync(dirB, { recursive: !0 }); fs.writeFileSync(path.join(dirB, "file.txt"), "Nested content"); @@ -552,37 +561,37 @@ describe("runtime_import", () => { describe("processRuntimeImport with line ranges", () => { (it("should extract specific line range", async () => { const content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"; - fs.writeFileSync(path.join(githubDir, "test.txt"), content); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), content); const result = await processRuntimeImport("test.txt", !1, tempDir, 2, 4); expect(result).toBe("Line 2\nLine 3\nLine 4"); }), it("should extract single line", async () => { const content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"; - fs.writeFileSync(path.join(githubDir, "test.txt"), content); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), content); const result = await processRuntimeImport("test.txt", !1, tempDir, 3, 3); expect(result).toBe("Line 3"); }), it("should extract from start line to end of file", async () => { const content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"; - fs.writeFileSync(path.join(githubDir, "test.txt"), content); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), content); const result = await processRuntimeImport("test.txt", !1, tempDir, 3, 5); expect(result).toBe("Line 3\nLine 4\nLine 5"); }), it("should throw error for invalid start line", async () => { const content = "Line 1\nLine 2\nLine 3"; - fs.writeFileSync(path.join(githubDir, "test.txt"), content); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), content); await expect(processRuntimeImport("test.txt", !1, tempDir, 0, 2)).rejects.toThrow("Invalid start line 0"); await expect(processRuntimeImport("test.txt", !1, tempDir, 10, 12)).rejects.toThrow("Invalid start line 10"); }), it("should throw error for invalid end line", async () => { const content = "Line 1\nLine 2\nLine 3"; - fs.writeFileSync(path.join(githubDir, "test.txt"), content); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), content); await expect(processRuntimeImport("test.txt", !1, tempDir, 1, 0)).rejects.toThrow("Invalid end line 0"); await expect(processRuntimeImport("test.txt", !1, tempDir, 1, 10)).rejects.toThrow("Invalid end line 10"); }), it("should throw error when start line > end line", async () => { const content = "Line 1\nLine 2\nLine 3"; - fs.writeFileSync(path.join(githubDir, "test.txt"), content); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), content); await expect(processRuntimeImport("test.txt", !1, tempDir, 3, 1)).rejects.toThrow("Start line 3 cannot be greater than end line 1"); }), it("should handle line range with front matter", async () => { @@ -592,7 +601,7 @@ describe("runtime_import", () => { // Line 3: --- // Line 4: (empty) // Line 5: Line 1 - fs.writeFileSync(path.join(githubDir, filepath), "---\ntitle: Test\n---\n\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5"); + fs.writeFileSync(path.join(workflowsDir, filepath), "---\ntitle: Test\n---\n\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5"); const result = await processRuntimeImport(filepath, !1, tempDir, 2, 4); // Lines 2-4 of raw file are: "title: Test", "---", "" // After front matter removal, these lines are part of front matter so they get removed @@ -602,12 +611,12 @@ describe("runtime_import", () => { }), describe("processRuntimeImports with line ranges from macros", () => { (it("should process {{#runtime-import path:line-line}} macro", async () => { - fs.writeFileSync(path.join(githubDir, "test.txt"), "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"); const result = await processRuntimeImports("Content: {{#runtime-import test.txt:2-4}} end", tempDir); expect(result).toBe("Content: Line 2\nLine 3\nLine 4 end"); }), it("should process multiple {{#runtime-import path:line-line}} macros", async () => { - fs.writeFileSync(path.join(githubDir, "test.txt"), "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"); + fs.writeFileSync(path.join(workflowsDir, "test.txt"), "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"); const result = await processRuntimeImports("First: {{#runtime-import test.txt:1-2}} Second: {{#runtime-import test.txt:4-5}}", tempDir); expect(result).toBe("First: Line 1\nLine 2 Second: Line 4\nLine 5"); })); From b3a12d9fefa0932c5ed713fbec812704ac6d2d54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:50:36 +0000 Subject: [PATCH 3/7] Fix all runtime_import tests - update expected messages and fix .actions/ handling Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/runtime_import.cjs | 8 +- actions/setup/js/runtime_import.test.cjs | 273 ++++++++++++----------- 2 files changed, 144 insertions(+), 137 deletions(-) diff --git a/actions/setup/js/runtime_import.cjs b/actions/setup/js/runtime_import.cjs index f367573dc7..411508f927 100644 --- a/actions/setup/js/runtime_import.cjs +++ b/actions/setup/js/runtime_import.cjs @@ -694,7 +694,13 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start filepath = filepath.substring(8); // Remove ".github/" } else if (filepath.startsWith(".github\\")) { filepath = filepath.substring(8); // Remove ".github\" (Windows) - } else if (!filepath.startsWith(".actions/") && !filepath.startsWith(".actions\\")) { + } else if (filepath.startsWith(".actions/")) { + // Remove the leading dot from .actions/ -> actions/ + filepath = filepath.substring(1); // Remove the leading "." + } else if (filepath.startsWith(".actions\\")) { + // Remove the leading dot from .actions\ -> actions\ (Windows) + filepath = filepath.substring(1); // Remove the leading "." + } else { // If path doesn't start with .github or .actions, prefix with workflows/ // This makes imports like "a.md" resolve to ".github/workflows/a.md" filepath = path.join("workflows", filepath); diff --git a/actions/setup/js/runtime_import.test.cjs b/actions/setup/js/runtime_import.test.cjs index 52a8b3c74f..fdb8fb39da 100644 --- a/actions/setup/js/runtime_import.test.cjs +++ b/actions/setup/js/runtime_import.test.cjs @@ -299,11 +299,11 @@ describe("runtime_import", () => { expect(result).toBe(content); }), it("should throw error for missing required file", async () => { - await expect(processRuntimeImport("missing.md", !1, tempDir)).rejects.toThrow("Runtime import file not found: missing.md"); + await expect(processRuntimeImport("missing.md", !1, tempDir)).rejects.toThrow("Runtime import file not found: workflows/missing.md"); }), it("should return empty string for missing optional file", async () => { const result = await processRuntimeImport("missing.md", !0, tempDir); - (expect(result).toBe(""), expect(core.warning).toHaveBeenCalledWith("Optional runtime import file not found: missing.md")); + (expect(result).toBe(""), expect(core.warning).toHaveBeenCalledWith("Optional runtime import file not found: workflows/missing.md")); }), it("should remove front matter and warn", async () => { const filepath = "with-frontmatter.md"; @@ -312,7 +312,7 @@ describe("runtime_import", () => { (expect(result).toContain("# Content"), expect(result).toContain("Actual content."), expect(result).not.toContain("title: Test"), - expect(core.warning).toHaveBeenCalledWith(`File ${filepath} contains front matter which will be ignored in runtime import`)); + expect(core.warning).toHaveBeenCalledWith(`File workflows/${filepath} contains front matter which will be ignored in runtime import`)); }), it("should remove XML comments", async () => { fs.writeFileSync(path.join(workflowsDir, "with-comments.md"), "# Title\n\n\x3c!-- This is a comment --\x3e\n\nContent here."); @@ -385,7 +385,8 @@ describe("runtime_import", () => { it("should reject paths outside .github folder", async () => { // Try to access a file in the root (not in .github) fs.writeFileSync(path.join(tempDir, "outside.md"), "Outside content"); - await expect(processRuntimeImport("../outside.md", !1, tempDir)).rejects.toThrow("Security: Path ../outside.md must be within .github folder"); + // Use ../../ to escape .github/workflows and go up to the temp directory + await expect(processRuntimeImport("../../outside.md", !1, tempDir)).rejects.toThrow("Security: Path"); })); }), describe("processRuntimeImports", () => { @@ -554,8 +555,8 @@ describe("runtime_import", () => { it("should reject attempts to access files outside .github", async () => { // Create a file outside .github fs.writeFileSync(path.join(tempDir, "root-file.txt"), "Root content"); - // Try to access it - await expect(processRuntimeImport("../root-file.txt", !1, tempDir)).rejects.toThrow("Security: Path ../root-file.txt must be within .github folder"); + // Try to access it with a path that truly escapes - using ../../ to go up twice + await expect(processRuntimeImport("../../root-file.txt", !1, tempDir)).rejects.toThrow("Security: Path"); })); }), describe("processRuntimeImport with line ranges", () => { @@ -739,14 +740,14 @@ describe("runtime_import", () => { describe("runtime import with expressions", () => { it("should process file with safe expressions", async () => { const content = "Actor: ${{ github.actor }}\nRepo: ${{ github.repository }}"; - fs.writeFileSync(path.join(githubDir, "with-expr.md"), content); + fs.writeFileSync(path.join(workflowsDir, "with-expr.md"), content); const result = await processRuntimeImport("with-expr.md", false, tempDir); expect(result).toBe("Actor: testuser\nRepo: testorg/testrepo"); }); it("should reject file with unsafe expressions", async () => { const content = "Secret: ${{ secrets.TOKEN }}"; - fs.writeFileSync(path.join(githubDir, "unsafe.md"), content); + fs.writeFileSync(path.join(workflowsDir, "unsafe.md"), content); await expect(processRuntimeImport("unsafe.md", false, tempDir)).rejects.toThrow("unauthorized"); }); @@ -757,7 +758,7 @@ describe("runtime_import", () => { it("should handle expressions with front matter removal", async () => { const content = "---\ntitle: Test\n---\n\nActor: ${{ github.actor }}"; - fs.writeFileSync(path.join(githubDir, "frontmatter-expr.md"), content); + fs.writeFileSync(path.join(workflowsDir, "frontmatter-expr.md"), content); const result = await processRuntimeImport("frontmatter-expr.md", false, tempDir); expect(result).toContain("Actor: testuser"); expect(result).not.toContain("title: Test"); @@ -765,7 +766,7 @@ describe("runtime_import", () => { it("should handle expressions with XML comments", async () => { const content = "\nActor: ${{ github.actor }}"; - fs.writeFileSync(path.join(githubDir, "comment-expr.md"), content); + fs.writeFileSync(path.join(workflowsDir, "comment-expr.md"), content); const result = await processRuntimeImport("comment-expr.md", false, tempDir); expect(result).toContain("Actor: testuser"); expect(result).not.toContain(""); @@ -775,9 +776,9 @@ describe("runtime_import", () => { describe("recursive imports", () => { it("should recursively process runtime-import macros in imported files", async () => { // Create a chain: main.md -> level1.md -> level2.md - fs.writeFileSync(path.join(githubDir, "level2.md"), "Level 2 content"); - fs.writeFileSync(path.join(githubDir, "level1.md"), "Level 1 before\n{{#runtime-import level2.md}}\nLevel 1 after"); - fs.writeFileSync(path.join(githubDir, "main.md"), "Main before\n{{#runtime-import level1.md}}\nMain after"); + fs.writeFileSync(path.join(workflowsDir, "level2.md"), "Level 2 content"); + fs.writeFileSync(path.join(workflowsDir, "level1.md"), "Level 1 before\n{{#runtime-import level2.md}}\nLevel 1 after"); + fs.writeFileSync(path.join(workflowsDir, "main.md"), "Main before\n{{#runtime-import level1.md}}\nMain after"); const result = await processRuntimeImports("{{#runtime-import main.md}}", tempDir); expect(result).toBe("Main before\nLevel 1 before\nLevel 2 content\nLevel 1 after\nMain after"); @@ -787,10 +788,10 @@ describe("runtime_import", () => { it("should handle multiple recursive imports at different levels", async () => { // Create: main.md -> [a.md, b.md] and a.md -> shared.md - fs.writeFileSync(path.join(githubDir, "shared.md"), "Shared content"); - fs.writeFileSync(path.join(githubDir, "a.md"), "A before\n{{#runtime-import shared.md}}\nA after"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B content"); - fs.writeFileSync(path.join(githubDir, "main.md"), "{{#runtime-import a.md}}\n---\n{{#runtime-import b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "shared.md"), "Shared content"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A before\n{{#runtime-import shared.md}}\nA after"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B content"); + fs.writeFileSync(path.join(workflowsDir, "main.md"), "{{#runtime-import a.md}}\n---\n{{#runtime-import b.md}}"); const result = await processRuntimeImports("{{#runtime-import main.md}}", tempDir); expect(result).toBe("A before\nShared content\nA after\n---\nB content"); @@ -798,10 +799,10 @@ describe("runtime_import", () => { it("should cache imported files and reuse them in recursive processing", async () => { // Create: main.md -> [a.md, b.md] where both import shared.md - fs.writeFileSync(path.join(githubDir, "shared.md"), "Shared content"); - fs.writeFileSync(path.join(githubDir, "a.md"), "A: {{#runtime-import shared.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B: {{#runtime-import shared.md}}"); - fs.writeFileSync(path.join(githubDir, "main.md"), "{{#runtime-import a.md}}\n{{#runtime-import b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "shared.md"), "Shared content"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A: {{#runtime-import shared.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B: {{#runtime-import shared.md}}"); + fs.writeFileSync(path.join(workflowsDir, "main.md"), "{{#runtime-import a.md}}\n{{#runtime-import b.md}}"); const result = await processRuntimeImports("{{#runtime-import main.md}}", tempDir); expect(result).toBe("A: Shared content\nB: Shared content"); @@ -811,42 +812,42 @@ describe("runtime_import", () => { it("should detect circular dependencies", async () => { // Create circular dependency: a.md -> b.md -> a.md - fs.writeFileSync(path.join(githubDir, "a.md"), "A content\n{{#runtime-import b.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B content\n{{#runtime-import a.md}}"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A content\n{{#runtime-import b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B content\n{{#runtime-import a.md}}"); await expect(processRuntimeImports("{{#runtime-import a.md}}", tempDir)).rejects.toThrow("Circular dependency detected: a.md -> b.md -> a.md"); }); it("should detect self-referencing circular dependencies", async () => { // Create self-referencing file: self.md -> self.md - fs.writeFileSync(path.join(githubDir, "self.md"), "Self content\n{{#runtime-import self.md}}"); + fs.writeFileSync(path.join(workflowsDir, "self.md"), "Self content\n{{#runtime-import self.md}}"); await expect(processRuntimeImports("{{#runtime-import self.md}}", tempDir)).rejects.toThrow("Circular dependency detected: self.md -> self.md"); }); it("should detect complex circular dependencies", async () => { // Create circular dependency: a.md -> b.md -> c.md -> a.md - fs.writeFileSync(path.join(githubDir, "a.md"), "A content\n{{#runtime-import b.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B content\n{{#runtime-import c.md}}"); - fs.writeFileSync(path.join(githubDir, "c.md"), "C content\n{{#runtime-import a.md}}"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A content\n{{#runtime-import b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B content\n{{#runtime-import c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "c.md"), "C content\n{{#runtime-import a.md}}"); await expect(processRuntimeImports("{{#runtime-import a.md}}", tempDir)).rejects.toThrow("Circular dependency detected: a.md -> b.md -> c.md -> a.md"); }); it("should handle recursive imports with optional files", async () => { // Create: main.md -> exists.md -> optional-missing.md (optional) - fs.writeFileSync(path.join(githubDir, "exists.md"), "Exists before\n{{#runtime-import? optional-missing.md}}\nExists after"); - fs.writeFileSync(path.join(githubDir, "main.md"), "Main\n{{#runtime-import exists.md}}"); + fs.writeFileSync(path.join(workflowsDir, "exists.md"), "Exists before\n{{#runtime-import? optional-missing.md}}\nExists after"); + fs.writeFileSync(path.join(workflowsDir, "main.md"), "Main\n{{#runtime-import exists.md}}"); const result = await processRuntimeImports("{{#runtime-import main.md}}", tempDir); expect(result).toBe("Main\nExists before\n\nExists after"); - expect(core.warning).toHaveBeenCalledWith("Optional runtime import file not found: optional-missing.md"); + expect(core.warning).toHaveBeenCalledWith("Optional runtime import file not found: workflows/optional-missing.md"); }); it("should process expressions in recursively imported files", async () => { // Create recursive imports with expressions - fs.writeFileSync(path.join(githubDir, "inner.md"), "Actor: ${{ github.actor }}"); - fs.writeFileSync(path.join(githubDir, "outer.md"), "Outer\n{{#runtime-import inner.md}}"); + fs.writeFileSync(path.join(workflowsDir, "inner.md"), "Actor: ${{ github.actor }}"); + fs.writeFileSync(path.join(workflowsDir, "outer.md"), "Outer\n{{#runtime-import inner.md}}"); const result = await processRuntimeImports("{{#runtime-import outer.md}}", tempDir); expect(result).toBe("Outer\nActor: testuser"); @@ -854,8 +855,8 @@ describe("runtime_import", () => { it("should remove XML comments from recursively imported files", async () => { // Create recursive imports with XML comments - fs.writeFileSync(path.join(githubDir, "inner.md"), "Inner text"); - fs.writeFileSync(path.join(githubDir, "outer.md"), "Outer \n{{#runtime-import inner.md}}"); + fs.writeFileSync(path.join(workflowsDir, "inner.md"), "Inner text"); + fs.writeFileSync(path.join(workflowsDir, "outer.md"), "Outer \n{{#runtime-import inner.md}}"); const result = await processRuntimeImports("{{#runtime-import outer.md}}", tempDir); expect(result).toBe("Outer \nInner text"); @@ -863,11 +864,11 @@ describe("runtime_import", () => { it("should handle deep nesting of imports", async () => { // Create a deep chain: level1 -> level2 -> level3 -> level4 -> level5 - fs.writeFileSync(path.join(githubDir, "level5.md"), "Level 5"); - fs.writeFileSync(path.join(githubDir, "level4.md"), "Level 4\n{{#runtime-import level5.md}}"); - fs.writeFileSync(path.join(githubDir, "level3.md"), "Level 3\n{{#runtime-import level4.md}}"); - fs.writeFileSync(path.join(githubDir, "level2.md"), "Level 2\n{{#runtime-import level3.md}}"); - fs.writeFileSync(path.join(githubDir, "level1.md"), "Level 1\n{{#runtime-import level2.md}}"); + fs.writeFileSync(path.join(workflowsDir, "level5.md"), "Level 5"); + fs.writeFileSync(path.join(workflowsDir, "level4.md"), "Level 4\n{{#runtime-import level5.md}}"); + fs.writeFileSync(path.join(workflowsDir, "level3.md"), "Level 3\n{{#runtime-import level4.md}}"); + fs.writeFileSync(path.join(workflowsDir, "level2.md"), "Level 2\n{{#runtime-import level3.md}}"); + fs.writeFileSync(path.join(workflowsDir, "level1.md"), "Level 1\n{{#runtime-import level2.md}}"); const result = await processRuntimeImports("{{#runtime-import level1.md}}", tempDir); expect(result).toBe("Level 1\nLevel 2\nLevel 3\nLevel 4\nLevel 5"); @@ -878,13 +879,13 @@ describe("runtime_import", () => { describe("tree topologies", () => { it("should handle balanced binary tree (depth 3)", async () => { // Root -> [L1, R1], L1 -> [L2, L3], R1 -> [R2, R3] - fs.writeFileSync(path.join(githubDir, "leaf-l2.md"), "L2"); - fs.writeFileSync(path.join(githubDir, "leaf-l3.md"), "L3"); - fs.writeFileSync(path.join(githubDir, "leaf-r2.md"), "R2"); - fs.writeFileSync(path.join(githubDir, "leaf-r3.md"), "R3"); - fs.writeFileSync(path.join(githubDir, "left-branch.md"), "L1-start\n{{#runtime-import leaf-l2.md}}\nL1-mid\n{{#runtime-import leaf-l3.md}}\nL1-end"); - fs.writeFileSync(path.join(githubDir, "right-branch.md"), "R1-start\n{{#runtime-import leaf-r2.md}}\nR1-mid\n{{#runtime-import leaf-r3.md}}\nR1-end"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import left-branch.md}}\n---\n{{#runtime-import right-branch.md}}"); + fs.writeFileSync(path.join(workflowsDir, "leaf-l2.md"), "L2"); + fs.writeFileSync(path.join(workflowsDir, "leaf-l3.md"), "L3"); + fs.writeFileSync(path.join(workflowsDir, "leaf-r2.md"), "R2"); + fs.writeFileSync(path.join(workflowsDir, "leaf-r3.md"), "R3"); + fs.writeFileSync(path.join(workflowsDir, "left-branch.md"), "L1-start\n{{#runtime-import leaf-l2.md}}\nL1-mid\n{{#runtime-import leaf-l3.md}}\nL1-end"); + fs.writeFileSync(path.join(workflowsDir, "right-branch.md"), "R1-start\n{{#runtime-import leaf-r2.md}}\nR1-mid\n{{#runtime-import leaf-r3.md}}\nR1-end"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import left-branch.md}}\n---\n{{#runtime-import right-branch.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nL1-start\nL2\nL1-mid\nL3\nL1-end\n---\nR1-start\nR2\nR1-mid\nR3\nR1-end"); @@ -893,10 +894,10 @@ describe("runtime_import", () => { it("should handle wide tree (one root, many leaves)", async () => { // Root -> [leaf1, leaf2, leaf3, ..., leaf10] for (let i = 1; i <= 10; i++) { - fs.writeFileSync(path.join(githubDir, `leaf${i}.md`), `Leaf ${i}`); + fs.writeFileSync(path.join(workflowsDir, `leaf${i}.md`), `Leaf ${i}`); } const imports = Array.from({ length: 10 }, (_, i) => `{{#runtime-import leaf${i + 1}.md}}`).join("\n"); - fs.writeFileSync(path.join(githubDir, "root.md"), `Root\n${imports}`); + fs.writeFileSync(path.join(workflowsDir, "root.md"), `Root\n${imports}`); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); const expected = "Root\n" + Array.from({ length: 10 }, (_, i) => `Leaf ${i + 1}`).join("\n"); @@ -905,11 +906,11 @@ describe("runtime_import", () => { it("should handle unbalanced tree (left-skewed)", async () => { // Root -> L1, L1 -> L2, L2 -> L3, Root -> R1 (short right branch) - fs.writeFileSync(path.join(githubDir, "l3.md"), "L3"); - fs.writeFileSync(path.join(githubDir, "l2.md"), "L2\n{{#runtime-import l3.md}}"); - fs.writeFileSync(path.join(githubDir, "l1.md"), "L1\n{{#runtime-import l2.md}}"); - fs.writeFileSync(path.join(githubDir, "r1.md"), "R1"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import l1.md}}\n{{#runtime-import r1.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l3.md"), "L3"); + fs.writeFileSync(path.join(workflowsDir, "l2.md"), "L2\n{{#runtime-import l3.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l1.md"), "L1\n{{#runtime-import l2.md}}"); + fs.writeFileSync(path.join(workflowsDir, "r1.md"), "R1"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import l1.md}}\n{{#runtime-import r1.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nL1\nL2\nL3\nR1"); @@ -917,9 +918,9 @@ describe("runtime_import", () => { it("should handle very deep tree (depth 15)", async () => { // Create a chain of 15 levels deep - fs.writeFileSync(path.join(githubDir, "d15.md"), "D15"); + fs.writeFileSync(path.join(workflowsDir, "d15.md"), "D15"); for (let i = 14; i >= 1; i--) { - fs.writeFileSync(path.join(githubDir, `d${i}.md`), `D${i}\n{{#runtime-import d${i + 1}.md}}`); + fs.writeFileSync(path.join(workflowsDir, `d${i}.md`), `D${i}\n{{#runtime-import d${i + 1}.md}}`); } const result = await processRuntimeImports("{{#runtime-import d1.md}}", tempDir); @@ -931,10 +932,10 @@ describe("runtime_import", () => { describe("DAG (Directed Acyclic Graph) patterns", () => { it("should handle diamond dependency pattern", async () => { // Root -> [A, B], A -> Shared, B -> Shared - fs.writeFileSync(path.join(githubDir, "shared.md"), "Shared content"); - fs.writeFileSync(path.join(githubDir, "path-a.md"), "Path A\n{{#runtime-import shared.md}}"); - fs.writeFileSync(path.join(githubDir, "path-b.md"), "Path B\n{{#runtime-import shared.md}}"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import path-a.md}}\n---\n{{#runtime-import path-b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "shared.md"), "Shared content"); + fs.writeFileSync(path.join(workflowsDir, "path-a.md"), "Path A\n{{#runtime-import shared.md}}"); + fs.writeFileSync(path.join(workflowsDir, "path-b.md"), "Path B\n{{#runtime-import shared.md}}"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import path-a.md}}\n---\n{{#runtime-import path-b.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nPath A\nShared content\n---\nPath B\nShared content"); @@ -944,11 +945,11 @@ describe("runtime_import", () => { it("should handle multiple diamond patterns", async () => { // Root -> [A, B], A -> [S1, S2], B -> [S1, S2] - fs.writeFileSync(path.join(githubDir, "s1.md"), "S1"); - fs.writeFileSync(path.join(githubDir, "s2.md"), "S2"); - fs.writeFileSync(path.join(githubDir, "a.md"), "A\n{{#runtime-import s1.md}}\n{{#runtime-import s2.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B\n{{#runtime-import s1.md}}\n{{#runtime-import s2.md}}"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "s1.md"), "S1"); + fs.writeFileSync(path.join(workflowsDir, "s2.md"), "S2"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A\n{{#runtime-import s1.md}}\n{{#runtime-import s2.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B\n{{#runtime-import s1.md}}\n{{#runtime-import s2.md}}"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nA\nS1\nS2\nB\nS1\nS2"); @@ -959,13 +960,13 @@ describe("runtime_import", () => { it("should handle complex DAG with multiple levels", async () => { // L1 -> [L2a, L2b], L2a -> [L3a, L3b], L2b -> [L3b, L3c], L3a -> L4, L3b -> L4, L3c -> L4 - fs.writeFileSync(path.join(githubDir, "l4.md"), "L4"); - fs.writeFileSync(path.join(githubDir, "l3a.md"), "L3a\n{{#runtime-import l4.md}}"); - fs.writeFileSync(path.join(githubDir, "l3b.md"), "L3b\n{{#runtime-import l4.md}}"); - fs.writeFileSync(path.join(githubDir, "l3c.md"), "L3c\n{{#runtime-import l4.md}}"); - fs.writeFileSync(path.join(githubDir, "l2a.md"), "L2a\n{{#runtime-import l3a.md}}\n{{#runtime-import l3b.md}}"); - fs.writeFileSync(path.join(githubDir, "l2b.md"), "L2b\n{{#runtime-import l3b.md}}\n{{#runtime-import l3c.md}}"); - fs.writeFileSync(path.join(githubDir, "l1.md"), "L1\n{{#runtime-import l2a.md}}\n{{#runtime-import l2b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l4.md"), "L4"); + fs.writeFileSync(path.join(workflowsDir, "l3a.md"), "L3a\n{{#runtime-import l4.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l3b.md"), "L3b\n{{#runtime-import l4.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l3c.md"), "L3c\n{{#runtime-import l4.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l2a.md"), "L2a\n{{#runtime-import l3a.md}}\n{{#runtime-import l3b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l2b.md"), "L2b\n{{#runtime-import l3b.md}}\n{{#runtime-import l3c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l1.md"), "L1\n{{#runtime-import l2a.md}}\n{{#runtime-import l2b.md}}"); const result = await processRuntimeImports("{{#runtime-import l1.md}}", tempDir); expect(result).toBe("L1\nL2a\nL3a\nL4\nL3b\nL4\nL2b\nL3b\nL4\nL3c\nL4"); @@ -975,12 +976,12 @@ describe("runtime_import", () => { it("should handle convergent DAG (many nodes converge to one)", async () => { // [A, B, C, D] -> Sink - fs.writeFileSync(path.join(githubDir, "sink.md"), "Sink"); - fs.writeFileSync(path.join(githubDir, "a.md"), "A\n{{#runtime-import sink.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B\n{{#runtime-import sink.md}}"); - fs.writeFileSync(path.join(githubDir, "c.md"), "C\n{{#runtime-import sink.md}}"); - fs.writeFileSync(path.join(githubDir, "d.md"), "D\n{{#runtime-import sink.md}}"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}\n{{#runtime-import c.md}}\n{{#runtime-import d.md}}"); + fs.writeFileSync(path.join(workflowsDir, "sink.md"), "Sink"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A\n{{#runtime-import sink.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B\n{{#runtime-import sink.md}}"); + fs.writeFileSync(path.join(workflowsDir, "c.md"), "C\n{{#runtime-import sink.md}}"); + fs.writeFileSync(path.join(workflowsDir, "d.md"), "D\n{{#runtime-import sink.md}}"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}\n{{#runtime-import c.md}}\n{{#runtime-import d.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nA\nSink\nB\nSink\nC\nSink\nD\nSink"); @@ -994,11 +995,11 @@ describe("runtime_import", () => { it("should handle hub-and-spoke (one center, many peripherals)", async () => { // Root -> Hub, Hub -> [Spoke1, Spoke2, ..., Spoke8] for (let i = 1; i <= 8; i++) { - fs.writeFileSync(path.join(githubDir, `spoke${i}.md`), `Spoke ${i}`); + fs.writeFileSync(path.join(workflowsDir, `spoke${i}.md`), `Spoke ${i}`); } const imports = Array.from({ length: 8 }, (_, i) => `{{#runtime-import spoke${i + 1}.md}}`).join("\n"); - fs.writeFileSync(path.join(githubDir, "hub.md"), `Hub\n${imports}`); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import hub.md}}"); + fs.writeFileSync(path.join(workflowsDir, "hub.md"), `Hub\n${imports}`); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import hub.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); const expected = "Root\nHub\n" + Array.from({ length: 8 }, (_, i) => `Spoke ${i + 1}`).join("\n"); @@ -1007,11 +1008,11 @@ describe("runtime_import", () => { it("should handle reverse star (many import one common file)", async () => { // Root -> [A, B, C], all import Common - fs.writeFileSync(path.join(githubDir, "common.md"), "Common"); - fs.writeFileSync(path.join(githubDir, "a.md"), "A\n{{#runtime-import common.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B\n{{#runtime-import common.md}}"); - fs.writeFileSync(path.join(githubDir, "c.md"), "C\n{{#runtime-import common.md}}"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}\n{{#runtime-import c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "common.md"), "Common"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A\n{{#runtime-import common.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B\n{{#runtime-import common.md}}"); + fs.writeFileSync(path.join(workflowsDir, "c.md"), "C\n{{#runtime-import common.md}}"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}\n{{#runtime-import c.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nA\nCommon\nB\nCommon\nC\nCommon"); @@ -1023,12 +1024,12 @@ describe("runtime_import", () => { describe("complex multi-level patterns", () => { it("should handle grid-like dependency pattern", async () => { // Row1 -> [C1, C2, C3], Row2 -> [C1, C2, C3], Root -> [Row1, Row2] - fs.writeFileSync(path.join(githubDir, "c1.md"), "C1"); - fs.writeFileSync(path.join(githubDir, "c2.md"), "C2"); - fs.writeFileSync(path.join(githubDir, "c3.md"), "C3"); - fs.writeFileSync(path.join(githubDir, "row1.md"), "Row1\n{{#runtime-import c1.md}}\n{{#runtime-import c2.md}}\n{{#runtime-import c3.md}}"); - fs.writeFileSync(path.join(githubDir, "row2.md"), "Row2\n{{#runtime-import c1.md}}\n{{#runtime-import c2.md}}\n{{#runtime-import c3.md}}"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import row1.md}}\n---\n{{#runtime-import row2.md}}"); + fs.writeFileSync(path.join(workflowsDir, "c1.md"), "C1"); + fs.writeFileSync(path.join(workflowsDir, "c2.md"), "C2"); + fs.writeFileSync(path.join(workflowsDir, "c3.md"), "C3"); + fs.writeFileSync(path.join(workflowsDir, "row1.md"), "Row1\n{{#runtime-import c1.md}}\n{{#runtime-import c2.md}}\n{{#runtime-import c3.md}}"); + fs.writeFileSync(path.join(workflowsDir, "row2.md"), "Row2\n{{#runtime-import c1.md}}\n{{#runtime-import c2.md}}\n{{#runtime-import c3.md}}"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import row1.md}}\n---\n{{#runtime-import row2.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nRow1\nC1\nC2\nC3\n---\nRow2\nC1\nC2\nC3"); @@ -1040,14 +1041,14 @@ describe("runtime_import", () => { it("should handle pyramid pattern (expanding then contracting)", async () => { // L1 -> [L2a, L2b], L2a -> [L3a, L3b, L3c], L2b -> [L3b, L3c, L3d], L3a/b/c/d -> Apex - fs.writeFileSync(path.join(githubDir, "apex.md"), "Apex"); - fs.writeFileSync(path.join(githubDir, "l3a.md"), "L3a\n{{#runtime-import apex.md}}"); - fs.writeFileSync(path.join(githubDir, "l3b.md"), "L3b\n{{#runtime-import apex.md}}"); - fs.writeFileSync(path.join(githubDir, "l3c.md"), "L3c\n{{#runtime-import apex.md}}"); - fs.writeFileSync(path.join(githubDir, "l3d.md"), "L3d\n{{#runtime-import apex.md}}"); - fs.writeFileSync(path.join(githubDir, "l2a.md"), "L2a\n{{#runtime-import l3a.md}}\n{{#runtime-import l3b.md}}\n{{#runtime-import l3c.md}}"); - fs.writeFileSync(path.join(githubDir, "l2b.md"), "L2b\n{{#runtime-import l3b.md}}\n{{#runtime-import l3c.md}}\n{{#runtime-import l3d.md}}"); - fs.writeFileSync(path.join(githubDir, "l1.md"), "L1\n{{#runtime-import l2a.md}}\n{{#runtime-import l2b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "apex.md"), "Apex"); + fs.writeFileSync(path.join(workflowsDir, "l3a.md"), "L3a\n{{#runtime-import apex.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l3b.md"), "L3b\n{{#runtime-import apex.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l3c.md"), "L3c\n{{#runtime-import apex.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l3d.md"), "L3d\n{{#runtime-import apex.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l2a.md"), "L2a\n{{#runtime-import l3a.md}}\n{{#runtime-import l3b.md}}\n{{#runtime-import l3c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l2b.md"), "L2b\n{{#runtime-import l3b.md}}\n{{#runtime-import l3c.md}}\n{{#runtime-import l3d.md}}"); + fs.writeFileSync(path.join(workflowsDir, "l1.md"), "L1\n{{#runtime-import l2a.md}}\n{{#runtime-import l2b.md}}"); const result = await processRuntimeImports("{{#runtime-import l1.md}}", tempDir); expect(result).toBe("L1\nL2a\nL3a\nApex\nL3b\nApex\nL3c\nApex\nL2b\nL3b\nApex\nL3c\nApex\nL3d\nApex"); @@ -1057,12 +1058,12 @@ describe("runtime_import", () => { it("should handle layered architecture (strict layer dependencies)", async () => { // Layer1 -> [Layer2a, Layer2b], Layer2a -> Layer3a, Layer2b -> Layer3b, Layer3a -> Core, Layer3b -> Core - fs.writeFileSync(path.join(githubDir, "core.md"), "Core"); - fs.writeFileSync(path.join(githubDir, "layer3a.md"), "Layer3a\n{{#runtime-import core.md}}"); - fs.writeFileSync(path.join(githubDir, "layer3b.md"), "Layer3b\n{{#runtime-import core.md}}"); - fs.writeFileSync(path.join(githubDir, "layer2a.md"), "Layer2a\n{{#runtime-import layer3a.md}}"); - fs.writeFileSync(path.join(githubDir, "layer2b.md"), "Layer2b\n{{#runtime-import layer3b.md}}"); - fs.writeFileSync(path.join(githubDir, "layer1.md"), "Layer1\n{{#runtime-import layer2a.md}}\n{{#runtime-import layer2b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "core.md"), "Core"); + fs.writeFileSync(path.join(workflowsDir, "layer3a.md"), "Layer3a\n{{#runtime-import core.md}}"); + fs.writeFileSync(path.join(workflowsDir, "layer3b.md"), "Layer3b\n{{#runtime-import core.md}}"); + fs.writeFileSync(path.join(workflowsDir, "layer2a.md"), "Layer2a\n{{#runtime-import layer3a.md}}"); + fs.writeFileSync(path.join(workflowsDir, "layer2b.md"), "Layer2b\n{{#runtime-import layer3b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "layer1.md"), "Layer1\n{{#runtime-import layer2a.md}}\n{{#runtime-import layer2b.md}}"); const result = await processRuntimeImports("{{#runtime-import layer1.md}}", tempDir); expect(result).toBe("Layer1\nLayer2a\nLayer3a\nCore\nLayer2b\nLayer3b\nCore"); @@ -1074,12 +1075,12 @@ describe("runtime_import", () => { describe("cache efficiency tests", () => { it("should efficiently cache heavily reused dependencies", async () => { // Create a pattern where one file is imported by many others - fs.writeFileSync(path.join(githubDir, "base.md"), "Base content"); + fs.writeFileSync(path.join(workflowsDir, "base.md"), "Base content"); for (let i = 1; i <= 20; i++) { - fs.writeFileSync(path.join(githubDir, `consumer${i}.md`), `Consumer ${i}\n{{#runtime-import base.md}}`); + fs.writeFileSync(path.join(workflowsDir, `consumer${i}.md`), `Consumer ${i}\n{{#runtime-import base.md}}`); } const imports = Array.from({ length: 20 }, (_, i) => `{{#runtime-import consumer${i + 1}.md}}`).join("\n"); - fs.writeFileSync(path.join(githubDir, "root.md"), `Root\n${imports}`); + fs.writeFileSync(path.join(workflowsDir, "root.md"), `Root\n${imports}`); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); @@ -1099,8 +1100,8 @@ describe("runtime_import", () => { it("should cache files with line ranges independently", async () => { // Create a file that is imported with different line ranges const content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"; - fs.writeFileSync(path.join(githubDir, "lines.md"), content); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import lines.md:1-2}}\n---\n{{#runtime-import lines.md:3-5}}\n===\n{{#runtime-import lines.md:1-2}}"); + fs.writeFileSync(path.join(workflowsDir, "lines.md"), content); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import lines.md:1-2}}\n---\n{{#runtime-import lines.md:3-5}}\n===\n{{#runtime-import lines.md:1-2}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nLine 1\nLine 2\n---\nLine 3\nLine 4\nLine 5\n===\nLine 1\nLine 2"); @@ -1111,11 +1112,11 @@ describe("runtime_import", () => { it("should handle mixed optional and required imports with caching", async () => { // Create pattern with both optional and required imports of same file - fs.writeFileSync(path.join(githubDir, "shared.md"), "Shared"); - fs.writeFileSync(path.join(githubDir, "a.md"), "A\n{{#runtime-import shared.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B\n{{#runtime-import? shared.md}}"); - fs.writeFileSync(path.join(githubDir, "c.md"), "C\n{{#runtime-import shared.md}}"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}\n{{#runtime-import c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "shared.md"), "Shared"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A\n{{#runtime-import shared.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B\n{{#runtime-import? shared.md}}"); + fs.writeFileSync(path.join(workflowsDir, "c.md"), "C\n{{#runtime-import shared.md}}"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}\n{{#runtime-import c.md}}"); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); expect(result).toBe("Root\nA\nShared\nB\nShared\nC\nShared"); @@ -1128,20 +1129,20 @@ describe("runtime_import", () => { describe("circular dependency detection in complex graphs", () => { it("should detect indirect cycles in DAG-like structures", async () => { // A -> B, B -> C, C -> D, D -> A (cycle) - fs.writeFileSync(path.join(githubDir, "a.md"), "A\n{{#runtime-import b.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B\n{{#runtime-import c.md}}"); - fs.writeFileSync(path.join(githubDir, "c.md"), "C\n{{#runtime-import d.md}}"); - fs.writeFileSync(path.join(githubDir, "d.md"), "D\n{{#runtime-import a.md}}"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A\n{{#runtime-import b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B\n{{#runtime-import c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "c.md"), "C\n{{#runtime-import d.md}}"); + fs.writeFileSync(path.join(workflowsDir, "d.md"), "D\n{{#runtime-import a.md}}"); await expect(processRuntimeImports("{{#runtime-import a.md}}", tempDir)).rejects.toThrow("Circular dependency detected: a.md -> b.md -> c.md -> d.md -> a.md"); }); it("should detect cycles in branching structures", async () => { // Root -> [A, B], A -> C, B -> C, C -> A (cycle through A) - fs.writeFileSync(path.join(githubDir, "a.md"), "A\n{{#runtime-import c.md}}"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B\n{{#runtime-import c.md}}"); - fs.writeFileSync(path.join(githubDir, "c.md"), "C\n{{#runtime-import a.md}}"); - fs.writeFileSync(path.join(githubDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A\n{{#runtime-import c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B\n{{#runtime-import c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "c.md"), "C\n{{#runtime-import a.md}}"); + fs.writeFileSync(path.join(workflowsDir, "root.md"), "Root\n{{#runtime-import a.md}}\n{{#runtime-import b.md}}"); await expect(processRuntimeImports("{{#runtime-import root.md}}", tempDir)).rejects.toThrow("Circular dependency detected"); await expect(processRuntimeImports("{{#runtime-import root.md}}", tempDir)).rejects.toThrow("a.md -> c.md -> a.md"); @@ -1150,9 +1151,9 @@ describe("runtime_import", () => { it("should detect long cycles (length 10)", async () => { // Create a cycle: f1 -> f2 -> f3 -> ... -> f10 -> f1 for (let i = 1; i <= 9; i++) { - fs.writeFileSync(path.join(githubDir, `f${i}.md`), `F${i}\n{{#runtime-import f${i + 1}.md}}`); + fs.writeFileSync(path.join(workflowsDir, `f${i}.md`), `F${i}\n{{#runtime-import f${i + 1}.md}}`); } - fs.writeFileSync(path.join(githubDir, "f10.md"), "F10\n{{#runtime-import f1.md}}"); + fs.writeFileSync(path.join(workflowsDir, "f10.md"), "F10\n{{#runtime-import f1.md}}"); await expect(processRuntimeImports("{{#runtime-import f1.md}}", tempDir)).rejects.toThrow("Circular dependency detected"); await expect(processRuntimeImports("{{#runtime-import f1.md}}", tempDir)).rejects.toThrow("f1.md"); @@ -1161,10 +1162,10 @@ describe("runtime_import", () => { it("should allow diamond patterns without false positive cycle detection", async () => { // Verify that diamond (A -> [B, C], B -> D, C -> D) is NOT detected as a cycle - fs.writeFileSync(path.join(githubDir, "d.md"), "D"); - fs.writeFileSync(path.join(githubDir, "b.md"), "B\n{{#runtime-import d.md}}"); - fs.writeFileSync(path.join(githubDir, "c.md"), "C\n{{#runtime-import d.md}}"); - fs.writeFileSync(path.join(githubDir, "a.md"), "A\n{{#runtime-import b.md}}\n{{#runtime-import c.md}}"); + fs.writeFileSync(path.join(workflowsDir, "d.md"), "D"); + fs.writeFileSync(path.join(workflowsDir, "b.md"), "B\n{{#runtime-import d.md}}"); + fs.writeFileSync(path.join(workflowsDir, "c.md"), "C\n{{#runtime-import d.md}}"); + fs.writeFileSync(path.join(workflowsDir, "a.md"), "A\n{{#runtime-import b.md}}\n{{#runtime-import c.md}}"); const result = await processRuntimeImports("{{#runtime-import a.md}}", tempDir); expect(result).toBe("A\nB\nD\nC\nD"); @@ -1176,10 +1177,10 @@ describe("runtime_import", () => { it("should handle large fan-out (1 root -> 50 direct imports)", async () => { // Create 50 leaf files for (let i = 1; i <= 50; i++) { - fs.writeFileSync(path.join(githubDir, `leaf${i}.md`), `Leaf ${i}`); + fs.writeFileSync(path.join(workflowsDir, `leaf${i}.md`), `Leaf ${i}`); } const imports = Array.from({ length: 50 }, (_, i) => `{{#runtime-import leaf${i + 1}.md}}`).join("\n"); - fs.writeFileSync(path.join(githubDir, "root.md"), `Root\n${imports}`); + fs.writeFileSync(path.join(workflowsDir, "root.md"), `Root\n${imports}`); const result = await processRuntimeImports("{{#runtime-import root.md}}", tempDir); const lines = result.split("\n"); @@ -1190,27 +1191,27 @@ describe("runtime_import", () => { it("should handle moderate depth with moderate breadth (depth 5, breadth 4)", async () => { // Each level has 4 nodes, each node imports 4 nodes from next level // But use a shared pattern to keep file count reasonable - fs.writeFileSync(path.join(githubDir, "l5.md"), "L5"); + fs.writeFileSync(path.join(workflowsDir, "l5.md"), "L5"); // Level 4: 4 nodes each importing l5 for (let i = 1; i <= 4; i++) { - fs.writeFileSync(path.join(githubDir, `l4-${i}.md`), `L4-${i}\n{{#runtime-import l5.md}}`); + fs.writeFileSync(path.join(workflowsDir, `l4-${i}.md`), `L4-${i}\n{{#runtime-import l5.md}}`); } // Level 3: 4 nodes each importing all level 4 nodes for (let i = 1; i <= 4; i++) { const imports = Array.from({ length: 4 }, (_, j) => `{{#runtime-import l4-${j + 1}.md}}`).join("\n"); - fs.writeFileSync(path.join(githubDir, `l3-${i}.md`), `L3-${i}\n${imports}`); + fs.writeFileSync(path.join(workflowsDir, `l3-${i}.md`), `L3-${i}\n${imports}`); } // Level 2: 4 nodes each importing one level 3 node for (let i = 1; i <= 4; i++) { - fs.writeFileSync(path.join(githubDir, `l2-${i}.md`), `L2-${i}\n{{#runtime-import l3-${i}.md}}`); + fs.writeFileSync(path.join(workflowsDir, `l2-${i}.md`), `L2-${i}\n{{#runtime-import l3-${i}.md}}`); } // Level 1: Root imports all level 2 nodes const imports = Array.from({ length: 4 }, (_, i) => `{{#runtime-import l2-${i + 1}.md}}`).join("\n"); - fs.writeFileSync(path.join(githubDir, "l1.md"), `L1\n${imports}`); + fs.writeFileSync(path.join(workflowsDir, "l1.md"), `L1\n${imports}`); const result = await processRuntimeImports("{{#runtime-import l1.md}}", tempDir); expect(result).toContain("L1"); From cd29036392f45d4d30c553dfdaacfe4e1f7c9ad6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:58:25 +0000 Subject: [PATCH 4/7] Fix Go runtime import test to use workflows directory Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/runtime_import.test.cjs | 6 +++++- docs/src/content/docs/agent-factory-status.mdx | 1 - pkg/parser/runtime_import_test.go | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/runtime_import.test.cjs b/actions/setup/js/runtime_import.test.cjs index fdb8fb39da..0d13994b49 100644 --- a/actions/setup/js/runtime_import.test.cjs +++ b/actions/setup/js/runtime_import.test.cjs @@ -10,7 +10,11 @@ describe("runtime_import", () => { let githubDir; let workflowsDir; (beforeEach(() => { - ((tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "runtime-import-test-"))), (githubDir = path.join(tempDir, ".github")), (workflowsDir = path.join(githubDir, "workflows")), fs.mkdirSync(workflowsDir, { recursive: true }), vi.clearAllMocks()); + ((tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "runtime-import-test-"))), + (githubDir = path.join(tempDir, ".github")), + (workflowsDir = path.join(githubDir, "workflows")), + fs.mkdirSync(workflowsDir, { recursive: true }), + vi.clearAllMocks()); }), afterEach(() => { tempDir && fs.existsSync(tempDir) && fs.rmSync(tempDir, { recursive: !0, force: !0 }); diff --git a/docs/src/content/docs/agent-factory-status.mdx b/docs/src/content/docs/agent-factory-status.mdx index ffe071b46c..1e45720313 100644 --- a/docs/src/content/docs/agent-factory-status.mdx +++ b/docs/src/content/docs/agent-factory-status.mdx @@ -72,7 +72,6 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Delight](https://github.com/github/gh-aw/blob/main/.github/workflows/delight.md) | copilot | [![Delight](https://github.com/github/gh-aw/actions/workflows/delight.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/delight.lock.yml) | - | - | | [Dependabot Burner](https://github.com/github/gh-aw/blob/main/.github/workflows/dependabot-burner.md) | copilot | [![Dependabot Burner](https://github.com/github/gh-aw/actions/workflows/dependabot-burner.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dependabot-burner.lock.yml) | - | - | | [Dependabot Dependency Checker](https://github.com/github/gh-aw/blob/main/.github/workflows/dependabot-go-checker.md) | copilot | [![Dependabot Dependency Checker](https://github.com/github/gh-aw/actions/workflows/dependabot-go-checker.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dependabot-go-checker.lock.yml) | `0 9 * * 1,3,5` | - | -| [Dependabot Project Manager](https://github.com/github/gh-aw/blob/main/.github/workflows/dependabot-project-manager.md) | copilot | [![Dependabot Project Manager](https://github.com/github/gh-aw/actions/workflows/dependabot-project-manager.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dependabot-project-manager.lock.yml) | - | - | | [Dev](https://github.com/github/gh-aw/blob/main/.github/workflows/dev.md) | copilot | [![Dev](https://github.com/github/gh-aw/actions/workflows/dev.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dev.lock.yml) | - | - | | [Dev Hawk](https://github.com/github/gh-aw/blob/main/.github/workflows/dev-hawk.md) | copilot | [![Dev Hawk](https://github.com/github/gh-aw/actions/workflows/dev-hawk.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dev-hawk.lock.yml) | - | - | | [Developer Documentation Consolidator](https://github.com/github/gh-aw/blob/main/.github/workflows/developer-docs-consolidator.md) | claude | [![Developer Documentation Consolidator](https://github.com/github/gh-aw/actions/workflows/developer-docs-consolidator.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/developer-docs-consolidator.lock.yml) | - | - | diff --git a/pkg/parser/runtime_import_test.go b/pkg/parser/runtime_import_test.go index 13e718dbd0..e7974a13fe 100644 --- a/pkg/parser/runtime_import_test.go +++ b/pkg/parser/runtime_import_test.go @@ -311,6 +311,10 @@ func TestRuntimeImportWithExpressions(t *testing.T) { if err := os.MkdirAll(githubDir, 0755); err != nil { t.Fatalf("Failed to create .github directory: %v", err) } + workflowsDir := filepath.Join(githubDir, "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + t.Fatalf("Failed to create workflows directory: %v", err) + } tests := []struct { name string @@ -364,8 +368,8 @@ Unsafe: ${{ runner.os }}`, for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Write test file - testFilePath := filepath.Join(githubDir, "test.md") + // Write test file to workflows directory + testFilePath := filepath.Join(workflowsDir, "test.md") if err := os.WriteFile(testFilePath, []byte(tt.fileContent), 0644); err != nil { t.Fatalf("Failed to write test file: %v", err) } From 617efd94f3eadec3eda7ed4746dc62617b78fde5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:09:50 +0000 Subject: [PATCH 5/7] Replace .actions/ handling with .agents/ for skills directory Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/runtime_import.cjs | 11 ++++++----- actions/setup/js/runtime_import.test.cjs | 12 ++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/actions/setup/js/runtime_import.cjs b/actions/setup/js/runtime_import.cjs index 411508f927..fcfc2fb901 100644 --- a/actions/setup/js/runtime_import.cjs +++ b/actions/setup/js/runtime_import.cjs @@ -694,14 +694,15 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start filepath = filepath.substring(8); // Remove ".github/" } else if (filepath.startsWith(".github\\")) { filepath = filepath.substring(8); // Remove ".github\" (Windows) - } else if (filepath.startsWith(".actions/")) { - // Remove the leading dot from .actions/ -> actions/ + } else if (filepath.startsWith(".agents/")) { + // Remove the leading dot from .agents/ -> agents/ + // This is the common folder location for skills filepath = filepath.substring(1); // Remove the leading "." - } else if (filepath.startsWith(".actions\\")) { - // Remove the leading dot from .actions\ -> actions\ (Windows) + } else if (filepath.startsWith(".agents\\")) { + // Remove the leading dot from .agents\ -> agents\ (Windows) filepath = filepath.substring(1); // Remove the leading "." } else { - // If path doesn't start with .github or .actions, prefix with workflows/ + // If path doesn't start with .github or .agents, prefix with workflows/ // This makes imports like "a.md" resolve to ".github/workflows/a.md" filepath = path.join("workflows", filepath); } diff --git a/actions/setup/js/runtime_import.test.cjs b/actions/setup/js/runtime_import.test.cjs index 0d13994b49..c3c04a48b8 100644 --- a/actions/setup/js/runtime_import.test.cjs +++ b/actions/setup/js/runtime_import.test.cjs @@ -378,12 +378,12 @@ describe("runtime_import", () => { const result = await processRuntimeImport("test-no-prefix.md", !1, tempDir); expect(result).toBe(content); }), - it("should support .actions/ prefix", async () => { - const actionsDir = path.join(githubDir, "actions"); - fs.mkdirSync(actionsDir, { recursive: true }); - const content = "Test with .actions prefix"; - fs.writeFileSync(path.join(actionsDir, "test-action.md"), content); - const result = await processRuntimeImport(".actions/test-action.md", !1, tempDir); + it("should support .agents/ prefix", async () => { + const agentsDir = path.join(githubDir, "agents"); + fs.mkdirSync(agentsDir, { recursive: true }); + const content = "Test with .agents prefix"; + fs.writeFileSync(path.join(agentsDir, "test-skill.md"), content); + const result = await processRuntimeImport(".agents/test-skill.md", !1, tempDir); expect(result).toBe(content); }), it("should reject paths outside .github folder", async () => { From ab67f3e65fa24eb69df35d51bfe94e2c7b21fef3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:19:17 +0000 Subject: [PATCH 6/7] Fix .agents/ to resolve to top-level folder (not .github/agents/) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/runtime_import.cjs | 79 +++++++++++++++--------- actions/setup/js/runtime_import.test.cjs | 5 +- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/actions/setup/js/runtime_import.cjs b/actions/setup/js/runtime_import.cjs index fcfc2fb901..fd06b667f9 100644 --- a/actions/setup/js/runtime_import.cjs +++ b/actions/setup/js/runtime_import.cjs @@ -687,45 +687,68 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start // Otherwise, process as a file let filepath = filepathOrUrl; + let isAgentsPath = false; - // Trim .github/ prefix if provided (support both .github/file and file) - // This allows users to use either format - if (filepath.startsWith(".github/")) { + // Check if this is a .agents/ path (top-level folder for skills) + if (filepath.startsWith(".agents/")) { + isAgentsPath = true; + // Keep .agents/ as is - it's a top-level folder at workspace root + } else if (filepath.startsWith(".agents\\")) { + isAgentsPath = true; + // Keep .agents\ as is - it's a top-level folder at workspace root (Windows) + } else if (filepath.startsWith(".github/")) { + // Trim .github/ prefix if provided (support both .github/file and file) filepath = filepath.substring(8); // Remove ".github/" } else if (filepath.startsWith(".github\\")) { filepath = filepath.substring(8); // Remove ".github\" (Windows) - } else if (filepath.startsWith(".agents/")) { - // Remove the leading dot from .agents/ -> agents/ - // This is the common folder location for skills - filepath = filepath.substring(1); // Remove the leading "." - } else if (filepath.startsWith(".agents\\")) { - // Remove the leading dot from .agents\ -> agents\ (Windows) - filepath = filepath.substring(1); // Remove the leading "." } else { // If path doesn't start with .github or .agents, prefix with workflows/ // This makes imports like "a.md" resolve to ".github/workflows/a.md" filepath = path.join("workflows", filepath); } - // Remove leading ./ or ../ if present - if (filepath.startsWith("./")) { - filepath = filepath.substring(2); - } else if (filepath.startsWith(".\\")) { - filepath = filepath.substring(2); + // Remove leading ./ or ../ if present (only for non-agents paths) + if (!isAgentsPath) { + if (filepath.startsWith("./")) { + filepath = filepath.substring(2); + } else if (filepath.startsWith(".\\")) { + filepath = filepath.substring(2); + } } - // Note: We don't allow ../ paths as they would escape .github folder - - // Construct the path within .github folder - const githubFolder = path.join(workspaceDir, ".github"); - const absolutePath = path.resolve(githubFolder, filepath); - const normalizedPath = path.normalize(absolutePath); - const normalizedGithubFolder = path.normalize(githubFolder); - - // Security check: ensure the resolved path is within the .github folder - // Use path.relative to check if the path escapes the .github folder - const relativePath = path.relative(normalizedGithubFolder, normalizedPath); - if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { - throw new Error(`Security: Path ${filepathOrUrl} must be within .github folder (resolves to: ${relativePath})`); + // Note: We don't allow ../ paths as they would escape the base folder + + // Construct the absolute path - .agents paths are relative to workspace root, others to .github + let absolutePath, normalizedPath, baseFolder, normalizedBaseFolder; + + if (isAgentsPath) { + // .agents/ paths resolve to top-level .agents folder at workspace root + baseFolder = workspaceDir; + absolutePath = path.resolve(workspaceDir, filepath); + normalizedPath = path.normalize(absolutePath); + normalizedBaseFolder = path.normalize(baseFolder); + + // Security check: ensure the resolved path is within the workspace + const relativePath = path.relative(normalizedBaseFolder, normalizedPath); + if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + throw new Error(`Security: Path ${filepathOrUrl} must be within workspace (resolves to: ${relativePath})`); + } + // Additional check: ensure path stays within .agents folder + if (!relativePath.startsWith(".agents" + path.sep) && relativePath !== ".agents") { + throw new Error(`Security: Path ${filepathOrUrl} must be within .agents folder`); + } + } else { + // Regular paths resolve within .github folder + const githubFolder = path.join(workspaceDir, ".github"); + baseFolder = githubFolder; + absolutePath = path.resolve(githubFolder, filepath); + normalizedPath = path.normalize(absolutePath); + normalizedBaseFolder = path.normalize(githubFolder); + + // Security check: ensure the resolved path is within the .github folder + const relativePath = path.relative(normalizedBaseFolder, normalizedPath); + if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + throw new Error(`Security: Path ${filepathOrUrl} must be within .github folder (resolves to: ${relativePath})`); + } } // Check if file exists diff --git a/actions/setup/js/runtime_import.test.cjs b/actions/setup/js/runtime_import.test.cjs index c3c04a48b8..c07455d9d1 100644 --- a/actions/setup/js/runtime_import.test.cjs +++ b/actions/setup/js/runtime_import.test.cjs @@ -378,8 +378,9 @@ describe("runtime_import", () => { const result = await processRuntimeImport("test-no-prefix.md", !1, tempDir); expect(result).toBe(content); }), - it("should support .agents/ prefix", async () => { - const agentsDir = path.join(githubDir, "agents"); + it("should support .agents/ prefix (top-level folder)", async () => { + // .agents is a top-level folder at workspace root, not inside .github + const agentsDir = path.join(tempDir, ".agents"); fs.mkdirSync(agentsDir, { recursive: true }); const content = "Test with .agents prefix"; fs.writeFileSync(path.join(agentsDir, "test-skill.md"), content); From 9675a82581cb5704dc92cf76aeb0d302f4494645 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:33:26 +0000 Subject: [PATCH 7/7] Merge main branch and verify linting passes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/runtime_import.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/runtime_import.cjs b/actions/setup/js/runtime_import.cjs index 0f68d161e1..0a1eb5aa18 100644 --- a/actions/setup/js/runtime_import.cjs +++ b/actions/setup/js/runtime_import.cjs @@ -745,14 +745,14 @@ async function processRuntimeImport(filepathOrUrl, optional, workspaceDir, start // Construct the absolute path - .agents paths are relative to workspace root, others to .github let absolutePath, normalizedPath, baseFolder, normalizedBaseFolder; - + if (isAgentsPath) { // .agents/ paths resolve to top-level .agents folder at workspace root baseFolder = workspaceDir; absolutePath = path.resolve(workspaceDir, filepath); normalizedPath = path.normalize(absolutePath); normalizedBaseFolder = path.normalize(baseFolder); - + // Security check: ensure the resolved path is within the workspace const relativePath = path.relative(normalizedBaseFolder, normalizedPath); if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {