Allow extending safe_outputs.needs from frontmatter so custom jobs can supply credentials to the consolidated safe_outputs job
Analysis
The consolidated safe_outputs job has a hardcoded Needs list built in pkg/workflow/compiler_safe_outputs_job.go:466-481 (v0.69.0):
// Build dependencies — safe_outputs depends on agent; when threat detection is enabled it also
// depends on the detection job (so that detection_success is available).
needs := []string{mainJobName}
if threatDetectionEnabled {
needs = append(needs, string(constants.DetectionJobName))
}
needs = append(needs, string(constants.ActivationJobName))
if data.LockForAgent {
needs = append(needs, "unlock")
}
There is no path for the user to extend this list from frontmatter, which prevents a useful pattern: defining a custom secrets_fetcher-style job that pulls credentials from an external secret manager and exposes them as job outputs that downstream built-in jobs reference via needs.<custom-job>.outputs.<name>.
This pattern works today for tools.github.github-app:
jobs:
secrets_fetcher:
runs-on: ubuntu-latest
outputs:
app_id: ${{ steps.fetch.outputs.app_id }}
app_private_key: ${{ steps.fetch.outputs.app_private_key }}
steps:
- id: fetch
uses: my-org/my-secrets-action@v1
with: { ... }
tools:
github:
github-app:
app-id: ${{ needs.secrets_fetcher.outputs.app_id }}
private-key: ${{ needs.secrets_fetcher.outputs.app_private_key }}
After compilation, the agent, activation, and conclusion jobs all gain secrets_fetcher as a dependency through existing auto-wiring (compiler_main_job.go:93-108 adds custom jobs to agent.Needs; ensureConclusionIsLastJob pulls in everything; configureActivationNeedsAndCondition propagates custom-job needs to activation). actionlint confirms the references resolve.
The same pattern with safe-outputs.github-app: ${{ needs.secrets_fetcher.outputs.* }} compiles cleanly but fails actionlint:
property "secrets_fetcher" is not defined in object type
{activation: {...}; agent: {...}; detection: {...}}
…because safe_outputs.needs does not include secrets_fetcher and there is no frontmatter knob to add it.
Why it matters
This blocks workflows that want to source GitHub App credentials (or any other secrets used by safe-outputs.github-app, safe-outputs.github-token, or other credentialed safe-output steps) from external secret managers via OIDC/JWT/etc., rather than storing them in repository or environment-scoped GitHub Secrets. Today users must:
- Use
${{ secrets.* }} in safe-outputs.github-app (defeats the purpose of a custom secret-fetch job), or
- Avoid
safe-outputs.github-app entirely and drive cross-repo writes from the agent itself via the GitHub MCP toolset (forfeits safe-output guard rails).
This is conceptually similar to the safe-outputs.environment field added in PR #20384 (which closed #20378) — a frontmatter knob that lets the user influence safe_outputs job configuration. That PR exposed environment:. This issue asks for an analogous knob for needs:.
Reproduction
Minimal workflow (spike.md):
---
on:
workflow_dispatch: {}
permissions:
contents: read
engine:
id: copilot
model: gpt-5.1-codex-mini
tools:
github:
github-app:
app-id: ${{ needs.secrets_fetcher.outputs.app_id }}
private-key: ${{ needs.secrets_fetcher.outputs.app_private_key }}
owner: "owner-placeholder"
repositories: ["*"]
safe-outputs:
github-app:
app-id: ${{ needs.secrets_fetcher.outputs.app_id }}
private-key: ${{ needs.secrets_fetcher.outputs.app_private_key }}
owner: "owner-placeholder"
repositories: ["*"]
noop:
report-as-issue: false
jobs:
secrets_fetcher:
runs-on: ubuntu-latest
outputs:
app_id: ${{ steps.fetch.outputs.app_id }}
app_private_key: ${{ steps.fetch.outputs.app_private_key }}
steps:
- id: fetch
run: |
echo "app_id=placeholder" >> "$GITHUB_OUTPUT"
echo "app_private_key=placeholder" >> "$GITHUB_OUTPUT"
---
# Spike
Noop.
Steps:
gh aw compile spike.md — succeeds with 0 errors.
actionlint spike.lock.yml — fails on the safe_outputs job's client-id/private-key lines with property "secrets_fetcher" is not defined.
- Inspect
spike.lock.yml: agent, activation, and conclusion all have secrets_fetcher in their needs:. safe_outputs.needs only contains [agent, activation] (or with detection: [agent, detection, activation]).
Proposed Solution
Add a frontmatter field — naming suggestion safe-outputs.extra-needs: [<job-id>, ...] — that the compiler appends to the consolidated safe_outputs job's Needs list during construction. Keep the auto-managed dependencies (agent, optionally detection, activation, optionally unlock) intact.
Implementation Plan
Please implement the following changes:
-
Update Schema (pkg/parser/schemas/frontmatter.json):
- Add an
extra-needs field under safe-outputs
- Type: array of strings
- Each string must match the existing job-id pattern (alphanumeric,
_, -)
additionalItems: false, uniqueItems: true
- Default: empty array
-
Update Config Type (pkg/workflow/safe_outputs_config.go):
- Add an
ExtraNeeds []string field to SafeOutputsConfig
- Parse it in the corresponding config loader (likely
safe_outputs_config_helpers.go)
-
Use it in Job Builder (pkg/workflow/compiler_safe_outputs_job.go, around lines 464-482):
- After the existing
needs = append(...) block, append data.SafeOutputs.ExtraNeeds... (deduplicated against the auto-managed entries)
- Log each appended dependency for traceability, mirroring the existing
consolidatedSafeOutputsJobLog.Print(...) calls
-
Validate Targets (new validation, likely in pkg/workflow/safe_jobs_needs_validation.go or a sibling file):
- Each value in
extra-needs must be a real job in the workflow (custom jobs.<name> entry) — error if it references an unknown job
- Reject built-in names (
agent, activation, pre_activation, pre-activation, conclusion, safe_outputs, detection, unlock, push_repo_memory, update_cache_memory) to avoid cycles
- Reject self-reference (always a no-op since
safe_outputs is the target)
- Error message format:
safe-outputs.extra-needs: unknown job %q. Expected one of the workflow's custom jobs. Example: safe-outputs.extra-needs: [secrets_fetcher]
-
Add Tests (pkg/workflow/compiler_safe_outputs_job_test.go and safe_jobs_needs_validation_test.go):
- Compiles a workflow with
safe-outputs.extra-needs: [secrets_fetcher] and asserts:
safe_outputs.needs in the compiled lock contains secrets_fetcher
- Existing auto-managed needs are still present
actionlint accepts a reference like ${{ needs.secrets_fetcher.outputs.x }} in safe-outputs.github-app
- Negative cases:
- Unknown job name → validation error
- Built-in name (
agent) → validation error
- Empty array / missing field → no behavior change (back-compat)
-
Update Documentation:
docs/src/content/docs/reference/frontmatter.md (or wherever safe-outputs fields are documented)
- Add
extra-needs to the field list with type, default, validation rules
- Add a worked example showing the custom-job credential-supply pattern
-
Follow Guidelines:
- Use
console formatting from pkg/console for any CLI output
- Follow the error message style guide
- Run
make agent-finish before completing
Acceptance Criteria
- A workflow with
safe-outputs.extra-needs: [<custom-job>] compiles to a lock file whose safe_outputs: block lists <custom-job> in needs:, in addition to the existing auto-managed dependencies.
actionlint passes on the compiled lock file when safe-outputs.github-app references needs.<custom-job>.outputs.*.
- Validation rejects unknown job names, built-in job names, and self-reference with actionable error messages.
- Existing workflows that do not set
extra-needs are unaffected (back-compat).
Context
Allow extending
safe_outputs.needsfrom frontmatter so custom jobs can supply credentials to the consolidatedsafe_outputsjobAnalysis
The consolidated
safe_outputsjob has a hardcodedNeedslist built inpkg/workflow/compiler_safe_outputs_job.go:466-481(v0.69.0):There is no path for the user to extend this list from frontmatter, which prevents a useful pattern: defining a custom
secrets_fetcher-style job that pulls credentials from an external secret manager and exposes them as job outputs that downstream built-in jobs reference vianeeds.<custom-job>.outputs.<name>.This pattern works today for
tools.github.github-app:After compilation, the
agent,activation, andconclusionjobs all gainsecrets_fetcheras a dependency through existing auto-wiring (compiler_main_job.go:93-108adds custom jobs toagent.Needs;ensureConclusionIsLastJobpulls in everything;configureActivationNeedsAndConditionpropagates custom-job needs toactivation).actionlintconfirms the references resolve.The same pattern with
safe-outputs.github-app: ${{ needs.secrets_fetcher.outputs.* }}compiles cleanly but failsactionlint:…because
safe_outputs.needsdoes not includesecrets_fetcherand there is no frontmatter knob to add it.Why it matters
This blocks workflows that want to source GitHub App credentials (or any other secrets used by
safe-outputs.github-app,safe-outputs.github-token, or other credentialed safe-output steps) from external secret managers via OIDC/JWT/etc., rather than storing them in repository or environment-scoped GitHub Secrets. Today users must:${{ secrets.* }}insafe-outputs.github-app(defeats the purpose of a custom secret-fetch job), orsafe-outputs.github-appentirely and drive cross-repo writes from the agent itself via the GitHub MCP toolset (forfeits safe-output guard rails).This is conceptually similar to the
safe-outputs.environmentfield added in PR #20384 (which closed #20378) — a frontmatter knob that lets the user influencesafe_outputsjob configuration. That PR exposedenvironment:. This issue asks for an analogous knob forneeds:.Reproduction
Minimal workflow (
spike.md):Steps:
gh aw compile spike.md— succeeds with 0 errors.actionlint spike.lock.yml— fails on thesafe_outputsjob'sclient-id/private-keylines withproperty "secrets_fetcher" is not defined.spike.lock.yml:agent,activation, andconclusionall havesecrets_fetcherin theirneeds:.safe_outputs.needsonly contains[agent, activation](or with detection:[agent, detection, activation]).Proposed Solution
Add a frontmatter field — naming suggestion
safe-outputs.extra-needs: [<job-id>, ...]— that the compiler appends to the consolidatedsafe_outputsjob'sNeedslist during construction. Keep the auto-managed dependencies (agent, optionallydetection,activation, optionallyunlock) intact.Implementation Plan
Please implement the following changes:
Update Schema (
pkg/parser/schemas/frontmatter.json):extra-needsfield undersafe-outputs_,-)additionalItems: false,uniqueItems: trueUpdate Config Type (
pkg/workflow/safe_outputs_config.go):ExtraNeeds []stringfield toSafeOutputsConfigsafe_outputs_config_helpers.go)Use it in Job Builder (
pkg/workflow/compiler_safe_outputs_job.go, around lines 464-482):needs = append(...)block, appenddata.SafeOutputs.ExtraNeeds...(deduplicated against the auto-managed entries)consolidatedSafeOutputsJobLog.Print(...)callsValidate Targets (new validation, likely in
pkg/workflow/safe_jobs_needs_validation.goor a sibling file):extra-needsmust be a real job in the workflow (customjobs.<name>entry) — error if it references an unknown jobagent,activation,pre_activation,pre-activation,conclusion,safe_outputs,detection,unlock,push_repo_memory,update_cache_memory) to avoid cyclessafe_outputsis the target)safe-outputs.extra-needs: unknown job %q. Expected one of the workflow's custom jobs. Example: safe-outputs.extra-needs: [secrets_fetcher]Add Tests (
pkg/workflow/compiler_safe_outputs_job_test.goandsafe_jobs_needs_validation_test.go):safe-outputs.extra-needs: [secrets_fetcher]and asserts:safe_outputs.needsin the compiled lock containssecrets_fetcheractionlintaccepts a reference like${{ needs.secrets_fetcher.outputs.x }}insafe-outputs.github-appagent) → validation errorUpdate Documentation:
docs/src/content/docs/reference/frontmatter.md(or whereversafe-outputsfields are documented)extra-needsto the field list with type, default, validation rulesFollow Guidelines:
consoleformatting frompkg/consolefor any CLI outputmake agent-finishbefore completingAcceptance Criteria
safe-outputs.extra-needs: [<custom-job>]compiles to a lock file whosesafe_outputs:block lists<custom-job>inneeds:, in addition to the existing auto-managed dependencies.actionlintpasses on the compiled lock file whensafe-outputs.github-appreferencesneeds.<custom-job>.outputs.*.extra-needsare unaffected (back-compat).Context
environment:frontmatter field not propagated tosafe_outputsjob — breaks environment-level secrets #20378 / fix: propagateenvironment:frontmatter field to all safe-output jobs #20384 (addedsafe-outputs.environmentfor the same general motivation — letting users influencesafe_outputsjob configuration without forking the compiler).