Skip to content

Multi-org GitHub App support: derive github-app.owner per target, or allow a computed value #36709

@corygehr

Description

@corygehr

Summary

When a single GitHub App is installed across multiple organizations and a workflow operates on a target repository whose org is supplied at dispatch time (e.g. via workflow_dispatch input trigger_ref: owner/repo), gh-aw currently forces github-app.owner to be a compile-time literal. There is no ergonomic way to scope the minted token to the target's org installation, because:

  1. actions/create-github-app-token mints one token per installation, and each org is a separate installation. Cross-org tokens don't exist.
  2. gh-aw injects the Generate GitHub App token step before any user steps: run, so a bash pre-step cannot compute owner and expose it.
  3. GitHub Actions expressions don't include a string-split function, so ${{ inputs.trigger_ref }} (e.g. acme/foo) cannot be reduced to just the org part (acme) inside the generated with.owner value.

The net effect: workflows that want to be re-usable across multiple App-installation owners either hardcode a single owner: (and silently fail for everything else), or have to introduce a redundant trigger_org input alongside trigger_ref and plumb it through every dispatcher and dispatched workflow — purely as a workaround for (2) and (3) above.

Use case

A central "ops" repo that runs evaluation / triage / reporting workflows against target repositories spread across multiple GitHub organizations, where:

  • The GitHub App has been installed in each org.
  • The target repo is provided at dispatch time as a single owner/repo input.
  • The workflow needs:
    • checkout of the target repo (target-org-scoped token).
    • tools.github (GitHub MCP) reading from the target repo.
    • safe-outputs writing back to the target repo (issues, PRs, comments).

All three of those require a token minted from the target's App installation, not the workflow repo's org installation.

Current workaround (what we'd like to retire)

  1. Add a trigger_org workflow input alongside trigger_ref (default <some-fallback> for backward compat).
  2. Every dispatcher computes trigger_org="${trigger_ref%%/*}" and passes both inputs.
  3. Every consumer workflow references owner: ${{ github.event.inputs.trigger_org || 'fallback-org' }} in:
    • tools.github.github-app.owner
    • safe-outputs.github-app.owner
    • checkout[*].github-app.owner
  4. Every actions/create-github-app-token pre-step in non-gh-aw workflows mints one token per known org and selects by repo prefix at runtime.

Concerns with this workaround:

  • trigger_org is mechanically derivable from trigger_ref — it's a redundant input that has to be threaded through every workflow in the chain (orchestrator → area agents → topic specialists → aggregator → re-dispatch helpers).
  • Drift risk: forgetting to pass trigger_org on one dispatch silently falls back to the default org, producing 404s when the App tries to read the target.
  • Documentation tax: every workflow's input contract grows by one field that exists only to work around this gap.

Proposed solutions (in priority order)

1. Auto-derive github-app.owner from checkout.repository

When a workflow has both a checkout entry with repository: <expr-or-literal> and a sibling github-app: block whose owner: is omitted, gh-aw could synthesize the owner at compile time as the part of repository before the /.

checkout:
  - repository: ${{ github.event.inputs.trigger_ref }}
    github-app:
      client-id: ${{ secrets.MY_APP_ID }}
      private-key: ${{ secrets.MY_APP_KEY }}
      # owner: omitted — gh-aw derives "acme" from inputs.trigger_ref at runtime
      repositories: ["*"]

Same idea for the top-level tools.github.github-app and safe-outputs.github-app — if their owner: is omitted and the workflow has a single checkout.repository, derive from that.

This is the cleanest path because it requires no new schema surface — just smarter defaulting.

Implementation sketch: gh-aw emits the Generate GitHub App token step with something like:

with:
  owner: ${{ fromJSON(toJSON(github.event.inputs.trigger_ref)) }}  # …or similar

…but since Actions expressions have no split, the practical emit would be a tiny inline shell step that exports OWNER to $GITHUB_OUTPUT, then the actions/create-github-app-token step references it via ${{ steps.derive-owner.outputs.owner }}. The inline step would be generated by gh-aw, not the user.

2. Support an explicit expression-friendly computed owner

Allow github-app.owner to be set to a small set of well-known computed values:

github-app:
  client-id: ${{ secrets.MY_APP_ID }}
  private-key: ${{ secrets.MY_APP_KEY }}
  owner: from-repository           # synthesized from sibling repository:
  # or
  owner: from-input.trigger_ref    # synthesized from workflow input

Less elegant than (1) but more discoverable, and unambiguous when there are multiple checkout entries with different repository: values.

3. Allow a user pre-step to run before gh-aw's token-minting step

Today, user steps: run after the gh-aw-injected Generate GitHub App token for checkout (0) step, which means a pre-step cannot influence owner. A new pre-steps: (or similar) hook that runs before the token step would let users derive owner themselves and pass it via $GITHUB_OUTPUT:

pre-steps:
  - name: Derive owner
    id: derive
    run: echo "owner=${TRIGGER_REF%%/*}" >> "$GITHUB_OUTPUT"
    env:
      TRIGGER_REF: ${{ github.event.inputs.trigger_ref }}
checkout:
  - repository: ${{ github.event.inputs.trigger_ref }}
    github-app:
      owner: ${{ steps.derive.outputs.owner }}

This is the most general solution but the broadest schema change.

4. Add a tiny string-split helper to gh-aw's expression evaluator

If gh-aw already does any expression rewriting at compile time, exposing something like ${{ aw.split(inputs.trigger_ref, '/')[0] }} (rendered into a shell step or Actions-script) would unblock this with minimal new surface.

Acceptance criteria

  • A single workflow file can operate against target repositories whose orgs are not known at compile time, given that the App is installed in each org.
  • Users do not need to declare a redundant trigger_org-style input.
  • The change is backward-compatible: existing workflows with a literal owner: <org> continue to work unchanged.

Out of scope / not asking for

  • Cross-org tokens from actions/create-github-app-token (upstream Actions limitation).
  • Automatic detection of which orgs the App is installed in (deployment-time concern).
  • Changes to the workflow input contract beyond what's strictly needed for the derivation.

Metadata

Metadata

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