From 42858e7c91802041e86234dc68fdf7d8f3d94538 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:23:00 +0000 Subject: [PATCH 1/3] Initial plan From ac2b8648e117fc715bc6d18fdb28c6ab4b6d4743 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:43:58 +0000 Subject: [PATCH 2/3] fix: cross-repo workflow_call hash check fails due to wrong event name gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the `eventName === "workflow_call"` guard in check_workflow_timestamp_api.cjs. Inside a reusable workflow, GITHUB_EVENT_NAME reflects the original trigger event (e.g., "push"), NOT "workflow_call", so the referenced_workflows API lookup was never firing for cross-repo scenarios triggered by non-workflow_call events. Also corrects misleading comments claiming ${{ github.workflow_ref }} identifies the callee; in practice it resolves to the caller's workflow ref. The referenced_workflows API is now the primary callee-resolution mechanism for all event types. Updates the test that previously asserted the API was NOT called for non-workflow_call events — now it asserts the API IS called (and resolves the callee correctly). Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6606ec27-bb3c-4249-91fe-37b1ea229829 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/check_workflow_timestamp_api.cjs | 118 +++++++++--------- .../js/check_workflow_timestamp_api.test.cjs | 51 ++++++-- pkg/workflow/compiler_activation_job.go | 10 +- 3 files changed, 105 insertions(+), 74 deletions(-) diff --git a/actions/setup/js/check_workflow_timestamp_api.cjs b/actions/setup/js/check_workflow_timestamp_api.cjs index 9d45ef0c299..da74c28e0a2 100644 --- a/actions/setup/js/check_workflow_timestamp_api.cjs +++ b/actions/setup/js/check_workflow_timestamp_api.cjs @@ -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; diff --git a/actions/setup/js/check_workflow_timestamp_api.test.cjs b/actions/setup/js/check_workflow_timestamp_api.test.cjs index c693b0fe6a1..3ea60000ca3 100644 --- a/actions/setup/js/check_workflow_timestamp_api.test.cjs +++ b/actions/setup/js/check_workflow_timestamp_api.test.cjs @@ -798,8 +798,11 @@ 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"; @@ -807,8 +810,10 @@ engine: copilot // 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" })); }); diff --git a/pkg/workflow/compiler_activation_job.go b/pkg/workflow/compiler_activation_job.go index de39f59714a..c460163900d 100644 --- a/pkg/workflow/compiler_activation_job.go +++ b/pkg/workflow/compiler_activation_job.go @@ -202,10 +202,12 @@ func (c *Compiler) buildActivationJob(data *WorkflowData, preActivationJobCreate steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script"))) steps = append(steps, " env:\n") steps = append(steps, fmt.Sprintf(" GH_AW_WORKFLOW_FILE: \"%s\"\n", lockFilename)) - // Inject the GitHub Actions context workflow_ref expression so that check_workflow_timestamp_api.cjs - // can identify the source repo correctly for cross-repo workflow_call invocations. - // Unlike the GITHUB_WORKFLOW_REF env var (which always reflects the top-level caller in workflow_call), - // ${{ github.workflow_ref }} correctly refers to the current reusable workflow being executed. + // Inject the GitHub Actions context workflow_ref expression as GH_AW_CONTEXT_WORKFLOW_REF + // for check_workflow_timestamp_api.cjs. Note: despite what was previously documented, + // ${{ github.workflow_ref }} resolves to the CALLER's workflow ref in reusable workflow + // contexts, not the callee's. The referenced_workflows API lookup in the script is the + // primary mechanism for resolving the callee's repo; GH_AW_CONTEXT_WORKFLOW_REF serves + // as a fallback when the API is unavailable or finds no matching entry. steps = append(steps, " GH_AW_CONTEXT_WORKFLOW_REF: \"${{ github.workflow_ref }}\"\n") steps = append(steps, " with:\n") steps = append(steps, " script: |\n") From dccf24866e46cd7831ef591b3351d50ac6cb7d78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:02:32 +0000 Subject: [PATCH 3/3] refactor: address PR reviewer feedback on referenced_workflows lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add short-circuit: skip getWorkflowRun API call when the env workflow ref (GH_AW_CONTEXT_WORKFLOW_REF / GITHUB_WORKFLOW_REF) already ends with the current workflow file, avoiding unnecessary API calls for same-repo runs - Fix misleading log message: "(used for source repo resolution)" → "(available as env fallback)" — the env var is a fallback, not necessarily what was used when the API lookup succeeded - Rename test describe block: "cross-repo invocation via workflow_call (GH_AW_CONTEXT_WORKFLOW_REF fix)" → "manual GH_AW_CONTEXT_WORKFLOW_REF fallback override" — better reflects that this tests the manual env override path, not a realistic cross-repo scenario from github.workflow_ref - Add test: "should skip referenced_workflows API when env ref already matches the workflow file, even with a valid GITHUB_RUN_ID" — explicitly covers the short-circuit behaviour Agent-Logs-Url: https://github.com/github/gh-aw/sessions/afa907f4-0878-41a8-b553-23fbe064225d Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/check_workflow_timestamp_api.cjs | 21 ++++++--- .../js/check_workflow_timestamp_api.test.cjs | 45 ++++++++++++------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/actions/setup/js/check_workflow_timestamp_api.cjs b/actions/setup/js/check_workflow_timestamp_api.cjs index da74c28e0a2..16cff685a64 100644 --- a/actions/setup/js/check_workflow_timestamp_api.cjs +++ b/actions/setup/js/check_workflow_timestamp_api.cjs @@ -83,17 +83,15 @@ async function main() { ref = undefined; } - // Always attempt referenced_workflows API lookup to resolve the callee repo/ref. - // This handles cross-repo reusable workflow scenarios reliably. + // Attempt referenced_workflows API lookup to detect cross-repo callee repo/ref. // // 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. + // name to detect cross-repo scenarios. // // 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. + // lookup is unavailable or finds no matching entry. // // Resolution priority: // 1. referenced_workflows[].sha — immutable commit SHA from the callee repo (most precise). @@ -105,12 +103,21 @@ 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. // + // Short-circuit: if the env workflow ref already ends with the current workflow file, + // the env vars already correctly identify the source (same-repo or non-reusable run). + // Skip the API call to avoid unnecessary rate-limit usage and permission noise. + // // GITHUB_RUN_ID is always set in GitHub Actions environments. // context.runId is a fallback for environments where env vars are absent. // // 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 envRefWithoutAt = workflowEnvRef.replace(/@.*$/, ""); + const envRefMatchesWorkflow = envRefWithoutAt.endsWith(`/.github/workflows/${workflowFile}`); + + if (envRefMatchesWorkflow) { + core.info("Env workflow ref already identifies this workflow, skipping referenced_workflows API lookup"); + } else if (Number.isFinite(runId)) { const [runOwner, runRepo] = currentRepo.split("/"); try { core.info(`Checking for cross-repo callee via referenced_workflows API (run ${runId})`); @@ -155,7 +162,7 @@ async function main() { const contextWorkflowRef = process.env.GH_AW_CONTEXT_WORKFLOW_REF; core.info(`GITHUB_WORKFLOW_REF: ${process.env.GITHUB_WORKFLOW_REF || "(not set)"}`); if (contextWorkflowRef) { - core.info(`GH_AW_CONTEXT_WORKFLOW_REF: ${contextWorkflowRef} (used for source repo resolution)`); + core.info(`GH_AW_CONTEXT_WORKFLOW_REF: ${contextWorkflowRef} (available as env fallback)`); } core.info(`GITHUB_REPOSITORY: ${currentRepo}`); core.info(`Resolved source repo: ${owner}/${repo} @ ${ref || "(default branch)"}`); diff --git a/actions/setup/js/check_workflow_timestamp_api.test.cjs b/actions/setup/js/check_workflow_timestamp_api.test.cjs index 3ea60000ca3..515e29ef235 100644 --- a/actions/setup/js/check_workflow_timestamp_api.test.cjs +++ b/actions/setup/js/check_workflow_timestamp_api.test.cjs @@ -794,30 +794,28 @@ engine: copilot }); }); - describe("cross-repo invocation via workflow_call (GH_AW_CONTEXT_WORKFLOW_REF fix)", () => { + describe("manual GH_AW_CONTEXT_WORKFLOW_REF fallback override", () => { // 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 }}) = 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). + // In reusable workflow contexts, both GITHUB_WORKFLOW_REF and + // ${{ github.workflow_ref }} resolve to the caller's workflow. + // The referenced_workflows API lookup is the primary fix for identifying the callee + // workflow. These tests cover the fallback path used when that API lookup is bypassed + // by the short-circuit (the env ref already ends with the current workflow file, meaning + // GH_AW_CONTEXT_WORKFLOW_REF was manually set to the callee's ref as a targeted override). 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. + // Simulate a caller workflow context where GITHUB_WORKFLOW_REF points at the caller. 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 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. + // Manually inject GH_AW_CONTEXT_WORKFLOW_REF to exercise the fallback/override path. + // This value intentionally points to the callee repo (platform-repo) so the env ref + // ends with "/.github/workflows/test.lock.yml", triggering the short-circuit and + // bypassing the API lookup. process.env.GH_AW_CONTEXT_WORKFLOW_REF = "platform-owner/platform-repo/.github/workflows/test.lock.yml@refs/heads/main"; }); - it("should use GH_AW_CONTEXT_WORKFLOW_REF to identify source repo, not GITHUB_WORKFLOW_REF", async () => { + it("should use GH_AW_CONTEXT_WORKFLOW_REF override to identify source repo when env ref matches workflow file", async () => { const validHash = "c2a79263dc72f28c76177afda9bf0935481b26da094407a50155a6e0244084e3"; const lockFileContent = `# frontmatter-hash: ${validHash}\nname: Test\n`; const mdFileContent = "---\nengine: copilot\n---\n# Test"; @@ -832,7 +830,7 @@ engine: copilot await main(); - // Must use the platform repo (from GH_AW_CONTEXT_WORKFLOW_REF), not the caller repo + // Must use the platform repo (from GH_AW_CONTEXT_WORKFLOW_REF override), not the caller repo expect(mockGithub.rest.repos.getContent).toHaveBeenCalledWith(expect.objectContaining({ owner: "platform-owner", repo: "platform-repo" })); expect(mockGithub.rest.repos.getContent).not.toHaveBeenCalledWith(expect.objectContaining({ owner: "caller-owner", repo: "caller-repo" })); expect(mockCore.setFailed).not.toHaveBeenCalled(); @@ -845,7 +843,7 @@ engine: copilot await main(); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GH_AW_CONTEXT_WORKFLOW_REF: platform-owner/platform-repo/.github/workflows/test.lock.yml@refs/heads/main")); - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GITHUB_WORKFLOW_REF: caller-owner/caller-repo/.github/workflows/caller.yml@refs/heads/main")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("available as env fallback")); }); it("should detect cross-repo invocation using GH_AW_CONTEXT_WORKFLOW_REF source vs GITHUB_REPOSITORY", async () => { @@ -924,6 +922,19 @@ engine: copilot expect(mockGithub.rest.repos.getContent).toHaveBeenCalledWith(expect.objectContaining({ ref: "refs/heads/main" })); expect(mockGithub.rest.repos.getContent).not.toHaveBeenCalledWith(expect.objectContaining({ ref: "abc123" })); }); + + it("should skip referenced_workflows API when env ref already matches the workflow file, even with a valid GITHUB_RUN_ID", async () => { + // Short-circuit: if the env ref ends with the current workflowFile, the API call is + // skipped to avoid unnecessary rate-limit usage in normal (non-reusable) runs. + process.env.GITHUB_RUN_ID = "99999"; + mockGithub.rest.repos.getContent.mockResolvedValue({ data: null }); + + await main(); + + // API must NOT be called — env ref already identifies this workflow + expect(mockGithub.rest.actions.getWorkflowRun).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("skipping referenced_workflows API lookup")); + }); }); describe("cross-repo reusable workflow via referenced_workflows API (issue #24422)", () => {