-
Notifications
You must be signed in to change notification settings - Fork 295
Description
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.
actions/setup/js/resolve_host_repo.cjsresolves the platform repository slug and emits it astarget_repo(for example<org>/<platform-repo>). That is correct for checkout and dispatch use cases.pkg/workflow/compiler_activation_job.goexposes that slug downstream asneeds.activation.outputs.target_repo.pkg/workflow/safe_outputs_jobs.goandpkg/workflow/notify_comment.gopass${{ needs.activation.outputs.target_repo }}intobuildGitHubAppTokenMintStep()asfallbackRepoExprfor safe-output and conclusion jobs.pkg/workflow/safe_outputs_app_config.gowrites 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.go→buildSafeOutputJob()pkg/workflow/notify_comment.go→buildConclusionJob()pkg/workflow/safe_outputs_app_config.go→buildGitHubAppTokenMintStep()pkg/workflow/compiler_activation_job.go→ activation outputs only exposetarget_repo, not a repo-name-only companion outputactions/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: writeThe 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
-
Edit
actions/setup/js/resolve_host_repo.cjs:- Compute
target_repo_namefrom the resolvedtarget_reposlug. - Emit
target_repo_namewithcore.setOutput().
- Compute
-
Edit
actions/setup/js/resolve_host_repo.test.cjs:- Add assertions that cross-repo resolution emits both
target_repoandtarget_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-reposhould output target_repo_name when same-repo invocation
- Add assertions that cross-repo resolution emits both
-
Edit
pkg/workflow/compiler_activation_job.go:- Expose
target_repo_nameas an activation job output alongsidetarget_repoandtarget_refwhenworkflow_callis present.
- Expose
-
Edit
pkg/workflow/compiler_activation_job_test.go:- Add a regression test mirroring
TestActivationJobTargetRepoOutputfor the new output. - Suggested test name:
TestActivationJobTargetRepoNameOutput.
- Add a regression test mirroring
-
Edit
pkg/workflow/safe_outputs_jobs.goandpkg/workflow/notify_comment.go:- Change the workflow-call GitHub App fallback from
needs.activation.outputs.target_repotoneeds.activation.outputs.target_repo_name.
- Change the workflow-call GitHub App fallback from
-
Edit
pkg/workflow/compiler_safe_outputs_job_test.go:- Add a regression test that compiles a workflow with
workflow_callplussafe-outputs.github-appand verifies the generated safe-output token mint step usesrepositories: ${{ needs.activation.outputs.target_repo_name }}. - Suggested test names:
TestJobWithGitHubAppWorkflowCallUsesTargetRepoNameFallbackTestConclusionJobWithGitHubAppWorkflowCallUsesTargetRepoNameFallback
- Add a regression test that compiles a workflow with
-
Optionally add a focused test in
pkg/workflow/safe_outputs_app_test.goor a new dedicated test file if that is a better fit for asserting the generated token-mint YAML. -
Update docs that mention the workflow-call relay fallback for safe-output GitHub App token minting:
docs/src/content/docs/reference/auth.mdxif needed- any cross-repo relay documentation that references
needs.activation.outputs.target_repofor app token fallback
-
Run the standard validation flow in the supported environment:
- develop in the Dev Container / Codespace
- run
make agent-finish
Reproduction
- Use gh-aw v0.58.0.
- Create a reusable platform workflow with both:
on.workflow_callsafe-outputs.github-appconfigured withapp-idandprivate-key, but no explicitrepositorieslist
- 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. - Ensure the workflow reaches a safe-output or conclusion job that mints a GitHub App token.
- 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 }}- Run the workflow. The
Generate GitHub App tokenstep fails with:
Not Found - https://docs.github.com/rest/apps/apps#get-a-repository-installation-for-the-authenticated-app
- In the failing job logs, the request path shows the doubled owner/slug form:
GET /repos/<org>/<org>%2F<platform-repo>/installation