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
2 changes: 1 addition & 1 deletion pkg/cli/compile_compiler_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func validateActionModeConfig(actionMode string) error {

mode := workflow.ActionMode(actionMode)
if !mode.IsValid() {
return fmt.Errorf("invalid action mode '%s'. Must be 'dev', 'release', or 'script'", actionMode)
return fmt.Errorf("invalid action mode '%s'. Must be 'dev', 'release', 'script', or 'action'", actionMode)
}

return nil
Expand Down
51 changes: 37 additions & 14 deletions pkg/cli/copilot_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
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, attempts to resolve the SHA for a SHA-pinned reference.
// 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.ActionSHAResolver) string {
if actionMode.IsRelease() && version != "" && version != "dev" {
Expand All @@ -28,16 +28,30 @@ func getActionRef(actionMode workflow.ActionMode, version string, resolver workf
}
return "@" + version
}
if actionMode.IsAction() && version != "" && version != "dev" {
if resolver != nil {
sha, err := resolver.ResolveSHA("github/gh-aw-actions/setup-cli", version)
if err == nil && sha != "" {
return fmt.Sprintf("@%s # %s", sha, version)
}
copilotSetupLog.Printf("Failed to resolve SHA for gh-aw-actions/setup-cli@%s: %v, falling back to version tag", version, err)
}
return "@" + version
}
return "@main"
}

// generateCopilotSetupStepsYAML generates the copilot-setup-steps.yml content based on action mode
func generateCopilotSetupStepsYAML(actionMode workflow.ActionMode, version string, resolver workflow.ActionSHAResolver) string {
// Determine the action reference - use SHA-pinned or version tag in release mode, @main in dev mode
// Determine the action reference - use SHA-pinned or version tag in release/action mode, @main in dev mode
actionRef := getActionRef(actionMode, version, resolver)

if actionMode.IsRelease() {
// Use the actions/setup-cli action in release mode
if actionMode.IsRelease() || actionMode.IsAction() {
// Determine the action repo based on mode
actionRepo := "github/gh-aw/actions/setup-cli"
if actionMode.IsAction() {
actionRepo = "github/gh-aw-actions/setup-cli"
}
return fmt.Sprintf(`name: "Copilot Setup Steps"

# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server
Expand All @@ -61,10 +75,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install gh-aw extension
uses: github/gh-aw/actions/setup-cli%s
uses: %s%s
with:
version: %s
`, actionRef, version)
`, actionRepo, actionRef, version)
}

// Default (dev/script mode): use curl to download install script
Expand Down Expand Up @@ -158,9 +172,9 @@ func upgradeCopilotSetupSteps(verbose bool, actionMode workflow.ActionMode, vers
func ensureCopilotSetupStepsWithUpgrade(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 mode to enable SHA-pinned action references
// Create a SHA resolver for release/action mode to enable SHA-pinned action references
var resolver workflow.ActionSHAResolver
if actionMode.IsRelease() {
if actionMode.IsRelease() || actionMode.IsAction() {
cache := workflow.NewActionCache(".")
_ = cache.Load() // Ignore errors if cache doesn't exist yet
resolver = workflow.NewActionResolver(cache)
Expand Down Expand Up @@ -259,11 +273,15 @@ func renderCopilotSetupUpdateInstructions(filePath string, actionMode workflow.A
// Determine the action reference
actionRef := getActionRef(actionMode, version, resolver)

if actionMode.IsRelease() {
if actionMode.IsRelease() || actionMode.IsAction() {
actionRepo := "github/gh-aw/actions/setup-cli"
if actionMode.IsAction() {
actionRepo = "github/gh-aw-actions/setup-cli"
}
fmt.Fprintln(os.Stderr, " - name: Checkout repository")
fmt.Fprintln(os.Stderr, " uses: actions/checkout@v6")
fmt.Fprintf(os.Stderr, " - name: Install gh-aw extension\n")
fmt.Fprintf(os.Stderr, " uses: github/gh-aw/actions/setup-cli%s\n", actionRef)
fmt.Fprintf(os.Stderr, " uses: %s%s\n", actionRepo, actionRef)
fmt.Fprintln(os.Stderr, " with:")
fmt.Fprintf(os.Stderr, " version: %s\n", version)
} else {
Expand All @@ -274,11 +292,12 @@ func renderCopilotSetupUpdateInstructions(filePath string, actionMode workflow.A
fmt.Fprintln(os.Stderr)
}

// setupCliUsesPattern matches the uses: line for github/gh-aw/actions/setup-cli.
// setupCliUsesPattern matches the uses: line for either github/gh-aw/actions/setup-cli
// or github/gh-aw-actions/setup-cli.
// It handles unquoted version-tag refs, unquoted SHA-pinned refs (with trailing comment),
// and quoted refs produced by some YAML marshalers (e.g. "...@sha # vX.Y.Z").
var setupCliUsesPattern = regexp.MustCompile(
`(?m)^(\s+uses:[ \t]*)"?(github/gh-aw/actions/setup-cli@[^"\n]*)"?([ \t]*)$`)
`(?m)^(\s+uses:[ \t]*)"?(github/gh-aw(?:-actions)?/(?:actions/)?setup-cli@[^"\n]*)"?([ \t]*)$`)

// upgradeSetupCliVersionInContent replaces the setup-cli action reference and the
// associated version: parameter in the raw YAML content using targeted regex
Expand All @@ -287,7 +306,7 @@ var setupCliUsesPattern = 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.ActionSHAResolver) (bool, []byte, error) {
if !actionMode.IsRelease() {
if !actionMode.IsRelease() && !actionMode.IsAction() {
return false, content, nil
}

Expand All @@ -296,7 +315,11 @@ func upgradeSetupCliVersionInContent(content []byte, actionMode workflow.ActionM
}

actionRef := getActionRef(actionMode, version, resolver)
newUses := "github/gh-aw/actions/setup-cli" + actionRef
actionRepo := "github/gh-aw/actions/setup-cli"
if actionMode.IsAction() {
actionRepo = "github/gh-aw-actions/setup-cli"
}
newUses := actionRepo + actionRef

// Replace the uses: line, stripping any surrounding quotes in the process.
updated := setupCliUsesPattern.ReplaceAll(content, []byte("${1}"+newUses+"${3}"))
Expand Down
10 changes: 9 additions & 1 deletion pkg/workflow/action_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const (

// ActionModeScript runs setup.sh script from checked-out .github folder instead of using action steps
ActionModeScript ActionMode = "script"

// ActionModeAction references custom actions from the github/gh-aw-actions repository using the same release version
ActionModeAction ActionMode = "action"
)

// String returns the string representation of the action mode
Expand All @@ -30,7 +33,7 @@ func (m ActionMode) String() string {

// IsValid checks if the action mode is valid
func (m ActionMode) IsValid() bool {
return m == ActionModeDev || m == ActionModeRelease || m == ActionModeScript
return m == ActionModeDev || m == ActionModeRelease || m == ActionModeScript || m == ActionModeAction
}

// IsDev returns true if the action mode is development mode
Expand All @@ -48,6 +51,11 @@ func (m ActionMode) IsScript() bool {
return m == ActionModeScript
}

// IsAction returns true if the action mode is action mode (uses github/gh-aw-actions repo)
func (m ActionMode) IsAction() bool {
return m == ActionModeAction
}

// UsesExternalActions returns true (always true since inline mode was removed)
func (m ActionMode) UsesExternalActions() bool {
return true
Expand Down
109 changes: 105 additions & 4 deletions pkg/workflow/action_reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,28 @@ var actionRefLog = logger.New("workflow:action_reference")
const (
// GitHubOrgRepo is the organization and repository name for custom action references
GitHubOrgRepo = "github/gh-aw"

// GitHubActionsOrgRepo is the organization and repository name for the external gh-aw-actions repository
GitHubActionsOrgRepo = "github/gh-aw-actions"
)

// ResolveSetupActionReference resolves the actions/setup action reference based on action mode and version.
// This is a standalone helper function that can be used by both Compiler methods and standalone
// workflow generators (like maintenance workflow) that don't have access to WorkflowData.
//
// Parameters:
// - actionMode: The action mode (dev or release)
// - version: The version string to use for release mode
// - actionMode: The action mode (dev, release, or action)
// - version: The version string to use for release/action mode
// - actionTag: Optional override tag/SHA (takes precedence over version when in release mode)
// - resolver: Optional ActionSHAResolver for dynamic SHA resolution (can be nil for standalone use)
//
// Returns:
// - For dev mode: "./actions/setup" (local path)
// - For release mode with resolver: "github/gh-aw/actions/setup@<sha> # <version>" (SHA-pinned)
// - For release mode without resolver: "github/gh-aw/actions/setup@<version>" (tag-based, SHA resolved later)
// - Falls back to local path if version is invalid in release mode
// - For action mode with resolver: "github/gh-aw-actions/setup@<sha> # <version>" (SHA-pinned)
// - For action mode without resolver: "github/gh-aw-actions/setup@<version>" (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 ActionSHAResolver) string {
localPath := "./actions/setup"

Expand All @@ -38,6 +43,42 @@ func ResolveSetupActionReference(actionMode ActionMode, version string, actionTa
return localPath
}

// Action mode - use external gh-aw-actions repository with SHA pinning if possible
if actionMode == ActionModeAction {
// Use actionTag if provided, otherwise fall back to version
tag := actionTag
if tag == "" {
tag = version
}

// Check if tag is valid for action mode
if tag == "" || tag == "dev" {
actionRefLog.Print("WARNING: No release tag available in binary version (version is 'dev' or empty), falling back to local path")
return localPath
}

// Construct the remote reference: github/gh-aw-actions/setup@tag
actionRepo := GitHubActionsOrgRepo + "/setup"
remoteRef := fmt.Sprintf("%s@%s", actionRepo, tag)

// If a resolver is available, try to resolve the SHA
if resolver != nil {
sha, err := resolver.ResolveSHA(actionRepo, tag)
if err == nil && sha != "" {
pinnedRef := formatActionReference(actionRepo, sha, tag)
actionRefLog.Printf("Action mode: resolved %s to SHA-pinned reference: %s", remoteRef, pinnedRef)
return pinnedRef
}
if err != nil {
actionRefLog.Printf("Failed to resolve SHA for %s@%s: %v", actionRepo, tag, err)
}
}

// If no resolver or SHA resolution failed, return tag-based reference
actionRefLog.Printf("Action mode: using tag-based external actions repo reference: %s (SHA will be resolved later)", remoteRef)
return remoteRef
}

// Release mode - convert to remote reference
if actionMode == ActionModeRelease {
actionPath := strings.TrimPrefix(localPath, "./")
Expand Down Expand Up @@ -83,10 +124,11 @@ func ResolveSetupActionReference(actionMode ActionMode, version string, actionTa
}

// resolveActionReference converts a local action path to the appropriate reference
// based on the current action mode (dev vs release).
// based on the current action mode (dev vs release vs action).
// If action-tag is specified in features, it overrides the mode check and enables release mode behavior.
// For dev mode: returns the local path as-is (e.g., "./actions/create-issue")
// For release mode: converts to SHA-pinned remote reference (e.g., "github/gh-aw/actions/create-issue@SHA # tag")
// For action mode: converts to SHA-pinned reference in external repo if possible (e.g., "github/gh-aw-actions/create-issue@SHA # version")
func (c *Compiler) resolveActionReference(localActionPath string, data *WorkflowData) string {
// Check if action-tag is specified in features - if so, override mode and use release behavior
hasActionTag := false
Expand Down Expand Up @@ -114,6 +156,11 @@ func (c *Compiler) resolveActionReference(localActionPath string, data *Workflow
}
}

// Action mode - use external gh-aw-actions repository with version tag (no SHA pinning)
if c.actionMode == ActionModeAction && !hasActionTag {
return c.convertToExternalActionsRef(localActionPath, data)
}

// Use release mode if either actionMode is release OR action-tag is specified
if c.actionMode == ActionModeRelease || hasActionTag {
// Convert to tag-based remote reference for release
Expand Down Expand Up @@ -214,3 +261,57 @@ func (c *Compiler) convertToRemoteActionRef(localPath string, data *WorkflowData

return remoteRef
}

// convertToExternalActionsRef converts a local action path to a SHA-pinned (if possible) reference
// in the external github/gh-aw-actions repository.
// Example: "./actions/create-issue" -> "github/gh-aw-actions/create-issue@<sha> # v1.0.0"
//
// If SHA resolution fails (no resolver or pin not available), falls back to version-tagged reference:
// Example: "./actions/create-issue" -> "github/gh-aw-actions/create-issue@v1.0.0"
func (c *Compiler) convertToExternalActionsRef(localPath string, data *WorkflowData) string {
// Strip the leading "./" prefix
actionPath := strings.TrimPrefix(localPath, "./")

// Strip the "actions/" prefix to get just the action name
// e.g., "actions/create-issue" -> "create-issue"
actionName := strings.TrimPrefix(actionPath, "actions/")

// Determine tag: use compiler actionTag or version
tag := c.actionTag
if tag == "" {
if data != nil && data.Features != nil {
if actionTagVal, exists := data.Features["action-tag"]; exists {
if actionTagStr, ok := actionTagVal.(string); ok && actionTagStr != "" {
tag = actionTagStr
}
}
}
}
if tag == "" {
tag = c.version
if tag == "" || tag == "dev" {
actionRefLog.Print("WARNING: No release tag available in binary version (version is 'dev' or empty)")
return ""
}
}

// Construct the external actions reference: github/gh-aw-actions/action-name@tag
actionRepo := fmt.Sprintf("%s/%s", GitHubActionsOrgRepo, actionName)
remoteRef := fmt.Sprintf("%s@%s", actionRepo, tag)

// Try to resolve the SHA using action pins
if data != nil {
pinnedRef, err := GetActionPinWithData(actionRepo, tag, data)
if err != nil {
// Log and fall through to tag-based reference (action mode is not strict)
actionRefLog.Printf("Failed to pin action %s@%s: %v, falling back to tag-based reference", actionRepo, tag, err)
} else if pinnedRef != "" {
actionRefLog.Printf("Action mode: resolved %s to SHA-pinned reference: %s", remoteRef, pinnedRef)
return pinnedRef
}
}

// If SHA resolution unavailable or pin not found, return tag-based reference
actionRefLog.Printf("Action mode: using tag-based external actions repo reference: %s (SHA will be resolved later)", remoteRef)
return remoteRef
}
Loading
Loading