safe-outputs: find_repo_checkout git-scan fallback cannot see step-cloned repositories inside the container (dubious ownership); error message points at actions/checkout which fails the same way
- Dominant language
- Go
- Stars
- 5.1k
- Forks
- 541
- Avg merge
- 5h 48m
- Merged PRs (30d)
- 773
Description
## Summary
Since the safe-outputs MCP server moved into the `gh-aw-node` container (#39100, v0.80.0), `find_repo_checkout.cjs`'s git-scan fallback can no longer discover repositories that were cloned into the workspace by a `steps:` entry (or by a manual `actions/checkout` step) rather than by `checkout:` frontmatter. `create_pull_request` and `push_to_pull_request_branch` then fail with:
```
Repository 'owner/repo' not found in workspace. Make sure it's checked out using actions/checkout with a path.
```
The advice in that message does not help: a manual `actions/checkout` step produces no checkout manifest either, so it hits the same broken fallback.
## Environment
- `github/gh-aw-actions/setup@v0.88.7`, compiler v0.88.7
- `ghcr.io/github/gh-aw-mcpg:v0.4.18`, `ghcr.io/github/gh-aw-node`
- engine: copilot (CLI 1.0.80)
- Workflow clones ~30 org repositories into `repos/` in a `steps:` block (parallel `git clone`), and uses `create-pull-request` with `allowed-repos` listing those repositories.
## What happens
From `mcp-logs/safeoutputs.log` (DEBUG enabled):
```
[safeoutputs] Multi-repo mode: looking for checkout of owner/analytics
[find_repo_checkout] Searching for repo: owner/analytics in workspace: /home/runner/work//
[find_repo_checkout] Found 30 git directories: /home/runner/work//, .../repos/admin, .../repos/analytics, ...
[debug] Executing git command: git config --get remote.origin.url
[debug] Git command output: https://github.com/owner/.git
[find_repo_checkout] Repo at /home/runner/work// has slug: owner/
[debug] Executing git command: git config --get remote.origin.url
[error] Git command failed: git config --get remote.origin.url
[error] Exit status: 1
[find_repo_checkout] No remote URL found for: /home/runner/work///repos/admin
... (identical for all 29 nested clones)
```
The scan finds every nested `.git`, but `git config --get remote.origin.url` exits 1 in each of them.
## Root cause
The container runs as a different uid than the runner user that owns the clones. `ensureSafeDirectoryTrust()` is called for `GITHUB_WORKSPACE` only (`safe_outputs_handlers.cjs` line ~378), and git's `safe.directory` trust does not extend to nested repositories. Under "dubious ownership", `git config --get` does not fail loudly (exit 128) — it silently ignores the repository config and exits 1 as "key not found". So `getRemoteOriginUrl()` returns `null` for every nested clone, and the scan reports nothing.
Reproduction outside gh-aw (git 2.55):
```bash
git init repo && git -C repo remote add origin https://github.com/o/r.git
sudo chown -R root:root repo
git -C repo config --get remote.origin.url; echo "exit=$?" # prints nothing, exit=1
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0='*' \
git -C repo config --get remote.origin.url; echo "exit=$?" # prints URL, exit=0
```
The workspace root works because it is trusted explicitly; the nested clones are not. All the dubious-ownership fixes since June (#40080, #46057) trust paths taken from the checkout manifest, which only exists for `checkout:` frontmatter entries, so step-cloned repositories are left out.
## Why step-based clones matter (motivation)
We run agentic workflows from one central repository against ~30 organisation repositories (CentralRepoOps pattern). The agent needs all of them on disk: it reads skills and instructions from every repo, cross-references code between services, and opens PRs in whichever repos turn out to need changes. Declaring them as `checkout:` entries is not a good fit:
- **Wall-clock.** `checkout:` entries compile to one `create-github-app-token` step plus one `actions/checkout` step per repository, executed sequentially. Our step-based clone runs 6 clones in parallel with `xargs -P6` and finishes all 30 repositories, full history, in about 66 seconds. Thirty sequential token mints and checkouts take several minutes, and with wildcard `target-repo` the safe_outputs job mirrors the same layout, so the cost is paid twice per run. Multiplied over ~16 workflows, several of them triggered per Jira ticket, this is significant.
- **One shared definition.** The repository list, the clone flags, the skills-directory discovery and the hooks installation live in a single imported `steps:` file. A 30-entry `checkout:` block would have to be kept in sync with that list, and per-workflow overrides (shallow vs. full history, which repos to include) are far easier to express in a script than in 30 YAML entries.
- **Dynamic selection.** Some workflows narrow the set of repositories at runtime (from a dispatch input or a ticket). A shell step can do that; `checkout:` entries are fixed at compile time.
- **This used to work.** Before v0.80.0 the safe-outputs server ran on the host and the git-scan fallback discovered any clone under the workspace regardless of how it got there. The container move changed the behaviour without a compile-time warning or a documentation note, and the error message still describes the old contract ("make sure it's checked out using actions/checkout with a path").
Trusting the scanned directories in the fallback would restore the previous behaviour with no change to the manifest-first design.
## Expected
Either of:
1. `find_repo_checkout.cjs` trusts each scanned directory before reading its remote, e.g. call `ensureSafeDirectoryTrust(repoPath)` in `getRemoteOriginUrl()` (or set `safe.directory=*` for the scan only). This restores discovery of any clone under the workspace, which is what the error message promises.
2. If step-based clones are intentionally unsupported, the error message should say that the repository must be declared in `checkout:` frontmatter, and the docs for `create-pull-request`/`allowed-repos` should state it.
## Workaround
Writing `${RUNNER_TEMP}/gh-aw/safeoutputs/checkout-manifest.json` from the clone step (same shape as `build_checkout_manifest.cjs` produces) makes the manifest-first lookup succeed and PRs are created again. That depends on an internal file path, so a proper fix would be preferable.
Contributor guide
Research direction
Start by reading find_repo_checkout.cjs, especially getRemoteOriginUrl(), then inspect ensureSafeDirectoryTrust() in safe_outputs_handlers.cjs and the manifest shape produced by build_checkout_manifest.cjs. Reproduce with nested step-cloned repositories under the container and DEBUG logging. Done means those repositories are discovered by create_pull_request and push_to_pull_request_branch, or the checkout-only contract and documentation are updated if they are intentionally unsupported.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, git, github-actions, javascript
- Domain
- ci-cd, devops, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100