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
40 changes: 27 additions & 13 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/fileutil"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/parser"
)

var commandsLog = logger.New("cli:commands")
Expand Down Expand Up @@ -115,19 +116,7 @@ permissions:
# Network access
network: defaults

# Outputs - what APIs and tools can the AI use?
safe-outputs:
create-issue: # Creates issues (default max: 1)
max: 5 # Optional: specify maximum number
# create-agent-session: # Creates GitHub Copilot coding agent sessions (max: 1)
# create-pull-request: # Creates exactly one pull request
# add-comment: # Adds comments (default max: 1)
# max: 2 # Optional: specify maximum number
# add-labels:
# update-issue:
# create-discussion:
# push-to-pull-request-branch:

` + buildSafeOutputsSection() + `
---

# ` + workflowName + `
Expand All @@ -150,3 +139,28 @@ Be clear and specific about what the AI should accomplish.
- See https://github.github.com/gh-aw/ for complete configuration options and tools documentation
`
}

// buildSafeOutputsSection generates the safe-outputs section of the workflow template.
// It uses the JSON schema to derive the valid safe output type names, ensuring the
// template is always consistent with what the schema accepts.
func buildSafeOutputsSection() string {
keys, err := parser.GetSafeOutputTypeKeys()
if err != nil {
commandsLog.Printf("Failed to get safe output type keys from schema: %v", err)
// Fallback to a minimal static section
return "# Outputs - what APIs and tools can the AI use?\nsafe-outputs:\n create-issue: # Creates issues (default max: 1)\n"
}

var sb strings.Builder
sb.WriteString("# Outputs - what APIs and tools can the AI use?\n")
sb.WriteString("safe-outputs:\n")
sb.WriteString(" create-issue: # Creates issues (default max: 1)\n")
sb.WriteString(" max: 5 # Optional: specify maximum number\n")
for _, key := range keys {
if key == "create-issue" {
continue
}
sb.WriteString(" # " + key + ":\n")
}
return sb.String()
}
109 changes: 109 additions & 0 deletions pkg/cli/commands_new_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//go:build !integration

package cli

import (
"strings"
"testing"

"github.com/github/gh-aw/pkg/parser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// extractSafeOutputKeys parses a YAML snippet that begins with "safe-outputs:" and
// returns the list of safe-output type keys that appear at exactly 2 spaces of
// indentation. Lines indented more deeply are sub-keys of the active entry and are
// skipped; comment-only lines and the section header itself are also skipped.
func extractSafeOutputKeys(section string) []string {
var keys []string
for line := range strings.SplitSeq(section, "\n") {
// Only examine lines with exactly 2 leading spaces (type-key level).
// Sub-keys (e.g. " max: 5") have 4+ spaces and are skipped.
if !strings.HasPrefix(line, " ") || strings.HasPrefix(line, " ") {
continue
}
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}

// Strip a leading comment marker so both active and commented keys are checked.
candidate := strings.TrimPrefix(trimmed, "# ")

// Extract the key name (the part before the colon).
key, _, found := strings.Cut(candidate, ":")
if !found {
continue
}
key = strings.TrimSpace(key)
if key == "" {
continue
}
keys = append(keys, key)
}
return keys
}

// TestBuildSafeOutputsSection validates that buildSafeOutputsSection generates
// safe-output names that are all valid according to the JSON schema.
func TestBuildSafeOutputsSection(t *testing.T) {
section := buildSafeOutputsSection()

require.NotEmpty(t, section, "buildSafeOutputsSection should return non-empty content")
assert.Contains(t, section, "safe-outputs:", "Section should contain safe-outputs key")
assert.Contains(t, section, "create-issue:", "Section should include create-issue as active example")

// Get valid schema keys to check against
validKeys, err := parser.GetSafeOutputTypeKeys()
require.NoError(t, err, "GetSafeOutputTypeKeys should not return error")
require.NotEmpty(t, validKeys, "Schema should have safe output type keys")

validKeySet := make(map[string]bool, len(validKeys))
for _, k := range validKeys {
validKeySet[k] = true
}

for _, key := range extractSafeOutputKeys(section) {
assert.True(t, validKeySet[key],
"safe-output name %q used in 'new' template is not a valid schema key", key)
}
}

// TestCreateWorkflowTemplateContainsOnlyValidSafeOutputs validates that the workflow
// template generated by the 'new' command only references safe-output names that are
// defined in the JSON schema.
func TestCreateWorkflowTemplateContainsOnlyValidSafeOutputs(t *testing.T) {
template := createWorkflowTemplate("test-workflow", "")

validKeys, err := parser.GetSafeOutputTypeKeys()
require.NoError(t, err, "GetSafeOutputTypeKeys should not return error")

validKeySet := make(map[string]bool, len(validKeys))
for _, k := range validKeys {
validKeySet[k] = true
}

// Isolate the safe-outputs section of the template.
inSafeOutputs := false
var safeOutputsLines []string
for line := range strings.SplitSeq(template, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "safe-outputs:" {
inSafeOutputs = true
safeOutputsLines = append(safeOutputsLines, line)
continue
}
if inSafeOutputs && (trimmed == "---" || (len(line) > 0 && line[0] != ' ' && !strings.HasPrefix(line, "#"))) {
break
}
if inSafeOutputs {
safeOutputsLines = append(safeOutputsLines, line)
}
}

for _, key := range extractSafeOutputKeys(strings.Join(safeOutputsLines, "\n")) {
assert.True(t, validKeySet[key],
"safe-output name %q in 'new' template is not a valid schema key", key)
}
}
4 changes: 0 additions & 4 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4833,10 +4833,6 @@
{
"type": "null",
"description": "Enable project creation with default configuration (max=1)"
},
{
"enum": [null],
"description": "Alternative null value syntax"
}
],
"default": {
Expand Down
Loading