Fix stale lock check to resolve callee repo for cross-repo reusable workflows#24433
Fix stale lock check to resolve callee repo for cross-repo reusable workflows#24433
Conversation
…check (#24422) Use referenced_workflows from GitHub API run object when workflow_call event is detected. This correctly identifies the callee's owner/repo/ref instead of relying on GH_AW_CONTEXT_WORKFLOW_REF which can still point to the caller's workflow in cross-repo scenarios. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/c4f86ff3-a4e6-4bbd-9b26-4ec6cce11dfc Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Updates the stale lock-file verification to correctly resolve the callee repository/ref when running as a cross-repo reusable workflow (workflow_call), avoiding lookups in the caller repo that can lead to “outdated or unverifiable” failures.
Changes:
- Adds
workflow_call-specific resolution usingactions.getWorkflowRun(...).data.referenced_workflowsto identify the callee repo/ref before fetching.md/.lock.ymlvia the Contents API. - Keeps existing behavior as a fallback when the API call fails or no matching referenced workflow entry is found.
- Extends unit tests with mocked
actions.getWorkflowRunand new cases covering success + fallback paths.
Show a summary per file
| File | Description |
|---|---|
| actions/setup/js/check_workflow_timestamp_api.cjs | Resolves callee owner/repo/ref for workflow_call runs via referenced_workflows before performing GitHub API content reads. |
| actions/setup/js/check_workflow_timestamp_api.test.cjs | Adds getWorkflowRun mock and new tests validating referenced-workflow-based resolution and fallbacks. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 3
| 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 a regex 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(/^([^/]+)\/([^/]+)\/.+?(?:@(.+))?$/); | ||
| if (pathMatch) { | ||
| owner = pathMatch[1]; | ||
| repo = pathMatch[2]; | ||
| ref = 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`); | ||
| } | ||
| } catch (error) { | ||
| core.info(`Could not fetch referenced_workflows from API: ${getErrorMessage(error)}, falling back to GH_AW_CONTEXT_WORKFLOW_REF`); |
There was a problem hiding this comment.
runId is derived via parseInt(process.env.GITHUB_RUN_ID || String(context.runId), 10) but isn’t validated before being sent to actions.getWorkflowRun. If this ever evaluates to NaN (e.g., local runs, or unexpected env/context values), the API call will fail and the log message will misleadingly include run NaN. Consider guarding with Number.isFinite(runId) (and only calling the API when valid) so fallback behavior is deterministic and logs are clearer.
| 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 a regex 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(/^([^/]+)\/([^/]+)\/.+?(?:@(.+))?$/); | |
| if (pathMatch) { | |
| owner = pathMatch[1]; | |
| repo = pathMatch[2]; | |
| ref = 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`); | |
| } | |
| } catch (error) { | |
| core.info(`Could not fetch referenced_workflows from API: ${getErrorMessage(error)}, falling back to GH_AW_CONTEXT_WORKFLOW_REF`); | |
| if (Number.isFinite(runId)) { | |
| 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 a regex 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(/^([^/]+)\/([^/]+)\/.+?(?:@(.+))?$/); | |
| if (pathMatch) { | |
| owner = pathMatch[1]; | |
| repo = pathMatch[2]; | |
| ref = 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`); | |
| } | |
| } catch (error) { | |
| core.info(`Could not fetch referenced_workflows from API: ${getErrorMessage(error)}, 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"); |
| if (pathMatch) { | ||
| owner = pathMatch[1]; | ||
| repo = pathMatch[2]; | ||
| ref = matchingEntry.ref || pathMatch[3]; |
There was a problem hiding this comment.
When resolving the callee from referenced_workflows, the API provides an immutable sha alongside ref. Using ref (branch/tag) can drift if the ref moves during a long-running job, leading to non-deterministic stale-lock results. Prefer using matchingEntry.sha as the ref passed to getContent/file reads (and fall back to matchingEntry.ref only if sha is missing).
| ref = matchingEntry.ref || pathMatch[3]; | |
| ref = matchingEntry.sha || matchingEntry.ref || pathMatch[3]; |
| // For workflow_call events, use referenced_workflows from the GitHub API run object to | ||
| // resolve the callee (reusable workflow) repo and ref. This is more reliable than | ||
| // GH_AW_CONTEXT_WORKFLOW_REF (${{ github.workflow_ref }}) which can still point to the | ||
| // caller workflow's repo in cross-repo reusable workflow scenarios. | ||
| // | ||
| // When a reusable workflow is called from another repo, GITHUB_RUN_ID and GITHUB_REPOSITORY | ||
| // 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. |
There was a problem hiding this comment.
The comment here says referenced_workflows is “more reliable than GH_AW_CONTEXT_WORKFLOW_REF … which can still point to the caller”, but earlier in this file (and existing tests) GH_AW_CONTEXT_WORKFLOW_REF is documented as the preferred/accurate source for cross-repo workflow_call. Can you reconcile these statements (and, if needed, update the later logging that labels GH_AW_CONTEXT_WORKFLOW_REF as “used for source repo resolution”) so future readers understand when each source is authoritative?
|
@copilot Review comments |
- Guard runId with Number.isFinite() to prevent NaN API calls - Prefer sha over ref for deterministic file lookups (no branch drift) - Update comment to clarify resolution priority and when each source is authoritative - Update/add tests: sha preference, sha-absent fallback, NaN runId fallback Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b4032cc1-d56d-4d8e-b209-05a5777ce276 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Addressed all three review comments in commit
Tests updated/added: sha preference, sha-absent fallback to ref, and NaN runId fallback (42 tests total, all passing). |
Extract the repeated regex /^([^/]+)\/([^/]+)\/.+?(?:@(.+))?$/ (used in both initial workflow ref parsing and referenced_workflows entry parsing) to a module-level named constant GITHUB_REPO_PATH_RE. This eliminates duplication added in PR #24433 and makes the intent of the pattern explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a workflow is triggered via
workflow_callfrom another repo,github.workflow_ref(injected asGH_AW_CONTEXT_WORKFLOW_REF) can still resolve to the caller's workflow ref rather than the callee's, causing the stale lock check to look for.md/.lock.ymlfiles in the wrong repo and fail with "outdated or unverifiable."Changes
check_workflow_timestamp_api.cjs: WhenGITHUB_EVENT_NAME === "workflow_call", fetch the caller's run viagithub.rest.actions.getWorkflowRun(usingGITHUB_REPOSITORY+GITHUB_RUN_ID, which are set to the caller's context in a reusable workflow run). Find the matching entry inreferenced_workflowsbyworkflowFilepath and use itsowner/repo/refto resolve the callee's source files. Falls back to the existingGH_AW_CONTEXT_WORKFLOW_REFlogic on API failure or missing entry.check_workflow_timestamp_api.test.cjs: Addedactions.getWorkflowRunto the mock, cleaned upGITHUB_EVENT_NAME/GITHUB_RUN_IDinbeforeEach, and added 7 tests covering: successful resolution, logging, no-match fallback, empty array fallback, API error fallback, non-workflow_callevents (no-op), and correctrefforwarding togetContent.Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
https://api.github.com/repos/github/gh-aw/contents/.github%2Fworkflows%2Faudit-workflows.md/opt/hostedtoolcache/node/24.14.1/x64/bin/node /opt/hostedtoolcache/node/24.14.1/x64/bin/node --experimental-import-meta-resolve --require /home/REDACTED/work/gh-aw/gh-aw/actions/setup/js/node_modules/vitest/suppress-warnings.cjs --conditions node --conditions development /home/REDACTED/work/gh-aw/gh-aw/actions/setup/js/node_modules/vitest/dist/workers/forks.js(http block)invalid.example.invalid/usr/lib/git-core/git-remote-https /usr/lib/git-core/git-remote-https origin https://invalid.example.invalid/nonexistent-repo.git git conf�� user.name lure tions/setup/js/node_modules/.bin/git -M main /usr/sbin/git git init�� --bare --initial-branch=main k/gh-aw/gh-aw/actions/setup/js/node_modules/.bin/git '/tmp/bare-incregit '/tmp/bare-increadd cal/bin/git git(dns block)If you need me to access, download, or install something from one of these locations, you can either: