-
Notifications
You must be signed in to change notification settings - Fork 414
Fix safe-outputs YAML corruption when OTLP header masking is enabled #37068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5071ce1
cdbaf26
1ecc9f8
7cd1c84
4f9fa25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,10 @@ import ( | |
|
|
||
| var consolidatedSafeOutputsJobLog = logger.New("workflow:compiler_safe_outputs_job") | ||
|
|
||
| // stepNameLinePrefix matches the canonical YAML line emitted by this compiler for | ||
| // step starts in job.Steps (6-space indent + "- name: "). | ||
| const stepNameLinePrefix = " - name: " | ||
|
|
||
| // buildConsolidatedSafeOutputsJob builds a single job containing all safe output operations | ||
| // as separate steps within that job. This reduces the number of jobs in the workflow | ||
| // while maintaining observability through distinct step names, IDs, and outputs. | ||
|
|
@@ -445,6 +449,15 @@ func (c *Compiler) buildSafeOutputsJobFromParts( | |
| countParentSpanID := setupParentSpanNeedsExpr(constants.ActivationJobName) | ||
| insertIndex += len(c.generateSetupStep(data, setupActionRef, SetupActionDestination, data.SafeOutputs != nil && data.SafeOutputs.UploadArtifact != nil, countTraceID, countParentSpanID)) | ||
| } | ||
| // Keep insertion index aligned with setup steps that may be injected between setup | ||
| // and artifact downloads. Count the entries each mask helper appends so the index | ||
| // stays self-consistent if either helper ever emits more than one step. | ||
| if isOTLPHeadersPresent(data) { | ||
| insertIndex += strings.Count(generateOTLPHeadersMaskStep(), stepNameLinePrefix) | ||
| } | ||
| if isOTLPAttributesPresent(data) { | ||
| insertIndex += strings.Count(generateOTLPAttributesMaskStep(), stepNameLinePrefix) | ||
| } | ||
|
|
||
| // Add artifact download steps count | ||
| insertIndex += len(buildAgentOutputDownloadSteps(agentArtifactPrefix, c.getActionPin)) | ||
|
|
@@ -469,6 +482,18 @@ func (c *Compiler) buildSafeOutputsJobFromParts( | |
|
|
||
| // Note: App token step must be inserted BEFORE shared checkout steps | ||
| // because those steps reference steps.safe-outputs-app-token.outputs.token | ||
| // | ||
| // The insertion index is line-oriented; if it lands in the middle of a | ||
| // multi-line run/with block, move it to the next step boundary. | ||
| for insertIndex < len(steps) && !strings.HasPrefix(steps[insertIndex], stepNameLinePrefix) { | ||
| insertIndex++ | ||
| } | ||
|
Comment on lines
+488
to
+490
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] If the loop exhausts 💡 Suggested guardA short diagnostic log after the loop would make this case visible: for insertIndex < len(steps) && !strings.HasPrefix(steps[insertIndex], stepNameLinePrefix) {
insertIndex++
}
if insertIndex == len(steps) {
consolidatedSafeOutputsJobLog.Printf(
"WARN: app-token insertion reached end of steps slice (len=%d); step ordering may be incorrect",
len(steps),
)
}This is a rare edge case today, but becomes more likely if future step sequences are ever added that push the final checkout step past the end of the current emission order. |
||
| if insertIndex == len(steps) { | ||
| consolidatedSafeOutputsJobLog.Printf( | ||
| "WARN: app-token insertion reached end of steps slice (len=%d); step ordering may be incorrect", | ||
| len(steps), | ||
| ) | ||
| } | ||
|
|
||
| // Insert app token steps | ||
| var newSteps []string | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -813,7 +813,141 @@ func TestGitHubAppWithPushToPRBranch(t *testing.T) { | |
| assert.Contains(t, stepsContent, "id: safe-outputs-app-token") | ||
| } | ||
|
|
||
| // TestJobWithGitHubAppWorkflowCallUsesTargetRepoNameFallback is a regression test verifying that | ||
| // TestGitHubAppTokenStepWithOTLPHeaders ensures the app token step insertion point | ||
| // stays aligned when OTLP header masking steps are present. | ||
| func TestGitHubAppTokenStepWithOTLPHeaders(t *testing.T) { | ||
| compiler := NewCompiler() | ||
| compiler.jobManager = NewJobManager() | ||
|
|
||
| workflowData := &WorkflowData{ | ||
| Name: "Test Workflow", | ||
| Env: `"env": | ||
| OTEL_EXPORTER_OTLP_HEADERS: "Authorization=******"`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The regression test only exercises the 💡 Suggested additionAdd a second test (or extend this one) with Env: `"env":
OTEL_EXPORTER_OTLP_HEADERS: "Authorization=******"
GH_AW_OTLP_ATTRIBUTES: "{\"service.name\":\"test\"}"`Without this, a regression in the attributes path would go undetected. |
||
| SafeOutputs: &SafeOutputsConfig{ | ||
| GitHubApp: &GitHubAppConfig{ | ||
| AppID: "${{ vars.ACTIONS_APP_ID }}", | ||
| PrivateKey: "${{ secrets.ACTIONS_PRIVATE_KEY }}", | ||
| }, | ||
| AddComments: &AddCommentsConfig{}, | ||
| AddLabels: &AddLabelsConfig{ | ||
| Allowed: []string{"bug"}, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test.md") | ||
| require.NoError(t, err, "Should successfully build job") | ||
| require.NotNil(t, job, "Job should not be nil") | ||
|
|
||
| stepsContent := strings.Join(job.Steps, "") | ||
| maskIndex := strings.Index(stepsContent, "Mask OTLP telemetry headers") | ||
| downloadIndex := strings.Index(stepsContent, "Download agent output artifact") | ||
| setupEnvEchoIndex := strings.Index(stepsContent, "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json") | ||
| tokenIndex := strings.Index(stepsContent, "Generate GitHub App token") | ||
|
|
||
| require.NotEqual(t, -1, maskIndex, "OTLP headers mask step should be present") | ||
| require.NotEqual(t, -1, downloadIndex, "agent output download step should be present") | ||
| require.NotEqual(t, -1, setupEnvEchoIndex, "setup-agent-output-env run command should be present") | ||
| require.NotEqual(t, -1, tokenIndex, "GitHub App token step should be present") | ||
|
|
||
| assert.Less(t, maskIndex, downloadIndex, "masking should happen before artifact download") | ||
| assert.Less(t, setupEnvEchoIndex, tokenIndex, "app token step must be inserted after setup-agent-output-env run block") | ||
| // Regression guard: app token insertion must not split setup-agent-output-env and leave | ||
| // an `echo ...` run line nested under a previous `with:` block. | ||
| assert.NotContains(t, stepsContent, "with:\n echo \"GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json\" >> \"$GITHUB_OUTPUT\"", | ||
| "app token step must not split setup-agent-output-env into a nested echo under with:") | ||
| } | ||
|
|
||
| // TestGitHubAppTokenStepWithOTLPAttributes ensures the app token step insertion point | ||
| // stays aligned when the OTLP attributes masking step is present (GH_AW_OTLP_ATTRIBUTES path). | ||
| func TestGitHubAppTokenStepWithOTLPAttributes(t *testing.T) { | ||
| compiler := NewCompiler() | ||
| compiler.jobManager = NewJobManager() | ||
|
|
||
| workflowData := &WorkflowData{ | ||
| Name: "Test Workflow", | ||
| Env: `"env": | ||
| GH_AW_OTLP_ATTRIBUTES: "{\"service.name\":\"test\"}"`, | ||
| SafeOutputs: &SafeOutputsConfig{ | ||
| GitHubApp: &GitHubAppConfig{ | ||
| AppID: "${{ vars.ACTIONS_APP_ID }}", | ||
| PrivateKey: "${{ secrets.ACTIONS_PRIVATE_KEY }}", | ||
| }, | ||
| AddComments: &AddCommentsConfig{}, | ||
| AddLabels: &AddLabelsConfig{ | ||
| Allowed: []string{"bug"}, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test.md") | ||
| require.NoError(t, err, "Should successfully build job") | ||
| require.NotNil(t, job, "Job should not be nil") | ||
|
|
||
| stepsContent := strings.Join(job.Steps, "") | ||
| attrMaskIndex := strings.Index(stepsContent, "Mask OTLP custom attribute values") | ||
| downloadIndex := strings.Index(stepsContent, "Download agent output artifact") | ||
| setupEnvEchoIndex := strings.Index(stepsContent, "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json") | ||
| tokenIndex := strings.Index(stepsContent, "Generate GitHub App token") | ||
|
|
||
| require.NotEqual(t, -1, attrMaskIndex, "OTLP attributes mask step should be present") | ||
| require.NotEqual(t, -1, downloadIndex, "agent output download step should be present") | ||
| require.NotEqual(t, -1, setupEnvEchoIndex, "setup-agent-output-env run command should be present") | ||
| require.NotEqual(t, -1, tokenIndex, "GitHub App token step should be present") | ||
|
|
||
| assert.Less(t, attrMaskIndex, downloadIndex, "attributes masking should happen before artifact download") | ||
| assert.Less(t, setupEnvEchoIndex, tokenIndex, "app token step must be inserted after setup-agent-output-env run block") | ||
| assert.NotContains(t, stepsContent, "with:\n echo \"GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json\" >> \"$GITHUB_OUTPUT\"", | ||
| "app token step must not split setup-agent-output-env into a nested echo under with:") | ||
| } | ||
|
|
||
| // TestGitHubAppTokenStepWithOTLPHeadersAndAttributes ensures the app token step insertion | ||
| // point stays aligned when both OTLP header masking and attribute masking steps are present | ||
| // (insertIndex is incremented twice — highest-risk composition). | ||
| func TestGitHubAppTokenStepWithOTLPHeadersAndAttributes(t *testing.T) { | ||
| compiler := NewCompiler() | ||
| compiler.jobManager = NewJobManager() | ||
|
|
||
| workflowData := &WorkflowData{ | ||
| Name: "Test Workflow", | ||
| Env: `"env": | ||
| OTEL_EXPORTER_OTLP_HEADERS: "Authorization=******" | ||
| GH_AW_OTLP_ATTRIBUTES: "{\"service.name\":\"test\"}"`, | ||
| SafeOutputs: &SafeOutputsConfig{ | ||
| GitHubApp: &GitHubAppConfig{ | ||
| AppID: "${{ vars.ACTIONS_APP_ID }}", | ||
| PrivateKey: "${{ secrets.ACTIONS_PRIVATE_KEY }}", | ||
| }, | ||
| AddComments: &AddCommentsConfig{}, | ||
| AddLabels: &AddLabelsConfig{ | ||
| Allowed: []string{"bug"}, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test.md") | ||
| require.NoError(t, err, "Should successfully build job") | ||
| require.NotNil(t, job, "Job should not be nil") | ||
|
|
||
| stepsContent := strings.Join(job.Steps, "") | ||
| headerMaskIndex := strings.Index(stepsContent, "Mask OTLP telemetry headers") | ||
| attrMaskIndex := strings.Index(stepsContent, "Mask OTLP custom attribute values") | ||
| downloadIndex := strings.Index(stepsContent, "Download agent output artifact") | ||
| setupEnvEchoIndex := strings.Index(stepsContent, "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json") | ||
| tokenIndex := strings.Index(stepsContent, "Generate GitHub App token") | ||
|
|
||
| require.NotEqual(t, -1, headerMaskIndex, "OTLP headers mask step should be present") | ||
| require.NotEqual(t, -1, attrMaskIndex, "OTLP attributes mask step should be present") | ||
| require.NotEqual(t, -1, downloadIndex, "agent output download step should be present") | ||
| require.NotEqual(t, -1, setupEnvEchoIndex, "setup-agent-output-env run command should be present") | ||
| require.NotEqual(t, -1, tokenIndex, "GitHub App token step should be present") | ||
|
|
||
| assert.Less(t, headerMaskIndex, downloadIndex, "header masking should happen before artifact download") | ||
| assert.Less(t, attrMaskIndex, downloadIndex, "attributes masking should happen before artifact download") | ||
| assert.Less(t, setupEnvEchoIndex, tokenIndex, "app token step must be inserted after setup-agent-output-env run block") | ||
| assert.NotContains(t, stepsContent, "with:\n echo \"GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json\" >> \"$GITHUB_OUTPUT\"", | ||
| "app token step must not split setup-agent-output-env into a nested echo under with:") | ||
| } | ||
| // a safe-output job compiled for a workflow_call trigger uses | ||
| // needs.activation.outputs.target_repo_name (repo name only, no owner prefix) as the repositories | ||
| // fallback for the GitHub App token mint step, instead of the full target_repo slug. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/diagnose] The
insertIndex++increments are load-bearing: each assumes the corresponding mask helper appends exactly one string entry to thestepsslice. The comment documents this ("Each mask helper currently appends one string entry"), but ifgenerateOTLPHeadersMaskStep()orgenerateOTLPAttributesMaskStep()ever grow to emit two entries (e.g. an added env-echo step), the arithmetic silently undercounts and the while-loop guard must compensate.💡 More robust alternative
The offset could be derived from the actual number of entries appended, keeping the arithmetic self-consistent regardless of how many lines each helper emits:
Or expose a step-count constant alongside each generator. Either approach removes the silent undercounting risk.