-
Notifications
You must be signed in to change notification settings - Fork 328
Fix cross-repo reusable workflow hash check: remove workflow_call event gate and correct workflow_ref assumptions #24924
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 2 commits
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 |
|---|---|---|
|
|
@@ -45,16 +45,17 @@ async function main() { | |
|
|
||
| // Determine workflow source repository from the workflow ref for cross-repo support. | ||
| // | ||
| // For cross-repo workflow_call invocations (reusable workflows called from another repo), | ||
| // the GITHUB_WORKFLOW_REF env var always points to the TOP-LEVEL CALLER's workflow, not | ||
| // the reusable workflow being executed. This causes the script to look for lock files in | ||
| // the wrong repository. | ||
| // For cross-repo reusable workflow invocations, both GITHUB_WORKFLOW_REF (env var) and | ||
| // ${{ github.workflow_ref }} (injected as GH_AW_CONTEXT_WORKFLOW_REF) resolve to the | ||
| // TOP-LEVEL CALLER's workflow, not the reusable workflow being executed. This causes the | ||
| // script to look for lock files in the wrong repository when used alone. | ||
| // | ||
| // The GitHub Actions expression ${{ github.workflow_ref }} is injected as GH_AW_CONTEXT_WORKFLOW_REF | ||
| // by the compiler and correctly identifies the CURRENT reusable workflow's ref even in | ||
| // cross-repo workflow_call scenarios. We prefer it over GITHUB_WORKFLOW_REF when available. | ||
| // The reliable fix is the referenced_workflows API lookup below, which identifies the | ||
| // callee's repo/ref from the caller's run object. GH_AW_CONTEXT_WORKFLOW_REF is only | ||
| // used as a fallback when the API lookup is unavailable or finds no matching entry. | ||
| // | ||
| // Ref: https://github.com/github/gh-aw/issues/23935 | ||
| // Refs: https://github.com/github/gh-aw/issues/23935 | ||
| // https://github.com/github/gh-aw/issues/24422 | ||
| const workflowEnvRef = process.env.GH_AW_CONTEXT_WORKFLOW_REF || process.env.GITHUB_WORKFLOW_REF || ""; | ||
| const currentRepo = process.env.GITHUB_REPOSITORY || `${context.repo.owner}/${context.repo.repo}`; | ||
|
|
||
|
|
@@ -82,14 +83,20 @@ async function main() { | |
| ref = undefined; | ||
| } | ||
|
|
||
| // For workflow_call events, use referenced_workflows from the GitHub API run object to | ||
| // resolve the callee (reusable workflow) repo and ref. | ||
| // Always attempt referenced_workflows API lookup to resolve the callee repo/ref. | ||
| // This handles cross-repo reusable workflow scenarios reliably. | ||
| // | ||
| // IMPORTANT: GITHUB_EVENT_NAME inside a reusable workflow reflects the ORIGINAL trigger | ||
| // event (e.g., "push", "issues"), NOT "workflow_call". We therefore cannot rely on event | ||
| // name to detect cross-repo scenarios and must always attempt the referenced_workflows | ||
| // API lookup. | ||
| // | ||
|
||
| // Similarly, GH_AW_CONTEXT_WORKFLOW_REF (${{ github.workflow_ref }}) resolves to the | ||
| // CALLER's workflow ref, not the callee's. It is used as a fallback only when the API | ||
| // lookup does not find a matching entry. | ||
| // | ||
| // Resolution priority: | ||
| // 1. referenced_workflows[].sha — immutable commit SHA from the callee repo (most precise). | ||
| // GH_AW_CONTEXT_WORKFLOW_REF (${{ github.workflow_ref }}) correctly identifies the callee | ||
| // in most cases, but referenced_workflows carries the pinned sha which won't drift if a | ||
| // branch ref moves during a long-running job. | ||
| // 2. referenced_workflows[].ref — branch/tag ref from the callee (fallback when sha absent). | ||
| // 3. GH_AW_CONTEXT_WORKFLOW_REF — injected by the compiler; used when the API is unavailable | ||
| // or when no matching entry is found in referenced_workflows. | ||
|
|
@@ -98,54 +105,51 @@ async function main() { | |
| // are set to the caller's run ID and repo. The caller's run object includes a | ||
| // referenced_workflows array listing the callee's exact path, sha, and ref. | ||
| // | ||
| // GITHUB_EVENT_NAME and GITHUB_RUN_ID are always set in GitHub Actions environments. | ||
| // context.eventName / context.runId are fallbacks for environments where env vars are absent. | ||
| // GITHUB_RUN_ID is always set in GitHub Actions environments. | ||
| // context.runId is a fallback for environments where env vars are absent. | ||
| // | ||
| // Ref: https://github.com/github/gh-aw/issues/24422 | ||
| const eventName = process.env.GITHUB_EVENT_NAME || context.eventName; | ||
| if (eventName === "workflow_call") { | ||
| const runId = parseInt(process.env.GITHUB_RUN_ID || String(context.runId), 10); | ||
| if (Number.isFinite(runId)) { | ||
| const [runOwner, runRepo] = currentRepo.split("/"); | ||
| try { | ||
| core.info(`workflow_call event detected, resolving callee repo 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 caller run`); | ||
|
|
||
| // Find the entry whose path matches the current workflow file. | ||
| // Path format: "org/repo/.github/workflows/file.lock.yml@ref" | ||
| // Using replace to robustly strip the optional @ref suffix before matching. | ||
| const matchingEntry = referencedWorkflows.find(wf => { | ||
| const pathWithoutRef = wf.path.replace(/@.*$/, ""); | ||
| return pathWithoutRef.endsWith(`/.github/workflows/${workflowFile}`); | ||
| }); | ||
|
|
||
| if (matchingEntry) { | ||
| const pathMatch = matchingEntry.path.match(GITHUB_REPO_PATH_RE); | ||
| if (pathMatch) { | ||
| owner = pathMatch[1]; | ||
| repo = pathMatch[2]; | ||
| // Prefer sha (immutable) over ref (branch/tag can drift) over path-parsed ref. | ||
| ref = matchingEntry.sha || matchingEntry.ref || pathMatch[3]; | ||
| workflowRepo = `${owner}/${repo}`; | ||
| core.info(`Resolved callee repo from referenced_workflows: ${owner}/${repo} @ ${ref || "(default branch)"}`); | ||
| core.info(` Referenced workflow path: ${matchingEntry.path}`); | ||
| } | ||
| } else { | ||
| core.info(`No matching entry in referenced_workflows for "${workflowFile}", falling back to GH_AW_CONTEXT_WORKFLOW_REF`); | ||
| // Refs: https://github.com/github/gh-aw/issues/24422 | ||
| const runId = parseInt(process.env.GITHUB_RUN_ID || String(context.runId), 10); | ||
| if (Number.isFinite(runId)) { | ||
| const [runOwner, runRepo] = currentRepo.split("/"); | ||
| try { | ||
| core.info(`Checking for cross-repo 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 entry whose path matches the current workflow file. | ||
| // Path format: "org/repo/.github/workflows/file.lock.yml@ref" | ||
| // Using replace to robustly strip the optional @ref suffix before matching. | ||
| const matchingEntry = referencedWorkflows.find(wf => { | ||
| const pathWithoutRef = wf.path.replace(/@.*$/, ""); | ||
| return pathWithoutRef.endsWith(`/.github/workflows/${workflowFile}`); | ||
| }); | ||
|
|
||
| if (matchingEntry) { | ||
| const pathMatch = matchingEntry.path.match(GITHUB_REPO_PATH_RE); | ||
| if (pathMatch) { | ||
| owner = pathMatch[1]; | ||
| repo = pathMatch[2]; | ||
| // Prefer sha (immutable) over ref (branch/tag can drift) over path-parsed ref. | ||
| ref = matchingEntry.sha || matchingEntry.ref || pathMatch[3]; | ||
| workflowRepo = `${owner}/${repo}`; | ||
| core.info(`Resolved callee repo from referenced_workflows: ${owner}/${repo} @ ${ref || "(default branch)"}`); | ||
| core.info(` Referenced workflow path: ${matchingEntry.path}`); | ||
| } | ||
| } catch (error) { | ||
| core.info(`Could not fetch referenced_workflows from API: ${getErrorMessage(error)}, falling back to GH_AW_CONTEXT_WORKFLOW_REF`); | ||
| } else { | ||
| core.info(`No matching entry in referenced_workflows for "${workflowFile}", falling back to GH_AW_CONTEXT_WORKFLOW_REF`); | ||
| } | ||
| } else { | ||
| core.info("workflow_call event detected but run ID is unavailable or invalid, falling back to GH_AW_CONTEXT_WORKFLOW_REF"); | ||
| } catch (error) { | ||
| core.info(`Could not fetch referenced_workflows from API: ${getErrorMessage(error)}, falling back to GH_AW_CONTEXT_WORKFLOW_REF`); | ||
| } | ||
| } else { | ||
| core.info("Run ID is unavailable or invalid, falling back to GH_AW_CONTEXT_WORKFLOW_REF"); | ||
| } | ||
|
|
||
| const contextWorkflowRef = process.env.GH_AW_CONTEXT_WORKFLOW_REF; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -798,17 +798,22 @@ engine: copilot | |
| // Regression test for https://github.com/github/gh-aw/issues/23935 | ||
| // When a reusable workflow is invoked cross-repo via workflow_call: | ||
| // - GITHUB_WORKFLOW_REF (env var) = top-level CALLER's workflow (e.g., repo-b/caller.yml@main) | ||
| // - GH_AW_CONTEXT_WORKFLOW_REF (injected from ${{ github.workflow_ref }}) = the CALLEE's reusable workflow | ||
| // Without this fix, the script would look for lock files in the caller's repo (404). | ||
| // - GH_AW_CONTEXT_WORKFLOW_REF (injected from ${{ github.workflow_ref }}) = CALLER's workflow too | ||
| // (github.workflow_ref resolves to the caller in reusable workflow contexts) | ||
| // The referenced_workflows API lookup is the primary fix; GH_AW_CONTEXT_WORKFLOW_REF is | ||
| // used as a fallback. These tests cover the fallback path (no GITHUB_RUN_ID set) where | ||
| // GH_AW_CONTEXT_WORKFLOW_REF happens to correctly identify the callee (e.g., same-repo case). | ||
|
|
||
|
||
| beforeEach(() => { | ||
| process.env.GH_AW_WORKFLOW_FILE = "test.lock.yml"; | ||
| // Simulate workflow_call cross-repo: reusable workflow defined in platform-repo, | ||
| // called from caller-repo. GITHUB_WORKFLOW_REF wrongly points to the caller's workflow. | ||
| process.env.GITHUB_WORKFLOW_REF = "caller-owner/caller-repo/.github/workflows/caller.yml@refs/heads/main"; | ||
| process.env.GITHUB_REPOSITORY = "caller-owner/caller-repo"; | ||
| // GH_AW_CONTEXT_WORKFLOW_REF is injected by the compiler from ${{ github.workflow_ref }} | ||
| // which correctly identifies the reusable workflow being executed. | ||
| // GH_AW_CONTEXT_WORKFLOW_REF is used as a fallback for repo resolution when the | ||
| // referenced_workflows API lookup is unavailable (no GITHUB_RUN_ID in these tests). | ||
| // Note: in practice, ${{ github.workflow_ref }} resolves to the caller's workflow, | ||
| // but when set correctly it still serves as a reliable fallback. | ||
| process.env.GH_AW_CONTEXT_WORKFLOW_REF = "platform-owner/platform-repo/.github/workflows/test.lock.yml@refs/heads/main"; | ||
| }); | ||
|
|
||
|
|
@@ -922,10 +927,14 @@ engine: copilot | |
| }); | ||
|
|
||
| describe("cross-repo reusable workflow via referenced_workflows API (issue #24422)", () => { | ||
| // Fix for https://github.com/github/gh-aw/issues/24422 | ||
| // When a reusable workflow is triggered by workflow_call, github.workflow_ref | ||
| // can still point to the caller's workflow. This fix uses referenced_workflows | ||
| // from the GitHub Actions API run object to reliably identify the callee's repo. | ||
| // Fix for https://github.com/github/gh-aw/issues/24422 and cross-repo bug | ||
| // When a reusable workflow is triggered, GITHUB_EVENT_NAME reflects the ORIGINAL trigger | ||
| // event (e.g., "push", "issues"), NOT "workflow_call". We therefore cannot rely on event | ||
| // name to detect cross-repo scenarios. | ||
| // | ||
| // Additionally, github.workflow_ref (injected as GH_AW_CONTEXT_WORKFLOW_REF) resolves to | ||
| // the CALLER's workflow ref, not the callee's. The referenced_workflows API lookup from | ||
| // the caller's run object is the reliable way to identify the callee's repo and ref. | ||
| // | ||
| // In the workflow_call context, GITHUB_RUN_ID and GITHUB_REPOSITORY are set to | ||
| // the caller's run and repo. The caller's run object includes referenced_workflows | ||
|
|
@@ -937,7 +946,7 @@ engine: copilot | |
| process.env.GITHUB_RUN_ID = "12345"; | ||
| // GITHUB_REPOSITORY is the caller's repo in a workflow_call context | ||
| process.env.GITHUB_REPOSITORY = "caller-owner/caller-repo"; | ||
| // GH_AW_CONTEXT_WORKFLOW_REF (from ${{ github.workflow_ref }}) may still point to caller | ||
| // GH_AW_CONTEXT_WORKFLOW_REF (from ${{ github.workflow_ref }}) resolves to the caller | ||
| process.env.GH_AW_CONTEXT_WORKFLOW_REF = "caller-owner/caller-repo/.github/workflows/caller.yml@refs/heads/main"; | ||
| process.env.GITHUB_WORKFLOW_REF = "caller-owner/caller-repo/.github/workflows/caller.yml@refs/heads/main"; | ||
| }); | ||
|
|
@@ -994,7 +1003,7 @@ engine: copilot | |
|
|
||
| await main(); | ||
|
|
||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("workflow_call event detected")); | ||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Checking for cross-repo callee via referenced_workflows API")); | ||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved callee repo from referenced_workflows: callee-owner/callee-repo")); | ||
| }); | ||
|
|
||
|
|
@@ -1044,13 +1053,29 @@ engine: copilot | |
| expect(mockGithub.rest.repos.getContent).toHaveBeenCalledWith(expect.objectContaining({ owner: "caller-owner", repo: "caller-repo" })); | ||
| }); | ||
|
|
||
| it("should not call referenced_workflows API for non-workflow_call events", async () => { | ||
| it("should call referenced_workflows API even for non-workflow_call events", async () => { | ||
| // In reusable workflows, GITHUB_EVENT_NAME reflects the original trigger event (e.g., | ||
| // "push"), not "workflow_call". We must try referenced_workflows regardless of event name. | ||
| process.env.GITHUB_EVENT_NAME = "push"; | ||
| mockGithub.rest.actions.getWorkflowRun.mockResolvedValueOnce({ | ||
| data: { | ||
| referenced_workflows: [ | ||
| { | ||
| path: "callee-owner/callee-repo/.github/workflows/callee-workflow.lock.yml@refs/heads/main", | ||
| sha: "deadbeef", | ||
| ref: "refs/heads/main", | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
| mockGithub.rest.repos.getContent.mockResolvedValue({ data: null }); | ||
|
|
||
| await main(); | ||
|
|
||
| expect(mockGithub.rest.actions.getWorkflowRun).not.toHaveBeenCalled(); | ||
| // API must be called even for "push" events | ||
| expect(mockGithub.rest.actions.getWorkflowRun).toHaveBeenCalled(); | ||
| // Resolves to callee repo even though GITHUB_EVENT_NAME is "push" | ||
| expect(mockGithub.rest.repos.getContent).toHaveBeenCalledWith(expect.objectContaining({ owner: "callee-owner", repo: "callee-repo" })); | ||
| }); | ||
|
|
||
| it("should prefer sha over ref from referenced_workflows entry", async () => { | ||
|
|
@@ -1112,7 +1137,7 @@ engine: copilot | |
|
|
||
| // API must not be called with a NaN run_id | ||
| expect(mockGithub.rest.actions.getWorkflowRun).not.toHaveBeenCalled(); | ||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("run ID is unavailable or invalid")); | ||
| expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Run ID is unavailable or invalid")); | ||
| // Falls back to caller repo from GH_AW_CONTEXT_WORKFLOW_REF | ||
| expect(mockGithub.rest.repos.getContent).toHaveBeenCalledWith(expect.objectContaining({ owner: "caller-owner", repo: "caller-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 script comments say GH_AW_CONTEXT_WORKFLOW_REF is only a fallback when the referenced_workflows API lookup misses/unavailable, but later logging still states
GH_AW_CONTEXT_WORKFLOW_REF ... (used for source repo resolution)even when the API successfully resolves the callee. Update the log wording (or gate it on whether fallback was actually used) to avoid misleading diagnostics.