diff --git a/docs/src/content/docs/agent-factory-status.mdx b/docs/src/content/docs/agent-factory-status.mdx index 8dc07cd272d..8e75e6c983f 100644 --- a/docs/src/content/docs/agent-factory-status.mdx +++ b/docs/src/content/docs/agent-factory-status.mdx @@ -172,6 +172,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Smoke Crush](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-crush.md) | crush | [![Smoke Crush](https://github.com/github/gh-aw/actions/workflows/smoke-crush.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-crush.lock.yml) | - | - | | [Smoke Gemini](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-gemini.md) | gemini | [![Smoke Gemini](https://github.com/github/gh-aw/actions/workflows/smoke-gemini.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-gemini.lock.yml) | - | - | | [Smoke Multi PR](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-multi-pr.md) | copilot | [![Smoke Multi PR](https://github.com/github/gh-aw/actions/workflows/smoke-multi-pr.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-multi-pr.lock.yml) | - | - | +| [Smoke OpenCode](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-opencode.md) | opencode | [![Smoke OpenCode](https://github.com/github/gh-aw/actions/workflows/smoke-opencode.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-opencode.lock.yml) | - | - | | [Smoke Project](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-project.md) | copilot | [![Smoke Project](https://github.com/github/gh-aw/actions/workflows/smoke-project.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-project.lock.yml) | - | - | | [Smoke Service Ports](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-service-ports.md) | copilot | [![Smoke Service Ports](https://github.com/github/gh-aw/actions/workflows/smoke-service-ports.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-service-ports.lock.yml) | - | - | | [Smoke Temporary ID](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-temporary-id.md) | copilot | [![Smoke Temporary ID](https://github.com/github/gh-aw/actions/workflows/smoke-temporary-id.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-temporary-id.lock.yml) | - | - | diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 31d4d419e1e..a0032ecaf02 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -64,11 +64,10 @@ metadata: # Workflow specifications to import. Supports array form (list of paths) or object # form with 'aw' (agentic workflow paths) subfield. Path resolution: (1) relative -# paths (e.g., 'shared/file.md') are -# resolved relative to the workflow's directory; (2) paths starting with -# '.github/' or '/' are resolved from the repository root (repo-root-relative); -# (3) paths matching 'owner/repo/path@ref' are fetched from GitHub at compile time -# (cross-repo). +# paths (e.g., 'shared/file.md') are resolved relative to the workflow's +# directory; (2) paths starting with '.github/' or '/' are resolved from the +# repository root (repo-root-relative); (3) paths matching 'owner/repo/path@ref' +# are fetched from GitHub at compile time (cross-repo). # (optional) # This field supports multiple formats (oneOf): @@ -1567,6 +1566,12 @@ engine: # (optional) command: "example-value" + # Custom Node.js driver script filename for an agentic engine. This replaces the + # engine's built-in driver wrapper (when the engine supports one) and must end + # with .js, .cjs, or .mjs. + # (optional) + driver: "example-value" + # Custom environment variables to pass to the AI engine, including secret # overrides (e.g., OPENAI_API_KEY: ${{ secrets.CUSTOM_KEY }}) # (optional) @@ -5549,6 +5554,12 @@ safe-outputs: # (optional) concurrency-group: "example-value" + # Explicit additional custom workflow jobs that the consolidated safe_outputs job + # should depend on. + # (optional) + needs: [] + # Array of strings + # Override the GitHub deployment environment for the safe-outputs job. When set, # this environment is used instead of the top-level environment: field. When not # set, the top-level environment: field is propagated automatically so that diff --git a/pkg/actionpins/actionpins.go b/pkg/actionpins/actionpins.go index 2a560b27421..bd23658c18a 100644 --- a/pkg/actionpins/actionpins.go +++ b/pkg/actionpins/actionpins.go @@ -92,11 +92,6 @@ var ( actionPinsOnce sync.Once ) -// GetActionPins returns all loaded action pins sorted by version descending. -func GetActionPins() []ActionPin { - return getActionPins() -} - func getActionPins() []ActionPin { actionPinsOnce.Do(func() { log.Print("Unmarshaling action pins from embedded JSON (first call, will be cached)") diff --git a/pkg/actionpins/spec_test.go b/pkg/actionpins/spec_test.go index 2a38d8c7dd5..97814561b16 100644 --- a/pkg/actionpins/spec_test.go +++ b/pkg/actionpins/spec_test.go @@ -129,12 +129,6 @@ func TestSpec_PublicAPI_ExtractVersion(t *testing.T) { } } -// TestSpec_PublicAPI_GetActionPins validates that GetActionPins returns a non-nil slice. -func TestSpec_PublicAPI_GetActionPins(t *testing.T) { - pins := actionpins.GetActionPins() - assert.NotNil(t, pins, "GetActionPins should return non-nil slice of all loaded pins") -} - // TestSpec_PublicAPI_GetActionPinsByRepo validates GetActionPinsByRepo for known and unknown repos. func TestSpec_PublicAPI_GetActionPinsByRepo(t *testing.T) { t.Run("returns no pins for unknown repository", func(t *testing.T) { @@ -144,11 +138,7 @@ func TestSpec_PublicAPI_GetActionPinsByRepo(t *testing.T) { }) t.Run("returns pins for a known repository when embedded data is loaded", func(t *testing.T) { - all := actionpins.GetActionPins() - if len(all) == 0 { - t.Skip("no embedded pin data available") - } - known := all[0].Repo + known := "actions/checkout" pins := actionpins.GetActionPinsByRepo(known) assert.NotEmpty(t, pins, "should return pins for a known repo from embedded data") }) @@ -162,11 +152,7 @@ func TestSpec_PublicAPI_GetActionPinByRepo(t *testing.T) { }) t.Run("returns a pin for a known repository", func(t *testing.T) { - all := actionpins.GetActionPins() - if len(all) == 0 { - t.Skip("no embedded pin data available") - } - known := all[0].Repo + known := "actions/checkout" pin, ok := actionpins.GetActionPinByRepo(known) assert.True(t, ok, "should return true for a known repo") assert.Equal(t, known, pin.Repo, "returned pin should belong to the queried repo") @@ -189,12 +175,7 @@ func TestSpec_PublicAPI_ResolveActionPin(t *testing.T) { // TestSpec_PublicAPI_ResolveLatestActionPin validates latest-version resolution behavior. func TestSpec_PublicAPI_ResolveLatestActionPin(t *testing.T) { t.Run("returns latest pinned reference for known repository", func(t *testing.T) { - all := actionpins.GetActionPins() - if len(all) == 0 { - t.Skip("no embedded pin data available") - } - - known := all[0].Repo + known := "actions/checkout" latestPin, ok := actionpins.GetActionPinByRepo(known) require.True(t, ok, "expected latest pin for known repository") diff --git a/pkg/cli/fetch.go b/pkg/cli/fetch.go index 8b9438d3575..9a0f468c5c4 100644 --- a/pkg/cli/fetch.go +++ b/pkg/cli/fetch.go @@ -36,16 +36,6 @@ type FetchedWorkflow struct { SourcePath string // The original source path (local path or remote path) } -// FetchWorkflowFromSource fetches a workflow file directly from GitHub without cloning. -// This is the preferred way to add remote workflows as it only fetches the specific -// files needed rather than cloning the entire repository. -// -// For local workflows (local filesystem paths), it reads from the local filesystem. -// For remote workflows, it uses the GitHub API to fetch the file content. -func FetchWorkflowFromSource(spec *WorkflowSpec, verbose bool) (*FetchedWorkflow, error) { - return FetchWorkflowFromSourceWithContext(context.Background(), spec, verbose) -} - // FetchWorkflowFromSourceWithContext fetches a workflow file from local disk or GitHub. // The context is used to cancel remote ref resolution retries (for example, on Ctrl-C). func FetchWorkflowFromSourceWithContext(ctx context.Context, spec *WorkflowSpec, verbose bool) (*FetchedWorkflow, error) { diff --git a/pkg/cli/remote_workflow_test.go b/pkg/cli/remote_workflow_test.go index 92544bc5614..9a6dd29e3d3 100644 --- a/pkg/cli/remote_workflow_test.go +++ b/pkg/cli/remote_workflow_test.go @@ -107,44 +107,6 @@ func TestFetchLocalWorkflow_DirectoryInsteadOfFile(t *testing.T) { assert.Nil(t, result, "result should be nil on error") } -func TestFetchWorkflowFromSource_LocalRouting(t *testing.T) { - // Create a temporary local workflow file - tempDir := t.TempDir() - tempFile := filepath.Join(tempDir, "local-workflow.md") - content := "# Local Workflow\n\nTest content." - err := os.WriteFile(tempFile, []byte(content), 0644) - require.NoError(t, err, "should create temp file") - - spec := &WorkflowSpec{ - WorkflowPath: tempFile, - WorkflowName: "local-workflow", - } - - result, err := FetchWorkflowFromSource(spec, false) - - require.NoError(t, err, "should not error for local workflow") - assert.True(t, result.IsLocal, "should route to local fetch") - assert.Equal(t, []byte(content), result.Content, "content should match") -} - -func TestFetchWorkflowFromSource_RemoteRoutingWithInvalidSlug(t *testing.T) { - // Test with a remote workflow spec that has an invalid slug - spec := &WorkflowSpec{ - RepoSpec: RepoSpec{ - RepoSlug: "invalid-slug-no-slash", - Version: "main", - }, - WorkflowPath: "workflow.md", - WorkflowName: "workflow", - } - - result, err := FetchWorkflowFromSource(spec, false) - - require.Error(t, err, "should error for invalid repo slug") - assert.Nil(t, result, "result should be nil on error") - assert.Contains(t, err.Error(), "invalid repository slug", "error should mention invalid slug") -} - func TestResolveCommitSHAWithRetries_TransientFailureThenSuccess(t *testing.T) { originalResolve := resolveRefToSHAForHost originalWait := waitBeforeSHAResolutionRetry diff --git a/pkg/workflow/action_pins.go b/pkg/workflow/action_pins.go index 3eb2f9a1a6d..be8e14c5b89 100644 --- a/pkg/workflow/action_pins.go +++ b/pkg/workflow/action_pins.go @@ -57,12 +57,6 @@ func getActionPin(repo string) string { return actionpins.FormatReference(repo, pins[0].SHA, pins[0].Version) } -// getActionPins returns all loaded action pins (sorted by version descending). -// Package-private wrapper used by tests in this package. -func getActionPins() []ActionPin { - return actionpins.GetActionPins() -} - // getCachedActionPinFromResolver returns the pinned action reference for repo, // preferring dynamic resolution via resolver over the embedded pins. // For use within pkg/workflow when only a resolver is available (no WorkflowData). diff --git a/pkg/workflow/action_pins_test.go b/pkg/workflow/action_pins_test.go index 4d791589876..6e5aa16c6a2 100644 --- a/pkg/workflow/action_pins_test.go +++ b/pkg/workflow/action_pins_test.go @@ -4,7 +4,6 @@ package workflow import ( "bytes" - "encoding/json" "os" "regexp" "strings" @@ -28,75 +27,6 @@ func expectedPinnedUses(t *testing.T, repo, version string) string { return result } -// TestActionPinsExist verifies that all action pinning entries exist -func TestActionPinsExist(t *testing.T) { - // Read action pins from JSON file instead of hardcoded list - actionPins := getActionPins() - - // Verify we have at least some pins loaded - if len(actionPins) == 0 { - t.Fatal("No action pins loaded from JSON file") - } - - // Verify each pin has required fields - for _, pin := range actionPins { - // Verify the pin has a repo - if pin.Repo == "" { - t.Errorf("Action pin has empty Repo field") - continue - } - - // Verify the pin has a valid SHA (40 character hex string) - if !isValidSHA(pin.SHA) { - t.Errorf("Invalid SHA for %s: %s (expected 40-character hex string)", pin.Repo, pin.SHA) - } - - // Verify the pin has a version - if pin.Version == "" { - t.Errorf("Missing version for %s", pin.Repo) - } - } -} - -// TestGetActionPinReturnsValidSHA tests that getActionPin returns valid SHA references -func TestGetActionPinReturnsValidSHA(t *testing.T) { - // Generate test cases dynamically from action pins JSON - actionPins := getActionPins() - - if len(actionPins) == 0 { - t.Fatal("No action pins loaded from JSON file") - } - - for _, pin := range actionPins { - t.Run(pin.Repo, func(t *testing.T) { - result := getActionPin(pin.Repo) - - // Check that the result contains a SHA (40-char hex after @ and before #) - // Format is: repo@sha # version - parts := strings.Split(result, "@") - if len(parts) != 2 { - t.Errorf("getActionPin(%s) = %s, expected format repo@sha # version", pin.Repo, result) - return - } - - // Extract SHA (before the comment marker " # ") - shaAndComment := parts[1] - before, _, ok := strings.Cut(shaAndComment, " # ") - if !ok { - t.Errorf("getActionPin(%s) = %s, expected comment with version tag", pin.Repo, result) - return - } - - sha := before - - // All action pins should have valid SHAs - if !isValidSHA(sha) { - t.Errorf("getActionPin(%s) = %s, expected SHA to be 40-char hex", pin.Repo, result) - } - }) - } -} - // TestGetActionPinFallback tests that getActionPin returns empty string for unknown actions func TestGetActionPinFallback(t *testing.T) { result := getActionPin("unknown/action") @@ -313,53 +243,6 @@ func TestApplyActionPinToStep(t *testing.T) { } } -// TestGetActionPinsSorting tests that getActionPins returns sorted action pins -func TestGetActionPinsSorting(t *testing.T) { - pins := getActionPins() - - // Dynamically derive the expected count from the JSON file to avoid - // hardcoding a number that breaks when new pins are added or when - // the Go test cache contains a stale binary. - var jsonData ActionPinsData - rawJSON, err := os.ReadFile("../actionpins/data/action_pins.json") - if err != nil { - t.Fatalf("Failed to read action_pins.json: %v", err) - } - if err := json.Unmarshal(rawJSON, &jsonData); err != nil { - t.Fatalf("Failed to parse action_pins.json: %v", err) - } - expectedCount := len(jsonData.Entries) - - // Verify we got all the pins from the JSON (catches parsing bugs) - if len(pins) != expectedCount { - t.Errorf("getActionPins() returned %d pins, expected %d (from action_pins.json)", len(pins), expectedCount) - } - - // Verify they are sorted by version (descending) then by repository name (ascending) - for i := range len(pins) - 1 { - if pins[i].Version < pins[i+1].Version { - t.Errorf("Pins not sorted correctly by version: %s (v%s) should come before %s (v%s)", - pins[i].Repo, pins[i].Version, pins[i+1].Repo, pins[i+1].Version) - } else if pins[i].Version == pins[i+1].Version && pins[i].Repo > pins[i+1].Repo { - t.Errorf("Pins not sorted correctly by repo name within same version: %s should come before %s", - pins[i].Repo, pins[i+1].Repo) - } - } - - // Verify all pins have the required fields - for _, pin := range pins { - if pin.Repo == "" { - t.Error("Found pin with empty Repo field") - } - if pin.Version == "" { - t.Errorf("Pin %s has empty Version field", pin.Repo) - } - if !isValidSHA(pin.SHA) { - t.Errorf("Pin %s has invalid SHA: %s", pin.Repo, pin.SHA) - } - } -} - // TestGetActionPinByRepo tests the getActionPinByRepo function func TestGetActionPinByRepo(t *testing.T) { tests := []struct { @@ -886,42 +769,6 @@ func TestApplyActionPinsToTypedSteps(t *testing.T) { } } -// TestActionPinsCaching verifies that action pins are cached and not re-parsed -func TestActionPinsCaching(t *testing.T) { - // Reset the cache by creating a new sync.Once - // Note: In production, this is handled automatically by sync.Once - - // First call - should load and cache - pins1 := getActionPins() - if len(pins1) == 0 { - t.Fatal("No action pins loaded on first call") - } - - // Second call - should return cached data (same slice reference) - pins2 := getActionPins() - if len(pins2) == 0 { - t.Fatal("No action pins loaded on second call") - } - - // Verify both calls return the same data - if len(pins1) != len(pins2) { - t.Errorf("Cache returned different number of pins: first=%d, second=%d", len(pins1), len(pins2)) - } - - // Verify the data is identical by checking a few pins - for i := 0; i < len(pins1) && i < 3; i++ { - if pins1[i].Repo != pins2[i].Repo { - t.Errorf("Pin %d repo mismatch: first=%s, second=%s", i, pins1[i].Repo, pins2[i].Repo) - } - if pins1[i].Version != pins2[i].Version { - t.Errorf("Pin %d version mismatch: first=%s, second=%s", i, pins1[i].Version, pins2[i].Version) - } - if pins1[i].SHA != pins2[i].SHA { - t.Errorf("Pin %d SHA mismatch: first=%s, second=%s", i, pins1[i].SHA, pins2[i].SHA) - } - } -} - // TestGetActionPinWithData_V7ExactMatch verifies that v7 resolves to its exact SHA func TestGetActionPinWithData_V7ExactMatch(t *testing.T) { data := &WorkflowData{ diff --git a/pkg/workflow/compiler_safe_outputs_steps.go b/pkg/workflow/compiler_safe_outputs_steps.go index b271036c7d0..f219bf0d65b 100644 --- a/pkg/workflow/compiler_safe_outputs_steps.go +++ b/pkg/workflow/compiler_safe_outputs_steps.go @@ -8,69 +8,6 @@ import ( var consolidatedSafeOutputsStepsLog = logger.New("workflow:compiler_safe_outputs_steps") -// buildConsolidatedSafeOutputStep builds a single step for a safe output operation -// within the consolidated safe-outputs job. This function handles both inline script -// mode and file mode (requiring from local filesystem). -func (c *Compiler) buildConsolidatedSafeOutputStep(data *WorkflowData, config SafeOutputStepConfig) []string { - var steps []string - - // Build step condition if provided - var conditionStr string - if config.Condition != nil { - conditionStr = RenderCondition(config.Condition) - } - - // Step name and metadata - steps = append(steps, fmt.Sprintf(" - name: %s\n", config.StepName)) - steps = append(steps, fmt.Sprintf(" id: %s\n", config.StepID)) - if conditionStr != "" { - steps = append(steps, fmt.Sprintf(" if: %s\n", conditionStr)) - } - if config.ContinueOnError { - steps = append(steps, " continue-on-error: true\n") - } - steps = append(steps, fmt.Sprintf(" uses: %s\n", getCachedActionPin("actions/github-script", data))) - - // Environment variables section - steps = append(steps, " env:\n") - steps = append(steps, " GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}\n") - steps = append(steps, config.CustomEnvVars...) - - // Add custom safe output env vars - c.addCustomSafeOutputEnvVars(&steps, data) - - // With section for github-token - steps = append(steps, " with:\n") - if config.UseCopilotCodingAgentToken { - c.addSafeOutputAgentGitHubTokenForConfig(&steps, data, config.Token) - } else if config.UseCopilotRequestsToken { - c.addSafeOutputCopilotGitHubTokenForConfig(&steps, data, config.Token) - } else { - c.addSafeOutputGitHubTokenForConfig(&steps, data, config.Token) - } - - steps = append(steps, " script: |\n") - - // Add the formatted JavaScript script - // Use require mode if ScriptName is set, otherwise inline the bundled script - if config.ScriptName != "" { - // Require mode: Use setup_globals helper - steps = append(steps, " const { setupGlobals } = require('"+SetupActionDestination+"/setup_globals.cjs');\n") - steps = append(steps, " setupGlobals(core, github, context, exec, io, getOctokit);\n") - steps = append(steps, fmt.Sprintf(" const { main } = require('"+SetupActionDestination+"/%s.cjs');\n", config.ScriptName)) - steps = append(steps, " await main();\n") - } else { - // Inline JavaScript: Use setup_globals helper - steps = append(steps, " const { setupGlobals } = require('"+SetupActionDestination+"/setup_globals.cjs');\n") - steps = append(steps, " setupGlobals(core, github, context, exec, io, getOctokit);\n") - // Inline mode: embed the bundled script directly - formattedScript := FormatJavaScriptForYAML(config.Script) - steps = append(steps, formattedScript...) - } - - return steps -} - // buildSharedPRCheckoutSteps builds checkout and git configuration steps that are shared // between create-pull-request and push-to-pull-request-branch operations. // These steps are added once with a combined condition to avoid duplication. diff --git a/pkg/workflow/compiler_safe_outputs_steps_test.go b/pkg/workflow/compiler_safe_outputs_steps_test.go index 946964bc819..9b280aecbc5 100644 --- a/pkg/workflow/compiler_safe_outputs_steps_test.go +++ b/pkg/workflow/compiler_safe_outputs_steps_test.go @@ -10,134 +10,6 @@ import ( "github.com/stretchr/testify/require" ) -// TestBuildConsolidatedSafeOutputStep tests individual step building -func TestBuildConsolidatedSafeOutputStep(t *testing.T) { - tests := []struct { - name string - config SafeOutputStepConfig - checkContains []string - checkNotContains []string - }{ - { - name: "basic step with inline script", - config: SafeOutputStepConfig{ - StepName: "Test Step", - StepID: "test_step", - Script: "console.log('test');", - Token: "${{ github.token }}", - }, - checkContains: []string{ - "name: Test Step", - "id: test_step", - "uses: actions/github-script@", - "GH_AW_AGENT_OUTPUT", - "github-token:", - "setupGlobals", - }, - }, - { - name: "step with script name (file mode)", - config: SafeOutputStepConfig{ - StepName: "Create Issue", - StepID: "create_issue", - ScriptName: "create_issue_handler", - Token: "${{ github.token }}", - }, - checkContains: []string{ - "name: Create Issue", - "id: create_issue", - "setupGlobals", - "require('${{ runner.temp }}/gh-aw/actions/create_issue_handler.cjs')", - "await main();", - }, - checkNotContains: []string{ - "console.log", // Should not inline script - }, - }, - { - name: "step with condition", - config: SafeOutputStepConfig{ - StepName: "Conditional Step", - StepID: "conditional", - Script: "console.log('test');", - Token: "${{ github.token }}", - Condition: BuildEquals(BuildStringLiteral("test"), BuildStringLiteral("test")), - }, - checkContains: []string{ - "if: 'test' == 'test'", - }, - }, - { - name: "step with custom env vars", - config: SafeOutputStepConfig{ - StepName: "Step with Env", - StepID: "env_step", - Script: "console.log('test');", - Token: "${{ github.token }}", - CustomEnvVars: []string{ - " CUSTOM_VAR: \"value\"\n", - " ANOTHER_VAR: \"value2\"\n", - }, - }, - checkContains: []string{ - "CUSTOM_VAR: \"value\"", - "ANOTHER_VAR: \"value2\"", - }, - }, - { - name: "step with copilot token", - config: SafeOutputStepConfig{ - StepName: "Copilot Step", - StepID: "copilot", - Script: "console.log('test');", - Token: "${{ secrets.COPILOT_GITHUB_TOKEN }}", - UseCopilotRequestsToken: true, - }, - checkContains: []string{ - "github-token:", - }, - }, - { - name: "step with agent token", - config: SafeOutputStepConfig{ - StepName: "Agent Step", - StepID: "agent", - Script: "console.log('test');", - Token: "${{ secrets.AGENT_TOKEN }}", - UseCopilotCodingAgentToken: true, - }, - checkContains: []string{ - "github-token:", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - compiler := NewCompiler() - - workflowData := &WorkflowData{ - Name: "Test Workflow", - SafeOutputs: &SafeOutputsConfig{}, - } - - steps := compiler.buildConsolidatedSafeOutputStep(workflowData, tt.config) - - require.NotEmpty(t, steps) - - stepsContent := strings.Join(steps, "") - - for _, expected := range tt.checkContains { - assert.Contains(t, stepsContent, expected, "Expected to find: "+expected) - } - - for _, notExpected := range tt.checkNotContains { - assert.NotContains(t, stepsContent, notExpected, "Should not contain: "+notExpected) - } - }) - } -} - // TestBuildSharedPRCheckoutSteps tests shared PR checkout step generation func TestBuildSharedPRCheckoutSteps(t *testing.T) { tests := []struct { @@ -625,134 +497,3 @@ func TestStepOrderInConsolidatedJob(t *testing.T) { assert.Less(t, gitConfigPos, handlerPos, "Git config should come before handler") } } - -// TestStepWithoutCondition tests step building without condition -func TestStepWithoutCondition(t *testing.T) { - compiler := NewCompiler() - - config := SafeOutputStepConfig{ - StepName: "Test Step", - StepID: "test", - Script: "console.log('test');", - Token: "${{ github.token }}", - } - - workflowData := &WorkflowData{ - Name: "Test Workflow", - SafeOutputs: &SafeOutputsConfig{}, - } - - steps := compiler.buildConsolidatedSafeOutputStep(workflowData, config) - - stepsContent := strings.Join(steps, "") - - // Should not have an 'if' line - assert.NotContains(t, stepsContent, "if:") -} - -// TestGitHubTokenPrecedence tests GitHub token selection logic -func TestGitHubTokenPrecedence(t *testing.T) { - tests := []struct { - name string - useCopilotCodingAgentToken bool - useCopilotRequestsToken bool - token string - expectedInContent string - }{ - { - name: "standard token", - useCopilotCodingAgentToken: false, - useCopilotRequestsToken: false, - token: "${{ github.token }}", - expectedInContent: "github-token:", - }, - { - name: "copilot token", - useCopilotCodingAgentToken: false, - useCopilotRequestsToken: true, - token: "${{ secrets.COPILOT_GITHUB_TOKEN }}", - expectedInContent: "github-token:", - }, - { - name: "agent token", - useCopilotCodingAgentToken: true, - useCopilotRequestsToken: false, - token: "${{ secrets.AGENT_TOKEN }}", - expectedInContent: "github-token:", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - compiler := NewCompiler() - - config := SafeOutputStepConfig{ - StepName: "Test Step", - StepID: "test", - Script: "console.log('test');", - Token: tt.token, - UseCopilotRequestsToken: tt.useCopilotRequestsToken, - UseCopilotCodingAgentToken: tt.useCopilotCodingAgentToken, - } - - workflowData := &WorkflowData{ - Name: "Test Workflow", - SafeOutputs: &SafeOutputsConfig{}, - } - - steps := compiler.buildConsolidatedSafeOutputStep(workflowData, config) - - stepsContent := strings.Join(steps, "") - - assert.Contains(t, stepsContent, tt.expectedInContent) - }) - } -} - -// TestScriptNameVsInlineScript tests the two modes of script inclusion -func TestScriptNameVsInlineScript(t *testing.T) { - compiler := NewCompiler() - - workflowData := &WorkflowData{ - Name: "Test Workflow", - SafeOutputs: &SafeOutputsConfig{}, - } - - // Test inline script mode - t.Run("inline script", func(t *testing.T) { - config := SafeOutputStepConfig{ - StepName: "Inline Test", - StepID: "inline", - Script: "console.log('inline script');", - Token: "${{ github.token }}", - } - - steps := compiler.buildConsolidatedSafeOutputStep(workflowData, config) - stepsContent := strings.Join(steps, "") - - assert.Contains(t, stepsContent, "setupGlobals") - assert.Contains(t, stepsContent, "console.log") - // Inline scripts now include setupGlobals require statement - assert.Contains(t, stepsContent, "require") - // Inline scripts should not call await main() - assert.NotContains(t, stepsContent, "await main()") - }) - - // Test file mode - t.Run("file mode", func(t *testing.T) { - config := SafeOutputStepConfig{ - StepName: "File Test", - StepID: "file", - ScriptName: "test_handler", - Token: "${{ github.token }}", - } - - steps := compiler.buildConsolidatedSafeOutputStep(workflowData, config) - stepsContent := strings.Join(steps, "") - - assert.Contains(t, stepsContent, "setupGlobals") - assert.Contains(t, stepsContent, "require('${{ runner.temp }}/gh-aw/actions/test_handler.cjs')") - assert.Contains(t, stepsContent, "await main()") - assert.NotContains(t, stepsContent, "console.log") - }) -}