Skip to content

Retain jobs.*.permissions for built-in safe_outputs and conclusion jobs - #50642

Open
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/fix-permissions-id-token
Open

Retain jobs.*.permissions for built-in safe_outputs and conclusion jobs#50642
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/fix-permissions-id-token

Conversation

Copilot AI commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Permissions declared under jobs.<built-in>.permissions (e.g. safe_outputs, conclusion) were dropped from the compiled lock file, since built-in job permissions come solely from least-privilege computation and the jobs.<builtin> block was only consulted for needs/if. Scopes like id-token: write went missing, breaking OIDC token minting.

Changes

  • compiler_custom_jobs.goapplyBuiltinJobAugmentations now also detects a permissions block under jobs.<builtin> and merges it into the built-in job via a new helper applyBuiltinJobPermissionsAugmentation. The merge is additive over the compiler-computed permissions (write overrides read), so no compiler-required scope is lost.
  • Error path — the "cannot augment ... job does not generate" message now reports .permissions when that is the only field configured.
  • Testbuiltin_job_permissions_integration_test.go compiles the issue scenario and asserts id-token: write survives on both safe_outputs and conclusion.

Example

permissions:
  contents: read
  id-token: write
safe-outputs:
  add-comment:
jobs:
  safe_outputs:
    permissions:
      id-token: write
  conclusion:
    permissions:
      id-token: write

Compiled safe_outputs / conclusion now include:

permissions:
  contents: read
  id-token: write
  issues: write
  pull-requests: write

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix permissions for id-token in safe_outputs and conclusion Retain jobs.*.permissions for built-in safe_outputs and conclusion jobs Aug 5, 2026
@pelikhan
pelikhan marked this pull request as ready for review August 5, 2026 17:40
Copilot AI balanced review requested due to automatic review settings August 5, 2026 17:40
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Copilot AI requested a review from pelikhan August 5, 2026 17:43

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation is correct and well-tested. applyBuiltinJobPermissionsAugmentation uses the existing Merge API correctly — compiler-computed permissions are preserved and user-declared scopes (e.g. id-token: write) are layered on top additively. The error-path logic for augmentedField reporting is correctly extended to cover the .permissions-only case. The integration test covers the stated scenario end-to-end. No blocking issues found.> 🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 25.8 AIC · ⌖ 7.4 AIC · ⊞ 5.4K

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (107 new lines in pkg/) but does not yet have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/50642-additive-permissions-merge-for-builtin-jobs.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch at docs/adr/50642-additive-permissions-merge-for-builtin-jobs.md — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-50642: Additive Permissions Merge for Built-in Jobs

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 43.4 AIC · ⌖ 19.1 AIC · ⊞ 4.4K ·
Comment /review to run again

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 70/100 — Acceptable

Analyzed 1 test(s): 1 design, 0 implementation, 1 violation.

📊 Metrics (1 test)
Metric Value
Analyzed 1 (Go: 1, JS: 0)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 0 (0%)
Duplicate clusters 0
Inflation No (68 test lines / 42 prod lines = 1.6:1)
🚨 Violations 1 (missing build tag)
Test File Classification Issues
TestBuiltinJobPermissionsAugmentation pkg/workflow/builtin_job_permissions_integration_test.go behavioral_contract, high_value, design_test Missing //go:build integration tag; happy-path only
⚠️ Flagged Tests (1)

TestBuiltinJobPermissionsAugmentation (pkg/workflow/builtin_job_permissions_integration_test.go:18) — Missing build tag (hard violation). The file is named _integration_test.go but the patch does not show //go:build integration on line 1. All Go test files must declare //go:build !integration (unit) or //go:build integration (integration) on line 1.

Additionally, the test covers only the happy-path regression scenario. Consider adding edge cases such as conflicting read/write merge precedence or an unknown built-in job name.

Fix: Add //go:build integration as line 1 (before the package declaration).

Verdict

Failed. 0% implementation tests (threshold: 30%) ✅ — but 1 hard violation: missing //go:build build tag on new integration test file.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 39.3 AIC · ⌖ 8.21 AIC · ⊞ 7.7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Test Quality Sentinel: 70/100. Hard violation: new integration test file builtin_job_permissions_integration_test.go is missing required //go:build integration build tag on line 1. Review flagged tests in the comment above.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /diagnosing-bugs — commenting, no blocking issues but three improvements recommended.

📋 Key Themes & Highlights

Key Themes

  1. Unreachable nil guard (compiler_custom_jobs.go:873) — ToPermissions() never returns nil; the guard silently swallows invalid permissions values.
  2. Test doesn't isolate the actual regression (builtin_job_permissions_integration_test.go:50) — the test fixture declares id-token: write at both the top-level and job level, so the compiler may compute it anyway; the test doesn't prove the augmentation path is what preserved it.
  3. Error message ambiguity (compiler_custom_jobs.go:793) — when needs + permissions are both set on a non-existent job, the error field collapses to the bare job name instead of naming the offending fields.

Positive Highlights

  • ✅ Additive merge semantics are correct — compiler-computed scopes are preserved, user scopes added on top.
  • ✅ Clean extraction into a standalone applyBuiltinJobPermissionsAugmentation helper makes the logic independently testable.
  • hasPermissions detection is correctly placed before the early-continue, so the early-exit is only skipped when needed.
  • ✅ Integration test exercises the full compile path end-to-end.
> 🧠 *Reviewed using Matt Pocock's skills by [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/runs/31031117809)* · sonnet46 · 42.2 AIC · ⌖ 8.17 AIC · ⊞ 7.1K > Comment /matt to run again

if userPermissions == nil {
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] userPermissions == nil is unreachable — ToPermissions() always returns a non-nil *Permissions. This guard silently swallows a misconfigured value (e.g. permissions: true) as a no-op instead of surfacing an error.

💡 Suggested fix

Either remove the nil check, or validate with an explicit empty/invalid check and return an error:

userPermissions := NewPermissionsParserFromValue(permissionsValue).ToPermissions()
// Remove the nil guard; ToPermissions() always returns non-nil

If you want to guard against a scalar value being passed, detect that before calling ToPermissions().

@copilot please address this.


workflowFile := filepath.Join(tmpDir, "builtin-job-permissions-augmentation.md")
require.NoError(t, os.WriteFile(workflowFile, []byte(workflowContent), 0644))
require.NoError(t, compiler.CompileWorkflow(workflowFile))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test only asserts the happy path where the user-declared permissions match the compiler-computed ones. It doesn't cover the additive-merge case where the user declares a scope not already present in the compiler-computed permissions (the core bug scenario: id-token: write being dropped because the compiler doesn't compute it).

💡 Suggested additional assertion

Add a separate sub-test (or extend the existing one) where the compiler-computed job would have contents: read, issues: write but id-token is absent, then assert that after augmentation id-token: write appears. For example:

// workflow only declares id-token at job level, not at top level
// compiler should NOT compute id-token — verify it is added by augmentation
assert.Equal(t, "write", perms["id-token"], "id-token should be injected by augmentation, not compiler")

This is the actual regression the PR is fixing; the current test doesn't isolate it.

@copilot please address this.

} else {
augmentedField = configuredJobName + ".permissions"
}
} else if augmentedIf != "" || hasPermissions {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The error-field reporting when needs is set has ambiguous logic. When len(augmentedNeeds) > 0 and hasPermissions is true but augmentedIf == "", the field is reported as just configuredJobName (no sub-field), which hides which specific field caused the augmentation attempt.

💡 Details

The original code reported the exact field (needs, if, or the combined job name). The new || hasPermissions in else if augmentedIf != "" || hasPermissions means that when only needs and permissions are set (no if), the error message points to the bare job name rather than naming the offending field. Consider listing all present fields explicitly, e.g. configuredJobName + ".needs+permissions", or enumerating them for clarity in the error.

@copilot please address this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds retention of user-declared permissions for generated built-in jobs.

Changes:

  • Merges built-in job permissions with compiler-computed permissions.
  • Improves missing-job error reporting.
  • Adds integration coverage for OIDC permissions.
Show a summary per file
File Description
pkg/workflow/compiler_custom_jobs.go Implements permission augmentation and errors.
pkg/workflow/builtin_job_permissions_integration_test.go Tests retained OIDC permissions.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Suppressed comments (1)

pkg/workflow/compiler_custom_jobs.go:792

  • When both .if and .permissions are configured and the target job is absent, this branch reports only .if, even though the nearby comment and PR description require reporting the configured field(s). Use the job path when multiple augmentation fields are present, and a field-specific path only when exactly one is present.
				if augmentedIf != "" {
					augmentedField = configuredJobName + ".if"
				} else {
					augmentedField = configuredJobName + ".permissions"
				}
  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +869 to +872
userPermissions := NewPermissionsParserFromValue(permissionsValue).ToPermissions()
if userPermissions == nil {
return nil
}
Comment on lines +778 to +779
_, hasPermissions := configMap["permissions"]
if len(augmentedNeeds) == 0 && augmentedIf == "" && !hasPermissions {
Comment on lines +64 to +66
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)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Retain jobs.*.permissions for built-in safe_outputs/conclusion jobs

No blocking issues found. The additive merge (write-over-read, via Permissions.Merge) correctly preserves compiler-computed least-privilege scopes while layering in user-declared ones, and the new integration test exercises the exact regression scenario from the linked issue.

💡 Verification notes

I independently compiled several edge-case workflows to stress-test the merge logic:

  • permissions: write-all shorthand on a built-in job → expands correctly, all compiler scopes retained at write.
  • issues: none on a scope the compiler had already set to write → correctly downgraded to none (author intent honored).
  • actions: none / models: none on scopes the compiler never touched → correctly added as none.
  • Invalid types (permissions: 123, all: write) are already rejected by the JSON schema before this code path runs, so no schema hole here.

The error-message branch in applyBuiltinJobAugmentations (deciding whether to report .needs, .if, .permissions, or the bare job name when the job does not exist) is a little convoluted with the added hasPermissions condition, but it is only diagnostic text and was verified to produce sensible output for all combinations I tried — not blocking.

> 🔎 *Code quality review by [PR Code Quality Reviewer](https://github.com/github/gh-aw/actions/runs/31031118062)* · auto · 142.9 AIC · ⌖ 4.46 AIC · ⊞ 7.9K > Comment /review to run again

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please address the remaining blockers on this PR:

  • add the required ADR link in the PR body for docs/adr/50642-additive-permissions-merge-for-builtin-jobs.md
  • fix the blocking review about the missing //go:build integration tag in pkg/workflow/builtin_job_permissions_integration_test.go
  • then run the pr-finisher skill

Run: https://github.com/github/gh-aw/actions/runs/31034308996

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 27.8 AIC · ⌖ 5.07 AIC · ⊞ 8.3K ·
Comment /souschef to run again

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR Triage: #50642

Category: bug | Risk: high | Priority Score: 82/100 (impact 45, urgency 22, quality 15)
Recommended action: fast_track
Fixes dropped OIDC permissions (id-token: write) on built-in jobs — critical-path compiler fix, well tested.

Generated by 🔧 PR Triage Agent · auto · 45.2 AIC · ⌖ 2.9 AIC · ⊞ 8K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

jobs.*.permissions.id-token dropped from compiled lock for safe_outputs and conclusion

4 participants