Summary
As of gh aw v0.58.3 and current main, safe-outputs.call-workflow still generates conditional uses: jobs without a job-level permissions: block.
GitHub validates reusable workflow calls against the caller job's declared permission envelope. When the selected worker requests any non-none permissions in nested jobs, GitHub rejects the generated call-* job before execution starts.
This issue is specifically about the compiler-generated call-* jobs created by safe-outputs.call-workflow. It is not about outer handwritten relay workflows or general reusable-workflow permission rules.
Why this still looks relevant after the recent gh aw update
The changelog entries after v0.58.1 add several safe-outputs and workflow_call fixes, but they do not address permission propagation for generated call-* jobs:
v0.58.2 fixes safe-output manifest accounting (#20899)
v0.58.2 fixes idle HTTP server disconnects in the safe-outputs MCP server (#20901)
v0.58.3 fixes artifact-prefix downloads in workflow_call downstream jobs (#21011)
Current main still contains the same buildCallWorkflowJobs() shape that creates uses: jobs with needs, if, uses, secrets: inherit, and with.payload, but no permissions:.
Current behavior
pkg/workflow/compiler_safe_output_jobs.go still generates call-workflow fan-out jobs like this:
callJob := &Job{
Name: jobName,
Needs: []string{"safe_outputs"},
If: fmt.Sprintf("needs.safe_outputs.outputs.call_workflow_name == '%s'", workflowName),
Uses: workflowPath,
SecretsInherit: true,
With: map[string]any{
"payload": "${{ needs.safe_outputs.outputs.call_workflow_payload }}",
},
}
That renders YAML like:
call-worker-docs:
needs: [safe_outputs]
if: needs.safe_outputs.outputs.call_workflow_name == 'worker-docs'
uses: ./.github/workflows/worker-docs.lock.yml
secrets: inherit
with:
payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }}
There is still no permissions: block on the caller job.
Why GitHub rejects this
gh aw writes top-level permissions: {} and relies on job-level permissions for actual access.
For reusable workflows, GitHub compares the caller job's declared permissions with the permissions requested by nested jobs in the called workflow. If the caller job omits permissions:, the nested worker jobs are effectively constrained to none and validation fails before the workflow runs.
Representative failures look like:
The nested job 'activation' is requesting 'contents: read', but is only allowed 'contents: none'.
The nested job 'agent' is requesting 'actions: read, contents: read, issues: read, pull-requests: read',
but is only allowed 'actions: none, contents: none, issues: none, pull-requests: none'.
Reproduction
- Use
gh aw v0.58.3.
- Create a gateway workflow that uses
safe-outputs.call-workflow.
- Point it at a reusable worker whose nested jobs request permissions such as:
activation: contents: read
agent: actions: read, contents: read, issues: read, pull-requests: read
safe_outputs or conclusion: contents: write, issues: write, pull-requests: write
- Compile the gateway.
- Inspect the generated
call-* job: it has no permissions: block.
- Run the workflow so GitHub validates the reusable workflow call.
- GitHub rejects the call before execution starts because the caller job does not grant the worker's required permissions.
Expected behavior
When gh aw generates a call-* reusable-workflow caller job, it also generates a job-level permissions: block that is a superset of the permissions required by the selected worker workflow.
Actual behavior
gh aw generates the call-* job without permissions:, so reusable workers that request permissions fail GitHub validation before they run.
Proposed fix
Teach buildCallWorkflowJobs() to compute and attach a permission superset for each generated caller job.
Suggested approach:
- Resolve the selected worker file using the existing workflow-file discovery logic.
- Prefer compiled
.lock.yml / .yml when available.
- Union all nested job-level
permissions from the resolved worker workflow.
- For same-batch
.md workers, fall back to source-derived permissions so compilation still succeeds.
- Render the merged result into
callJob.Permissions before adding the job.
Minimal acceptance criteria
- Generated
call-* jobs include permissions: when the target worker requires permissions.
- The generated permission set is at least the union of the called workflow's nested job permissions.
- Regression tests cover
.lock.yml, .yml, and same-batch .md worker targets.
Summary
As of
gh awv0.58.3and currentmain,safe-outputs.call-workflowstill generates conditionaluses:jobs without a job-levelpermissions:block.GitHub validates reusable workflow calls against the caller job's declared permission envelope. When the selected worker requests any non-
nonepermissions in nested jobs, GitHub rejects the generatedcall-*job before execution starts.This issue is specifically about the compiler-generated
call-*jobs created bysafe-outputs.call-workflow. It is not about outer handwritten relay workflows or general reusable-workflow permission rules.Why this still looks relevant after the recent
gh awupdateThe changelog entries after
v0.58.1add severalsafe-outputsandworkflow_callfixes, but they do not address permission propagation for generatedcall-*jobs:v0.58.2fixes safe-output manifest accounting (#20899)v0.58.2fixes idle HTTP server disconnects in the safe-outputs MCP server (#20901)v0.58.3fixes artifact-prefix downloads inworkflow_calldownstream jobs (#21011)Current
mainstill contains the samebuildCallWorkflowJobs()shape that createsuses:jobs withneeds,if,uses,secrets: inherit, andwith.payload, but nopermissions:.Current behavior
pkg/workflow/compiler_safe_output_jobs.gostill generatescall-workflowfan-out jobs like this:That renders YAML like:
There is still no
permissions:block on the caller job.Why GitHub rejects this
gh awwrites top-levelpermissions: {}and relies on job-level permissions for actual access.For reusable workflows, GitHub compares the caller job's declared permissions with the permissions requested by nested jobs in the called workflow. If the caller job omits
permissions:, the nested worker jobs are effectively constrained tononeand validation fails before the workflow runs.Representative failures look like:
Reproduction
gh awv0.58.3.safe-outputs.call-workflow.activation:contents: readagent:actions: read,contents: read,issues: read,pull-requests: readsafe_outputsorconclusion:contents: write,issues: write,pull-requests: writecall-*job: it has nopermissions:block.Expected behavior
When
gh awgenerates acall-*reusable-workflow caller job, it also generates a job-levelpermissions:block that is a superset of the permissions required by the selected worker workflow.Actual behavior
gh awgenerates thecall-*job withoutpermissions:, so reusable workers that request permissions fail GitHub validation before they run.Proposed fix
Teach
buildCallWorkflowJobs()to compute and attach a permission superset for each generated caller job.Suggested approach:
.lock.yml/.ymlwhen available.permissionsfrom the resolved worker workflow..mdworkers, fall back to source-derived permissions so compilation still succeeds.callJob.Permissionsbefore adding the job.Minimal acceptance criteria
call-*jobs includepermissions:when the target worker requires permissions..lock.yml,.yml, and same-batch.mdworker targets.