From a2b7712438f3da87e3f6fa2bd8dccf2c4555f1bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:22:05 +0000 Subject: [PATCH 1/4] Initial plan From 6d95509ee6f6f782a5b6e3f2180d706dd4673cb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:31:09 +0000 Subject: [PATCH 2/4] Retain jobs.*.permissions for built-in safe_outputs and conclusion jobs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...uiltin_job_permissions_integration_test.go | 68 +++++++++++++++++++ pkg/workflow/compiler_custom_jobs.go | 42 +++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 pkg/workflow/builtin_job_permissions_integration_test.go diff --git a/pkg/workflow/builtin_job_permissions_integration_test.go b/pkg/workflow/builtin_job_permissions_integration_test.go new file mode 100644 index 00000000000..e38c5c0381d --- /dev/null +++ b/pkg/workflow/builtin_job_permissions_integration_test.go @@ -0,0 +1,68 @@ +package workflow + +import ( + "os" + "path/filepath" + "testing" + + "github.com/goccy/go-yaml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/github/gh-aw/pkg/testutil" +) + +// TestBuiltinJobPermissionsAugmentation verifies that user-declared permissions under +// jobs..permissions (e.g. safe_outputs, conclusion) are merged additively into the +// compiled built-in jobs, so scopes such as id-token: write are retained in the lock file. +func TestBuiltinJobPermissionsAugmentation(t *testing.T) { + tmpDir := testutil.TempDir(t, "builtin-job-permissions-augmentation") + compiler := NewCompiler() + + workflowContent := `--- +on: + issue_comment: + types: [created] +engine: copilot +strict: false +permissions: + contents: read + id-token: write +safe-outputs: + add-comment: +jobs: + safe_outputs: + permissions: + id-token: write + contents: read + issues: write + conclusion: + permissions: + id-token: write + contents: read + issues: write +--- +Builtin job permissions augmentation +` + + workflowFile := filepath.Join(tmpDir, "builtin-job-permissions-augmentation.md") + require.NoError(t, os.WriteFile(workflowFile, []byte(workflowContent), 0644)) + require.NoError(t, compiler.CompileWorkflow(workflowFile)) + + lockFile := filepath.Join(tmpDir, "builtin-job-permissions-augmentation.lock.yml") + lockBytes, err := os.ReadFile(lockFile) + require.NoError(t, err) + + var lock map[string]any + require.NoError(t, yaml.Unmarshal(lockBytes, &lock)) + jobs, ok := lock["jobs"].(map[string]any) + require.True(t, ok) + + for _, jobName := range []string{"safe_outputs", "conclusion"} { + job, ok := jobs[jobName].(map[string]any) + require.True(t, ok, "expected %s job in compiled workflow", jobName) + perms, ok := job["permissions"].(map[string]any) + require.True(t, ok, "expected %s permissions to be a map", jobName) + assert.Equal(t, "write", perms["id-token"], "%s should retain id-token: write from jobs.%s.permissions", jobName, jobName) + } +} diff --git a/pkg/workflow/compiler_custom_jobs.go b/pkg/workflow/compiler_custom_jobs.go index 057517d11c1..0d092f2caf8 100644 --- a/pkg/workflow/compiler_custom_jobs.go +++ b/pkg/workflow/compiler_custom_jobs.go @@ -775,7 +775,8 @@ func (c *Compiler) applyBuiltinJobAugmentations(data *WorkflowData) error { if err != nil { return err } - if len(augmentedNeeds) == 0 && augmentedIf == "" { + _, hasPermissions := configMap["permissions"] + if len(augmentedNeeds) == 0 && augmentedIf == "" && !hasPermissions { continue } @@ -784,13 +785,23 @@ func (c *Compiler) applyBuiltinJobAugmentations(data *WorkflowData) error { // Report the actual field(s) the author configured so they can identify the problem. augmentedField := configuredJobName + ".needs" if len(augmentedNeeds) == 0 { - augmentedField = configuredJobName + ".if" - } else if augmentedIf != "" { + if augmentedIf != "" { + augmentedField = configuredJobName + ".if" + } else { + augmentedField = configuredJobName + ".permissions" + } + } else if augmentedIf != "" || hasPermissions { augmentedField = configuredJobName } return fmt.Errorf("jobs.%s: cannot augment %q because this workflow does not generate that job", augmentedField, targetJobName) } + if hasPermissions { + if err := applyBuiltinJobPermissionsAugmentation(configuredJobName, targetJobName, configMap, targetJob); err != nil { + return err + } + } + normalizedNeeds := make([]string, 0, len(augmentedNeeds)) for _, rawNeed := range augmentedNeeds { need := normalizeBuiltinJobAlias(rawNeed) @@ -840,7 +851,32 @@ func (c *Compiler) applyBuiltinJobAugmentations(data *WorkflowData) error { compilerJobsLog.Printf("Applied jobs.%s.needs augmentation to %q: %v", configuredJobName, targetJobName, normalizedNeeds) } } + return nil +} + +// applyBuiltinJobPermissionsAugmentation merges user-declared jobs..permissions +// into a compiler-generated built-in job (e.g. safe_outputs, conclusion). The merge is +// additive: the compiler-computed permissions are preserved and the user's declared scopes +// are added on top, with write overriding read. This ensures scopes such as id-token: write +// that authors declare under jobs.*.permissions are retained in the compiled lock file rather +// than being dropped by the minimal least-privilege permission computation. +func applyBuiltinJobPermissionsAugmentation(configuredJobName, targetJobName string, configMap map[string]any, targetJob *Job) error { + permissionsValue, exists := configMap["permissions"] + if !exists || permissionsValue == nil { + return nil + } + + userPermissions := NewPermissionsParserFromValue(permissionsValue).ToPermissions() + if userPermissions == nil { + return nil + } + // Start from the compiler-computed permissions already rendered on the job, then merge + // the user-declared permissions additively so no compiler-required scope is lost. + merged := NewPermissionsParser(targetJob.Permissions).ToPermissions() + merged.Merge(userPermissions) + targetJob.Permissions = merged.RenderToYAML() + compilerJobsLog.Printf("Applied jobs.%s.permissions augmentation to %q", configuredJobName, targetJobName) return nil } From 813ab29e11f13807acfc1b73bfeda82ba69528ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:47:41 +0000 Subject: [PATCH 3/4] docs(adr): draft ADR-50642 for additive permissions merge on built-in jobs --- ...tive-permissions-merge-for-builtin-jobs.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/adr/50642-additive-permissions-merge-for-builtin-jobs.md diff --git a/docs/adr/50642-additive-permissions-merge-for-builtin-jobs.md b/docs/adr/50642-additive-permissions-merge-for-builtin-jobs.md new file mode 100644 index 00000000000..dc7069e0df2 --- /dev/null +++ b/docs/adr/50642-additive-permissions-merge-for-builtin-jobs.md @@ -0,0 +1,43 @@ +# ADR-50642: Additive Permissions Merge for Built-in Jobs + +**Date**: 2026-08-05 +**Status**: Draft +**Deciders**: Unknown + +--- + +### Context + +The workflow compiler generates built-in jobs (`safe_outputs`, `conclusion`) with a least-privilege permission set computed automatically. Authors sometimes need scopes beyond this baseline — most notably `id-token: write` for OIDC token minting — and express these by declaring a `permissions` block under `jobs.` in their workflow file. Prior to this change, `applyBuiltinJobAugmentations` only consulted `needs` and `if` fields from a built-in job's config entry; a `permissions` block was silently skipped, causing the declared scopes to be absent from the compiled lock file. This caused OIDC token minting to fail at runtime with "Missing id-token permission" errors despite the author having explicitly declared `id-token: write`. + +### Decision + +We will support user-declared `permissions` blocks under `jobs.` entries in `applyBuiltinJobAugmentations`, merging them **additively** with the compiler-computed least-privilege permissions. The merge applies "write overrides read" semantics and preserves all compiler-required scopes. A new helper `applyBuiltinJobPermissionsAugmentation` encapsulates the merge logic and is invoked before the existing `needs`/`if` augmentation path. + +### Alternatives Considered + +#### Alternative 1: Reject permissions blocks on built-in jobs (error on detection) + +Treat any `permissions:` key under a built-in job config entry as a validation error. Authors would be required to rely solely on the compiler's least-privilege computation and could not extend it. This is the safest option for minimizing permission surface, but it makes OIDC-dependent workflows impossible to author without the compiler growing built-in knowledge of every OIDC-related scope. + +#### Alternative 2: Full replacement of compiler-computed permissions + +Allow the user's declared permissions to entirely replace the compiler-computed set, giving authors complete control. This is simpler to implement but risks silently dropping required scopes (e.g., `issues: write` that `safe_outputs` needs to post comments), which would cause different runtime failures. The additive approach avoids this regression. + +### Consequences + +#### Positive +- OIDC-dependent workflows that declare `id-token: write` under `jobs.safe_outputs.permissions` or `jobs.conclusion.permissions` now compile correctly and retain the scope in the lock file. +- Additive merge preserves all compiler-required scopes; no existing workflow is affected by this change (the new path is only entered when a `permissions` block is present). + +#### Negative +- Authors can widen the permission surface of compiler-generated built-in jobs beyond the least-privilege baseline, potentially granting scopes that are not strictly needed at runtime. +- The compiler gains a new augmentation code path for permissions that must be kept in sync with future changes to built-in job generation and the `Permissions.Merge` / `RenderToYAML` helpers. + +#### Neutral +- The error message for invalid built-in job augmentation now also covers the `.permissions` field, improving diagnostics when a user references a built-in job that the workflow does not generate. +- The integration test (`builtin_job_permissions_integration_test.go`) compiles a real workflow fixture and asserts lock-file contents, establishing a regression guard for this behavior. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From 8a96c0169f281447dcc36fea5696eac4fa500771 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:28:16 +0000 Subject: [PATCH 4/4] chore: plan CI failure triage and fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/discussion-task-miner.lock.yml | 2 +- pkg/actionpins/data/action_pins.json | 10 +++++----- pkg/workflow/data/action_pins.json | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/discussion-task-miner.lock.yml b/.github/workflows/discussion-task-miner.lock.yml index 4f74ed95383..4e32bca860d 100644 --- a/.github/workflows/discussion-task-miner.lock.yml +++ b/.github/workflows/discussion-task-miner.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a33ed75d7a3b3250217e65bb04247452c200e0e1a6011929441296ebe2f8b331","body_hash":"30a2a145f9a81af5f4df91cb34aaee48c6671db9721c0f6bd19dd73c859f0663","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77","copilot-sdk":"1.0.8"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43","digest":"sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43@sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.44","digest":"sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.44@sha256:c064d15974f7c933ec7d3f7b4038f4fd203547b3154bdc821afd379144887eff"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.8","digest":"sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.8@sha256:38bbea36cdb46a3c9d04d1db05e672966f5239b431a2022eb35881688e5721d8"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ diff --git a/pkg/actionpins/data/action_pins.json b/pkg/actionpins/data/action_pins.json index b1b9be1fbba..1aeb18aac35 100644 --- a/pkg/actionpins/data/action_pins.json +++ b/pkg/actionpins/data/action_pins.json @@ -165,6 +165,11 @@ } }, "containers": { + "ghcr.io/fabio-rovai/open-ontologies:latest": { + "image": "ghcr.io/fabio-rovai/open-ontologies:latest", + "digest": "sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530", + "pinned_image": "ghcr.io/fabio-rovai/open-ontologies:latest@sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530" + }, "ghcr.io/github/gh-aw-firewall/agent:0.27.43": { "image": "ghcr.io/github/gh-aw-firewall/agent:0.27.43", "digest": "sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6", @@ -264,11 +269,6 @@ "image": "python:alpine", "digest": "sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92", "pinned_image": "python:alpine@sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92" - }, - "ghcr.io/fabio-rovai/open-ontologies:latest": { - "image": "ghcr.io/fabio-rovai/open-ontologies:latest", - "digest": "sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530", - "pinned_image": "ghcr.io/fabio-rovai/open-ontologies:latest@sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530" } } } diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index b1b9be1fbba..1aeb18aac35 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -165,6 +165,11 @@ } }, "containers": { + "ghcr.io/fabio-rovai/open-ontologies:latest": { + "image": "ghcr.io/fabio-rovai/open-ontologies:latest", + "digest": "sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530", + "pinned_image": "ghcr.io/fabio-rovai/open-ontologies:latest@sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530" + }, "ghcr.io/github/gh-aw-firewall/agent:0.27.43": { "image": "ghcr.io/github/gh-aw-firewall/agent:0.27.43", "digest": "sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6", @@ -264,11 +269,6 @@ "image": "python:alpine", "digest": "sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92", "pinned_image": "python:alpine@sha256:26730869004e2b9c4b9ad09cab8625e81d256d1ce97e72df5520e806b1709f92" - }, - "ghcr.io/fabio-rovai/open-ontologies:latest": { - "image": "ghcr.io/fabio-rovai/open-ontologies:latest", - "digest": "sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530", - "pinned_image": "ghcr.io/fabio-rovai/open-ontologies:latest@sha256:2932c10682eac29ccf840a6bd6c4c7c82c5ce770ad9e94697d44057187452530" } } }