Skip to content

Bug: dispatch_workflow ignores target-repo and dispatches to context.repo in cross-repo relays #20694

@johnwilliams-12

Description

@johnwilliams-12

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:

  1. keeping the compiled target-repo pointed at <org>/<platform-repo>
  2. preflighting getWorkflow() against <org>/<platform-repo>/<platform-worker>.lock.yml with the same authenticated client
  3. observing getWorkflow() succeed
  4. observing createWorkflowDispatch() still return Not Found
  5. monkey-patching the actual createWorkflowDispatch() call to send owner=<org>, repo=<platform-repo>
  6. 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

  1. 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()
  2. 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
  3. 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
  4. 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
  5. Validate

    • Run make agent-finish
    • Confirm no regression in same-repo dispatch behavior

Reproduction

  1. Use gh-aw main as of 2026-03-12.

  2. Create a platform workflow in <org>/<platform-repo> with a worker that supports workflow_dispatch.

  3. In <org>/<app-repo>, create a caller-hosted relay that invokes <org>/<platform-repo>/.github/workflows/<platform-gateway>.lock.yml@main via uses:.

  4. Ensure the compiled safe-outputs config contains:

    dispatch-workflow:
      target-repo: "<org>/<platform-repo>"
      workflows: ["<platform-worker>"]
  5. Trigger the relay from <org>/<app-repo>.

  6. Observe the relay failing in Process Safe Outputs with:

    Failed to dispatch workflow "<platform-worker>": Not Found
    
  7. In the same failing run, confirm that a direct getWorkflow() against <org>/<platform-repo>/<platform-worker>.lock.yml succeeds with the same authenticated client.

  8. Temporarily monkey-patch the generated lock file so the actual createWorkflowDispatch() call is forced to owner=<org>, repo=<platform-repo>.

  9. Re-run the same topology.

  10. 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

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.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions