-
Notifications
You must be signed in to change notification settings - Fork 343
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
Changes from 5 commits
c41cca3
66546b1
4ff79f7
51fb1ca
5560c5a
c61dc1e
89801cc
49e9b1d
33dab50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,27 +10,100 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
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. @copilot add more logging
Contributor
Author
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. Added in |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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})`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find the first referenced workflow from a different repo than the caller. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // In cross-org workflow_call, the callee (platform) repo is different from currentRepo | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (the caller's repo). For same-repo invocations there will be no cross-repo entry. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Capture the repo from the path match so we don't run the regex twice. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let calleeRepo = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let matchingEntry = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const wf of referencedWorkflows) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pathRepoMatch = wf.path.match(REPO_PREFIX_RE); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entryRepo = pathRepoMatch ? pathRepoMatch[1] : ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entryRepo && entryRepo !== currentRepo) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| matchingEntry = wf; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| calleeRepo = entryRepo; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (matchingEntry) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Prefer sha (immutable) over ref (branch/tag can drift) over path-parsed ref. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pathRefMatch = matchingEntry.path.match(/@(.+)$/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const calleeRef = matchingEntry.sha || matchingEntry.ref || (pathRefMatch ? pathRefMatch[1] : ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| core.info(`Resolved callee repo from referenced_workflows: ${calleeRepo} @ ${calleeRef || "(default branch)"}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| core.info(` Referenced workflow path: ${matchingEntry.path}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { repo: calleeRepo, ref: calleeRef }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| core.info("No cross-org callee found in referenced_workflows, using current repo"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find the first referenced workflow from a different repo than the caller. | |
| // In cross-org workflow_call, the callee (platform) repo is different from currentRepo | |
| // (the caller's repo). For same-repo invocations there will be no cross-repo entry. | |
| // Capture the repo from the path match so we don't run the regex twice. | |
| let calleeRepo = ""; | |
| let matchingEntry = null; | |
| for (const wf of referencedWorkflows) { | |
| const pathRepoMatch = wf.path.match(REPO_PREFIX_RE); | |
| const entryRepo = pathRepoMatch ? pathRepoMatch[1] : ""; | |
| if (entryRepo && entryRepo !== currentRepo) { | |
| matchingEntry = wf; | |
| calleeRepo = entryRepo; | |
| break; | |
| } | |
| } | |
| if (matchingEntry) { | |
| // Prefer sha (immutable) over ref (branch/tag can drift) over path-parsed ref. | |
| const pathRefMatch = matchingEntry.path.match(/@(.+)$/); | |
| const calleeRef = matchingEntry.sha || matchingEntry.ref || (pathRefMatch ? pathRefMatch[1] : ""); | |
| core.info(`Resolved callee repo from referenced_workflows: ${calleeRepo} @ ${calleeRef || "(default branch)"}`); | |
| core.info(` Referenced workflow path: ${matchingEntry.path}`); | |
| return { repo: calleeRepo, ref: calleeRef }; | |
| } else { | |
| core.info("No cross-org callee found in referenced_workflows, using current repo"); | |
| return null; | |
| } | |
| // Only resolve when there is exactly one cross-repo candidate. | |
| // If multiple cross-repo reusable workflows are referenced, selecting the first one | |
| // would be a guess and could resolve the wrong callee repository/ref. | |
| 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 }); | |
| } | |
| } | |
| 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(/@(.+)$/); | |
| const calleeRef = matchingEntry.sha || matchingEntry.ref || (pathRefMatch ? pathRefMatch[1] : ""); | |
| core.info(`Resolved callee repo from referenced_workflows: ${calleeRepo} @ ${calleeRef || "(default branch)"}`); | |
| core.info(` Referenced workflow path: ${matchingEntry.path}`); | |
| return { repo: calleeRepo, ref: calleeRef }; |
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.
The REPO_PREFIX_RE constant is a nice improvement — extracting this repeated regex into a named constant improves readability and ensures consistency. Consider adding a brief JSDoc comment explaining the expected format of the matched string for future maintainers.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,8 +14,31 @@ const mockCore = { | |
| }, | ||
| }; | ||
|
|
||
| const mockGetWorkflowRun = vi.fn(); | ||
| const mockGithub = { | ||
| rest: { | ||
| actions: { | ||
| getWorkflowRun: mockGetWorkflowRun, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const mockContext = { | ||
| runId: 99999, | ||
| }; | ||
|
|
||
| // Set up global mocks before importing the module | ||
| global.core = mockCore; | ||
| global.github = mockGithub; | ||
| global.context = mockContext; | ||
|
|
||
| /** | ||
| * Creates a default mock response for getWorkflowRun with no referenced workflows. | ||
| * Used for same-repo and same-org cross-repo tests where the API should not change the result. | ||
| */ | ||
| function mockNoReferencedWorkflows() { | ||
| mockGetWorkflowRun.mockResolvedValue({ data: { referenced_workflows: [] } }); | ||
| } | ||
|
Comment on lines
+35
to
+41
|
||
|
|
||
| describe("resolve_host_repo.cjs", () => { | ||
| let main; | ||
|
|
@@ -33,6 +56,9 @@ describe("resolve_host_repo.cjs", () => { | |
| delete process.env.GITHUB_WORKFLOW_REF; | ||
|
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. Good use of |
||
| delete process.env.GITHUB_REPOSITORY; | ||
| delete process.env.GITHUB_REF; | ||
| delete process.env.GITHUB_RUN_ID; | ||
| // Reset context.runId to the default value to prevent test state leakage | ||
| mockContext.runId = 99999; | ||
| }); | ||
|
|
||
| it("should output the platform repo when invoked cross-repo", async () => { | ||
|
|
@@ -58,6 +84,7 @@ describe("resolve_host_repo.cjs", () => { | |
| it("should output the current repo when same-repo invocation", async () => { | ||
| process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "my-org/platform-repo"; | ||
| mockNoReferencedWorkflows(); | ||
|
|
||
| await main(); | ||
|
|
||
|
|
@@ -68,6 +95,7 @@ describe("resolve_host_repo.cjs", () => { | |
| it("should not write step summary for same-repo invocations", async () => { | ||
| process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "my-org/platform-repo"; | ||
| mockNoReferencedWorkflows(); | ||
|
|
||
| await main(); | ||
|
|
||
|
|
@@ -211,6 +239,7 @@ describe("resolve_host_repo.cjs", () => { | |
| it("should output target_repo_name when same-repo invocation", async () => { | ||
| process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "my-org/platform-repo"; | ||
| mockNoReferencedWorkflows(); | ||
|
|
||
| await main(); | ||
|
|
||
|
|
@@ -235,4 +264,162 @@ describe("resolve_host_repo.cjs", () => { | |
| expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("refs/heads/feature-branch")); | ||
| expect(mockCore.summary.write).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| describe("cross-org workflow_call scenarios", () => { | ||
| it("should resolve callee repo via referenced_workflows API when GITHUB_WORKFLOW_REF matches GITHUB_REPOSITORY", async () => { | ||
| // Cross-org workflow_call: GITHUB_WORKFLOW_REF points to the caller's repo (not the callee), | ||
| // so workflowRepo === currentRepo. The referenced_workflows API returns the actual callee. | ||
| process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "caller-org/caller-repo"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
|
|
||
| mockGetWorkflowRun.mockResolvedValue({ | ||
| data: { | ||
| referenced_workflows: [ | ||
| { | ||
| path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main", | ||
| sha: "abc123def456", | ||
| ref: "refs/heads/main", | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
|
|
||
| await main(); | ||
|
|
||
| expect(mockGetWorkflowRun).toHaveBeenCalledWith({ | ||
| owner: "caller-org", | ||
| repo: "caller-repo", | ||
| run_id: 12345, | ||
| }); | ||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "platform-org/platform-repo"); | ||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo_name", "platform-repo"); | ||
| // sha is preferred over ref | ||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "abc123def456"); | ||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved callee repo from referenced_workflows")); | ||
| }); | ||
|
|
||
| it("should use ref from referenced_workflows entry when sha is absent", async () => { | ||
| process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "caller-org/caller-repo"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
|
|
||
| mockGetWorkflowRun.mockResolvedValue({ | ||
| data: { | ||
| referenced_workflows: [ | ||
| { | ||
| path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/feature", | ||
| sha: undefined, | ||
| ref: "refs/heads/feature", | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
|
|
||
| await main(); | ||
|
|
||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "platform-org/platform-repo"); | ||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "refs/heads/feature"); | ||
| }); | ||
|
|
||
| it("should fall back to path-parsed ref when sha and ref are absent in referenced_workflows", async () => { | ||
| process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "caller-org/caller-repo"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
|
|
||
| mockGetWorkflowRun.mockResolvedValue({ | ||
| data: { | ||
| referenced_workflows: [ | ||
| { | ||
| path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/stable", | ||
| sha: undefined, | ||
| ref: undefined, | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
|
|
||
| await main(); | ||
|
|
||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "platform-org/platform-repo"); | ||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_ref", "refs/heads/stable"); | ||
| }); | ||
|
|
||
| it("should log cross-repo detection and write step summary for cross-org callee", async () => { | ||
| process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "caller-org/caller-repo"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
|
|
||
| mockGetWorkflowRun.mockResolvedValue({ | ||
| data: { | ||
| referenced_workflows: [ | ||
| { | ||
| path: "platform-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main", | ||
| sha: "abc123", | ||
| ref: "refs/heads/main", | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
|
|
||
| await main(); | ||
|
|
||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Cross-repo invocation detected")); | ||
| expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("platform-org/platform-repo")); | ||
| expect(mockCore.summary.write).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("should fall back to GITHUB_REPOSITORY when referenced_workflows has no cross-org entry", async () => { | ||
| // workflowRepo === currentRepo but no cross-org entry (same-org same-repo, no callee) | ||
| process.env.GITHUB_WORKFLOW_REF = "my-org/my-repo/.github/workflows/my-workflow.lock.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "my-org/my-repo"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
|
|
||
| mockGetWorkflowRun.mockResolvedValue({ data: { referenced_workflows: [] } }); | ||
|
|
||
| await main(); | ||
|
|
||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/my-repo"); | ||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No cross-org callee found in referenced_workflows")); | ||
| }); | ||
|
|
||
| it("should fall back gracefully when referenced_workflows API call fails", async () => { | ||
| process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "caller-org/caller-repo"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
|
|
||
| mockGetWorkflowRun.mockRejectedValue(new Error("API unavailable")); | ||
|
|
||
| await main(); | ||
|
|
||
| // Should fall back to the currentRepo (caller) — not ideal but safe degradation | ||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "caller-org/caller-repo"); | ||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("API unavailable")); | ||
| }); | ||
|
|
||
| it("should fall back gracefully when GITHUB_RUN_ID is missing", async () => { | ||
| process.env.GITHUB_WORKFLOW_REF = "caller-org/caller-repo/.github/workflows/relay.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "caller-org/caller-repo"; | ||
| delete process.env.GITHUB_RUN_ID; | ||
| mockContext.runId = NaN; | ||
|
|
||
| await main(); | ||
|
|
||
| expect(mockGetWorkflowRun).not.toHaveBeenCalled(); | ||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "caller-org/caller-repo"); | ||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Run ID is unavailable or invalid")); | ||
| }); | ||
|
|
||
| it("should not call referenced_workflows API for normal cross-repo (same-org) invocations", async () => { | ||
| // workflowRepo !== currentRepo → no API call needed | ||
| process.env.GITHUB_WORKFLOW_REF = "my-org/platform-repo/.github/workflows/gateway.lock.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "my-org/app-repo"; | ||
| process.env.GITHUB_RUN_ID = "12345"; | ||
|
|
||
| await main(); | ||
|
|
||
| expect(mockGetWorkflowRun).not.toHaveBeenCalled(); | ||
| expect(mockCore.setOutput).toHaveBeenCalledWith("target_repo", "my-org/platform-repo"); | ||
| }); | ||
| }); | ||
| }); | ||
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.
The SEC-005 note now says targetRepo/targetRef may come from the GitHub Actions API (referenced_workflows), but the
@safe-outputs-exempttag still claims the values are sourced from env vars only. Please update the exemption text to match the new behavior (or remove/adjust it if the exemption criteria changed).