Skip to content

Fix stale lock check to resolve callee repo for cross-repo reusable workflows#24433

Merged
pelikhan merged 3 commits intomainfrom
copilot/fix-stale-lock-check
Apr 4, 2026
Merged

Fix stale lock check to resolve callee repo for cross-repo reusable workflows#24433
pelikhan merged 3 commits intomainfrom
copilot/fix-stale-lock-check

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 4, 2026

When a workflow is triggered via workflow_call from another repo, github.workflow_ref (injected as GH_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.yml files in the wrong repo and fail with "outdated or unverifiable."

Changes

  • check_workflow_timestamp_api.cjs: When GITHUB_EVENT_NAME === "workflow_call", fetch the caller's run via github.rest.actions.getWorkflowRun (using GITHUB_REPOSITORY + GITHUB_RUN_ID, which are set to the caller's context in a reusable workflow run). Find the matching entry in referenced_workflows by workflowFile path and use its owner/repo/ref to resolve the callee's source files. Falls back to the existing GH_AW_CONTEXT_WORKFLOW_REF logic on API failure or missing entry.

  • check_workflow_timestamp_api.test.cjs: Added actions.getWorkflowRun to the mock, cleaned up GITHUB_EVENT_NAME/GITHUB_RUN_ID in beforeEach, and added 7 tests covering: successful resolution, logging, no-match fallback, empty array fallback, API error fallback, non-workflow_call events (no-op), and correct ref forwarding to getContent.

// workflow_call detected → fetch caller run → scan referenced_workflows
const matchingEntry = referencedWorkflows.find((wf) => {
  const pathWithoutRef = wf.path.replace(/@.*$/, "");
  return pathWithoutRef.endsWith(`/.github/workflows/${workflowFile}`);
});
// override owner/repo/ref with callee values; falls back on miss/error

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
    • Triggering command: /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
    • Triggering command: /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:

…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>
Copilot AI changed the title [WIP] Fix stale lock check for cross-repo reusable workflows Fix stale lock check to resolve callee repo for cross-repo reusable workflows Apr 4, 2026
Copilot AI requested a review from pelikhan April 4, 2026 03:58
@pelikhan pelikhan marked this pull request as ready for review April 4, 2026 04:00
Copilot AI review requested due to automatic review settings April 4, 2026 04:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 using actions.getWorkflowRun(...).data.referenced_workflows to identify the callee repo/ref before fetching .md/.lock.yml via 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.getWorkflowRun and 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

Comment on lines +97 to +130
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`);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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");

Copilot uses AI. Check for mistakes.
if (pathMatch) {
owner = pathMatch[1];
repo = pathMatch[2];
ref = matchingEntry.ref || pathMatch[3];
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
ref = matchingEntry.ref || pathMatch[3];
ref = matchingEntry.sha || matchingEntry.ref || pathMatch[3];

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +88
// 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.
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses AI. Check for mistakes.
@pelikhan
Copy link
Copy Markdown
Collaborator

pelikhan commented Apr 4, 2026

@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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 4, 2026

@copilot Review comments

Addressed all three review comments in commit 0e97a4c:

  1. NaN runId guard: Added Number.isFinite(runId) check before calling the API; logs "run ID is unavailable or invalid, falling back to GH_AW_CONTEXT_WORKFLOW_REF" when invalid.
  2. Prefer sha over ref: Changed to matchingEntry.sha || matchingEntry.ref || pathMatch[3] so the immutable commit SHA is used for file lookups, preventing branch drift during long-running jobs.
  3. Updated comment: Rewrote the block comment to clearly document the resolution priority (sha → ref → GH_AW_CONTEXT_WORKFLOW_REF) and when each source is authoritative, while updating the downstream logging label accordingly.

Tests updated/added: sha preference, sha-absent fallback to ref, and NaN runId fallback (42 tests total, all passing).

@pelikhan pelikhan merged commit 2d67d71 into main Apr 4, 2026
56 checks passed
@pelikhan pelikhan deleted the copilot/fix-stale-lock-check branch April 4, 2026 04:25
github-actions bot added a commit that referenced this pull request Apr 4, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[plan] Fix stale lock check to resolve callee repo for cross-repo reusable workflows

3 participants