diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index 2fc9e41cdc2..c9629cc1120 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -205,7 +205,7 @@ Examples: ` + string(constants.CLIExtensionPrefix) + ` enable ci-doctor --repo owner/repo # Enable workflow in specific repository`, RunE: func(cmd *cobra.Command, args []string) error { repoOverride, _ := cmd.Flags().GetString("repo") - return cli.EnableWorkflowsByNames(args, repoOverride) + return cli.EnableWorkflowsByNames(cmd.Context(), args, repoOverride) }, } @@ -226,7 +226,7 @@ Examples: ` + string(constants.CLIExtensionPrefix) + ` disable ci-doctor --repo owner/repo # Disable workflow in specific repository`, RunE: func(cmd *cobra.Command, args []string) error { repoOverride, _ := cmd.Flags().GetString("repo") - return cli.DisableWorkflowsByNames(args, repoOverride) + return cli.DisableWorkflowsByNames(cmd.Context(), args, repoOverride) }, } diff --git a/docs/adr/32295-thread-context-through-resolvesha-call-chains.md b/docs/adr/32295-thread-context-through-resolvesha-call-chains.md new file mode 100644 index 00000000000..f1b8f6c8f4c --- /dev/null +++ b/docs/adr/32295-thread-context-through-resolvesha-call-chains.md @@ -0,0 +1,77 @@ +## ADR-32295: Thread `context.Context` Through `ResolveSHA` Call Chains + +**Date**: 2026-05-15 +**Status**: Draft +**Deciders**: Unknown (auto-generated from PR diff) + +--- + +### Part 1 — Narrative (Human-Friendly) + +#### Context + +`ActionResolver.ResolveSHA` performs `gh api` network calls to resolve action references to pinned commit SHAs and is invoked from compile, lock-file validation, maintenance-workflow generation, and the Copilot setup pipeline. Five production call sites passed a hardcoded `context.Background()`, which meant cobra's signal-aware `cmd.Context()` (Ctrl-C, timeouts) could not propagate into the network layer, and callers had no way to inject deadlines or cancellation. The hardcoded boundaries were spread across `pkg/workflow/` and `pkg/cli/` and reached through long internal call chains, so the fix is not local: every intermediate function must accept and forward `ctx`. The goal of this change is to eliminate all hidden `context.Background()` usage on the resolution path and confine `context.Background()` to top-level CLI entry points and tests. + +#### Decision + +We will thread `context.Context` end-to-end from cobra `RunE` handlers (via `cmd.Context()`) down to every `ActionResolver.ResolveSHA` invocation, requiring all intermediate functions in `pkg/workflow/` and `pkg/cli/` to take `ctx context.Context` as their first parameter. To support struct-based call paths, the `Compiler` type gains a `ctx` field with `WithContext(ctx)` option and `SetContext(ctx)` mutator, defaulting to `context.Background()` only inside `NewCompiler()` so existing struct constructions remain valid. After this change, `context.Background()` is permitted only at top-level CLI entry points (as a fallback when `cmd.Context()` is unavailable) and in tests; all other call sites **must** propagate `ctx` received from above. + +#### Alternatives Considered + +##### Alternative 1: Keep `context.Background()` and add timeouts inside `ResolveSHA` + +Leave call signatures unchanged and apply a fixed timeout (e.g. `context.WithTimeout(context.Background(), 30s)`) inside `ResolveSHA` itself. Rejected because it solves only the hang-protection symptom and not the cancellation problem: Ctrl-C from cobra still cannot interrupt a hung `gh api` call, tests cannot inject deterministic contexts, and the timeout policy becomes invisible to the caller that actually knows the budget. + +##### Alternative 2: Stash context in a package-level or `ActionResolver` field + +Set the context once (e.g. on `ActionResolver`) and have `ResolveSHA` read it from `r.ctx` instead of taking a parameter. Rejected because it spreads context lifetime across goroutines and resolver reuse, contradicts the Go convention that `ctx` is the first parameter of any call that may block, and makes per-call deadline injection awkward. + +##### Alternative 3: Thread `ctx` only into `Compiler` and a shallow wrapper + +Add `ctx` to the `Compiler` struct (the centerpiece of the change) but leave the free functions in `pkg/workflow/` (`CheckActionSHAUpdates`, `ValidateActionSHAsInLockFile`, `ResolveSetupActionReference`, …) and their `pkg/cli/` callers unchanged, having them all read from a single ambient context. Rejected because several of these functions are reachable from CLI entry points that do not construct a `Compiler` (e.g. `update_actions.go`, `enable.go`, `add_command.go`), so a partial threading would leave the original five hardcoded boundaries intact under different names. + +#### Consequences + +##### Positive + +- Cobra's `cmd.Context()` (which is cancelled on SIGINT/SIGTERM) now flows into every `gh api` network call, so `gh aw compile`, `gh aw update-actions`, etc. can be interrupted cleanly while a resolution is in flight. +- Per-call-site deadlines become possible without touching `ResolveSHA`: callers can wrap with `context.WithTimeout` at the boundary that knows the budget. +- Tests can inject controlled or already-cancelled contexts to exercise cancellation paths deterministically. + +##### Negative + +- The change has very broad signature churn — roughly two dozen functions across `pkg/workflow/` and `pkg/cli/` (compile pipeline, `CheckActionSHAUpdates`, `ValidateActionSHAsInLockFile`, `ResolveSetupActionReference`, `GenerateMaintenanceWorkflow`, the entire `copilot_setup.go` chain, `AddResolvedWorkflows`, `EnableWorkflowsByNames`, `InitRepository`, …) take a new first parameter, and any downstream branch or fork must rebase against it. +- The `Compiler` struct now has two ways to supply context (constructor option `WithContext` and post-hoc `SetContext`), with a `context.Background()` fallback inside `NewCompiler()`; future readers must understand that the fallback exists only for compatibility and is not the intended path. +- `InitOptions.Ctx` is a nullable field (`InitRepository` falls back to `context.Background()` if nil), which is inconsistent with the "ctx is the first parameter" rule applied everywhere else and may need follow-up to align. + +##### Neutral + +- `context.Background()` remains in the codebase, but only at top-level CLI entry points (as a fallback when `cmd.Context()` is unavailable) and inside tests; a future lint rule could enforce this boundary. +- The change is mechanical at most call sites and produces no behavior change when contexts are never cancelled, so it is low-risk to revert if needed. + +--- + +### Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +#### Context Propagation on the Action-Resolution Path + +1. Every call site of `ActionResolver.ResolveSHA` **MUST** pass a `context.Context` obtained from its caller; it **MUST NOT** pass a freshly constructed `context.Background()` or `context.TODO()`. +2. Every internal function in `pkg/workflow/` and `pkg/cli/` that transitively calls `ActionResolver.ResolveSHA` **MUST** accept a `ctx context.Context` as its first parameter and forward it to its callees. +3. `context.Background()` **MUST NOT** appear on the resolution call chain except at top-level CLI entry points (cobra `RunE` handlers, `main`) as a fallback when `cmd.Context()` is unavailable, and in test files. + +#### Compiler and Init Boundaries + +1. The `Compiler` type **MUST** expose a way to associate a `context.Context` with the compiler instance (e.g. `WithContext(ctx)` constructor option and/or `SetContext(ctx)` method). +2. `NewCompiler()` **MAY** default the embedded context to `context.Background()` when no context is supplied, but compile entry points reachable from a cobra command **SHOULD** call `SetContext(cmd.Context())` (or equivalent) before invoking compile. +3. CLI entry-point handlers (cobra `RunE`) **MUST** pass `cmd.Context()` into the first downstream function call rather than `context.Background()`. +4. `InitOptions.Ctx` **MAY** be nil; `InitRepository` **MUST** fall back to `context.Background()` only when `Ctx` is nil. + +#### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/25916052628) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 54f2882ceb6..7a00a19760e 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -188,13 +188,13 @@ func AddWorkflows(ctx context.Context, workflows []string, opts AddOptions) (*Ad return nil, err } - return AddResolvedWorkflows(workflows, resolved, opts) + return AddResolvedWorkflows(ctx, workflows, resolved, opts) } // AddResolvedWorkflows adds workflows using pre-resolved workflow data. // This allows callers to resolve workflows early (e.g., to show descriptions) and then add them later. // The opts.Quiet parameter suppresses detailed output (useful for interactive mode where output is already shown). -func AddResolvedWorkflows(workflowStrings []string, resolved *ResolvedWorkflows, opts AddOptions) (*AddWorkflowsResult, error) { +func AddResolvedWorkflows(ctx context.Context, workflowStrings []string, resolved *ResolvedWorkflows, opts AddOptions) (*AddWorkflowsResult, error) { addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, noGitattributes=%v, opts.WorkflowDir=%s, noStopAfter=%v, stopAfter=%s", len(workflowStrings), opts.EngineOverride, opts.CreatePR, opts.NoGitattributes, opts.WorkflowDir, opts.NoStopAfter, opts.StopAfter) result := &AddWorkflowsResult{} @@ -230,7 +230,7 @@ func AddResolvedWorkflows(workflowStrings []string, resolved *ResolvedWorkflows, // Handle PR creation workflow if opts.CreatePR { addLog.Print("Creating workflow with PR") - prNumber, prURL, err := addWorkflowsWithPR(resolved.Workflows, opts) + prNumber, prURL, err := addWorkflowsWithPR(ctx, resolved.Workflows, opts) if err != nil { return nil, err } @@ -241,19 +241,19 @@ func AddResolvedWorkflows(workflowStrings []string, resolved *ResolvedWorkflows, // Handle normal workflow addition - pass resolved workflows with content addLog.Print("Adding workflows normally without PR") - return result, addWorkflows(resolved.Workflows, opts) + return result, addWorkflows(ctx, resolved.Workflows, opts) } // addWorkflows handles workflow addition using pre-fetched content -func addWorkflows(workflows []*ResolvedWorkflow, opts AddOptions) error { +func addWorkflows(ctx context.Context, workflows []*ResolvedWorkflow, opts AddOptions) error { addLog.Printf("Adding %d workflow(s) to repository", len(workflows)) // Create file tracker for all operations tracker := NewFileTracker() - return addWorkflowsWithTracking(workflows, tracker, opts) + return addWorkflowsWithTracking(ctx, workflows, tracker, opts) } // addWorkflows handles workflow addition using pre-fetched content -func addWorkflowsWithTracking(workflows []*ResolvedWorkflow, tracker *FileTracker, opts AddOptions) error { +func addWorkflowsWithTracking(ctx context.Context, workflows []*ResolvedWorkflow, tracker *FileTracker, opts AddOptions) error { addLog.Printf("Adding %d workflow(s) with tracking: force=%v, disableSecurityScanner=%v", len(workflows), opts.Force, opts.DisableSecurityScanner) // Ensure .gitattributes is configured unless flag is set if !opts.NoGitattributes { @@ -279,7 +279,7 @@ func addWorkflowsWithTracking(workflows []*ResolvedWorkflow, tracker *FileTracke fmt.Fprintln(os.Stderr, console.FormatProgressMessage(fmt.Sprintf("Adding workflow %d/%d: %s", i+1, len(workflows), resolved.Spec.WorkflowName))) } - if err := addWorkflowWithTracking(resolved, tracker, opts); err != nil { + if err := addWorkflowWithTracking(ctx, resolved, tracker, opts); err != nil { return fmt.Errorf("failed to add workflow '%s': %w", resolved.Spec.String(), err) } } @@ -292,7 +292,7 @@ func addWorkflowsWithTracking(workflows []*ResolvedWorkflow, tracker *FileTracke } // addWorkflowWithTracking adds a workflow using pre-fetched content with file tracking -func addWorkflowWithTracking(resolved *ResolvedWorkflow, tracker *FileTracker, opts AddOptions) error { +func addWorkflowWithTracking(ctx context.Context, resolved *ResolvedWorkflow, tracker *FileTracker, opts AddOptions) error { workflowSpec := resolved.Spec sourceContent := resolved.Content sourceInfo := resolved.SourceInfo @@ -364,7 +364,7 @@ func addWorkflowWithTracking(resolved *ResolvedWorkflow, tracker *FileTracker, o // For remote workflows, fetch and save all dependencies (includes, imports, dispatch workflows, resources) if !isLocalWorkflowPath(workflowSpec.WorkflowPath) { - if err := fetchAllRemoteDependencies(context.Background(), string(sourceContent), workflowSpec, githubWorkflowsDir, opts.Verbose, opts.Force, tracker); err != nil { + if err := fetchAllRemoteDependencies(ctx, string(sourceContent), workflowSpec, githubWorkflowsDir, opts.Verbose, opts.Force, tracker); err != nil { return err } } else if sourceInfo != nil && sourceInfo.IsLocal { @@ -537,15 +537,15 @@ func addWorkflowWithTracking(resolved *ResolvedWorkflow, tracker *FileTracker, o // Compile any dispatch-workflow .md dependencies that were just fetched and lack a // .lock.yml. The dispatch-workflow validator requires every .md dispatch target to be // compiled before the main workflow can be validated. - compileDispatchWorkflowDependencies(destFile, opts.Verbose, opts.Quiet, opts.EngineOverride, tracker) + compileDispatchWorkflowDependencies(ctx, destFile, opts.Verbose, opts.Quiet, opts.EngineOverride, tracker) // Compile the workflow if tracker != nil { - if err := compileWorkflowWithTracking(destFile, opts.Verbose, opts.Quiet, opts.EngineOverride, tracker); err != nil { + if err := compileWorkflowWithTracking(ctx, destFile, opts.Verbose, opts.Quiet, opts.EngineOverride, tracker); err != nil { printCompilationError(err, opts.Quiet) } } else { - if err := compileWorkflow(destFile, opts.Verbose, opts.Quiet, opts.EngineOverride); err != nil { + if err := compileWorkflow(ctx, destFile, opts.Verbose, opts.Quiet, opts.EngineOverride); err != nil { printCompilationError(err, opts.Quiet) } } diff --git a/pkg/cli/add_command_test.go b/pkg/cli/add_command_test.go index 7973c3d6439..246b4b036e4 100644 --- a/pkg/cli/add_command_test.go +++ b/pkg/cli/add_command_test.go @@ -151,6 +151,7 @@ func TestAddResolvedWorkflows(t *testing.T) { opts := AddOptions{} _, err := AddResolvedWorkflows( + context.Background(), []string{"test/repo/test-workflow"}, resolved, opts, @@ -461,7 +462,7 @@ func TestAddWorkflowWithTracking_SourceFieldVariants(t *testing.T) { } opts := AddOptions{DisableSecurityScanner: true} - err := addWorkflowWithTracking(resolved, nil, opts) + err := addWorkflowWithTracking(context.Background(), resolved, nil, opts) require.NoError(t, err, "addWorkflowWithTracking should succeed") written, err := os.ReadFile(filepath.Join(workflowsDir, tt.spec.WorkflowName+".md")) @@ -515,7 +516,7 @@ func TestAddWorkflowWithTracking_UsesActualFetchedPath(t *testing.T) { opts := AddOptions{ DisableSecurityScanner: true, } - err := addWorkflowWithTracking(resolved, nil, opts) + err := addWorkflowWithTracking(context.Background(), resolved, nil, opts) require.NoError(t, err, "addWorkflowWithTracking should succeed") // Read the written file diff --git a/pkg/cli/add_gitattributes_test.go b/pkg/cli/add_gitattributes_test.go index c4c80c7f0ad..a502e01e4d8 100644 --- a/pkg/cli/add_gitattributes_test.go +++ b/pkg/cli/add_gitattributes_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "os" "os/exec" "path/filepath" @@ -89,7 +90,7 @@ This is a test workflow.` // Call addWorkflows with noGitattributes=false opts := AddOptions{} - err := addWorkflows([]*ResolvedWorkflow{resolved}, opts) + err := addWorkflows(context.Background(), []*ResolvedWorkflow{resolved}, opts) if err != nil { // Log any error but don't fail - we're testing gitattributes behavior t.Logf("Note: workflow addition returned: %v", err) @@ -119,7 +120,7 @@ This is a test workflow.` opts := AddOptions{NoGitattributes: true} // Call addWorkflows with noGitattributes=true - err := addWorkflows([]*ResolvedWorkflow{resolved}, opts) + err := addWorkflows(context.Background(), []*ResolvedWorkflow{resolved}, opts) if err != nil { // Log any error but don't fail - we're testing gitattributes behavior t.Logf("Note: workflow addition returned: %v", err) @@ -142,7 +143,7 @@ This is a test workflow.` opts := AddOptions{NoGitattributes: true} // Call addWorkflows with noGitattributes=true - err := addWorkflows([]*ResolvedWorkflow{resolved}, opts) + err := addWorkflows(context.Background(), []*ResolvedWorkflow{resolved}, opts) if err != nil { // Log any error but don't fail - we're testing gitattributes behavior t.Logf("Note: workflow addition returned: %v", err) diff --git a/pkg/cli/add_interactive_git.go b/pkg/cli/add_interactive_git.go index 3ab419d1b81..2983ec4d8cc 100644 --- a/pkg/cli/add_interactive_git.go +++ b/pkg/cli/add_interactive_git.go @@ -39,7 +39,7 @@ func (c *AddInteractiveConfig) createWorkflowPRAndConfigureSecret(ctx context.Co StopAfter: c.StopAfter, DisableSecurityScanner: false, } - result, err := AddResolvedWorkflows(c.WorkflowSpecs, c.resolvedWorkflows, opts) + result, err := AddResolvedWorkflows(ctx, c.WorkflowSpecs, c.resolvedWorkflows, opts) if err != nil { return fmt.Errorf("failed to add workflow: %w", err) } diff --git a/pkg/cli/add_workflow_compilation.go b/pkg/cli/add_workflow_compilation.go index 13573c733bb..c31d28c7619 100644 --- a/pkg/cli/add_workflow_compilation.go +++ b/pkg/cli/add_workflow_compilation.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "os" "path/filepath" @@ -16,13 +17,13 @@ var addWorkflowCompilationLog = logger.New("cli:add_workflow_compilation") // compileWorkflow compiles a workflow file without refreshing stop time. // This is a convenience wrapper around compileWorkflowWithRefresh. -func compileWorkflow(filePath string, verbose bool, quiet bool, engineOverride string) error { - return compileWorkflowWithRefresh(filePath, verbose, quiet, engineOverride, false) +func compileWorkflow(ctx context.Context, filePath string, verbose bool, quiet bool, engineOverride string) error { + return compileWorkflowWithRefresh(ctx, filePath, verbose, quiet, engineOverride, false) } // compileWorkflowWithRefresh compiles a workflow file with optional stop time refresh. // This function handles the compilation process and ensures .gitattributes is updated. -func compileWorkflowWithRefresh(filePath string, verbose bool, quiet bool, engineOverride string, refreshStopTime bool) error { +func compileWorkflowWithRefresh(ctx context.Context, filePath string, verbose bool, quiet bool, engineOverride string, refreshStopTime bool) error { addWorkflowCompilationLog.Printf("Compiling workflow: file=%s, refresh_stop_time=%v, engine=%s", filePath, refreshStopTime, engineOverride) // Create compiler with auto-detected version and action mode @@ -33,7 +34,7 @@ func compileWorkflowWithRefresh(filePath string, verbose bool, quiet bool, engin compiler.SetRefreshStopTime(refreshStopTime) compiler.SetQuiet(quiet) - if err := CompileWorkflowWithValidation(compiler, filePath, verbose, false, false, false, false, false); err != nil { + if err := CompileWorkflowWithValidation(ctx, compiler, filePath, verbose, false, false, false, false, false); err != nil { addWorkflowCompilationLog.Printf("Compilation failed: %v", err) return err } @@ -55,13 +56,13 @@ func compileWorkflowWithRefresh(filePath string, verbose bool, quiet bool, engin // compileWorkflowWithTracking compiles a workflow and tracks generated files. // This is a convenience wrapper around compileWorkflowWithTrackingAndRefresh. -func compileWorkflowWithTracking(filePath string, verbose bool, quiet bool, engineOverride string, tracker *FileTracker) error { - return compileWorkflowWithTrackingAndRefresh(filePath, verbose, quiet, engineOverride, tracker, false) +func compileWorkflowWithTracking(ctx context.Context, filePath string, verbose bool, quiet bool, engineOverride string, tracker *FileTracker) error { + return compileWorkflowWithTrackingAndRefresh(ctx, filePath, verbose, quiet, engineOverride, tracker, false) } // compileWorkflowWithTrackingAndRefresh compiles a workflow, tracks generated files, and optionally refreshes stop time. // This function ensures that the file tracker records all files created or modified during compilation. -func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, quiet bool, engineOverride string, tracker *FileTracker, refreshStopTime bool) error { +func compileWorkflowWithTrackingAndRefresh(ctx context.Context, filePath string, verbose bool, quiet bool, engineOverride string, tracker *FileTracker, refreshStopTime bool) error { addWorkflowCompilationLog.Printf("Compiling workflow with tracking: file=%s, refresh_stop_time=%v", filePath, refreshStopTime) // Generate the expected lock file path @@ -102,7 +103,7 @@ func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, quiet compiler.SetFileTracker(tracker) compiler.SetRefreshStopTime(refreshStopTime) compiler.SetQuiet(quiet) - if err := CompileWorkflowWithValidation(compiler, filePath, verbose, false, false, false, false, false); err != nil { + if err := CompileWorkflowWithValidation(ctx, compiler, filePath, verbose, false, false, false, false, false); err != nil { return err } @@ -128,7 +129,7 @@ func compileWorkflowWithTrackingAndRefresh(filePath string, verbose bool, quiet // workflowFile that are present locally but lack a corresponding .lock.yml. This must be // called before compiling the main workflow, because the dispatch-workflow validator // requires every referenced .md workflow to have an up-to-date .lock.yml. -func compileDispatchWorkflowDependencies(workflowFile string, verbose, quiet bool, engineOverride string, tracker *FileTracker) { +func compileDispatchWorkflowDependencies(ctx context.Context, workflowFile string, verbose, quiet bool, engineOverride string, tracker *FileTracker) { // Parse the merged safe-outputs to get the canonical list of dispatch-workflow names. compiler := workflow.NewCompiler() data, err := compiler.ParseWorkflowFile(workflowFile) @@ -157,9 +158,9 @@ func compileDispatchWorkflowDependencies(workflowFile string, verbose, quiet boo var compileErr error if tracker != nil { - compileErr = compileWorkflowWithTracking(mdPath, verbose, quiet, engineOverride, tracker) + compileErr = compileWorkflowWithTracking(ctx, mdPath, verbose, quiet, engineOverride, tracker) } else { - compileErr = compileWorkflow(mdPath, verbose, quiet, engineOverride) + compileErr = compileWorkflow(ctx, mdPath, verbose, quiet, engineOverride) } if compileErr != nil { // Best-effort: log and continue so the main workflow can still give a clear error. diff --git a/pkg/cli/add_workflow_pr.go b/pkg/cli/add_workflow_pr.go index 5d37eda45a4..e038910e2a6 100644 --- a/pkg/cli/add_workflow_pr.go +++ b/pkg/cli/add_workflow_pr.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "math/rand" "os" @@ -45,7 +46,7 @@ func sanitizeBranchName(name string) string { } // addWorkflowsWithPR handles workflow addition with PR creation using pre-resolved workflows. -func addWorkflowsWithPR(workflows []*ResolvedWorkflow, opts AddOptions) (int, string, error) { +func addWorkflowsWithPR(ctx context.Context, workflows []*ResolvedWorkflow, opts AddOptions) (int, string, error) { addWorkflowPRLog.Printf("Adding %d workflow(s) with PR creation (resolved)", len(workflows)) // Get current branch for restoration later @@ -83,7 +84,7 @@ func addWorkflowsWithPR(workflows []*ResolvedWorkflow, opts AddOptions) (int, st addWorkflowPRLog.Print("Adding workflows to repository") prOpts := opts prOpts.DisableSecurityScanner = false - if err := addWorkflowsWithTracking(workflows, tracker, prOpts); err != nil { + if err := addWorkflowsWithTracking(ctx, workflows, tracker, prOpts); err != nil { addWorkflowPRLog.Printf("Failed to add workflows: %v", err) // Rollback on error if rollbackErr := tracker.RollbackAllFiles(opts.Verbose); rollbackErr != nil && opts.Verbose { diff --git a/pkg/cli/commands_compile_workflow_test.go b/pkg/cli/commands_compile_workflow_test.go index c989ce02009..54e27d55981 100644 --- a/pkg/cli/commands_compile_workflow_test.go +++ b/pkg/cli/commands_compile_workflow_test.go @@ -223,7 +223,7 @@ Test compilation with invalid engine. } // Test compileWorkflow function - err = compileWorkflow(workflowFile, tt.verbose, false, tt.engineOverride) + err = compileWorkflow(context.Background(), workflowFile, tt.verbose, false, tt.engineOverride) if tt.expectError { if err == nil { diff --git a/pkg/cli/commands_file_watching_test.go b/pkg/cli/commands_file_watching_test.go index a765005bd5a..ce631b3c94f 100644 --- a/pkg/cli/commands_file_watching_test.go +++ b/pkg/cli/commands_file_watching_test.go @@ -34,7 +34,7 @@ func TestWatchAndCompileWorkflows(t *testing.T) { compiler := workflow.NewCompiler() - err := watchAndCompileWorkflows("", compiler, false) + err := watchAndCompileWorkflows(context.Background(), "", compiler, false) if err == nil { t.Error("watchAndCompileWorkflows should require git repository") } @@ -59,7 +59,7 @@ func TestWatchAndCompileWorkflows(t *testing.T) { compiler := workflow.NewCompiler() - err := watchAndCompileWorkflows("", compiler, false) + err := watchAndCompileWorkflows(context.Background(), "", compiler, false) if err == nil { t.Error("watchAndCompileWorkflows should require .github/workflows directory") } @@ -86,7 +86,7 @@ func TestWatchAndCompileWorkflows(t *testing.T) { compiler := workflow.NewCompiler() - err := watchAndCompileWorkflows("nonexistent.md", compiler, false) + err := watchAndCompileWorkflows(context.Background(), "nonexistent.md", compiler, false) if err == nil { t.Error("watchAndCompileWorkflows should error for nonexistent specific file") } @@ -124,7 +124,7 @@ func TestWatchAndCompileWorkflows(t *testing.T) { // Run in a goroutine so we can control it with context done := make(chan error, 1) go func() { - done <- watchAndCompileWorkflows("test.md", compiler, true) + done <- watchAndCompileWorkflows(context.Background(), "test.md", compiler, true) }() select { @@ -150,7 +150,7 @@ func TestCompileAllWorkflowFiles(t *testing.T) { compiler := &workflow.Compiler{} - stats, err := compileAllWorkflowFiles(compiler, workflowsDir, true) + stats, err := compileAllWorkflowFiles(context.Background(), compiler, workflowsDir, true) if err != nil { t.Errorf("compileAllWorkflowFiles should handle empty directory: %v", err) } @@ -175,7 +175,7 @@ func TestCompileAllWorkflowFiles(t *testing.T) { // Create a basic compiler compiler := workflow.NewCompiler() - stats, err := compileAllWorkflowFiles(compiler, workflowsDir, true) + stats, err := compileAllWorkflowFiles(context.Background(), compiler, workflowsDir, true) if err != nil { t.Errorf("compileAllWorkflowFiles failed: %v", err) } @@ -210,7 +210,7 @@ func TestCompileAllWorkflowFiles(t *testing.T) { require.NoError(t, err) compiler := workflow.NewCompiler() - stats, err := compileAllWorkflowFiles(compiler, workflowsDir, false) + stats, err := compileAllWorkflowFiles(context.Background(), compiler, workflowsDir, false) if err != nil { t.Fatalf("compileAllWorkflowFiles failed: %v", err) } @@ -231,7 +231,7 @@ func TestCompileAllWorkflowFiles(t *testing.T) { compiler := &workflow.Compiler{} - _, err := compileAllWorkflowFiles(compiler, invalidDir, false) + _, err := compileAllWorkflowFiles(context.Background(), compiler, invalidDir, false) if err == nil { t.Error("compileAllWorkflowFiles should handle glob errors") } @@ -254,7 +254,7 @@ func TestCompileAllWorkflowFiles(t *testing.T) { compiler := workflow.NewCompiler() // This should not return an error (it prints errors but continues) - stats, err := compileAllWorkflowFiles(compiler, workflowsDir, false) + stats, err := compileAllWorkflowFiles(context.Background(), compiler, workflowsDir, false) if err != nil { t.Errorf("compileAllWorkflowFiles should handle compilation errors gracefully: %v", err) } @@ -276,7 +276,7 @@ func TestCompileAllWorkflowFiles(t *testing.T) { compiler := workflow.NewCompiler() // Test verbose mode (should not error) - stats, err := compileAllWorkflowFiles(compiler, workflowsDir, true) + stats, err := compileAllWorkflowFiles(context.Background(), compiler, workflowsDir, true) if err != nil { t.Errorf("compileAllWorkflowFiles verbose mode failed: %v", err) } @@ -384,7 +384,7 @@ func TestCompileSingleFile(t *testing.T) { stats := &CompilationStats{} // Compile without checking existence - result := compileSingleFile(compiler, filePath, stats, false, false) + result := compileSingleFile(context.Background(), compiler, filePath, stats, false, false) if !result { t.Error("Expected compilation to be attempted") @@ -419,7 +419,7 @@ func TestCompileSingleFile(t *testing.T) { stats := &CompilationStats{} // Compile without checking existence - result := compileSingleFile(compiler, filePath, stats, false, false) + result := compileSingleFile(context.Background(), compiler, filePath, stats, false, false) if !result { t.Error("Expected compilation to be attempted") @@ -460,7 +460,7 @@ func TestCompileSingleFile(t *testing.T) { os.Stderr = w t.Cleanup(func() { os.Stderr = oldStderr }) - result := compileSingleFile(compiler, filePath, stats, false, false) + result := compileSingleFile(context.Background(), compiler, filePath, stats, false, false) w.Close() @@ -490,7 +490,7 @@ func TestCompileSingleFile(t *testing.T) { stats := &CompilationStats{} // Compile with existence check - result := compileSingleFile(compiler, filePath, stats, false, true) + result := compileSingleFile(context.Background(), compiler, filePath, stats, false, true) if !result { t.Error("Expected compilation to be attempted") @@ -513,7 +513,7 @@ func TestCompileSingleFile(t *testing.T) { stats := &CompilationStats{} // Compile with existence check - should skip - result := compileSingleFile(compiler, filePath, stats, false, true) + result := compileSingleFile(context.Background(), compiler, filePath, stats, false, true) if result { t.Error("Expected compilation to be skipped for non-existent file") @@ -538,7 +538,7 @@ func TestCompileSingleFile(t *testing.T) { stats := &CompilationStats{} // Compile in verbose mode - result := compileSingleFile(compiler, filePath, stats, true, false) + result := compileSingleFile(context.Background(), compiler, filePath, stats, true, false) if !result { t.Error("Expected compilation to be attempted") @@ -573,7 +573,7 @@ func TestCompileModifiedFilesWithDependencies_FormatsWatchMessage(t *testing.T) os.Stderr = w t.Cleanup(func() { os.Stderr = oldStderr }) - compileModifiedFilesWithDependencies(compiler, depGraph, []string{filePath}, false) + compileModifiedFilesWithDependencies(context.Background(), compiler, depGraph, []string{filePath}, false) w.Close() diff --git a/pkg/cli/compile_command_test.go b/pkg/cli/compile_command_test.go index 786138e1a97..7d041e0dbef 100644 --- a/pkg/cli/compile_command_test.go +++ b/pkg/cli/compile_command_test.go @@ -222,6 +222,7 @@ func TestCompileWorkflowWithValidation_InvalidFile(t *testing.T) { // Try to compile a non-existent file err := CompileWorkflowWithValidation( + context.Background(), compiler, "/nonexistent/file.md", false, // verbose @@ -445,6 +446,7 @@ This is a test workflow. // Compile the workflow err := CompileWorkflowWithValidation( + context.Background(), compiler, testFile, false, // verbose diff --git a/pkg/cli/compile_file_operations.go b/pkg/cli/compile_file_operations.go index b29817f6341..9ae64b497ea 100644 --- a/pkg/cli/compile_file_operations.go +++ b/pkg/cli/compile_file_operations.go @@ -35,6 +35,7 @@ package cli import ( + "context" "fmt" "os" "path/filepath" @@ -51,7 +52,7 @@ var compileHelpersLog = logger.New("cli:compile_file_operations") // compileSingleFile compiles a single markdown workflow file and updates compilation statistics // If checkExists is true, the function will check if the file exists before compiling // Returns true if compilation was attempted (file exists or checkExists is false), false otherwise -func compileSingleFile(compiler *workflow.Compiler, file string, stats *CompilationStats, verbose bool, checkExists bool) bool { +func compileSingleFile(ctx context.Context, compiler *workflow.Compiler, file string, stats *CompilationStats, verbose bool, checkExists bool) bool { // Check if file exists if requested (for watch mode) if checkExists { if _, err := os.Stat(file); os.IsNotExist(err) { @@ -68,7 +69,7 @@ func compileSingleFile(compiler *workflow.Compiler, file string, stats *Compilat fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Compiling: "+file)) } - if err := CompileWorkflowWithValidation(compiler, file, verbose, false, false, false, false, false); err != nil { + if err := CompileWorkflowWithValidation(ctx, compiler, file, verbose, false, false, false, false, false); err != nil { // Always show compilation errors on a new line using standard CLI error styling. fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) stats.Errors++ @@ -81,7 +82,7 @@ func compileSingleFile(compiler *workflow.Compiler, file string, stats *Compilat } // compileAllWorkflowFiles compiles all markdown files in the workflows directory -func compileAllWorkflowFiles(compiler *workflow.Compiler, workflowsDir string, verbose bool) (*CompilationStats, error) { +func compileAllWorkflowFiles(ctx context.Context, compiler *workflow.Compiler, workflowsDir string, verbose bool) (*CompilationStats, error) { compileHelpersLog.Printf("Compiling all workflow files in directory: %s", workflowsDir) // Reset warning count before compilation compiler.ResetWarningCount() @@ -121,7 +122,7 @@ func compileAllWorkflowFiles(compiler *workflow.Compiler, workflowsDir string, v } else { file = absFile } - compileSingleFile(compiler, file, stats, verbose, false) + compileSingleFile(ctx, compiler, file, stats, verbose, false) } // Get warning count from compiler @@ -137,7 +138,7 @@ func compileAllWorkflowFiles(compiler *workflow.Compiler, workflowsDir string, v } // compileModifiedFilesWithDependencies compiles modified files and their dependencies using the dependency graph -func compileModifiedFilesWithDependencies(compiler *workflow.Compiler, depGraph *DependencyGraph, files []string, verbose bool) { +func compileModifiedFilesWithDependencies(ctx context.Context, compiler *workflow.Compiler, depGraph *DependencyGraph, files []string, verbose bool) { if len(files) == 0 { return } @@ -182,7 +183,7 @@ func compileModifiedFilesWithDependencies(compiler *workflow.Compiler, depGraph stats := &CompilationStats{} for _, file := range workflowsToCompile { - compileSingleFile(compiler, file, stats, verbose, true) + compileSingleFile(ctx, compiler, file, stats, verbose, true) } // Get warning count from compiler diff --git a/pkg/cli/compile_guard_policy_test.go b/pkg/cli/compile_guard_policy_test.go index 56883205a8b..2641d0b0200 100644 --- a/pkg/cli/compile_guard_policy_test.go +++ b/pkg/cli/compile_guard_policy_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "os" "path/filepath" "testing" @@ -133,7 +134,7 @@ This workflow specifies repos without min-integrity. require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) if tt.expectError { require.Error(t, err, "Expected compilation to fail") @@ -173,7 +174,7 @@ This workflow uses min-integrity without specifying repos. require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) require.NoError(t, err, "Expected compilation to succeed") // Read the compiled lock file and verify it contains the correct guard-policies JSON block. @@ -233,7 +234,7 @@ This workflow uses blocked-users and approval-labels. require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) require.NoError(t, err, "Expected compilation to succeed") lockFilePath := filepath.Join(tmpDir, "test-guard-policy-blocked.lock.yml") @@ -283,7 +284,7 @@ This workflow passes blocked-users and approval-labels as expressions. require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) require.NoError(t, err, "Expected compilation to succeed") lockFilePath := filepath.Join(tmpDir, "test-guard-policy-expr.lock.yml") @@ -331,7 +332,7 @@ This workflow passes blocked-users as a comma-separated string. require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) require.NoError(t, err, "Expected compilation to succeed") lockFilePath := filepath.Join(tmpDir, "test-guard-policy-csv.lock.yml") @@ -379,7 +380,7 @@ This workflow uses trusted-users alongside blocked-users. require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) require.NoError(t, err, "Expected compilation to succeed") lockFilePath := filepath.Join(tmpDir, "test-guard-policy-trusted.lock.yml") @@ -424,7 +425,7 @@ This workflow passes trusted-users as a GitHub Actions expression. require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) require.NoError(t, err, "Expected compilation to succeed") lockFilePath := filepath.Join(tmpDir, "test-guard-policy-trusted-expr.lock.yml") @@ -465,7 +466,7 @@ This workflow sets trusted-users without min-integrity (should fail). require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) require.Error(t, err, "Expected compilation to fail without min-integrity") assert.Contains(t, err.Error(), "min-integrity", "Error should mention min-integrity requirement") } diff --git a/pkg/cli/compile_orchestrator.go b/pkg/cli/compile_orchestrator.go index 84133ec9837..8663eb4d51f 100644 --- a/pkg/cli/compile_orchestrator.go +++ b/pkg/cli/compile_orchestrator.go @@ -69,6 +69,7 @@ func CompileWorkflows(ctx context.Context, config CompileConfig) ([]*workflow.Wo // Create and configure compiler compiler := createAndConfigureCompiler(config) + compiler.SetContext(ctx) if err := validateRepositoryManifestForCompilation(config, stats, &validationResults); err != nil { if config.JSONOutput { @@ -96,15 +97,15 @@ func CompileWorkflows(ctx context.Context, config CompileConfig) ([]*workflow.Wo } markdownFile = resolvedFile } - return nil, watchAndCompileWorkflows(markdownFile, compiler, config.Verbose) + return nil, watchAndCompileWorkflows(ctx, markdownFile, compiler, config.Verbose) } // Compile specific files or all files in directory if len(config.MarkdownFiles) > 0 { // Compile specific workflow files - return compileSpecificFiles(compiler, config, stats, &validationResults) + return compileSpecificFiles(ctx, compiler, config, stats, &validationResults) } // Compile all workflow files in directory - return compileAllFilesInDirectory(compiler, config, workflowDir, stats, &validationResults) + return compileAllFilesInDirectory(ctx, compiler, config, workflowDir, stats, &validationResults) } diff --git a/pkg/cli/compile_pipeline.go b/pkg/cli/compile_pipeline.go index 3089398230b..c8320360dfe 100644 --- a/pkg/cli/compile_pipeline.go +++ b/pkg/cli/compile_pipeline.go @@ -22,6 +22,7 @@ package cli import ( + "context" "errors" "fmt" "os" @@ -39,6 +40,7 @@ var compileOrchestrationLog = logger.New("cli:compile_pipeline") // compileSpecificFiles compiles a specific list of workflow files func compileSpecificFiles( + ctx context.Context, compiler *workflow.Compiler, config CompileConfig, stats *CompilationStats, @@ -96,7 +98,7 @@ func compileSpecificFiles( // Compile regular workflow file (disable per-file security tools) fileResult := compileWorkflowFile( - compiler, resolvedFile, config.Verbose, config.JSONOutput, + ctx, compiler, resolvedFile, config.Verbose, config.JSONOutput, config.NoEmit, false, false, false, // Disable per-file security tools config.Strict, shouldValidate, ) @@ -204,6 +206,7 @@ func compileSpecificFiles( // compileAllFilesInDirectory compiles all workflow files in a directory func compileAllFilesInDirectory( + ctx context.Context, compiler *workflow.Compiler, config CompileConfig, workflowDir string, @@ -273,7 +276,7 @@ func compileAllFilesInDirectory( // Compile regular workflow file (disable per-file security tools) fileResult := compileWorkflowFile( - compiler, file, config.Verbose, config.JSONOutput, + ctx, compiler, file, config.Verbose, config.JSONOutput, config.NoEmit, false, false, false, // Disable per-file security tools config.Strict, shouldValidate, ) @@ -368,7 +371,7 @@ func compileAllFilesInDirectory( } // Post-processing - if err := runPostProcessingForDirectory(compiler, workflowDataList, config, workflowsDir, gitRoot, successCount); err != nil { + if err := runPostProcessingForDirectory(ctx, compiler, workflowDataList, config, workflowsDir, gitRoot, successCount); err != nil { return workflowDataList, err } @@ -486,6 +489,7 @@ func runPostProcessing( // runPostProcessingForDirectory runs post-processing for directory compilation func runPostProcessingForDirectory( + ctx context.Context, compiler *workflow.Compiler, workflowDataList []*workflow.WorkflowData, config CompileConfig, @@ -523,12 +527,12 @@ func runPostProcessingForDirectory( // Skip maintenance workflow generation when using custom --dir option if !config.NoEmit && config.WorkflowDir == "" { absWorkflowDir := getAbsoluteWorkflowDir(workflowsDir, gitRoot) - if err := generateMaintenanceWorkflowWrapper(compiler, workflowDataList, absWorkflowDir, gitRoot, config.Verbose, config.Strict); err != nil { + if err := generateMaintenanceWorkflowWrapper(ctx, compiler, workflowDataList, absWorkflowDir, gitRoot, config.Verbose, config.Strict); err != nil { if config.Strict { return err } } - if err := generateCentralSlashCommandWorkflowWrapper(workflowDataList, absWorkflowDir, config.Strict); err != nil { + if err := generateCentralSlashCommandWorkflowWrapper(ctx, workflowDataList, absWorkflowDir, config.Strict); err != nil { if config.Strict { return err } diff --git a/pkg/cli/compile_post_processing.go b/pkg/cli/compile_post_processing.go index e08f6c9bb66..4bdc703873b 100644 --- a/pkg/cli/compile_post_processing.go +++ b/pkg/cli/compile_post_processing.go @@ -35,6 +35,7 @@ package cli import ( + "context" "fmt" "os" "path/filepath" @@ -70,6 +71,7 @@ func generateDependabotManifestsWrapper( // generateMaintenanceWorkflowWrapper generates maintenance workflow if any workflow uses expires field func generateMaintenanceWorkflowWrapper( + ctx context.Context, compiler *workflow.Compiler, workflowDataList []*workflow.WorkflowData, workflowsDir string, @@ -89,7 +91,7 @@ func generateMaintenanceWorkflowWrapper( repoConfig = nil } - if err := workflow.GenerateMaintenanceWorkflow(workflowDataList, workflowsDir, compiler.GetVersion(), compiler.GetActionMode(), compiler.GetActionTag(), verbose, repoConfig, compiler.GetRepositorySlug()); err != nil { + if err := workflow.GenerateMaintenanceWorkflow(ctx, workflowDataList, workflowsDir, compiler.GetVersion(), compiler.GetActionMode(), compiler.GetActionTag(), verbose, repoConfig, compiler.GetRepositorySlug()); err != nil { if strict { return fmt.Errorf("failed to generate maintenance workflow: %w", err) } @@ -103,13 +105,14 @@ func generateMaintenanceWorkflowWrapper( // generateCentralSlashCommandWorkflowWrapper generates a single centralized // slash-command trigger workflow for all participating workflows. func generateCentralSlashCommandWorkflowWrapper( + ctx context.Context, workflowDataList []*workflow.WorkflowData, workflowsDir string, strict bool, ) error { compilePostProcessingLog.Print("Generating centralized slash-command workflow") - if err := workflow.GenerateCentralSlashCommandWorkflow(workflowDataList, workflowsDir); err != nil { + if err := workflow.GenerateCentralSlashCommandWorkflow(ctx, workflowDataList, workflowsDir); err != nil { if strict { return fmt.Errorf("failed to generate centralized slash-command workflow: %w", err) } diff --git a/pkg/cli/compile_security_benchmark_test.go b/pkg/cli/compile_security_benchmark_test.go index ac6f364ed32..0cec73b0d4c 100644 --- a/pkg/cli/compile_security_benchmark_test.go +++ b/pkg/cli/compile_security_benchmark_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "os" "path/filepath" "testing" @@ -56,7 +57,7 @@ PR Number: ${{ github.event.pull_request.number }} b.ReportAllocs() for b.Loop() { // Compile with actionlint enabled (per-file mode for benchmarking) - _ = CompileWorkflowWithValidation(compiler, testFile, false, false, false, true, false, false) + _ = CompileWorkflowWithValidation(context.Background(), compiler, testFile, false, false, false, true, false, false) } } @@ -105,7 +106,7 @@ Issue: ${{ needs.activation.outputs.text }} b.ReportAllocs() for b.Loop() { // Compile with zizmor enabled - _ = CompileWorkflowWithValidation(compiler, testFile, false, true, false, false, false, false) + _ = CompileWorkflowWithValidation(context.Background(), compiler, testFile, false, true, false, false, false, false) } } @@ -150,7 +151,7 @@ Repository: ${{ github.repository }} b.ReportAllocs() for b.Loop() { // Compile with poutine enabled - _ = CompileWorkflowWithValidation(compiler, testFile, false, false, true, false, false, false) + _ = CompileWorkflowWithValidation(context.Background(), compiler, testFile, false, false, true, false, false, false) } } @@ -220,7 +221,7 @@ PR Details: b.ReportAllocs() for b.Loop() { // Compile with all security tools enabled (zizmor, poutine, actionlint) - _ = CompileWorkflowWithValidation(compiler, testFile, false, true, true, true, false, false) + _ = CompileWorkflowWithValidation(context.Background(), compiler, testFile, false, true, true, true, false, false) } } @@ -269,7 +270,7 @@ PR Number: ${{ github.event.pull_request.number }} b.ReportAllocs() for b.Loop() { // Compile without any security tools - _ = CompileWorkflowWithValidation(compiler, testFile, false, false, false, false, false, false) + _ = CompileWorkflowWithValidation(context.Background(), compiler, testFile, false, false, false, false, false, false) } } @@ -422,6 +423,6 @@ Triggered by: ${{ github.actor }} b.ReportAllocs() for b.Loop() { // Compile with all security tools enabled - _ = CompileWorkflowWithValidation(compiler, testFile, false, true, true, true, false, false) + _ = CompileWorkflowWithValidation(context.Background(), compiler, testFile, false, true, true, true, false, false) } } diff --git a/pkg/cli/compile_update_discussion_test.go b/pkg/cli/compile_update_discussion_test.go index dcabbd278df..b048fa04f0e 100644 --- a/pkg/cli/compile_update_discussion_test.go +++ b/pkg/cli/compile_update_discussion_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "os" "path/filepath" "testing" @@ -52,7 +53,7 @@ the agent can modify when using update-discussion. require.NoError(t, err, "Failed to write workflow file") compiler := workflow.NewCompiler() - err = CompileWorkflowWithValidation(compiler, workflowPath, false, false, false, false, false, false) + err = CompileWorkflowWithValidation(context.Background(), compiler, workflowPath, false, false, false, false, false, false) require.NoError(t, err, "Expected compilation to succeed") lockFilePath := filepath.Join(tmpDir, "test-update-discussion-field-enforcement.lock.yml") diff --git a/pkg/cli/compile_validation.go b/pkg/cli/compile_validation.go index 8f8de1d5e7e..e9e6bc61d31 100644 --- a/pkg/cli/compile_validation.go +++ b/pkg/cli/compile_validation.go @@ -18,9 +18,11 @@ import ( var compileValidationLog = logger.New("cli:compile_validation") // CompileWorkflowWithValidation compiles a workflow with always-on YAML validation for CLI usage -func CompileWorkflowWithValidation(compiler *workflow.Compiler, filePath string, verbose bool, runZizmorPerFile bool, runPoutinePerFile bool, runActionlintPerFile bool, strict bool, validateActionSHAs bool) error { +func CompileWorkflowWithValidation(ctx context.Context, compiler *workflow.Compiler, filePath string, verbose bool, runZizmorPerFile bool, runPoutinePerFile bool, runActionlintPerFile bool, strict bool, validateActionSHAs bool) error { compileValidationLog.Printf("Compiling workflow with validation: file=%s, strict=%v, validateSHAs=%v", filePath, strict, validateActionSHAs) + compiler.SetContext(ctx) + // Set workflow identifier for schedule scattering (use repository-relative path for stability) relPath, err := getRepositoryRelativePath(filePath) if err != nil { @@ -74,7 +76,7 @@ func CompileWorkflowWithValidation(compiler *workflow.Compiler, filePath string, compileValidationLog.Print("Validating action SHAs in lock file") // Use the compiler's shared action cache to benefit from cached resolutions actionCache := compiler.GetSharedActionCache() - if err := workflow.ValidateActionSHAsInLockFile(lockFile, actionCache, verbose); err != nil { + if err := workflow.ValidateActionSHAsInLockFile(ctx, lockFile, actionCache, verbose); err != nil { // Action SHA validation warnings are non-fatal compileValidationLog.Printf("Action SHA validation completed with warnings: %v", err) } @@ -97,7 +99,7 @@ func CompileWorkflowWithValidation(compiler *workflow.Compiler, filePath string, // Run actionlint on the generated lock file if requested // Note: For batch processing, use RunActionlintOnFiles instead if runActionlintPerFile { - if err := runActionlintOnFiles(context.Background(), []string{lockFile}, verbose, strict); err != nil { + if err := runActionlintOnFiles(ctx, []string{lockFile}, verbose, strict); err != nil { return fmt.Errorf("actionlint linter failed: %w", err) } } @@ -107,9 +109,11 @@ func CompileWorkflowWithValidation(compiler *workflow.Compiler, filePath string, // CompileWorkflowDataWithValidation compiles from already-parsed WorkflowData with validation // This avoids re-parsing when the workflow data has already been parsed -func CompileWorkflowDataWithValidation(compiler *workflow.Compiler, workflowData *workflow.WorkflowData, filePath string, verbose bool, runZizmorPerFile bool, runPoutinePerFile bool, runActionlintPerFile bool, strict bool, validateActionSHAs bool) error { +func CompileWorkflowDataWithValidation(ctx context.Context, compiler *workflow.Compiler, workflowData *workflow.WorkflowData, filePath string, verbose bool, runZizmorPerFile bool, runPoutinePerFile bool, runActionlintPerFile bool, strict bool, validateActionSHAs bool) error { compileValidationLog.Printf("Compiling from parsed WorkflowData: file=%s", filePath) + compiler.SetContext(ctx) + // Compile the workflow using already-parsed data if err := compiler.CompileWorkflowData(workflowData, filePath); err != nil { compileValidationLog.Printf("WorkflowData compilation failed: %v", err) @@ -142,7 +146,7 @@ func CompileWorkflowDataWithValidation(compiler *workflow.Compiler, workflowData compileValidationLog.Print("Validating action SHAs in lock file") // Use the compiler's shared action cache to benefit from cached resolutions actionCache := compiler.GetSharedActionCache() - if err := workflow.ValidateActionSHAsInLockFile(lockFile, actionCache, verbose); err != nil { + if err := workflow.ValidateActionSHAsInLockFile(ctx, lockFile, actionCache, verbose); err != nil { // Action SHA validation warnings are non-fatal compileValidationLog.Printf("Action SHA validation completed with warnings: %v", err) } @@ -165,7 +169,7 @@ func CompileWorkflowDataWithValidation(compiler *workflow.Compiler, workflowData // Run actionlint on the generated lock file if requested // Note: For batch processing, use RunActionlintOnFiles instead if runActionlintPerFile { - if err := runActionlintOnFiles(context.Background(), []string{lockFile}, verbose, strict); err != nil { + if err := runActionlintOnFiles(ctx, []string{lockFile}, verbose, strict); err != nil { return fmt.Errorf("actionlint linter failed: %w", err) } } diff --git a/pkg/cli/compile_watch.go b/pkg/cli/compile_watch.go index da282514ef6..876eca071d4 100644 --- a/pkg/cli/compile_watch.go +++ b/pkg/cli/compile_watch.go @@ -1,6 +1,7 @@ package cli import ( + "context" "errors" "fmt" "os" @@ -23,7 +24,7 @@ import ( var compileWatchLog = logger.New("cli:compile_watch") // watchAndCompileWorkflows watches for changes to workflow files and recompiles them automatically -func watchAndCompileWorkflows(markdownFile string, compiler *workflow.Compiler, verbose bool) error { +func watchAndCompileWorkflows(ctx context.Context, markdownFile string, compiler *workflow.Compiler, verbose bool) error { // Find git root for consistent behavior gitRoot, err := gitutil.FindGitRoot() if err != nil { @@ -126,7 +127,7 @@ func watchAndCompileWorkflows(markdownFile string, compiler *workflow.Compiler, if verbose { fmt.Fprintln(os.Stderr, "🔨 Initial compilation of all workflow files...") } - stats, err := compileAllWorkflowFiles(compiler, workflowsDir, verbose) + stats, err := compileAllWorkflowFiles(ctx, compiler, workflowsDir, verbose) if err != nil { // Always show initial compilation errors, not just in verbose mode fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Initial compilation failed: %v", err))) @@ -146,7 +147,7 @@ func watchAndCompileWorkflows(markdownFile string, compiler *workflow.Compiler, } // Use compileSingleFile to handle both regular workflows and campaign files - compileSingleFile(compiler, markdownFile, stats, verbose, false) + compileSingleFile(ctx, compiler, markdownFile, stats, verbose, false) // Get warning count from compiler stats.Warnings = compiler.GetWarningCount() @@ -210,7 +211,7 @@ func watchAndCompileWorkflows(markdownFile string, compiler *workflow.Compiler, debounceMu.Unlock() // Compile the modified files using dependency graph - compileModifiedFilesWithDependencies(compiler, depGraph, filesToCompile, verbose) + compileModifiedFilesWithDependencies(ctx, compiler, depGraph, filesToCompile, verbose) }) debounceMu.Unlock() } diff --git a/pkg/cli/compile_workflow_processor.go b/pkg/cli/compile_workflow_processor.go index 02cc84be843..70af3585d78 100644 --- a/pkg/cli/compile_workflow_processor.go +++ b/pkg/cli/compile_workflow_processor.go @@ -22,6 +22,7 @@ package cli import ( + "context" "errors" "fmt" "os" @@ -46,6 +47,7 @@ type compileWorkflowFileResult struct { // compileWorkflowFile compiles a single workflow file (not a campaign spec) // Returns the workflow data, lock file path, validation result, and success status func compileWorkflowFile( + ctx context.Context, compiler *workflow.Compiler, resolvedFile string, verbose bool, @@ -149,7 +151,7 @@ func compileWorkflowFile( // Compile the workflow // Disable per-file actionlint run (false instead of actionlint && !noEmit) - we'll batch them - if err := CompileWorkflowDataWithValidation(compiler, workflowData, resolvedFile, verbose && !jsonOutput, zizmor && !noEmit, poutine && !noEmit, false, strict, validate && !noEmit); err != nil { + if err := CompileWorkflowDataWithValidation(ctx, compiler, workflowData, resolvedFile, verbose && !jsonOutput, zizmor && !noEmit, poutine && !noEmit, false, strict, validate && !noEmit); err != nil { // Don't print error here - it will be displayed in the compilation summary // The error is stored in ValidationResult for JSON output and summary display result.validationResult.Valid = false diff --git a/pkg/cli/copilot_setup.go b/pkg/cli/copilot_setup.go index a187bababe5..7abc2fd7138 100644 --- a/pkg/cli/copilot_setup.go +++ b/pkg/cli/copilot_setup.go @@ -19,10 +19,10 @@ var copilotSetupLog = logger.New("cli:copilot_setup") // getActionRef returns the action reference string based on action mode and version. // If a resolver is provided and mode is release or action, attempts to resolve the SHA for a SHA-pinned reference. // Falls back to a version tag reference if SHA resolution fails or resolver is nil. -func getActionRef(actionMode workflow.ActionMode, version string, resolver workflow.SHAResolver) string { +func getActionRef(ctx context.Context, actionMode workflow.ActionMode, version string, resolver workflow.SHAResolver) string { if actionMode.IsRelease() && version != "" && version != "dev" { if resolver != nil { - sha, err := resolver.ResolveSHA(context.Background(), "github/gh-aw/actions/setup-cli", version) + sha, err := resolver.ResolveSHA(ctx, "github/gh-aw/actions/setup-cli", version) if err == nil && sha != "" { return fmt.Sprintf("@%s # %s", sha, version) } @@ -32,7 +32,7 @@ func getActionRef(actionMode workflow.ActionMode, version string, resolver workf } if actionMode.IsAction() && version != "" && version != "dev" { if resolver != nil { - sha, err := resolver.ResolveSHA(context.Background(), "github/gh-aw-actions/setup-cli", version) + sha, err := resolver.ResolveSHA(ctx, "github/gh-aw-actions/setup-cli", version) if err == nil && sha != "" { return fmt.Sprintf("@%s # %s", sha, version) } @@ -44,9 +44,9 @@ func getActionRef(actionMode workflow.ActionMode, version string, resolver workf } // generateCopilotSetupStepsYAML generates the copilot-setup-steps.yml content based on action mode -func generateCopilotSetupStepsYAML(actionMode workflow.ActionMode, version string, resolver workflow.SHAResolver) string { +func generateCopilotSetupStepsYAML(ctx context.Context, actionMode workflow.ActionMode, version string, resolver workflow.SHAResolver) string { // Determine the action reference - use SHA-pinned or version tag in release/action mode, @main in dev mode - actionRef := getActionRef(actionMode, version, resolver) + actionRef := getActionRef(ctx, actionMode, version, resolver) if actionMode.IsRelease() || actionMode.IsAction() { // Determine the action repo based on mode @@ -159,19 +159,19 @@ type Workflow struct { } // ensureCopilotSetupSteps creates or updates .github/workflows/copilot-setup-steps.yml -func ensureCopilotSetupSteps(verbose bool, actionMode workflow.ActionMode, version string) error { - return ensureCopilotSetupStepsWithUpgrade(verbose, actionMode, version, false) +func ensureCopilotSetupSteps(ctx context.Context, verbose bool, actionMode workflow.ActionMode, version string) error { + return ensureCopilotSetupStepsWithUpgrade(ctx, verbose, actionMode, version, false) } // upgradeCopilotSetupSteps upgrades the version in existing copilot-setup-steps.yml -func upgradeCopilotSetupSteps(verbose bool, actionMode workflow.ActionMode, version string) error { - return ensureCopilotSetupStepsWithUpgrade(verbose, actionMode, version, true) +func upgradeCopilotSetupSteps(ctx context.Context, verbose bool, actionMode workflow.ActionMode, version string) error { + return ensureCopilotSetupStepsWithUpgrade(ctx, verbose, actionMode, version, true) } // ensureCopilotSetupStepsWithUpgrade creates .github/workflows/copilot-setup-steps.yml // If the file already exists, it renders console instructions instead of editing // When upgradeVersion is true and called from upgrade command, this is a special case -func ensureCopilotSetupStepsWithUpgrade(verbose bool, actionMode workflow.ActionMode, version string, upgradeVersion bool) error { +func ensureCopilotSetupStepsWithUpgrade(ctx context.Context, verbose bool, actionMode workflow.ActionMode, version string, upgradeVersion bool) error { copilotSetupLog.Printf("Creating copilot-setup-steps.yml with action mode: %s, version: %s, upgradeVersion: %v", actionMode, version, upgradeVersion) // Create a SHA resolver for release/action mode to enable SHA-pinned action references @@ -213,7 +213,7 @@ func ensureCopilotSetupStepsWithUpgrade(verbose bool, actionMode workflow.Action if (hasLegacyInstall || hasActionInstall) && upgradeVersion { copilotSetupLog.Print("Extension install step exists, attempting version upgrade (upgrade command)") - upgraded, updatedContent, err := upgradeSetupCliVersionInContent(content, actionMode, version, resolver) + upgraded, updatedContent, err := upgradeSetupCliVersionInContent(ctx, content, actionMode, version, resolver) if err != nil { return fmt.Errorf("failed to upgrade setup-cli version: %w", err) } @@ -248,12 +248,12 @@ func ensureCopilotSetupStepsWithUpgrade(verbose bool, actionMode workflow.Action // File exists but needs update - render instructions copilotSetupLog.Print("File exists without install step, rendering update instructions instead of editing") - renderCopilotSetupUpdateInstructions(setupStepsPath, actionMode, version, resolver) + renderCopilotSetupUpdateInstructions(ctx, setupStepsPath, actionMode, version, resolver) return nil } // File doesn't exist - create it - if err := os.WriteFile(setupStepsPath, []byte(generateCopilotSetupStepsYAML(actionMode, version, resolver)), constants.FilePermSensitive); err != nil { + if err := os.WriteFile(setupStepsPath, []byte(generateCopilotSetupStepsYAML(ctx, actionMode, version, resolver)), constants.FilePermSensitive); err != nil { return fmt.Errorf("failed to write copilot-setup-steps.yml: %w", err) } copilotSetupLog.Printf("Created file: %s", setupStepsPath) @@ -262,7 +262,7 @@ func ensureCopilotSetupStepsWithUpgrade(verbose bool, actionMode workflow.Action } // renderCopilotSetupUpdateInstructions renders console instructions for updating copilot-setup-steps.yml -func renderCopilotSetupUpdateInstructions(filePath string, actionMode workflow.ActionMode, version string, resolver workflow.SHAResolver) { +func renderCopilotSetupUpdateInstructions(ctx context.Context, filePath string, actionMode workflow.ActionMode, version string, resolver workflow.SHAResolver) { fmt.Fprintln(os.Stderr) fmt.Fprintf(os.Stderr, "%s %s\n", "ℹ", @@ -273,7 +273,7 @@ func renderCopilotSetupUpdateInstructions(filePath string, actionMode workflow.A fmt.Fprintln(os.Stderr) // Determine the action reference - actionRef := getActionRef(actionMode, version, resolver) + actionRef := getActionRef(ctx, actionMode, version, resolver) if actionMode.IsRelease() || actionMode.IsAction() { actionRepo := "github/gh-aw/actions/setup-cli" @@ -333,7 +333,7 @@ var versionInWithPattern = regexp.MustCompile( // // Returns (upgraded, updatedContent, error). upgraded is false when no change // was required (e.g. already at the target version, or file has no setup-cli step). -func upgradeSetupCliVersionInContent(content []byte, actionMode workflow.ActionMode, version string, resolver workflow.SHAResolver) (bool, []byte, error) { +func upgradeSetupCliVersionInContent(ctx context.Context, content []byte, actionMode workflow.ActionMode, version string, resolver workflow.SHAResolver) (bool, []byte, error) { if !actionMode.IsRelease() && !actionMode.IsAction() { return false, content, nil } @@ -342,7 +342,7 @@ func upgradeSetupCliVersionInContent(content []byte, actionMode workflow.ActionM return false, content, nil } - actionRef := getActionRef(actionMode, version, resolver) + actionRef := getActionRef(ctx, actionMode, version, resolver) actionRepo := "github/gh-aw/actions/setup-cli" if actionMode.IsAction() { actionRepo = "github/gh-aw-actions/setup-cli" diff --git a/pkg/cli/copilot_setup_test.go b/pkg/cli/copilot_setup_test.go index 5bc816c0389..405c8c3a10b 100644 --- a/pkg/cli/copilot_setup_test.go +++ b/pkg/cli/copilot_setup_test.go @@ -162,10 +162,10 @@ func TestEnsureCopilotSetupSteps(t *testing.T) { } // Call the function - err = ensureCopilotSetupSteps(tt.verbose, workflow.ActionModeDev, "dev") + err = ensureCopilotSetupSteps(context.Background(), tt.verbose, workflow.ActionModeDev, "dev") if (err != nil) != tt.wantErr { - t.Errorf("ensureCopilotSetupSteps() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ensureCopilotSetupSteps(context.Background()) error = %v, wantErr %v", err, tt.wantErr) return } @@ -308,9 +308,9 @@ func TestEnsureCopilotSetupStepsFilePermissions(t *testing.T) { t.Fatalf("Failed to change to temp directory: %v", err) } - err = ensureCopilotSetupSteps(false, workflow.ActionModeDev, "dev") + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev") if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } // Check file permissions @@ -408,9 +408,9 @@ func TestEnsureCopilotSetupStepsDirectoryCreation(t *testing.T) { } // Call function when .github/workflows doesn't exist - err = ensureCopilotSetupSteps(false, workflow.ActionModeDev, "dev") + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev") if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } // Verify directory structure was created @@ -452,9 +452,9 @@ func TestEnsureCopilotSetupSteps_ReleaseMode(t *testing.T) { // Call function with release mode testVersion := "v1.2.3" - err = ensureCopilotSetupSteps(false, workflow.ActionModeRelease, testVersion) + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeRelease, testVersion) if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } // Read generated file @@ -506,9 +506,9 @@ func TestEnsureCopilotSetupSteps_DevMode(t *testing.T) { } // Call function with dev mode - err = ensureCopilotSetupSteps(false, workflow.ActionModeDev, "dev") + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev") if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } // Read generated file @@ -546,9 +546,9 @@ func TestEnsureCopilotSetupSteps_CreateWithReleaseMode(t *testing.T) { // Create new file with release mode and specific version testVersion := "v2.0.0" - err = ensureCopilotSetupSteps(false, workflow.ActionModeRelease, testVersion) + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeRelease, testVersion) if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } setupStepsPath := filepath.Join(".github", "workflows", "copilot-setup-steps.yml") @@ -585,9 +585,9 @@ func TestEnsureCopilotSetupSteps_CreateWithDevMode(t *testing.T) { } // Create new file with dev mode - err = ensureCopilotSetupSteps(false, workflow.ActionModeDev, "dev") + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev") if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } setupStepsPath := filepath.Join(".github", "workflows", "copilot-setup-steps.yml") @@ -651,9 +651,9 @@ jobs: // Call with release mode - should render instructions instead of modifying testVersion := "v3.0.0" - err = ensureCopilotSetupSteps(false, workflow.ActionModeRelease, testVersion) + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeRelease, testVersion) if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } // Read file - should remain unchanged @@ -715,9 +715,9 @@ jobs: } // Call with dev mode - should render instructions instead of modifying - err = ensureCopilotSetupSteps(false, workflow.ActionModeDev, "dev") + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev") if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } // Read file - should remain unchanged @@ -785,9 +785,9 @@ jobs: } // Attempt to update - should skip - err = ensureCopilotSetupSteps(false, workflow.ActionModeRelease, "v2.0.0") + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeRelease, "v2.0.0") if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } // Read file - should be unchanged @@ -842,9 +842,9 @@ jobs: } // Attempt to update - should skip - err = ensureCopilotSetupSteps(false, workflow.ActionModeDev, "dev") + err = ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev") if err != nil { - t.Fatalf("ensureCopilotSetupSteps() failed: %v", err) + t.Fatalf("ensureCopilotSetupSteps(context.Background()) failed: %v", err) } // Verify file content matches expected (should be unchanged) @@ -899,9 +899,9 @@ jobs: } // Upgrade to v2.0.0 - err = upgradeCopilotSetupSteps(false, workflow.ActionModeRelease, "v2.0.0") + err = upgradeCopilotSetupSteps(context.Background(), false, workflow.ActionModeRelease, "v2.0.0") if err != nil { - t.Fatalf("upgradeCopilotSetupSteps() failed: %v", err) + t.Fatalf("upgradeCopilotSetupSteps(context.Background()) failed: %v", err) } // Read updated file @@ -940,9 +940,9 @@ func TestUpgradeCopilotSetupSteps_NoFile(t *testing.T) { } // Attempt to upgrade when file doesn't exist - should create new file - err = upgradeCopilotSetupSteps(false, workflow.ActionModeRelease, "v2.0.0") + err = upgradeCopilotSetupSteps(context.Background(), false, workflow.ActionModeRelease, "v2.0.0") if err != nil { - t.Fatalf("upgradeCopilotSetupSteps() failed: %v", err) + t.Fatalf("upgradeCopilotSetupSteps(context.Background()) failed: %v", err) } // Verify file was created with the new version @@ -995,9 +995,9 @@ jobs: } // Attempt upgrade in dev mode - should not modify file - err = upgradeCopilotSetupSteps(false, workflow.ActionModeDev, "dev") + err = upgradeCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev") if err != nil { - t.Fatalf("upgradeCopilotSetupSteps() failed: %v", err) + t.Fatalf("upgradeCopilotSetupSteps(context.Background()) failed: %v", err) } // Verify file was not changed (dev mode doesn't upgrade curl-based installs) @@ -1201,9 +1201,9 @@ jobs: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - upgraded, got, err := upgradeSetupCliVersionInContent([]byte(tt.content), tt.actionMode, tt.version, tt.resolver) + upgraded, got, err := upgradeSetupCliVersionInContent(context.Background(), []byte(tt.content), tt.actionMode, tt.version, tt.resolver) if err != nil { - t.Fatalf("upgradeSetupCliVersionInContent() error: %v", err) + t.Fatalf("upgradeSetupCliVersionInContent(context.Background()) error: %v", err) } if upgraded != tt.expectUpgrade { t.Errorf("upgraded = %v, want %v", upgraded, tt.expectUpgrade) @@ -1305,9 +1305,9 @@ jobs: run: echo "hello" # inline run comment ` - upgraded, got, err := upgradeSetupCliVersionInContent([]byte(input), workflow.ActionModeRelease, "v2.0.0", nil) + upgraded, got, err := upgradeSetupCliVersionInContent(context.Background(), []byte(input), workflow.ActionModeRelease, "v2.0.0", nil) if err != nil { - t.Fatalf("upgradeSetupCliVersionInContent() error: %v", err) + t.Fatalf("upgradeSetupCliVersionInContent(context.Background()) error: %v", err) } if !upgraded { t.Fatal("Expected upgrade to occur") @@ -1391,9 +1391,9 @@ jobs: // upgradeSetupCliVersionInContent with a SHA resolver — the result must be unquoted sha := "bd9c0ca491e6334a2797ef56ad6ee89958d54ab9" resolver := &mockSHAResolver{sha: sha} - upgraded, updated, err := upgradeSetupCliVersionInContent([]byte(existingContent), workflow.ActionModeRelease, "v2.0.0", resolver) + upgraded, updated, err := upgradeSetupCliVersionInContent(context.Background(), []byte(existingContent), workflow.ActionModeRelease, "v2.0.0", resolver) if err != nil { - t.Fatalf("upgradeSetupCliVersionInContent() error: %v", err) + t.Fatalf("upgradeSetupCliVersionInContent(context.Background()) error: %v", err) } if !upgraded { t.Fatal("Expected upgrade to occur") @@ -1471,9 +1471,9 @@ func TestGetActionRef(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ref := getActionRef(tt.actionMode, tt.version, tt.resolver) + ref := getActionRef(context.Background(), tt.actionMode, tt.version, tt.resolver) if ref != tt.expectedRef { - t.Errorf("getActionRef() = %q, want %q", ref, tt.expectedRef) + t.Errorf("getActionRef(context.Background()) = %q, want %q", ref, tt.expectedRef) } }) } diff --git a/pkg/cli/enable.go b/pkg/cli/enable.go index dbf6bf73b04..35fd898b501 100644 --- a/pkg/cli/enable.go +++ b/pkg/cli/enable.go @@ -1,6 +1,7 @@ package cli import ( + "context" "errors" "fmt" "os" @@ -20,19 +21,19 @@ import ( var enableLog = logger.New("cli:enable") // EnableWorkflowsByNames enables workflows by specific names, or all if no names provided -func EnableWorkflowsByNames(workflowNames []string, repoOverride string) error { +func EnableWorkflowsByNames(ctx context.Context, workflowNames []string, repoOverride string) error { enableLog.Printf("EnableWorkflowsByNames called: workflow_count=%d, repo=%s", len(workflowNames), repoOverride) - return toggleWorkflowsByNames(workflowNames, true, repoOverride) + return toggleWorkflowsByNames(ctx, workflowNames, true, repoOverride) } // DisableWorkflowsByNames disables workflows by specific names, or all if no names provided -func DisableWorkflowsByNames(workflowNames []string, repoOverride string) error { +func DisableWorkflowsByNames(ctx context.Context, workflowNames []string, repoOverride string) error { enableLog.Printf("DisableWorkflowsByNames called: workflow_count=%d, repo=%s", len(workflowNames), repoOverride) - return toggleWorkflowsByNames(workflowNames, false, repoOverride) + return toggleWorkflowsByNames(ctx, workflowNames, false, repoOverride) } // toggleWorkflowsByNames toggles workflows by specific names, or all if no names provided -func toggleWorkflowsByNames(workflowNames []string, enable bool, repoOverride string) error { +func toggleWorkflowsByNames(ctx context.Context, workflowNames []string, enable bool, repoOverride string) error { action := "enable" if !enable { action = "disable" @@ -63,7 +64,7 @@ func toggleWorkflowsByNames(workflowNames []string, enable bool, repoOverride st } // Recursively call with all workflow names - return toggleWorkflowsByNames(allWorkflowNames, enable, repoOverride) + return toggleWorkflowsByNames(ctx, allWorkflowNames, enable, repoOverride) } // Check if gh CLI is available @@ -119,7 +120,7 @@ func toggleWorkflowsByNames(workflowNames []string, enable bool, repoOverride st // If enabling and lock file doesn't exist locally, try to compile it if enable { if _, err := os.Stat(lockFile); os.IsNotExist(err) { - if err := compileWorkflow(file, false, false, ""); err != nil { + if err := compileWorkflow(ctx, file, false, false, ""); err != nil { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to compile workflow %s to create lock file: %v", name, err))) // If we can't compile and there's no GitHub entry, skip because we can't address it if !exists { diff --git a/pkg/cli/error_formatting_test.go b/pkg/cli/error_formatting_test.go index 7b2c6019fff..3f2e790ae8b 100644 --- a/pkg/cli/error_formatting_test.go +++ b/pkg/cli/error_formatting_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "bytes" "errors" "fmt" @@ -38,7 +39,7 @@ This is not valid frontmatter // Create compiler and attempt to compile compiler := workflow.NewCompiler() - _ = CompileWorkflowWithValidation(compiler, invalidWorkflow, false, false, false, false, false, false) + _ = CompileWorkflowWithValidation(context.Background(), compiler, invalidWorkflow, false, false, false, false, false, false) // Restore stderr and read captured output w.Close() diff --git a/pkg/cli/file_tracker_test.go b/pkg/cli/file_tracker_test.go index 9cee77a951b..d9b9a5f0ce9 100644 --- a/pkg/cli/file_tracker_test.go +++ b/pkg/cli/file_tracker_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "os" "os/exec" "path/filepath" @@ -296,7 +297,7 @@ This uses reaction. tracker := NewFileTracker() // Compile the workflow with tracking - if err := compileWorkflowWithTracking(workflowFileWithReaction, false, false, "", tracker); err != nil { + if err := compileWorkflowWithTracking(context.Background(), workflowFileWithReaction, false, false, "", tracker); err != nil { t.Fatalf("Failed to compile workflow: %v", err) } @@ -338,7 +339,7 @@ This does NOT use ai-reaction. // (Note: Since reaction is now inline, this removal step is no longer needed) // Compile the workflow with tracking - if err := compileWorkflowWithTracking(workflowFileWithoutReaction, false, false, "", tracker2); err != nil { + if err := compileWorkflowWithTracking(context.Background(), workflowFileWithoutReaction, false, false, "", tracker2); err != nil { t.Fatalf("Failed to compile workflow: %v", err) } diff --git a/pkg/cli/fuzzy_matching_integration_test.go b/pkg/cli/fuzzy_matching_integration_test.go index a6513f03864..7a308ceeb81 100644 --- a/pkg/cli/fuzzy_matching_integration_test.go +++ b/pkg/cli/fuzzy_matching_integration_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "os" "path/filepath" "strings" @@ -167,7 +168,7 @@ Test content } // Test enable command with typo - err = EnableWorkflowsByNames([]string{"audti-workflows"}, "") + err = EnableWorkflowsByNames(context.Background(), []string{"audti-workflows"}, "") if err == nil { t.Fatal("Expected error for non-existent workflow") } diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 08a34a60611..c2a4964c63c 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -1,6 +1,7 @@ package cli import ( + "context" "encoding/json" "errors" "fmt" @@ -20,6 +21,7 @@ var initLog = logger.New("cli:init") // InitOptions contains all configuration options for repository initialization type InitOptions struct { + Ctx context.Context Verbose bool MCP bool CodespaceRepos []string @@ -33,6 +35,11 @@ type InitOptions struct { func InitRepository(opts InitOptions) error { initLog.Print("Starting repository initialization for agentic workflows") + ctx := opts.Ctx + if ctx == nil { + ctx = context.Background() + } + // Show welcome banner for interactive mode console.ShowWelcomeBanner("This tool will initialize your repository for GitHub Agentic Workflows.") @@ -95,7 +102,7 @@ func InitRepository(opts InitOptions) error { initLog.Printf("Using action mode for copilot-setup-steps.yml: %s", actionMode) // Create copilot-setup-steps.yml - if err := ensureCopilotSetupSteps(opts.Verbose, actionMode, GetVersion()); err != nil { + if err := ensureCopilotSetupSteps(ctx, opts.Verbose, actionMode, GetVersion()); err != nil { initLog.Printf("Failed to create copilot-setup-steps.yml: %v", err) return fmt.Errorf("failed to create copilot-setup-steps.yml: %w", err) } @@ -154,7 +161,7 @@ func InitRepository(opts InitOptions) error { // Generate/update maintenance workflow if any workflows use expires field initLog.Print("Checking for workflows with expires field to generate maintenance workflow") - if err := ensureMaintenanceWorkflow(opts.Verbose); err != nil { + if err := ensureMaintenanceWorkflow(ctx, opts.Verbose); err != nil { initLog.Printf("Failed to generate maintenance workflow: %v", err) // Don't fail init if maintenance workflow generation has issues fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to generate maintenance workflow: %v", err))) @@ -194,7 +201,7 @@ func InitRepository(opts InitOptions) error { // ensureMaintenanceWorkflow checks existing workflows for expires field and generates/updates // the maintenance workflow file if any workflows use it -func ensureMaintenanceWorkflow(verbose bool) error { +func ensureMaintenanceWorkflow(ctx context.Context, verbose bool) error { initLog.Print("Checking for workflows with expires field") // Find git root @@ -249,7 +256,7 @@ func ensureMaintenanceWorkflow(verbose bool) error { repoConfig = nil } - if err := workflow.GenerateMaintenanceWorkflow(workflowDataList, workflowsDir, GetVersion(), compiler.GetActionMode(), compiler.GetActionTag(), verbose, repoConfig, compiler.GetRepositorySlug()); err != nil { + if err := workflow.GenerateMaintenanceWorkflow(ctx, workflowDataList, workflowsDir, GetVersion(), compiler.GetActionMode(), compiler.GetActionTag(), verbose, repoConfig, compiler.GetRepositorySlug()); err != nil { return fmt.Errorf("failed to generate maintenance workflow: %w", err) } diff --git a/pkg/cli/init_command.go b/pkg/cli/init_command.go index 75422db0da3..0177627b00d 100644 --- a/pkg/cli/init_command.go +++ b/pkg/cli/init_command.go @@ -94,6 +94,7 @@ Examples: initCommandLog.Printf("Executing init command: verbose=%v, mcp=%v, codespaces=%v, codespaceEnabled=%v, completions=%v, createPR=%v", verbose, mcp, codespaceRepos, codespaceEnabled, completions, createPR) opts := InitOptions{ + Ctx: cmd.Context(), Verbose: verbose, MCP: mcp, CodespaceRepos: codespaceRepos, diff --git a/pkg/cli/init_mcp_test.go b/pkg/cli/init_mcp_test.go index bc01b6d4c45..5bfce17057f 100644 --- a/pkg/cli/init_mcp_test.go +++ b/pkg/cli/init_mcp_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "encoding/json" "os" "os/exec" @@ -263,8 +264,8 @@ jobs: } // Call ensureCopilotSetupSteps - if err := ensureCopilotSetupSteps(false, workflow.ActionModeDev, "dev"); err != nil { - t.Fatalf("ensureCopilotSetupSteps() returned error: %v", err) + if err := ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev"); err != nil { + t.Fatalf("ensureCopilotSetupSteps(context.Background()) returned error: %v", err) } // Verify the file was NOT modified (should render instructions instead) @@ -332,8 +333,8 @@ jobs: } // Call ensureCopilotSetupSteps - if err := ensureCopilotSetupSteps(false, workflow.ActionModeDev, "dev"); err != nil { - t.Fatalf("ensureCopilotSetupSteps() returned error: %v", err) + if err := ensureCopilotSetupSteps(context.Background(), false, workflow.ActionModeDev, "dev"); err != nil { + t.Fatalf("ensureCopilotSetupSteps(context.Background()) returned error: %v", err) } // Verify the file was not modified (content should be the same) diff --git a/pkg/cli/init_test.go b/pkg/cli/init_test.go index 1fd324845ce..39f0acdbe1d 100644 --- a/pkg/cli/init_test.go +++ b/pkg/cli/init_test.go @@ -3,6 +3,7 @@ package cli import ( + "context" "os" "os/exec" "path/filepath" @@ -264,7 +265,7 @@ This is a test workflow. } // Call ensureMaintenanceWorkflow - err = ensureMaintenanceWorkflow(false) + err = ensureMaintenanceWorkflow(context.Background(), false) if err != nil { t.Logf("ensureMaintenanceWorkflow returned error (may be expected): %v", err) } diff --git a/pkg/cli/update_actions.go b/pkg/cli/update_actions.go index 118d18d6862..e51bd325aa7 100644 --- a/pkg/cli/update_actions.go +++ b/pkg/cli/update_actions.go @@ -636,7 +636,7 @@ func UpdateActionsInWorkflowFiles(ctx context.Context, workflowsDir, engineOverr // Recompile the updated workflow (unless --no-compile is set) if !noCompile { - if err := compileWorkflowWithRefresh(path, verbose, false, engineOverride, false); err != nil { + if err := compileWorkflowWithRefresh(ctx, path, verbose, false, engineOverride, false); err != nil { if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to recompile %s: %v", path, err))) } diff --git a/pkg/cli/update_command_test.go b/pkg/cli/update_command_test.go index 06620ac1b7c..092c486a51d 100644 --- a/pkg/cli/update_command_test.go +++ b/pkg/cli/update_command_test.go @@ -692,7 +692,7 @@ This is a test workflow. // Test with refreshStopTime=false (should preserve existing stop time if lock exists) t.Run("compileWorkflowWithRefresh false", func(t *testing.T) { - err := compileWorkflowWithRefresh(workflowFile, false, false, "", false) + err := compileWorkflowWithRefresh(context.Background(), workflowFile, false, false, "", false) if err != nil { t.Logf("Compilation failed (expected in test environment): %v", err) // In a test environment without full setup, compilation may fail, @@ -702,7 +702,7 @@ This is a test workflow. // Test with refreshStopTime=true (should regenerate stop time) t.Run("compileWorkflowWithRefresh true", func(t *testing.T) { - err := compileWorkflowWithRefresh(workflowFile, false, false, "", true) + err := compileWorkflowWithRefresh(context.Background(), workflowFile, false, false, "", true) if err != nil { t.Logf("Compilation failed (expected in test environment): %v", err) // In a test environment without full setup, compilation may fail, diff --git a/pkg/cli/update_workflows.go b/pkg/cli/update_workflows.go index 1c1eaba816b..9491bf84bdf 100644 --- a/pkg/cli/update_workflows.go +++ b/pkg/cli/update_workflows.go @@ -632,7 +632,7 @@ func updateWorkflow(ctx context.Context, wf *workflowWithSource, opts UpdateWork // Compile the updated workflow with refreshStopTime enabled (unless --no-compile is set) if !opts.NoCompile { updateLog.Printf("Compiling updated workflow: %s", wf.Name) - if err := compileWorkflowWithRefresh(wf.Path, opts.Verbose, false, opts.EngineOverride, true); err != nil { + if err := compileWorkflowWithRefresh(ctx, wf.Path, opts.Verbose, false, opts.EngineOverride, true); err != nil { updateLog.Printf("Compilation failed for workflow %s: %v", wf.Name, err) return fmt.Errorf("failed to compile updated workflow: %w", err) } diff --git a/pkg/cli/upgrade_command.go b/pkg/cli/upgrade_command.go index 6dc94614f97..45c07a1c534 100644 --- a/pkg/cli/upgrade_command.go +++ b/pkg/cli/upgrade_command.go @@ -183,7 +183,7 @@ func runUpgradeCommand(ctx context.Context, verbose bool, workflowDir string, no fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Updating agent file...")) upgradeLog.Print("Updating agent file") - if err := updateAgentFiles(verbose); err != nil { + if err := updateAgentFiles(ctx, verbose); err != nil { upgradeLog.Printf("Failed to update agent file: %v", err) return fmt.Errorf("failed to update agent file: %w", err) } @@ -276,7 +276,7 @@ func runUpgradeCommand(ctx context.Context, verbose bool, workflowDir string, no } // Compile all workflow files - stats, compileErr := compileAllWorkflowFiles(compiler, workflowsDir, verbose) + stats, compileErr := compileAllWorkflowFiles(ctx, compiler, workflowsDir, verbose) if compileErr != nil { upgradeLog.Printf("Failed to compile workflows: %v", compileErr) // Don't fail the upgrade if compilation fails - this is non-critical @@ -312,7 +312,7 @@ func runUpgradeCommand(ctx context.Context, verbose bool, workflowDir string, no } // updateAgentFiles updates the dispatcher agent file to the latest template -func updateAgentFiles(verbose bool) error { +func updateAgentFiles(ctx context.Context, verbose bool) error { // Update dispatcher agent if err := ensureAgenticWorkflowsDispatcher(verbose, false); err != nil { upgradeLog.Printf("Failed to update dispatcher agent: %v", err) @@ -321,7 +321,7 @@ func updateAgentFiles(verbose bool) error { // Upgrade copilot-setup-steps.yml version actionMode := workflow.DetectActionMode(GetVersion()) - if err := upgradeCopilotSetupSteps(verbose, actionMode, GetVersion()); err != nil { + if err := upgradeCopilotSetupSteps(ctx, verbose, actionMode, GetVersion()); err != nil { upgradeLog.Printf("Failed to upgrade copilot-setup-steps.yml: %v", err) // Don't fail the upgrade if copilot-setup-steps upgrade fails - this is non-critical fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to upgrade copilot-setup-steps.yml: %v", err))) diff --git a/pkg/workflow/action_reference.go b/pkg/workflow/action_reference.go index 3780d713e66..210a9668ea4 100644 --- a/pkg/workflow/action_reference.go +++ b/pkg/workflow/action_reference.go @@ -35,14 +35,14 @@ const ( // - For action mode with resolver: "github/gh-aw-actions/setup@ # " (SHA-pinned) // - For action mode without resolver: "github/gh-aw-actions/setup@" (tag-based, SHA resolved later) // - Falls back to local path if version is invalid in release/action mode -func ResolveSetupActionReference(actionMode ActionMode, version string, actionTag string, resolver SHAResolver) string { - return resolveSetupActionRef(actionMode, version, actionTag, resolver, "") +func ResolveSetupActionReference(ctx context.Context, actionMode ActionMode, version string, actionTag string, resolver SHAResolver) string { + return resolveSetupActionRef(ctx, actionMode, version, actionTag, resolver, "") } // resolveSetupActionRef is the internal implementation of ResolveSetupActionReference // that accepts an optional actionsOrgRepo override. When actionsOrgRepo is empty, // GitHubActionsOrgRepo is used. -func resolveSetupActionRef(actionMode ActionMode, version string, actionTag string, resolver SHAResolver, actionsOrgRepo string) string { +func resolveSetupActionRef(ctx context.Context, actionMode ActionMode, version string, actionTag string, resolver SHAResolver, actionsOrgRepo string) string { if actionsOrgRepo == "" { actionsOrgRepo = GitHubActionsOrgRepo } @@ -75,7 +75,7 @@ func resolveSetupActionRef(actionMode ActionMode, version string, actionTag stri // If a resolver is available, try to resolve the SHA if resolver != nil { - sha, err := resolver.ResolveSHA(context.Background(), actionRepo, tag) + sha, err := resolver.ResolveSHA(ctx, actionRepo, tag) if err == nil && sha != "" { pinnedRef := formatActionReference(actionRepo, sha, tag) actionRefLog.Printf("Action mode: resolved %s to SHA-pinned reference: %s", remoteRef, pinnedRef) @@ -113,7 +113,7 @@ func resolveSetupActionRef(actionMode ActionMode, version string, actionTag stri // If a resolver is available, try to resolve the SHA if resolver != nil { - sha, err := resolver.ResolveSHA(context.Background(), actionRepo, tag) + sha, err := resolver.ResolveSHA(ctx, actionRepo, tag) if err == nil && sha != "" { pinnedRef := formatActionReference(actionRepo, sha, tag) actionRefLog.Printf("Release mode: resolved %s to SHA-pinned reference: %s", remoteRef, pinnedRef) @@ -164,13 +164,13 @@ func (c *Compiler) resolveActionReference(localActionPath string, data *Workflow resolver = data.ActionResolver } if c.actionTag != "" { - return resolveSetupActionRef(c.actionMode, c.version, c.actionTag, resolver, c.effectiveActionsRepo()) + return resolveSetupActionRef(c.ctx, c.actionMode, c.version, c.actionTag, resolver, c.effectiveActionsRepo()) } if !hasActionTag { - return resolveSetupActionRef(c.actionMode, c.version, "", resolver, c.effectiveActionsRepo()) + return resolveSetupActionRef(c.ctx, c.actionMode, c.version, "", resolver, c.effectiveActionsRepo()) } // hasActionTag is true and no compiler actionTag: use action mode with the frontmatter tag - return resolveSetupActionRef(ActionModeAction, c.version, frontmatterActionTag, resolver, c.effectiveActionsRepo()) + return resolveSetupActionRef(c.ctx, ActionModeAction, c.version, frontmatterActionTag, resolver, c.effectiveActionsRepo()) } // Action mode - use external gh-aw-actions repository diff --git a/pkg/workflow/action_reference_test.go b/pkg/workflow/action_reference_test.go index ef16016259e..ea44eb945f1 100644 --- a/pkg/workflow/action_reference_test.go +++ b/pkg/workflow/action_reference_test.go @@ -3,6 +3,7 @@ package workflow import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -338,7 +339,7 @@ func TestResolveSetupActionReference(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Pass nil for data to test backward compatibility with standalone usage - ref := ResolveSetupActionReference(tt.actionMode, tt.version, tt.actionTag, nil) + ref := ResolveSetupActionReference(context.Background(), tt.actionMode, tt.version, tt.actionTag, nil) assert.Equal(t, tt.expectedRef, ref, tt.description) }) } @@ -352,14 +353,14 @@ func TestResolveSetupActionReferenceWithData(t *testing.T) { // The resolver will fail to resolve github/gh-aw/actions/setup@v1.0.0 // since it's not a real tag, but it should fall back gracefully - ref := ResolveSetupActionReference(ActionModeRelease, "v1.0.0", "", resolver) + ref := ResolveSetupActionReference(context.Background(), ActionModeRelease, "v1.0.0", "", resolver) // Without a valid pin or successful resolution, should return tag-based reference assert.Equal(t, "github/gh-aw/actions/setup@v1.0.0", ref, "should return tag-based reference when SHA resolution fails") }) t.Run("release mode with nil resolver returns tag-based reference", func(t *testing.T) { - ref := ResolveSetupActionReference(ActionModeRelease, "v1.0.0", "", nil) + ref := ResolveSetupActionReference(context.Background(), ActionModeRelease, "v1.0.0", "", nil) assert.Equal(t, "github/gh-aw/actions/setup@v1.0.0", ref, "should return tag-based reference when no resolver provided") }) } diff --git a/pkg/workflow/action_sha_checker.go b/pkg/workflow/action_sha_checker.go index 57a3ea3c3dc..504b669a67d 100644 --- a/pkg/workflow/action_sha_checker.go +++ b/pkg/workflow/action_sha_checker.go @@ -100,7 +100,7 @@ func ExtractActionsFromLockFile(lockFilePath string) ([]ActionUsage, error) { } // CheckActionSHAUpdates checks if actions need updating by comparing with latest SHAs -func CheckActionSHAUpdates(actions []ActionUsage, resolver *ActionResolver) []ActionUpdateCheck { +func CheckActionSHAUpdates(ctx context.Context, actions []ActionUsage, resolver *ActionResolver) []ActionUpdateCheck { actionSHACheckerLog.Printf("Checking %d actions for updates", len(actions)) results := make([]ActionUpdateCheck, 0, len(actions)) @@ -118,7 +118,7 @@ func CheckActionSHAUpdates(actions []ActionUsage, resolver *ActionResolver) []Ac } // Resolve the latest SHA for this version - latestSHA, err := resolver.ResolveSHA(context.Background(), action.Repo, action.Version) + latestSHA, err := resolver.ResolveSHA(ctx, action.Repo, action.Version) if err != nil { actionSHACheckerLog.Printf("Failed to resolve %s@%s: %v", action.Repo, action.Version, err) check.Message = fmt.Sprintf("Unable to check for updates: %v", err) diff --git a/pkg/workflow/action_sha_checker_integration_test.go b/pkg/workflow/action_sha_checker_integration_test.go index c44c4db4d32..2e1566177d6 100644 --- a/pkg/workflow/action_sha_checker_integration_test.go +++ b/pkg/workflow/action_sha_checker_integration_test.go @@ -3,6 +3,7 @@ package workflow import ( + "context" "os" "path/filepath" "strings" @@ -53,7 +54,7 @@ jobs: // Test 1: Validation with up-to-date actions (should not error) t.Run("UpToDate", func(t *testing.T) { - err := ValidateActionSHAsInLockFile(lockFile, cache, false) + err := ValidateActionSHAsInLockFile(context.Background(), lockFile, cache, false) if err != nil { t.Errorf("Unexpected error with up-to-date actions: %v", err) } @@ -79,7 +80,7 @@ jobs: // Test 2: Validation with outdated actions (should emit warnings but not error) t.Run("Outdated", func(t *testing.T) { // Note: This will emit warnings to stderr, but should not return an error - err := ValidateActionSHAsInLockFile(outdatedLockFile, cache, false) + err := ValidateActionSHAsInLockFile(context.Background(), outdatedLockFile, cache, false) if err != nil { t.Errorf("Unexpected error with outdated actions: %v", err) } @@ -110,7 +111,7 @@ jobs: cache := NewActionCache(tmpDir) // Validation should handle missing cache gracefully - err := ValidateActionSHAsInLockFile(lockFile, cache, false) + err := ValidateActionSHAsInLockFile(context.Background(), lockFile, cache, false) if err != nil { t.Errorf("Unexpected error with missing cache: %v", err) } @@ -220,7 +221,7 @@ jobs: // Capture stderr output to verify message format // Note: In a real scenario, we'd redirect stderr, but for this test // we just ensure it doesn't error - err := ValidateActionSHAsInLockFile(lockFile, cache, true) + err := ValidateActionSHAsInLockFile(context.Background(), lockFile, cache, true) if err != nil { t.Errorf("Unexpected error: %v", err) } diff --git a/pkg/workflow/action_sha_checker_test.go b/pkg/workflow/action_sha_checker_test.go index 7097551833a..e2aaebe1615 100644 --- a/pkg/workflow/action_sha_checker_test.go +++ b/pkg/workflow/action_sha_checker_test.go @@ -3,6 +3,7 @@ package workflow import ( + "context" "os" "path/filepath" "testing" @@ -137,7 +138,7 @@ func TestCheckActionSHAUpdates(t *testing.T) { resolver := NewActionResolver(cache) // Check for updates - checks := CheckActionSHAUpdates(actions, resolver) + checks := CheckActionSHAUpdates(context.Background(), actions, resolver) // Verify results if len(checks) != 2 { diff --git a/pkg/workflow/action_sha_validation_test.go b/pkg/workflow/action_sha_validation_test.go index 222ed4c4958..b72529af664 100644 --- a/pkg/workflow/action_sha_validation_test.go +++ b/pkg/workflow/action_sha_validation_test.go @@ -3,6 +3,7 @@ package workflow import ( + "context" "os" "path/filepath" "regexp" @@ -228,7 +229,7 @@ jobs: // Run validation - even if no updates are detected, this exercises the code path // In a real scenario with network access, this would detect and save updates - err := ValidateActionSHAsInLockFile(lockFile, cache, false) + err := ValidateActionSHAsInLockFile(context.Background(), lockFile, cache, false) if err != nil { t.Fatalf("Validation failed: %v", err) } diff --git a/pkg/workflow/central_slash_command_workflow.go b/pkg/workflow/central_slash_command_workflow.go index bdbfce7c321..aeb9e0647d8 100644 --- a/pkg/workflow/central_slash_command_workflow.go +++ b/pkg/workflow/central_slash_command_workflow.go @@ -1,6 +1,7 @@ package workflow import ( + "context" "encoding/json" "fmt" "os" @@ -37,7 +38,7 @@ type commandsHeaderMetadata struct { // GenerateCentralSlashCommandWorkflow generates a single centralized slash-command trigger // workflow for workflows that opt into on.slash_command.strategy: centralized. // When no centralized slash-command workflows are found, any existing generated file is deleted. -func GenerateCentralSlashCommandWorkflow(workflowDataList []*WorkflowData, workflowDir string) error { +func GenerateCentralSlashCommandWorkflow(ctx context.Context, workflowDataList []*WorkflowData, workflowDir string) error { centralSlashCommandWorkflowLog.Printf("Generating centralized slash-command workflow from %d workflow(s)", len(workflowDataList)) slashRoutesByCommand, labelRoutesByCommand, mergedEvents := collectCentralCommandRoutes(workflowDataList) @@ -55,7 +56,7 @@ func GenerateCentralSlashCommandWorkflow(workflowDataList []*WorkflowData, workf } actionMode := DetectActionMode(GetVersion()) - setupActionRef := ResolveSetupActionReference(actionMode, GetVersion(), "", nil) + setupActionRef := ResolveSetupActionReference(ctx, actionMode, GetVersion(), "", nil) content, err := buildCentralSlashCommandWorkflowYAML(slashRoutesByCommand, labelRoutesByCommand, mergedEvents, resolveCentralSlashRunsOn(workflowDataList), setupActionRef) if err != nil { diff --git a/pkg/workflow/central_slash_command_workflow_test.go b/pkg/workflow/central_slash_command_workflow_test.go index c5acfc683d1..26ce468b36a 100644 --- a/pkg/workflow/central_slash_command_workflow_test.go +++ b/pkg/workflow/central_slash_command_workflow_test.go @@ -3,6 +3,7 @@ package workflow import ( + "context" "encoding/json" "os" "path/filepath" @@ -56,7 +57,7 @@ func TestGenerateCentralSlashCommandWorkflow_GeneratesWorkflow(t *testing.T) { }, } - require.NoError(t, GenerateCentralSlashCommandWorkflow(data, tmpDir)) + require.NoError(t, GenerateCentralSlashCommandWorkflow(context.Background(), data, tmpDir)) generatedPath := filepath.Join(tmpDir, centralSlashCommandWorkflowFilename) content, err := os.ReadFile(generatedPath) @@ -120,7 +121,7 @@ func TestGenerateCentralSlashCommandWorkflow_DeletesWhenUnused(t *testing.T) { }, } - require.NoError(t, GenerateCentralSlashCommandWorkflow(data, tmpDir)) + require.NoError(t, GenerateCentralSlashCommandWorkflow(context.Background(), data, tmpDir)) _, err := os.Stat(generatedPath) require.Error(t, err) require.True(t, os.IsNotExist(err)) @@ -137,7 +138,7 @@ func TestGenerateCentralSlashCommandWorkflow_GeneratesForDecentralizedLabelsOnly }, } - require.NoError(t, GenerateCentralSlashCommandWorkflow(data, tmpDir)) + require.NoError(t, GenerateCentralSlashCommandWorkflow(context.Background(), data, tmpDir)) content, err := os.ReadFile(filepath.Join(tmpDir, centralSlashCommandWorkflowFilename)) require.NoError(t, err) text := string(content) @@ -300,7 +301,7 @@ func TestGenerateCentralSlashCommandWorkflow_UsesCentralizedRunsOnResolution(t * }, } - require.NoError(t, GenerateCentralSlashCommandWorkflow(data, tmpDir)) + require.NoError(t, GenerateCentralSlashCommandWorkflow(context.Background(), data, tmpDir)) content, err := os.ReadFile(filepath.Join(tmpDir, centralSlashCommandWorkflowFilename)) require.NoError(t, err) require.Contains(t, string(content), "runs-on: self-hosted") diff --git a/pkg/workflow/compiler_custom_actions_test.go b/pkg/workflow/compiler_custom_actions_test.go index 3531e1eb7cb..c89ec9bedc9 100644 --- a/pkg/workflow/compiler_custom_actions_test.go +++ b/pkg/workflow/compiler_custom_actions_test.go @@ -3,6 +3,7 @@ package workflow import ( + "context" "os" "strings" "testing" @@ -378,7 +379,7 @@ func TestCheckoutActionsFolderDevModeAlwaysEmitsCheckout(t *testing.T) { // TestResolveSetupActionReferenceActionMode tests that action mode resolves to the external gh-aw-actions repo func TestResolveSetupActionReferenceActionMode(t *testing.T) { - ref := ResolveSetupActionReference(ActionModeAction, "v1.2.3", "", nil) + ref := ResolveSetupActionReference(context.Background(), ActionModeAction, "v1.2.3", "", nil) if ref != "github/gh-aw-actions/setup@v1.2.3" { t.Errorf("Action mode should resolve to 'github/gh-aw-actions/setup@v1.2.3', got %q", ref) } @@ -386,7 +387,7 @@ func TestResolveSetupActionReferenceActionMode(t *testing.T) { // TestResolveSetupActionReferenceActionModeWithTag tests action mode with an explicit action tag func TestResolveSetupActionReferenceActionModeWithTag(t *testing.T) { - ref := ResolveSetupActionReference(ActionModeAction, "v1.0.0", "v2.0.0", nil) + ref := ResolveSetupActionReference(context.Background(), ActionModeAction, "v1.0.0", "v2.0.0", nil) if ref != "github/gh-aw-actions/setup@v2.0.0" { t.Errorf("Action mode with tag should resolve to 'github/gh-aw-actions/setup@v2.0.0', got %q", ref) } @@ -394,7 +395,7 @@ func TestResolveSetupActionReferenceActionModeWithTag(t *testing.T) { // TestResolveSetupActionReferenceActionModeDevVersion tests action mode falls back to local path for dev version func TestResolveSetupActionReferenceActionModeDevVersion(t *testing.T) { - ref := ResolveSetupActionReference(ActionModeAction, "dev", "", nil) + ref := ResolveSetupActionReference(context.Background(), ActionModeAction, "dev", "", nil) if ref != "./actions/setup" { t.Errorf("Action mode with dev version should fall back to './actions/setup', got %q", ref) } @@ -472,7 +473,7 @@ func TestResolveSetupActionReferenceActionModeWithResolver(t *testing.T) { // The resolver will fail to resolve github/gh-aw-actions/setup@v1.0.0 // since it's not a real tag, but it should fall back gracefully to tag-based reference - ref := ResolveSetupActionReference(ActionModeAction, "v1.0.0", "", resolver) + ref := ResolveSetupActionReference(context.Background(), ActionModeAction, "v1.0.0", "", resolver) // Without a valid pin or successful resolution, should return tag-based reference if ref != "github/gh-aw-actions/setup@v1.0.0" { @@ -481,7 +482,7 @@ func TestResolveSetupActionReferenceActionModeWithResolver(t *testing.T) { }) t.Run("action mode with nil resolver returns tag-based reference", func(t *testing.T) { - ref := ResolveSetupActionReference(ActionModeAction, "v1.0.0", "", nil) + ref := ResolveSetupActionReference(context.Background(), ActionModeAction, "v1.0.0", "", nil) if ref != "github/gh-aw-actions/setup@v1.0.0" { t.Errorf("expected 'github/gh-aw-actions/setup@v1.0.0', got %q", ref) } diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 485d6ec3f67..784d0c7cf16 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -165,6 +165,7 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error) // Use shared action cache and resolver from the compiler actionCache, actionResolver := c.getSharedActionResolver() + workflowData.Ctx = c.ctx workflowData.ActionCache = actionCache workflowData.ActionResolver = actionResolver workflowData.ActionPinWarnings = c.actionPinWarnings diff --git a/pkg/workflow/compiler_string_api.go b/pkg/workflow/compiler_string_api.go index 84a08e5e54d..1c77249caf0 100644 --- a/pkg/workflow/compiler_string_api.go +++ b/pkg/workflow/compiler_string_api.go @@ -183,6 +183,7 @@ func (c *Compiler) ParseWorkflowString(content string, virtualPath string) (*Wor // Setup action cache and resolver actionCache, actionResolver := c.getSharedActionResolver() + workflowData.Ctx = c.ctx workflowData.ActionCache = actionCache workflowData.ActionResolver = actionResolver workflowData.ActionPinWarnings = c.actionPinWarnings diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 0cc199be7d8..11a101300a8 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -1,6 +1,7 @@ package workflow import ( + "context" "os" actionpins "github.com/github/gh-aw/pkg/actionpins" @@ -48,6 +49,12 @@ func WithVersion(version string) CompilerOption { return func(c *Compiler) { c.version = version } } +// WithContext sets the context used for network operations such as SHA resolution. +// Defaults to context.Background() if not specified. +func WithContext(ctx context.Context) CompilerOption { + return func(c *Compiler) { c.ctx = ctx } +} + // FileCreationTracker interface for tracking files created during compilation type FileCreationTracker interface { TrackCreated(filePath string) @@ -55,6 +62,7 @@ type FileCreationTracker interface { // Compiler handles converting markdown workflows to GitHub Actions YAML type Compiler struct { + ctx context.Context // Context for network operations (e.g. SHA resolution); defaults to context.Background() verbose bool quiet bool // If true, suppress success messages (for interactive mode) engineOverride string @@ -119,6 +127,7 @@ func NewCompiler(opts ...CompilerOption) *Compiler { // Create compiler with defaults c := &Compiler{ + ctx: context.Background(), // Default context; override with WithContext verbose: false, engineOverride: "", version: version, @@ -151,6 +160,11 @@ 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 +} + // 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. @@ -534,6 +548,7 @@ type WorkflowData struct { ToolsTimeout string // timeout for tool/MCP operations: numeric string (seconds) or GitHub Actions expression (empty = use engine default) ToolsStartupTimeout string // timeout for MCP server startup: numeric string (seconds) or GitHub Actions expression (empty = use engine default) Features map[string]any // feature flags and configuration options from frontmatter (supports bool and string values) + Ctx context.Context // context propagated from the caller for network operations (e.g. SHA resolution) ActionCache *ActionCache // cache for action pin resolutions ActionResolver *ActionResolver // resolver for action pins DockerImages []string // container images collected at compile time (pinned refs when pins are cached) @@ -587,7 +602,8 @@ func (d *WorkflowData) PinContext() *actionpins.PinContext { if d.ActionPinWarnings == nil { d.ActionPinWarnings = make(map[string]bool) } - ctx := &actionpins.PinContext{ + pinCtx := &actionpins.PinContext{ + Ctx: d.Ctx, StrictMode: d.StrictMode, EnforcePinned: true, AllowActionRefs: d.AllowActionRefs, @@ -603,9 +619,9 @@ func (d *WorkflowData) PinContext() *actionpins.PinContext { // Only set Resolver if non-nil to avoid passing a typed nil interface value // (which would be non-nil in actionpins but crash on method call). if d.ActionResolver != nil { - ctx.Resolver = d.ActionResolver + pinCtx.Resolver = d.ActionResolver } - return ctx + return pinCtx } // BaseSafeOutputConfig holds common configuration fields for all safe output types diff --git a/pkg/workflow/lock_validation.go b/pkg/workflow/lock_validation.go index cd4fea43704..6e1d7bb3b6a 100644 --- a/pkg/workflow/lock_validation.go +++ b/pkg/workflow/lock_validation.go @@ -24,6 +24,7 @@ package workflow import ( + "context" "fmt" "os" "strings" @@ -67,7 +68,7 @@ func ValidateLockSchemaCompatibility(content string, lockFilePath string) error } // ValidateActionSHAsInLockFile validates action SHAs in a lock file and emits warnings -func ValidateActionSHAsInLockFile(lockFilePath string, cache *ActionCache, verbose bool) error { +func ValidateActionSHAsInLockFile(ctx context.Context, lockFilePath string, cache *ActionCache, verbose bool) error { actionSHACheckerLog.Printf("Validating action SHAs in: %s", lockFilePath) // Extract actions from lock file @@ -88,7 +89,7 @@ func ValidateActionSHAsInLockFile(lockFilePath string, cache *ActionCache, verbo resolver := NewActionResolver(cache) // Check for updates - checks := CheckActionSHAUpdates(actions, resolver) + checks := CheckActionSHAUpdates(ctx, actions, resolver) // Count and report updates updateCount := 0 diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index 1d7def12f6e..0b5d381b3dc 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -20,7 +20,7 @@ var maintenanceLog = logger.New("workflow:maintenance_workflow") // In release mode: installs the released CLI via the setup-cli action (gh aw available) // In action mode: installs the released CLI via the gh-aw-actions/setup-cli action (gh aw available) // When resolver is non-nil, attempts to resolve the setup-cli action to a SHA-pinned reference. -func generateInstallCLISteps(actionMode ActionMode, version string, actionTag string, resolver SHAResolver) string { +func generateInstallCLISteps(ctx context.Context, actionMode ActionMode, version string, actionTag string, resolver SHAResolver) string { if actionMode == ActionModeDev { return ` - name: Setup Go uses: ` + getActionPin("actions/setup-go") + ` @@ -42,7 +42,7 @@ func generateInstallCLISteps(actionMode ActionMode, version string, actionTag st // Action mode: use setup-cli action from external gh-aw-actions repository if actionMode == ActionModeAction { actionRepo := GitHubActionsOrgRepo + "/setup-cli" - ref := resolveActionRef(actionRepo, cliTag, resolver) + ref := resolveActionRef(ctx, actionRepo, cliTag, resolver) return ` - name: Install gh-aw uses: ` + ref + ` with: @@ -53,7 +53,7 @@ func generateInstallCLISteps(actionMode ActionMode, version string, actionTag st // Release mode: use setup-cli action (consistent with copilot-setup-steps.yml) actionRepo := GitHubOrgRepo + "/actions/setup-cli" - ref := resolveActionRef(actionRepo, cliTag, resolver) + ref := resolveActionRef(ctx, actionRepo, cliTag, resolver) return ` - name: Install gh-aw uses: ` + ref + ` with: @@ -65,9 +65,9 @@ func generateInstallCLISteps(actionMode ActionMode, version string, actionTag st // resolveActionRef attempts to resolve an action repo@tag to a SHA-pinned reference // using the provided resolver. If the resolver is nil or resolution fails, it returns // the tag-based reference (repo@tag). -func resolveActionRef(actionRepo, tag string, resolver SHAResolver) string { +func resolveActionRef(ctx context.Context, actionRepo, tag string, resolver SHAResolver) string { if resolver != nil && tag != "" && tag != "dev" { - sha, err := resolver.ResolveSHA(context.Background(), actionRepo, tag) + sha, err := resolver.ResolveSHA(ctx, actionRepo, tag) if err != nil { maintenanceLog.Printf("Failed to resolve SHA for %s@%s: %v, falling back to tag reference", actionRepo, tag, err) } else if sha != "" { @@ -117,7 +117,7 @@ func FetchDefaultBranch(slug string) string { // maintenance workflow is deleted and the function returns immediately. // repoSlug is the owner/repo slug used to determine the default branch for the push // trigger; pass an empty string to fall back to "main". -func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir string, version string, actionMode ActionMode, actionTag string, verbose bool, repoConfig *RepoConfig, repoSlug string) error { +func GenerateMaintenanceWorkflow(ctx context.Context, workflowDataList []*WorkflowData, workflowDir string, version string, actionMode ActionMode, actionTag string, verbose bool, repoConfig *RepoConfig, repoSlug string) error { maintenanceLog.Print("Checking if maintenance workflow is needed") // Respect explicit opt-out from aw.json: maintenance: false @@ -161,7 +161,7 @@ func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir s // Even without expires, side-repo targets still need maintenance workflows // for safe_outputs, create_labels, and validate operations. - return generateAllSideRepoMaintenanceWorkflows(workflowDataList, workflowDir, version, actionMode, actionTag, runsOnValue, resolver, false, 0) + return generateAllSideRepoMaintenanceWorkflows(ctx, workflowDataList, workflowDir, version, actionMode, actionTag, runsOnValue, resolver, false, 0) } maintenanceLog.Printf("Generating maintenance workflow for expired discussions, issues, and pull requests (minimum expires: %d hours)", minExpires) @@ -181,7 +181,7 @@ func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir s defaultBranch := FetchDefaultBranch(repoSlug) // Generate the YAML content for the maintenance workflow - content := buildMaintenanceWorkflowYAML(cronSchedule, scheduleDesc, minExpiresDays, runsOnValue, actionMode, version, actionTag, resolver, configuredRunsOn, defaultBranch, disableLabelTrigger) + content := buildMaintenanceWorkflowYAML(ctx, cronSchedule, scheduleDesc, minExpiresDays, runsOnValue, actionMode, version, actionTag, resolver, configuredRunsOn, defaultBranch, disableLabelTrigger) // Write the maintenance workflow file maintenanceFile := filepath.Join(workflowDir, "agentics-maintenance.yml") @@ -194,7 +194,7 @@ func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir s maintenanceLog.Print("Maintenance workflow generated successfully") // Generate side-repo maintenance workflows for any SideRepoOps targets detected. - if err := generateAllSideRepoMaintenanceWorkflows(workflowDataList, workflowDir, version, actionMode, actionTag, runsOnValue, resolver, hasExpires, minExpiresDays); err != nil { + if err := generateAllSideRepoMaintenanceWorkflows(ctx, workflowDataList, workflowDir, version, actionMode, actionTag, runsOnValue, resolver, hasExpires, minExpiresDays); err != nil { return err } diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go index d07ad6c6469..5a3674bf5f0 100644 --- a/pkg/workflow/maintenance_workflow_test.go +++ b/pkg/workflow/maintenance_workflow_test.go @@ -3,6 +3,7 @@ package workflow import ( + "context" "fmt" "os" "path/filepath" @@ -152,7 +153,7 @@ func TestGenerateMaintenanceWorkflow_WithExpires(t *testing.T) { tmpDir := t.TempDir() // Call GenerateMaintenanceWorkflow - err := GenerateMaintenanceWorkflow(tt.workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), tt.workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") // Check error expectation if tt.expectError && err == nil { @@ -241,7 +242,7 @@ func TestGenerateMaintenanceWorkflow_DeletesExistingFile(t *testing.T) { } // Call GenerateMaintenanceWorkflow - err := GenerateMaintenanceWorkflow(tt.workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), tt.workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -273,7 +274,7 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) { } tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -613,7 +614,7 @@ func TestGenerateMaintenanceWorkflow_DisableAgenticWorkflowJob(t *testing.T) { cfg := &RepoConfig{ Maintenance: &MaintenanceConfig{LabelTriggers: &trueVal}, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -760,7 +761,7 @@ func TestGenerateMaintenanceWorkflow_LabelTriggers_Disabled(t *testing.T) { cfg := &RepoConfig{ Maintenance: &MaintenanceConfig{LabelTriggers: &falseVal}, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -803,7 +804,7 @@ func TestGenerateMaintenanceWorkflow_LabelTriggers_Default(t *testing.T) { tmpDir := t.TempDir() // Default: LabelTriggers is nil (omitted) → treated as false (opt-in semantics) → jobs absent - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -844,7 +845,7 @@ func TestGenerateMaintenanceWorkflow_LabelTriggers_ExplicitTrue(t *testing.T) { cfg := &RepoConfig{ Maintenance: &MaintenanceConfig{LabelTriggers: &trueVal}, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -904,7 +905,7 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Run("dev mode includes push trigger on main for workflow md files", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -928,7 +929,7 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Run("dev mode uses custom default branch from buildMaintenanceWorkflowYAML", func(t *testing.T) { // Call buildMaintenanceWorkflowYAML directly to test the branch substitution // without needing a live GitHub API call (FetchDefaultBranch falls back to "main" with no slug) - yaml := buildMaintenanceWorkflowYAML("37 */2 * * *", "Every 2 hours", 1, "ubuntu-slim", ActionModeDev, "v1.0.0", "", nil, nil, "develop", false) + yaml := buildMaintenanceWorkflowYAML(context.Background(), "37 */2 * * *", "Every 2 hours", 1, "ubuntu-slim", ActionModeDev, "v1.0.0", "", nil, nil, "develop", false) if !strings.Contains(yaml, " - develop") { t.Errorf("Push trigger should use the provided default branch 'develop', got:\n%s", yaml[:min(500, len(yaml))]) } @@ -939,7 +940,7 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Run("release mode does not include push trigger", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -956,7 +957,7 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Run("close-expired-entities and secret-validation exclude push events", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -983,7 +984,7 @@ func TestGenerateMaintenanceWorkflow_PushTrigger(t *testing.T) { t.Run("compile-workflows runs on push events (no push exclusion)", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1024,7 +1025,7 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { t.Run("release mode with action-tag uses remote ref", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1060,7 +1061,7 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataListWithResolver, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataListWithResolver, tmpDir, "v1.0.0", ActionModeRelease, "v0.47.4", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1079,7 +1080,7 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { t.Run("dev mode ignores action-tag and uses local path", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "v0.47.4", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "v0.47.4", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1095,7 +1096,7 @@ func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) { func TestGenerateInstallCLISteps(t *testing.T) { t.Run("dev mode generates Setup Go and Build gh-aw steps", func(t *testing.T) { - result := generateInstallCLISteps(ActionModeDev, "v1.0.0", "", nil) + result := generateInstallCLISteps(context.Background(), ActionModeDev, "v1.0.0", "", nil) if !strings.Contains(result, "Setup Go") { t.Errorf("Dev mode should include Setup Go step, got:\n%s", result) } @@ -1108,7 +1109,7 @@ func TestGenerateInstallCLISteps(t *testing.T) { }) t.Run("release mode generates setup-cli action step", func(t *testing.T) { - result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "", nil) + result := generateInstallCLISteps(context.Background(), ActionModeRelease, "v1.0.0", "", nil) if !strings.Contains(result, "github/gh-aw/actions/setup-cli@v1.0.0") { t.Errorf("Release mode should use setup-cli action with version, got:\n%s", result) } @@ -1121,7 +1122,7 @@ func TestGenerateInstallCLISteps(t *testing.T) { }) t.Run("release mode uses actionTag over version", func(t *testing.T) { - result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "v2.0.0", nil) + result := generateInstallCLISteps(context.Background(), ActionModeRelease, "v1.0.0", "v2.0.0", nil) if !strings.Contains(result, "setup-cli@v2.0.0") { t.Errorf("Release mode should use actionTag v2.0.0, got:\n%s", result) } @@ -1133,7 +1134,7 @@ func TestGenerateInstallCLISteps(t *testing.T) { cache.Set("github/gh-aw/actions/setup-cli", "v1.0.0", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") resolver := NewActionResolver(cache) - result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "", resolver) + result := generateInstallCLISteps(context.Background(), ActionModeRelease, "v1.0.0", "", resolver) expectedRef := "github/gh-aw/actions/setup-cli@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # v1.0.0" if !strings.Contains(result, expectedRef) { t.Errorf("Release mode with resolver should use SHA-pinned setup-cli reference %q, got:\n%s", expectedRef, result) @@ -1150,7 +1151,7 @@ func TestGenerateInstallCLISteps(t *testing.T) { cache.Set("github/gh-aw-actions/setup-cli", "v1.0.0", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") resolver := NewActionResolver(cache) - result := generateInstallCLISteps(ActionModeAction, "v1.0.0", "", resolver) + result := generateInstallCLISteps(context.Background(), ActionModeAction, "v1.0.0", "", resolver) expectedRef := "github/gh-aw-actions/setup-cli@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # v1.0.0" if !strings.Contains(result, expectedRef) { t.Errorf("Action mode with resolver should use SHA-pinned setup-cli reference %q, got:\n%s", expectedRef, result) @@ -1162,7 +1163,7 @@ func TestGenerateInstallCLISteps(t *testing.T) { }) t.Run("release mode without resolver falls back to tag reference", func(t *testing.T) { - result := generateInstallCLISteps(ActionModeRelease, "v1.0.0", "", nil) + result := generateInstallCLISteps(context.Background(), ActionModeRelease, "v1.0.0", "", nil) if !strings.Contains(result, "github/gh-aw/actions/setup-cli@v1.0.0") { t.Errorf("Release mode without resolver should fall back to tag reference, got:\n%s", result) } @@ -1192,7 +1193,7 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) { t.Run("dev mode run_operation uses build from source", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1211,7 +1212,7 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) { t.Run("release mode run_operation uses setup-cli action not gh extension install", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1233,7 +1234,7 @@ func TestGenerateMaintenanceWorkflow_RunOperationCLICodegen(t *testing.T) { t.Run("dev mode compile_workflows uses same codegen as run_operation", func(t *testing.T) { tmpDir := t.TempDir() - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1283,7 +1284,7 @@ func TestGenerateMaintenanceWorkflow_SetupCLISHAPinning(t *testing.T) { cache.Set("github/gh-aw/actions/setup", "v1.0.0", "dddddddddddddddddddddddddddddddddddddddd") resolver := NewActionResolver(cache) - err := GenerateMaintenanceWorkflow(workflowDataListWithResolver(resolver), tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataListWithResolver(resolver), tmpDir, "v1.0.0", ActionModeRelease, "v1.0.0", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1322,7 +1323,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { cfg := &RepoConfig{ Maintenance: &MaintenanceConfig{RunsOn: RunsOnValue{"my-custom-runner"}}, } - err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") + err := GenerateMaintenanceWorkflow(context.Background(), makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1345,7 +1346,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { cfg := &RepoConfig{ Maintenance: &MaintenanceConfig{RunsOn: RunsOnValue{"self-hosted", "linux"}}, } - err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") + err := GenerateMaintenanceWorkflow(context.Background(), makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1367,7 +1368,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { t.Fatalf("Failed to write pre-existing file: %v", err) } cfg := &RepoConfig{MaintenanceDisabled: true} - err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") + err := GenerateMaintenanceWorkflow(context.Background(), makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1379,7 +1380,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { t.Run("maintenance disabled skips generation even with expires", func(t *testing.T) { tmpDir := t.TempDir() cfg := &RepoConfig{MaintenanceDisabled: true} - err := GenerateMaintenanceWorkflow(makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") + err := GenerateMaintenanceWorkflow(context.Background(), makeList(), tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1401,7 +1402,7 @@ func TestGenerateMaintenanceWorkflow_RepoConfig(t *testing.T) { } cfg := &RepoConfig{MaintenanceDisabled: true} // The function must succeed (no error), even though a warning is printed. - err := GenerateMaintenanceWorkflow(list, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") + err := GenerateMaintenanceWorkflow(context.Background(), list, tmpDir, "v1.0.0", ActionModeDev, "", false, cfg, "") if err != nil { t.Fatalf("Expected no error when maintenance is disabled with expires, got: %v", err) } @@ -1673,7 +1674,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1727,7 +1728,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1756,7 +1757,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1801,7 +1802,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1830,7 +1831,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1875,7 +1876,7 @@ func TestGenerateSideRepoMaintenanceWorkflow(t *testing.T) { }, } - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") if err != nil { t.Fatalf("Unexpected error: %v", err) } diff --git a/pkg/workflow/maintenance_workflow_yaml.go b/pkg/workflow/maintenance_workflow_yaml.go index e20b2927f8a..e300ace1ea2 100644 --- a/pkg/workflow/maintenance_workflow_yaml.go +++ b/pkg/workflow/maintenance_workflow_yaml.go @@ -1,6 +1,7 @@ package workflow import ( + "context" "strconv" "strings" @@ -13,6 +14,7 @@ var maintenanceWorkflowYAMLLog = logger.New("workflow:maintenance_workflow_yaml" // agentics-maintenance.yml workflow. It is called by GenerateMaintenanceWorkflow // after the cron schedule and setup parameters have been resolved. func buildMaintenanceWorkflowYAML( + ctx context.Context, cronSchedule, scheduleDesc string, minExpiresDays int, runsOnValue string, @@ -124,7 +126,7 @@ jobs: steps: `) - setupActionRef := ResolveSetupActionReference(actionMode, version, actionTag, resolver) + setupActionRef := ResolveSetupActionReference(ctx, actionMode, version, actionTag, resolver) // Add checkout step only in dev/script mode (for local action paths) if actionMode == ActionModeDev || actionMode == ActionModeScript { @@ -254,7 +256,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Run operation uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: @@ -408,7 +410,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Create missing labels uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: @@ -455,7 +457,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Restore activity report logs cache id: activity_report_logs_cache uses: ` + getActionPin("actions/cache/restore") + ` @@ -562,7 +564,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Restore forecast report logs cache id: forecast_report_logs_cache uses: ` + getActionPin("actions/cache/restore") + ` @@ -684,7 +686,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Validate workflows and file issue on findings uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` @@ -829,7 +831,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Compile workflows run: | ` + getCLICmdPrefix(actionMode) + ` compile --validate --validate-images --verbose diff --git a/pkg/workflow/side_repo_maintenance.go b/pkg/workflow/side_repo_maintenance.go index 2929bb9ba3d..16f405bf5d9 100644 --- a/pkg/workflow/side_repo_maintenance.go +++ b/pkg/workflow/side_repo_maintenance.go @@ -1,6 +1,7 @@ package workflow import ( + "context" _ "embed" "fmt" "os" @@ -86,6 +87,7 @@ func effectiveSideRepoToken(checkout SideRepoTarget) string { // generateAllSideRepoMaintenanceWorkflows detects SideRepoOps targets and // generates a per-target maintenance workflow for each unique static repository. func generateAllSideRepoMaintenanceWorkflows( + ctx context.Context, workflowDataList []*WorkflowData, workflowDir string, version string, @@ -109,7 +111,7 @@ func generateAllSideRepoMaintenanceWorkflows( outPath := filepath.Join(workflowDir, filename) maintenanceLog.Printf("Generating side-repo maintenance workflow: %s → %s", target.Repository, filename) - if err := generateSideRepoMaintenanceWorkflow(target, outPath, version, actionMode, actionTag, runsOnValue, resolver, hasExpires, minExpiresDays); err != nil { + if err := generateSideRepoMaintenanceWorkflow(ctx, target, outPath, version, actionMode, actionTag, runsOnValue, resolver, hasExpires, minExpiresDays); err != nil { return fmt.Errorf("failed to generate side-repo maintenance workflow for %s: %w", target.Repository, err) } fmt.Fprintf(os.Stderr, " Generated side-repo maintenance workflow: %s\n", filename) @@ -148,6 +150,7 @@ func generateAllSideRepoMaintenanceWorkflows( // the target repository using the token from the checkout config and sets // GH_AW_TARGET_REPO_SLUG for all cross-repo operations. func generateSideRepoMaintenanceWorkflow( + ctx context.Context, target SideRepoTarget, outPath string, version string, @@ -236,7 +239,7 @@ jobs: ` yaml.WriteString(onSection) - setupActionRef := ResolveSetupActionReference(actionMode, version, actionTag, resolver) + setupActionRef := ResolveSetupActionReference(ctx, actionMode, version, actionTag, resolver) // Add close-expired-entities job only when any workflow uses expires. if hasExpires { @@ -394,7 +397,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Create missing labels in target repository uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: @@ -442,7 +445,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Restore activity report logs cache id: activity_report_logs_cache uses: ` + getActionPin("actions/cache/restore") + ` @@ -554,7 +557,7 @@ jobs: `) - yaml.WriteString(generateInstallCLISteps(actionMode, version, actionTag, resolver)) + yaml.WriteString(generateInstallCLISteps(ctx, actionMode, version, actionTag, resolver)) yaml.WriteString(` - name: Validate workflows and file issue on findings uses: ` + getCachedActionPinFromResolver("actions/github-script", resolver) + ` env: diff --git a/pkg/workflow/side_repo_maintenance_integration_test.go b/pkg/workflow/side_repo_maintenance_integration_test.go index 1172cc9e5f3..de0346aa39d 100644 --- a/pkg/workflow/side_repo_maintenance_integration_test.go +++ b/pkg/workflow/side_repo_maintenance_integration_test.go @@ -3,6 +3,7 @@ package workflow import ( + "context" "os" "path/filepath" "strings" @@ -59,7 +60,7 @@ This workflow operates on a separate repository. workflowDataList, tmpDir := compileSideRepoWorkflow(t, workflowContent) - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") require.NoError(t, err, "generate maintenance workflow") sideRepoFile := filepath.Join(tmpDir, "agentics-maintenance-my-org-target-repo.yml") @@ -160,7 +161,7 @@ Create issues that expire after 14 days. workflowDataList, tmpDir := compileSideRepoWorkflow(t, workflowContent) - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") require.NoError(t, err, "generate maintenance workflow") sideRepoFile := filepath.Join(tmpDir, "agentics-maintenance-corp-infra-tools.yml") @@ -210,7 +211,7 @@ checkout: workflowDataList, tmpDir := compileSideRepoWorkflow(t, workflowContent) - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") require.NoError(t, err, "generate maintenance workflow") sideRepoFile := filepath.Join(tmpDir, "agentics-maintenance-acme-shared-services.yml") @@ -246,7 +247,7 @@ checkout: workflowDataList, tmpDir := compileSideRepoWorkflow(t, workflowContent) - err := GenerateMaintenanceWorkflow(workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") + err := GenerateMaintenanceWorkflow(context.Background(), workflowDataList, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "") require.NoError(t, err, "generate maintenance workflow") // No side-repo file should be created because the repository is an expression. @@ -305,7 +306,7 @@ safe-outputs: } { t.Run(tc.repo, func(t *testing.T) { wdl, tmpDir := compileSideRepoWorkflow(t, makeContent(tc.repo)) - require.NoError(t, GenerateMaintenanceWorkflow(wdl, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "")) + require.NoError(t, GenerateMaintenanceWorkflow(context.Background(), wdl, tmpDir, "v1.0.0", ActionModeDev, "", false, nil, "")) slug := stringutil.SanitizeForFilename(tc.repo) sideFile := filepath.Join(tmpDir, "agentics-maintenance-"+slug+".yml")