From 1efbfe4ebed84f88afc554bf455433c9558c6958 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:37:51 +0000 Subject: [PATCH 1/4] Initial plan From c54f3cadaaedfd358e355140d76abd2bdf64bf0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:03:28 +0000 Subject: [PATCH 2/4] Fix missing id-token: write on activation, conclusion, safe_outputs jobs for OTLP OIDC auth When observability.otlp.github-app is configured with audience-only (credential-less OIDC mode, no app-id/private-key), three jobs were missing the id-token: write permission needed to call core.getIDToken(audience): - activation job (compiler_activation_permissions.go) - conclusion job (notify_comment.go) - safe_outputs job (compiler_safe_outputs_job.go) Also adds 'audience' as a valid property in the observability.otlp.github-app JSON schema, fixing a related schema validation error that blocked the audience-only config. Adds a regression test (otlp_oidc_permissions_test.go) that compiles a workflow with audience-only OTLP config and asserts all three jobs get id-token: write. Fixes #45755 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...atch-fix-otlp-oidc-id-token-permissions.md | 7 ++ pkg/parser/schema_test.go | 15 ++-- pkg/parser/schemas/main_workflow_schema.json | 4 + .../compiler_activation_permissions.go | 6 ++ pkg/workflow/compiler_safe_outputs_job.go | 6 ++ pkg/workflow/notify_comment.go | 9 ++- pkg/workflow/otlp_oidc_permissions_test.go | 81 +++++++++++++++++++ 7 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 .changeset/patch-fix-otlp-oidc-id-token-permissions.md create mode 100644 pkg/workflow/otlp_oidc_permissions_test.go diff --git a/.changeset/patch-fix-otlp-oidc-id-token-permissions.md b/.changeset/patch-fix-otlp-oidc-id-token-permissions.md new file mode 100644 index 00000000000..7d3e6022236 --- /dev/null +++ b/.changeset/patch-fix-otlp-oidc-id-token-permissions.md @@ -0,0 +1,7 @@ +--- +"gh-aw": patch +--- + +Fix missing `id-token: write` permission on activation, conclusion, and safe_outputs jobs when using OTLP OIDC audience-only authentication (`observability.otlp.github-app` with only an `audience`, no `app-id`/`private-key`). These jobs receive the OTLP OIDC mint step (`core.getIDToken`) at compile time but previously lacked the permission required to call the GitHub OIDC token API, causing `Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable` at runtime. + +Also adds `audience` as a valid field in the `observability.otlp.github-app` JSON schema so that credential-less OIDC mode can be configured without schema validation errors. diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index f9c0680a5fc..0c635a668c9 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -1375,9 +1375,9 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_OTLPResourceAttrib } } -func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_OTLPGitHubAppAudienceRejected(t *testing.T) { +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_OTLPGitHubAppAudienceAccepted(t *testing.T) { frontmatter := map[string]any{ - "name": "OTLP github-app audience rejection", + "name": "OTLP github-app audience acceptance", "on": map[string]any{ "issues": map[string]any{ "types": []any{"opened"}, @@ -1392,14 +1392,9 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_OTLPGitHubAppAudie }, } - err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/otlp-github-app-audience-reject-schema-test.md") - if err == nil { - t.Fatal("expected observability.otlp.github-app.audience to fail schema validation") - } - errText := err.Error() - if !strings.Contains(errText, "audience") || - (!strings.Contains(errText, "github-app") && !strings.Contains(errText, "Unknown property")) { - t.Fatalf("expected schema validation error to reference unsupported github-app.audience syntax, got: %v", err) + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/otlp-github-app-audience-accept-schema-test.md") + if err != nil { + t.Fatalf("expected observability.otlp.github-app.audience to pass schema validation (it is the credential-less OIDC audience field), got: %v", err) } } diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index ecf025e8dce..a78e0d4d10a 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -11174,6 +11174,10 @@ "ignore-if-missing": { "type": "boolean", "description": "If true, skip token minting when client-id/private-key resolve to empty strings at runtime. Defaults to false." + }, + "audience": { + "type": "string", + "description": "Optional OIDC audience passed to core.getIDToken(audience) when using credential-less OIDC mode (github-app present without app-id/private-key). Leave omitted to use the default audience." } }, "additionalProperties": false diff --git a/pkg/workflow/compiler_activation_permissions.go b/pkg/workflow/compiler_activation_permissions.go index 7a2ac698421..b5599ed47bd 100644 --- a/pkg/workflow/compiler_activation_permissions.go +++ b/pkg/workflow/compiler_activation_permissions.go @@ -150,6 +150,12 @@ func (c *Compiler) buildActivationBasePermissions(ctx *activationJobBuildContext statusCommentIncludesPullRequests: ctx.statusCommentPRs, statusCommentIncludesDiscussions: ctx.statusCommentDiscussions, }) + // When observability.otlp.github-app is configured without app-id/private-key + // credentials, id-token: write is needed so the activation job can mint the OTLP + // OIDC token via core.getIDToken(audience) (mirrors threat_detection_job.go). + if hasOTLPGitHubOIDCAuth(ctx.data.ParsedFrontmatter, ctx.data.RawFrontmatter) { + permsMap[PermissionIdToken] = PermissionWrite + } return permsMap } diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index f3f099b176a..3e4d36d2370 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -72,6 +72,12 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // Compute permissions and threat detection flag up front; both are used across phases. permissions := ComputePermissionsForSafeOutputs(data.SafeOutputs) + // When observability.otlp.github-app is configured without app-id/private-key + // credentials, id-token: write is needed so the safe_outputs job can mint the OTLP + // OIDC token via core.getIDToken(audience) (mirrors threat_detection_job.go). + if hasOTLPGitHubOIDCAuth(data.ParsedFrontmatter, data.RawFrontmatter) { + permissions.Set(PermissionIdToken, PermissionWrite) + } threatDetectionEnabled := IsDetectionJobEnabled(data.SafeOutputs) // Compute artifact prefix once; it is referenced in all three phases. diff --git a/pkg/workflow/notify_comment.go b/pkg/workflow/notify_comment.go index e4b59aa260c..b82cb64e272 100644 --- a/pkg/workflow/notify_comment.go +++ b/pkg/workflow/notify_comment.go @@ -74,12 +74,19 @@ func (c *Compiler) buildConclusionJob(data *WorkflowData, mainJobName string, sa } } notifyCommentLog.Printf("Job built successfully: dependencies_count=%d", len(needs)) + conclusionPerms := ComputePermissionsForSafeOutputs(data.SafeOutputs) + // When observability.otlp.github-app is configured without app-id/private-key + // credentials, id-token: write is needed so the conclusion job can mint the OTLP + // OIDC token via core.getIDToken(audience) (mirrors threat_detection_job.go). + if hasOTLPGitHubOIDCAuth(data.ParsedFrontmatter, data.RawFrontmatter) { + conclusionPerms.Set(PermissionIdToken, PermissionWrite) + } return &Job{ Name: "conclusion", If: RenderCondition(buildConclusionJobCondition(data, mainJobName, safeOutputJobNames)), RunsOn: c.formatFrameworkJobRunsOn(data), Environment: c.indentYAMLLines(resolveSafeOutputsEnvironment(data), " "), - Permissions: ComputePermissionsForSafeOutputs(data.SafeOutputs).RenderToYAML(), + Permissions: conclusionPerms.RenderToYAML(), Concurrency: c.buildConclusionJobConcurrency(data), Steps: steps, Needs: needs, diff --git a/pkg/workflow/otlp_oidc_permissions_test.go b/pkg/workflow/otlp_oidc_permissions_test.go new file mode 100644 index 00000000000..68814211e3c --- /dev/null +++ b/pkg/workflow/otlp_oidc_permissions_test.go @@ -0,0 +1,81 @@ +//go:build !integration + +package workflow + +import ( + "os" + "path/filepath" + "testing" + + "github.com/github/gh-aw/pkg/constants" + "github.com/github/gh-aw/pkg/stringutil" + "github.com/github/gh-aw/pkg/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestOTLPOIDCAudienceAllJobsHaveIDTokenWrite is a regression test for the bug where +// the activation, conclusion, and safe_outputs jobs were missing the id-token: write +// permission when observability.otlp.github-app was configured with audience-only +// (credential-less OIDC mode, no app-id/private-key). Each of these jobs receives the +// "Mint OTLP OIDC token" setup step (core.getIDToken) but previously lacked the +// permission required to call that API, causing: +// +// "Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable" +// +// The detection and evals jobs already had the fix; this test locks the invariant for +// the three that were missing it. +func TestOTLPOIDCAudienceAllJobsHaveIDTokenWrite(t *testing.T) { + tmpDir := testutil.TempDir(t, "otlp-oidc-perms-all-jobs") + testFile := filepath.Join(tmpDir, "otlp-oidc-audience.md") + + // Audience-only (no app-id/private-key) triggers the credential-less OIDC path. + // safe-outputs is required to generate the conclusion and safe_outputs jobs. + testContent := `--- +on: + issues: + types: [opened] +permissions: + contents: read + id-token: write +observability: + otlp: + endpoint: ${{ secrets.OTEL_ENDPOINT }} + github-app: + audience: https://otel.example.com +safe-outputs: + add-comment: +engine: copilot +--- + +# OTLP OIDC audience-only permissions regression test +` + require.NoError(t, os.WriteFile(testFile, []byte(testContent), 0644)) + + compiler := NewCompiler() + require.NoError(t, compiler.CompileWorkflow(testFile), "workflow should compile successfully") + + lockBytes, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile)) + require.NoError(t, err, "failed to read generated lock file") + lockContent := string(lockBytes) + + jobs := []string{ + string(constants.ActivationJobName), + "conclusion", + "safe_outputs", + } + for _, jobName := range jobs { + t.Run(jobName, func(t *testing.T) { + section := extractJobSection(lockContent, jobName) + require.NotEmpty(t, section, "job %q should be present in the compiled output", jobName) + + // Verify the OTLP OIDC mint step is present so that the permission is genuinely required. + assert.Contains(t, lockContent, "id: mint-otlp-oidc-token", + "compiled output should contain the OTLP OIDC mint step (id: mint-otlp-oidc-token)") + + // The core assertion: every job that receives the mint step must declare id-token: write. + assert.Contains(t, section, "id-token: write", + "job %q must have id-token: write when OTLP OIDC audience-only auth is configured", jobName) + }) + } +} From 20fabb5babed8cc443b8930599bd8e30a2356ec3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:04:42 +0000 Subject: [PATCH 3/4] Address code review: scope mint-step assertion to job section in test Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/otlp_oidc_permissions_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/workflow/otlp_oidc_permissions_test.go b/pkg/workflow/otlp_oidc_permissions_test.go index 68814211e3c..13650e06931 100644 --- a/pkg/workflow/otlp_oidc_permissions_test.go +++ b/pkg/workflow/otlp_oidc_permissions_test.go @@ -69,9 +69,10 @@ engine: copilot section := extractJobSection(lockContent, jobName) require.NotEmpty(t, section, "job %q should be present in the compiled output", jobName) - // Verify the OTLP OIDC mint step is present so that the permission is genuinely required. - assert.Contains(t, lockContent, "id: mint-otlp-oidc-token", - "compiled output should contain the OTLP OIDC mint step (id: mint-otlp-oidc-token)") + // Verify the OTLP OIDC mint step is present within this job's section so that + // the id-token: write permission is genuinely required for this specific job. + assert.Contains(t, section, "id: mint-otlp-oidc-token", + "job %q should contain the OTLP OIDC mint step (id: mint-otlp-oidc-token)", jobName) // The core assertion: every job that receives the mint step must declare id-token: write. assert.Contains(t, section, "id-token: write", From 6d00671447f6312a108e2f41a7805f8e845ece26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:37:35 +0000 Subject: [PATCH 4/4] Refresh generated OTLP audience docs Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- docs/public/editor/autocomplete-data.json | 5 +++++ docs/src/content/docs/reference/frontmatter-full.md | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/docs/public/editor/autocomplete-data.json b/docs/public/editor/autocomplete-data.json index e39ef772998..f8a84c16c67 100644 --- a/docs/public/editor/autocomplete-data.json +++ b/docs/public/editor/autocomplete-data.json @@ -2383,6 +2383,11 @@ "desc": "If true, skip token minting when client-id/private-key resolve to empty strings at runtime.", "enum": [true, false], "leaf": true + }, + "audience": { + "type": "string", + "desc": "Optional OIDC audience passed to core.getIDToken(audience) when using credential-less OIDC mode (github-app present without app-id/private-key). Leave omitted to use the default audience.", + "leaf": true } } } diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index eb694657e5b..0af666b95ae 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -9978,6 +9978,12 @@ observability: # (optional) ignore-if-missing: true + # Optional OIDC audience passed to core.getIDToken(audience) when using + # credential-less OIDC mode (github-app present without app-id/private-key). Leave + # omitted to use the default audience. + # (optional) + audience: "example-value" + # Rate limiting configuration to restrict how frequently users can trigger the # workflow. Helps prevent abuse and resource exhaustion from programmatically # triggered events.