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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions pkg/cli/add_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <workflow>...\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 <workflow>...\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
},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Platform-specific install hint — brew install gh is macOS-only and will mislead Windows/Linux users.

💡 Suggestion

Replace with a cross-platform reference:

return nil, errors.New("GitHub CLI (gh) is not available. Expected gh to be installed and on PATH before using --create-pull-request. See: https://cli.github.com/")

The same pattern also appears in run_workflow_execution.go line 87.

@copilot please address this.

}

// Check if we're in a git repository
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/add_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions pkg/cli/project_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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 the GitHub GraphQL mutation payload to include data.createProjectV2.projectV2. Example: run 'gh auth status' to verify token scopes, then retry the command")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Three consecutive GraphQL response-field errors share the same message and recovery hint, making them indistinguishable in logs.

💡 Suggestion

Each error already names the missing field. The gh auth status recovery hint is generic and unlikely to help for createProjectV2/projectV2 fields. Consider marking inner checks as internal errors:

return nil, errors.New("response is missing 'data' field (internal error)")
return nil, errors.New("response is missing 'createProjectV2' field (internal error)")
return nil, errors.New("response is missing 'projectV2' field (internal error)")

This keeps the gh auth status hint only at the top-level where it is actionable.

@copilot please address this.

}

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 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("invalid response: missing 'projectV2' field")
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"]))
Expand All @@ -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 '<owner>/<repo>'. Example: github/gh-aw", repoSlug)
}
repoOwner := parts[0]
repoName := parts[1]
Expand Down Expand Up @@ -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/<org>/projects/<number> or https://github.com/users/<user>/projects/<number>. Example: https://github.com/orgs/github/projects/123")
}

var scope, ownerLogin, numberStr string
Expand All @@ -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/<org>/projects/<number> or https://github.com/users/<user>/projects/<number>. Example: https://github.com/users/octocat/projects/123")
}

projectNumber, err := strconv.Atoi(numberStr)
Expand Down
14 changes: 7 additions & 7 deletions pkg/cli/run_workflow_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -112,18 +112,18 @@ 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")
}
return nil
}

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
Expand Down Expand Up @@ -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 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
Expand Down Expand Up @@ -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 an `on: workflow_dispatch` trigger in the source workflow frontmatter, then recompile. Example:\non:\n workflow_dispatch:\n\nRun: gh aw compile", workflowName)
}
}
}
Expand Down Expand Up @@ -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")
}
select {
case <-ctx.Done():
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/run_workflow_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions pkg/workflow/call_workflow_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions pkg/workflow/sandbox_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("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)
}
})
}
Expand Down Expand Up @@ -532,38 +532,38 @@ 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: [9000]", 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 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] The new blocked-port error embeds a YAML comment (# Do not list blocked service ports under allow-host-ports) inside the error message string.

💡 Suggestion

YAML comments in error text are confusing outside a YAML context and add noise. Replace with a plain prose sentence:

return fmt.Errorf("allow-host-ports value %d maps to blocked service %s. That port is always unreachable from allow-host-ports, even with legacy-security. Declare the service under services: instead. Example:\nservices:\n  db:\n    image: postgres\n    ports: [\"5432:5432\"]", port, service)

@copilot please address this.

}

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("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\"")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Two error paths — workflowData == nil and workflowData.Features == nil — now emit the same message, making it impossible to distinguish them in tests or logs.

💡 Suggestion

Give each branch a distinct message (they already existed as separate branches before this PR):

if workflowData == nil {
    return "", errors.New("workflow data is nil; cannot read dangerously-disable-sandbox-agent configuration")
}
if workflowData.Features == nil {
    return "", errors.New("features block is missing dangerously-disable-sandbox-agent configuration. ...")
}

This also makes test assertions more precise.

@copilot please address this.

}

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 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)
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
Expand Down
Loading