Skip to content

GitHub App token fallback uses full slug instead of repo name in workflow_call relays #20821

@johnwilliams-12

Description

@johnwilliams-12

Summary

gh-aw v0.58.0 generates invalid actions/create-github-app-token inputs for safe-output jobs in cross-repository workflow_call relay scenarios when safe-outputs.github-app is configured without an explicit repositories list. In that path, the compiler scopes the minted token to ${{ needs.activation.outputs.target_repo }}, but target_repo is an owner/repo slug while create-github-app-token expects repositories to contain repo names only when owner is also set. The generated workflow therefore asks GitHub for an installation on /repos/<org>/<org>%2F<platform-repo>/installation and fails before any safe output can run.

Root Cause

The bug is caused by a mismatch between the meaning of target_repo in the cross-repo relay path and the input contract of actions/create-github-app-token.

  1. actions/setup/js/resolve_host_repo.cjs resolves the platform repository slug and emits it as target_repo (for example <org>/<platform-repo>). That is correct for checkout and dispatch use cases.
  2. pkg/workflow/compiler_activation_job.go exposes that slug downstream as needs.activation.outputs.target_repo.
  3. pkg/workflow/safe_outputs_jobs.go and pkg/workflow/notify_comment.go pass ${{ needs.activation.outputs.target_repo }} into buildGitHubAppTokenMintStep() as fallbackRepoExpr for safe-output and conclusion jobs.
  4. pkg/workflow/safe_outputs_app_config.go writes that fallback expression directly into the generated YAML as:
owner: ${{ github.repository_owner }}
repositories: ${{ needs.activation.outputs.target_repo }}

That is invalid when target_repo is <org>/<platform-repo>, because create-github-app-token interprets owner and repositories together as owner + repo-name, not owner + owner/repo-slug.

The broken logic is in these places:

  • pkg/workflow/safe_outputs_jobs.gobuildSafeOutputJob()
  • pkg/workflow/notify_comment.gobuildConclusionJob()
  • pkg/workflow/safe_outputs_app_config.gobuildGitHubAppTokenMintStep()
  • pkg/workflow/compiler_activation_job.go → activation outputs only expose target_repo, not a repo-name-only companion output
  • actions/setup/js/resolve_host_repo.cjs → resolves only the full slug, not the repository name needed by app token minting

Affected Code

Generated YAML in the failing relay workflow:

- name: Generate GitHub App token
  id: safe-outputs-app-token
  uses: actions/create-github-app-token@...
  with:
	 app-id: ${{ vars.APP_ID }}
	 private-key: ${{ secrets.APP_PRIVATE_KEY }}
	 owner: ${{ github.repository_owner }}
	 repositories: ${{ needs.activation.outputs.target_repo }}
	 github-api-url: ${{ github.api_url }}
	 permission-actions: write
	 permission-contents: read
	 permission-issues: write
	 permission-pull-requests: write

The problem is the repositories: line. In a cross-repo relay, needs.activation.outputs.target_repo resolves to a full slug like <org>/<platform-repo>, but the action expects only <platform-repo> when owner: is also present.

Observed failure from a private reproduction run (RUN_ID 23054595540, JOB_ID 66964877120):

  • failing step: Generate GitHub App token
  • error: Not Found - https://docs.github.com/rest/apps/apps#get-a-repository-installation-for-the-authenticated-app
  • logged request: GET /repos/<org>/<org>%2F<platform-repo>/installation

Proposed Fix

Add a repo-name-only activation output and use it for GitHub App token minting fallbacks in safe-output and conclusion jobs, while preserving the existing full-slug target_repo output for checkout and dispatch logic.

1. Emit both target_repo and target_repo_name from resolve_host_repo.cjs

// actions/setup/js/resolve_host_repo.cjs
async function main() {
  // ... existing targetRepo / targetRef resolution ...

  const targetRepoName = targetRepo.includes("/")
	 ? targetRepo.substring(targetRepo.indexOf("/") + 1)
	 : targetRepo;

  core.info(`Resolved host ref for activation checkout: ${targetRef}`);

  if (targetRepo !== currentRepo && targetRepo !== "") {
	 core.info(`Cross-repo invocation detected: platform repo is "${targetRepo}", caller is "${currentRepo}"`);
	 await core.summary.addRaw(`**Activation Checkout**: Checking out platform repo \`${targetRepo}\` @ \`${targetRef}\` (caller: \`${currentRepo}\`)`).write();
  } else {
	 core.info(`Same-repo invocation: checking out ${targetRepo} @ ${targetRef}`);
  }

  core.setOutput("target_repo", targetRepo);
  core.setOutput("target_repo_name", targetRepoName);
  core.setOutput("target_ref", targetRef);
}

2. Expose target_repo_name from the activation job

// pkg/workflow/compiler_activation_job.go
if hasWorkflowCallTrigger(data.On) && !data.InlinedImports {
	outputs["target_repo"] = "${{ steps.resolve-host-repo.outputs.target_repo }}"
	outputs["target_repo_name"] = "${{ steps.resolve-host-repo.outputs.target_repo_name }}"
	outputs["target_ref"] = "${{ steps.resolve-host-repo.outputs.target_ref }}"
}

3. Use target_repo_name for GitHub App token fallback in safe-output jobs

// pkg/workflow/safe_outputs_jobs.go
if data.SafeOutputs != nil && data.SafeOutputs.GitHubApp != nil {
	safeOutputsJobsLog.Print("Adding GitHub App token minting step with auto-computed permissions")
	var appTokenFallbackRepo string
	if hasWorkflowCallTrigger(data.On) {
		appTokenFallbackRepo = "${{ needs.activation.outputs.target_repo_name }}"
	}
	steps = append(steps, c.buildGitHubAppTokenMintStep(data.SafeOutputs.GitHubApp, config.Permissions, appTokenFallbackRepo)...)
}

4. Use target_repo_name for the conclusion job as well

// pkg/workflow/notify_comment.go
if data.SafeOutputs.GitHubApp != nil {
	permissions := ComputePermissionsForSafeOutputs(data.SafeOutputs)
	var appTokenFallbackRepo string
	if hasWorkflowCallTrigger(data.On) {
		appTokenFallbackRepo = "${{ needs.activation.outputs.target_repo_name }}"
	}
	steps = append(steps, c.buildGitHubAppTokenMintStep(data.SafeOutputs.GitHubApp, permissions, appTokenFallbackRepo)...)
}

5. Update the buildGitHubAppTokenMintStep() comment to match the actual fallback contract

// pkg/workflow/safe_outputs_app_config.go
// fallbackRepoExpr overrides the default ${{ github.event.repository.name }} fallback when
// no explicit repositories are configured (e.g. pass needs.activation.outputs.target_repo_name for
// workflow_call relay workflows so the token is scoped to the platform repo's NAME, not the caller repo).

Implementation Plan

  1. Edit actions/setup/js/resolve_host_repo.cjs:

    • Compute target_repo_name from the resolved target_repo slug.
    • Emit target_repo_name with core.setOutput().
  2. Edit actions/setup/js/resolve_host_repo.test.cjs:

    • Add assertions that cross-repo resolution emits both target_repo and target_repo_name.
    • Add assertions that same-repo resolution emits the current repo name as target_repo_name.
    • Suggested test names:
      • should output target_repo_name when invoked cross-repo
      • should output target_repo_name when same-repo invocation
  3. Edit pkg/workflow/compiler_activation_job.go:

    • Expose target_repo_name as an activation job output alongside target_repo and target_ref when workflow_call is present.
  4. Edit pkg/workflow/compiler_activation_job_test.go:

    • Add a regression test mirroring TestActivationJobTargetRepoOutput for the new output.
    • Suggested test name: TestActivationJobTargetRepoNameOutput.
  5. Edit pkg/workflow/safe_outputs_jobs.go and pkg/workflow/notify_comment.go:

    • Change the workflow-call GitHub App fallback from needs.activation.outputs.target_repo to needs.activation.outputs.target_repo_name.
  6. Edit pkg/workflow/compiler_safe_outputs_job_test.go:

    • Add a regression test that compiles a workflow with workflow_call plus safe-outputs.github-app and verifies the generated safe-output token mint step uses repositories: ${{ needs.activation.outputs.target_repo_name }}.
    • Suggested test names:
      • TestJobWithGitHubAppWorkflowCallUsesTargetRepoNameFallback
      • TestConclusionJobWithGitHubAppWorkflowCallUsesTargetRepoNameFallback
  7. Optionally add a focused test in pkg/workflow/safe_outputs_app_test.go or a new dedicated test file if that is a better fit for asserting the generated token-mint YAML.

  8. Update docs that mention the workflow-call relay fallback for safe-output GitHub App token minting:

    • docs/src/content/docs/reference/auth.mdx if needed
    • any cross-repo relay documentation that references needs.activation.outputs.target_repo for app token fallback
  9. Run the standard validation flow in the supported environment:

    • develop in the Dev Container / Codespace
    • run make agent-finish

Reproduction

  1. Use gh-aw v0.58.0.
  2. Create a reusable platform workflow with both:
    • on.workflow_call
    • safe-outputs.github-app configured with app-id and private-key, but no explicit repositories list
  3. Invoke that workflow from a different repository through a cross-repo relay path so that the activation job resolves the host repository via resolve_host_repo.cjs.
  4. Ensure the workflow reaches a safe-output or conclusion job that mints a GitHub App token.
  5. Inspect the generated YAML or the run logs: the safe-output token mint step will contain:
owner: ${{ github.repository_owner }}
repositories: ${{ needs.activation.outputs.target_repo }}
  1. Run the workflow. The Generate GitHub App token step fails with:
Not Found - https://docs.github.com/rest/apps/apps#get-a-repository-installation-for-the-authenticated-app
  1. In the failing job logs, the request path shows the doubled owner/slug form:
GET /repos/<org>/<org>%2F<platform-repo>/installation

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions