From 96f17e88a47570729736e3edce2e4731c291adb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:00:11 +0000 Subject: [PATCH 1/7] feat: merge bootstrap config into add/add-wizard commands Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/add_command.go | 13 ++- pkg/cli/add_interactive_orchestrator.go | 7 ++ pkg/cli/add_package_manifest_test.go | 12 +-- pkg/cli/add_workflow_resolution.go | 48 +++++++++++ pkg/cli/bootstrap.go | 2 +- pkg/cli/bootstrap_config.go | 83 +++++++++++++++++++ pkg/cli/bootstrap_profile_manifest.go | 58 ++++++------- pkg/cli/bootstrap_profile_runner.go | 10 +-- pkg/cli/bootstrap_test.go | 55 +++++++------ pkg/parser/schemas/aw_manifest_schema.json | 95 +++++++++++++++++++--- 10 files changed, 304 insertions(+), 79 deletions(-) create mode 100644 pkg/cli/bootstrap_config.go diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index be4da2694a7..57af4e155d9 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -152,8 +152,17 @@ func runAddCommand(cmd *cobra.Command, args []string, validateEngine func(string StopAfter: stopAfter, DisableSecurityScanner: disableSecurityScanner, } - _, err := AddWorkflows(cmd.Context(), args, opts) - return err + resolved, err := ResolveWorkflows(cmd.Context(), args, verbose) + if err != nil { + return err + } + if _, err := AddResolvedWorkflows(cmd.Context(), args, resolved, opts); err != nil { + return err + } + if resolved.BootstrapProfile != nil { + printBootstrapConfigTODO(resolved.BootstrapProfile) + } + return nil } func registerAddCommandFlags(cmd *cobra.Command) { diff --git a/pkg/cli/add_interactive_orchestrator.go b/pkg/cli/add_interactive_orchestrator.go index 7becae55270..6ac878b2b1a 100644 --- a/pkg/cli/add_interactive_orchestrator.go +++ b/pkg/cli/add_interactive_orchestrator.go @@ -164,6 +164,13 @@ func RunAddInteractive(ctx context.Context, config *AddInteractiveConfig) error return err } + // Step 9b: Apply bootstrap config steps interactively (if the package declares any) + if config.resolvedWorkflows != nil && config.resolvedWorkflows.BootstrapProfile != nil { + if err := executeBootstrapConfigForAdd(ctx, config.RepoOverride, config.WorkflowSpecs, config.resolvedWorkflows.BootstrapProfile, config.Verbose); err != nil { + return err + } + } + // Step 10: Check status and offer to run if err := config.checkStatusAndOfferRun(ctx); err != nil { return err diff --git a/pkg/cli/add_package_manifest_test.go b/pkg/cli/add_package_manifest_test.go index 09b584e2018..af31355fc03 100644 --- a/pkg/cli/add_package_manifest_test.go +++ b/pkg/cli/add_package_manifest_test.go @@ -449,7 +449,7 @@ files: case "aw.yml": return []byte(`name: Repo Assist bootstrap: - actions: + config: - type: require-owner-type owner: repo value: org @@ -481,11 +481,11 @@ bootstrap: pkg, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") require.NoError(t, err) require.NotNil(t, pkg.Bootstrap) - require.Len(t, pkg.Bootstrap.Actions, 3) - assert.Equal(t, "require-owner-type", pkg.Bootstrap.Actions[0].Type) - assert.Equal(t, "repo-variable", pkg.Bootstrap.Actions[1].Type) - assert.Equal(t, []string{"preview", "review", "live"}, pkg.Bootstrap.Actions[1].Enum) - assert.Equal(t, "handoff", pkg.Bootstrap.Actions[2].Type) + require.Len(t, pkg.Bootstrap.Config, 3) + assert.Equal(t, "require-owner-type", pkg.Bootstrap.Config[0].Type) + assert.Equal(t, "repo-variable", pkg.Bootstrap.Config[1].Type) + assert.Equal(t, []string{"preview", "review", "live"}, pkg.Bootstrap.Config[1].Enum) + assert.Equal(t, "handoff", pkg.Bootstrap.Config[2].Type) assert.Contains(t, pkg.Warnings, "Using experimental feature: manifest.bootstrap") }) diff --git a/pkg/cli/add_workflow_resolution.go b/pkg/cli/add_workflow_resolution.go index 071321fd8cd..7da15acf1b1 100644 --- a/pkg/cli/add_workflow_resolution.go +++ b/pkg/cli/add_workflow_resolution.go @@ -58,6 +58,10 @@ type ResolvedWorkflows struct { HasWorkflowDispatch bool // Warnings contains non-fatal package-resolution warnings to show during add Warnings []string + // BootstrapProfile holds the bootstrap profile from an aw.yml package manifest, + // when exactly one source package declares a bootstrap.config section. + // Used by add (non-interactive TODO list) and add-wizard (interactive setup). + BootstrapProfile *resolvedBootstrapProfile } // ResolveWorkflows resolves workflow specifications by parsing specs and fetching workflow content. @@ -79,6 +83,7 @@ func ResolveWorkflows(ctx context.Context, workflows []string, verbose bool) (*R // Parse workflow specifications parsedSpecs := make([]*WorkflowSpec, 0, len(workflows)) var resolutionWarnings []string + var bootstrapProfiles []*resolvedBootstrapProfile for _, workflow := range workflows { if pkg, pkgErr := resolveLocalRepositoryPackage(workflow); pkgErr != nil { @@ -86,6 +91,13 @@ func ResolveWorkflows(ctx context.Context, workflows []string, verbose bool) (*R } else if pkg != nil { resolutionWarnings = append(resolutionWarnings, pkg.Warnings...) parsedSpecs = appendLocalRepositoryPackageWorkflowSpecs(parsedSpecs, pkg) + if pkg.Bootstrap != nil { + bootstrapProfiles = append(bootstrapProfiles, &resolvedBootstrapProfile{ + PackageID: pkg.ManifestPath, + Source: workflow, + Profile: pkg.Bootstrap, + }) + } continue } @@ -98,6 +110,14 @@ func ResolveWorkflows(ctx context.Context, workflows []string, verbose bool) (*R if pkgErr == nil { resolutionWarnings = append(resolutionWarnings, pkg.Warnings...) parsedSpecs = appendRepositoryPackageWorkflowSpecs(parsedSpecs, repoSpec, pkg) + if pkg.Bootstrap != nil { + packageID := repositoryPackageIdentifier(repoSpec.RepoSlug, repoSpec.PackagePath) + bootstrapProfiles = append(bootstrapProfiles, &resolvedBootstrapProfile{ + PackageID: packageID, + Source: workflow, + Profile: pkg.Bootstrap, + }) + } continue } if repoSpec.PackagePath == "" || !isRepositoryPackageManifestNotFound(pkgErr) { @@ -118,6 +138,14 @@ func ResolveWorkflows(ctx context.Context, workflows []string, verbose bool) (*R } resolutionWarnings = append(resolutionWarnings, pkg.Warnings...) parsedSpecs = appendRepositoryPackageWorkflowSpecs(parsedSpecs, repoSpec, pkg) + if pkg.Bootstrap != nil { + packageID := repositoryPackageIdentifier(repoSpec.RepoSlug, repoSpec.PackagePath) + bootstrapProfiles = append(bootstrapProfiles, &resolvedBootstrapProfile{ + PackageID: packageID, + Source: workflow, + Profile: pkg.Bootstrap, + }) + } continue } @@ -264,11 +292,31 @@ func ResolveWorkflows(ctx context.Context, workflows []string, verbose bool) (*R resolutionLog.Printf("Resolution complete: resolved=%d workflows, has_wildcard=%t, has_dispatch=%t", len(resolvedWorkflows), hasWildcard, hasWorkflowDispatch) + // Collect the single bootstrap profile if exactly one package declared one. + // Multiple conflicting profiles produce a warning; the caller gets nil. + var bootstrapProfile *resolvedBootstrapProfile + switch len(bootstrapProfiles) { + case 0: + // nothing to do + case 1: + bootstrapProfile = bootstrapProfiles[0] + resolutionLog.Printf("Bootstrap profile found: packageID=%s", bootstrapProfile.PackageID) + default: + ids := make([]string, 0, len(bootstrapProfiles)) + for _, p := range bootstrapProfiles { + ids = append(ids, p.PackageID) + } + resolutionLog.Printf("Multiple bootstrap profiles found (%v); skipping all", ids) + resolutionWarnings = append(resolutionWarnings, + fmt.Sprintf("multiple bootstrap profiles found (%s); bootstrap config will be skipped — run each package separately to apply its config", strings.Join(ids, ", "))) + } + return &ResolvedWorkflows{ Workflows: resolvedWorkflows, HasWildcard: hasWildcard, HasWorkflowDispatch: hasWorkflowDispatch, Warnings: resolutionWarnings, + BootstrapProfile: bootstrapProfile, }, nil } diff --git a/pkg/cli/bootstrap.go b/pkg/cli/bootstrap.go index 0dfa29ee6f8..809eb558cbd 100644 --- a/pkg/cli/bootstrap.go +++ b/pkg/cli/bootstrap.go @@ -553,7 +553,7 @@ func buildBootstrapPlanLines(plan *bootstrapPlan, opts BootstrapOptions) []strin if plan.BootstrapProfile != nil { lines = append(lines, "- evaluate bootstrap actions from "+plan.BootstrapProfile.PackageID) if plan.ProfileNeedsAction { - lines = append(lines, fmt.Sprintf("- apply bootstrap profile actions (%d action(s))", len(plan.BootstrapProfile.Profile.Actions))) + lines = append(lines, fmt.Sprintf("- apply bootstrap profile actions (%d action(s))", len(plan.BootstrapProfile.Profile.Config))) } else { lines = append(lines, "- bootstrap profile actions already satisfied") } diff --git a/pkg/cli/bootstrap_config.go b/pkg/cli/bootstrap_config.go new file mode 100644 index 00000000000..6dac20624fb --- /dev/null +++ b/pkg/cli/bootstrap_config.go @@ -0,0 +1,83 @@ +package cli + +import ( + "context" + "fmt" + "os" + + "github.com/github/gh-aw/pkg/console" +) + +// printBootstrapConfigTODO prints a TODO checklist of manual steps required by the +// bootstrap.config entries in the package manifest. Called by the non-interactive +// "add" command after workflows have been installed. +func printBootstrapConfigTODO(profile *resolvedBootstrapProfile) { + if profile == nil || profile.Profile == nil || len(profile.Profile.Config) == 0 { + return + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Post-installation steps from "+profile.PackageID+":")) + + for _, action := range profile.Profile.Config { + switch action.Type { + case "require-owner-type": + fmt.Fprintf(os.Stderr, " ✓ Repository owner type constraint: %s\n", action.Value) + case "repo-variable": + line := " ☐ Set repository variable: " + action.Name + if action.Prompt != "" { + line += " — " + action.Prompt + } + if action.Optional { + line += " (optional)" + } + fmt.Fprintln(os.Stderr, line) + case "repo-secret": + line := " ☐ Set repository secret: " + action.Name + if action.Prompt != "" { + line += " — " + action.Prompt + } + if action.Optional { + line += " (optional)" + } + fmt.Fprintln(os.Stderr, line) + case "github-app": + appLabel := action.AppName + if appLabel == "" { + appLabel = "GitHub App" + } + fmt.Fprintf(os.Stderr, " ☐ Configure %s (variable: %s, secret: %s)\n", + appLabel, action.AppIDVariable, action.PrivateKeySecret) + case "copilot-auth": + secret := action.Secret + if secret == "" { + secret = "COPILOT_GITHUB_TOKEN" + } + fmt.Fprintf(os.Stderr, " ☐ Set Copilot PAT secret: %s\n", secret) + case "handoff": + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(action.Message)) + } + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Run 'gh aw bootstrap --repo OWNER/REPO' to apply these steps interactively.")) + fmt.Fprintln(os.Stderr, "") +} + +// executeBootstrapConfigForAdd runs the bootstrap config actions interactively. +// Used by add-wizard after the workflow PR has been created and merged. +func executeBootstrapConfigForAdd(ctx context.Context, repo string, sources []string, profile *resolvedBootstrapProfile, verbose bool) error { + if profile == nil || profile.Profile == nil || len(profile.Profile.Config) == 0 { + return nil + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Applying post-installation steps from "+profile.PackageID+"...")) + + return executeBootstrapProfile(ctx, bootstrapProfileRunConfig{ + Repo: repo, + Sources: sources, + Profile: profile, + Verbose: verbose, + }) +} diff --git a/pkg/cli/bootstrap_profile_manifest.go b/pkg/cli/bootstrap_profile_manifest.go index 8587ec2f00d..d5416d8d4a5 100644 --- a/pkg/cli/bootstrap_profile_manifest.go +++ b/pkg/cli/bootstrap_profile_manifest.go @@ -13,7 +13,7 @@ import ( const bootstrapActionTypeExample = "require-owner-type, repo-variable, repo-secret, github-app, copilot-auth, or handoff" type repositoryPackageBootstrap struct { - Actions []repositoryPackageBootstrapAction + Config []repositoryPackageBootstrapAction } type repositoryPackageBootstrapAction struct { @@ -183,39 +183,39 @@ func localBootstrapManifestPath(resolvedPath string) (string, string, error) { func extractManifestBootstrap(value any, manifestPath string) (*repositoryPackageBootstrap, error) { root, ok := value.(map[string]any) if !ok { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap must be a mapping. Example: bootstrap: { actions: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap must be a mapping. Example: bootstrap: { config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) } - actionsValue, ok := root["actions"] + configValue, ok := root["config"] if !ok { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions is required. Example: bootstrap: { actions: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config is required. Example: bootstrap: { config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) } - actionItems, ok := actionsValue.([]any) + configItems, ok := configValue.([]any) if !ok { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions must be a list. Example: bootstrap: { actions: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config must be a list. Example: bootstrap: { config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) } - if len(actionItems) == 0 { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions must not be empty. Example: bootstrap: { actions: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) + if len(configItems) == 0 { + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config must not be empty. Example: bootstrap: { config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) } bootstrap := &repositoryPackageBootstrap{} - for index, item := range actionItems { + for index, item := range configItems { actionMap, ok := item.(map[string]any) if !ok { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d] must be a mapping. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d] must be a mapping. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) } actionType, ok := stringValue(actionMap["type"]) if !ok || strings.TrimSpace(actionType) == "" { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].type must be a non-empty string. Example: type: repo-variable", manifestPath, index) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].type must be a non-empty string. Example: type: repo-variable", manifestPath, index) } action, err := parseManifestBootstrapAction(strings.TrimSpace(actionType), actionMap, manifestPath, index) if err != nil { return nil, err } - bootstrap.Actions = append(bootstrap.Actions, action) + bootstrap.Config = append(bootstrap.Config, action) } return bootstrap, nil @@ -283,7 +283,7 @@ func parseManifestBootstrapAction(actionType string, actionMap map[string]any, m action.Events = events } if _, exists := actionMap["when"]; exists { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].when is not supported yet. Example: remove the when field and keep only supported keys such as type, name, and prompt", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].when is not supported yet. Example: remove the when field and keep only supported keys such as type, name, and prompt", manifestPath, index) } if permissionsValue, exists := actionMap["permissions"]; exists { permissions, err := stringMapValue(permissionsValue) @@ -296,31 +296,31 @@ func parseManifestBootstrapAction(actionType string, actionMap map[string]any, m switch actionType { case "require-owner-type": if action.Owner != "" && action.Owner != "repo" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].owner must be 'repo' when type=require-owner-type. Example: { type: require-owner-type, owner: repo, value: org }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].owner must be 'repo' when type=require-owner-type. Example: { type: require-owner-type, owner: repo, value: org }", manifestPath, index) } if action.Value != "any" && action.Value != "org" && action.Value != "user" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].value must be one of: any, org, user. Example: { type: require-owner-type, value: org }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].value must be one of: any, org, user. Example: { type: require-owner-type, value: org }", manifestPath, index) } case "repo-variable": if action.Name == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].name is required when type=repo-variable. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].name is required when type=repo-variable. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) } if action.Prompt == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].prompt is required when type=repo-variable. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].prompt is required when type=repo-variable. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) } case "repo-secret": if action.Name == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].name is required when type=repo-secret. Example: { type: repo-secret, name: EXAMPLE_SECRET, prompt: Enter a secret }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].name is required when type=repo-secret. Example: { type: repo-secret, name: EXAMPLE_SECRET, prompt: Enter a secret }", manifestPath, index) } if action.Prompt == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].prompt is required when type=repo-secret. Example: { type: repo-secret, name: EXAMPLE_SECRET, prompt: Enter a secret }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].prompt is required when type=repo-secret. Example: { type: repo-secret, name: EXAMPLE_SECRET, prompt: Enter a secret }", manifestPath, index) } case "github-app": if action.AppName == "" && action.Name != "" { action.AppName = action.Name } if action.ExistingOnly && action.Mode != "" && action.Mode != "existing" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].existing-only requires mode to be 'existing' or unset. Remove mode=%q or set it to 'existing'", manifestPath, index, action.Mode) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].existing-only requires mode to be 'existing' or unset. Remove mode=%q or set it to 'existing'", manifestPath, index, action.Mode) } if action.ExistingOnly && action.Mode == "" { action.Mode = "existing" @@ -329,16 +329,16 @@ func parseManifestBootstrapAction(actionType string, actionMap map[string]any, m action.Mode = "create-or-existing" } if action.Mode != "create-or-existing" && action.Mode != "existing" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].mode must be one of: create-or-existing, existing. Example: { type: github-app, mode: existing, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].mode must be one of: create-or-existing, existing. Example: { type: github-app, mode: existing, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) } if action.Owner != "" && action.Owner != "repo" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].owner must be 'repo' when type=github-app. Example: { type: github-app, owner: repo, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].owner must be 'repo' when type=github-app. Example: { type: github-app, owner: repo, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) } if action.AppIDVariable == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].app-id-variable is required when type=github-app. Example: { type: github-app, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].app-id-variable is required when type=github-app. Example: { type: github-app, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) } if action.PrivateKeySecret == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].private-key-secret is required when type=github-app. Example: { type: github-app, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].private-key-secret is required when type=github-app. Example: { type: github-app, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) } case "copilot-auth": if action.Secret == "" { @@ -348,14 +348,14 @@ func parseManifestBootstrapAction(actionType string, actionMap map[string]any, m action.Strategy = "prompt-if-actions-auth-unavailable" } if action.Strategy != "prompt-if-actions-auth-unavailable" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].strategy must be 'prompt-if-actions-auth-unavailable'. Example: { type: copilot-auth, strategy: prompt-if-actions-auth-unavailable }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].strategy must be 'prompt-if-actions-auth-unavailable'. Example: { type: copilot-auth, strategy: prompt-if-actions-auth-unavailable }", manifestPath, index) } case "handoff": if action.Message == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].message is required when type=handoff. Example: { type: handoff, message: Continue with repository-specific setup. }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].message is required when type=handoff. Example: { type: handoff, message: Continue with repository-specific setup. }", manifestPath, index) } default: - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].type %q is not supported. Example: use one of %s", manifestPath, index, actionType, bootstrapActionTypeExample) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].type %q is not supported. Example: use one of %s", manifestPath, index, actionType, bootstrapActionTypeExample) } return action, nil @@ -401,9 +401,9 @@ func stringMapValue(value any) (map[string]string, error) { func manifestBootstrapFieldError(manifestPath string, index int, field string, err error) error { if example, ok := manifestBootstrapFieldExample(field); ok { - return fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].%s %s. Example: bootstrap.actions[%d].%s: %s", manifestPath, index, field, err.Error(), index, field, example) + return fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].%s %s. Example: bootstrap.config[%d].%s: %s", manifestPath, index, field, err.Error(), index, field, example) } - return fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.actions[%d].%s %s", manifestPath, index, field, err.Error()) + return fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].%s %s", manifestPath, index, field, err.Error()) } func manifestBootstrapFieldExample(field string) (string, bool) { diff --git a/pkg/cli/bootstrap_profile_runner.go b/pkg/cli/bootstrap_profile_runner.go index db69b28e978..dab6e12e5c3 100644 --- a/pkg/cli/bootstrap_profile_runner.go +++ b/pkg/cli/bootstrap_profile_runner.go @@ -108,9 +108,9 @@ func buildBootstrapProfilePlan(ctx context.Context, repo string, profile *resolv return false, nil, nil } - lines := make([]string, 0, len(profile.Profile.Actions)) + lines := make([]string, 0, len(profile.Profile.Config)) if !repoReady { - for _, action := range profile.Profile.Actions { + for _, action := range profile.Profile.Config { if err := validateBootstrapActionPreRepo(ctx, repo, action); err != nil { return false, nil, err } @@ -131,7 +131,7 @@ func buildBootstrapProfilePlan(ctx context.Context, repo string, profile *resolv } needsMutation := false - for _, action := range profile.Profile.Actions { + for _, action := range profile.Profile.Config { pending, err := bootstrapActionNeedsMutation(ctx, repo, action, state, usesActionsToken) if err != nil { return false, nil, err @@ -159,7 +159,7 @@ func executeBootstrapProfile(ctx context.Context, config bootstrapProfileRunConf return err } - for _, action := range config.Profile.Profile.Actions { + for _, action := range config.Profile.Profile.Config { pending, err := bootstrapActionNeedsMutation(ctx, config.Repo, action, state, usesActionsToken) if err != nil { return err @@ -305,7 +305,7 @@ func runBootstrapRequireOwnerType(ctx context.Context, repo string, action repos } normalized := normalizeSetupOwnerType(ownerType) if action.Value != "" && action.Value != "any" && normalized != action.Value { - return fmt.Errorf("owner %s is %s, but bootstrap profile requires %s. Example: set bootstrap.actions[].value to %s or use a repository owned by a matching account type", owner, normalized, action.Value, normalized) + return fmt.Errorf("owner %s is %s, but bootstrap profile requires %s. Example: set bootstrap.config[].value to %s or use a repository owned by a matching account type", owner, normalized, action.Value, normalized) } return nil } diff --git a/pkg/cli/bootstrap_test.go b/pkg/cli/bootstrap_test.go index 59223ca514c..e23fb995c89 100644 --- a/pkg/cli/bootstrap_test.go +++ b/pkg/cli/bootstrap_test.go @@ -141,7 +141,7 @@ func TestBuildBootstrapPlan_WithBootstrapProfileNeedsAction(t *testing.T) { return &resolvedBootstrapProfile{ PackageID: "github/central-agentic-ops", Profile: &repositoryPackageBootstrap{ - Actions: []repositoryPackageBootstrapAction{{Type: "repo-variable", Name: "CENTRAL_AGENTIC_OPS_MODE"}}, + Config: []repositoryPackageBootstrapAction{{Type: "repo-variable", Name: "CENTRAL_AGENTIC_OPS_MODE"}}, }, }, nil }, @@ -197,7 +197,7 @@ func TestRunBootstrapWithRuntime_ExecutesBootstrapProfile(t *testing.T) { return &resolvedBootstrapProfile{ PackageID: "github/central-agentic-ops", Profile: &repositoryPackageBootstrap{ - Actions: []repositoryPackageBootstrapAction{{Type: "handoff", Message: "Run readiness"}}, + Config: []repositoryPackageBootstrapAction{{Type: "handoff", Message: "Run readiness"}}, }, }, nil }, @@ -245,7 +245,7 @@ func TestBuildBootstrapPlan_CreateRepoProfileValidatesRequireOwnerType(t *testin return &resolvedBootstrapProfile{ PackageID: "github/central-agentic-ops", Profile: &repositoryPackageBootstrap{ - Actions: []repositoryPackageBootstrapAction{{Type: "require-owner-type", Value: "user"}}, + Config: []repositoryPackageBootstrapAction{{Type: "require-owner-type", Value: "user"}}, }, }, nil }, @@ -292,7 +292,7 @@ func TestResolveBootstrapProfileFromSources_IgnoresNestedWorkflowWithoutManifest func TestParseRepositoryPackageManifest_RejectsUnsupportedBootstrapWhen(t *testing.T) { _, _, err := parseRepositoryPackageManifest("aw.yml", []byte(`name: Control Plane bootstrap: - actions: + config: - type: handoff message: run readiness when: @@ -302,7 +302,9 @@ bootstrap: if err == nil { t.Fatal("expected unsupported bootstrap when error") } - if !strings.Contains(err.Error(), "bootstrap.actions[0].when is not supported yet") { + // The strict anyOf schema rejects unknown fields before the extractor runs. + // Accept errors that mention "when" in any form. + if !strings.Contains(err.Error(), "when") { t.Fatalf("unexpected error: %v", err) } } @@ -310,7 +312,7 @@ bootstrap: func TestParseRepositoryPackageManifest_GitHubAppFields(t *testing.T) { manifest, _, err := parseRepositoryPackageManifest("aw.yml", []byte(`name: Control Plane bootstrap: - actions: + config: - type: github-app app-id-variable: APP_ID private-key-secret: APP_PRIVATE_KEY @@ -320,10 +322,10 @@ bootstrap: if err != nil { t.Fatalf("parseRepositoryPackageManifest returned error: %v", err) } - if manifest.Bootstrap == nil || len(manifest.Bootstrap.Actions) != 1 { + if manifest.Bootstrap == nil || len(manifest.Bootstrap.Config) != 1 { t.Fatalf("expected one bootstrap action, got %#v", manifest.Bootstrap) } - action := manifest.Bootstrap.Actions[0] + action := manifest.Bootstrap.Config[0] if action.AppName != "Control Plane Bootstrap" { t.Fatalf("expected app-name to populate AppName, got %q", action.AppName) } @@ -335,7 +337,7 @@ bootstrap: func TestParseRepositoryPackageManifest_GitHubAppLegacyNameBackfillsAppName(t *testing.T) { manifest, _, err := parseRepositoryPackageManifest("aw.yml", []byte(`name: Control Plane bootstrap: - actions: + config: - type: github-app name: Legacy Bootstrap App app-id-variable: APP_ID @@ -344,7 +346,7 @@ bootstrap: if err != nil { t.Fatalf("parseRepositoryPackageManifest returned error: %v", err) } - action := manifest.Bootstrap.Actions[0] + action := manifest.Bootstrap.Config[0] if action.AppName != "Legacy Bootstrap App" { t.Fatalf("expected legacy name to backfill AppName, got %q", action.AppName) } @@ -408,7 +410,7 @@ func TestResolveBootstrapProfileFromSources(t *testing.T) { downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": - return []byte("name: Control Plane\nbootstrap:\n actions:\n - type: handoff\n message: Run readiness\n"), nil + return []byte("name: Control Plane\nbootstrap:\n config:\n - type: handoff\n message: Run readiness\n"), nil case "README.md": return []byte("# Control Plane\n"), nil default: @@ -426,15 +428,15 @@ func TestResolveBootstrapProfileFromSources(t *testing.T) { if profile.PackageID != "github/central-agentic-ops" { t.Fatalf("unexpected package id: %s", profile.PackageID) } - if len(profile.Profile.Actions) != 1 { - t.Fatalf("unexpected action count: %d", len(profile.Profile.Actions)) + if len(profile.Profile.Config) != 1 { + t.Fatalf("unexpected action count: %d", len(profile.Profile.Config)) } }) t.Run("returns local package bootstrap profile", func(t *testing.T) { packageDir := t.TempDir() manifestPath := filepath.Join(packageDir, "aw.yml") - manifest := []byte("name: Control Plane\nbootstrap:\n actions:\n - type: handoff\n message: Run readiness\n") + manifest := []byte("name: Control Plane\nbootstrap:\n config:\n - type: handoff\n message: Run readiness\n") if err := os.WriteFile(manifestPath, manifest, 0o644); err != nil { t.Fatalf("write manifest: %v", err) } @@ -452,8 +454,8 @@ func TestResolveBootstrapProfileFromSources(t *testing.T) { if profile.PackageID != filepath.Clean(packageDir) { t.Fatalf("unexpected package id: %s", profile.PackageID) } - if len(profile.Profile.Actions) != 1 { - t.Fatalf("unexpected action count: %d", len(profile.Profile.Actions)) + if len(profile.Profile.Config) != 1 { + t.Fatalf("unexpected action count: %d", len(profile.Profile.Config)) } }) @@ -461,9 +463,9 @@ func TestResolveBootstrapProfileFromSources(t *testing.T) { downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": - return []byte("name: Root\nbootstrap:\n actions:\n - type: handoff\n message: one\n"), nil + return []byte("name: Root\nbootstrap:\n config:\n - type: handoff\n message: one\n"), nil case "readiness/aw.yml": - return []byte("name: Readiness\nbootstrap:\n actions:\n - type: handoff\n message: two\n"), nil + return []byte("name: Readiness\nbootstrap:\n config:\n - type: handoff\n message: two\n"), nil case "README.md", "readiness/README.md": return []byte("# Package\n"), nil default: @@ -503,7 +505,7 @@ func TestBuildBootstrapPlan_CreateRepoProfileDoesNotAssumeRepoExists(t *testing. return &resolvedBootstrapProfile{ PackageID: "github/central-agentic-ops", Profile: &repositoryPackageBootstrap{ - Actions: []repositoryPackageBootstrapAction{{Type: "repo-variable", Name: "CENTRAL_AGENTIC_OPS_MODE"}}, + Config: []repositoryPackageBootstrapAction{{Type: "repo-variable", Name: "CENTRAL_AGENTIC_OPS_MODE"}}, }, }, nil }, @@ -554,8 +556,9 @@ func TestRunBootstrapWithRuntime_CreateCloneInitAddCompile(t *testing.T) { }, checkCleanWorktree: func(bool) error { return nil }, }, - confirmAction: func(string, string, string) (bool, error) { return false, nil }, - initRepo: func(InitOptions) error { initCalls++; return nil }, + confirmAction: func(string, string, string) (bool, error) { return false, nil }, + initRepo: func(InitOptions) error { initCalls++; return nil }, + resolveProfile: func(context.Context, []string) (*resolvedBootstrapProfile, error) { return nil, nil }, addWorkflows: func(context.Context, []string, AddOptions) (*AddWorkflowsResult, error) { addCalls++ return &AddWorkflowsResult{}, nil @@ -658,10 +661,8 @@ func TestRunBootstrapWithRuntime_AddWorkflowFailureAfterInitIncludesRecoveryHint dirOriginRepo: func(string) (string, error) { return "octo/platform-ops", nil }, checkCleanWorktree: func(bool) error { return nil }, }, - initRepo: func(InitOptions) error { - initCalled = true - return nil - }, + initRepo: func(InitOptions) error { initCalled = true; return nil }, + resolveProfile: func(context.Context, []string) (*resolvedBootstrapProfile, error) { return nil, nil }, addWorkflows: func(context.Context, []string, AddOptions) (*AddWorkflowsResult, error) { return nil, addErr }, @@ -750,6 +751,7 @@ func TestRunBootstrapWithRuntime_SkipsExistingSourcedWorkflow(t *testing.T) { dirOriginRepo: func(string) (string, error) { return "octo/platform-ops", nil }, checkCleanWorktree: func(bool) error { return nil }, }, + resolveProfile: func(context.Context, []string) (*resolvedBootstrapProfile, error) { return nil, nil }, }, repoDir) if err != nil { t.Fatalf("buildBootstrapPlan returned error: %v", err) @@ -781,7 +783,8 @@ func TestRunBootstrapWithRuntime_SkipsExistingSourcedWorkflow(t *testing.T) { dirOriginRepo: func(string) (string, error) { return "octo/platform-ops", nil }, checkCleanWorktree: func(bool) error { return nil }, }, - initRepo: func(InitOptions) error { initCalls++; return nil }, + initRepo: func(InitOptions) error { initCalls++; return nil }, + resolveProfile: func(context.Context, []string) (*resolvedBootstrapProfile, error) { return nil, nil }, addWorkflows: func(context.Context, []string, AddOptions) (*AddWorkflowsResult, error) { addCalls++ return &AddWorkflowsResult{}, nil diff --git a/pkg/parser/schemas/aw_manifest_schema.json b/pkg/parser/schemas/aw_manifest_schema.json index a1e8454c688..0d8453585f0 100644 --- a/pkg/parser/schemas/aw_manifest_schema.json +++ b/pkg/parser/schemas/aw_manifest_schema.json @@ -43,21 +43,96 @@ "bootstrap": { "type": "object", "additionalProperties": false, - "required": ["actions"], + "required": ["config"], "properties": { - "actions": { + "config": { "type": "array", "minItems": 1, "items": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "minLength": 1 + "anyOf": [ + { + "description": "require-owner-type: enforce a repository owner type constraint", + "type": "object", + "required": ["type", "value"], + "additionalProperties": false, + "properties": { + "type": { "type": "string", "const": "require-owner-type" }, + "owner": { "type": "string", "const": "repo" }, + "value": { "type": "string", "enum": ["any", "org", "user"] } + } + }, + { + "description": "repo-variable: prompt for a repository variable value", + "type": "object", + "required": ["type", "name", "prompt"], + "additionalProperties": false, + "properties": { + "type": { "type": "string", "const": "repo-variable" }, + "name": { "type": "string", "minLength": 1 }, + "prompt": { "type": "string", "minLength": 1 }, + "description": { "type": "string" }, + "default": { "type": "string" }, + "optional": { "type": "boolean" }, + "enum": { "type": "array", "items": { "type": "string" } } + } + }, + { + "description": "repo-secret: prompt for a repository secret value", + "type": "object", + "required": ["type", "name", "prompt"], + "additionalProperties": false, + "properties": { + "type": { "type": "string", "const": "repo-secret" }, + "name": { "type": "string", "minLength": 1 }, + "prompt": { "type": "string", "minLength": 1 }, + "description": { "type": "string" }, + "optional": { "type": "boolean" } + } + }, + { + "description": "github-app: configure GitHub App credentials", + "type": "object", + "required": ["type", "app-id-variable", "private-key-secret"], + "additionalProperties": false, + "properties": { + "type": { "type": "string", "const": "github-app" }, + "owner": { "type": "string", "const": "repo" }, + "name": { "type": "string" }, + "app-id-variable": { "type": "string", "minLength": 1 }, + "private-key-secret": { "type": "string", "minLength": 1 }, + "app-name": { "type": "string" }, + "homepage-url": { "type": "string" }, + "mode": { "type": "string", "enum": ["create-or-existing", "existing"] }, + "existing-only": { "type": "boolean" }, + "permissions": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "events": { "type": "array", "items": { "type": "string" } } + } + }, + { + "description": "copilot-auth: configure Copilot authentication", + "type": "object", + "required": ["type"], + "additionalProperties": false, + "properties": { + "type": { "type": "string", "const": "copilot-auth" }, + "secret": { "type": "string" }, + "strategy": { "type": "string", "const": "prompt-if-actions-auth-unavailable" } + } + }, + { + "description": "handoff: display a message and hand off to the user", + "type": "object", + "required": ["type", "message"], + "additionalProperties": false, + "properties": { + "type": { "type": "string", "const": "handoff" }, + "message": { "type": "string", "minLength": 1 } + } } - }, - "additionalProperties": true + ] } } } From 508fad31bb2080292c05fa9370167540821fec18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:34:18 +0000 Subject: [PATCH 2/7] feat: flatten bootstrap.config to top-level config in aw.yml manifest Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/add_package_manifest.go | 6 +- pkg/cli/add_package_manifest_test.go | 25 +- pkg/cli/add_workflow_resolution.go | 2 +- pkg/cli/bootstrap_config.go | 2 +- pkg/cli/bootstrap_profile_manifest.go | 54 ++-- pkg/cli/bootstrap_profile_runner.go | 2 +- pkg/cli/bootstrap_test.go | 45 ++-- pkg/parser/schemas/aw_manifest_schema.json | 273 +++++++++++++-------- 8 files changed, 237 insertions(+), 172 deletions(-) diff --git a/pkg/cli/add_package_manifest.go b/pkg/cli/add_package_manifest.go index 95634f46dc0..f77d7aa89db 100644 --- a/pkg/cli/add_package_manifest.go +++ b/pkg/cli/add_package_manifest.go @@ -294,9 +294,9 @@ func parseRepositoryPackageManifest(manifestPath string, content []byte) (*repos warnings = append(warnings, agentWarnings...) } - if bootstrapValue, ok := root["bootstrap"]; ok { - warnings = append(warnings, "Using experimental feature: manifest.bootstrap") - bootstrap, err := extractManifestBootstrap(bootstrapValue, manifestPath) + if configValue, ok := root["config"]; ok { + warnings = append(warnings, "Using experimental feature: manifest.config") + bootstrap, err := extractManifestBootstrap(configValue, manifestPath) if err != nil { return nil, nil, err } diff --git a/pkg/cli/add_package_manifest_test.go b/pkg/cli/add_package_manifest_test.go index af31355fc03..ff5b7e9d2a7 100644 --- a/pkg/cli/add_package_manifest_test.go +++ b/pkg/cli/add_package_manifest_test.go @@ -448,18 +448,17 @@ files: switch path { case "aw.yml": return []byte(`name: Repo Assist -bootstrap: - config: - - type: require-owner-type - owner: repo - value: org - - type: repo-variable - name: CENTRAL_AGENTIC_OPS_MODE - prompt: Rollout mode - default: preview - enum: [preview, review, live] - - type: handoff - message: Run gh aw run readiness. +config: + - type: require-owner-type + owner: repo + value: org + - type: repo-variable + name: CENTRAL_AGENTIC_OPS_MODE + prompt: Rollout mode + default: preview + enum: [preview, review, live] + - type: handoff + message: Run gh aw run readiness. `), nil case "README.md": return []byte("# Repo Assist\n"), nil @@ -486,7 +485,7 @@ bootstrap: assert.Equal(t, "repo-variable", pkg.Bootstrap.Config[1].Type) assert.Equal(t, []string{"preview", "review", "live"}, pkg.Bootstrap.Config[1].Enum) assert.Equal(t, "handoff", pkg.Bootstrap.Config[2].Type) - assert.Contains(t, pkg.Warnings, "Using experimental feature: manifest.bootstrap") + assert.Contains(t, pkg.Warnings, "Using experimental feature: manifest.config") }) t.Run("rejects unsupported branding icon", func(t *testing.T) { diff --git a/pkg/cli/add_workflow_resolution.go b/pkg/cli/add_workflow_resolution.go index 7da15acf1b1..5046dc1f66b 100644 --- a/pkg/cli/add_workflow_resolution.go +++ b/pkg/cli/add_workflow_resolution.go @@ -59,7 +59,7 @@ type ResolvedWorkflows struct { // Warnings contains non-fatal package-resolution warnings to show during add Warnings []string // BootstrapProfile holds the bootstrap profile from an aw.yml package manifest, - // when exactly one source package declares a bootstrap.config section. + // when exactly one source package declares a config section. // Used by add (non-interactive TODO list) and add-wizard (interactive setup). BootstrapProfile *resolvedBootstrapProfile } diff --git a/pkg/cli/bootstrap_config.go b/pkg/cli/bootstrap_config.go index 6dac20624fb..9bf74d3adc5 100644 --- a/pkg/cli/bootstrap_config.go +++ b/pkg/cli/bootstrap_config.go @@ -9,7 +9,7 @@ import ( ) // printBootstrapConfigTODO prints a TODO checklist of manual steps required by the -// bootstrap.config entries in the package manifest. Called by the non-interactive +// config entries in the package manifest. Called by the non-interactive // "add" command after workflows have been installed. func printBootstrapConfigTODO(profile *resolvedBootstrapProfile) { if profile == nil || profile.Profile == nil || len(profile.Profile.Config) == 0 { diff --git a/pkg/cli/bootstrap_profile_manifest.go b/pkg/cli/bootstrap_profile_manifest.go index d5416d8d4a5..0c9f967c038 100644 --- a/pkg/cli/bootstrap_profile_manifest.go +++ b/pkg/cli/bootstrap_profile_manifest.go @@ -181,34 +181,24 @@ func localBootstrapManifestPath(resolvedPath string) (string, string, error) { } func extractManifestBootstrap(value any, manifestPath string) (*repositoryPackageBootstrap, error) { - root, ok := value.(map[string]any) - if !ok { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap must be a mapping. Example: bootstrap: { config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) - } - - configValue, ok := root["config"] - if !ok { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config is required. Example: bootstrap: { config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) - } - - configItems, ok := configValue.([]any) + configItems, ok := value.([]any) if !ok { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config must be a list. Example: bootstrap: { config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: config must be a list. Example: config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }]", manifestPath) } if len(configItems) == 0 { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config must not be empty. Example: bootstrap: { config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }] }", manifestPath) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: config must not be empty. Example: config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }]", manifestPath) } bootstrap := &repositoryPackageBootstrap{} for index, item := range configItems { actionMap, ok := item.(map[string]any) if !ok { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d] must be a mapping. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d] must be a mapping. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) } actionType, ok := stringValue(actionMap["type"]) if !ok || strings.TrimSpace(actionType) == "" { - return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].type must be a non-empty string. Example: type: repo-variable", manifestPath, index) + return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].type must be a non-empty string. Example: type: repo-variable", manifestPath, index) } action, err := parseManifestBootstrapAction(strings.TrimSpace(actionType), actionMap, manifestPath, index) @@ -283,7 +273,7 @@ func parseManifestBootstrapAction(actionType string, actionMap map[string]any, m action.Events = events } if _, exists := actionMap["when"]; exists { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].when is not supported yet. Example: remove the when field and keep only supported keys such as type, name, and prompt", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].when is not supported yet. Example: remove the when field and keep only supported keys such as type, name, and prompt", manifestPath, index) } if permissionsValue, exists := actionMap["permissions"]; exists { permissions, err := stringMapValue(permissionsValue) @@ -296,31 +286,31 @@ func parseManifestBootstrapAction(actionType string, actionMap map[string]any, m switch actionType { case "require-owner-type": if action.Owner != "" && action.Owner != "repo" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].owner must be 'repo' when type=require-owner-type. Example: { type: require-owner-type, owner: repo, value: org }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].owner must be 'repo' when type=require-owner-type. Example: { type: require-owner-type, owner: repo, value: org }", manifestPath, index) } if action.Value != "any" && action.Value != "org" && action.Value != "user" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].value must be one of: any, org, user. Example: { type: require-owner-type, value: org }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].value must be one of: any, org, user. Example: { type: require-owner-type, value: org }", manifestPath, index) } case "repo-variable": if action.Name == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].name is required when type=repo-variable. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].name is required when type=repo-variable. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) } if action.Prompt == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].prompt is required when type=repo-variable. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].prompt is required when type=repo-variable. Example: { type: repo-variable, name: EXAMPLE, prompt: Enter a value }", manifestPath, index) } case "repo-secret": if action.Name == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].name is required when type=repo-secret. Example: { type: repo-secret, name: EXAMPLE_SECRET, prompt: Enter a secret }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].name is required when type=repo-secret. Example: { type: repo-secret, name: EXAMPLE_SECRET, prompt: Enter a secret }", manifestPath, index) } if action.Prompt == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].prompt is required when type=repo-secret. Example: { type: repo-secret, name: EXAMPLE_SECRET, prompt: Enter a secret }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].prompt is required when type=repo-secret. Example: { type: repo-secret, name: EXAMPLE_SECRET, prompt: Enter a secret }", manifestPath, index) } case "github-app": if action.AppName == "" && action.Name != "" { action.AppName = action.Name } if action.ExistingOnly && action.Mode != "" && action.Mode != "existing" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].existing-only requires mode to be 'existing' or unset. Remove mode=%q or set it to 'existing'", manifestPath, index, action.Mode) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].existing-only requires mode to be 'existing' or unset. Remove mode=%q or set it to 'existing'", manifestPath, index, action.Mode) } if action.ExistingOnly && action.Mode == "" { action.Mode = "existing" @@ -329,16 +319,16 @@ func parseManifestBootstrapAction(actionType string, actionMap map[string]any, m action.Mode = "create-or-existing" } if action.Mode != "create-or-existing" && action.Mode != "existing" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].mode must be one of: create-or-existing, existing. Example: { type: github-app, mode: existing, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].mode must be one of: create-or-existing, existing. Example: { type: github-app, mode: existing, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) } if action.Owner != "" && action.Owner != "repo" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].owner must be 'repo' when type=github-app. Example: { type: github-app, owner: repo, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].owner must be 'repo' when type=github-app. Example: { type: github-app, owner: repo, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) } if action.AppIDVariable == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].app-id-variable is required when type=github-app. Example: { type: github-app, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].app-id-variable is required when type=github-app. Example: { type: github-app, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) } if action.PrivateKeySecret == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].private-key-secret is required when type=github-app. Example: { type: github-app, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].private-key-secret is required when type=github-app. Example: { type: github-app, app-id-variable: APP_ID, private-key-secret: APP_PRIVATE_KEY }", manifestPath, index) } case "copilot-auth": if action.Secret == "" { @@ -348,14 +338,14 @@ func parseManifestBootstrapAction(actionType string, actionMap map[string]any, m action.Strategy = "prompt-if-actions-auth-unavailable" } if action.Strategy != "prompt-if-actions-auth-unavailable" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].strategy must be 'prompt-if-actions-auth-unavailable'. Example: { type: copilot-auth, strategy: prompt-if-actions-auth-unavailable }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].strategy must be 'prompt-if-actions-auth-unavailable'. Example: { type: copilot-auth, strategy: prompt-if-actions-auth-unavailable }", manifestPath, index) } case "handoff": if action.Message == "" { - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].message is required when type=handoff. Example: { type: handoff, message: Continue with repository-specific setup. }", manifestPath, index) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].message is required when type=handoff. Example: { type: handoff, message: Continue with repository-specific setup. }", manifestPath, index) } default: - return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].type %q is not supported. Example: use one of %s", manifestPath, index, actionType, bootstrapActionTypeExample) + return repositoryPackageBootstrapAction{}, fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].type %q is not supported. Example: use one of %s", manifestPath, index, actionType, bootstrapActionTypeExample) } return action, nil @@ -401,9 +391,9 @@ func stringMapValue(value any) (map[string]string, error) { func manifestBootstrapFieldError(manifestPath string, index int, field string, err error) error { if example, ok := manifestBootstrapFieldExample(field); ok { - return fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].%s %s. Example: bootstrap.config[%d].%s: %s", manifestPath, index, field, err.Error(), index, field, example) + return fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].%s %s. Example: config[%d].%s: %s", manifestPath, index, field, err.Error(), index, field, example) } - return fmt.Errorf("invalid Agentic Workflow manifest %q: bootstrap.config[%d].%s %s", manifestPath, index, field, err.Error()) + return fmt.Errorf("invalid Agentic Workflow manifest %q: config[%d].%s %s", manifestPath, index, field, err.Error()) } func manifestBootstrapFieldExample(field string) (string, bool) { diff --git a/pkg/cli/bootstrap_profile_runner.go b/pkg/cli/bootstrap_profile_runner.go index dab6e12e5c3..8113b07cc01 100644 --- a/pkg/cli/bootstrap_profile_runner.go +++ b/pkg/cli/bootstrap_profile_runner.go @@ -305,7 +305,7 @@ func runBootstrapRequireOwnerType(ctx context.Context, repo string, action repos } normalized := normalizeSetupOwnerType(ownerType) if action.Value != "" && action.Value != "any" && normalized != action.Value { - return fmt.Errorf("owner %s is %s, but bootstrap profile requires %s. Example: set bootstrap.config[].value to %s or use a repository owned by a matching account type", owner, normalized, action.Value, normalized) + return fmt.Errorf("owner %s is %s, but bootstrap profile requires %s. Example: set config[].value to %s or use a repository owned by a matching account type", owner, normalized, action.Value, normalized) } return nil } diff --git a/pkg/cli/bootstrap_test.go b/pkg/cli/bootstrap_test.go index e23fb995c89..8a5faad0495 100644 --- a/pkg/cli/bootstrap_test.go +++ b/pkg/cli/bootstrap_test.go @@ -291,13 +291,12 @@ func TestResolveBootstrapProfileFromSources_IgnoresNestedWorkflowWithoutManifest func TestParseRepositoryPackageManifest_RejectsUnsupportedBootstrapWhen(t *testing.T) { _, _, err := parseRepositoryPackageManifest("aw.yml", []byte(`name: Control Plane -bootstrap: - config: - - type: handoff - message: run readiness - when: - variable: MODE - equals: prod +config: + - type: handoff + message: run readiness + when: + variable: MODE + equals: prod `)) if err == nil { t.Fatal("expected unsupported bootstrap when error") @@ -311,13 +310,12 @@ bootstrap: func TestParseRepositoryPackageManifest_GitHubAppFields(t *testing.T) { manifest, _, err := parseRepositoryPackageManifest("aw.yml", []byte(`name: Control Plane -bootstrap: - config: - - type: github-app - app-id-variable: APP_ID - private-key-secret: APP_PRIVATE_KEY - app-name: Control Plane Bootstrap - existing-only: true +config: + - type: github-app + app-id-variable: APP_ID + private-key-secret: APP_PRIVATE_KEY + app-name: Control Plane Bootstrap + existing-only: true `)) if err != nil { t.Fatalf("parseRepositoryPackageManifest returned error: %v", err) @@ -336,12 +334,11 @@ bootstrap: func TestParseRepositoryPackageManifest_GitHubAppLegacyNameBackfillsAppName(t *testing.T) { manifest, _, err := parseRepositoryPackageManifest("aw.yml", []byte(`name: Control Plane -bootstrap: - config: - - type: github-app - name: Legacy Bootstrap App - app-id-variable: APP_ID - private-key-secret: APP_PRIVATE_KEY +config: + - type: github-app + name: Legacy Bootstrap App + app-id-variable: APP_ID + private-key-secret: APP_PRIVATE_KEY `)) if err != nil { t.Fatalf("parseRepositoryPackageManifest returned error: %v", err) @@ -410,7 +407,7 @@ func TestResolveBootstrapProfileFromSources(t *testing.T) { downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": - return []byte("name: Control Plane\nbootstrap:\n config:\n - type: handoff\n message: Run readiness\n"), nil + return []byte("name: Control Plane\nconfig:\n - type: handoff\n message: Run readiness\n"), nil case "README.md": return []byte("# Control Plane\n"), nil default: @@ -436,7 +433,7 @@ func TestResolveBootstrapProfileFromSources(t *testing.T) { t.Run("returns local package bootstrap profile", func(t *testing.T) { packageDir := t.TempDir() manifestPath := filepath.Join(packageDir, "aw.yml") - manifest := []byte("name: Control Plane\nbootstrap:\n config:\n - type: handoff\n message: Run readiness\n") + manifest := []byte("name: Control Plane\nconfig:\n - type: handoff\n message: Run readiness\n") if err := os.WriteFile(manifestPath, manifest, 0o644); err != nil { t.Fatalf("write manifest: %v", err) } @@ -463,9 +460,9 @@ func TestResolveBootstrapProfileFromSources(t *testing.T) { downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { switch path { case "aw.yml": - return []byte("name: Root\nbootstrap:\n config:\n - type: handoff\n message: one\n"), nil + return []byte("name: Root\nconfig:\n - type: handoff\n message: one\n"), nil case "readiness/aw.yml": - return []byte("name: Readiness\nbootstrap:\n config:\n - type: handoff\n message: two\n"), nil + return []byte("name: Readiness\nconfig:\n - type: handoff\n message: two\n"), nil case "README.md", "readiness/README.md": return []byte("# Package\n"), nil default: diff --git a/pkg/parser/schemas/aw_manifest_schema.json b/pkg/parser/schemas/aw_manifest_schema.json index 0d8453585f0..91e211dbd22 100644 --- a/pkg/parser/schemas/aw_manifest_schema.json +++ b/pkg/parser/schemas/aw_manifest_schema.json @@ -40,103 +40,6 @@ "type": "string" } }, - "bootstrap": { - "type": "object", - "additionalProperties": false, - "required": ["config"], - "properties": { - "config": { - "type": "array", - "minItems": 1, - "items": { - "anyOf": [ - { - "description": "require-owner-type: enforce a repository owner type constraint", - "type": "object", - "required": ["type", "value"], - "additionalProperties": false, - "properties": { - "type": { "type": "string", "const": "require-owner-type" }, - "owner": { "type": "string", "const": "repo" }, - "value": { "type": "string", "enum": ["any", "org", "user"] } - } - }, - { - "description": "repo-variable: prompt for a repository variable value", - "type": "object", - "required": ["type", "name", "prompt"], - "additionalProperties": false, - "properties": { - "type": { "type": "string", "const": "repo-variable" }, - "name": { "type": "string", "minLength": 1 }, - "prompt": { "type": "string", "minLength": 1 }, - "description": { "type": "string" }, - "default": { "type": "string" }, - "optional": { "type": "boolean" }, - "enum": { "type": "array", "items": { "type": "string" } } - } - }, - { - "description": "repo-secret: prompt for a repository secret value", - "type": "object", - "required": ["type", "name", "prompt"], - "additionalProperties": false, - "properties": { - "type": { "type": "string", "const": "repo-secret" }, - "name": { "type": "string", "minLength": 1 }, - "prompt": { "type": "string", "minLength": 1 }, - "description": { "type": "string" }, - "optional": { "type": "boolean" } - } - }, - { - "description": "github-app: configure GitHub App credentials", - "type": "object", - "required": ["type", "app-id-variable", "private-key-secret"], - "additionalProperties": false, - "properties": { - "type": { "type": "string", "const": "github-app" }, - "owner": { "type": "string", "const": "repo" }, - "name": { "type": "string" }, - "app-id-variable": { "type": "string", "minLength": 1 }, - "private-key-secret": { "type": "string", "minLength": 1 }, - "app-name": { "type": "string" }, - "homepage-url": { "type": "string" }, - "mode": { "type": "string", "enum": ["create-or-existing", "existing"] }, - "existing-only": { "type": "boolean" }, - "permissions": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "events": { "type": "array", "items": { "type": "string" } } - } - }, - { - "description": "copilot-auth: configure Copilot authentication", - "type": "object", - "required": ["type"], - "additionalProperties": false, - "properties": { - "type": { "type": "string", "const": "copilot-auth" }, - "secret": { "type": "string" }, - "strategy": { "type": "string", "const": "prompt-if-actions-auth-unavailable" } - } - }, - { - "description": "handoff: display a message and hand off to the user", - "type": "object", - "required": ["type", "message"], - "additionalProperties": false, - "properties": { - "type": { "type": "string", "const": "handoff" }, - "message": { "type": "string", "minLength": 1 } - } - } - ] - } - } - } - }, "skills": { "type": "array", "description": "Skill directory paths relative to the package root. Each entry is a directory that must contain a SKILL.md file; all direct files in the directory are installed to the agentic engine skill folder.", @@ -424,6 +327,182 @@ } }, "additionalProperties": false + }, + "config": { + "type": "array", + "minItems": 1, + "items": { + "anyOf": [ + { + "description": "require-owner-type: enforce a repository owner type constraint", + "type": "object", + "required": ["type", "value"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "require-owner-type" + }, + "owner": { + "type": "string", + "const": "repo" + }, + "value": { + "type": "string", + "enum": ["any", "org", "user"] + } + } + }, + { + "description": "repo-variable: prompt for a repository variable value", + "type": "object", + "required": ["type", "name", "prompt"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "repo-variable" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "prompt": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "default": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "enum": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + { + "description": "repo-secret: prompt for a repository secret value", + "type": "object", + "required": ["type", "name", "prompt"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "repo-secret" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "prompt": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "optional": { + "type": "boolean" + } + } + }, + { + "description": "github-app: configure GitHub App credentials", + "type": "object", + "required": ["type", "app-id-variable", "private-key-secret"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "github-app" + }, + "owner": { + "type": "string", + "const": "repo" + }, + "name": { + "type": "string" + }, + "app-id-variable": { + "type": "string", + "minLength": 1 + }, + "private-key-secret": { + "type": "string", + "minLength": 1 + }, + "app-name": { + "type": "string" + }, + "homepage-url": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": ["create-or-existing", "existing"] + }, + "existing-only": { + "type": "boolean" + }, + "permissions": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + { + "description": "copilot-auth: configure Copilot authentication", + "type": "object", + "required": ["type"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "copilot-auth" + }, + "secret": { + "type": "string" + }, + "strategy": { + "type": "string", + "const": "prompt-if-actions-auth-unavailable" + } + } + }, + { + "description": "handoff: display a message and hand off to the user", + "type": "object", + "required": ["type", "message"], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "const": "handoff" + }, + "message": { + "type": "string", + "minLength": 1 + } + } + } + ] + } } } } From 1b48f85815e30c9567f315f4f9e9ab8cae490d85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:36:22 +0000 Subject: [PATCH 3/7] refactor: rename extractManifestBootstrap to extractManifestConfig; simplify warning message Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/add_package_manifest.go | 4 ++-- pkg/cli/add_package_manifest_test.go | 2 +- pkg/cli/bootstrap_profile_manifest.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cli/add_package_manifest.go b/pkg/cli/add_package_manifest.go index f77d7aa89db..62916e201fc 100644 --- a/pkg/cli/add_package_manifest.go +++ b/pkg/cli/add_package_manifest.go @@ -295,8 +295,8 @@ func parseRepositoryPackageManifest(manifestPath string, content []byte) (*repos } if configValue, ok := root["config"]; ok { - warnings = append(warnings, "Using experimental feature: manifest.config") - bootstrap, err := extractManifestBootstrap(configValue, manifestPath) + warnings = append(warnings, "Using experimental feature: config") + bootstrap, err := extractManifestConfig(configValue, manifestPath) if err != nil { return nil, nil, err } diff --git a/pkg/cli/add_package_manifest_test.go b/pkg/cli/add_package_manifest_test.go index ff5b7e9d2a7..429b13a9de6 100644 --- a/pkg/cli/add_package_manifest_test.go +++ b/pkg/cli/add_package_manifest_test.go @@ -485,7 +485,7 @@ config: assert.Equal(t, "repo-variable", pkg.Bootstrap.Config[1].Type) assert.Equal(t, []string{"preview", "review", "live"}, pkg.Bootstrap.Config[1].Enum) assert.Equal(t, "handoff", pkg.Bootstrap.Config[2].Type) - assert.Contains(t, pkg.Warnings, "Using experimental feature: manifest.config") + assert.Contains(t, pkg.Warnings, "Using experimental feature: config") }) t.Run("rejects unsupported branding icon", func(t *testing.T) { diff --git a/pkg/cli/bootstrap_profile_manifest.go b/pkg/cli/bootstrap_profile_manifest.go index 0c9f967c038..02b25f2498c 100644 --- a/pkg/cli/bootstrap_profile_manifest.go +++ b/pkg/cli/bootstrap_profile_manifest.go @@ -180,7 +180,7 @@ func localBootstrapManifestPath(resolvedPath string) (string, string, error) { return resolvedPath, filepath.Clean(filepath.Dir(resolvedPath)), nil } -func extractManifestBootstrap(value any, manifestPath string) (*repositoryPackageBootstrap, error) { +func extractManifestConfig(value any, manifestPath string) (*repositoryPackageBootstrap, error) { configItems, ok := value.([]any) if !ok { return nil, fmt.Errorf("invalid Agentic Workflow manifest %q: config must be a list. Example: config: [{ type: repo-variable, name: EXAMPLE, prompt: Enter a value }]", manifestPath) From 76f873c508c4cb7ce4c8be2adb0edf2f21cb3393 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:34:38 +0000 Subject: [PATCH 4/7] docs(adr): add draft ADR-45758 for extending bootstrap config to add/add-wizard commands --- ...tend-bootstrap-config-to-add-and-wizard.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/adr/45758-extend-bootstrap-config-to-add-and-wizard.md diff --git a/docs/adr/45758-extend-bootstrap-config-to-add-and-wizard.md b/docs/adr/45758-extend-bootstrap-config-to-add-and-wizard.md new file mode 100644 index 00000000000..e774c5b9401 --- /dev/null +++ b/docs/adr/45758-extend-bootstrap-config-to-add-and-wizard.md @@ -0,0 +1,50 @@ +# ADR-45758: Extend Bootstrap Config Execution to `add` and `add-wizard` Commands + +**Date**: 2026-07-15 +**Status**: Draft +**Deciders**: Unknown + +--- + +### Context + +The `gh aw` CLI has three workflow-installation commands: `bootstrap` (dedicated setup runner), `add` (non-interactive batch install), and `add-wizard` (interactive install wizard). Package manifests (`aw.yml`) can declare a `config` section listing post-installation steps (set a repository variable, configure a secret, install a GitHub App, etc.). Before this change, only the `bootstrap` command read and acted on those config steps. Users running `gh aw add` or `gh aw add-wizard` received no indication that post-installation setup was required, creating a silent gap: workflows were installed but the configuration steps needed for them to work were never surfaced or executed. + +### Decision + +We will surface and execute bootstrap config steps from all three install entry points. The `add` command will print a checklist of required manual steps after installation; the `add-wizard` command will execute the steps interactively via the existing `executeBootstrapProfile` runner. Shared helpers (`printBootstrapConfigTODO`, `executeBootstrapConfigForAdd`) are extracted into a new `bootstrap_config.go` file and called from both commands. Concurrently, the `aw.yml` manifest schema is simplified from the nested `bootstrap: { actions: [...] }` structure to a flat top-level `config: [...]` array with strict per-action-type `anyOf` validation, replacing the former `additionalProperties: true` permissiveness. + +### Alternatives Considered + +#### Alternative 1: Document `gh aw bootstrap` as a required follow-up step + +Users would be told in README/docs to run `gh aw bootstrap --repo OWNER/REPO` after `gh aw add`. The existing machinery would remain siloed in the `bootstrap` command. + +Not chosen because it relies on users reading external documentation; in practice many users will skip this step, leaving their workflows misconfigured. A nudge within the install flow (a TODO checklist or interactive prompt) is more reliable. + +#### Alternative 2: Duplicate bootstrap execution logic into each command + +Rather than extracting shared helpers, each command (add, add-wizard) would contain its own copy of the profile-running logic. + +Not chosen because duplication creates divergence risk: future changes to bootstrap behavior (new action types, error handling) would need to be applied in three places. Extracting to `bootstrap_config.go` keeps the three commands converged on a single execution path. + +### Consequences + +#### Positive +- Users installing workflows via `gh aw add` immediately see a TODO checklist of required post-install steps; they no longer need to know about the `bootstrap` subcommand. +- Users running `gh aw add-wizard` get interactive config setup as part of the wizard flow, reducing manual steps after PR creation. +- The new strict `anyOf` schema per action type rejects unknown fields at schema-validation time rather than silently accepting malformed manifests. +- All three commands converge on `executeBootstrapProfile`, so future changes to bootstrap execution propagate automatically. + +#### Negative +- **Breaking schema change**: any existing package manifests using the old `bootstrap: { actions: [...] }` structure must be updated to `config: [...]`. There is no migration shim or backward compatibility path. +- The `add` and `add-wizard` commands now carry a secondary responsibility (bootstrap config surfacing) beyond installing workflows, increasing cognitive load for maintainers of those code paths. +- When multiple packages are installed simultaneously and more than one declares a `config` section, bootstrap config is silently skipped for all with only a warning log — the user must install packages separately to apply their config. + +#### Neutral +- `ResolveWorkflows` now returns a `BootstrapProfile` field on `ResolvedWorkflows`, widening the surface of the resolution result type. +- The `add_command.go` flow is split from a single `AddWorkflows` call into explicit `ResolveWorkflows` + `AddResolvedWorkflows` stages to allow access to the resolved bootstrap profile before the add result is returned. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From e9b7ae20cfd6f336c99a3abf6ba7b282538d0782 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:06:22 +0000 Subject: [PATCH 5/7] fix: address review comments on bootstrap config in add/add-wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Show `☐` for require-owner-type in printBootstrapConfigTODO (cannot verify owner type without querying the repo at install time) - Gate interactive bootstrap execution in add-wizard on hasWriteAccess; fall back to printBootstrapConfigTODO when write access is absent so users without permissions see the manual checklist instead of hitting a permission error - Thread UseCopilotRequests through bootstrapProfileRunConfig and skip copilot-auth actions when org-billing is selected (matching the behavior of permissions.copilot-requests: write injection) - Add ResolveWorkflows tests covering BootstrapProfile propagation: single package populates the profile, multiple packages warn and suppress execution Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/add_interactive_orchestrator.go | 8 +- pkg/cli/add_package_manifest_test.go | 114 ++++++++++++++++++++++++ pkg/cli/bootstrap_config.go | 13 +-- pkg/cli/bootstrap_profile_runner.go | 8 ++ 4 files changed, 135 insertions(+), 8 deletions(-) diff --git a/pkg/cli/add_interactive_orchestrator.go b/pkg/cli/add_interactive_orchestrator.go index 6ac878b2b1a..babf1bf5424 100644 --- a/pkg/cli/add_interactive_orchestrator.go +++ b/pkg/cli/add_interactive_orchestrator.go @@ -166,8 +166,12 @@ func RunAddInteractive(ctx context.Context, config *AddInteractiveConfig) error // Step 9b: Apply bootstrap config steps interactively (if the package declares any) if config.resolvedWorkflows != nil && config.resolvedWorkflows.BootstrapProfile != nil { - if err := executeBootstrapConfigForAdd(ctx, config.RepoOverride, config.WorkflowSpecs, config.resolvedWorkflows.BootstrapProfile, config.Verbose); err != nil { - return err + if config.hasWriteAccess { + if err := executeBootstrapConfigForAdd(ctx, config.RepoOverride, config.WorkflowSpecs, config.resolvedWorkflows.BootstrapProfile, config.UseCopilotRequests, config.Verbose); err != nil { + return err + } + } else { + printBootstrapConfigTODO(config.resolvedWorkflows.BootstrapProfile) } } diff --git a/pkg/cli/add_package_manifest_test.go b/pkg/cli/add_package_manifest_test.go index 429b13a9de6..58f2250997d 100644 --- a/pkg/cli/add_package_manifest_test.go +++ b/pkg/cli/add_package_manifest_test.go @@ -2020,3 +2020,117 @@ func TestIsGhAwRepository(t *testing.T) { }) } } + +// bootstrapTestHelpers sets up the common mock functions used by bootstrap profile +// propagation tests and registers their cleanup. +func bootstrapTestHelpers(t *testing.T) { + t.Helper() + originalFetchFn := fetchWorkflowFromSourceWithContextFn + originalDownload := downloadPackageFileFromGitHubForHost + originalList := listPackageWorkflowFilesForHost + originalDirFiles := listPackageDirFilesForHost + originalDirSubdirs := listPackageDirSubdirsForHost + originalDefaultBranch := getRepositoryPackageDefaultBranch + t.Cleanup(func() { + fetchWorkflowFromSourceWithContextFn = originalFetchFn + downloadPackageFileFromGitHubForHost = originalDownload + listPackageWorkflowFilesForHost = originalList + listPackageDirFilesForHost = originalDirFiles + listPackageDirSubdirsForHost = originalDirSubdirs + getRepositoryPackageDefaultBranch = originalDefaultBranch + }) + + getRepositoryPackageDefaultBranch = func(repoSlug, host string) (string, error) { + return "main", nil + } + listPackageDirFilesForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { + return nil, createRepositoryPackageNotFoundError(dirPath) + } + listPackageDirSubdirsForHost = func(_ context.Context, owner, repo, ref, dirPath, host string) ([]string, error) { + return nil, createRepositoryPackageNotFoundError(dirPath) + } + listPackageWorkflowFilesForHost = func(_ context.Context, owner, repo, ref, workflowPath, host string) ([]string, error) { + t.Fatalf("unexpected scan of %s", workflowPath) + return nil, nil + } + fetchWorkflowFromSourceWithContextFn = func(_ context.Context, spec *WorkflowSpec, _ bool) (*FetchedWorkflow, error) { + return &FetchedWorkflow{ + Content: []byte("---\nname: Test\non: push\n---\n"), + CommitSHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + IsLocal: false, + SourcePath: spec.WorkflowPath, + }, nil + } +} + +func TestResolveWorkflows_BootstrapProfile_SinglePackage(t *testing.T) { + bootstrapTestHelpers(t) + + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { + switch path { + case "aw.yml": + return []byte(`name: My Package +files: + - workflows/review.md +config: + - type: repo-variable + name: MY_VAR + prompt: Enter a value +`), nil + case "README.md": + return []byte("# My Package\n"), nil + } + return nil, createRepositoryPackageNotFoundError(path) + } + + resolved, err := ResolveWorkflows(context.Background(), []string{"owner/repo"}, false) + require.NoError(t, err) + require.Len(t, resolved.Workflows, 1) + + require.NotNil(t, resolved.BootstrapProfile, "BootstrapProfile should be populated from the package config") + assert.Equal(t, "owner/repo", resolved.BootstrapProfile.PackageID) + require.Len(t, resolved.BootstrapProfile.Profile.Config, 1) + assert.Equal(t, "repo-variable", resolved.BootstrapProfile.Profile.Config[0].Type) + assert.Equal(t, "MY_VAR", resolved.BootstrapProfile.Profile.Config[0].Name) +} + +func TestResolveWorkflows_BootstrapProfile_MultiplePackagesWarnsAndSuppresses(t *testing.T) { + bootstrapTestHelpers(t) + + // Two separate repository packages, each declaring a config section. + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { + var pkgName, varName string + switch repo { + case "pkg-a": + pkgName, varName = "Package A", "VAR_A" + case "pkg-b": + pkgName, varName = "Package B", "VAR_B" + default: + return nil, createRepositoryPackageNotFoundError(path) + } + switch path { + case "aw.yml": + return []byte("name: " + pkgName + "\nfiles:\n - workflows/review.md\nconfig:\n - type: repo-variable\n name: " + varName + "\n prompt: Enter a value\n"), nil + case "README.md": + return []byte("# " + pkgName + "\n"), nil + } + return nil, createRepositoryPackageNotFoundError(path) + } + + resolved, err := ResolveWorkflows(context.Background(), []string{"owner/pkg-a", "owner/pkg-b"}, false) + require.NoError(t, err) + + assert.Nil(t, resolved.BootstrapProfile, "BootstrapProfile should be nil when multiple packages declare config") + + // Verify the multi-profile warning is present (other deprecation/experimental warnings may also be present) + found := false + for _, w := range resolved.Warnings { + if strings.Contains(w, "multiple bootstrap profiles found") { + assert.Contains(t, w, "owner/pkg-a") + assert.Contains(t, w, "owner/pkg-b") + found = true + break + } + } + assert.True(t, found, "expected a warning about multiple bootstrap profiles, got: %v", resolved.Warnings) +} diff --git a/pkg/cli/bootstrap_config.go b/pkg/cli/bootstrap_config.go index 9bf74d3adc5..43c834ab3ed 100644 --- a/pkg/cli/bootstrap_config.go +++ b/pkg/cli/bootstrap_config.go @@ -22,7 +22,7 @@ func printBootstrapConfigTODO(profile *resolvedBootstrapProfile) { for _, action := range profile.Profile.Config { switch action.Type { case "require-owner-type": - fmt.Fprintf(os.Stderr, " ✓ Repository owner type constraint: %s\n", action.Value) + fmt.Fprintf(os.Stderr, " ☐ Verify repository owner type: %s\n", action.Value) case "repo-variable": line := " ☐ Set repository variable: " + action.Name if action.Prompt != "" { @@ -66,7 +66,7 @@ func printBootstrapConfigTODO(profile *resolvedBootstrapProfile) { // executeBootstrapConfigForAdd runs the bootstrap config actions interactively. // Used by add-wizard after the workflow PR has been created and merged. -func executeBootstrapConfigForAdd(ctx context.Context, repo string, sources []string, profile *resolvedBootstrapProfile, verbose bool) error { +func executeBootstrapConfigForAdd(ctx context.Context, repo string, sources []string, profile *resolvedBootstrapProfile, useCopilotRequests bool, verbose bool) error { if profile == nil || profile.Profile == nil || len(profile.Profile.Config) == 0 { return nil } @@ -75,9 +75,10 @@ func executeBootstrapConfigForAdd(ctx context.Context, repo string, sources []st fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Applying post-installation steps from "+profile.PackageID+"...")) return executeBootstrapProfile(ctx, bootstrapProfileRunConfig{ - Repo: repo, - Sources: sources, - Profile: profile, - Verbose: verbose, + Repo: repo, + Sources: sources, + Profile: profile, + UseCopilotRequests: useCopilotRequests, + Verbose: verbose, }) } diff --git a/pkg/cli/bootstrap_profile_runner.go b/pkg/cli/bootstrap_profile_runner.go index 8113b07cc01..d8275364156 100644 --- a/pkg/cli/bootstrap_profile_runner.go +++ b/pkg/cli/bootstrap_profile_runner.go @@ -59,6 +59,10 @@ type bootstrapProfileRunConfig struct { PlanOnly bool Verbose bool Force bool + // UseCopilotRequests indicates the user chose org-billing (copilot-requests) auth + // instead of a PAT. When true, copilot-auth config actions are skipped because + // the workflow already has permissions.copilot-requests: write injected. + UseCopilotRequests bool } type bootstrapProfileExistingState struct { @@ -197,6 +201,10 @@ func executeBootstrapProfile(ctx context.Context, config bootstrapProfileRunConf state.variables[action.AppIDVariable] = struct{}{} state.secrets[action.PrivateKeySecret] = struct{}{} case "copilot-auth": + if config.UseCopilotRequests { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Skipping Copilot PAT setup because org Copilot billing is enabled.")) + continue + } applied, err := runBootstrapCopilotAuthAction(ctx, config.Repo, action, state, usesActionsToken) if err != nil { return err From 954f39dd625d462a0e66fef1401f5fbe24519102 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:07:48 +0000 Subject: [PATCH 6/7] refactor: improve test YAML readability in bootstrap profile tests Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/add_package_manifest_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/cli/add_package_manifest_test.go b/pkg/cli/add_package_manifest_test.go index 58f2250997d..66f84dc0cff 100644 --- a/pkg/cli/add_package_manifest_test.go +++ b/pkg/cli/add_package_manifest_test.go @@ -2110,7 +2110,14 @@ func TestResolveWorkflows_BootstrapProfile_MultiplePackagesWarnsAndSuppresses(t } switch path { case "aw.yml": - return []byte("name: " + pkgName + "\nfiles:\n - workflows/review.md\nconfig:\n - type: repo-variable\n name: " + varName + "\n prompt: Enter a value\n"), nil + return []byte(fmt.Sprintf(`name: %s +files: + - workflows/review.md +config: + - type: repo-variable + name: %s + prompt: Enter a value +`, pkgName, varName)), nil case "README.md": return []byte("# " + pkgName + "\n"), nil } From ce3cd7d5268bc8c03c2207b5e8f82b257a9e7382 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:51:32 +0000 Subject: [PATCH 7/7] fix: address PR Code Quality Reviewer feedback on bootstrap config - Add empty repo validation in executeBootstrapConfigForAdd to prevent confusing API errors when --repo flag is omitted - Change printBootstrapConfigTODO to accept io.Writer for testability; add_command passes cmd.ErrOrStderr(), add-wizard passes os.Stderr - Fix lint: replace fmt.Sprintf return with fmt.Appendf (modernize) - Add test: rejects old bootstrap key with clear schema error - Add test: TestPrintBootstrapConfigTODO verifying output goes to writer Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/add_command.go | 2 +- pkg/cli/add_interactive_orchestrator.go | 2 +- pkg/cli/add_package_manifest_test.go | 59 ++++++++++++++++++++++++- pkg/cli/bootstrap_config.go | 30 ++++++++----- 4 files changed, 77 insertions(+), 16 deletions(-) diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 57af4e155d9..2b3807351d5 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -160,7 +160,7 @@ func runAddCommand(cmd *cobra.Command, args []string, validateEngine func(string return err } if resolved.BootstrapProfile != nil { - printBootstrapConfigTODO(resolved.BootstrapProfile) + printBootstrapConfigTODO(cmd.ErrOrStderr(), resolved.BootstrapProfile) } return nil } diff --git a/pkg/cli/add_interactive_orchestrator.go b/pkg/cli/add_interactive_orchestrator.go index babf1bf5424..faf6604b2a9 100644 --- a/pkg/cli/add_interactive_orchestrator.go +++ b/pkg/cli/add_interactive_orchestrator.go @@ -171,7 +171,7 @@ func RunAddInteractive(ctx context.Context, config *AddInteractiveConfig) error return err } } else { - printBootstrapConfigTODO(config.resolvedWorkflows.BootstrapProfile) + printBootstrapConfigTODO(os.Stderr, config.resolvedWorkflows.BootstrapProfile) } } diff --git a/pkg/cli/add_package_manifest_test.go b/pkg/cli/add_package_manifest_test.go index 66f84dc0cff..f4ac33cfc16 100644 --- a/pkg/cli/add_package_manifest_test.go +++ b/pkg/cli/add_package_manifest_test.go @@ -488,6 +488,29 @@ config: assert.Contains(t, pkg.Warnings, "Using experimental feature: config") }) + t.Run("rejects old bootstrap key with schema error", func(t *testing.T) { + downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { + switch path { + case "aw.yml": + return []byte(`name: Repo Assist +bootstrap: + config: + - type: repo-variable + name: MY_VAR + prompt: Enter a value +`), nil + case "README.md": + return []byte("# Repo Assist\n"), nil + default: + return nil, createRepositoryPackageNotFoundError(path) + } + } + + _, err := resolveRepositoryPackage(t.Context(), &RepoSpec{RepoSlug: "owner/repo"}, "") + require.Error(t, err, "old bootstrap key must produce an error, not be silently ignored") + assert.Contains(t, err.Error(), "bootstrap") + }) + t.Run("rejects unsupported branding icon", func(t *testing.T) { downloadPackageFileFromGitHubForHost = func(_ context.Context, owner, repo, path, ref, host string) ([]byte, error) { if path == "aw.yml" { @@ -2110,14 +2133,14 @@ func TestResolveWorkflows_BootstrapProfile_MultiplePackagesWarnsAndSuppresses(t } switch path { case "aw.yml": - return []byte(fmt.Sprintf(`name: %s + return fmt.Appendf(nil, `name: %s files: - workflows/review.md config: - type: repo-variable name: %s prompt: Enter a value -`, pkgName, varName)), nil +`, pkgName, varName), nil case "README.md": return []byte("# " + pkgName + "\n"), nil } @@ -2141,3 +2164,35 @@ config: } assert.True(t, found, "expected a warning about multiple bootstrap profiles, got: %v", resolved.Warnings) } + +func TestPrintBootstrapConfigTODO(t *testing.T) { + t.Run("noop when profile is nil", func(t *testing.T) { + var buf strings.Builder + printBootstrapConfigTODO(&buf, nil) + assert.Empty(t, buf.String()) + }) + + t.Run("prints checklist items to provided writer", func(t *testing.T) { + profile := &resolvedBootstrapProfile{ + PackageID: "owner/repo", + Profile: &repositoryPackageBootstrap{ + Config: []repositoryPackageBootstrapAction{ + {Type: "require-owner-type", Value: "org"}, + {Type: "repo-variable", Name: "MY_VAR", Prompt: "Enter a value"}, + {Type: "repo-secret", Name: "MY_SECRET", Prompt: "Enter secret"}, + {Type: "copilot-auth", Secret: "COPILOT_TOKEN"}, + {Type: "handoff", Message: "Run the bootstrap wizard."}, + }, + }, + } + var buf strings.Builder + printBootstrapConfigTODO(&buf, profile) + out := buf.String() + assert.Contains(t, out, "owner/repo") + assert.Contains(t, out, "☐ Verify repository owner type: org") + assert.Contains(t, out, "☐ Set repository variable: MY_VAR") + assert.Contains(t, out, "☐ Set repository secret: MY_SECRET") + assert.Contains(t, out, "☐ Set Copilot PAT secret: COPILOT_TOKEN") + assert.Contains(t, out, "Run the bootstrap wizard.") + }) +} diff --git a/pkg/cli/bootstrap_config.go b/pkg/cli/bootstrap_config.go index 43c834ab3ed..1673dcf2029 100644 --- a/pkg/cli/bootstrap_config.go +++ b/pkg/cli/bootstrap_config.go @@ -2,7 +2,9 @@ package cli import ( "context" + "errors" "fmt" + "io" "os" "github.com/github/gh-aw/pkg/console" @@ -11,18 +13,18 @@ import ( // printBootstrapConfigTODO prints a TODO checklist of manual steps required by the // config entries in the package manifest. Called by the non-interactive // "add" command after workflows have been installed. -func printBootstrapConfigTODO(profile *resolvedBootstrapProfile) { +func printBootstrapConfigTODO(w io.Writer, profile *resolvedBootstrapProfile) { if profile == nil || profile.Profile == nil || len(profile.Profile.Config) == 0 { return } - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Post-installation steps from "+profile.PackageID+":")) + fmt.Fprintln(w, "") + fmt.Fprintln(w, console.FormatInfoMessage("Post-installation steps from "+profile.PackageID+":")) for _, action := range profile.Profile.Config { switch action.Type { case "require-owner-type": - fmt.Fprintf(os.Stderr, " ☐ Verify repository owner type: %s\n", action.Value) + fmt.Fprintf(w, " ☐ Verify repository owner type: %s\n", action.Value) case "repo-variable": line := " ☐ Set repository variable: " + action.Name if action.Prompt != "" { @@ -31,7 +33,7 @@ func printBootstrapConfigTODO(profile *resolvedBootstrapProfile) { if action.Optional { line += " (optional)" } - fmt.Fprintln(os.Stderr, line) + fmt.Fprintln(w, line) case "repo-secret": line := " ☐ Set repository secret: " + action.Name if action.Prompt != "" { @@ -40,28 +42,28 @@ func printBootstrapConfigTODO(profile *resolvedBootstrapProfile) { if action.Optional { line += " (optional)" } - fmt.Fprintln(os.Stderr, line) + fmt.Fprintln(w, line) case "github-app": appLabel := action.AppName if appLabel == "" { appLabel = "GitHub App" } - fmt.Fprintf(os.Stderr, " ☐ Configure %s (variable: %s, secret: %s)\n", + fmt.Fprintf(w, " ☐ Configure %s (variable: %s, secret: %s)\n", appLabel, action.AppIDVariable, action.PrivateKeySecret) case "copilot-auth": secret := action.Secret if secret == "" { secret = "COPILOT_GITHUB_TOKEN" } - fmt.Fprintf(os.Stderr, " ☐ Set Copilot PAT secret: %s\n", secret) + fmt.Fprintf(w, " ☐ Set Copilot PAT secret: %s\n", secret) case "handoff": - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(action.Message)) + fmt.Fprintln(w, console.FormatInfoMessage(action.Message)) } } - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Run 'gh aw bootstrap --repo OWNER/REPO' to apply these steps interactively.")) - fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(w, "") + fmt.Fprintln(w, console.FormatInfoMessage("Run 'gh aw bootstrap --repo OWNER/REPO' to apply these steps interactively.")) + fmt.Fprintln(w, "") } // executeBootstrapConfigForAdd runs the bootstrap config actions interactively. @@ -71,6 +73,10 @@ func executeBootstrapConfigForAdd(ctx context.Context, repo string, sources []st return nil } + if repo == "" { + return errors.New("--repo OWNER/REPO is required to apply bootstrap config steps interactively") + } + fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Applying post-installation steps from "+profile.PackageID+"..."))