-
Notifications
You must be signed in to change notification settings - Fork 341
Fix resolve_host_repo.cjs to correctly identify callee repo in cross-org workflow_call #24974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+361
−16
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
c41cca3
Initial plan
Copilot 66546b1
Fix resolve_host_repo.cjs to correctly identify callee repo in cross-…
Copilot 4ff79f7
Address code review feedback: safer runId extraction and test state c…
Copilot 51fb1ca
Refactor: extract REPO_PREFIX_RE constant and improve runId readability
Copilot 5560c5a
Address final code review feedback: simplify runId, deduplicate regex…
Copilot c61dc1e
Address PR review comments: ambiguity guard, safe-outputs-exempt tag,…
Copilot 89801cc
Polish: rename mockNoReferencedWorkflowsOnce and update beforeEach co…
Copilot 49e9b1d
Improve logging in resolve_host_repo.cjs for easier debugging
Copilot 33dab50
Simplify calleeRefSource ternary chain to if-else for readability
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,41 +10,140 @@ | |
| * so the expression introduced in #20301 incorrectly fell back to github.repository | ||
| * (the caller's repo) instead of the platform repo. | ||
| * | ||
| * GITHUB_WORKFLOW_REF always reflects the currently executing workflow file, not the | ||
| * triggering event. Its format is: | ||
| * GITHUB_WORKFLOW_REF reflects the currently executing workflow file for most triggers, but | ||
| * in cross-org workflow_call scenarios it resolves to the TOP-LEVEL CALLER's workflow ref, | ||
| * not the reusable (callee) workflow being executed. Its format is: | ||
| * owner/repo/.github/workflows/file.yml@refs/heads/main | ||
| * | ||
| * When the platform workflow runs cross-repo (called via uses:), GITHUB_WORKFLOW_REF | ||
| * starts with the platform repo slug, while GITHUB_REPOSITORY is the caller repo. | ||
| * Comparing the two lets us detect cross-repo invocations without relying on event_name. | ||
| * When the platform workflow runs cross-repo (called via uses: from the same org), | ||
| * GITHUB_WORKFLOW_REF starts with the platform repo slug, while GITHUB_REPOSITORY is the | ||
| * caller repo. Comparing the two lets us detect cross-repo invocations without relying on | ||
| * event_name. | ||
| * | ||
| * For cross-org workflow_call, GITHUB_WORKFLOW_REF and GITHUB_REPOSITORY both resolve to | ||
| * the caller's repo. In that case we fall back to the referenced_workflows API lookup to | ||
| * find the actual callee (platform) repo and ref. | ||
| * | ||
| * In a caller-hosted relay pinned to a feature branch (e.g. uses: platform/.github/workflows/ | ||
| * gateway.lock.yml@feature-branch), the @feature-branch portion is encoded in | ||
| * GITHUB_WORKFLOW_REF. Emitting it as target_ref allows the activation checkout to use | ||
| * the correct branch rather than the platform repo's default branch. | ||
| * | ||
| * SEC-005: The targetRepo and targetRef values are resolved solely from trusted system | ||
| * environment variables (GITHUB_WORKFLOW_REF, GITHUB_REPOSITORY, GITHUB_REF) set by the | ||
| * GitHub Actions runtime. They are not derived from user-supplied input, so no allowlist | ||
| * check is required in this handler. | ||
| * environment variables (GITHUB_WORKFLOW_REF, GITHUB_REPOSITORY, GITHUB_REF) and the | ||
| * GitHub Actions API (referenced_workflows), set/provided by the GitHub Actions runtime. | ||
| * They are not derived from user-supplied input, so no allowlist check is required here. | ||
| * | ||
| * @safe-outputs-exempt SEC-005: values sourced from trusted runtime env vars only | ||
| * @safe-outputs-exempt SEC-005: values sourced from trusted GitHub Actions runtime env vars and referenced_workflows API only | ||
| */ | ||
|
|
||
| // Matches the "owner/repo" prefix from a GitHub workflow path of the form "owner/repo/...". | ||
| const REPO_PREFIX_RE = /^([^/]+\/[^/]+)\//; | ||
|
|
||
| /** | ||
| * Attempts to resolve the callee repository and ref from the referenced_workflows API. | ||
| * | ||
| * This is used as a fallback when GITHUB_WORKFLOW_REF points to the same repo as | ||
| * GITHUB_REPOSITORY (cross-org workflow_call scenario), because in that case | ||
| * GITHUB_WORKFLOW_REF reflects the caller's workflow ref, not the callee's. | ||
| * | ||
| * @param {string} currentRepo - The value of GITHUB_REPOSITORY (owner/repo) | ||
| * @returns {Promise<{repo: string, ref: string} | null>} Resolved callee repo and ref, or null | ||
| */ | ||
| async function resolveFromReferencedWorkflows(currentRepo) { | ||
| const rawRunId = process.env.GITHUB_RUN_ID; | ||
| const runId = rawRunId ? parseInt(rawRunId, 10) : typeof context.runId === "number" ? context.runId : NaN; | ||
| if (!Number.isFinite(runId)) { | ||
| core.info("Run ID is unavailable or invalid, cannot perform referenced_workflows lookup"); | ||
| return null; | ||
| } | ||
|
|
||
| const [runOwner, runRepo] = currentRepo.split("/"); | ||
| try { | ||
| core.info(`Checking for cross-org callee via referenced_workflows API (run ${runId}, repo ${currentRepo})`); | ||
| const runResponse = await github.rest.actions.getWorkflowRun({ | ||
| owner: runOwner, | ||
| repo: runRepo, | ||
| run_id: runId, | ||
| }); | ||
|
|
||
| const referencedWorkflows = runResponse.data.referenced_workflows || []; | ||
| core.info(`Found ${referencedWorkflows.length} referenced workflow(s) in run`); | ||
| for (const wf of referencedWorkflows) { | ||
| core.info(` referenced workflow: path=${wf.path} sha=${wf.sha || "(none)"} ref=${wf.ref || "(none)"}`); | ||
| } | ||
|
|
||
| // Collect all referenced workflows from a different repo than the caller. | ||
| // In cross-org workflow_call, the callee (platform) repo is different from currentRepo. | ||
| // If multiple cross-repo candidates are found we cannot safely pick one, so we bail out. | ||
| const crossRepoCandidates = []; | ||
| for (const wf of referencedWorkflows) { | ||
| const pathRepoMatch = wf.path.match(REPO_PREFIX_RE); | ||
| const entryRepo = pathRepoMatch ? pathRepoMatch[1] : ""; | ||
| if (entryRepo && entryRepo !== currentRepo) { | ||
| crossRepoCandidates.push({ wf, repo: entryRepo }); | ||
| } | ||
| } | ||
| core.info(`Found ${crossRepoCandidates.length} cross-repo candidate(s) (excluding current repo ${currentRepo})`); | ||
|
|
||
| if (crossRepoCandidates.length === 0) { | ||
| core.info("No cross-org callee found in referenced_workflows, using current repo"); | ||
| return null; | ||
| } | ||
|
|
||
| if (crossRepoCandidates.length > 1) { | ||
| core.info(`Referenced workflows lookup is ambiguous; found ${crossRepoCandidates.length} cross-repo candidates, not selecting one`); | ||
| for (const candidate of crossRepoCandidates) { | ||
| core.info(` Candidate referenced workflow path: ${candidate.wf.path}`); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| const matchingEntry = crossRepoCandidates[0].wf; | ||
| const calleeRepo = crossRepoCandidates[0].repo; | ||
|
|
||
| // Prefer sha (immutable) over ref (branch/tag can drift) over path-parsed ref. | ||
| const pathRefMatch = matchingEntry.path.match(/@(.+)$/); | ||
| let calleeRefSource; | ||
| if (matchingEntry.sha) { | ||
| calleeRefSource = "sha"; | ||
| } else if (matchingEntry.ref) { | ||
| calleeRefSource = "ref"; | ||
| } else if (pathRefMatch) { | ||
| calleeRefSource = "path"; | ||
| } else { | ||
| calleeRefSource = "none"; | ||
| } | ||
| const calleeRef = matchingEntry.sha || matchingEntry.ref || (pathRefMatch ? pathRefMatch[1] : ""); | ||
| core.info(`Resolved callee repo from referenced_workflows: ${calleeRepo} @ ${calleeRef || "(default branch)"} (source: ${calleeRefSource})`); | ||
| core.info(` Referenced workflow path: ${matchingEntry.path}`); | ||
| return { repo: calleeRepo, ref: calleeRef }; | ||
| } catch (error) { | ||
| const msg = error instanceof Error ? error.message : String(error); | ||
| core.info(`Could not fetch referenced_workflows from API: ${msg}, using current repo`); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async function main() { | ||
| const workflowRef = process.env.GITHUB_WORKFLOW_REF || ""; | ||
| const currentRepo = process.env.GITHUB_REPOSITORY || ""; | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| core.info(`GITHUB_WORKFLOW_REF: ${workflowRef || "(not set)"}`); | ||
| core.info(`GITHUB_REPOSITORY: ${currentRepo || "(not set)"}`); | ||
| core.info(`GITHUB_RUN_ID: ${process.env.GITHUB_RUN_ID || "(not set)"}`); | ||
|
|
||
| // GITHUB_WORKFLOW_REF format: owner/repo/.github/workflows/file.yml@ref | ||
| // The regex captures everything before the third slash segment (i.e., the owner/repo prefix). | ||
| const repoMatch = workflowRef.match(/^([^/]+\/[^/]+)\//); | ||
| const repoMatch = workflowRef.match(REPO_PREFIX_RE); | ||
| const workflowRepo = repoMatch ? repoMatch[1] : ""; | ||
| core.info(`Parsed workflow repo from GITHUB_WORKFLOW_REF: ${workflowRepo || "(could not parse)"}`); | ||
|
|
||
| // Fall back to currentRepo when GITHUB_WORKFLOW_REF cannot be parsed | ||
| const targetRepo = workflowRepo || currentRepo; | ||
| let targetRepo = workflowRepo || currentRepo; | ||
|
|
||
| // Extract the ref portion after '@' from GITHUB_WORKFLOW_REF. | ||
| // GITHUB_WORKFLOW_REF format: owner/repo/.github/workflows/file.yml@ref | ||
|
|
@@ -57,24 +156,49 @@ async function main() { | |
| // scenarios GITHUB_REF is the *caller* repo's ref, not the callee's, and using it | ||
| // would check out the wrong branch. | ||
| const refMatch = workflowRef.match(/@(.+)$/); | ||
| const targetRef = refMatch ? refMatch[1] : ""; | ||
| let targetRef = refMatch ? refMatch[1] : ""; | ||
| core.info(`Parsed workflow ref from GITHUB_WORKFLOW_REF: ${targetRef || "(none — will use default branch)"}`); | ||
|
|
||
| // Cross-org workflow_call detection: when GITHUB_WORKFLOW_REF points to the same repo as | ||
| // GITHUB_REPOSITORY, it means GITHUB_WORKFLOW_REF is resolving to the caller's workflow | ||
| // (not the callee's). This happens in cross-org workflow_call invocations where GitHub | ||
| // Actions sets GITHUB_WORKFLOW_REF to the top-level caller's workflow ref rather than the | ||
| // reusable workflow being executed. In that case, fall back to the referenced_workflows API | ||
| // to find the actual callee (platform) repo and ref. | ||
| // | ||
| // Note: GITHUB_EVENT_NAME inside a reusable workflow reflects the ORIGINAL trigger event | ||
| // (e.g., "push", "issues"), NOT "workflow_call", so we cannot use event_name to detect | ||
| // this scenario. | ||
| if (workflowRepo && workflowRepo === currentRepo) { | ||
| core.info(`Cross-org workflow_call detected (workflowRepo === currentRepo = ${currentRepo}): falling back to referenced_workflows API`); | ||
| const resolved = await resolveFromReferencedWorkflows(currentRepo); | ||
| if (resolved) { | ||
| targetRepo = resolved.repo; | ||
| targetRef = resolved.ref || targetRef; | ||
| } else { | ||
| core.info("referenced_workflows lookup returned no result; keeping current repo as target"); | ||
| } | ||
| } else if (!workflowRepo) { | ||
| core.info("Could not parse workflowRepo from GITHUB_WORKFLOW_REF; falling back to GITHUB_REPOSITORY"); | ||
| } else { | ||
| core.info(`Same-org cross-repo invocation: workflowRepo=${workflowRepo}, currentRepo=${currentRepo}`); | ||
| } | ||
|
|
||
| core.info(`GITHUB_WORKFLOW_REF: ${workflowRef}`); | ||
| core.info(`GITHUB_REPOSITORY: ${currentRepo}`); | ||
| core.info(`Resolved host repo for activation checkout: ${targetRepo}`); | ||
| core.info(`Resolved host ref for activation checkout: ${targetRef}`); | ||
| core.info(`Resolved host ref for activation checkout: ${targetRef || "(default branch)"}`); | ||
|
|
||
| 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.info(`Same-repo invocation: checking out ${targetRepo} @ ${targetRef || "(default branch)"}`); | ||
| } | ||
|
|
||
| // Compute the repository name (without owner prefix) for use cases that require | ||
| // only the repo name, such as actions/create-github-app-token which expects | ||
| // `repositories` to contain repo names only when `owner` is also provided. | ||
| const targetRepoName = targetRepo.split("/").at(-1); | ||
| core.info(`target_repo=${targetRepo} target_repo_name=${targetRepoName} target_ref=${targetRef || "(default branch)"}`); | ||
|
|
||
| core.setOutput("target_repo", targetRepo); | ||
| core.setOutput("target_repo_name", targetRepoName); | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot add more logging
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in
49e9b1d: logscurrentRepoandrunIdat function entry; dumps everyreferenced_workflowentry (path/sha/ref) as they're scanned; logs cross-repo candidate count after filtering; logs which ref source was used (sha/ref/path/none) when a single candidate is selected.