From 0514f468b7318220cdbc2d2ecc77db2c0f6a0928 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:12:04 +0000 Subject: [PATCH 1/4] Initial plan From 16b34de1d4e38571c39cbebb9aee1646d81abbed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:33:36 +0000 Subject: [PATCH 2/4] Improve error message compliance in cli and sandbox validation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/add_command.go | 14 +++++++------- pkg/cli/add_command_test.go | 2 +- pkg/cli/project_command.go | 14 +++++++------- pkg/cli/run_workflow_execution.go | 14 +++++++------- pkg/cli/run_workflow_execution_test.go | 8 ++++---- pkg/workflow/sandbox_validation.go | 14 +++++++------- pkg/workflow/sandbox_validation_test.go | 6 +++--- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 5dc8dcd16b6..1146343a1c3 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -105,7 +105,7 @@ func NewAddCommand(validateEngine func(string) error) *cobra.Command { Example: addCommandExample, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { - return fmt.Errorf("missing workflow specification\n\nUsage:\n %s ...\n\nExamples:\n %[1]s githubnext/agentics/daily-repo-status Add from repository\n %[1]s ./my-workflow.md Add local workflow\n\nRun '%[1]s --help' for more information", cmd.CommandPath()) + return fmt.Errorf("missing workflow specification. Expected at least one workflow source argument. Example: %[1]s githubnext/agentics/daily-repo-status\n\nUsage:\n %[1]s ...\n\nExamples:\n %[1]s githubnext/agentics/daily-repo-status Add from repository\n %[1]s ./my-workflow.md Add local workflow\n\nRun '%[1]s --help' for more information", cmd.CommandPath()) } return nil }, @@ -133,7 +133,7 @@ func runAddCommand(cmd *cobra.Command, args []string, validateEngine func(string disableSecurityScanner := resolveDeprecatedBoolFlag(cmd, "no-security-scanner", "disable-security-scanner") if nameFlag != "" && len(args) > 1 { - return errors.New("--name flag cannot be used when adding multiple workflows at once") + return errors.New("--name was set while multiple workflows were provided. Expected --name only with a single workflow source. Example: gh aw add githubnext/agentics/daily-repo-status --name daily-repo-status") } if err := validateEngine(engineOverride); err != nil { return err @@ -178,7 +178,7 @@ func rejectBootstrapProfileForRegularAdd(sources []string, profile *resolvedBoot requestedSources = profile.PackageID } - return fmt.Errorf("package %s declares aw.yml config and cannot be installed with 'gh aw add'. Use 'gh aw add-wizard %s' so the config steps can run interactively", profile.PackageID, requestedSources) + return fmt.Errorf("package %s declares aw.yml config, so 'gh aw add' cannot run its interactive setup. Expected interactive setup via add-wizard for packages with aw.yml config. Example: gh aw add-wizard %s", profile.PackageID, requestedSources) } func registerAddCommandFlags(cmd *cobra.Command) { @@ -255,7 +255,7 @@ func AddResolvedWorkflows(ctx context.Context, workflowStrings []string, resolve if opts.CreatePR { // Check if GitHub CLI is available if !isGHCLIAvailable() { - return nil, errors.New("GitHub CLI (gh) is required for PR creation but not available") + return nil, errors.New("GitHub CLI (gh) is not available. Expected gh to be installed and on PATH before using --create-pull-request. Example: brew install gh") } // Check if we're in a git repository @@ -524,7 +524,7 @@ func resolveWorkflowTargetDir(opts AddOptions) (gitRoot, githubWorkflowsDir stri } if opts.WorkflowDir != "" { if filepath.IsAbs(opts.WorkflowDir) { - return "", "", fmt.Errorf("workflow directory must be a relative path, got: %s", opts.WorkflowDir) + return "", "", fmt.Errorf("workflow directory is absolute: %s. Expected a relative path from the repository root. Example: --dir .github/workflows", opts.WorkflowDir) } githubWorkflowsDir = filepath.Join(gitRoot, filepath.Clean(opts.WorkflowDir)) } else { @@ -873,7 +873,7 @@ func resolveSkillRelativePath(resolved *ResolvedWorkflow) (string, error) { } relPath := filepath.Clean(filepath.Join(relParts...)) if relPath == "." || relPath == "" || relPath == string(os.PathSeparator) { - return "", fmt.Errorf("invalid relative skill path %q from source path %q", relPath, resolved.Spec.WorkflowPath) + return "", fmt.Errorf("relative skill path %q from source path %q is empty. Expected a file path under the skill directory. Example: scripts/query.sh", relPath, resolved.Spec.WorkflowPath) } return relPath, nil } @@ -998,7 +998,7 @@ func addCopilotRequestsPermissionToContent(content string) (string, error) { return updated, modified }) if injectionFailed { - return content, errors.New("cannot inject permissions.copilot-requests: write: 'permissions' is a non-mapping scalar value; update it manually") + return content, errors.New("permissions.copilot-requests could not be injected because 'permissions' is a non-mapping scalar value. Expected 'permissions' to be a mapping object. Example:\npermissions:\n contents: read\n copilot-requests: write") } if err != nil { return content, err diff --git a/pkg/cli/add_command_test.go b/pkg/cli/add_command_test.go index 5b7d6f110cb..af467e00cdf 100644 --- a/pkg/cli/add_command_test.go +++ b/pkg/cli/add_command_test.go @@ -683,7 +683,7 @@ func TestAddMultipleWorkflowsNameFlag(t *testing.T) { err := cmd.Execute() require.Error(t, err, "Should error when --name is used with multiple workflows") - require.ErrorContains(t, err, "--name flag cannot be used when adding multiple workflows", "Error should mention --name restriction") + require.ErrorContains(t, err, "--name was set while multiple workflows were provided", "Error should mention --name restriction") } // setupMinimalGitRepo initialises a bare-minimum git repo in dir and returns the diff --git a/pkg/cli/project_command.go b/pkg/cli/project_command.go index 57d948e6b6b..09ad5675610 100644 --- a/pkg/cli/project_command.go +++ b/pkg/cli/project_command.go @@ -88,7 +88,7 @@ Project Setup: withProjectSetup, _ := cmd.Flags().GetBool("with-project-setup") if owner == "" { - return errors.New("--owner flag is required. Use '@me' for current user or specify org name") + return errors.New("--owner flag is missing. Expected '@me' for the current user or an organization login. Example: gh aw project new \"My Project\" --owner @me") } config := ProjectConfig{ @@ -355,17 +355,17 @@ func createProject(ctx context.Context, ownerId, title string, verbose bool) (ma // Extract project data data, ok := response["data"].(map[string]any) if !ok { - return nil, errors.New("invalid response: missing 'data' field") + return nil, errors.New("response is missing the 'data' field. Expected a GraphQL payload with data.createProjectV2.projectV2. Example: {\"data\":{\"createProjectV2\":{\"projectV2\":{\"id\":\"PVT_id\",\"number\":123}}}}") } createResult, ok := data["createProjectV2"].(map[string]any) if !ok { - return nil, errors.New("invalid response: missing 'createProjectV2' field") + return nil, errors.New("response is missing the 'createProjectV2' field. Expected data.createProjectV2.projectV2 in the GraphQL payload. Example: {\"data\":{\"createProjectV2\":{\"projectV2\":{\"id\":\"PVT_id\",\"number\":123}}}}") } project, ok := createResult["projectV2"].(map[string]any) if !ok { - return nil, errors.New("invalid response: missing 'projectV2' field") + return nil, errors.New("response is missing the 'projectV2' field. Expected data.createProjectV2.projectV2 in the GraphQL payload. Example: {\"data\":{\"createProjectV2\":{\"projectV2\":{\"id\":\"PVT_id\",\"number\":123}}}}") } console.LogVerbose(verbose, fmt.Sprintf("✓ Project created: #%v", project["number"])) @@ -380,7 +380,7 @@ func linkProjectToRepo(ctx context.Context, projectId, repoSlug string, verbose // Parse repo slug parts := strings.Split(repoSlug, "/") if len(parts) != 2 { - return fmt.Errorf("invalid repository format. Expected 'owner/repo', got '%s'", repoSlug) + return fmt.Errorf("repository slug '%s' is not in owner/repo format. Expected '/'. Example: github/gh-aw", repoSlug) } repoOwner := parts[0] repoName := parts[1] @@ -451,7 +451,7 @@ func parseProjectURL(projectURL string) (projectURLInfo, error) { // Expected format: https://github.com/orgs/myorg/projects/123 or https://github.com/users/myuser/projects/123 parts := strings.Split(projectURL, "/") if len(parts) < 6 { - return projectURLInfo{}, errors.New("invalid project URL format") + return projectURLInfo{}, errors.New("project URL format is not recognized. Expected https://github.com/orgs//projects/ or https://github.com/users//projects/. Example: https://github.com/orgs/github/projects/123") } var scope, ownerLogin, numberStr string @@ -467,7 +467,7 @@ func parseProjectURL(projectURL string) (projectURLInfo, error) { } if scope == "" { - return projectURLInfo{}, errors.New("invalid project URL: could not find orgs/users segment") + return projectURLInfo{}, errors.New("project URL is missing an 'orgs' or 'users' segment. Expected https://github.com/orgs//projects/ or https://github.com/users//projects/. Example: https://github.com/users/octocat/projects/123") } projectNumber, err := strconv.Atoi(numberStr) diff --git a/pkg/cli/run_workflow_execution.go b/pkg/cli/run_workflow_execution.go index 0df799b80cd..6e1125b667e 100644 --- a/pkg/cli/run_workflow_execution.go +++ b/pkg/cli/run_workflow_execution.go @@ -84,7 +84,7 @@ func RunWorkflowOnGitHub(ctx context.Context, workflowIdOrName string, opts RunO fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Running workflow on GitHub Actions: "+workflowIdOrName)) } if !isGHCLIAvailable() { - return errors.New("GitHub CLI (gh) is required but not available") + return errors.New("GitHub CLI (gh) is not available. Expected gh to be installed and on PATH before running workflows. Example: brew install gh") } prep, err := prepareWorkflowRun(ctx, workflowIdOrName, opts) if err != nil { @@ -112,7 +112,7 @@ func checkWorkflowRunContext(ctx context.Context, workflowIdOrName string) error default: } if workflowIdOrName == "" { - return errors.New("workflow name or ID is required") + return errors.New("workflow name or ID is missing. Expected a workflow file name or numeric workflow ID. Example: gh aw run ci.lock.yml") } return nil } @@ -120,10 +120,10 @@ func checkWorkflowRunContext(ctx context.Context, workflowIdOrName string) error func validateRunInputs(inputs []string) error { for _, input := range inputs { if !strings.Contains(input, "=") { - return fmt.Errorf("invalid input format '%s': expected key=value", input) + return fmt.Errorf("input '%s' is not in key=value format. Expected each --input value as key=value. Example: --input environment=staging", input) } if parts := strings.SplitN(input, "=", 2); parts[0] == "" { - return fmt.Errorf("invalid input format '%s': key cannot be empty", input) + return fmt.Errorf("input '%s' has an empty key before '='. Expected a non-empty key in key=value format. Example: --input environment=staging", input) } } return nil @@ -185,7 +185,7 @@ func ensureWorkflowRunnable(workflowFile, workflowIdOrName string) error { return fmt.Errorf("failed to check if workflow %s is runnable: %w", workflowFile, err) } if !runnable { - return fmt.Errorf("workflow '%s' cannot be run on GitHub Actions - it must have 'workflow_dispatch' trigger", workflowIdOrName) + return fmt.Errorf("workflow '%s' does not declare a workflow_dispatch trigger, so it cannot be run manually on GitHub Actions. Expected a workflow_dispatch trigger in the lock file. Example: on: workflow_dispatch", workflowIdOrName) } executionLog.Printf("Workflow is runnable: %s", workflowFile) return nil @@ -545,7 +545,7 @@ func validateWorkflowsForRun(workflowNames []string, opts RunOptions) error { return fmt.Errorf("failed to check if workflow '%s' is runnable: %w", workflowName, err) } if !runnable { - return fmt.Errorf("workflow '%s' cannot be run on GitHub Actions - it must have 'workflow_dispatch' trigger", workflowName) + return fmt.Errorf("workflow '%s' does not declare a workflow_dispatch trigger, so it cannot be run manually on GitHub Actions. Expected a workflow_dispatch trigger in the lock file. Example: on: workflow_dispatch", workflowName) } } } @@ -618,7 +618,7 @@ func wrapRunWithJSONOutput(inner func() error, workflowNames []string, opts RunO // RunWorkflowsOnGitHub runs multiple agentic workflows on GitHub Actions, optionally repeating a specified number of times func RunWorkflowsOnGitHub(ctx context.Context, workflowNames []string, opts RunOptions) error { if len(workflowNames) == 0 { - return errors.New("at least one workflow name or ID is required") + return errors.New("workflow list is empty. Expected at least one workflow file name or numeric workflow ID. Example: gh aw run ci.lock.yml") } select { case <-ctx.Done(): diff --git a/pkg/cli/run_workflow_execution_test.go b/pkg/cli/run_workflow_execution_test.go index 35fef0ecc5b..2cbdb21e661 100644 --- a/pkg/cli/run_workflow_execution_test.go +++ b/pkg/cli/run_workflow_execution_test.go @@ -31,21 +31,21 @@ func TestRunWorkflowOnGitHub_InputValidation(t *testing.T) { workflowName: "", inputs: []string{}, expectError: true, - errorContains: "workflow name or ID is required", + errorContains: "workflow name or ID is missing", }, { name: "invalid input format - no equals sign", workflowName: "test-workflow", inputs: []string{"invalidinput"}, expectError: true, - errorContains: "invalid input format", + errorContains: "not in key=value format", }, { name: "invalid input format - empty key", workflowName: "test-workflow", inputs: []string{"=value"}, expectError: true, - errorContains: "key cannot be empty", + errorContains: "empty key before '='", }, { name: "valid input format - workflow resolution fails", @@ -132,7 +132,7 @@ func TestRunWorkflowsOnGitHub_InputValidation(t *testing.T) { name: "empty workflow list", workflowNames: []string{}, expectError: true, - errorContains: "at least one workflow name or ID is required", + errorContains: "workflow list is empty", }, { name: "single workflow - resolution fails", diff --git a/pkg/workflow/sandbox_validation.go b/pkg/workflow/sandbox_validation.go index dd9bc8191a8..eadaf37af42 100644 --- a/pkg/workflow/sandbox_validation.go +++ b/pkg/workflow/sandbox_validation.go @@ -66,7 +66,7 @@ func validateMountsSyntax(mounts []string) error { fmt.Sprintf("Provide a valid destination path.\n\nExample:\nsandbox:\n mounts:\n - \"/host/path:/container/path:ro\"\n\nSee: %s", constants.DocsSandboxURL), ) default: - return fmt.Errorf("internal error: unsupported mount validation kind %d for sandbox mount %q", kind, mount) + return fmt.Errorf("sandbox mount validation kind %d for mount %q is not supported. Expected one of the known sandbox mount validation kinds. Example: \"/host/path:/container/path:ro\"", kind, mount) } }) } @@ -535,7 +535,7 @@ func validateAllowHostPorts(ports []int) error { return fmt.Errorf("invalid allow-host-ports value: %d. Expected a TCP port between 1 and 65535. Example: allow-host-ports: [5432]", port) } if service, dangerous := awfDangerousHostPorts[port]; dangerous { - return fmt.Errorf("invalid allow-host-ports value: %d. This port is blocked by AWF as a dangerous port (%s) and cannot be reached via allow-host-ports even in legacy-security mode. To reach a service on this port, declare it under services: with a port mapping and enable sandbox.agent.legacy-security", port, service) + return fmt.Errorf("allow-host-ports value %d targets blocked service port %s. Expected allow-host-ports to include only non-dangerous TCP ports, or to expose blocked service ports through services with a port mapping. Example:\nsandbox:\n agent:\n legacy-security: true\nservices:\n db:\n image: postgres\n ports: [\"5432:5432\"]", port, service) } } return nil @@ -543,27 +543,27 @@ func validateAllowHostPorts(ports []int) error { func getSandboxDisableJustification(workflowData *WorkflowData) (string, error) { if workflowData == nil || workflowData.Features == nil { - return "", errors.New("dangerously-disable-sandbox-agent feature is missing") + return "", errors.New("dangerously-disable-sandbox-agent feature is missing. Expected a non-empty string justification under features when sandbox.agent is false. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"") } flagName := string(constants.DangerouslyDisableSandboxAgentFeatureFlag) value, found := getFeatureValueCaseInsensitive(workflowData.Features, flagName) if !found { - return "", errors.New("dangerously-disable-sandbox-agent feature is missing") + return "", errors.New("dangerously-disable-sandbox-agent feature is missing. Expected a non-empty string justification under features when sandbox.agent is false. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"") } justification, ok := value.(string) if !ok { - return "", fmt.Errorf("feature must be a string, got %T", value) + return "", fmt.Errorf("dangerously-disable-sandbox-agent feature value has type %T. Expected a string justification. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"", value) } trimmed := strings.TrimSpace(justification) if len(trimmed) < minSandboxDisableJustificationLength { - return "", fmt.Errorf("feature must be at least %d characters", minSandboxDisableJustificationLength) + return "", fmt.Errorf("dangerously-disable-sandbox-agent justification is shorter than %d characters. Expected a descriptive justification string with at least %d characters. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"", minSandboxDisableJustificationLength, minSandboxDisableJustificationLength) } if githubActionsExpressionPattern.MatchString(trimmed) { - return "", errors.New("feature cannot use GitHub Actions expressions") + return "", errors.New("dangerously-disable-sandbox-agent justification uses a GitHub Actions expression. Expected a literal explanatory string, not an expression. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"") } return trimmed, nil diff --git a/pkg/workflow/sandbox_validation_test.go b/pkg/workflow/sandbox_validation_test.go index b0926f39514..12a2eaea2ab 100644 --- a/pkg/workflow/sandbox_validation_test.go +++ b/pkg/workflow/sandbox_validation_test.go @@ -136,13 +136,13 @@ func TestGetSandboxDisableJustification(t *testing.T) { t.Run("GitHub Actions expression is rejected", func(t *testing.T) { _, err := getSandboxDisableJustification(makeData("${{ inputs.reason }}")) require.Error(t, err) - require.ErrorContains(t, err, "expressions") + require.ErrorContains(t, err, "expression") }) t.Run("longer expression with surrounding text is rejected", func(t *testing.T) { _, err := getSandboxDisableJustification(makeData("reason: ${{ inputs.reason }} end")) require.Error(t, err) - require.ErrorContains(t, err, "expressions") + require.ErrorContains(t, err, "expression") }) t.Run("20+ character literal reason passes", func(t *testing.T) { @@ -378,7 +378,7 @@ func TestValidateSandboxConfigAllowHostPorts(t *testing.T) { err := validateSandboxConfig(workflowData) require.Error(t, err, "a dangerous port should fail validation") - assert.Contains(t, err.Error(), "invalid allow-host-ports value: 5432") + assert.Contains(t, err.Error(), "allow-host-ports value 5432") assert.Contains(t, err.Error(), "PostgreSQL") assert.Contains(t, err.Error(), "services:") }) From 3bcde09f70cdd932ceffe9d0670c7e27f10ee044 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:43:11 +0000 Subject: [PATCH 3/4] Polish sandbox and project error guidance messages Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/project_command.go | 6 +++--- pkg/workflow/sandbox_validation.go | 10 +++++----- pkg/workflow/sandbox_validation_test.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/cli/project_command.go b/pkg/cli/project_command.go index 09ad5675610..fa4620fc062 100644 --- a/pkg/cli/project_command.go +++ b/pkg/cli/project_command.go @@ -355,17 +355,17 @@ func createProject(ctx context.Context, ownerId, title string, verbose bool) (ma // Extract project data data, ok := response["data"].(map[string]any) if !ok { - return nil, errors.New("response is missing the 'data' field. Expected a GraphQL payload with data.createProjectV2.projectV2. Example: {\"data\":{\"createProjectV2\":{\"projectV2\":{\"id\":\"PVT_id\",\"number\":123}}}}") + return nil, errors.New("response is missing the 'data' field. Expected the GitHub GraphQL mutation payload to include data.createProjectV2.projectV2. Example: run 'gh auth status' to verify token scopes, then retry the command") } createResult, ok := data["createProjectV2"].(map[string]any) if !ok { - return nil, errors.New("response is missing the 'createProjectV2' field. Expected data.createProjectV2.projectV2 in the GraphQL payload. Example: {\"data\":{\"createProjectV2\":{\"projectV2\":{\"id\":\"PVT_id\",\"number\":123}}}}") + return nil, errors.New("response is missing the 'createProjectV2' field. Expected the GitHub GraphQL mutation payload to include data.createProjectV2.projectV2. Example: run 'gh auth status' to verify token scopes, then retry the command") } project, ok := createResult["projectV2"].(map[string]any) if !ok { - return nil, errors.New("response is missing the 'projectV2' field. Expected data.createProjectV2.projectV2 in the GraphQL payload. Example: {\"data\":{\"createProjectV2\":{\"projectV2\":{\"id\":\"PVT_id\",\"number\":123}}}}") + return nil, errors.New("response is missing the 'projectV2' field. Expected the GitHub GraphQL mutation payload to include data.createProjectV2.projectV2. Example: run 'gh auth status' to verify token scopes, then retry the command") } console.LogVerbose(verbose, fmt.Sprintf("✓ Project created: #%v", project["number"])) diff --git a/pkg/workflow/sandbox_validation.go b/pkg/workflow/sandbox_validation.go index eadaf37af42..48cdfb8b9b0 100644 --- a/pkg/workflow/sandbox_validation.go +++ b/pkg/workflow/sandbox_validation.go @@ -66,7 +66,7 @@ func validateMountsSyntax(mounts []string) error { fmt.Sprintf("Provide a valid destination path.\n\nExample:\nsandbox:\n mounts:\n - \"/host/path:/container/path:ro\"\n\nSee: %s", constants.DocsSandboxURL), ) default: - return fmt.Errorf("sandbox mount validation kind %d for mount %q is not supported. Expected one of the known sandbox mount validation kinds. Example: \"/host/path:/container/path:ro\"", kind, mount) + return fmt.Errorf("internal error: sandbox mount validation kind %d for mount %q is not supported. Expected one of: invalid-format, too-few-parts, too-many-parts, empty-host-path, empty-destination. Example: \"/host/path:/container/path:ro\"", kind, mount) } }) } @@ -532,10 +532,10 @@ func validateAgentMemoryLimit(memory string) error { func validateAllowHostPorts(ports []int) error { for _, port := range ports { if port < minPort || port > maxPort { - return fmt.Errorf("invalid allow-host-ports value: %d. Expected a TCP port between 1 and 65535. Example: allow-host-ports: [5432]", port) + return fmt.Errorf("allow-host-ports value %d is out of range. Expected a TCP port between 1 and 65535. Example: allow-host-ports: [5432]", port) } if service, dangerous := awfDangerousHostPorts[port]; dangerous { - return fmt.Errorf("allow-host-ports value %d targets blocked service port %s. Expected allow-host-ports to include only non-dangerous TCP ports, or to expose blocked service ports through services with a port mapping. Example:\nsandbox:\n agent:\n legacy-security: true\nservices:\n db:\n image: postgres\n ports: [\"5432:5432\"]", port, service) + return fmt.Errorf("allow-host-ports value %d maps to blocked service %s. Expected blocked service ports to be removed from allow-host-ports because they remain unreachable there, even with legacy-security. Example:\n# Do not list blocked service ports under allow-host-ports\nservices:\n db:\n image: postgres\n ports: [\"5432:5432\"]", port, service) } } return nil @@ -543,13 +543,13 @@ func validateAllowHostPorts(ports []int) error { func getSandboxDisableJustification(workflowData *WorkflowData) (string, error) { if workflowData == nil || workflowData.Features == nil { - return "", errors.New("dangerously-disable-sandbox-agent feature is missing. Expected a non-empty string justification under features when sandbox.agent is false. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"") + return "", errors.New("features block is missing dangerously-disable-sandbox-agent configuration. Expected a non-empty string justification under features when sandbox.agent is false. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"") } flagName := string(constants.DangerouslyDisableSandboxAgentFeatureFlag) value, found := getFeatureValueCaseInsensitive(workflowData.Features, flagName) if !found { - return "", errors.New("dangerously-disable-sandbox-agent feature is missing. Expected a non-empty string justification under features when sandbox.agent is false. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"") + return "", errors.New("dangerously-disable-sandbox-agent key is missing from features. Expected a non-empty string justification under features when sandbox.agent is false. Example:\nfeatures:\n dangerously-disable-sandbox-agent: \"Temporary migration while hardening container profile\"") } justification, ok := value.(string) diff --git a/pkg/workflow/sandbox_validation_test.go b/pkg/workflow/sandbox_validation_test.go index 12a2eaea2ab..9d5cb0b7872 100644 --- a/pkg/workflow/sandbox_validation_test.go +++ b/pkg/workflow/sandbox_validation_test.go @@ -364,7 +364,7 @@ func TestValidateSandboxConfigAllowHostPorts(t *testing.T) { err := validateSandboxConfig(workflowData) require.Error(t, err, "out-of-range allow-host-ports should fail validation") - assert.Contains(t, err.Error(), "invalid allow-host-ports value: 0") + assert.Contains(t, err.Error(), "allow-host-ports value 0 is out of range") assert.Contains(t, err.Error(), "Example: allow-host-ports: [5432]") }) From 5024851d4e837c1cce68c12fa4cf4fc36f1bafc2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:56:08 +0000 Subject: [PATCH 4/4] Address review feedback on sandbox and workflow-run error examples Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/run_workflow_execution.go | 8 ++++---- pkg/workflow/call_workflow_validation.go | 6 +++--- pkg/workflow/sandbox_validation.go | 4 ++-- pkg/workflow/sandbox_validation_test.go | 3 ++- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/cli/run_workflow_execution.go b/pkg/cli/run_workflow_execution.go index 6e1125b667e..e12cb24b6cf 100644 --- a/pkg/cli/run_workflow_execution.go +++ b/pkg/cli/run_workflow_execution.go @@ -112,7 +112,7 @@ func checkWorkflowRunContext(ctx context.Context, workflowIdOrName string) error default: } if workflowIdOrName == "" { - return errors.New("workflow name or ID is missing. Expected a workflow file name or numeric workflow ID. Example: gh aw run ci.lock.yml") + return errors.New("workflow name or ID is missing. Expected a workflow file name or numeric workflow ID. Example: gh aw run ci") } return nil } @@ -185,7 +185,7 @@ func ensureWorkflowRunnable(workflowFile, workflowIdOrName string) error { return fmt.Errorf("failed to check if workflow %s is runnable: %w", workflowFile, err) } if !runnable { - return fmt.Errorf("workflow '%s' does not declare a workflow_dispatch trigger, so it cannot be run manually on GitHub Actions. Expected a workflow_dispatch trigger in the lock file. Example: on: workflow_dispatch", workflowIdOrName) + return fmt.Errorf("workflow '%s' does not declare a workflow_dispatch trigger, so it cannot be run manually on GitHub Actions. Expected an `on: workflow_dispatch` trigger in the source workflow frontmatter, then recompile. Example:\non:\n workflow_dispatch:\n\nRun: gh aw compile", workflowIdOrName) } executionLog.Printf("Workflow is runnable: %s", workflowFile) return nil @@ -545,7 +545,7 @@ func validateWorkflowsForRun(workflowNames []string, opts RunOptions) error { return fmt.Errorf("failed to check if workflow '%s' is runnable: %w", workflowName, err) } if !runnable { - return fmt.Errorf("workflow '%s' does not declare a workflow_dispatch trigger, so it cannot be run manually on GitHub Actions. Expected a workflow_dispatch trigger in the lock file. Example: on: workflow_dispatch", workflowName) + return fmt.Errorf("workflow '%s' does not declare a workflow_dispatch trigger, so it cannot be run manually on GitHub Actions. Expected an `on: workflow_dispatch` trigger in the source workflow frontmatter, then recompile. Example:\non:\n workflow_dispatch:\n\nRun: gh aw compile", workflowName) } } } @@ -618,7 +618,7 @@ func wrapRunWithJSONOutput(inner func() error, workflowNames []string, opts RunO // RunWorkflowsOnGitHub runs multiple agentic workflows on GitHub Actions, optionally repeating a specified number of times func RunWorkflowsOnGitHub(ctx context.Context, workflowNames []string, opts RunOptions) error { if len(workflowNames) == 0 { - return errors.New("workflow list is empty. Expected at least one workflow file name or numeric workflow ID. Example: gh aw run ci.lock.yml") + return errors.New("workflow list is empty. Expected at least one workflow file name or numeric workflow ID. Example: gh aw run ci") } select { case <-ctx.Done(): diff --git a/pkg/workflow/call_workflow_validation.go b/pkg/workflow/call_workflow_validation.go index da1a1674965..296c0cf4574 100644 --- a/pkg/workflow/call_workflow_validation.go +++ b/pkg/workflow/call_workflow_validation.go @@ -117,12 +117,12 @@ func validateYAMLWorkflowHasCallTrigger(path, workflowName string) error { } onSection, hasOn := workflow["on"] if !hasOn { - return fmt.Errorf("call-workflow: workflow '%s' has no 'on' trigger section, expected an 'on' section with a 'workflow_call' trigger. Example:\non:\n workflow_call:", workflowName) + return fmt.Errorf("call-workflow: workflow '%s' has no 'on' trigger section, expected an 'on' section with a 'workflow_call' trigger, for example:\non:\n workflow_call: {}", workflowName) } if containsWorkflowCall(onSection) { return nil } - return fmt.Errorf("call-workflow: workflow '%s' does not support the workflow_call trigger, expected 'workflow_call' in the 'on' section. Example:\non:\n workflow_call:", workflowName) + return fmt.Errorf("call-workflow: workflow '%s' does not support the workflow_call trigger, expected 'workflow_call' in the 'on' section, for example:\non:\n workflow_call: {}", workflowName) } func validateMarkdownWorkflowHasCallTrigger(path, workflowName string) error { @@ -131,7 +131,7 @@ func validateMarkdownWorkflowHasCallTrigger(path, workflowName string) error { return fmt.Errorf("call-workflow: failed to read workflow source %s: %w", path, checkErr) } if !mdHasCall { - return fmt.Errorf("call-workflow: workflow '%s' does not support the workflow_call trigger, expected 'workflow_call' in the 'on' section. Example:\non:\n workflow_call:", workflowName) + return fmt.Errorf("call-workflow: workflow '%s' does not support the workflow_call trigger, expected 'workflow_call' in the 'on' section, for example:\non:\n workflow_call: {}", workflowName) } callWorkflowValidationLog.Printf("Workflow '%s' is valid for call-workflow (found .md source at %s with workflow_call trigger)", workflowName, path) return nil diff --git a/pkg/workflow/sandbox_validation.go b/pkg/workflow/sandbox_validation.go index 48cdfb8b9b0..afc7d7543c8 100644 --- a/pkg/workflow/sandbox_validation.go +++ b/pkg/workflow/sandbox_validation.go @@ -532,10 +532,10 @@ func validateAgentMemoryLimit(memory string) error { func validateAllowHostPorts(ports []int) error { for _, port := range ports { if port < minPort || port > maxPort { - return fmt.Errorf("allow-host-ports value %d is out of range. Expected a TCP port between 1 and 65535. Example: allow-host-ports: [5432]", port) + return fmt.Errorf("allow-host-ports value %d is out of range. Expected a TCP port between 1 and 65535. Example: allow-host-ports: [9000]", port) } if service, dangerous := awfDangerousHostPorts[port]; dangerous { - return fmt.Errorf("allow-host-ports value %d maps to blocked service %s. Expected blocked service ports to be removed from allow-host-ports because they remain unreachable there, even with legacy-security. Example:\n# Do not list blocked service ports under allow-host-ports\nservices:\n db:\n image: postgres\n ports: [\"5432:5432\"]", port, service) + return fmt.Errorf("allow-host-ports value %d maps to blocked service %s. Expected blocked service ports to be removed from allow-host-ports because they remain unreachable there even with legacy-security enabled; expose the service via GitHub Actions services: with sandbox.agent.legacy-security: enable instead. Example:\n# Do not list blocked service ports under allow-host-ports\nsandbox:\n agent:\n legacy-security: enable\nservices:\n db:\n image: postgres\n ports: [\"5432:5432\"]", port, service) } } return nil diff --git a/pkg/workflow/sandbox_validation_test.go b/pkg/workflow/sandbox_validation_test.go index 9d5cb0b7872..e0f7ebcf040 100644 --- a/pkg/workflow/sandbox_validation_test.go +++ b/pkg/workflow/sandbox_validation_test.go @@ -365,7 +365,7 @@ func TestValidateSandboxConfigAllowHostPorts(t *testing.T) { err := validateSandboxConfig(workflowData) require.Error(t, err, "out-of-range allow-host-ports should fail validation") assert.Contains(t, err.Error(), "allow-host-ports value 0 is out of range") - assert.Contains(t, err.Error(), "Example: allow-host-ports: [5432]") + assert.Contains(t, err.Error(), "Example: allow-host-ports: [9000]") }) t.Run("dangerous allow-host-ports fails validation", func(t *testing.T) { @@ -381,5 +381,6 @@ func TestValidateSandboxConfigAllowHostPorts(t *testing.T) { assert.Contains(t, err.Error(), "allow-host-ports value 5432") assert.Contains(t, err.Error(), "PostgreSQL") assert.Contains(t, err.Error(), "services:") + assert.Contains(t, err.Error(), "legacy-security: enable") }) }