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-repo pointed at <org>/<platform-repo>
- preflighting
getWorkflow() against <org>/<platform-repo>/<platform-worker>.lock.yml with the same authenticated client
- observing
getWorkflow() succeed
- observing
createWorkflowDispatch() still return Not Found
- monkey-patching the actual
createWorkflowDispatch() call to send owner=<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:
repo is always context.repo
target-repo is ignored
- default-branch lookup also uses the wrong repository
- cross-repo
dispatch-workflow safe 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
resolveTargetRepoConfig and parseRepoSlug from actions/setup/js/repo_helpers.cjs
- Replace the hard-coded
const repo = context.repo path with target-repo-aware resolution
- Use the resolved repo for both default-branch lookup and
createWorkflowDispatch()
-
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.repo is <org>/<app-repo>
- assert
createWorkflowDispatch() receives <org>/<platform-repo>
- 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>
- Add regression test:
falls back to context.repo when no target-repo is configured
-
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
-
Update docs
- Update the cross-repository/safe-outputs documentation to explicitly state that
dispatch-workflow.target-repo controls the dispatch destination repository
- Add a short note in the central-repo pattern docs that caller-hosted relays depend on
dispatch_workflow honoring target-repo
-
Validate
- Run
make agent-finish
- Confirm no regression in same-repo dispatch behavior
Reproduction
-
Use gh-aw main as of 2026-03-12.
-
Create a platform workflow in <org>/<platform-repo> with a worker that supports workflow_dispatch.
-
In <org>/<app-repo>, create a caller-hosted relay that invokes <org>/<platform-repo>/.github/workflows/<platform-gateway>.lock.yml@main via uses:.
-
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 Outputs with:
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.yml succeeds with the same authenticated client.
-
Temporarily monkey-patch the generated lock file so the actual createWorkflowDispatch() call is forced to owner=<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.
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.
Summary
Observed on gh-aw
mainas of 2026-03-12, after the activation-checkout fixes discussed in #20567 and theCheckout actions folderfix discussed in #20658. In a caller-hosted relay topology where<org>/<app-repo>calls<org>/<platform-repo>/.github/workflows/<platform-gateway>.lock.ymlviauses:, the compiler emitssafe-outputs.dispatch-workflow.target-repo: "<org>/<platform-repo>", but the runtime still dispatches against the caller repository. The relay fails inProcess Safe OutputswithFailed to dispatch workflow "<platform-worker>": Not Founduntil the generated lock file is monkey-patched to override the actualcreateWorkflowDispatch()ownerandrepoparameters. Once that override is applied, the same topology successfully creates the downstream worker run. This isolates the bug to runtime repository resolution insidedispatch_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-repovalue, but it never uses it when resolving the dispatch destination. Instead, it resolves the destination repository fromcontext.repo, which is the caller repository in a cross-repository reusable-workflow invocation.The broken logic is the repository selection immediately before
createWorkflowDispatch():In a caller-hosted relay topology:
context.repo=<org>/<app-repo>target-repo=<org>/<platform-repo><org>/<platform-repo>So the runtime looks for
<platform-worker>.lock.ymlin the wrong repository and GitHub returnsNot Found.This was proven in a live run by:
target-repopointed at<org>/<platform-repo>getWorkflow()against<org>/<platform-repo>/<platform-worker>.lock.ymlwith the same authenticated clientgetWorkflow()succeedcreateWorkflowDispatch()still returnNot FoundcreateWorkflowDispatch()call to sendowner=<org>,repo=<platform-repo>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:What is wrong:
repois alwayscontext.repotarget-repois ignoreddispatch-workflowsafe outputs therefore cannot dispatch into the compiled destination repoProposed Fix
Update
actions/setup/js/dispatch_workflow.cjsto resolve the dispatch repository from the compiledtarget-repoconfig before doing either default-branch lookup orcreateWorkflowDispatch().Paste-ready replacement:
This is the minimum fix needed for cross-repository dispatch:
target-repocreateWorkflowDispatch()Implementation Plan
Update runtime repository resolution
actions/setup/js/dispatch_workflow.cjsresolveTargetRepoConfigandparseRepoSlugfromactions/setup/js/repo_helpers.cjsconst repo = context.repopath with target-repo-aware resolutioncreateWorkflowDispatch()Add runtime tests
actions/setup/js/dispatch_workflow.test.cjsdispatches to target-repo when configuredtarget-repo: <org>/<platform-repo>context.repois<org>/<app-repo>createWorkflowDispatch()receives<org>/<platform-repo>default-branch lookup uses target-repotarget-repo: <org>/<platform-repo>refrepos.get()is called for<org>/<platform-repo>falls back to context.repo when no target-repo is configuredAdd focused integration coverage
dispatch-workflow.target-repo: <org>/<platform-repo>Update docs
dispatch-workflow.target-repocontrols the dispatch destination repositorydispatch_workflowhonoringtarget-repoValidate
make agent-finishReproduction
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:
Trigger the relay from
<org>/<app-repo>.Observe the relay failing in
Process Safe Outputswith: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
ref:in cross-repo checkout for workflow_call triggers #20508 is about missing activation checkoutref:for cross-repoworkflow_calltriggers.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 becausedispatch_workflow.cjsignores the compiledtarget-repoand always dispatches againstcontext.repo.