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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/release.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions pkg/workflow/action_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package workflow

// ActionMode defines how JavaScript is embedded in workflow steps
type ActionMode string

const (
// ActionModeInline embeds JavaScript inline using actions/github-script (current behavior)
ActionModeInline ActionMode = "inline"

// ActionModeDev references custom actions using local paths (development mode)
ActionModeDev ActionMode = "dev"
)

// String returns the string representation of the action mode
func (m ActionMode) String() string {
return string(m)
}

// IsValid checks if the action mode is valid
func (m ActionMode) IsValid() bool {
return m == ActionModeInline || m == ActionModeDev
}
286 changes: 286 additions & 0 deletions pkg/workflow/compiler_custom_actions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package workflow

import (
"os"
"strings"
"testing"
)

// TestActionModeValidation tests the ActionMode type validation
func TestActionModeValidation(t *testing.T) {
tests := []struct {
mode ActionMode
valid bool
}{
{ActionModeInline, true},
{ActionModeDev, true},
{ActionMode("invalid"), false},
{ActionMode(""), false},
}

for _, tt := range tests {
t.Run(string(tt.mode), func(t *testing.T) {
if got := tt.mode.IsValid(); got != tt.valid {
t.Errorf("ActionMode(%q).IsValid() = %v, want %v", tt.mode, got, tt.valid)
}
})
}
}

// TestActionModeString tests the String() method
func TestActionModeString(t *testing.T) {
tests := []struct {
mode ActionMode
want string
}{
{ActionModeInline, "inline"},
{ActionModeDev, "dev"},
}

for _, tt := range tests {
t.Run(string(tt.mode), func(t *testing.T) {
if got := tt.mode.String(); got != tt.want {
t.Errorf("ActionMode.String() = %q, want %q", got, tt.want)
}
})
}
}

// TestCompilerActionModeDefault tests that the compiler defaults to inline mode
func TestCompilerActionModeDefault(t *testing.T) {
compiler := NewCompiler(false, "", "1.0.0")
if compiler.GetActionMode() != ActionModeInline {
t.Errorf("Default action mode should be inline, got %s", compiler.GetActionMode())
}
}

// TestCompilerSetActionMode tests setting the action mode
func TestCompilerSetActionMode(t *testing.T) {
compiler := NewCompiler(false, "", "1.0.0")

compiler.SetActionMode(ActionModeDev)
if compiler.GetActionMode() != ActionModeDev {
t.Errorf("Expected action mode dev, got %s", compiler.GetActionMode())
}

compiler.SetActionMode(ActionModeInline)
if compiler.GetActionMode() != ActionModeInline {
t.Errorf("Expected action mode inline, got %s", compiler.GetActionMode())
}
}

// TestScriptRegistryWithAction tests registering scripts with action paths
func TestScriptRegistryWithAction(t *testing.T) {
registry := NewScriptRegistry()

testScript := `console.log('test');`
actionPath := "./actions/test-action"

registry.RegisterWithAction("test_script", testScript, RuntimeModeGitHubScript, actionPath)

if !registry.Has("test_script") {
t.Error("Script should be registered")
}

if got := registry.GetActionPath("test_script"); got != actionPath {
t.Errorf("Expected action path %q, got %q", actionPath, got)
}

if got := registry.GetSource("test_script"); got != testScript {
t.Errorf("Expected source %q, got %q", testScript, got)
}
}

// TestScriptRegistryActionPathEmpty tests that scripts without action paths return empty string
func TestScriptRegistryActionPathEmpty(t *testing.T) {
registry := NewScriptRegistry()

testScript := `console.log('test');`
registry.Register("test_script", testScript)

if got := registry.GetActionPath("test_script"); got != "" {
t.Errorf("Expected empty action path, got %q", got)
}
}

// TestCustomActionModeCompilation tests workflow compilation with custom action mode
func TestCustomActionModeCompilation(t *testing.T) {
// Create a temporary directory for the test
tempDir := t.TempDir()

// Create a test workflow file
workflowContent := `---
name: Test Custom Actions
on: issues
safe-outputs:
create-issue:
max: 1
---

Test workflow with safe-outputs.
`

workflowPath := tempDir + "/test-workflow.md"
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write test workflow: %v", err)
}

// Register a test script with an action path
testScript := `
const { core } = require('@actions/core');
core.info('Creating issue');
`
DefaultScriptRegistry.RegisterWithAction(
"create_issue",
testScript,
RuntimeModeGitHubScript,
"./actions/create-issue",
)

// Compile with dev action mode
compiler := NewCompiler(false, "", "1.0.0")
compiler.SetActionMode(ActionModeDev)
compiler.SetNoEmit(false)

if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Compilation failed: %v", err)
}

// Read the generated lock file
lockPath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml"
lockContent, err := os.ReadFile(lockPath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}

lockStr := string(lockContent)

// Verify it uses custom action reference instead of actions/github-script
if !strings.Contains(lockStr, "uses: ./actions/create-issue") {
t.Error("Expected custom action reference './actions/create-issue' not found in lock file")
}

// Verify it does NOT contain actions/github-script
if strings.Contains(lockStr, "actions/github-script@") {
t.Error("Lock file should not contain 'actions/github-script@' when using dev action mode")
}

// Verify it has the token input instead of github-token with script
if strings.Contains(lockStr, "github-token:") {
t.Error("Dev action mode should use 'token:' input, not 'github-token:'")
}

if !strings.Contains(lockStr, "token:") {
t.Error("Expected 'token:' input not found for custom action")
}

// Clean up: reset the registry to avoid affecting other tests
DefaultScriptRegistry.RegisterWithMode("create_issue", testScript, RuntimeModeGitHubScript)
}

// TestInlineActionModeCompilation tests workflow compilation with inline mode (default)
func TestInlineActionModeCompilation(t *testing.T) {
// Create a temporary directory for the test
tempDir := t.TempDir()

// Create a test workflow file
workflowContent := `---
name: Test Inline Actions
on: issues
safe-outputs:
create-issue:
max: 1
---

Test workflow with inline mode.
`

workflowPath := tempDir + "/test-workflow.md"
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write test workflow: %v", err)
}

// Compile with inline mode (default)
compiler := NewCompiler(false, "", "1.0.0")
compiler.SetActionMode(ActionModeInline)
compiler.SetNoEmit(false)

if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Compilation failed: %v", err)
}

// Read the generated lock file
lockPath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml"
lockContent, err := os.ReadFile(lockPath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}

lockStr := string(lockContent)

// Verify it uses actions/github-script
if !strings.Contains(lockStr, "actions/github-script@") {
t.Error("Expected 'actions/github-script@' not found in lock file for inline mode")
}

// Verify it has github-token parameter
if !strings.Contains(lockStr, "github-token:") {
t.Error("Expected 'github-token:' parameter not found for inline mode")
}

// Verify it has script: parameter
if !strings.Contains(lockStr, "script: |") {
t.Error("Expected 'script: |' parameter not found for inline mode")
}
}

// TestCustomActionModeFallback tests that compilation falls back to inline mode
// when action path is not registered
func TestCustomActionModeFallback(t *testing.T) {
// Create a temporary directory for the test
tempDir := t.TempDir()

// Create a test workflow file
workflowContent := `---
name: Test Fallback
on: issues
safe-outputs:
create-issue:
max: 1
---

Test fallback to inline mode.
`

workflowPath := tempDir + "/test-workflow.md"
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write test workflow: %v", err)
}

// Ensure create_issue is registered without an action path
testScript := `console.log('test');`
DefaultScriptRegistry.RegisterWithMode("create_issue", testScript, RuntimeModeGitHubScript)

// Compile with dev action mode
compiler := NewCompiler(false, "", "1.0.0")
compiler.SetActionMode(ActionModeDev)
compiler.SetNoEmit(false)

if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Compilation failed: %v", err)
}

// Read the generated lock file
lockPath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml"
lockContent, err := os.ReadFile(lockPath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}

lockStr := string(lockContent)

// Verify it falls back to actions/github-script when action path is not found
if !strings.Contains(lockStr, "actions/github-script@") {
t.Error("Expected fallback to 'actions/github-script@' when action path not found")
}
}
13 changes: 13 additions & 0 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Compiler struct {
trialMode bool // If true, suppress safe outputs for trial mode execution
trialLogicalRepoSlug string // If set in trial mode, the logical repository to checkout
refreshStopTime bool // If true, regenerate stop-after times instead of preserving existing ones
actionMode ActionMode // Mode for generating JavaScript steps (inline vs custom actions)
jobManager *JobManager // Manages jobs and dependencies
engineRegistry *EngineRegistry // Registry of available agentic engines
fileTracker FileTracker // Optional file tracker for tracking created files
Expand All @@ -43,6 +44,7 @@ func NewCompiler(verbose bool, engineOverride string, version string) *Compiler
engineOverride: engineOverride,
version: version,
skipValidation: true, // Skip validation by default for now since existing workflows don't fully comply
actionMode: ActionModeInline, // Default to inline mode for backwards compatibility
jobManager: NewJobManager(),
engineRegistry: GetGlobalEngineRegistry(),
stepOrderTracker: NewStepOrderTracker(),
Expand All @@ -59,6 +61,7 @@ func NewCompilerWithCustomOutput(verbose bool, engineOverride string, customOutp
customOutput: customOutput,
version: version,
skipValidation: true, // Skip validation by default for now since existing workflows don't fully comply
actionMode: ActionModeInline, // Default to inline mode for backwards compatibility
jobManager: NewJobManager(),
engineRegistry: GetGlobalEngineRegistry(),
stepOrderTracker: NewStepOrderTracker(),
Expand Down Expand Up @@ -102,6 +105,16 @@ func (c *Compiler) SetRefreshStopTime(refresh bool) {
c.refreshStopTime = refresh
}

// SetActionMode configures the action mode for JavaScript step generation
func (c *Compiler) SetActionMode(mode ActionMode) {
c.actionMode = mode
}

// GetActionMode returns the current action mode
func (c *Compiler) GetActionMode() ActionMode {
return c.actionMode
}

// IncrementWarningCount increments the warning counter
func (c *Compiler) IncrementWarningCount() {
c.warningCount++
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/create_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str
MainJobName: mainJobName,
CustomEnvVars: customEnvVars,
Script: getCreateIssueScript(),
ScriptName: "create_issue", // For custom action mode
Permissions: NewPermissionsContentsReadIssuesWrite(),
Outputs: outputs,
PostSteps: postSteps,
Expand Down
Loading