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
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import (
"github.com/github/gh-aw/pkg/workflow"
)

var compileHelpersLog = logger.New("cli:compile_helpers")
var compileHelpersLog = logger.New("cli:compile_file_operations")

// getRepositoryRelativePath converts an absolute file path to a repository-relative path
// This ensures stable workflow identifiers regardless of where the repository is cloned
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import (
"github.com/github/gh-aw/pkg/workflow"
)

var compileOrchestrationLog = logger.New("cli:compile_orchestration")
var compileOrchestrationLog = logger.New("cli:compile_pipeline")

// compileSpecificFiles compiles a specific list of workflow files
func compileSpecificFiles(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/github/gh-aw/pkg/parser"
)

var yamlUtilsLog = logger.New("cli:codemod_yaml_utils")
var yamlUtilsLog = logger.New("cli:yaml_frontmatter_utils")

// reconstructContent rebuilds the full markdown content from frontmatter lines and body
func reconstructContent(frontmatterLines []string, markdown string) string {
Expand Down
122 changes: 122 additions & 0 deletions pkg/workflow/compiler_github_actions_steps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package workflow

import (
"fmt"
"strings"

"github.com/github/gh-aw/pkg/logger"
)

var compilerGitHubActionsStepsLog = logger.New("workflow:compiler_github_actions_steps")

// generateGitHubScriptWithRequire generates a github-script step that loads a module using require().
// Instead of repeating the global variable assignments inline, it uses the setup_globals helper function.
//
// Parameters:
// - scriptPath: The path to the .cjs file to require (e.g., "check_stop_time.cjs")
//
// Returns a string containing the complete script content to be used in a github-script action's "script:" field.
func generateGitHubScriptWithRequire(scriptPath string) string {
var script strings.Builder

// Use the setup_globals helper to store GitHub Actions objects in global scope
script.WriteString(" const { setupGlobals } = require('" + SetupActionDestination + "/setup_globals.cjs');\n")
script.WriteString(" setupGlobals(core, github, context, exec, io);\n")
script.WriteString(" const { main } = require('" + SetupActionDestination + "/" + scriptPath + "');\n")
script.WriteString(" await main();\n")

return script.String()
}

// generateInlineGitHubScriptStep generates a simple inline github-script step
// for validation or utility operations that don't require artifact downloads.
//
// Parameters:
// - stepName: The name of the step (e.g., "Validate cache-memory file types")
// - script: The JavaScript code to execute (pre-formatted with proper indentation)
// - condition: Optional if condition (e.g., "always()"). Empty string means no condition.
//
// Returns a string containing the complete YAML for the github-script step.
func generateInlineGitHubScriptStep(stepName, script, condition string) string {
var step strings.Builder

step.WriteString(" - name: " + stepName + "\n")
if condition != "" {
step.WriteString(" if: " + condition + "\n")
}
step.WriteString(" uses: " + GetActionPin("actions/github-script") + "\n")
step.WriteString(" with:\n")
step.WriteString(" script: |\n")
step.WriteString(script)

return step.String()
}

// generatePlaceholderSubstitutionStep generates a JavaScript-based step that performs
// safe placeholder substitution using the substitute_placeholders script.
// This replaces the multiple sed commands with a single JavaScript step.
func generatePlaceholderSubstitutionStep(yaml *strings.Builder, expressionMappings []*ExpressionMapping, indent string) {
if len(expressionMappings) == 0 {
return
}

compilerGitHubActionsStepsLog.Printf("Generating placeholder substitution step with %d mappings", len(expressionMappings))

// Use actions/github-script to perform the substitutions
yaml.WriteString(indent + "- name: Substitute placeholders\n")
fmt.Fprintf(yaml, indent+" uses: %s\n", GetActionPin("actions/github-script"))
yaml.WriteString(indent + " env:\n")
yaml.WriteString(indent + " GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt\n")

// Add all environment variables
// For static values (wrapped in quotes), output them directly without ${{ }}
// For GitHub expressions, wrap them in ${{ }}
for _, mapping := range expressionMappings {
content := mapping.Content
// Check if this is a static quoted value (starts and ends with quotes)
if (strings.HasPrefix(content, "'") && strings.HasSuffix(content, "'")) ||
(strings.HasPrefix(content, "\"") && strings.HasSuffix(content, "\"")) {
// Static value - output directly without ${{ }} wrapper
// Check if inner value is multi-line; if so use a YAML double-quoted scalar
// with escaped newlines to avoid invalid YAML.
innerValue := content[1 : len(content)-1]
if strings.Contains(innerValue, "\n") {
escaped := strings.ReplaceAll(innerValue, `\`, `\\`)
escaped = strings.ReplaceAll(escaped, `"`, `\"`)
escaped = strings.ReplaceAll(escaped, "\n", `\n`)
fmt.Fprintf(yaml, indent+" %s: \"%s\"\n", mapping.EnvVar, escaped)
} else {
fmt.Fprintf(yaml, indent+" %s: %s\n", mapping.EnvVar, content)
}
} else {
// GitHub expression - wrap in ${{ }}
fmt.Fprintf(yaml, indent+" %s: ${{ %s }}\n", mapping.EnvVar, content)
}
}

yaml.WriteString(indent + " with:\n")
yaml.WriteString(indent + " script: |\n")

// Use setup_globals helper to make GitHub Actions objects available globally
yaml.WriteString(indent + " const { setupGlobals } = require('" + SetupActionDestination + "/setup_globals.cjs');\n")
yaml.WriteString(indent + " setupGlobals(core, github, context, exec, io);\n")
yaml.WriteString(indent + " \n")
// Use require() to load script from copied files
yaml.WriteString(indent + " const substitutePlaceholders = require('" + SetupActionDestination + "/substitute_placeholders.cjs');\n")
yaml.WriteString(indent + " \n")
yaml.WriteString(indent + " // Call the substitution function\n")
yaml.WriteString(indent + " return await substitutePlaceholders({\n")
yaml.WriteString(indent + " file: process.env.GH_AW_PROMPT,\n")
yaml.WriteString(indent + " substitutions: {\n")

for i, mapping := range expressionMappings {
comma := ","
if i == len(expressionMappings)-1 {
comma = ""
}
fmt.Fprintf(yaml, indent+" %s: process.env.%s%s\n", mapping.EnvVar, mapping.EnvVar, comma)
}

yaml.WriteString(indent + " }\n")
yaml.WriteString(indent + " });\n")
}
140 changes: 140 additions & 0 deletions pkg/workflow/compiler_github_mcp_steps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package workflow

import (
"fmt"
"strings"

"github.com/github/gh-aw/pkg/constants"
)

// generateGitHubMCPLockdownDetectionStep generates a step to determine automatic guard policy
// for GitHub MCP server based on repository visibility.
// This step is added when:
// - GitHub tool is enabled AND
// - guard policy (repos/min-integrity) is not fully configured in the workflow AND
// - tools.github.app is NOT configured (GitHub App tokens are already repo-scoped, so
// automatic guard policy detection is unnecessary and skipped)
//
// For public repositories, the step automatically sets min-integrity to "approved" and
// repos to "all" if they are not already configured.
func (c *Compiler) generateGitHubMCPLockdownDetectionStep(yaml *strings.Builder, data *WorkflowData) {
// Check if GitHub tool is present
githubTool, hasGitHub := data.Tools["github"]
if !hasGitHub || githubTool == false {
return
}

// Skip when guard policy is already fully configured in the workflow.
// The step is only needed to auto-configure guard policies for public repos.
if len(getGitHubGuardPolicies(githubTool)) > 0 {
githubConfigLog.Print("Guard policy already configured in workflow, skipping automatic guard policy determination")
return
}

// Skip automatic guard policy detection when a GitHub App is configured.
// GitHub App tokens are already scoped to specific repositories, so automatic
// guard policy detection is not needed — the token's access is inherently bounded
// by the app installation and the listed repositories.
if hasGitHubApp(githubTool) {
githubConfigLog.Print("GitHub App configured, skipping automatic guard policy determination (app tokens are already repo-scoped)")
return
}

githubConfigLog.Print("Generating automatic guard policy determination step for GitHub MCP server")

// Resolve the latest version of actions/github-script
actionRepo := "actions/github-script"
actionVersion := string(constants.DefaultGitHubScriptVersion)
pinnedAction, err := GetActionPinWithData(actionRepo, actionVersion, data)
if err != nil {
githubConfigLog.Printf("Failed to resolve %s@%s: %v", actionRepo, actionVersion, err)
// In strict mode, this error would have been returned by GetActionPinWithData
// In normal mode, we fall back to using the version tag without pinning
pinnedAction = fmt.Sprintf("%s@%s", actionRepo, actionVersion)
}

// Extract current guard policy configuration to pass as env vars so the step can
// detect whether each field is already configured and avoid overriding it.
configuredMinIntegrity := ""
configuredRepos := ""
if toolConfig, ok := githubTool.(map[string]any); ok {
if v, exists := toolConfig["min-integrity"]; exists {
configuredMinIntegrity = fmt.Sprintf("%v", v)
}
if v, exists := toolConfig["repos"]; exists {
configuredRepos = fmt.Sprintf("%v", v)
}
}

// Generate the step using the determine_automatic_lockdown.cjs action
yaml.WriteString(" - name: Determine automatic lockdown mode for GitHub MCP Server\n")
yaml.WriteString(" id: determine-automatic-lockdown\n")
fmt.Fprintf(yaml, " uses: %s\n", pinnedAction)
yaml.WriteString(" env:\n")
yaml.WriteString(" GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}\n")
yaml.WriteString(" GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}\n")
if configuredMinIntegrity != "" {
fmt.Fprintf(yaml, " GH_AW_GITHUB_MIN_INTEGRITY: %s\n", configuredMinIntegrity)
}
if configuredRepos != "" {
fmt.Fprintf(yaml, " GH_AW_GITHUB_REPOS: %s\n", configuredRepos)
}
yaml.WriteString(" with:\n")
yaml.WriteString(" script: |\n")
yaml.WriteString(" const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs');\n")
yaml.WriteString(" await determineAutomaticLockdown(github, context, core);\n")
}

// generateGitHubMCPAppTokenMintingStep generates a step to mint a GitHub App token for GitHub MCP server
// This step is added when:
// - GitHub tool is enabled with app configuration
// The step mints an installation access token with permissions matching the agent job permissions
func (c *Compiler) generateGitHubMCPAppTokenMintingStep(yaml *strings.Builder, data *WorkflowData) {
// Check if GitHub tool has app configuration
if data.ParsedTools == nil || data.ParsedTools.GitHub == nil || data.ParsedTools.GitHub.GitHubApp == nil {
return
}

app := data.ParsedTools.GitHub.GitHubApp
githubConfigLog.Printf("Generating GitHub App token minting step for GitHub MCP server: app-id=%s", app.AppID)

// Get permissions from the agent job - parse from YAML string
var permissions *Permissions
if data.Permissions != "" {
parser := NewPermissionsParser(data.Permissions)
permissions = parser.ToPermissions()
} else {
githubConfigLog.Print("No permissions specified, using empty permissions")
permissions = NewPermissions()
}

// Generate the token minting step using the existing helper from safe_outputs_app.go
steps := c.buildGitHubAppTokenMintStep(app, permissions, "")

// Modify the step ID to differentiate from safe-outputs app token
// Replace "safe-outputs-app-token" with "github-mcp-app-token"
for _, step := range steps {
modifiedStep := strings.ReplaceAll(step, "id: safe-outputs-app-token", "id: github-mcp-app-token")
yaml.WriteString(modifiedStep)
}
}

// generateGitHubMCPAppTokenInvalidationStep generates a step to invalidate the GitHub App token for GitHub MCP server
// This step always runs (even on failure) to ensure tokens are properly cleaned up
func (c *Compiler) generateGitHubMCPAppTokenInvalidationStep(yaml *strings.Builder, data *WorkflowData) {
// Check if GitHub tool has app configuration
if data.ParsedTools == nil || data.ParsedTools.GitHub == nil || data.ParsedTools.GitHub.GitHubApp == nil {
return
}

githubConfigLog.Print("Generating GitHub App token invalidation step for GitHub MCP server")

// Generate the token invalidation step using the existing helper from safe_outputs_app.go
steps := c.buildGitHubAppTokenInvalidationStep()

// Modify the step references to use github-mcp-app-token instead of safe-outputs-app-token
for _, step := range steps {
modifiedStep := strings.ReplaceAll(step, "steps.safe-outputs-app-token.outputs.token", "steps.github-mcp-app-token.outputs.token")
yaml.WriteString(modifiedStep)
}
}
Loading
Loading