Skip to content

fix: dispatch_workflow honors target-repo in cross-repo relays#20708

Merged
pelikhan merged 3 commits intomainfrom
copilot/bugfix-dispatch-workflow-target-repo
Mar 12, 2026
Merged

fix: dispatch_workflow honors target-repo in cross-repo relays#20708
pelikhan merged 3 commits intomainfrom
copilot/bugfix-dispatch-workflow-target-repo

Conversation

Copy link
Contributor

Copilot AI commented Mar 12, 2026

In caller-hosted relay topologies, dispatch_workflow always dispatched against context.repo (the caller) regardless of the compiled target-repo value, causing createWorkflowDispatch to return Not Found when the worker workflow lived in a different repository.

Changes

  • actions/setup/js/dispatch_workflow.cjs

    • Import resolveTargetRepoConfig and parseRepoSlug from repo_helpers.cjs
    • Normalize target-repo with nullish coalescing and trim before parsing; emit a warning (including the invalid value and fallback repo) and fall back to context.repo when the configured slug is malformed — no silent failures
    • Extract isCrossRepoDispatch boolean to avoid repeated slug comparison
    • getDefaultBranchRef now skips the context.payload.repository.default_branch shortcut for cross-repo dispatch — that value belongs to the caller, not the target
  • actions/setup/js/dispatch_workflow.test.cjs — four new tests:

    • dispatches to target-repo when configured — asserts createWorkflowDispatch receives the target owner/repo, not context.repo
    • default-branch lookup uses target-repo when configured — asserts repos.get targets the configured repo and the caller's payload default_branch is ignored; test wrapped in try/finally to restore mutated globals
    • falls back to context.repo when no target-repo is configured — regression guard for same-repo behavior
    • falls back to context.repo and warns when target-repo is an invalid slug — covers the new validation path
    • beforeEach now resets global.context.ref and global.context.payload to a known baseline for order-independent test execution
  • docs/src/content/docs/reference/safe-outputs-specification.md

    • Updated dispatch_workflow Cross-Repository Support from No (same repository only) to Yes (via \target-repo`)`
    • Added Configuration Parameters section listing target-repo and allowed-repos
    • Added cross-repo note about requiring actions: write in the target repository
// Before — always used caller repo
const repo = context.repo;

// After — normalized, validated, falls back with warning on bad slug
const contextRepoSlug = `${context.repo.owner}/${context.repo.repo}`;
const normalizedTargetRepo = (defaultTargetRepo ?? "").toString().trim();
let resolvedRepoSlug = contextRepoSlug;
let repo = context.repo;
if (normalizedTargetRepo) {
  const parsedRepo = parseRepoSlug(normalizedTargetRepo);
  if (!parsedRepo) {
    core.warning(`Invalid 'target-repo' configuration value '${normalizedTargetRepo}'; falling back to workflow context repository ${contextRepoSlug}.`);
  } else {
    resolvedRepoSlug = normalizedTargetRepo;
    repo = parsedRepo;
  }
}
const isCrossRepoDispatch = resolvedRepoSlug !== contextRepoSlug;

📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

In a caller-hosted relay topology, dispatch_workflow always dispatched
to context.repo (the caller's repository) because the compiled
target-repo value was never used. This caused createWorkflowDispatch
to return Not Found when the worker workflow lived in a different repo.

Fix: import resolveTargetRepoConfig and parseRepoSlug from repo_helpers,
resolve the dispatch destination from target-repo config, and use the
resolved repo for both default-branch lookup and createWorkflowDispatch.
Same-repo behavior is unchanged (falls back to context.repo).

Tests: add 3 tests covering cross-repo dispatch, default-branch lookup
using target-repo, and regression for same-repo fallback.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix dispatch_workflow ignoring target-repo in cross-repo relays fix: dispatch_workflow honors target-repo in cross-repo relays Mar 12, 2026
Copilot AI requested a review from pelikhan March 12, 2026 17:40
@pelikhan pelikhan marked this pull request as ready for review March 12, 2026 17:43
Copilot AI review requested due to automatic review settings March 12, 2026 17:43
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes cross-repository relay behavior in dispatch_workflow by resolving the dispatch destination repo from configured target-repo (instead of always using context.repo), and ensures default-branch resolution targets the correct repository.

Changes:

  • Resolve dispatch owner/repo from target-repo (fallback to context.repo when not configured).
  • Adjust default-branch lookup to avoid using caller payload defaults when dispatching cross-repo.
  • Add tests covering cross-repo dispatch, cross-repo default-branch lookup, and same-repo fallback behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
actions/setup/js/dispatch_workflow.cjs Resolve dispatch repo from target-repo and adjust default-branch behavior for cross-repo dispatch.
actions/setup/js/dispatch_workflow.test.cjs Add regression tests for cross-repo dispatch and default-branch lookup, plus same-repo fallback.
Comments suppressed due to low confidence (1)

actions/setup/js/dispatch_workflow.cjs:31

  • isCrossRepoDispatch is computed from the raw slug string, but repo may fall back to context.repo when parsing fails. That can produce inconsistent behavior (e.g., logging “Dispatching to target repo …” while actually dispatching to the caller repo, and skipping the payload default_branch shortcut). Recommend computing cross-repo status from the parsed repo vs context.repo (after normalization), and/or only setting isCrossRepoDispatch when parsing succeeds.
  // Resolve the dispatch destination repository from target-repo config, falling back to context.repo
  const resolvedRepoSlug = defaultTargetRepo || `${context.repo.owner}/${context.repo.repo}`;
  const repo = parseRepoSlug(resolvedRepoSlug) || context.repo;
  const isCrossRepoDispatch = resolvedRepoSlug !== `${context.repo.owner}/${context.repo.repo}`;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +40 to +42
if (isCrossRepoDispatch) {
core.info(`Dispatching to target repo: ${resolvedRepoSlug}`);
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dispatch_workflow now supports cross-repo dispatch via target-repo, but the safe-outputs specification currently documents this type as “Cross-Repository Support: No” (docs/src/content/docs/reference/safe-outputs-specification.md around the dispatch_workflow section). Please update the spec (and consider whether cross-repo dispatch should honor an allowed_repos allowlist similar to other handlers) so the documented contract matches the new behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +573 to +605
delete process.env.GITHUB_REF;
delete process.env.GITHUB_HEAD_REF;
global.context.ref = undefined;
// context.payload has a default_branch for the caller repo – must be ignored for cross-repo dispatch
global.context.payload = { repository: { default_branch: "caller-main" } };

github.rest.repos.get.mockResolvedValueOnce({
data: { default_branch: "platform-main" },
});

const config = {
"target-repo": "platform-org/platform-repo",
workflows: ["platform-worker"],
workflow_files: { "platform-worker": ".lock.yml" },
};
const handler = await main(config);

const result = await handler({ type: "dispatch_workflow", workflow_name: "platform-worker", inputs: {} }, {});

expect(result.success).toBe(true);
// Default-branch API lookup must target the configured target-repo
expect(github.rest.repos.get).toHaveBeenCalledWith({
owner: "platform-org",
repo: "platform-repo",
});
// Dispatch must use the target repo's default branch
expect(github.rest.actions.createWorkflowDispatch).toHaveBeenCalledWith(
expect.objectContaining({
owner: "platform-org",
repo: "platform-repo",
ref: "refs/heads/platform-main",
})
);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new tests mutate shared globals (global.context.ref / global.context.payload) but don’t restore them (and beforeEach only resets env/mocks). To keep the suite order-independent, reset global.context to a known baseline in beforeEach (or restore within each test) so future tests added after these won’t inherit modified context state.

Suggested change
delete process.env.GITHUB_REF;
delete process.env.GITHUB_HEAD_REF;
global.context.ref = undefined;
// context.payload has a default_branch for the caller repo – must be ignored for cross-repo dispatch
global.context.payload = { repository: { default_branch: "caller-main" } };
github.rest.repos.get.mockResolvedValueOnce({
data: { default_branch: "platform-main" },
});
const config = {
"target-repo": "platform-org/platform-repo",
workflows: ["platform-worker"],
workflow_files: { "platform-worker": ".lock.yml" },
};
const handler = await main(config);
const result = await handler({ type: "dispatch_workflow", workflow_name: "platform-worker", inputs: {} }, {});
expect(result.success).toBe(true);
// Default-branch API lookup must target the configured target-repo
expect(github.rest.repos.get).toHaveBeenCalledWith({
owner: "platform-org",
repo: "platform-repo",
});
// Dispatch must use the target repo's default branch
expect(github.rest.actions.createWorkflowDispatch).toHaveBeenCalledWith(
expect.objectContaining({
owner: "platform-org",
repo: "platform-repo",
ref: "refs/heads/platform-main",
})
);
const originalRef = global.context.ref;
const originalPayload = global.context.payload;
try {
delete process.env.GITHUB_REF;
delete process.env.GITHUB_HEAD_REF;
global.context.ref = undefined;
// context.payload has a default_branch for the caller repo – must be ignored for cross-repo dispatch
global.context.payload = { repository: { default_branch: "caller-main" } };
github.rest.repos.get.mockResolvedValueOnce({
data: { default_branch: "platform-main" },
});
const config = {
"target-repo": "platform-org/platform-repo",
workflows: ["platform-worker"],
workflow_files: { "platform-worker": ".lock.yml" },
};
const handler = await main(config);
const result = await handler({ type: "dispatch_workflow", workflow_name: "platform-worker", inputs: {} }, {});
expect(result.success).toBe(true);
// Default-branch API lookup must target the configured target-repo
expect(github.rest.repos.get).toHaveBeenCalledWith({
owner: "platform-org",
repo: "platform-repo",
});
// Dispatch must use the target repo's default branch
expect(github.rest.actions.createWorkflowDispatch).toHaveBeenCalledWith(
expect.objectContaining({
owner: "platform-org",
repo: "platform-repo",
ref: "refs/heads/platform-main",
})
);
} finally {
global.context.ref = originalRef;
global.context.payload = originalPayload;
}

Copilot uses AI. Check for mistakes.
Comment on lines 29 to 32
const resolvedRepoSlug = defaultTargetRepo || `${context.repo.owner}/${context.repo.repo}`;
const repo = parseRepoSlug(resolvedRepoSlug) || context.repo;
const isCrossRepoDispatch = resolvedRepoSlug !== `${context.repo.owner}/${context.repo.repo}`;

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultTargetRepo can be non-string (or contain whitespace), and parseRepoSlug(resolvedRepoSlug) will throw if it’s not a string (because .split won’t exist). Consider normalizing with String(...).trim() before parsing, and treat an explicitly configured but invalid target-repo as a configuration error (or at least warn and avoid silently falling back to context.repo).

This issue also appears on line 28 of the same file.

Suggested change
const resolvedRepoSlug = defaultTargetRepo || `${context.repo.owner}/${context.repo.repo}`;
const repo = parseRepoSlug(resolvedRepoSlug) || context.repo;
const isCrossRepoDispatch = resolvedRepoSlug !== `${context.repo.owner}/${context.repo.repo}`;
const contextRepoSlug = `${context.repo.owner}/${context.repo.repo}`;
const normalizedTargetRepo =
defaultTargetRepo == null
? ""
: String(defaultTargetRepo).trim();
let resolvedRepoSlug = contextRepoSlug;
let repo = context.repo;
if (normalizedTargetRepo) {
try {
const parsedRepo = parseRepoSlug(normalizedTargetRepo);
if (!parsedRepo) {
core.warning(
`Invalid 'target-repo' configuration value '${normalizedTargetRepo}'; falling back to workflow context repository ${contextRepoSlug}.`
);
} else {
resolvedRepoSlug = normalizedTargetRepo;
repo = parsedRepo;
}
} catch (error) {
core.warning(
`Error parsing 'target-repo' configuration value '${normalizedTargetRepo}': ${getErrorMessage(
error
)}; falling back to workflow context repository ${contextRepoSlug}.`
);
}
}
const isCrossRepoDispatch = resolvedRepoSlug !== contextRepoSlug;

Copilot uses AI. Check for mistakes.
@pelikhan
Copy link
Contributor

@copilot review comments, add tests

- Normalize target-repo with nullish coalescing (??) and trim before parsing
- Warn with invalid value and fallback repo when target-repo slug is malformed
- Fix test isolation: reset global.context in beforeEach; wrap mutating test in try/finally
- Add test: warns and falls back to context.repo when target-repo is an invalid slug
- Update safe-outputs-specification.md: dispatch_workflow now has cross-repo support via target-repo

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 12, 2026

@copilot review comments, add tests

Addressed all three review comments in db9c64d:

  1. Validation + warning (dispatch_workflow.cjs line 29-32): normalized target-repo with ?? "" and .trim(), emit a warning with the invalid value and fallback repo when the slug can't be parsed (e.g. missing /), then fall back to context.repo.
  2. Test isolation (dispatch_workflow.test.cjs): beforeEach now resets global.context.ref and global.context.payload to a known baseline; the test that mutates those globals is wrapped in try/finally.
  3. Docs (safe-outputs-specification.md): changed dispatch_workflow Cross-Repository Support from "No (same repository only)" to "Yes (via target-repo)" and added Configuration Parameters and a cross-repo Notes entry.
  4. Bonus test: added "falls back to context.repo and warns when target-repo is an invalid slug" to cover the new validation path.

Copilot AI requested a review from pelikhan March 12, 2026 18:00
@pelikhan pelikhan merged commit d478096 into main Mar 12, 2026
81 checks passed
@pelikhan pelikhan deleted the copilot/bugfix-dispatch-workflow-target-repo branch March 12, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants