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
4 changes: 4 additions & 0 deletions pkg/console/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
)

// ANSI escape sequences for terminal control
//
// These two sequences are a deliberate exception to the Lipgloss-based styling used
// elsewhere in this package: Lipgloss has no equivalent for cursor/screen control
// operations (clear-screen, clear-to-end-of-line), so raw ANSI codes are required here.
const (
// ansiClearScreen clears the screen and moves cursor to home position
ansiClearScreen = "\033[H\033[2J"
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,7 @@ This appendix is generated from the current non-test Go source files in this pac
| `permissions_toolset_data.go` | `GitHubToolsetsData` | `type GitHubToolsetsData struct { Version string `json:"version"` Description string `json:"description"` Toolsets map[string]struct { Description string `json:"description"` ReadPermissions []string `json:"read_permissions"` WritePermissions []string `json:"write_permissions"` Tools []string `json:"tools"` } `json:"toolsets"` }` | GitHubToolsetsData represents the structure of the embedded JSON file |
| `repo_config.go` | `MaintenanceCompileConfig` | `type MaintenanceCompileConfig struct { // CreatePullRequestGitHubToken is the secret name used by the compile-workflows // maintenance job for GitHub API calls and branch pushes. When configured, // out-of-sync compiled workflows are reported via a deduplicated pull request // instead of an issue. CreatePullRequestGitHubToken string `json:"create_pull_request_github_token,omitempty"` }` | MaintenanceConfig holds maintenance-workflow-specific settings from aw. |
| `repo_config.go` | `MaintenanceConfig` | `type MaintenanceConfig struct { // RunsOn is the runner label or labels used for all jobs in agentics-maintenance.yml. RunsOn RunsOnValue `json:"runs_on,omitempty"` // ActionFailureIssueExpires configures expiration (in hours) for action // failure issues opened by the conclusion job. Defaults to 168 (7 days). ActionFailureIssueExpires int `json:"action_failure_issue_expires,omitempty"` // LabelTriggers controls all label-triggered jobs (disable_agentic_workflow, // label_apply_safe_outputs, etc.). // The value is treated as an opt-in flag: only true enables the jobs. // nil (omitted) or false both disable label-triggered jobs. // To opt in, set label_triggers: true in aw.json. LabelTriggers *bool `json:"label_triggers,omitempty"` // DisabledJobs lists maintenance job IDs that should be omitted from generated // agentics-maintenance workflows. DisabledJobs []string `json:"disabled_jobs,omitempty"` // Compile controls compile-workflows maintenance job behavior. Compile *MaintenanceCompileConfig `json:"compile,omitempty"` }` | Exported type declared in `repo_config.go`. |
| `repository_features_validation_wasm.go` | `RepositoryFeatures` | `type RepositoryFeatures struct { HasDiscussions bool HasIssues bool }` | Exported type declared in `repository_features_validation_wasm.go`. |
| `repository_features.go` | `RepositoryFeatures` | `type RepositoryFeatures struct { HasDiscussions bool HasIssues bool }` | Exported type declared in `repository_features.go`. |
| `runtime_definitions.go` | `RuntimeRequirement` | `type RuntimeRequirement struct { Runtime *Runtime Version string // Empty string means use default ExtraFields map[string]any // Additional 'with' fields from user's setup step (e.g., cache settings) GoModFile string // Path to go.mod file for Go runtime (Go-specific) IfCondition string // Optional GitHub Actions if condition Cooldown bool // If false, disables default dependency cooldown behavior for installs associated with this runtime }` | RuntimeRequirement represents a detected runtime requirement |
| `safe_jobs.go` | `SafeJobConfig` | `type SafeJobConfig struct { // Standard GitHub Actions job properties Name string `yaml:"name,omitempty"` Description string `yaml:"description,omitempty"` RunsOn RunsOnValue `yaml:"runs-on,omitempty"` If string `yaml:"if,omitempty"` Needs []string `yaml:"needs,omitempty"` Steps []any `yaml:"steps,omitempty"` Env map[string]string `yaml:"env,omitempty"` Permissions map[string]string `yaml:"permissions,omitempty"` // Additional safe-job specific properties Inputs map[string]*InputDefinition `yaml:"inputs,omitempty"` GitHubToken string `yaml:"github-token,omitempty"` Output string `yaml:"output,omitempty"` Max int `yaml:"max,omitempty"` // Maximum number of times this output type may be emitted per run (default: 1) }` | SafeJobConfig defines a safe job configuration with GitHub Actions job properties |
| `safe_outputs_actions.go` | `SafeOutputActionConfig` | `type SafeOutputActionConfig struct { Uses string `yaml:"uses"` Description string `yaml:"description,omitempty"` // optional override of the action's description Env map[string]string `yaml:"env,omitempty"` // additional environment variables for the injected step // Computed at compile time (not from frontmatter): ResolvedRef string `yaml:"-"` // Pinned action reference (e.g., "owner/repo@sha # v1") Inputs map[string]*ActionYAMLInput `yaml:"-"` // Inputs parsed from action.yml ActionDescription string `yaml:"-"` // Description from action.yml }` | SafeOutputActionConfig holds configuration for a single custom safe output action. |
Expand Down
14 changes: 14 additions & 0 deletions pkg/workflow/repository_features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// This file defines the RepositoryFeatures struct shared by both the native
// (repository_features_validation.go) and WASM/playground (repository_features_validation_wasm.go)
// build variants. It has no build tags so both variants compile against a single
// definition, preventing the two implementations' field lists from drifting apart.

package workflow

// RepositoryFeatures holds cached information about repository capabilities.
// In WASM builds its fields are never populated because feature queries require
// GitHub API access; see repository_features_validation_wasm.go for details.
type RepositoryFeatures struct {
HasDiscussions bool
HasIssues bool
}
90 changes: 72 additions & 18 deletions pkg/workflow/repository_features_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@ const checkRepositoryHasDiscussionsQuery = `query($owner: String!, $name: String
}
}`

// RepositoryFeatures holds cached information about repository capabilities
type RepositoryFeatures struct {
HasDiscussions bool
HasIssues bool
}

// Global cache for repository features and current repository info
var (
repositoryFeaturesCache = sync.Map{} // sync.Map is thread-safe and efficient for read-heavy workloads
Expand Down Expand Up @@ -157,7 +151,12 @@ func (c *Compiler) validateRepositoryFeatures(workflowData *WorkflowData) error
}
// Continue to return aggregated errors even if this check fails
} else if !hasIssues {
issueErr := fmt.Errorf("workflow uses safe-outputs.create-issue but repository %s does not have issues enabled. Enable issues in repository settings or remove create-issue from safe-outputs", repo)
issueErr := NewValidationError(
"safe-outputs.create-issue",
repo,
"repository requires issues to be enabled when create-issue is configured",
"Enable issues in repository settings or remove create-issue from safe-outputs. Example:\nsafe-outputs:\n add-comment: {}",
)
if returnErr := collector.Add(issueErr); returnErr != nil {
return returnErr // Fail-fast mode
}
Expand Down Expand Up @@ -190,12 +189,22 @@ func getCurrentRepositoryUncached() (string, error) {
// This works when in a git repository with GitHub remote and respects GH_REPO
repo, err := repository.Current()
if err != nil {
return "", fmt.Errorf("failed to get current repository: %w", err)
return "", NewValidationError(
"repository",
"",
"current repository requires a GitHub remote or GH_REPO",
fmt.Sprintf("Run from a GitHub repository checkout or set GH_REPO. Example: GH_REPO=github/gh-aw gh aw compile workflow.md. Underlying error: %v", err),
)
}

// Validate that owner and name are not empty
if repo.Owner == "" || repo.Name == "" {
return "", fmt.Errorf("repository owner or name is empty (owner: %q, name: %q)", repo.Owner, repo.Name)
return "", NewValidationError(
"repository",
fmt.Sprintf("owner=%q name=%q", repo.Owner, repo.Name),
"repository owner and name must be non-empty",
"Use owner/repo format. Example: github/gh-aw",
)
}

repoName := fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
Expand All @@ -210,7 +219,12 @@ func getRepositoryFeatures(repo string, verbose bool) (*RepositoryFeatures, erro
features, ok := cached.(*RepositoryFeatures)
if !ok {
repositoryFeaturesCache.Delete(repo)
return nil, fmt.Errorf("invalid repository feature cache entry for %s: expected *RepositoryFeatures, got %T", repo, cached)
return nil, NewValidationError(
"repository.features.cache",
repo,
"repository feature cache entry must contain *RepositoryFeatures",
fmt.Sprintf("Restart the process to rebuild the repository feature cache and retry. Example: gh aw compile workflow.md. Actual cache type: %T", cached),
)
}
repositoryFeaturesLog.Printf("Using cached repository features for: %s", repo)
return features, nil
Expand All @@ -224,14 +238,24 @@ func getRepositoryFeatures(repo string, verbose bool) (*RepositoryFeatures, erro
// Check discussions
hasDiscussions, err := checkRepositoryHasDiscussionsUncached(repo)
if err != nil {
return nil, fmt.Errorf("failed to check discussions: %w", err)
return nil, NewValidationError(
"repository.discussions",
repo,
"repository discussions status requires a successful GitHub API lookup",
fmt.Sprintf("Ensure the repository exists and the token can read repository metadata. Example: gh auth status. Underlying error: %v", err),
)
}
features.HasDiscussions = hasDiscussions

// Check issues
hasIssues, err := checkRepositoryHasIssuesUncached(repo)
if err != nil {
return nil, fmt.Errorf("failed to check issues: %w", err)
return nil, NewValidationError(
"repository.issues",
repo,
"repository issues status requires a successful GitHub API lookup",
fmt.Sprintf("Ensure the repository exists and the token can read repository metadata. Example: gh auth status. Underlying error: %v", err),
)
}
features.HasIssues = hasIssues

Expand All @@ -241,7 +265,12 @@ func getRepositoryFeatures(repo string, verbose bool) (*RepositoryFeatures, erro
actualFeatures, ok := actual.(*RepositoryFeatures)
if !ok {
repositoryFeaturesCache.Delete(repo)
return nil, fmt.Errorf("invalid repository feature cache entry for %s: expected *RepositoryFeatures, got %T", repo, actual)
return nil, NewValidationError(
"repository.features.cache",
repo,
"repository feature cache entry must contain *RepositoryFeatures",
fmt.Sprintf("Restart the process to rebuild the repository feature cache and retry. Example: gh aw compile workflow.md. Actual cache type: %T", actual),
)
}

repositoryFeaturesLog.Printf("Cached repository features for: %s (discussions: %v, issues: %v)", repo, actualFeatures.HasDiscussions, actualFeatures.HasIssues)
Expand Down Expand Up @@ -291,7 +320,12 @@ func checkRepositoryHasDiscussionsUncached(repo string) (bool, error) {
CacheTTL: repositoryFeaturesCacheTTL,
})
if err != nil {
return false, fmt.Errorf("failed to create GraphQL client: %w", err)
return false, NewValidationError(
"repository.discussions.client",
repo,
"failed to create GraphQL client",
fmt.Sprintf("Ensure GitHub authentication is configured before checking discussions. Example: gh auth login. Underlying error: %v", err),
)
}
return checkRepositoryHasDiscussionsUncachedWithClient(repo, client)
}
Expand All @@ -315,7 +349,12 @@ func checkRepositoryHasDiscussionsUncachedWithClient(repo string, client *api.Gr
defer cancel()

if err := client.DoWithContext(ctx, checkRepositoryHasDiscussionsQuery, map[string]any{"owner": owner, "name": name}, &response); err != nil {
return false, fmt.Errorf("failed to query discussions status: %w", err)
return false, NewValidationError(
"repository.discussions",
repo,
"failed to query discussions status",
fmt.Sprintf("Ensure the repository exists and the token can read repository discussions metadata. Example: gh auth status. Underlying error: %v", err),
)
}

return response.Repository.HasDiscussionsEnabled, nil
Expand Down Expand Up @@ -345,7 +384,12 @@ func checkRepositoryHasIssuesUncached(repo string) (bool, error) {
CacheTTL: repositoryFeaturesCacheTTL,
})
if err != nil {
return false, fmt.Errorf("failed to create REST client: %w", err)
return false, NewValidationError(
"repository.issues.client",
repo,
"failed to create REST client",
fmt.Sprintf("Ensure GitHub authentication is configured before checking issues. Example: gh auth login. Underlying error: %v", err),
)
}
return checkRepositoryHasIssuesUncachedWithClient(repo, client)
}
Expand All @@ -365,7 +409,12 @@ func checkRepositoryHasIssuesUncachedWithClient(repo string, client *api.RESTCli
// Fetch repository data using REST client with timeout context
var response RepositoryResponse
if err := client.DoWithContext(ctx, http.MethodGet, "repos/"+repo, nil, &response); err != nil {
return false, fmt.Errorf("failed to query repository: %w", err)
return false, NewValidationError(
"repository.issues",
repo,
"failed to query repository",
fmt.Sprintf("Ensure the repository exists and the token can read repository metadata. Example: gh auth status. Underlying error: %v", err),
)
}

return response.HasIssues, nil
Expand All @@ -379,7 +428,12 @@ func validateRepositoryName(repo string) error {
func parseRepositoryName(repo string) (owner string, name string, err error) {
parts := strings.SplitN(repo, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", fmt.Errorf("invalid repository format: %s. Expected format: owner/repo. Example: github/gh-aw", repo)
return "", "", NewValidationError(
"repository",
repo,
"invalid repository format. Expected format: owner/repo. Example: github/gh-aw",
"Use an owner/repo repository name. Example: github/gh-aw",
)
}

return parts[0], parts[1], nil
Expand Down
8 changes: 0 additions & 8 deletions pkg/workflow/repository_features_validation_wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@

package workflow

// RepositoryFeatures holds information about repository capabilities.
// In WASM builds this struct is kept for API compatibility with the native build; its
// fields are never populated because feature queries require GitHub API access.
type RepositoryFeatures struct {
HasDiscussions bool
HasIssues bool
}

// validateRepositoryFeatures is a no-op in WASM/playground builds.
//
// The native implementation queries the GitHub API to verify that features such as
Expand Down
Loading