Skip to content

Fix sample replay multi-repo lookup when Configure Git credentials clobbers origin (#37545)#37565

Merged
dsyme merged 3 commits into
mainfrom
fix/issue-37545-manifest-first-lookup
Jun 7, 2026
Merged

Fix sample replay multi-repo lookup when Configure Git credentials clobbers origin (#37545)#37565
dsyme merged 3 commits into
mainfrom
fix/issue-37545-manifest-first-lookup

Conversation

@dsyme

@dsyme dsyme commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes multi-repo checkout lookup failing when a "Configure Git credentials" workflow step rewrites remote.origin.url at the workspace root (#37545). The previous implementation relied solely on git config remote-URL scanning; once that URL was overwritten by the credentials step, side-repo checkout paths became unreachable. The fix makes findRepoCheckout and buildRepoCheckoutMap consult the compiler-written checkout manifest first, falling back to the git-scan only when the manifest cannot resolve the requested repo.


Problem

During sample-replay runs involving multiple repositories, the safe-output handler calls findRepoCheckout / buildRepoCheckoutMap to locate side-repo working directories. When "Configure Git credentials" runs after the checkout steps, it overwrites remote.origin.url for the workspace root. Because the old logic walked git config to match remote URLs to paths, any repo whose remote URL had been replaced was silently unfindable, causing the entire multi-repo lookup to fail.


Changes

actions/setup/js/find_repo_checkout.cjs ⚠️ high impact

  • Introduces a resolveManifestPath helper that locates the compiler-written checkout manifest relative to the workspace root, with workspace-escape safety checks to reject absolute paths and ..-traversal.
  • Rewrites findRepoCheckout and buildRepoCheckoutMap to call resolveManifestPath (manifest-first) and only fall back to the git config remote-URL scan when the manifest cannot supply an answer.
  • Manifest entries with non-existent paths are silently skipped; manifest results take priority over conflicting git-scan results for the same repo slug.

actions/setup/js/checkout_manifest.cjs

  • Adds and exports a new loadAllCheckouts() function returning all manifest entries as a Map keyed by lowercase repo slug, enabling callers to seed bulk lookup structures from the manifest in one call.

actions/setup/js/find_repo_checkout.test.cjs

  • Adds a "checkout manifest fallback (issue #37545)" describe block covering:
    • Manifest-first resolution path (happy path)
    • Subdirectory paths within a checkout
    • Case-insensitive slug matching
    • allowedRepos validation
    • Stale / missing manifest entries
    • Absolute-path rejection
    • ..-traversal rejection
    • Manifest-over-git-scan priority in buildRepoCheckoutMap
    • Manifest entry with non-existent path is silently skipped

.changeset/patch-find-repo-checkout-manifest-first.md

  • Patch-level changeset entry documenting the fix for consumers tracking the changelog.

Test Coverage

All new behaviour is covered by the new "checkout manifest fallback (issue #37545)" suite. Key edge cases explicitly tested: path-traversal safety, case normalisation, allowedRepos gate, priority of manifest over live git-scan, and graceful handling of stale manifest entries pointing to missing paths.


Risk

Area Level Notes
Core lookup logic Medium Manifest-first path is new; git-scan fallback is preserved for backwards compatibility
Security (path traversal) Low resolveManifestPath explicitly rejects absolute paths and .. segments
Breaking changes None No public API changes; purely additive (loadAllCheckouts) plus internal logic rewrite

Related

Generated by PR Description Updater for issue #37565 · 190 AIC · ⌖ 13.2 AIC · ⊞ 19.5K ·

…ntials" rewrote the workspace remote (#37545)

When a workflow uses `checkout:` to bring a non-workflow repository into
`$GITHUB_WORKSPACE`, the compiler-emitted "Configure Git credentials"
step runs after the side-repo checkout and unconditionally executes:

  git remote set-url origin \
    "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"

This clobbers `remote.origin.url` at the workspace root so it points
back at the workflow's own repository, even though the on-disk contents
are the side repo. `find_repo_checkout.cjs` then fails because it
identifies repos by scanning `git config --get remote.origin.url`, and
the URL no longer matches the slug being searched for. In sample-replay
mode the safe-output handler reports
"Repository '...' not found in workspace" and emits zero outputs.

Fix by consulting the existing compile-time checkout manifest first.
The "Build checkout manifest for safe-outputs handlers" step writes
`$RUNNER_TEMP/gh-aw/checkout-manifest.json` mapping
`lowercase(slug) -> { repository, path, default_branch }` before any
credential rewriting happens. The manifest is therefore the authoritative
source for cross-repo lookup paths and is unaffected by later
`git remote set-url` operations.

Changes:
- `checkout_manifest.cjs`: export `loadAllCheckouts()` returning a Map
  of every parsed manifest entry.
- `find_repo_checkout.cjs`:
  - `findRepoCheckout()` calls `lookupCheckout(targetSlug)` after the
    allowed-repos validation block; on hit, returns the resolved path
    directly and skips the git-remote scan. Falls through to the
    existing git-dir + workspace-root scan when the manifest has no
    entry (covers cases where no manifest is written, e.g. unit tests
    or non-multi-repo workflows).
  - `buildRepoCheckoutMap()` seeds the map from `loadAllCheckouts()`
    first, then only adds git-scan results for slugs not already
    present from the manifest.
  - New helper `resolveManifestPath()` handles the manifest's `path: ""`
    convention (= workspace root) and path joining.
- `find_repo_checkout.test.cjs`: six new tests under
  `describe("checkout manifest fallback (issue #37545)")` covering the
  clobbered-origin regression scenario, subdirectory paths,
  case-insensitive slug matching, allowedRepos interaction with
  manifest hits, fallback when the manifest is empty, and
  `buildRepoCheckoutMap` seeding from manifest entries.
Copilot AI review requested due to automatic review settings June 7, 2026 13:27
@dsyme dsyme marked this pull request as draft June 7, 2026 13:29
@dsyme dsyme marked this pull request as ready for review June 7, 2026 13:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a multi-repo sample replay/safe-output regression where the compiler-emitted “Configure Git credentials” step can clobber remote.origin.url at the workspace root, causing runtime git-remote scanning to misidentify the checked-out repository. The fix prioritizes the compiler-written checkout manifest as the source of truth, with git scanning as a fallback.

Changes:

  • Added checkout-manifest-first lookup in findRepoCheckout() and manifest seeding in buildRepoCheckoutMap().
  • Exported loadAllCheckouts() to enumerate all manifest entries for map seeding.
  • Added targeted tests covering the clobbered-origin scenario and related manifest behaviors.
Show a summary per file
File Description
actions/setup/js/find_repo_checkout.cjs Consults checkout manifest before git-remote scanning; seeds checkout map from manifest entries.
actions/setup/js/checkout_manifest.cjs Adds loadAllCheckouts() export to provide all manifest entries to callers.
actions/setup/js/find_repo_checkout.test.cjs Adds tests for manifest-first lookup and fallback behavior (issue #37545).
.changeset/patch-find-repo-checkout-manifest-first.md Documents the patch-level change and rationale.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

Comment thread actions/setup/js/checkout_manifest.cjs
Comment thread actions/setup/js/find_repo_checkout.cjs
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #37565 does not have the implementation label (has_implementation_label=false) and has 0 new lines of code in business logic directories (well below the 100-line threshold). Neither enforcement condition is met.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel completed test quality analysis.

test

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnose and /tdd — the core fix is correct and well-structured; commenting with a few targeted improvements.

📋 Key Themes & Highlights

Key Themes

  • Untested absolute-path branch (resolveManifestPath): the path.isAbsolute escape hatch is unreachable in normal operation and opens a path-traversal vector if the manifest is tampered with. Remove it or add a containment assertion.
  • Implicit key-casing invariant (loadAllCheckouts): the function trusts that manifest keys are already lowercase without enforcing it, creating a silent failure mode if the invariant drifts.
  • Test coverage gaps: the !map.has(slug) guard — the actual fix for buildRepoCheckoutMap — has no dedicated test; and the fallback tests only cover an empty manifest, not an absent one.

Positive Highlights

  • ✅ Excellent PR description — problem, root cause, chosen approach, and alternatives all documented.
  • ✅ Manifest-first with git-scan fallback is the right ordering, and the fallthrough is clean.
  • ✅ Defensive type-guards in loadAllCheckouts() protect against malformed manifest entries.
  • ✅ 135 lines of new targeted tests covering the regression scenario and five edge cases — great coverage for a bug fix.
  • allowedRepos validation is correctly preserved even when the manifest matches (confirmed by test).
  • ✅ Inline comment in findRepoCheckout clearly explains why the manifest is consulted before git config.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 935.5K · 280.6 AIC · ⌖ 13.8 AIC

Comment thread actions/setup/js/checkout_manifest.cjs
Comment thread actions/setup/js/find_repo_checkout.cjs
Comment thread actions/setup/js/find_repo_checkout.cjs
Comment thread actions/setup/js/find_repo_checkout.test.cjs
Comment thread actions/setup/js/find_repo_checkout.test.cjs
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Three issues must be addressed before merge. The core manifest-first lookup design is correct and the regression test coverage is good; the problems are in the new helper functions, not the main fix logic.

### Blocking issues

1. loadAllCheckouts() does not normalize keyscheckout_manifest.cjs:110 sets map.set(key, ...) using the raw JSON object key without .trim().toLowerCase(). All callers use normalized (lowercase) slugs via normalizeRepoSlug, so a manifest with any mixed-case key causes buildRepoCheckoutMap to silently seed the map with an unreachable entry while the git-scan path adds a duplicate normalized entry for the same repo. One-line fix: map.set(key.trim().toLowerCase(), { ... }).

2. resolveManifestPath() does not clamp to workspace rootfind_repo_checkout.cjs:138–139 passes absolute relPath values through unchanged and does not detect .. segments in relative paths. A malformed or tampered manifest path field can resolve outside $GITHUB_WORKSPACE. Fix: resolve the joined path and reject/ignore it if path.relative(workspaceRoot, resolved) starts with .., and treat absolute relPath as an error rather than a passthrough.

3. Manifest hit returns success: true without verifying the path exists — see inline comment at line 188. The manifest is written before actions/checkout runs; a failed checkout or stale manifest returns a nonexistent path with a success indicator. The fix is a cheap fs.existsSync guard before the early return, falling back to the git scan on miss.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 391K · 8.26 AIC · ⌖ 13.2 AIC

Comment thread actions/setup/js/find_repo_checkout.cjs Outdated
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 85/100 — Excellent

Analyzed 6 test(s) across 1 JavaScript file: 6 design tests (behavioral contracts), 0 implementation tests, 0 guideline violations.

📊 Metrics & Test Classification (6 tests analyzed)
Metric Value
New/modified tests analyzed 6
✅ Design tests (behavioral contracts) 6 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 5 (83%)
Duplicate test clusters 0
Test inflation detected Yes — 135 test lines / 43 production lines = 3.1:1 (see notes)
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
findRepoCheckout returns the manifest path even when remote.origin.url has been clobbered find_repo_checkout.test.cjs ✅ Design
findRepoCheckout resolves manifest entries with a non-empty path to a subdirectory find_repo_checkout.test.cjs ✅ Design
findRepoCheckout is case-insensitive on the repo slug find_repo_checkout.test.cjs ✅ Design
findRepoCheckout still applies allowedRepos validation when the manifest matches find_repo_checkout.test.cjs ✅ Design
findRepoCheckout falls back to the git-remote scan when the manifest has no entry find_repo_checkout.test.cjs ✅ Design
buildRepoCheckoutMap seeds from manifest entries find_repo_checkout.test.cjs ✅ Design No explicit error path

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests — no Go test files changed
  • 🟨 JavaScript (*.test.cjs, *.test.js): 6 tests (vitest) in actions/setup/js/find_repo_checkout.test.cjs
📝 Notes — No Violations, Minor Observations

�� Test inflation ratio — find_repo_checkout.test.cjs

Observation (not a violation): 135 test lines added vs. 43 production lines in find_repo_checkout.cjs = 3.1:1 ratio (threshold: 2:1). However, the new tests also exercise checkout_manifest.cjs (+20 new lines), the module integrated by this fix. Counting all changed production code: 135 / (43 + 20) = 2.1:1 — well-justified given the cross-module behavioral coverage. Each of the 6 tests targets a distinct contract and the inline comments document the exact bug scenario being reproduced.

📝 Assertion messages not used in JavaScript tests

Observation (not a violation): The new expect(...) calls do not include custom failure messages (vitest supports expect(value, 'message').toBe(expected)). The it("description", ...) names here are descriptive enough to serve that purpose; no action needed.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All 6 new tests verify observable behavioral contracts: manifest-first lookup priority over a clobbered git remote, subdirectory path resolution, case-insensitive slug matching, allowedRepos validation gate, fallback to git-remote scan when the manifest has no entry, and buildRepoCheckoutMap seeding.

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §27093952031

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 1.6M · 470.4 AIC · ⌖ 43.5 AIC ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 85/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 6 new tests verify observable behavioral contracts for the manifest-first lookup fix.

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

@copilot review all comments and address unresolved review feedback, then refresh the branch and summarize any remaining blockers.

Generated by 👨‍🍳 PR Sous Chef · 97.4 AIC · ⌖ 1.03 AIC · ⊞ 17K ·

- resolveManifestPath: simplify redundant empty-string check; clamp result to
  workspace root, refusing absolute paths and .. traversal so a malformed or
  tampered manifest cannot redirect lookups outside $GITHUB_WORKSPACE.
- findRepoCheckout: verify resolved manifest path exists on disk before
  returning success; fall back to git scan on stale/missing entries.
- buildRepoCheckoutMap: only seed manifest entries whose resolved path is
  safe and exists, matching the guarantee provided by the git-scan branch.
- Tests: cover unset/missing manifest env var, stale path fallback, absolute
  + .. traversal rejection, manifest-wins-over-git-scan priority, and the
  new buildRepoCheckoutMap existence guard.
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

✅ smoke-ci: safeoutputs CLI comment + comment-memory run (27094465052)

Generated by 🧪 Smoke CI for issue #37565 ·

@dsyme dsyme merged commit 1801e79 into main Jun 7, 2026
12 checks passed
@dsyme dsyme deleted the fix/issue-37545-manifest-first-lookup branch June 7, 2026 13:56
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.

Sample replay handler fails to find side-repo checkout because 'Configure Git credentials' overwrites origin URL

2 participants