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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/src/content/docs/agent-factory-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn,
| [Daily File Diet](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-file-diet.md) | copilot | [![Daily File Diet](https://github.com/github/gh-aw/actions/workflows/daily-file-diet.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-file-diet.lock.yml) | `daily around 13:00 on weekdays` | - |
| [Daily Firewall Logs Collector and Reporter](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-firewall-report.md) | copilot | [![Daily Firewall Logs Collector and Reporter](https://github.com/github/gh-aw/actions/workflows/daily-firewall-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-firewall-report.lock.yml) | - | - |
| [Daily Go Function Namer](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-function-namer.md) | claude | [![Daily Go Function Namer](https://github.com/github/gh-aw/actions/workflows/daily-function-namer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-function-namer.lock.yml) | - | - |
| [Daily Grafana OTel Instrumentation Advisor](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-grafana-otel-instrumentation-advisor.md) | claude | [![Daily Grafana OTel Instrumentation Advisor](https://github.com/github/gh-aw/actions/workflows/daily-grafana-otel-instrumentation-advisor.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-grafana-otel-instrumentation-advisor.lock.yml) | - | - |
| [Daily Hippo Learn](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-hippo-learn.md) | copilot | [![Daily Hippo Learn](https://github.com/github/gh-aw/actions/workflows/daily-hippo-learn.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-hippo-learn.lock.yml) | `daily around 7:00` | - |
| [Daily Issues Report Generator](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-issues-report.md) | copilot | [![Daily Issues Report Generator](https://github.com/github/gh-aw/actions/workflows/daily-issues-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-issues-report.lock.yml) | - | - |
| [Daily Malicious Code Scan Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-malicious-code-scan.md) | copilot | [![Daily Malicious Code Scan Agent](https://github.com/github/gh-aw/actions/workflows/daily-malicious-code-scan.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-malicious-code-scan.lock.yml) | - | - |
Expand Down Expand Up @@ -118,6 +119,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn,
| [Example: Properly Provisioned Permissions](https://github.com/github/gh-aw/blob/main/.github/workflows/example-permissions-warning.md) | copilot | [![Example: Properly Provisioned Permissions](https://github.com/github/gh-aw/actions/workflows/example-permissions-warning.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/example-permissions-warning.lock.yml) | - | - |
| [Firewall Test Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/firewall.md) | copilot | [![Firewall Test Agent](https://github.com/github/gh-aw/actions/workflows/firewall.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/firewall.lock.yml) | - | - |
| [Functional Pragmatist](https://github.com/github/gh-aw/blob/main/.github/workflows/functional-pragmatist.md) | copilot | [![Functional Pragmatist](https://github.com/github/gh-aw/actions/workflows/functional-pragmatist.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/functional-pragmatist.lock.yml) | `25 9 * * 2,4` | - |
| [GEO Optimizer Daily Audit](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-geo-optimizer.md) | copilot | [![GEO Optimizer Daily Audit](https://github.com/github/gh-aw/actions/workflows/daily-geo-optimizer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-geo-optimizer.lock.yml) | - | - |
| [GitHub API Consumption Report Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/api-consumption-report.md) | claude | [![GitHub API Consumption Report Agent](https://github.com/github/gh-aw/actions/workflows/api-consumption-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/api-consumption-report.lock.yml) | - | - |
| [GitHub MCP Remote Server Tools Report Generator](https://github.com/github/gh-aw/blob/main/.github/workflows/github-mcp-tools-report.md) | claude | [![GitHub MCP Remote Server Tools Report Generator](https://github.com/github/gh-aw/actions/workflows/github-mcp-tools-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/github-mcp-tools-report.lock.yml) | - | - |
| [GitHub MCP Structural Analysis](https://github.com/github/gh-aw/blob/main/.github/workflows/github-mcp-structural-analysis.md) | claude | [![GitHub MCP Structural Analysis](https://github.com/github/gh-aw/actions/workflows/github-mcp-structural-analysis.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/github-mcp-structural-analysis.lock.yml) | `daily around 11:00 on weekdays` | - |
Expand All @@ -137,6 +139,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn,
| [jsweep - JavaScript Unbloater](https://github.com/github/gh-aw/blob/main/.github/workflows/jsweep.md) | copilot | [![jsweep - JavaScript Unbloater](https://github.com/github/gh-aw/actions/workflows/jsweep.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/jsweep.lock.yml) | - | - |
| [Layout Specification Maintainer](https://github.com/github/gh-aw/blob/main/.github/workflows/layout-spec-maintainer.md) | copilot | [![Layout Specification Maintainer](https://github.com/github/gh-aw/actions/workflows/layout-spec-maintainer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/layout-spec-maintainer.lock.yml) | `weekly on monday around 7:00` | - |
| [Lockfile Statistics Analysis Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/lockfile-stats.md) | claude | [![Lockfile Statistics Analysis Agent](https://github.com/github/gh-aw/actions/workflows/lockfile-stats.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/lockfile-stats.lock.yml) | - | - |
| [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/blob/main/.github/workflows/mattpocock-skills-reviewer.md) | copilot | [![Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/workflows/mattpocock-skills-reviewer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mattpocock-skills-reviewer.lock.yml) | - | - |
| [MCP Inspector Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/mcp-inspector.md) | copilot | [![MCP Inspector Agent](https://github.com/github/gh-aw/actions/workflows/mcp-inspector.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mcp-inspector.lock.yml) | - | - |
| [Mergefest](https://github.com/github/gh-aw/blob/main/.github/workflows/mergefest.md) | copilot | [![Mergefest](https://github.com/github/gh-aw/actions/workflows/mergefest.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mergefest.lock.yml) | - | `/mergefest` |
| [Metrics Collector - Infrastructure Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/metrics-collector.md) | copilot | [![Metrics Collector - Infrastructure Agent](https://github.com/github/gh-aw/actions/workflows/metrics-collector.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/metrics-collector.lock.yml) | - | - |
Expand Down
29 changes: 27 additions & 2 deletions pkg/workflow/engine_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func GenerateMultiSecretValidationStep(secretNames []string, engineName, docsURL
expr = override
}
}
stepLines = append(stepLines, fmt.Sprintf(" %s: %s", secretName, expr))
stepLines = appendEnvVarLine(stepLines, secretName, expr)
}

return GitHubActionStep(stepLines)
Expand Down Expand Up @@ -263,13 +263,38 @@ func FormatStepWithCommandAndEnv(stepLines []string, command string, env map[str

for _, key := range envKeys {
value := env[key]
stepLines = append(stepLines, fmt.Sprintf(" %s: %s", key, yamlStringValue(value)))
stepLines = appendEnvVarLine(stepLines, key, value)
}
}

return stepLines
}

// appendEnvVarLine appends a YAML env var entry to lines.
// If the value contains embedded newlines (e.g. from a multi-line YAML block scalar
// like >- with extra-indented continuation lines), it is emitted as a YAML literal
// block scalar (|) with proper indentation. At most one trailing newline (produced
// by block scalars) is trimmed before processing; multiple intentional trailing
// newlines are preserved.
func appendEnvVarLine(lines []string, key, value string) []string {
// Trim at most one trailing newline added by YAML | or > block scalars.
// Using TrimSuffix (not TrimRight) to avoid stripping multiple trailing
// newlines that may be intentional in the value.
value = strings.TrimSuffix(value, "\n")

if !strings.Contains(value, "\n") {
// Single-line: emit inline with YAML-safe quoting
return append(lines, fmt.Sprintf(" %s: %s", key, yamlStringValue(value)))
}

// Multi-line: emit as a literal block scalar so embedded newlines are preserved
lines = append(lines, fmt.Sprintf(" %s: |", key))
for line := range strings.SplitSeq(value, "\n") {
lines = append(lines, " "+line)
}
return lines
}

// yamlStringValue returns a YAML-safe representation of a string value.
// If the value starts with a YAML flow indicator ('{' or '[') or other characters
// that would cause it to be misinterpreted by YAML parsers, it wraps the value
Expand Down
144 changes: 144 additions & 0 deletions pkg/workflow/engine_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,148 @@ func TestFormatStepWithCommandAndEnvYAMLSafe(t *testing.T) {
t.Errorf("Expected unquoted github expression in env, got:\n%s", output)
}
})

t.Run("multi-line env var emitted as literal block scalar", func(t *testing.T) {
stepLines := []string{" - name: Test step"}
// Continuation lines have 4-space leading whitespace (as produced by goccy/go-yaml
// when parsing a >- block scalar with extra-indented continuation lines).
multiLineValue := "${{ secrets.PAT_1 != '' && secrets.PAT_1 ||\n secrets.PAT_2 != '' && secrets.PAT_2 ||\n secrets.PAT_3 }}"
env := map[string]string{
"COPILOT_GITHUB_TOKEN": multiLineValue,
}
result := FormatStepWithCommandAndEnv(stepLines, "echo test", env)
output := strings.Join(result, "\n")

// Multi-line value must be emitted as a literal block scalar
if !strings.Contains(output, " COPILOT_GITHUB_TOKEN: |") {
t.Errorf("Expected literal block scalar indicator, got:\n%s", output)
}
if !strings.Contains(output, " ${{ secrets.PAT_1 != '' && secrets.PAT_1 ||") {
t.Errorf("Expected first line of multi-line value, got:\n%s", output)
}
// Continuation lines have 4-space prefix preserved, so total indentation is 12+4=16 spaces.
if !strings.Contains(output, " secrets.PAT_3 }}") {
t.Errorf("Expected last line of multi-line value with preserved continuation indentation (16 spaces), got:\n%s", output)
}
})

t.Run("trailing newline in env var is trimmed", func(t *testing.T) {
stepLines := []string{" - name: Test step"}
env := map[string]string{
"MY_TOKEN": "${{ secrets.TOKEN }}\n",
}
result := FormatStepWithCommandAndEnv(stepLines, "echo test", env)
output := strings.Join(result, "\n")

// Trailing newline should be trimmed; value should be emitted inline
if !strings.Contains(output, "MY_TOKEN: ${{ secrets.TOKEN }}") {
t.Errorf("Expected trailing newline to be trimmed, got:\n%s", output)
}
// Should not emit a block scalar for a value that only had a trailing newline
if strings.Contains(output, "MY_TOKEN: |") {
t.Errorf("Expected inline emission (not block scalar) after trimming trailing newline, got:\n%s", output)
}
})
}

func TestAppendEnvVarLine(t *testing.T) {
tests := []struct {
name string
key string
value string
expectedContent []string
notExpected []string
}{
{
name: "single-line value emitted inline",
key: "MY_VAR",
value: "simple value",
expectedContent: []string{
" MY_VAR: simple value",
},
},
{
name: "github expression emitted inline without extra quoting",
key: "TOKEN",
value: "${{ secrets.TOKEN }}",
expectedContent: []string{
" TOKEN: ${{ secrets.TOKEN }}",
},
},
{
name: "json value gets single-quoted inline",
key: "CONFIG",
value: `{"key":"value"}`,
expectedContent: []string{
` CONFIG: '{"key":"value"}'`,
},
},
{
name: "multi-line value emitted as literal block scalar",
key: "COPILOT_GITHUB_TOKEN",
// Continuation lines have 4-space leading whitespace (as produced by goccy/go-yaml
// when parsing a >- block scalar with extra-indented continuation lines).
value: "${{ secrets.PAT_1 != '' && secrets.PAT_1 ||\n secrets.PAT_2 }}",
expectedContent: []string{
" COPILOT_GITHUB_TOKEN: |",
" ${{ secrets.PAT_1 != '' && secrets.PAT_1 ||",
// Continuation line has 4-space prefix preserved: 12 base + 4 continuation = 16 spaces total.
" secrets.PAT_2 }}",
},
notExpected: []string{
"COPILOT_GITHUB_TOKEN: ${{ secrets.PAT_1",
},
},
{
name: "trailing newline is trimmed before deciding inline vs block",
key: "TRIMMED",
value: "${{ secrets.TOKEN }}\n",
expectedContent: []string{
" TRIMMED: ${{ secrets.TOKEN }}",
},
notExpected: []string{
"TRIMMED: |",
},
},
{
name: "only one trailing newline is trimmed (not multiple)",
key: "MULTI_NEWLINE",
value: "line one\nline two\n\n",
expectedContent: []string{
" MULTI_NEWLINE: |",
" line one",
" line two",
// The second trailing newline becomes an empty line in the block scalar.
" ",
},
},
{
name: "multi-line value with trailing newline trimmed",
key: "MULTI",
value: "line one\nline two\n",
expectedContent: []string{
" MULTI: |",
" line one",
" line two",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := appendEnvVarLine([]string{}, tt.key, tt.value)
output := strings.Join(result, "\n")

for _, expected := range tt.expectedContent {
if !strings.Contains(output, expected) {
t.Errorf("Expected result to contain %q\nGot:\n%s", expected, output)
}
}
for _, notExp := range tt.notExpected {
if strings.Contains(output, notExp) {
t.Errorf("Expected result NOT to contain %q\nGot:\n%s", notExp, output)
}
}
})
}
}
32 changes: 32 additions & 0 deletions pkg/workflow/secret_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,38 @@ func TestGenerateMultiSecretValidationStepWithEnvOverrides(t *testing.T) {
t.Errorf("Expected default OPENAI_API_KEY expression (not overridden), got:\n%s", stepContent)
}
})

t.Run("multi-line override emitted as literal block scalar", func(t *testing.T) {
// Continuation lines have 4-space leading whitespace (as produced by goccy/go-yaml
// when parsing a >- block scalar with extra-indented continuation lines).
multiLineExpr := "${{ secrets.GH_AW_PAT_1 != '' && secrets.GH_AW_PAT_1 ||\n secrets.GH_AW_PAT_2 != '' && secrets.GH_AW_PAT_2 ||\n secrets.GH_AW_PAT_3 }}"
overrides := map[string]string{
"COPILOT_GITHUB_TOKEN": multiLineExpr,
}
step := GenerateMultiSecretValidationStep(
[]string{"COPILOT_GITHUB_TOKEN"},
"GitHub Copilot CLI",
"https://docs.example.com",
overrides,
)
stepContent := strings.Join(step, "\n")

// Multi-line value must be emitted as a literal block scalar
if !strings.Contains(stepContent, " COPILOT_GITHUB_TOKEN: |") {
t.Errorf("Expected literal block scalar indicator, got:\n%s", stepContent)
}
if !strings.Contains(stepContent, " ${{ secrets.GH_AW_PAT_1 != '' && secrets.GH_AW_PAT_1 ||") {
t.Errorf("Expected first line of multi-line expression, got:\n%s", stepContent)
}
// Continuation lines have 4-space prefix preserved: 12 base + 4 continuation = 16 spaces total.
if !strings.Contains(stepContent, " secrets.GH_AW_PAT_3 }}") {
t.Errorf("Expected last line of multi-line expression with preserved continuation indentation (16 spaces), got:\n%s", stepContent)
}
// Should not emit the raw multi-line value inline
if strings.Contains(stepContent, "COPILOT_GITHUB_TOKEN: ${{ secrets.GH_AW_PAT_1") {
t.Errorf("Expected block scalar, not inline multi-line value, got:\n%s", stepContent)
}
})
}

func TestValidationStepUsesEngineEnvOverride(t *testing.T) {
Expand Down