-
Notifications
You must be signed in to change notification settings - Fork 301
Description
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.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 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 awv0.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: readagent:actions: read,contents: read,issues: read,pull-requests: readsafe_outputsorconclusion:contents: write,issues: write,pull-requests: write
- Compile the gateway.
- Inspect the generated
call-*job: it has nopermissions: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/.ymlwhen available. - Union all nested job-level
permissionsfrom the resolved worker workflow. - For same-batch
.mdworkers, fall back to source-derived permissions so compilation still succeeds. - Render the merged result into
callJob.Permissionsbefore adding the job.
Minimal acceptance criteria
- Generated
call-*jobs includepermissions: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.mdworker targets.