|
| 1 | +package workflow |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "strconv" |
| 6 | + "strings" |
| 7 | + |
| 8 | + "github.com/github/gh-aw/pkg/logger" |
| 9 | +) |
| 10 | + |
| 11 | +var safeOutputsEnvLog = logger.New("workflow:safe_outputs_env") |
| 12 | + |
| 13 | +// ======================================== |
| 14 | +// Safe Output Environment Variables |
| 15 | +// ======================================== |
| 16 | + |
| 17 | +// applySafeOutputEnvToMap adds safe-output related environment variables to an env map |
| 18 | +// This extracts the duplicated safe-output env setup logic across all engines (copilot, codex, claude, custom) |
| 19 | +func applySafeOutputEnvToMap(env map[string]string, data *WorkflowData) { |
| 20 | + if data.SafeOutputs == nil { |
| 21 | + return |
| 22 | + } |
| 23 | + |
| 24 | + safeOutputsEnvLog.Printf("Applying safe output env vars: trial_mode=%t, staged=%t", data.TrialMode, data.SafeOutputs.Staged) |
| 25 | + |
| 26 | + env["GH_AW_SAFE_OUTPUTS"] = "${{ env.GH_AW_SAFE_OUTPUTS }}" |
| 27 | + |
| 28 | + // Add staged flag if specified |
| 29 | + if data.TrialMode || data.SafeOutputs.Staged { |
| 30 | + env["GH_AW_SAFE_OUTPUTS_STAGED"] = "true" |
| 31 | + } |
| 32 | + if data.TrialMode && data.TrialLogicalRepo != "" { |
| 33 | + env["GH_AW_TARGET_REPO_SLUG"] = data.TrialLogicalRepo |
| 34 | + } |
| 35 | + |
| 36 | + // Add branch name if upload assets is configured |
| 37 | + if data.SafeOutputs.UploadAssets != nil { |
| 38 | + safeOutputsEnvLog.Printf("Adding upload assets env vars: branch=%s", data.SafeOutputs.UploadAssets.BranchName) |
| 39 | + env["GH_AW_ASSETS_BRANCH"] = fmt.Sprintf("%q", data.SafeOutputs.UploadAssets.BranchName) |
| 40 | + env["GH_AW_ASSETS_MAX_SIZE_KB"] = strconv.Itoa(data.SafeOutputs.UploadAssets.MaxSizeKB) |
| 41 | + env["GH_AW_ASSETS_ALLOWED_EXTS"] = fmt.Sprintf("%q", strings.Join(data.SafeOutputs.UploadAssets.AllowedExts, ",")) |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +// buildWorkflowMetadataEnvVars builds workflow name and source environment variables |
| 46 | +// This extracts the duplicated workflow metadata setup logic from safe-output job builders |
| 47 | +func buildWorkflowMetadataEnvVars(workflowName string, workflowSource string) []string { |
| 48 | + var customEnvVars []string |
| 49 | + |
| 50 | + // Add workflow name |
| 51 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_WORKFLOW_NAME: %q\n", workflowName)) |
| 52 | + |
| 53 | + // Add workflow source and source URL if present |
| 54 | + if workflowSource != "" { |
| 55 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_WORKFLOW_SOURCE: %q\n", workflowSource)) |
| 56 | + sourceURL := buildSourceURL(workflowSource) |
| 57 | + if sourceURL != "" { |
| 58 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_WORKFLOW_SOURCE_URL: %q\n", sourceURL)) |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + return customEnvVars |
| 63 | +} |
| 64 | + |
| 65 | +// buildWorkflowMetadataEnvVarsWithTrackerID builds workflow metadata env vars including tracker-id |
| 66 | +func buildWorkflowMetadataEnvVarsWithTrackerID(workflowName string, workflowSource string, trackerID string) []string { |
| 67 | + customEnvVars := buildWorkflowMetadataEnvVars(workflowName, workflowSource) |
| 68 | + |
| 69 | + // Add tracker-id if present |
| 70 | + if trackerID != "" { |
| 71 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_TRACKER_ID: %q\n", trackerID)) |
| 72 | + } |
| 73 | + |
| 74 | + return customEnvVars |
| 75 | +} |
| 76 | + |
| 77 | +// buildSafeOutputJobEnvVars builds environment variables for safe-output jobs with staged/target repo handling |
| 78 | +// This extracts the duplicated env setup logic in safe-output job builders (create_issue, add_comment, etc.) |
| 79 | +func buildSafeOutputJobEnvVars(trialMode bool, trialLogicalRepoSlug string, staged bool, targetRepoSlug string) []string { |
| 80 | + var customEnvVars []string |
| 81 | + |
| 82 | + // Pass the staged flag if it's set to true |
| 83 | + if trialMode || staged { |
| 84 | + safeOutputsEnvLog.Printf("Setting staged flag: trial_mode=%t, staged=%t", trialMode, staged) |
| 85 | + customEnvVars = append(customEnvVars, " GH_AW_SAFE_OUTPUTS_STAGED: \"true\"\n") |
| 86 | + } |
| 87 | + |
| 88 | + // Set GH_AW_TARGET_REPO_SLUG - prefer target-repo config over trial target repo |
| 89 | + if targetRepoSlug != "" { |
| 90 | + safeOutputsEnvLog.Printf("Setting target repo slug from config: %s", targetRepoSlug) |
| 91 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_TARGET_REPO_SLUG: %q\n", targetRepoSlug)) |
| 92 | + } else if trialMode && trialLogicalRepoSlug != "" { |
| 93 | + safeOutputsEnvLog.Printf("Setting target repo slug from trial mode: %s", trialLogicalRepoSlug) |
| 94 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_TARGET_REPO_SLUG: %q\n", trialLogicalRepoSlug)) |
| 95 | + } |
| 96 | + |
| 97 | + return customEnvVars |
| 98 | +} |
| 99 | + |
| 100 | +// buildStandardSafeOutputEnvVars builds the standard set of environment variables |
| 101 | +// that all safe-output job builders need: metadata + staged/target repo handling |
| 102 | +// This reduces duplication in safe-output job builders |
| 103 | +func (c *Compiler) buildStandardSafeOutputEnvVars(data *WorkflowData, targetRepoSlug string) []string { |
| 104 | + var customEnvVars []string |
| 105 | + |
| 106 | + // Add workflow metadata (name, source, and tracker-id) |
| 107 | + customEnvVars = append(customEnvVars, buildWorkflowMetadataEnvVarsWithTrackerID(data.Name, data.Source, data.TrackerID)...) |
| 108 | + |
| 109 | + // Add engine metadata (id, version, model) for XML comment marker |
| 110 | + customEnvVars = append(customEnvVars, buildEngineMetadataEnvVars(data.EngineConfig)...) |
| 111 | + |
| 112 | + // Add common safe output job environment variables (staged/target repo) |
| 113 | + customEnvVars = append(customEnvVars, buildSafeOutputJobEnvVars( |
| 114 | + c.trialMode, |
| 115 | + c.trialLogicalRepoSlug, |
| 116 | + data.SafeOutputs.Staged, |
| 117 | + targetRepoSlug, |
| 118 | + )...) |
| 119 | + |
| 120 | + // Add messages config if present |
| 121 | + if data.SafeOutputs.Messages != nil { |
| 122 | + messagesJSON, err := serializeMessagesConfig(data.SafeOutputs.Messages) |
| 123 | + if err != nil { |
| 124 | + safeOutputsEnvLog.Printf("Warning: failed to serialize messages config: %v", err) |
| 125 | + } else if messagesJSON != "" { |
| 126 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_SAFE_OUTPUT_MESSAGES: %q\n", messagesJSON)) |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + return customEnvVars |
| 131 | +} |
| 132 | + |
| 133 | +// buildStepLevelSafeOutputEnvVars builds environment variables for consolidated safe output steps |
| 134 | +// This excludes variables that are already set at the job level in consolidated jobs |
| 135 | +func (c *Compiler) buildStepLevelSafeOutputEnvVars(data *WorkflowData, targetRepoSlug string) []string { |
| 136 | + var customEnvVars []string |
| 137 | + |
| 138 | + // Only add target repo slug if it's different from the job-level setting |
| 139 | + // (i.e., this step has a specific target-repo config that overrides the global trial mode target) |
| 140 | + if targetRepoSlug != "" { |
| 141 | + // Step-specific target repo overrides job-level setting |
| 142 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_TARGET_REPO_SLUG: %q\n", targetRepoSlug)) |
| 143 | + } else if !c.trialMode && data.SafeOutputs.Staged { |
| 144 | + // Step needs staged flag but there's no job-level target repo (not in trial mode) |
| 145 | + // Job level only sets this if trialMode is true |
| 146 | + customEnvVars = append(customEnvVars, " GH_AW_SAFE_OUTPUTS_STAGED: \"true\"\n") |
| 147 | + } |
| 148 | + |
| 149 | + // Note: The following are now set at job level and should NOT be included here: |
| 150 | + // - GH_AW_WORKFLOW_NAME |
| 151 | + // - GH_AW_WORKFLOW_SOURCE |
| 152 | + // - GH_AW_WORKFLOW_SOURCE_URL |
| 153 | + // - GH_AW_TRACKER_ID |
| 154 | + // - GH_AW_ENGINE_ID |
| 155 | + // - GH_AW_ENGINE_VERSION |
| 156 | + // - GH_AW_ENGINE_MODEL |
| 157 | + // - GH_AW_SAFE_OUTPUTS_STAGED (if in trial mode) |
| 158 | + // - GH_AW_TARGET_REPO_SLUG (if in trial mode and no step override) |
| 159 | + // - GH_AW_SAFE_OUTPUT_MESSAGES |
| 160 | + |
| 161 | + return customEnvVars |
| 162 | +} |
| 163 | + |
| 164 | +// buildEngineMetadataEnvVars builds engine metadata environment variables (id, version, model) |
| 165 | +// These are used by the JavaScript footer generation to create XML comment markers for traceability |
| 166 | +func buildEngineMetadataEnvVars(engineConfig *EngineConfig) []string { |
| 167 | + var customEnvVars []string |
| 168 | + |
| 169 | + if engineConfig == nil { |
| 170 | + return customEnvVars |
| 171 | + } |
| 172 | + |
| 173 | + safeOutputsEnvLog.Printf("Building engine metadata env vars: id=%s, version=%s", engineConfig.ID, engineConfig.Version) |
| 174 | + |
| 175 | + // Add engine ID if present |
| 176 | + if engineConfig.ID != "" { |
| 177 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ENGINE_ID: %q\n", engineConfig.ID)) |
| 178 | + } |
| 179 | + |
| 180 | + // Add engine version if present |
| 181 | + if engineConfig.Version != "" { |
| 182 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ENGINE_VERSION: %q\n", engineConfig.Version)) |
| 183 | + } |
| 184 | + |
| 185 | + // Add engine model if present |
| 186 | + if engineConfig.Model != "" { |
| 187 | + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_ENGINE_MODEL: %q\n", engineConfig.Model)) |
| 188 | + } |
| 189 | + |
| 190 | + return customEnvVars |
| 191 | +} |
| 192 | + |
| 193 | +// ======================================== |
| 194 | +// Safe Output Environment Helpers |
| 195 | +// ======================================== |
| 196 | + |
| 197 | +// addCustomSafeOutputEnvVars adds custom environment variables to safe output job steps |
| 198 | +func (c *Compiler) addCustomSafeOutputEnvVars(steps *[]string, data *WorkflowData) { |
| 199 | + if data.SafeOutputs != nil && len(data.SafeOutputs.Env) > 0 { |
| 200 | + for key, value := range data.SafeOutputs.Env { |
| 201 | + *steps = append(*steps, fmt.Sprintf(" %s: %s\n", key, value)) |
| 202 | + } |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +// addSafeOutputGitHubTokenForConfig adds github-token to the with section, preferring per-config token over global |
| 207 | +// Uses precedence: config token > safe-outputs global github-token > GH_AW_GITHUB_TOKEN || GITHUB_TOKEN |
| 208 | +func (c *Compiler) addSafeOutputGitHubTokenForConfig(steps *[]string, data *WorkflowData, configToken string) { |
| 209 | + var safeOutputsToken string |
| 210 | + if data.SafeOutputs != nil { |
| 211 | + safeOutputsToken = data.SafeOutputs.GitHubToken |
| 212 | + } |
| 213 | + |
| 214 | + // If app is configured, use app token |
| 215 | + if data.SafeOutputs != nil && data.SafeOutputs.GitHubApp != nil { |
| 216 | + *steps = append(*steps, " github-token: ${{ steps.safe-outputs-app-token.outputs.token }}\n") |
| 217 | + return |
| 218 | + } |
| 219 | + |
| 220 | + // Choose the first non-empty custom token for precedence |
| 221 | + effectiveCustomToken := configToken |
| 222 | + if effectiveCustomToken == "" { |
| 223 | + effectiveCustomToken = safeOutputsToken |
| 224 | + } |
| 225 | + |
| 226 | + // Get effective token |
| 227 | + effectiveToken := getEffectiveSafeOutputGitHubToken(effectiveCustomToken) |
| 228 | + *steps = append(*steps, fmt.Sprintf(" github-token: %s\n", effectiveToken)) |
| 229 | +} |
| 230 | + |
| 231 | +// addSafeOutputCopilotGitHubTokenForConfig adds github-token to the with section for Copilot-related operations |
| 232 | +// Uses precedence: config token > safe-outputs global github-token > COPILOT_GITHUB_TOKEN |
| 233 | +func (c *Compiler) addSafeOutputCopilotGitHubTokenForConfig(steps *[]string, data *WorkflowData, configToken string) { |
| 234 | + var safeOutputsToken string |
| 235 | + if data.SafeOutputs != nil { |
| 236 | + safeOutputsToken = data.SafeOutputs.GitHubToken |
| 237 | + } |
| 238 | + |
| 239 | + // If app is configured, use app token |
| 240 | + if data.SafeOutputs != nil && data.SafeOutputs.GitHubApp != nil { |
| 241 | + *steps = append(*steps, " github-token: ${{ steps.safe-outputs-app-token.outputs.token }}\n") |
| 242 | + return |
| 243 | + } |
| 244 | + |
| 245 | + // Choose the first non-empty custom token for precedence |
| 246 | + effectiveCustomToken := configToken |
| 247 | + if effectiveCustomToken == "" { |
| 248 | + effectiveCustomToken = safeOutputsToken |
| 249 | + } |
| 250 | + |
| 251 | + // Get effective token |
| 252 | + effectiveToken := getEffectiveCopilotRequestsToken(effectiveCustomToken) |
| 253 | + *steps = append(*steps, fmt.Sprintf(" github-token: %s\n", effectiveToken)) |
| 254 | +} |
| 255 | + |
| 256 | +// addSafeOutputAgentGitHubTokenForConfig adds github-token to the with section for agent assignment operations |
| 257 | +// Uses precedence: config token > safe-outputs token > GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN |
| 258 | +// This is specifically for assign-to-agent operations which require elevated permissions. |
| 259 | +// |
| 260 | +// Note: GitHub App tokens are intentionally NOT used here, even when github-app: is configured. |
| 261 | +// The Copilot assignment API only accepts PATs (fine-grained or classic), not GitHub App |
| 262 | +// installation tokens. Callers must provide an explicit github-token or rely on GH_AW_AGENT_TOKEN. |
| 263 | +func (c *Compiler) addSafeOutputAgentGitHubTokenForConfig(steps *[]string, data *WorkflowData, configToken string) { |
| 264 | + // Get safe-outputs level token |
| 265 | + var safeOutputsToken string |
| 266 | + if data.SafeOutputs != nil { |
| 267 | + safeOutputsToken = data.SafeOutputs.GitHubToken |
| 268 | + } |
| 269 | + |
| 270 | + // Choose the first non-empty custom token for precedence |
| 271 | + effectiveCustomToken := configToken |
| 272 | + if effectiveCustomToken == "" { |
| 273 | + effectiveCustomToken = safeOutputsToken |
| 274 | + } |
| 275 | + |
| 276 | + // Get effective token - falls back to ${{ secrets.GH_AW_AGENT_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} |
| 277 | + // when no explicit token is provided. GitHub App tokens are never used here because the |
| 278 | + // Copilot assignment API rejects them. |
| 279 | + effectiveToken := getEffectiveCopilotCodingAgentGitHubToken(effectiveCustomToken) |
| 280 | + *steps = append(*steps, fmt.Sprintf(" github-token: %s\n", effectiveToken)) |
| 281 | +} |
| 282 | + |
| 283 | +// buildAllowedReposEnvVar builds an allowed-repos environment variable line for safe-output jobs. |
| 284 | +// envVarName should be the full env var name like "GH_AW_ALLOWED_REPOS". |
| 285 | +// Returns an empty slice if allowedRepos is empty. |
| 286 | +func buildAllowedReposEnvVar(envVarName string, allowedRepos []string) []string { |
| 287 | + if len(allowedRepos) == 0 { |
| 288 | + return nil |
| 289 | + } |
| 290 | + reposStr := strings.Join(allowedRepos, ",") |
| 291 | + return []string{fmt.Sprintf(" %s: %q\n", envVarName, reposStr)} |
| 292 | +} |
0 commit comments