Skip to content

Allow extending safe_outputs.needs from frontmatter for custom credential-supply jobs #27472

@bbonafed

Description

@bbonafed

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:

  1. Use ${{ secrets.* }} in safe-outputs.github-app (defeats the purpose of a custom secret-fetch job), or
  2. 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:

  1. gh aw compile spike.md — succeeds with 0 errors.
  2. actionlint spike.lock.yml — fails on the safe_outputs job's client-id/private-key lines with property "secrets_fetcher" is not defined.
  3. 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:

  1. 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
  2. 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)
  3. 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
  4. 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]
  5. 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)
  6. 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
  7. 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

Metadata

Metadata

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions