diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index d2e0935da2..743368479f 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -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") @@ -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 + ` @@ -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() +} diff --git a/pkg/cli/commands_new_test.go b/pkg/cli/commands_new_test.go new file mode 100644 index 0000000000..8e5b22fae5 --- /dev/null +++ b/pkg/cli/commands_new_test.go @@ -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) + } +} diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index ae970e1b8b..311f2816dd 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4833,10 +4833,6 @@ { "type": "null", "description": "Enable project creation with default configuration (max=1)" - }, - { - "enum": [null], - "description": "Alternative null value syntax" } ], "default": {