Skip to content

Support secret-manager-backed GitHub App credentials without masked cross-job outputs #33592

Description

@bbonafed

Summary

Please add first-class support for GitHub App credentials that are fetched from an external secret manager at runtime, without requiring the app private key to be stored directly in GitHub Actions secrets.

Today gh-aw supports GitHub App authentication through github-app: frontmatter:

github-app:
  app-id: ${{ vars.WORKFLOW_APP_ID }}
  private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }}
  owner: my-org
  repositories: ["*"]

This works when the app id/private key are directly available as GitHub Actions vars/secrets. It does not work cleanly when an organization requires GitHub App credentials to live in an external secret manager such as Conjur, Vault, Keeper, or a similar enterprise secret-management system.

Prior Art / Related Issues

I searched existing issues and did not find an exact duplicate for this secret-manager-backed GitHub App credential flow.

Related but different issues:

Use Case

Some deployments do not allow long-lived GitHub App private keys to be stored in GitHub Actions secrets. Instead:

  • GitHub Actions stores only secret-manager bootstrap credentials.
  • GitHub App credentials live in an external secret manager.
  • Workflows fetch WORKFLOW_APP_ID and WORKFLOW_APP_PRIVATE_KEY at runtime.
  • gh-aw should then mint short-lived GitHub App installation tokens for checkout, GitHub tools, and safe outputs.

Current Behavior

A natural gh-aw configuration is to use a custom credential-fetching job and wire it through needs:

on:
  workflow_dispatch:
  needs: [secrets_fetcher]

jobs:
  secrets_fetcher:
    runs-on: ubuntu-latest
    outputs:
      app_id: ${{ steps.fetch.outputs.WORKFLOW_APP_ID }}
      private_key: ${{ steps.fetch.outputs.WORKFLOW_APP_PRIVATE_KEY }}
    steps:
      - id: fetch
        uses: some-secret-manager-action@v1
        with:
          # Fetch WORKFLOW_APP_ID and WORKFLOW_APP_PRIVATE_KEY from an external secret manager.

safe-outputs:
  needs: [secrets_fetcher]
  github-app:
    app-id: ${{ needs.secrets_fetcher.outputs.app_id }}
    private-key: ${{ needs.secrets_fetcher.outputs.private_key }}

This can compile, but it fails at runtime with many secret-manager actions because they mask retrieved values using ::add-mask:: / core.setSecret().

GitHub Actions then refuses to expose masked values as job outputs:

Skip output 'app_id' since it may contain secret.
Skip output 'private_key' since it may contain secret.

Then gh-aw's generated GitHub App token-minting step receives empty inputs and fails:

The 'client-id' (or deprecated 'app-id') input must be set to a non-empty string.

Root Cause

gh-aw correctly mints GitHub App installation tokens inside each generated job that needs them. That is the right security model.

The gap is that secret-manager-fetched credentials cannot safely cross job boundaries as needs.<job>.outputs.* once the secret-fetching action masks them.

The pattern works only when the secret-fetch step and actions/create-github-app-token run in the same job:

- id: get-secrets
  uses: some-secret-manager-action@v1

- id: app-token
  uses: actions/create-github-app-token@v2
  with:
    app-id: ${{ steps.get-secrets.outputs.WORKFLOW_APP_ID }}
    private-key: ${{ steps.get-secrets.outputs.WORKFLOW_APP_PRIVATE_KEY }}

But gh-aw owns the generated token-minting jobs/steps, so workflow authors cannot express: "run this credential-fetch step immediately before every generated GitHub App token mint."

Expected Behavior

gh-aw should provide a first-class way to fetch GitHub App credentials inside every generated job that needs to mint an app token, without passing masked secrets through job outputs.

The app token itself should still be generated by gh-aw using actions/create-github-app-token. The feature should only solve credential provisioning.

Proposed Design

Add support for credential-provider steps on github-app: configs.

Example syntax:

github-app:
  credentials:
    steps:
      - name: Fetch GitHub App credentials
        id: app-creds
        uses: some-secret-manager-action@v1
        with:
          # Fetch WORKFLOW_APP_ID and WORKFLOW_APP_PRIVATE_KEY.
    app-id: ${{ steps.app-creds.outputs.WORKFLOW_APP_ID }}
    private-key: ${{ steps.app-creds.outputs.WORKFLOW_APP_PRIVATE_KEY }}
  owner: my-org
  repositories: ["*"]

The compiler would inject credentials.steps into each generated job immediately before any generated actions/create-github-app-token step for that github-app config.

This avoids cross-job masked output loss because steps.app-creds.outputs.* is consumed within the same job.

Scope

This should apply consistently to all gh-aw token-minting sites:

  1. checkout.github-app
  2. tools.github.github-app
  3. safe-outputs.github-app
  4. top-level github-app fallback
  5. on.github-app / activation token minting

Implementation Plan

Please implement the following changes:

  1. Extend GitHub App configuration

    • Update GitHubAppConfig in pkg/workflow/safe_outputs_app_config.go with an optional credential-provider config.
    • Suggested shape: credentials.steps, credentials.app-id, credentials.private-key.
    • Preserve existing app-id / client-id and private-key behavior for direct vars/secrets.
  2. Update schema and validation

    • Update frontmatter schema files to accept github-app.credentials wherever github-app is valid.
    • Validate that credentials.steps is only emitted into generated jobs and is not interpreted as normal workflow-level steps.
    • Validate that credentials.app-id and credentials.private-key reference same-job step outputs, for example ${{ steps.app-creds.outputs.WORKFLOW_APP_ID }}.
    • Reject or warn on needs.*.outputs.* in credentials.app-id / credentials.private-key, because that recreates the masked-output problem.
    • Follow the existing error-message style: [what's wrong]. [what's expected]. [example].
  3. Inject credential-provider steps before token minting

    • Update generated token-minting paths so credential-provider steps appear immediately before actions/create-github-app-token.
    • Ensure this works for checkout token minting, GitHub MCP/tool token minting, safe-output token minting, and activation token minting.
    • Ensure step IDs remain collision-safe if multiple GitHub App configs are used in one workflow.
  4. Preserve existing security model

    • gh-aw should still mint short-lived installation tokens with actions/create-github-app-token.
    • The agent job should still receive only the minted token where appropriate, not the raw private key.
    • Safe outputs should keep existing least-privilege permission calculation.
    • Existing ignore-if-missing behavior should continue to work for direct GitHub Actions vars/secrets. Define and document whether it also applies to credential-provider outputs.
  5. Add tests

    • Add unit tests for parsing and validating github-app.credentials.
    • Add compiler-output tests showing credential steps emitted before every relevant token-minting step.
    • Add regression tests proving that direct github-app.app-id/private-key configurations compile unchanged.
    • Add negative tests for invalid credentials shapes and needs.*.outputs.* references in credential fields.
  6. Update documentation

    • Update docs/src/content/docs/reference/auth.mdx with a section like "Secret-manager-backed GitHub App credentials".
    • Mention why job outputs from masked secrets are not reliable across job boundaries.
    • Provide a generic external secret-manager example.
    • Cross-link from docs/src/content/docs/reference/triggers.md and docs/src/content/docs/reference/safe-outputs.md where on.needs / safe-outputs.needs currently show secret-fetching examples, clarifying that masked credentials should use the new same-job credential-provider mechanism instead.
  7. Run standard validation

    • Run the project validation flow recommended in CONTRIBUTING.md, including make agent-finish.

Acceptance Criteria

  • A workflow can keep GitHub App id/private key in an external secret manager.
  • GitHub Actions secrets only need secret-manager bootstrap credentials.
  • gh-aw still mints GitHub App installation tokens itself.
  • No app private key is passed through needs.<job>.outputs.*.
  • Generated workflows do not require manual lockfile patching.
  • Existing github-app behavior using vars.* / secrets.* remains unchanged.
  • Documentation clearly explains when to use direct vars/secrets vs same-job credential-provider steps.

Why This Matters

This enables enterprise CentralRepoOps and cross-repository workflows where GitHub App credentials must remain in approved secret managers, while still using gh-aw's native GitHub App token model and safe-output permission boundaries.

Follow-up Validation

A throwaway validation confirmed that the workaround is viable when the GitHub App credentials are fetched from the external secret manager inside each job immediately before actions/create-github-app-token runs.

The gh-aw-generated token-minting sites that needed same-job credential access were:

  • activation checkout token
  • agent checkout token
  • agent GitHub/MCP token
  • safe_outputs token
  • conclusion token

With that pattern, cross-repository checkout, agent execution, PR review submission, and label updates all completed successfully.

This reinforces the feature request: secret-manager-backed GitHub App credentials need to be injectable at every gh-aw-generated token-minting site, without requiring masked values to cross job boundaries.

Metadata

Metadata

Assignees

No one assigned

    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