diff --git a/docs/adr/52109-split-compiler-types-by-lifecycle.md b/docs/adr/52109-split-compiler-types-by-lifecycle.md new file mode 100644 index 00000000000..8b8163dfbde --- /dev/null +++ b/docs/adr/52109-split-compiler-types-by-lifecycle.md @@ -0,0 +1,62 @@ +# ADR-52109: Split compiler_types.go by Lifecycle Responsibility + +**Date**: 2026-08-11 +**Status**: Draft +**Deciders**: pelikhan (via copilot-swe-agent, PR #52109) + +--- + +### Context + +`pkg/workflow/compiler_types.go` had grown to mix three distinct lifecycle categories in a single file: + +1. **Build-time functional options** — `CompilerOption` type, `With*` builder functions, and the `NewCompiler` constructor. +2. **Post-construction runtime mutators/accessors** — ~38 `Set*/Get*` methods on `*Compiler` (e.g. `SetContext`, `SetStrictMode`, `GetSharedActionResolver`). +3. **Pure type declarations** — the `Compiler` struct, `FileCreationTracker` interface, `logTypes`, `FileCreationTracker`, and `allowedDomain`. + +The file exceeded 400 lines and made it hard to locate any given responsibility. This also prevented adding tests alongside each group without placing everything in a single, oversized test file. + +### Decision + +We will split `pkg/workflow/compiler_types.go` into three focused files within the same Go package (`package workflow`), each owning exactly one lifecycle group: + +- **`compiler_options.go`** — `CompilerOption` type, `With*` builders, and `NewCompiler`. +- **`compiler_mutators.go`** — all `*Compiler` setter/getter methods and lazily-initialized shared cache helpers (`ensureSharedActionCacheAndResolver`, `getSharedImportCache`). +- **`compiler_types.go`** — the `Compiler` struct, `FileCreationTracker` interface, `logTypes`, and `allowedDomain`. + +This is a pure mechanical refactor with no behavior change. All symbols remain in the same Go package, so no import paths change. + +### Alternatives Considered + +#### Alternative 1: Keep Everything in compiler_types.go + +Retain the status quo and leave all three lifecycle groups in one file. This avoids any file proliferation and is the zero-effort option. + +Rejected because the file already exceeded 400 lines and was on a growth trajectory. Mixing construction-time and runtime-mutation responsibilities in one file makes code review harder and obscures the public API surface for each lifecycle. + +#### Alternative 2: Split by Public vs. Private, Not by Lifecycle + +Group all public symbols in one file and all private helpers in another, regardless of their lifecycle role. + +Rejected because this does not capture the semantically important boundary between construction-time options (only called in `NewCompiler`) and post-construction mutators (called by callers after the compiler is built). The lifecycle split is the conceptually cleaner boundary for navigation and future extension. + +### Consequences + +#### Positive +- Each file has a single clear responsibility; the correct file to open for any given symbol is immediately obvious from the file name. +- Test files can be co-located with the files they cover (`compiler_options_test.go`, `compiler_mutators_test.go`), keeping tests close to the code they exercise. +- The public mutator API (38 relocated symbols) is now isolated in `compiler_mutators.go`, making it easy to audit what callers can change post-construction. +- Smaller individual files speed up code review of future changes to any one lifecycle group. + +#### Negative +- More files to navigate: contributors unfamiliar with the split must learn which file owns which kind of symbol (options vs. mutators vs. types). +- Future additions to `Compiler` require a judgment call about which file to place them in; the lifecycle boundary is not always clear-cut (e.g., `GetVersion` is a read-only accessor placed in `compiler_mutators.go` rather than `compiler_options.go`). + +#### Neutral +- No behavior change — this is a pure mechanical refactor. Existing tests pass without modification. +- `pkg/workflow/README.md` symbol table was updated to reference the new file names for the 38 relocated symbols. +- The orphaned doc comment for `SkipIfMatchConfig` (whose type lives in `workflow_data.go`) was removed as part of the cleanup. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* diff --git a/pkg/workflow/README.md b/pkg/workflow/README.md index 6f411b70baf..30c157f5bdd 100644 --- a/pkg/workflow/README.md +++ b/pkg/workflow/README.md @@ -1242,47 +1242,47 @@ This appendix is generated from the current non-test Go source files in this pac | `comment.go` | `ParseCommandEvents` | `func ParseCommandEvents(eventsValue any) []string` | ParseCommandEvents parses the events field from command configuration Returns a list of event identifiers to enable, or nil for default (all events) | | `compiler_experiments.go` | `ExperimentExpressionMappings` | `func ExperimentExpressionMappings(experiments map[string][]string) []*ExpressionMapping` | ExperimentExpressionMappings generates ExpressionMapping entries for all declared experiments. | | `compiler_filters_validation.go` | `ValidatePushBranchScope` | `func ValidatePushBranchScope(frontmatter map[string]any) error` | ValidatePushBranchScope ensures that any push event in the on: section specifies a branch or tag ref filter. | +| `compiler_mutators.go` | `(*Compiler).AddSafeUpdateWarning` | `func (*Compiler).AddSafeUpdateWarning(warning string)` | AddSafeUpdateWarning appends a safe update warning to the compiler's accumulated list. | +| `compiler_mutators.go` | `(*Compiler).EffectiveActionsRepo` | `func (*Compiler).EffectiveActionsRepo() string` | EffectiveActionsRepo returns the actions repository used for action mode references. | +| `compiler_mutators.go` | `(*Compiler).GetActionMode` | `func (*Compiler).GetActionMode() ActionMode` | GetActionMode returns the current action mode | +| `compiler_mutators.go` | `(*Compiler).GetActionTag` | `func (*Compiler).GetActionTag() string` | GetActionTag returns the action tag override (empty if not set) | +| `compiler_mutators.go` | `(*Compiler).GetRepositorySlug` | `func (*Compiler).GetRepositorySlug() string` | GetRepositorySlug returns the repository slug (owner/repo) set on this compiler instance. | +| `compiler_mutators.go` | `(*Compiler).GetSafeUpdateWarnings` | `func (*Compiler).GetSafeUpdateWarnings() []string` | GetSafeUpdateWarnings returns all accumulated safe update warnings for this compiler instance. | +| `compiler_mutators.go` | `(*Compiler).GetScheduleWarnings` | `func (*Compiler).GetScheduleWarnings() []string` | GetScheduleWarnings returns all accumulated schedule warnings for this compiler instance | +| `compiler_mutators.go` | `(*Compiler).GetSharedActionCache` | `func (*Compiler).GetSharedActionCache() *ActionCache` | GetSharedActionCache returns the shared action cache used by this compiler instance. | +| `compiler_mutators.go` | `(*Compiler).GetSharedActionResolver` | `func (*Compiler).GetSharedActionResolver() *ActionResolver` | GetSharedActionResolver returns the shared action resolver used by this compiler instance. | +| `compiler_mutators.go` | `(*Compiler).GetWarningCount` | `func (*Compiler).GetWarningCount() int` | GetWarningCount returns the current warning count | +| `compiler_mutators.go` | `(*Compiler).IncrementWarningCount` | `func (*Compiler).IncrementWarningCount()` | IncrementWarningCount increments the warning counter | +| `compiler_mutators.go` | `(*Compiler).IsRepositorySlugLocked` | `func (*Compiler).IsRepositorySlugLocked() bool` | IsRepositorySlugLocked reports whether the repository slug has been locked via LockRepositorySlug and must not be overridden by per-file detection. | +| `compiler_mutators.go` | `(*Compiler).LockRepositorySlug` | `func (*Compiler).LockRepositorySlug()` | LockRepositorySlug marks the repository slug as explicitly set (e. | +| `compiler_mutators.go` | `(*Compiler).ResetWarningCount` | `func (*Compiler).ResetWarningCount()` | ResetWarningCount resets the warning counter to zero | +| `compiler_mutators.go` | `(*Compiler).SetActionMode` | `func (*Compiler).SetActionMode(mode ActionMode)` | SetActionMode configures the action mode for JavaScript step generation | +| `compiler_mutators.go` | `(*Compiler).SetActionTag` | `func (*Compiler).SetActionTag(tag string)` | SetActionTag sets the action tag override for actions/setup | +| `compiler_mutators.go` | `(*Compiler).SetActionsRepo` | `func (*Compiler).SetActionsRepo(repo string)` | SetActionsRepo sets the external actions repository override. | +| `compiler_mutators.go` | `(*Compiler).SetAllowActionRefs` | `func (*Compiler).SetAllowActionRefs(allow bool)` | SetAllowActionRefs configures whether unresolved action refs are warnings. | +| `compiler_mutators.go` | `(*Compiler).SetApprove` | `func (*Compiler).SetApprove(approve bool)` | SetApprove configures whether to skip safe update enforcement via the CLI --approve flag. | +| `compiler_mutators.go` | `(*Compiler).SetContext` | `func (*Compiler).SetContext(ctx context.Context)` | SetContext sets the context used for network operations such as SHA resolution. | +| `compiler_mutators.go` | `(*Compiler).SetFileTracker` | `func (*Compiler).SetFileTracker(tracker FileCreationTracker)` | SetFileTracker sets the file tracker for tracking created files | +| `compiler_mutators.go` | `(*Compiler).SetForceRefreshActionPins` | `func (*Compiler).SetForceRefreshActionPins(force bool)` | SetForceRefreshActionPins configures whether to force refresh of action pins | +| `compiler_mutators.go` | `(*Compiler).SetForceStaged` | `func (*Compiler).SetForceStaged(force bool)` | SetForceStaged configures whether safe-outputs should always compile in staged mode. | +| `compiler_mutators.go` | `(*Compiler).SetGHESCompat` | `func (*Compiler).SetGHESCompat(enabled bool)` | SetGHESCompat enables GHES compatibility mode via the --ghes CLI flag. | +| `compiler_mutators.go` | `(*Compiler).SetModelPricingResolver` | `func (*Compiler).SetModelPricingResolver(fn func(ctx context.Context, provider, model string) (map[string]float64, bool))` | SetModelPricingResolver registers a callback used to resolve pricing for models that are not present in the embedded models. | +| `compiler_mutators.go` | `(*Compiler).SetNoEmit` | `func (*Compiler).SetNoEmit(noEmit bool)` | SetNoEmit configures whether to validate without generating lock files | +| `compiler_mutators.go` | `(*Compiler).SetPriorManifests` | `func (*Compiler).SetPriorManifests(manifests map[string]*GHAWManifest)` | SetPriorManifests replaces the entire pre-cached manifest map. | +| `compiler_mutators.go` | `(*Compiler).SetQuiet` | `func (*Compiler).SetQuiet(quiet bool)` | SetQuiet configures whether to suppress success messages (for interactive mode) | +| `compiler_mutators.go` | `(*Compiler).SetRefreshStopTime` | `func (*Compiler).SetRefreshStopTime(refresh bool)` | SetRefreshStopTime configures whether to force regeneration of stop-after times | +| `compiler_mutators.go` | `(*Compiler).SetRepositorySlug` | `func (*Compiler).SetRepositorySlug(slug string)` | SetRepositorySlug sets the repository slug for schedule scattering | +| `compiler_mutators.go` | `(*Compiler).SetRepositorySlugIfUnlocked` | `func (*Compiler).SetRepositorySlugIfUnlocked(slug string)` | SetRepositorySlugIfUnlocked sets the repository slug only when it has not been locked via LockRepositorySlug. | +| `compiler_mutators.go` | `(*Compiler).SetRequireDocker` | `func (*Compiler).SetRequireDocker(require bool)` | SetRequireDocker configures whether Docker must be available for container image validation. | +| `compiler_mutators.go` | `(*Compiler).SetSkipValidation` | `func (*Compiler).SetSkipValidation(skip bool)` | SetSkipValidation configures whether to skip schema validation | +| `compiler_mutators.go` | `(*Compiler).SetStrictMode` | `func (*Compiler).SetStrictMode(strict bool)` | SetStrictMode configures whether to enable strict validation mode | +| `compiler_mutators.go` | `(*Compiler).SetTrialLogicalRepoSlug` | `func (*Compiler).SetTrialLogicalRepoSlug(repo string)` | SetTrialLogicalRepoSlug configures the target repository for trial mode | +| `compiler_mutators.go` | `(*Compiler).SetTrialMode` | `func (*Compiler).SetTrialMode(trialMode bool)` | SetTrialMode configures whether to run in trial mode (suppresses safe outputs) | +| `compiler_mutators.go` | `(*Compiler).SetUseSamples` | `func (*Compiler).SetUseSamples(use bool)` | SetUseSamples configures whether to replace the agentic step with a deterministic replay driver that feeds `samples` entries to the safe-outputs MCP server via real `tools/call` JSON-RPC. | +| `compiler_mutators.go` | `(*Compiler).SetWorkflowIdentifier` | `func (*Compiler).SetWorkflowIdentifier(identifier string)` | SetWorkflowIdentifier sets the identifier for the current workflow being compiled This is used for deterministic schedule scattering | | `compiler_orchestrator_workflow.go` | `(*Compiler).ParseWorkflowFile` | `func (*Compiler).ParseWorkflowFile(markdownPath string) (*WorkflowData, error)` | ParseWorkflowFile parses a workflow markdown file and returns a WorkflowData structure. | | `compiler_string_api.go` | `(*Compiler).CompileToYAML` | `func (*Compiler).CompileToYAML(workflowData *WorkflowData, markdownPath string) (string, error)` | CompileToYAML compiles workflow data and returns the YAML as a string without writing to disk. | | `compiler_string_api.go` | `(*Compiler).ParseWorkflowString` | `func (*Compiler).ParseWorkflowString(content string, virtualPath string) (*WorkflowData, error)` | ParseWorkflowString parses workflow markdown content from a string rather than a file. | -| `compiler_types.go` | `(*Compiler).AddSafeUpdateWarning` | `func (*Compiler).AddSafeUpdateWarning(warning string)` | AddSafeUpdateWarning appends a safe update warning to the compiler's accumulated list. | -| `compiler_types.go` | `(*Compiler).EffectiveActionsRepo` | `func (*Compiler).EffectiveActionsRepo() string` | EffectiveActionsRepo returns the actions repository used for action mode references. | -| `compiler_types.go` | `(*Compiler).GetActionMode` | `func (*Compiler).GetActionMode() ActionMode` | GetActionMode returns the current action mode | -| `compiler_types.go` | `(*Compiler).GetActionTag` | `func (*Compiler).GetActionTag() string` | GetActionTag returns the action tag override (empty if not set) | -| `compiler_types.go` | `(*Compiler).GetRepositorySlug` | `func (*Compiler).GetRepositorySlug() string` | GetRepositorySlug returns the repository slug (owner/repo) set on this compiler instance. | -| `compiler_types.go` | `(*Compiler).GetSafeUpdateWarnings` | `func (*Compiler).GetSafeUpdateWarnings() []string` | GetSafeUpdateWarnings returns all accumulated safe update warnings for this compiler instance. | -| `compiler_types.go` | `(*Compiler).GetScheduleWarnings` | `func (*Compiler).GetScheduleWarnings() []string` | GetScheduleWarnings returns all accumulated schedule warnings for this compiler instance | -| `compiler_types.go` | `(*Compiler).GetSharedActionCache` | `func (*Compiler).GetSharedActionCache() *ActionCache` | GetSharedActionCache returns the shared action cache used by this compiler instance. | -| `compiler_types.go` | `(*Compiler).GetSharedActionResolver` | `func (*Compiler).GetSharedActionResolver() *ActionResolver` | GetSharedActionResolver returns the shared action resolver used by this compiler instance. | -| `compiler_types.go` | `(*Compiler).GetWarningCount` | `func (*Compiler).GetWarningCount() int` | GetWarningCount returns the current warning count | -| `compiler_types.go` | `(*Compiler).IncrementWarningCount` | `func (*Compiler).IncrementWarningCount()` | IncrementWarningCount increments the warning counter | -| `compiler_types.go` | `(*Compiler).IsRepositorySlugLocked` | `func (*Compiler).IsRepositorySlugLocked() bool` | IsRepositorySlugLocked reports whether the repository slug has been locked via LockRepositorySlug and must not be overridden by per-file detection. | -| `compiler_types.go` | `(*Compiler).LockRepositorySlug` | `func (*Compiler).LockRepositorySlug()` | LockRepositorySlug marks the repository slug as explicitly set (e. | -| `compiler_types.go` | `(*Compiler).ResetWarningCount` | `func (*Compiler).ResetWarningCount()` | ResetWarningCount resets the warning counter to zero | -| `compiler_types.go` | `(*Compiler).SetActionMode` | `func (*Compiler).SetActionMode(mode ActionMode)` | SetActionMode configures the action mode for JavaScript step generation | -| `compiler_types.go` | `(*Compiler).SetActionTag` | `func (*Compiler).SetActionTag(tag string)` | SetActionTag sets the action tag override for actions/setup | -| `compiler_types.go` | `(*Compiler).SetActionsRepo` | `func (*Compiler).SetActionsRepo(repo string)` | SetActionsRepo sets the external actions repository override. | -| `compiler_types.go` | `(*Compiler).SetAllowActionRefs` | `func (*Compiler).SetAllowActionRefs(allow bool)` | SetAllowActionRefs configures whether unresolved action refs are warnings. | -| `compiler_types.go` | `(*Compiler).SetApprove` | `func (*Compiler).SetApprove(approve bool)` | SetApprove configures whether to skip safe update enforcement via the CLI --approve flag. | -| `compiler_types.go` | `(*Compiler).SetContext` | `func (*Compiler).SetContext(ctx context.Context)` | SetContext sets the context used for network operations such as SHA resolution. | -| `compiler_types.go` | `(*Compiler).SetFileTracker` | `func (*Compiler).SetFileTracker(tracker FileCreationTracker)` | SetFileTracker sets the file tracker for tracking created files | -| `compiler_types.go` | `(*Compiler).SetForceRefreshActionPins` | `func (*Compiler).SetForceRefreshActionPins(force bool)` | SetForceRefreshActionPins configures whether to force refresh of action pins | -| `compiler_types.go` | `(*Compiler).SetForceStaged` | `func (*Compiler).SetForceStaged(force bool)` | SetForceStaged configures whether safe-outputs should always compile in staged mode. | -| `compiler_types.go` | `(*Compiler).SetGHESCompat` | `func (*Compiler).SetGHESCompat(enabled bool)` | SetGHESCompat enables GHES compatibility mode via the --ghes CLI flag. | -| `compiler_types.go` | `(*Compiler).SetModelPricingResolver` | `func (*Compiler).SetModelPricingResolver(fn func(ctx context.Context, provider, model string) (map[string]float64, bool))` | SetModelPricingResolver registers a callback used to resolve pricing for models that are not present in the embedded models. | -| `compiler_types.go` | `(*Compiler).SetNoEmit` | `func (*Compiler).SetNoEmit(noEmit bool)` | SetNoEmit configures whether to validate without generating lock files | -| `compiler_types.go` | `(*Compiler).SetPriorManifests` | `func (*Compiler).SetPriorManifests(manifests map[string]*GHAWManifest)` | SetPriorManifests replaces the entire pre-cached manifest map. | -| `compiler_types.go` | `(*Compiler).SetQuiet` | `func (*Compiler).SetQuiet(quiet bool)` | SetQuiet configures whether to suppress success messages (for interactive mode) | -| `compiler_types.go` | `(*Compiler).SetRefreshStopTime` | `func (*Compiler).SetRefreshStopTime(refresh bool)` | SetRefreshStopTime configures whether to force regeneration of stop-after times | -| `compiler_types.go` | `(*Compiler).SetRepositorySlug` | `func (*Compiler).SetRepositorySlug(slug string)` | SetRepositorySlug sets the repository slug for schedule scattering | -| `compiler_types.go` | `(*Compiler).SetRepositorySlugIfUnlocked` | `func (*Compiler).SetRepositorySlugIfUnlocked(slug string)` | SetRepositorySlugIfUnlocked sets the repository slug only when it has not been locked via LockRepositorySlug. | -| `compiler_types.go` | `(*Compiler).SetRequireDocker` | `func (*Compiler).SetRequireDocker(require bool)` | SetRequireDocker configures whether Docker must be available for container image validation. | -| `compiler_types.go` | `(*Compiler).SetSkipValidation` | `func (*Compiler).SetSkipValidation(skip bool)` | SetSkipValidation configures whether to skip schema validation | -| `compiler_types.go` | `(*Compiler).SetStrictMode` | `func (*Compiler).SetStrictMode(strict bool)` | SetStrictMode configures whether to enable strict validation mode | -| `compiler_types.go` | `(*Compiler).SetTrialLogicalRepoSlug` | `func (*Compiler).SetTrialLogicalRepoSlug(repo string)` | SetTrialLogicalRepoSlug configures the target repository for trial mode | -| `compiler_types.go` | `(*Compiler).SetTrialMode` | `func (*Compiler).SetTrialMode(trialMode bool)` | SetTrialMode configures whether to run in trial mode (suppresses safe outputs) | -| `compiler_types.go` | `(*Compiler).SetUseSamples` | `func (*Compiler).SetUseSamples(use bool)` | SetUseSamples configures whether to replace the agentic step with a deterministic replay driver that feeds `samples` entries to the safe-outputs MCP server via real `tools/call` JSON-RPC. | -| `compiler_types.go` | `(*Compiler).SetWorkflowIdentifier` | `func (*Compiler).SetWorkflowIdentifier(identifier string)` | SetWorkflowIdentifier sets the identifier for the current workflow being compiled This is used for deterministic schedule scattering | | `compiler_workflow_helpers.go` | `ContainsCheckout` | `func ContainsCheckout(customSteps string) bool` | ContainsCheckout returns true if the given custom steps contain an actions/checkout step | | `config_helpers.go` | `ParseBoolFromConfig` | `func ParseBoolFromConfig(m map[string]any, key string, debugLog *logger.Logger) bool` | ParseBoolFromConfig is a generic helper that extracts and validates a boolean value from a map. | | `config_helpers.go` | `ParseStringArrayFromConfig` | `func ParseStringArrayFromConfig(m map[string]any, key string, debugLog *logger.Logger) []string` | ParseStringArrayFromConfig is a generic helper that extracts and validates a string array from a map Returns a slice of strings, or nil if not present or invalid If log is provided, it will log the extracted values for … | diff --git a/pkg/workflow/compiler_mutators.go b/pkg/workflow/compiler_mutators.go new file mode 100644 index 00000000000..0807809297f --- /dev/null +++ b/pkg/workflow/compiler_mutators.go @@ -0,0 +1,334 @@ +package workflow + +import ( + "context" + "maps" + "os" + + "github.com/github/gh-aw/pkg/parser" +) + +// SetSkipValidation configures whether to skip schema validation +func (c *Compiler) SetSkipValidation(skip bool) { + c.skipValidation = skip +} + +// SetContext sets the context used for network operations such as SHA resolution. +func (c *Compiler) SetContext(ctx context.Context) { + c.ctx = ctx +} + +// SetModelPricingResolver registers a callback used to resolve pricing for models that are +// not present in the embedded models.json catalog. The resolver receives the workflow's +// inference provider and model name; it should return per-token pricing (USD) and true when +// pricing is available, or (nil, false) when it is not. Injected by the cli package so that +// the compiler can fetch missing pricing from models.dev without a circular import. +func (c *Compiler) SetModelPricingResolver(fn func(ctx context.Context, provider, model string) (map[string]float64, bool)) { + c.modelPricingResolver = fn +} + +// SetRequireDocker configures whether Docker must be available for container image validation. +// When true, validation fails with an error if Docker is not installed or the daemon is not running. +// When false (default), validation is silently skipped when Docker is unavailable. +func (c *Compiler) SetRequireDocker(require bool) { + c.requireDocker = require +} + +// SetQuiet configures whether to suppress success messages (for interactive mode) +func (c *Compiler) SetQuiet(quiet bool) { + c.quiet = quiet +} + +// SetBatchMode configures whether repetitive notices should be aggregated. +func (c *Compiler) SetBatchMode(batchMode bool) { + c.batchMode = batchMode +} + +// GetExperimentalFeatureUsage returns experimental feature usage counts collected in batch mode. +func (c *Compiler) GetExperimentalFeatureUsage() map[string]int { + usage := make(map[string]int, len(c.featureUsage)) + maps.Copy(usage, c.featureUsage) + return usage +} + +// CopilotRequestsTipNeeded reports whether batch output should show the token-based inference tip. +func (c *Compiler) CopilotRequestsTipNeeded() bool { + return c.copilotTipNeeded +} + +// SetExperimentalFeatureUsage replaces the experimental feature usage map. +// Intended for use in tests that need to exercise aggregation output. +func (c *Compiler) SetExperimentalFeatureUsage(usage map[string]int) { + c.featureUsage = usage +} + +// SetCopilotTipNeeded sets whether the Copilot billing tip should be shown. +// Intended for use in tests that need to exercise aggregation output. +func (c *Compiler) SetCopilotTipNeeded(needed bool) { + c.copilotTipNeeded = needed +} + +// SetNoEmit configures whether to validate without generating lock files +func (c *Compiler) SetNoEmit(noEmit bool) { + c.noEmit = noEmit +} + +// SetApprove configures whether to skip safe update enforcement via the CLI --approve flag. +// When true, safe update enforcement is disabled regardless of strict mode setting, +// approving all changes. +func (c *Compiler) SetApprove(approve bool) { + c.approve = approve +} + +// SetForceStaged configures whether safe-outputs should always compile in staged mode. +func (c *Compiler) SetForceStaged(force bool) { + c.forceStaged = force +} + +// SetFileTracker sets the file tracker for tracking created files +func (c *Compiler) SetFileTracker(tracker FileCreationTracker) { + c.fileTracker = tracker +} + +// SetTrialMode configures whether to run in trial mode (suppresses safe outputs) +func (c *Compiler) SetTrialMode(trialMode bool) { + c.trialMode = trialMode +} + +// SetTrialLogicalRepoSlug configures the target repository for trial mode +func (c *Compiler) SetTrialLogicalRepoSlug(repo string) { + c.trialLogicalRepoSlug = repo +} + +// SetUseSamples configures whether to replace the agentic step with a +// deterministic replay driver that feeds `samples` entries to the safe-outputs +// MCP server via real `tools/call` JSON-RPC. Hidden feature used by +// `gh aw compile --use-samples`. +func (c *Compiler) SetUseSamples(use bool) { + c.useSamples = use +} + +// SetStrictMode configures whether to enable strict validation mode +func (c *Compiler) SetStrictMode(strict bool) { + c.strictMode = strict +} + +// SetAllowActionRefs configures whether unresolved action refs are warnings. +// When false (default), unresolved action refs are compiler errors. +func (c *Compiler) SetAllowActionRefs(allow bool) { + c.allowActionRefs = allow +} + +// SetGHESCompat enables GHES compatibility mode via the --ghes CLI flag. +// It overrides the aw.json ghes field for the current compilation run. +// Artifact actions still use the latest non-v3 pins. +func (c *Compiler) SetGHESCompat(enabled bool) { + c.ghesCompatFromCLI = enabled +} + +// SetRefreshStopTime configures whether to force regeneration of stop-after times +func (c *Compiler) SetRefreshStopTime(refresh bool) { + c.refreshStopTime = refresh +} + +// SetForceRefreshActionPins configures whether to force refresh of action pins +func (c *Compiler) SetForceRefreshActionPins(force bool) { + c.forceRefreshActionPins = force +} + +// SetActionMode configures the action mode for JavaScript step generation +func (c *Compiler) SetActionMode(mode ActionMode) { + c.actionMode = mode +} + +// GetActionMode returns the current action mode +func (c *Compiler) GetActionMode() ActionMode { + return c.actionMode +} + +// SetActionTag sets the action tag override for actions/setup +func (c *Compiler) SetActionTag(tag string) { + c.actionTag = tag +} + +// GetActionTag returns the action tag override (empty if not set) +func (c *Compiler) GetActionTag() string { + return c.actionTag +} + +// SetActionsRepo sets the external actions repository override. +// When set, this overrides the default "github/gh-aw-actions" repository used in action mode. +func (c *Compiler) SetActionsRepo(repo string) { + c.actionsRepo = repo +} + +// effectiveActionsRepo returns the actions repository to use for action mode references. +// Returns the override if set, otherwise returns the default GitHubActionsOrgRepo constant. +func (c *Compiler) effectiveActionsRepo() string { + if c.actionsRepo != "" { + return c.actionsRepo + } + return GitHubActionsOrgRepo +} + +// EffectiveActionsRepo returns the actions repository used for action mode references. +// Returns the override if set, otherwise returns the default GitHubActionsOrgRepo. +func (c *Compiler) EffectiveActionsRepo() string { + return c.effectiveActionsRepo() +} + +// GetVersion returns the version string used by the compiler +func (c *Compiler) GetVersion() string { + return c.version +} + +// IncrementWarningCount increments the warning counter +func (c *Compiler) IncrementWarningCount() { + c.warningCount++ +} + +// GetWarningCount returns the current warning count +func (c *Compiler) GetWarningCount() int { + return c.warningCount +} + +// ResetWarningCount resets the warning counter to zero +func (c *Compiler) ResetWarningCount() { + c.warningCount = 0 +} + +// SetWorkflowIdentifier sets the identifier for the current workflow being compiled +// This is used for deterministic schedule scattering +func (c *Compiler) SetWorkflowIdentifier(identifier string) { + c.workflowIdentifier = identifier +} + +// SetRepositorySlug sets the repository slug for schedule scattering +func (c *Compiler) SetRepositorySlug(slug string) { + c.repositorySlug = slug +} + +// LockRepositorySlug marks the repository slug as explicitly set (e.g. via --schedule-seed) +// so that per-file git-remote detection cannot override it. +func (c *Compiler) LockRepositorySlug() { + c.repositorySlugLocked = true +} + +// IsRepositorySlugLocked reports whether the repository slug has been locked +// via LockRepositorySlug and must not be overridden by per-file detection. +func (c *Compiler) IsRepositorySlugLocked() bool { + return c.repositorySlugLocked +} + +// SetRepositorySlugIfUnlocked sets the repository slug only when it has not been +// locked via LockRepositorySlug. This is the method per-file git-remote detection +// should call so that an explicit --schedule-seed flag is never overridden. +func (c *Compiler) SetRepositorySlugIfUnlocked(slug string) { + if !c.repositorySlugLocked { + c.SetRepositorySlug(slug) + } +} + +// GetRepositorySlug returns the repository slug (owner/repo) set on this compiler instance. +func (c *Compiler) GetRepositorySlug() string { + return c.repositorySlug +} + +// GetScheduleWarnings returns all accumulated schedule warnings for this compiler instance +func (c *Compiler) GetScheduleWarnings() []string { + return c.scheduleWarnings +} + +// AddSafeUpdateWarning appends a safe update warning to the compiler's accumulated list. +// Callers should invoke this when a safe update violation is detected instead of +// returning a compilation error, so that compilation still succeeds and the agent +// receives actionable guidance. +func (c *Compiler) AddSafeUpdateWarning(warning string) { + if c.safeUpdateWarnings == nil { + c.safeUpdateWarnings = []string{} + } + c.safeUpdateWarnings = append(c.safeUpdateWarnings, warning) +} + +// GetSafeUpdateWarnings returns all accumulated safe update warnings for this compiler instance. +func (c *Compiler) GetSafeUpdateWarnings() []string { + return c.safeUpdateWarnings +} + +// SetPriorManifests replaces the entire pre-cached manifest map. +func (c *Compiler) SetPriorManifests(manifests map[string]*GHAWManifest) { + if manifests == nil { + manifests = make(map[string]*GHAWManifest) + } + c.priorManifests = manifests +} + +// ensureSharedActionCacheAndResolver lazily initializes (on first call) and returns the +// compiler's shared ActionCache and ActionResolver pair. The resolver always wraps the +// returned cache, so both values are initialized and returned together to keep that +// pairing explicit; all workflows compiled by this compiler instance share the same +// in-memory cache. +func (c *Compiler) ensureSharedActionCacheAndResolver() (*ActionCache, *ActionResolver) { + if c.actionCache == nil { + // Initialize cache and resolver on first use + // Use git root if provided, otherwise fall back to current working directory + baseDir := c.gitRoot + if baseDir == "" { + cwd, err := os.Getwd() + if err != nil { + cwd = "." + } + baseDir = cwd + } + c.actionCache = NewActionCache(baseDir) + + // Load existing cache unless force refresh is enabled + if !c.forceRefreshActionPins { + _ = c.actionCache.Load() // Ignore errors if cache doesn't exist + } else { + logTypes.Print("Force refresh action pins enabled: skipping cache load and will resolve all actions dynamically") + // Mark as cleared since we skipped loading + c.actionCacheCleared = true + } + + c.actionResolver = NewActionResolver(c.actionCache) + logTypes.Print("Initialized shared action cache and resolver for compiler") + } else if c.forceRefreshActionPins && !c.actionCacheCleared { + // If cache already exists but force refresh is set and we haven't cleared it yet, clear it once + logTypes.Print("Force refresh action pins: clearing existing cache once for this run") + c.actionCache.Entries = make(map[string]ActionCacheEntry) + c.actionCacheCleared = true + } + return c.actionCache, c.actionResolver +} + +// getSharedImportCache returns the shared import cache, initializing it on first use +// This ensures all workflows compiled by this compiler instance share the same import cache +func (c *Compiler) getSharedImportCache() *parser.ImportCache { + if c.importCache == nil { + // Initialize cache on first use + cwd, err := os.Getwd() + if err != nil { + cwd = "." + } + c.importCache = parser.NewImportCache(cwd) + logTypes.Print("Initialized shared import cache for compiler") + } + return c.importCache +} + +// GetSharedActionCache returns the shared action cache used by this compiler instance. +// The cache is lazily initialized on first access and shared across all workflows. +// This allows action SHA validation and other operations to reuse cached resolutions. +func (c *Compiler) GetSharedActionCache() *ActionCache { + cache, _ := c.ensureSharedActionCacheAndResolver() + return cache +} + +// GetSharedActionResolver returns the shared action resolver used by this compiler instance. +// The resolver is lazily initialized on first access and shared across all workflows. +// It tracks which cache keys were used during compilation, enabling orphaned-entry pruning. +func (c *Compiler) GetSharedActionResolver() *ActionResolver { + _, resolver := c.ensureSharedActionCacheAndResolver() + return resolver +} diff --git a/pkg/workflow/compiler_mutators_test.go b/pkg/workflow/compiler_mutators_test.go new file mode 100644 index 00000000000..421fdb4a367 --- /dev/null +++ b/pkg/workflow/compiler_mutators_test.go @@ -0,0 +1,108 @@ +//go:build !integration + +package workflow + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCompilerSetSkipValidation(t *testing.T) { + c := NewCompiler() + + c.SetSkipValidation(false) + assert.False(t, c.skipValidation) +} + +func TestCompilerSetContext(t *testing.T) { + c := NewCompiler() + + ctx := context.WithValue(context.Background(), struct{ k string }{"k"}, "v") + c.SetContext(ctx) + assert.Equal(t, ctx, c.ctx) +} + +func TestCompilerSetNoEmit(t *testing.T) { + c := NewCompiler() + + c.SetNoEmit(true) + assert.True(t, c.noEmit) +} + +func TestCompilerSetStrictMode(t *testing.T) { + c := NewCompiler() + + c.SetStrictMode(true) + assert.True(t, c.strictMode) +} + +func TestCompilerSetActionTag(t *testing.T) { + c := NewCompiler() + + c.SetActionTag("v1") + assert.Equal(t, "v1", c.GetActionTag()) +} + +func TestCompilerMutatorsSetActionMode(t *testing.T) { + c := NewCompiler() + + c.SetActionMode(ActionModeRelease) + assert.Equal(t, ActionModeRelease, c.GetActionMode()) +} + +func TestCompilerWarningCount(t *testing.T) { + c := NewCompiler() + + c.IncrementWarningCount() + assert.Equal(t, 1, c.GetWarningCount()) + c.ResetWarningCount() + assert.Equal(t, 0, c.GetWarningCount()) +} + +func TestCompilerSafeUpdateWarnings(t *testing.T) { + c := NewCompiler() + + c.AddSafeUpdateWarning("warning") + assert.Equal(t, []string{"warning"}, c.GetSafeUpdateWarnings()) +} + +func TestCompilerRepositorySlugLocking(t *testing.T) { + c := NewCompiler() + + c.SetRepositorySlug("owner/repo") + assert.Equal(t, "owner/repo", c.GetRepositorySlug()) + assert.False(t, c.IsRepositorySlugLocked()) + + c.SetRepositorySlugIfUnlocked("other/repo") + assert.Equal(t, "other/repo", c.GetRepositorySlug()) + + c.LockRepositorySlug() + assert.True(t, c.IsRepositorySlugLocked()) + + c.SetRepositorySlugIfUnlocked("ignored/repo") + assert.Equal(t, "other/repo", c.GetRepositorySlug()) +} + +func TestCompilerSharedActionCacheAndResolver(t *testing.T) { + c := NewCompiler() + + cache := c.GetSharedActionCache() + require.NotNil(t, cache) + resolver := c.GetSharedActionResolver() + require.NotNil(t, resolver) + + // Subsequent calls reuse the same shared instances + assert.Same(t, cache, c.GetSharedActionCache()) + assert.Same(t, resolver, c.GetSharedActionResolver()) +} + +func TestCompilerSetPriorManifestsNilResetsMap(t *testing.T) { + c := NewCompiler() + + c.SetPriorManifests(nil) + assert.NotNil(t, c.priorManifests) + assert.Empty(t, c.priorManifests) +} diff --git a/pkg/workflow/compiler_options.go b/pkg/workflow/compiler_options.go new file mode 100644 index 00000000000..8bcb554d365 --- /dev/null +++ b/pkg/workflow/compiler_options.go @@ -0,0 +1,104 @@ +package workflow + +import ( + "context" +) + +// CompilerOption is a functional option for configuring a Compiler +type CompilerOption func(*Compiler) + +// WithVerbose sets the verbose logging flag +func WithVerbose(verbose bool) CompilerOption { + return func(c *Compiler) { c.verbose = verbose } +} + +// WithEngineOverride sets the AI engine override +func WithEngineOverride(engine string) CompilerOption { + return func(c *Compiler) { c.engineOverride = engine } +} + +// WithSkipValidation configures whether to skip schema validation +func WithSkipValidation(skip bool) CompilerOption { + return func(c *Compiler) { c.skipValidation = skip } +} + +// WithNoEmit configures whether to validate without generating lock files +func WithNoEmit(noEmit bool) CompilerOption { + return func(c *Compiler) { c.noEmit = noEmit } +} + +// WithFailFast configures whether to stop at first validation error +func WithFailFast(failFast bool) CompilerOption { + return func(c *Compiler) { c.failFast = failFast } +} + +// WithWorkflowIdentifier sets the identifier for the current workflow being compiled +func WithWorkflowIdentifier(identifier string) CompilerOption { + return func(c *Compiler) { c.workflowIdentifier = identifier } +} + +// WithVersion sets the compiler version, used to determine action mode and version-specific behavior +func WithVersion(version string) CompilerOption { + return func(c *Compiler) { c.version = version } +} + +// NewCompiler creates a new workflow compiler with functional options. +// By default, it auto-detects the version and action mode. +// +// Available options: +// - WithVerbose: enable verbose logging +// - WithEngineOverride: force a specific AI engine +// - WithSkipValidation: skip schema validation +// - WithNoEmit: validate without generating lock files +// - WithFailFast: stop at the first validation error +// - WithWorkflowIdentifier: set the identifier for the workflow being compiled +// - WithVersion: set the compiler version (also re-derives actionMode) +// +// Constructor options (With*) configure values that are fixed for the +// lifetime of the Compiler and are only meaningful before compilation +// begins. Runtime mutators (Set*, defined in compiler_mutators.go) are for +// state that changes after construction, such as SetContext for per-run +// cancellation/deadlines or SetStrictMode for per-workflow overrides. +func NewCompiler(opts ...CompilerOption) *Compiler { + // Get the current compiler version (set by SetVersion during CLI initialization) + version := GetVersion() + + // Auto-detect git repository root for action cache path resolution + // This ensures actions-lock.json is created at repo root regardless of CWD + gitRoot := findGitRoot() + + engineRegistry := NewEngineRegistry() + + // Create compiler with defaults + c := &Compiler{ + ctx: context.Background(), // Default context; override with SetContext + verbose: false, + engineOverride: "", + version: version, + skipValidation: true, // Skip validation by default for now since existing workflows don't fully comply + jobManager: NewJobManager(), + engineRegistry: engineRegistry, + engineCatalog: NewEngineCatalog(engineRegistry), + stepOrderTracker: NewStepOrderTracker(), + artifactManager: NewArtifactManager(), + actionPinWarnings: make(map[string]bool), // Initialize warning cache + priorManifests: make(map[string]*GHAWManifest), + ownerTypeCache: make(map[string]string), // Initialize owner-type cache (keyed by owner login) + copilotRequestsTipShown: make(map[string]bool), // Initialize one-time tip tracking (keyed by markdown path) + featureUsage: make(map[string]int), // Initialize batch feature usage counts + permissionWarningShown: make(map[string]string), // Initialize one-time permission warning tracking (keyed by markdown path) + allowedDomainsCache: make(map[string]allowedDomain), // Initialize allowed-domains cache (keyed by markdown path) + gitRoot: gitRoot, // Auto-detected git root + } + + // Apply functional options + for _, opt := range opts { + opt(c) + } + // Auto-detect action mode based on version in case version has been update + c.actionMode = DetectActionMode(c.version) + + logTypes.Printf("Created compiler: version=%s, actionMode=%s, skipValidation=%t, strictMode=%t", c.version, c.actionMode, c.skipValidation, c.strictMode) + + return c +} diff --git a/pkg/workflow/compiler_options_test.go b/pkg/workflow/compiler_options_test.go new file mode 100644 index 00000000000..2571e5ef4ee --- /dev/null +++ b/pkg/workflow/compiler_options_test.go @@ -0,0 +1,39 @@ +//go:build !integration + +package workflow + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCompilerOptionsAppliedByNewCompiler(t *testing.T) { + c := NewCompiler( + WithVerbose(true), + WithEngineOverride("claude"), + WithSkipValidation(false), + WithNoEmit(true), + WithFailFast(true), + WithWorkflowIdentifier("my-workflow"), + WithVersion("v1.2.3"), + ) + + assert.True(t, c.verbose) + assert.Equal(t, "claude", c.engineOverride) + assert.False(t, c.skipValidation) + assert.True(t, c.noEmit) + assert.True(t, c.failFast) + assert.Equal(t, "my-workflow", c.workflowIdentifier) + assert.Equal(t, "v1.2.3", c.GetVersion()) +} + +func TestNewCompilerDefaults(t *testing.T) { + c := NewCompiler() + + assert.False(t, c.verbose) + assert.Empty(t, c.engineOverride) + assert.True(t, c.skipValidation) + assert.NotNil(t, c.ctx) + assert.Equal(t, DetectActionMode(c.GetVersion()), c.GetActionMode()) +} diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 78ba73f9819..2853507e0c3 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -234,7 +234,7 @@ func (c *Compiler) populateWorkflowBuildContext(ctx *workflowBuildContext) error } func (c *Compiler) attachSharedActionResolver(workflowData *WorkflowData) { - actionCache, actionResolver := c.getSharedActionResolver() + actionCache, actionResolver := c.ensureSharedActionCacheAndResolver() workflowData.Ctx = c.ctx workflowData.ActionCache = actionCache workflowData.ActionResolver = actionResolver diff --git a/pkg/workflow/compiler_shared_cache_test.go b/pkg/workflow/compiler_shared_cache_test.go index 5b3ec83551b..7185e3d65e2 100644 --- a/pkg/workflow/compiler_shared_cache_test.go +++ b/pkg/workflow/compiler_shared_cache_test.go @@ -21,7 +21,7 @@ func TestCompilerSharedActionCache(t *testing.T) { compiler := NewCompiler() // Get the shared action resolver (first time - should initialize) - cache1, resolver1 := compiler.getSharedActionResolver() + cache1, resolver1 := compiler.ensureSharedActionCacheAndResolver() if cache1 == nil { t.Error("Expected cache to be initialized") } @@ -33,7 +33,7 @@ func TestCompilerSharedActionCache(t *testing.T) { cache1.Set("actions/checkout", "v5", "test-sha-abc") // Get the shared action resolver again (should be same instance) - cache2, resolver2 := compiler.getSharedActionResolver() + cache2, resolver2 := compiler.ensureSharedActionCacheAndResolver() // Verify it's the same instance if cache1 != cache2 { @@ -140,7 +140,7 @@ func TestCompilerForceRefreshClearsOnlyOnce(t *testing.T) { compiler.SetForceRefreshActionPins(true) // Get the shared action resolver (first time - should initialize empty) - cache1, _ := compiler.getSharedActionResolver() + cache1, _ := compiler.ensureSharedActionCacheAndResolver() if cache1 == nil { t.Fatal("Expected cache to be initialized") } @@ -160,7 +160,7 @@ func TestCompilerForceRefreshClearsOnlyOnce(t *testing.T) { } // Get the shared action resolver again (second workflow in same run) - cache2, _ := compiler.getSharedActionResolver() + cache2, _ := compiler.ensureSharedActionCacheAndResolver() // Verify it's the same instance if cache1 != cache2 { @@ -190,7 +190,7 @@ func TestCompilerForceRefreshClearsOnlyOnce(t *testing.T) { } // Get the resolver a third time (third workflow in same run) - cache3, _ := compiler.getSharedActionResolver() + cache3, _ := compiler.ensureSharedActionCacheAndResolver() // Verify it's still the same instance with entries intact if cache1 != cache3 { diff --git a/pkg/workflow/compiler_string_api.go b/pkg/workflow/compiler_string_api.go index f348fe3fd8f..ff8aff49759 100644 --- a/pkg/workflow/compiler_string_api.go +++ b/pkg/workflow/compiler_string_api.go @@ -190,7 +190,7 @@ func (c *Compiler) ParseWorkflowString(content string, virtualPath string) (*Wor } // Setup action cache and resolver - actionCache, actionResolver := c.getSharedActionResolver() + actionCache, actionResolver := c.ensureSharedActionCacheAndResolver() workflowData.Ctx = c.ctx workflowData.ActionCache = actionCache workflowData.ActionResolver = actionResolver diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 9c5e50feee0..972a543da0f 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -2,8 +2,6 @@ package workflow import ( "context" - "maps" - "os" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" @@ -11,44 +9,6 @@ import ( var logTypes = logger.New("workflow:compiler_types") -// CompilerOption is a functional option for configuring a Compiler -type CompilerOption func(*Compiler) - -// WithVerbose sets the verbose logging flag -func WithVerbose(verbose bool) CompilerOption { - return func(c *Compiler) { c.verbose = verbose } -} - -// WithEngineOverride sets the AI engine override -func WithEngineOverride(engine string) CompilerOption { - return func(c *Compiler) { c.engineOverride = engine } -} - -// WithSkipValidation configures whether to skip schema validation -func WithSkipValidation(skip bool) CompilerOption { - return func(c *Compiler) { c.skipValidation = skip } -} - -// WithNoEmit configures whether to validate without generating lock files -func WithNoEmit(noEmit bool) CompilerOption { - return func(c *Compiler) { c.noEmit = noEmit } -} - -// WithFailFast configures whether to stop at first validation error -func WithFailFast(failFast bool) CompilerOption { - return func(c *Compiler) { c.failFast = failFast } -} - -// WithWorkflowIdentifier sets the identifier for the current workflow being compiled -func WithWorkflowIdentifier(identifier string) CompilerOption { - return func(c *Compiler) { c.workflowIdentifier = identifier } -} - -// WithVersion sets the compiler version, used to determine action mode and version-specific behavior -func WithVersion(version string) CompilerOption { - return func(c *Compiler) { c.version = version } -} - // FileCreationTracker interface for tracking files created during compilation type FileCreationTracker interface { TrackCreated(filePath string) @@ -126,375 +86,3 @@ type allowedDomain struct { frontmatterHash string domains string } - -// NewCompiler creates a new workflow compiler with functional options. -// By default, it auto-detects the version and action mode. -// Common options: WithVerbose, WithEngineOverride, WithNoEmit, WithSkipValidation -func NewCompiler(opts ...CompilerOption) *Compiler { - // Get the current compiler version (set by SetVersion during CLI initialization) - version := GetVersion() - - // Auto-detect git repository root for action cache path resolution - // This ensures actions-lock.json is created at repo root regardless of CWD - gitRoot := findGitRoot() - - engineRegistry := NewEngineRegistry() - - // Create compiler with defaults - c := &Compiler{ - ctx: context.Background(), // Default context; override with WithContext - verbose: false, - engineOverride: "", - version: version, - skipValidation: true, // Skip validation by default for now since existing workflows don't fully comply - actionMode: DetectActionMode(version), // Auto-detect action mode based on version - jobManager: NewJobManager(), - engineRegistry: engineRegistry, - engineCatalog: NewEngineCatalog(engineRegistry), - stepOrderTracker: NewStepOrderTracker(), - artifactManager: NewArtifactManager(), - actionPinWarnings: make(map[string]bool), // Initialize warning cache - priorManifests: make(map[string]*GHAWManifest), - ownerTypeCache: make(map[string]string), // Initialize owner-type cache (keyed by owner login) - copilotRequestsTipShown: make(map[string]bool), // Initialize one-time tip tracking (keyed by markdown path) - featureUsage: make(map[string]int), // Initialize batch feature usage counts - permissionWarningShown: make(map[string]string), // Initialize one-time permission warning tracking (keyed by markdown path) - allowedDomainsCache: make(map[string]allowedDomain), // Initialize allowed-domains cache (keyed by markdown path) - gitRoot: gitRoot, // Auto-detected git root - } - - // Apply functional options - for _, opt := range opts { - opt(c) - } - // Auto-detect action mode based on version in case version has been update - c.actionMode = DetectActionMode(c.version) - - logTypes.Printf("Created compiler: version=%s, actionMode=%s, skipValidation=%t, strictMode=%t", c.version, c.actionMode, c.skipValidation, c.strictMode) - - return c -} - -// SetSkipValidation configures whether to skip schema validation -func (c *Compiler) SetSkipValidation(skip bool) { - c.skipValidation = skip -} - -// SetContext sets the context used for network operations such as SHA resolution. -func (c *Compiler) SetContext(ctx context.Context) { - c.ctx = ctx -} - -// SetModelPricingResolver registers a callback used to resolve pricing for models that are -// not present in the embedded models.json catalog. The resolver receives the workflow's -// inference provider and model name; it should return per-token pricing (USD) and true when -// pricing is available, or (nil, false) when it is not. Injected by the cli package so that -// the compiler can fetch missing pricing from models.dev without a circular import. -func (c *Compiler) SetModelPricingResolver(fn func(ctx context.Context, provider, model string) (map[string]float64, bool)) { - c.modelPricingResolver = fn -} - -// SetRequireDocker configures whether Docker must be available for container image validation. -// When true, validation fails with an error if Docker is not installed or the daemon is not running. -// When false (default), validation is silently skipped when Docker is unavailable. -func (c *Compiler) SetRequireDocker(require bool) { - c.requireDocker = require -} - -// SetQuiet configures whether to suppress success messages (for interactive mode) -func (c *Compiler) SetQuiet(quiet bool) { - c.quiet = quiet -} - -// SetBatchMode configures whether repetitive notices should be aggregated. -func (c *Compiler) SetBatchMode(batchMode bool) { - c.batchMode = batchMode -} - -// GetExperimentalFeatureUsage returns experimental feature usage counts collected in batch mode. -func (c *Compiler) GetExperimentalFeatureUsage() map[string]int { - usage := make(map[string]int, len(c.featureUsage)) - maps.Copy(usage, c.featureUsage) - return usage -} - -// CopilotRequestsTipNeeded reports whether batch output should show the token-based inference tip. -func (c *Compiler) CopilotRequestsTipNeeded() bool { - return c.copilotTipNeeded -} - -// SetExperimentalFeatureUsage replaces the experimental feature usage map. -// Intended for use in tests that need to exercise aggregation output. -func (c *Compiler) SetExperimentalFeatureUsage(usage map[string]int) { - c.featureUsage = usage -} - -// SetCopilotTipNeeded sets whether the Copilot billing tip should be shown. -// Intended for use in tests that need to exercise aggregation output. -func (c *Compiler) SetCopilotTipNeeded(needed bool) { - c.copilotTipNeeded = needed -} - -// SetNoEmit configures whether to validate without generating lock files -func (c *Compiler) SetNoEmit(noEmit bool) { - c.noEmit = noEmit -} - -// SetApprove configures whether to skip safe update enforcement via the CLI --approve flag. -// When true, safe update enforcement is disabled regardless of strict mode setting, -// approving all changes. -func (c *Compiler) SetApprove(approve bool) { - c.approve = approve -} - -// SetForceStaged configures whether safe-outputs should always compile in staged mode. -func (c *Compiler) SetForceStaged(force bool) { - c.forceStaged = force -} - -// SetFileTracker sets the file tracker for tracking created files -func (c *Compiler) SetFileTracker(tracker FileCreationTracker) { - c.fileTracker = tracker -} - -// SetTrialMode configures whether to run in trial mode (suppresses safe outputs) -func (c *Compiler) SetTrialMode(trialMode bool) { - c.trialMode = trialMode -} - -// SetTrialLogicalRepoSlug configures the target repository for trial mode -func (c *Compiler) SetTrialLogicalRepoSlug(repo string) { - c.trialLogicalRepoSlug = repo -} - -// SetUseSamples configures whether to replace the agentic step with a -// deterministic replay driver that feeds `samples` entries to the safe-outputs -// MCP server via real `tools/call` JSON-RPC. Hidden feature used by -// `gh aw compile --use-samples`. -func (c *Compiler) SetUseSamples(use bool) { - c.useSamples = use -} - -// SetStrictMode configures whether to enable strict validation mode -func (c *Compiler) SetStrictMode(strict bool) { - c.strictMode = strict -} - -// SetAllowActionRefs configures whether unresolved action refs are warnings. -// When false (default), unresolved action refs are compiler errors. -func (c *Compiler) SetAllowActionRefs(allow bool) { - c.allowActionRefs = allow -} - -// SetGHESCompat enables GHES compatibility mode via the --ghes CLI flag. -// It overrides the aw.json ghes field for the current compilation run. -// Artifact actions still use the latest non-v3 pins. -func (c *Compiler) SetGHESCompat(enabled bool) { - c.ghesCompatFromCLI = enabled -} - -// SetRefreshStopTime configures whether to force regeneration of stop-after times -func (c *Compiler) SetRefreshStopTime(refresh bool) { - c.refreshStopTime = refresh -} - -// SetForceRefreshActionPins configures whether to force refresh of action pins -func (c *Compiler) SetForceRefreshActionPins(force bool) { - c.forceRefreshActionPins = force -} - -// SetActionMode configures the action mode for JavaScript step generation -func (c *Compiler) SetActionMode(mode ActionMode) { - c.actionMode = mode -} - -// GetActionMode returns the current action mode -func (c *Compiler) GetActionMode() ActionMode { - return c.actionMode -} - -// SetActionTag sets the action tag override for actions/setup -func (c *Compiler) SetActionTag(tag string) { - c.actionTag = tag -} - -// GetActionTag returns the action tag override (empty if not set) -func (c *Compiler) GetActionTag() string { - return c.actionTag -} - -// SetActionsRepo sets the external actions repository override. -// When set, this overrides the default "github/gh-aw-actions" repository used in action mode. -func (c *Compiler) SetActionsRepo(repo string) { - c.actionsRepo = repo -} - -// effectiveActionsRepo returns the actions repository to use for action mode references. -// Returns the override if set, otherwise returns the default GitHubActionsOrgRepo constant. -func (c *Compiler) effectiveActionsRepo() string { - if c.actionsRepo != "" { - return c.actionsRepo - } - return GitHubActionsOrgRepo -} - -// EffectiveActionsRepo returns the actions repository used for action mode references. -// Returns the override if set, otherwise returns the default GitHubActionsOrgRepo. -func (c *Compiler) EffectiveActionsRepo() string { - return c.effectiveActionsRepo() -} - -// GetVersion returns the version string used by the compiler -func (c *Compiler) GetVersion() string { - return c.version -} - -// IncrementWarningCount increments the warning counter -func (c *Compiler) IncrementWarningCount() { - c.warningCount++ -} - -// GetWarningCount returns the current warning count -func (c *Compiler) GetWarningCount() int { - return c.warningCount -} - -// ResetWarningCount resets the warning counter to zero -func (c *Compiler) ResetWarningCount() { - c.warningCount = 0 -} - -// SetWorkflowIdentifier sets the identifier for the current workflow being compiled -// This is used for deterministic schedule scattering -func (c *Compiler) SetWorkflowIdentifier(identifier string) { - c.workflowIdentifier = identifier -} - -// SetRepositorySlug sets the repository slug for schedule scattering -func (c *Compiler) SetRepositorySlug(slug string) { - c.repositorySlug = slug -} - -// LockRepositorySlug marks the repository slug as explicitly set (e.g. via --schedule-seed) -// so that per-file git-remote detection cannot override it. -func (c *Compiler) LockRepositorySlug() { - c.repositorySlugLocked = true -} - -// IsRepositorySlugLocked reports whether the repository slug has been locked -// via LockRepositorySlug and must not be overridden by per-file detection. -func (c *Compiler) IsRepositorySlugLocked() bool { - return c.repositorySlugLocked -} - -// SetRepositorySlugIfUnlocked sets the repository slug only when it has not been -// locked via LockRepositorySlug. This is the method per-file git-remote detection -// should call so that an explicit --schedule-seed flag is never overridden. -func (c *Compiler) SetRepositorySlugIfUnlocked(slug string) { - if !c.repositorySlugLocked { - c.SetRepositorySlug(slug) - } -} - -// GetRepositorySlug returns the repository slug (owner/repo) set on this compiler instance. -func (c *Compiler) GetRepositorySlug() string { - return c.repositorySlug -} - -// GetScheduleWarnings returns all accumulated schedule warnings for this compiler instance -func (c *Compiler) GetScheduleWarnings() []string { - return c.scheduleWarnings -} - -// AddSafeUpdateWarning appends a safe update warning to the compiler's accumulated list. -// Callers should invoke this when a safe update violation is detected instead of -// returning a compilation error, so that compilation still succeeds and the agent -// receives actionable guidance. -func (c *Compiler) AddSafeUpdateWarning(warning string) { - if c.safeUpdateWarnings == nil { - c.safeUpdateWarnings = []string{} - } - c.safeUpdateWarnings = append(c.safeUpdateWarnings, warning) -} - -// GetSafeUpdateWarnings returns all accumulated safe update warnings for this compiler instance. -func (c *Compiler) GetSafeUpdateWarnings() []string { - return c.safeUpdateWarnings -} - -// SetPriorManifests replaces the entire pre-cached manifest map. -func (c *Compiler) SetPriorManifests(manifests map[string]*GHAWManifest) { - if manifests == nil { - manifests = make(map[string]*GHAWManifest) - } - c.priorManifests = manifests -} - -// getSharedActionResolver returns the shared action resolver, initializing it on first use -// This ensures all workflows compiled by this compiler instance share the same in-memory cache -func (c *Compiler) getSharedActionResolver() (*ActionCache, *ActionResolver) { - if c.actionCache == nil { - // Initialize cache and resolver on first use - // Use git root if provided, otherwise fall back to current working directory - baseDir := c.gitRoot - if baseDir == "" { - cwd, err := os.Getwd() - if err != nil { - cwd = "." - } - baseDir = cwd - } - c.actionCache = NewActionCache(baseDir) - - // Load existing cache unless force refresh is enabled - if !c.forceRefreshActionPins { - _ = c.actionCache.Load() // Ignore errors if cache doesn't exist - } else { - logTypes.Print("Force refresh action pins enabled: skipping cache load and will resolve all actions dynamically") - // Mark as cleared since we skipped loading - c.actionCacheCleared = true - } - - c.actionResolver = NewActionResolver(c.actionCache) - logTypes.Print("Initialized shared action cache and resolver for compiler") - } else if c.forceRefreshActionPins && !c.actionCacheCleared { - // If cache already exists but force refresh is set and we haven't cleared it yet, clear it once - logTypes.Print("Force refresh action pins: clearing existing cache once for this run") - c.actionCache.Entries = make(map[string]ActionCacheEntry) - c.actionCacheCleared = true - } - return c.actionCache, c.actionResolver -} - -// getSharedImportCache returns the shared import cache, initializing it on first use -// This ensures all workflows compiled by this compiler instance share the same import cache -func (c *Compiler) getSharedImportCache() *parser.ImportCache { - if c.importCache == nil { - // Initialize cache on first use - cwd, err := os.Getwd() - if err != nil { - cwd = "." - } - c.importCache = parser.NewImportCache(cwd) - logTypes.Print("Initialized shared import cache for compiler") - } - return c.importCache -} - -// GetSharedActionCache returns the shared action cache used by this compiler instance. -// The cache is lazily initialized on first access and shared across all workflows. -// This allows action SHA validation and other operations to reuse cached resolutions. -func (c *Compiler) GetSharedActionCache() *ActionCache { - cache, _ := c.getSharedActionResolver() - return cache -} - -// GetSharedActionResolver returns the shared action resolver used by this compiler instance. -// The resolver is lazily initialized on first access and shared across all workflows. -// It tracks which cache keys were used during compilation, enabling orphaned-entry pruning. -func (c *Compiler) GetSharedActionResolver() *ActionResolver { - _, resolver := c.getSharedActionResolver() - return resolver -} - -// SkipIfMatchConfig holds the configuration for skip-if-match conditions