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: 2 additions & 0 deletions pkg/workflow/compiler_jobs_engine_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ func TestBuildMainJobEngineEnvNeedsExpression(t *testing.T) {
// references its outputs; without this, needs.provide_value_to_agent would be undefined.
assert.Contains(t, job.Needs, "provide_value_to_agent",
"agent job must directly depend on provide_value_to_agent referenced in engine.env")
assert.Contains(t, workflowData.AgentJobNeeds, "provide_value_to_agent",
"agent dependencies must be available while generating safe-outputs config")
assert.Contains(t, job.Needs, string(constants.ActivationJobName),
"agent job must also depend on activation")
}
Expand Down
6 changes: 2 additions & 4 deletions pkg/workflow/compiler_main_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) (
compilerMainJobLog.Print("Adding runtime-paths step for safe-outputs")
steps = append(steps, c.generateSetRuntimePathsStep()...)
}

jobCondition := c.buildMainJobCondition(data, activationJobCreated)
depends, engineEnvContent := c.buildMainJobDependencies(data, activationJobCreated)
c.warnBuiltinJobEnvReferences(depends, engineEnvContent)

// Build agent step content (checkout app tokens minted here to avoid masked-value drops).
var stepBuilder strings.Builder
Expand All @@ -49,9 +50,6 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) (
steps = append(steps, stepsContent)
}

depends, engineEnvContent := c.buildMainJobDependencies(data, activationJobCreated)
c.warnBuiltinJobEnvReferences(depends, engineEnvContent)

outputs := c.buildMainJobOutputs(data)
env := c.buildMainJobEnv(data)
agentConcurrency := GenerateJobConcurrencyConfig(data)
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_main_job_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func (c *Compiler) buildMainJobDependencies(data *WorkflowData, activationJobCre
compilerMainJobLog.Printf("Added direct dependency on custom job '%s' because it's referenced in workflow content or engine.env", jobName)
}
}
data.AgentJobNeeds = depends
return depends, engineEnvContent
}

Expand Down
18 changes: 11 additions & 7 deletions pkg/workflow/safe_outputs_config_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"fmt"
"slices"
"sort"
"strings"

Expand Down Expand Up @@ -53,13 +54,16 @@ func generateSafeOutputsConfig(data *WorkflowData) (string, error) {
if len(safeOutputsConfig) == 0 {
return "", nil
}
// The agent job never depends on the custom jobs listed in safe-outputs.needs (those
// dependencies are only wired onto the later safe_outputs handler job by
// buildSafeOutputsJobNeeds). Any templated expression referencing needs.<job> for one of
// those jobs would therefore be unresolvable inside the agent job and trip an actionlint
// "undefined property" error. Neutralize such expressions here; the handler job's own copy
// of the config (GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG) is unaffected and keeps the real value.
sanitizeAgentSafeOutputsConfig(safeOutputsConfig, data.SafeOutputs.Needs)
// Custom jobs listed in safe-outputs.needs are wired onto the later safe_outputs handler job,
// not automatically onto the agent job. Neutralize expressions for those jobs unless the
// agent already directly depends on them; the handler job's own config is unaffected.
unresolvableJobs := make([]string, 0, len(data.SafeOutputs.Needs))
for _, job := range data.SafeOutputs.Needs {
if !slices.Contains(data.AgentJobNeeds, job) {
unresolvableJobs = append(unresolvableJobs, job)
}
}
sanitizeAgentSafeOutputsConfig(safeOutputsConfig, unresolvableJobs)
configJSON, err := marshalSafeOutputsConfig(safeOutputsConfig)
if err != nil {
return "", fmt.Errorf("marshaling safe-outputs config: %w", err)
Expand Down
17 changes: 13 additions & 4 deletions pkg/workflow/safe_outputs_config_generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,26 @@ func TestGenerateSafeOutputsConfigNeutralizesAllUnresolvableNeedsForms(t *testin

func TestGenerateSafeOutputsConfigPreservesResolvableNeedsExpressions(t *testing.T) {
data := &WorkflowData{
AgentJobNeeds: []string{"setup"},
SafeOutputs: &SafeOutputsConfig{
Needs: []string{"approval_allowlist"},
AddComments: &AddCommentsConfig{
AllowedCommentIDs: []string{"${{ needs.prepare.outputs.comment_ids }}"},
Needs: []string{"setup", "approval_allowlist"},
UpdatePullRequests: &UpdatePullRequestsConfig{
UpdateEntityConfig: UpdateEntityConfig{
SafeOutputTargetConfig: SafeOutputTargetConfig{
Target: "${{ needs.setup.outputs.pull_request_number }}",
},
},
},
ApproveWorkflowRun: &ApproveWorkflowRunConfig{
AllowedPullRequests: []string{"${{ needs.approval_allowlist.outputs.eligible_pull_request_numbers }}"},
},
},
}

result, err := generateSafeOutputsConfig(data)
require.NoError(t, err)
assert.Contains(t, result, "needs.prepare.outputs.comment_ids")
assert.Contains(t, result, "needs.setup.outputs.pull_request_number")
assert.NotContains(t, result, "needs.approval_allowlist")
}

func TestSanitizeAgentSafeOutputsConfigNoOp(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/workflow_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ type WorkflowData struct {
TopLevelGitHubApp *GitHubAppConfig // top-level github-app fallback for all nested github-app token minting operations
LockForAgent bool // whether to lock the issue during agent workflow execution
Jobs map[string]any // custom job configurations with dependencies
AgentJobNeeds []string // resolved direct dependencies of the agent job
Cache string // cache configuration
NeedsTextOutput bool // whether the workflow uses ${{ needs.task.outputs.text }}
NetworkPermissions *NetworkPermissions // parsed network permissions
Expand Down
Loading