Skip to content
Closed
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
92 changes: 92 additions & 0 deletions pkg/workflow/agentic_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,95 @@ func TestAgenticWorkflowsExtractToolsEdgeCases(t *testing.T) {
})
}
}

// TestGhAwWorkspaceBinaryStepGenerated verifies that in production (release) mode,
// a step is generated to extract the gh-aw binary from the published container and
// place it at ./gh-aw in the workspace before custom steps run.
func TestGhAwWorkspaceBinaryStepGenerated(t *testing.T) {
tests := []struct {
name string
actionMode ActionMode
customSteps string
importedFiles []string
expectInstallStep bool
description string
}{
{
name: "release mode with custom steps generates install step",
actionMode: ActionModeRelease,
customSteps: "- name: Use binary\n run: ./gh-aw logs\n",
expectInstallStep: true,
description: "Should generate Install gh-aw CLI step before custom steps in production mode",
},
{
name: "dev mode skips install step",
actionMode: ActionModeDev,
customSteps: "- name: Use binary\n run: ./gh-aw logs\n",
expectInstallStep: false,
description: "Should not generate Install gh-aw CLI step in dev mode (builds from source)",
},
{
name: "release mode without custom steps skips install step",
actionMode: ActionModeRelease,
customSteps: "",
expectInstallStep: false,
description: "Should not generate Install gh-aw CLI step when there are no custom steps",
},
{
name: "release mode with gh-aw import skips install step",
actionMode: ActionModeRelease,
customSteps: "- name: Use binary\n run: ./gh-aw logs\n",
importedFiles: []string{"shared/mcp/gh-aw.md"},
expectInstallStep: false,
description: "Should not generate Install gh-aw CLI step when shared/mcp/gh-aw.md provides it",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := testCompiler()
c.SetActionMode(tt.actionMode)

workflowData := &WorkflowData{
Name: "test-workflow",
CustomSteps: tt.customSteps,
ImportedFiles: tt.importedFiles,
Tools: map[string]any{
"agentic-workflows": nil,
},
}

var yaml strings.Builder
// Call generateGhAwWorkspaceBinaryStep conditionally as generateMainJobSteps would
if !c.actionMode.IsDev() && workflowData.CustomSteps != "" {
if _, hasAgenticWorkflows := workflowData.Tools["agentic-workflows"]; hasAgenticWorkflows {
if !workflowHasGhAwImport(workflowData) {
c.generateGhAwWorkspaceBinaryStep(&yaml)
}
}
}

result := yaml.String()

if tt.expectInstallStep {
assert.Contains(t, result, "Install gh-aw CLI",
"%s: expected Install gh-aw CLI step to be present", tt.description)
assert.Contains(t, result, "docker pull ghcr.io/github/gh-aw:",
"%s: expected docker pull of published container", tt.description)
assert.Contains(t, result, "docker create",
"%s: expected docker create to extract binary", tt.description)
assert.Contains(t, result, "docker cp",
"%s: expected docker cp to copy binary to workspace", tt.description)
assert.Contains(t, result, "/usr/local/bin/gh-aw\" ./gh-aw",
"%s: expected binary to be copied to ./gh-aw in workspace", tt.description)
assert.Contains(t, result, "chmod +x ./gh-aw",
"%s: expected chmod +x on ./gh-aw", tt.description)
assert.NotContains(t, result, "gh extension install",
"%s: should not install gh extension", tt.description)
} else {
assert.NotContains(t, result, "Install gh-aw CLI",
"%s: expected Install gh-aw CLI step to be absent", tt.description)
}
})
}
}
47 changes: 47 additions & 0 deletions pkg/workflow/compiler_yaml_main_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat
yaml.WriteString(" - name: Create gh-aw temp directory\n")
yaml.WriteString(" run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh\n")

// In production mode with agentic-workflows tool, install gh-aw early so
// ./gh-aw is available in the workspace for custom steps. In dev mode the
// binary is already built from source (see generateDevModeCLIBuildSteps).
if !c.actionMode.IsDev() && data.CustomSteps != "" {
if _, hasAgenticWorkflows := data.Tools["agentic-workflows"]; hasAgenticWorkflows {
if !workflowHasGhAwImport(data) {
compilerYamlLog.Printf("Generating gh-aw workspace binary step for production mode (agentic-workflows tool enabled)")
c.generateGhAwWorkspaceBinaryStep(yaml)
}
}
}

// Add custom steps if present
if data.CustomSteps != "" {
if customStepsContainCheckout && len(runtimeSetupSteps) > 0 {
Expand Down Expand Up @@ -694,3 +706,38 @@ func (c *Compiler) generateDevModeCLIBuildSteps(yaml *strings.Builder) {
yaml.WriteString(" build-args: |\n")
yaml.WriteString(" BINARY=dist/gh-aw-linux-amd64\n")
}

// workflowHasGhAwImport returns true if the workflow imports shared/mcp/gh-aw.md,
// which provides its own gh-aw installation steps.
func workflowHasGhAwImport(data *WorkflowData) bool {
for _, importPath := range data.ImportedFiles {
if strings.Contains(importPath, "shared/mcp/gh-aw.md") {
return true
}
}
return false
}

// generateGhAwWorkspaceBinaryStep generates a step that extracts the gh-aw binary
// from the published release container (ghcr.io/github/gh-aw:<version>) and places
// it at ./gh-aw in the workspace, making it available for custom steps before the
// MCP setup phase runs.
//
// This step is only generated in production mode when the agentic-workflows tool
// is enabled and there are custom steps that may need the ./gh-aw binary.
// In dev mode, the binary is built from source by generateDevModeCLIBuildSteps instead.
func (c *Compiler) generateGhAwWorkspaceBinaryStep(yaml *strings.Builder) {
// Container images are tagged with the semver version without the 'v' prefix
// (e.g., 'ghcr.io/github/gh-aw:0.50.7' for compiler version 'v0.50.7')
version := strings.TrimPrefix(c.version, "v")
containerImage := "ghcr.io/github/gh-aw:" + version

yaml.WriteString(" - name: Install gh-aw CLI\n")
yaml.WriteString(" run: |\n")
fmt.Fprintf(yaml, " docker pull %s\n", containerImage)
fmt.Fprintf(yaml, " CONTAINER=$(docker create %s)\n", containerImage)
yaml.WriteString(" docker cp \"${CONTAINER}:/usr/local/bin/gh-aw\" ./gh-aw\n")
yaml.WriteString(" docker rm \"${CONTAINER}\" > /dev/null\n")
yaml.WriteString(" chmod +x ./gh-aw\n")
yaml.WriteString(" echo \"gh-aw CLI binary available at ./gh-aw\"\n")
}