-
Notifications
You must be signed in to change notification settings - Fork 296
Description
Summary
Observed on gh-aw main as of 2026-03-12, after the activation-checkout fixes discussed in #20567 and the Checkout actions folder fix discussed in #20658. In a caller-hosted relay topology where <org>/<app-repo> calls <org>/<platform-repo>/.github/workflows/<platform-gateway>.lock.yml via uses:, the compiler emits safe-outputs.dispatch-workflow.target-repo: "<org>/<platform-repo>", but the runtime still dispatches against the caller repository. The relay fails in Process Safe Outputs with Failed to dispatch workflow "<platform-worker>": Not Found until the generated lock file is monkey-patched to override the actual createWorkflowDispatch() owner and repo parameters. Once that override is applied, the same topology successfully creates the downstream worker run. This isolates the bug to runtime repository resolution inside dispatch_workflow, not token visibility, workflow visibility, or workflow filename resolution.
Root Cause
The bug is in actions/setup/js/dispatch_workflow.cjs.
The handler receives a compiled target-repo value, but it never uses it when resolving the dispatch destination. Instead, it resolves the destination repository from context.repo, which is the caller repository in a cross-repository reusable-workflow invocation.
The broken logic is the repository selection immediately before createWorkflowDispatch():
const repo = context.repo;
await githubClient.rest.actions.createWorkflowDispatch({
owner: repo.owner,
repo: repo.repo,
workflow_id: workflowFile,
ref,
inputs,
return_run_details: true,
});In a caller-hosted relay topology:
context.repo=<org>/<app-repo>- compiled
target-repo=<org>/<platform-repo> - worker workflow file lives only in
<org>/<platform-repo>
So the runtime looks for <platform-worker>.lock.yml in the wrong repository and GitHub returns Not Found.
This was proven in a live run by:
- keeping the compiled
target-repopointed at<org>/<platform-repo> - preflighting
getWorkflow()against<org>/<platform-repo>/<platform-worker>.lock.ymlwith the same authenticated client - observing
getWorkflow()succeed - observing
createWorkflowDispatch()still returnNot Found - monkey-patching the actual
createWorkflowDispatch()call to sendowner=<org>,repo=<platform-repo> - observing the downstream worker run get created successfully
That sequence rules out missing installation scope, missing workflow visibility, and workflow filename resolution as the primary defect. The runtime is dispatching to the wrong repository.
Affected Code
Current runtime behavior in actions/setup/js/dispatch_workflow.cjs:
const repo = context.repo;
const getDefaultBranchRef = async () => {
if (context.payload.repository?.default_branch) {
return `refs/heads/${context.payload.repository.default_branch}`;
}
const { data: repoData } = await githubClient.rest.repos.get({
owner: repo.owner,
repo: repo.repo,
});
return `refs/heads/${repoData.default_branch}`;
};
await githubClient.rest.actions.createWorkflowDispatch({
owner: repo.owner,
repo: repo.repo,
workflow_id: workflowFile,
ref,
inputs,
return_run_details: true,
});What is wrong:
repois alwayscontext.repotarget-repois ignored- default-branch lookup also uses the wrong repository
- cross-repo
dispatch-workflowsafe outputs therefore cannot dispatch into the compiled destination repo
Proposed Fix
Update actions/setup/js/dispatch_workflow.cjs to resolve the dispatch repository from the compiled target-repo config before doing either default-branch lookup or createWorkflowDispatch().
Paste-ready replacement:
const { resolveTargetRepoConfig, parseRepoSlug } = require("./repo_helpers.cjs");
async function main(config = {}) {
const allowedWorkflows = config.workflows || [];
const maxCount = config.max || 1;
const workflowFiles = config.workflow_files || {};
const githubClient = await createAuthenticatedGitHubClient(config);
const { defaultTargetRepo } = resolveTargetRepoConfig(config);
const resolvedRepoSlug =
defaultTargetRepo || `${context.repo.owner}/${context.repo.repo}`;
const repo = parseRepoSlug(resolvedRepoSlug) || context.repo;
const getDefaultBranchRef = async () => {
if (
resolvedRepoSlug === `${context.repo.owner}/${context.repo.repo}` &&
context.payload.repository?.default_branch
) {
return `refs/heads/${context.payload.repository.default_branch}`;
}
const { data: repoData } = await githubClient.rest.repos.get({
owner: repo.owner,
repo: repo.repo,
});
return `refs/heads/${repoData.default_branch}`;
};
// ... existing workflow selection, validation, and ref logic ...
await githubClient.rest.actions.createWorkflowDispatch({
owner: repo.owner,
repo: repo.repo,
workflow_id: workflowFile,
ref,
inputs,
return_run_details: true,
});
}This is the minimum fix needed for cross-repository dispatch:
- honor
target-repo - use the resolved repo for default-branch lookup
- use the resolved repo for
createWorkflowDispatch()
Implementation Plan
-
Update runtime repository resolution
- Edit
actions/setup/js/dispatch_workflow.cjs - Import
resolveTargetRepoConfigandparseRepoSlugfromactions/setup/js/repo_helpers.cjs - Replace the hard-coded
const repo = context.repopath with target-repo-aware resolution - Use the resolved repo for both default-branch lookup and
createWorkflowDispatch()
- Edit
-
Add runtime tests
- Edit
actions/setup/js/dispatch_workflow.test.cjs - Add test:
dispatches to target-repo when configured- config contains
target-repo: <org>/<platform-repo> context.repois<org>/<app-repo>- assert
createWorkflowDispatch()receives<org>/<platform-repo>
- config contains
- Add test:
default-branch lookup uses target-repo- config contains
target-repo: <org>/<platform-repo> - no explicit
ref - assert
repos.get()is called for<org>/<platform-repo>
- config contains
- Add regression test:
falls back to context.repo when no target-repo is configured
- Edit
-
Add focused integration coverage
- Add or extend a cross-repo safe-output test fixture so a caller-hosted relay compiles with
dispatch-workflow.target-repo: <org>/<platform-repo> - Assert that the runtime dispatch path uses the compiled destination repo rather than the caller repo
- Add or extend a cross-repo safe-output test fixture so a caller-hosted relay compiles with
-
Update docs
- Update the cross-repository/safe-outputs documentation to explicitly state that
dispatch-workflow.target-repocontrols the dispatch destination repository - Add a short note in the central-repo pattern docs that caller-hosted relays depend on
dispatch_workflowhonoringtarget-repo
- Update the cross-repository/safe-outputs documentation to explicitly state that
-
Validate
- Run
make agent-finish - Confirm no regression in same-repo dispatch behavior
- Run
Reproduction
-
Use gh-aw
mainas of 2026-03-12. -
Create a platform workflow in
<org>/<platform-repo>with a worker that supportsworkflow_dispatch. -
In
<org>/<app-repo>, create a caller-hosted relay that invokes<org>/<platform-repo>/.github/workflows/<platform-gateway>.lock.yml@mainviauses:. -
Ensure the compiled safe-outputs config contains:
dispatch-workflow: target-repo: "<org>/<platform-repo>" workflows: ["<platform-worker>"]
-
Trigger the relay from
<org>/<app-repo>. -
Observe the relay failing in
Process Safe Outputswith:Failed to dispatch workflow "<platform-worker>": Not Found -
In the same failing run, confirm that a direct
getWorkflow()against<org>/<platform-repo>/<platform-worker>.lock.ymlsucceeds with the same authenticated client. -
Temporarily monkey-patch the generated lock file so the actual
createWorkflowDispatch()call is forced toowner=<org>,repo=<platform-repo>. -
Re-run the same topology.
-
Observe that the relay now succeeds and creates the downstream worker run.
Private runs used to verify the bug and the workaround are available as opaque run IDs on request; they are omitted here because they belong to private repositories.
Relationship to #20508, #20567, and #20658
- Activation job missing
ref:in cross-repo checkout for workflow_call triggers #20508 is about missing activation checkoutref:for cross-repoworkflow_calltriggers. - Cross-repo activation checkout still broken for event-driven relay workflows after #20301 #20567 addressed activation checkout repository resolution for event-driven relays.
- Bug:
Checkout actions folderemitted withoutrepository:orref:—Setup Scriptsfails in cross-repo relay #20658 is aboutCheckout actions folderbeing emitted withoutrepository:/ref:.
This issue is downstream of all three. Even after activation checkout is fixed and the actions/ checkout works, caller-hosted relays still cannot dispatch sibling worker workflows cross-repo because dispatch_workflow.cjs ignores the compiled target-repo and always dispatches against context.repo.