Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/code-simplifier.lock.yml

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

2 changes: 1 addition & 1 deletion .github/workflows/code-simplifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ strict: true
---

<!-- Edit the file linked below to modify the agent without recompilation. Feel free to move the entire markdown body to that file. -->
@./agentics/code-simplifier.md
{{#runtime-import agentics/code-simplifier.md}}
1,649 changes: 0 additions & 1,649 deletions .github/workflows/file-size-reduction-project64.campaign.lock.yml

This file was deleted.

2,042 changes: 0 additions & 2,042 deletions .github/workflows/file-size-reduction-project71.campaign.lock.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/repo-audit-analyzer.lock.yml

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

2 changes: 1 addition & 1 deletion .github/workflows/repo-audit-analyzer.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ imports:
---

<!-- Edit the file linked below to modify the agent without recompilation. Feel free to move the entire markdown body to that file. -->
@./agentics/repo-audit-analyzer.md
{{#runtime-import agentics/repo-audit-analyzer.md}}
18 changes: 4 additions & 14 deletions actions/setup/js/interpolate_prompt.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

const fs = require("fs");
const { isTruthy } = require("./is_truthy.cjs");
const { processRuntimeImports, convertInlinesToMacros } = require("./runtime_import.cjs");
const { processRuntimeImports } = require("./runtime_import.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");

/**
Expand Down Expand Up @@ -79,17 +79,7 @@ async function main() {
// Read the prompt file
let content = fs.readFileSync(promptPath, "utf8");

// Step 1: Convert @./path and @url inline syntax to {{#runtime-import}} macros
const hasInlines = /@[^\s]+/.test(content);
if (hasInlines) {
core.info("Converting inline references (@./path, @../path, and @url) to runtime-import macros");
content = convertInlinesToMacros(content);
core.info("Inline references converted successfully");
} else {
core.info("No inline references found, skipping conversion");
}

// Step 2: Process runtime imports (including converted @./path and @url macros)
// Step 1: Process runtime imports (files and URLs)
const hasRuntimeImports = /{{#runtime-import\??[ \t]+[^\}]+}}/.test(content);
if (hasRuntimeImports) {
core.info("Processing runtime import macros (files and URLs)");
Expand All @@ -99,7 +89,7 @@ async function main() {
core.info("No runtime import macros found, skipping runtime import processing");
}

// Step 3: Interpolate variables
// Step 2: Interpolate variables
/** @type {Record<string, string>} */
const variables = {};
for (const [key, value] of Object.entries(process.env)) {
Expand All @@ -117,7 +107,7 @@ async function main() {
core.info("No expression variables found, skipping interpolation");
}

// Step 4: Render template conditionals
// Step 3: Render template conditionals
const hasConditionals = /{{#if\s+[^}]+}}/.test(content);
if (hasConditionals) {
core.info("Processing conditional template blocks");
Expand Down
83 changes: 0 additions & 83 deletions actions/setup/js/runtime_import.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -622,92 +622,9 @@ async function processRuntimeImports(content, workspaceDir) {
return processedContent;
}

/**
* Converts inline syntax to runtime-import macros
* - File paths: `@./path` or `@../path` (must start with ./ or ../)
* - URLs: `@https://...` or `@http://...`
* @param {string} content - The markdown content containing inline references
* @returns {string} - Content with inline references converted to runtime-import macros
*/
function convertInlinesToMacros(content) {
let processedContent = content;

// First, process URL patterns (@https://... or @http://...)
const urlPattern = /@(https?:\/\/[^\s]+?)(?::(\d+)-(\d+))?(?=[\s\n]|$)/g;
let match;

urlPattern.lastIndex = 0;
while ((match = urlPattern.exec(content)) !== null) {
const url = match[1];
const startLine = match[2];
const endLine = match[3];
const fullMatch = match[0];

// Skip if this looks like part of an email address
const matchIndex = match.index;
if (matchIndex > 0) {
const charBefore = content[matchIndex - 1];
if (/[a-zA-Z0-9_]/.test(charBefore)) {
continue;
}
}

// Convert to {{#runtime-import URL}} or {{#runtime-import URL:start-end}}
let macro;
if (startLine && endLine) {
macro = `{{#runtime-import ${url}:${startLine}-${endLine}}}`;
} else {
macro = `{{#runtime-import ${url}}}`;
}

processedContent = processedContent.replace(fullMatch, macro);
}

// Then, process file path patterns (@./path or @../path or @./path:line-line)
// This pattern matches ONLY relative paths starting with ./ or ../
// - @./file.ext
// - @./path/to/file.ext
// - @../path/to/file.ext:10-20
// But NOT:
// - @path (without ./ or ../)
// - email addresses like user@example.com
// - URLs (already processed)
const filePattern = /@(\.\.?\/[a-zA-Z0-9_\-./]+)(?::(\d+)-(\d+))?/g;

filePattern.lastIndex = 0;
while ((match = filePattern.exec(processedContent)) !== null) {
const filepath = match[1];
const startLine = match[2];
const endLine = match[3];
const fullMatch = match[0];

// Skip if this looks like part of an email address
const matchIndex = match.index;
if (matchIndex > 0) {
const charBefore = processedContent[matchIndex - 1];
if (/[a-zA-Z0-9_]/.test(charBefore)) {
continue;
}
}

// Convert to {{#runtime-import filepath}} or {{#runtime-import filepath:start-end}}
let macro;
if (startLine && endLine) {
macro = `{{#runtime-import ${filepath}:${startLine}-${endLine}}}`;
} else {
macro = `{{#runtime-import ${filepath}}}`;
}

processedContent = processedContent.replace(fullMatch, macro);
}

return processedContent;
}

module.exports = {
processRuntimeImports,
processRuntimeImport,
convertInlinesToMacros,
hasFrontMatter,
removeXMLComments,
hasGitHubActionsMacros,
Expand Down
68 changes: 1 addition & 67 deletions actions/setup/js/runtime_import.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from "path";
import os from "os";
const core = { info: vi.fn(), warning: vi.fn(), setFailed: vi.fn() };
global.core = core;
const { processRuntimeImports, processRuntimeImport, convertInlinesToMacros, hasFrontMatter, removeXMLComments, hasGitHubActionsMacros } = require("./runtime_import.cjs");
const { processRuntimeImports, processRuntimeImport, hasFrontMatter, removeXMLComments, hasGitHubActionsMacros } = require("./runtime_import.cjs");
describe("runtime_import", () => {
let tempDir;
let githubDir;
Expand Down Expand Up @@ -386,72 +386,6 @@ describe("runtime_import", () => {
expect(result).toBeTruthy(); // At minimum, it should not fail
}));
}),
describe("convertInlinesToMacros", () => {
(it("should convert @./path to {{#runtime-import ./path}}", () => {
const result = convertInlinesToMacros("Before @./test.txt after");
expect(result).toBe("Before {{#runtime-import ./test.txt}} after");
}),
it("should convert @./path:line-line to {{#runtime-import ./path:line-line}}", () => {
const result = convertInlinesToMacros("Content: @./test.txt:2-4 end");
expect(result).toBe("Content: {{#runtime-import ./test.txt:2-4}} end");
}),
it("should convert multiple @./path references", () => {
const result = convertInlinesToMacros("Start @./file1.txt middle @./file2.txt end");
expect(result).toBe("Start {{#runtime-import ./file1.txt}} middle {{#runtime-import ./file2.txt}} end");
}),
it("should handle @./path with subdirectories", () => {
const result = convertInlinesToMacros("See @./docs/readme.md for details");
expect(result).toBe("See {{#runtime-import ./docs/readme.md}} for details");
}),
it("should handle @../path references", () => {
const result = convertInlinesToMacros("Parent: @../parent.md content");
expect(result).toBe("Parent: {{#runtime-import ../parent.md}} content");
}),
it("should NOT convert @path without ./ or ../", () => {
const result = convertInlinesToMacros("Before @test.txt after");
expect(result).toBe("Before @test.txt after");
}),
it("should NOT convert @docs/file without ./ prefix", () => {
const result = convertInlinesToMacros("See @docs/readme.md for details");
expect(result).toBe("See @docs/readme.md for details");
}),
it("should convert @url to {{#runtime-import url}}", () => {
const result = convertInlinesToMacros("Content from @https://example.com/file.txt ");
expect(result).toBe("Content from {{#runtime-import https://example.com/file.txt}} ");
}),
it("should convert @url:line-line to {{#runtime-import url:line-line}}", () => {
const result = convertInlinesToMacros("Lines: @https://example.com/file.txt:10-20 ");
expect(result).toBe("Lines: {{#runtime-import https://example.com/file.txt:10-20}} ");
}),
it("should not convert email addresses", () => {
const result = convertInlinesToMacros("Email: user@example.com is valid");
expect(result).toBe("Email: user@example.com is valid");
}),
it("should handle content without @path references", () => {
const result = convertInlinesToMacros("No inline references here");
expect(result).toBe("No inline references here");
}),
it("should handle @./path at start of content", () => {
const result = convertInlinesToMacros("@./start.txt following text");
expect(result).toBe("{{#runtime-import ./start.txt}} following text");
}),
it("should handle @./path at end of content", () => {
const result = convertInlinesToMacros("Preceding text @./end.txt");
expect(result).toBe("Preceding text {{#runtime-import ./end.txt}}");
}),
it("should handle @./path on its own line", () => {
const result = convertInlinesToMacros("Before\n@./content.txt\nAfter");
expect(result).toBe("Before\n{{#runtime-import ./content.txt}}\nAfter");
}),
it("should handle multiple line ranges", () => {
const result = convertInlinesToMacros("First: @./test.txt:1-2 Second: @./test.txt:4-5");
expect(result).toBe("First: {{#runtime-import ./test.txt:1-2}} Second: {{#runtime-import ./test.txt:4-5}}");
}),
it("should convert mixed @./path and @url references", () => {
const result = convertInlinesToMacros("File: @./local.txt URL: @https://example.com/remote.txt ");
expect(result).toBe("File: {{#runtime-import ./local.txt}} URL: {{#runtime-import https://example.com/remote.txt}} ");
}));
}),
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");
Expand Down
75 changes: 42 additions & 33 deletions docs/file-url-inlining.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,52 @@
# File/URL Inlining Syntax
# Runtime Import Syntax

This document describes the file and URL inlining syntax feature for GitHub Agentic Workflows.
This document describes the runtime import syntax feature for GitHub Agentic Workflows.

## Overview

The file/URL inlining syntax allows you to include content from files and URLs directly within your workflow prompts at runtime. This provides a convenient way to reference external content without using the `{{#runtime-import}}` macro.
The runtime import syntax allows you to include content from files and URLs directly within your workflow prompts at runtime. This provides a convenient way to reference external content using the `{{#runtime-import}}` macro.

**Important:** File paths must start with `./` or `../` (relative paths only). Paths are resolved relative to `GITHUB_WORKSPACE` and are validated to ensure they stay within the git root for security.
**Important:** File paths are resolved within the `.github` folder. Paths are validated to ensure they stay within the git repository root for security.

## Security

**Path Validation**: All file paths are validated to ensure they stay within the git repository root:
**Path Validation**: All file paths are validated to ensure they stay within the `.github` folder:
- Paths are normalized to resolve `.` and `..` components
- After normalization, the resolved path must be within `GITHUB_WORKSPACE`
- Attempts to escape the git root (e.g., `../../../etc/passwd`) are rejected with a security error
- Example: `./a/b/../../c/file.txt` is allowed if it resolves to `c/file.txt` within the git root
- After normalization, the resolved path must be within `.github` folder
- Attempts to escape the folder (e.g., `../../../etc/passwd`) are rejected with a security error
- Example: `.github/a/b/../../c/file.txt` is allowed if it resolves to `.github/c/file.txt`

## Syntax

### File Inlining
### File Import

**Full File**: `@./path/to/file.ext` or `@../path/to/file.ext`
- Includes the entire content of the file
- Path MUST start with `./` (current directory) or `../` (parent directory)
- Path is resolved relative to `GITHUB_WORKSPACE`
- Example: `@./docs/README.md`
**Full File**: `{{#runtime-import filepath}}`
- Includes the entire content of the file from `.github` folder
- Path can be specified with or without `.github/` prefix
- Example: `{{#runtime-import docs/README.md}}` or `{{#runtime-import .github/docs/README.md}}`

**Line Range**: `@./path/to/file.ext:start-end`
**Line Range**: `{{#runtime-import filepath:start-end}}`
- Includes specific lines from the file (1-indexed, inclusive)
- Start and end are line numbers
- Example: `@./src/main.go:10-20` includes lines 10 through 20
- Example: `{{#runtime-import src/main.go:10-20}}` includes lines 10 through 20

**Important Notes:**
- `@path` (without `./` or `../`) will NOT be processed - it stays as plain text
- Only relative paths starting with `./` or `../` are supported
- The resolved path must stay within the git repository root
### URL Import

### URL Inlining

**HTTP/HTTPS URLs**: `@https://example.com/file.txt`
**HTTP/HTTPS URLs**: `{{#runtime-import https://example.com/file.txt}}`
- Fetches content from the URL
- Content is cached for 1 hour to reduce network requests
- Cache is stored in `/tmp/gh-aw/url-cache/`
- Example: `@https://raw.githubusercontent.com/owner/repo/main/README.md`
- Example: `{{#runtime-import https://raw.githubusercontent.com/owner/repo/main/README.md}}`

## Features

### Content Sanitization

All inlined content is automatically sanitized:
All imported content is automatically sanitized:
- **Front matter removal**: YAML front matter (between `---` delimiters) is stripped
- **XML comment removal**: HTML/XML comments (`<!-- ... -->`) are removed
- **GitHub Actions macro detection**: Content containing `${{ ... }}` expressions is rejected with an error

### Email Address Handling

The parser is smart about email addresses:
- `user@example.com` is NOT treated as a file reference
- Only `@./path`, `@../path`, and `@https://` patterns are processed

## Examples

### Example 1: Include Documentation
Expand All @@ -76,7 +64,7 @@ Please review the following code changes.

## Coding Guidelines

@./docs/coding-guidelines.md
{{#runtime-import docs/coding-guidelines.md}}

## Changes Summary

Expand All @@ -96,7 +84,28 @@ engine: copilot

The original buggy code was:

@./src/auth.go:45-52
{{#runtime-import src/auth.go:45-52}}

Verify that the fix addresses the issue.
```

### Example 3: External Checklist

```markdown
---
description: Security review
on: pull_request
engine: copilot
---

# Security Review

Follow this security checklist:

{{#runtime-import https://raw.githubusercontent.com/org/security/main/checklist.md}}

Review the changes for security vulnerabilities.
```

Verify the fix addresses the issue.
```
Expand Down
Loading
Loading