Skip to content

fix: changed_files resolved to zero files in the Docker container action (git dubious ownership) - #105

Open
David Larsen (dc-larsen) wants to merge 8 commits into
mainfrom
dc-larsen/fix-changed-files-safe-directory
Open

fix: changed_files resolved to zero files in the Docker container action (git dubious ownership)#105
David Larsen (dc-larsen) wants to merge 8 commits into
mainfrom
dc-larsen/fix-changed-files-safe-directory

Conversation

@dc-larsen

@dc-larsen David Larsen (dc-larsen) commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Problem

changed_files diff-only mode resolves to zero files on every PR when Socket Basics runs as the pre-built Docker container action. Each run logs No scan targets to analyze (scoped scan matched no existing files); skipping OpenGrep and exits green with zero alerts. The scan looks healthy while scanning nothing. Full-repo mode on the same checkout works. A customer hit this rolling the action out org-wide on v2.2.1 (eng-support thread: https://socketdev.slack.com/archives/C05TT2Q3FEZ/p1786394320795579), and it reproduces identically on 3.0.0.

Root cause

GitHub runs Docker container actions as root, while the checkout mounted at /github/workspace is owned by the runner user (uid 1001). Git 2.35.2+ refuses to read a repository owned by a different user. Every git subprocess in _detect_git_changed_files fails, the helper catches the CalledProcessError and returns [], and the empty scope makes every scanner skip.

actions/checkout does not cover this case: its safe.directory entry is written to /home/runner/.gitconfig, but container actions run with HOME=/github/home. A prior workflow step running git config --global --add safe.directory misses the container for the same reason.

The same mismatch breaks the git fallbacks in _discover_repository, _discover_branch, _discover_commit_hash, and _discover_is_default_branch. That surface shows up in local Docker runs, where no GITHUB_* env vars mask it.

Fix

A _git_env() helper injects safe.directory=<workspace> into the environment of each git subprocess via command-scope GIT_CONFIG_COUNT / GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n entries. Git honors command-scope safe.directory since 2.38, and the image ships 2.47.

  • No git config files are written, and nothing outside these subprocesses changes.
  • Caller-provided GIT_CONFIG_* entries are preserved, with ours appended after them, so users who already deployed the env-var workaround are unaffected.
  • The workspace is an explicit scan target, not an incidentally discovered repository, so marking it safe matches what the user asked the tool to do.

Testing

Unit (tests/test_changed_files_scope.py)

  • TestDubiousOwnership drives the real git ownership check end to end using GIT_TEST_ASSUME_DIFFERENT_OWNER, git's own test knob for this code path. A control probe skips the tests on a git build without the knob. Both tests fail on the unpatched code and pass with the fix. I confirmed that by disabling the injection and re-running.
  • TestGitEnv covers appending after caller entries, a malformed GIT_CONFIG_COUNT, and the GITHUB_WORKSPACE default.
  • Full suite: 222 passed.

Published container images

Repo owned by uid 1001, process as root, GITHUB_BASE_REF=main, comment-only Go change:

Case Image Result
Unpatched, flags given 3.0.0 skip, Total alerts: 0 (bug)
Unpatched, no --repo/--branch 3.0.0 repository discovery fails (bug, second surface)
Patched 3.0.0 diff resolves, seeded finding reported, Total alerts: 1
Patched, no --repo/--branch 3.0.0 git discovery works, Total alerts: 1
Patched 2.2.1 Total alerts: 1
Patched + user GIT_CONFIG_* block already set 3.0.0 Total alerts: 1 (workaround coexists)
Patched, root-owned repo (ownership matches) 3.0.0 Total alerts: 1 (no regression)
Patched, delete-only PR 3.0.0 skip, Total alerts: 0 (empty-diff semantics preserved)

Real GitHub Actions A/B

One workflow, two jobs on a comment-only Go-file PR in a scratch repo. Each job builds this action from source at a different ref. The rig branches on my fork swap image: to Dockerfile and the trivy base to the public upstream, and are otherwise identical to base and fix.

  • baseline (unpatched main): No scan targets to analyze (scoped scan matched no existing files); skipping OpenGrep, Total alerts: 0, job passed green. That is the customer symptom on a real runner.
  • fix (this branch): the diff resolved to svc/main.go, OpenGrep ran on it, and the job failed the check with Total alerts: 1 and Found 1 high/critical severity issues. Failing the check is the intended blocking behavior for a seeded critical finding.

Run: https://github.com/dc-larsen/sb-changed-files-test/actions/runs/31432924728 (private scratch repo, log lines quoted verbatim above). The rig branches test-ownership-baseline and test-ownership-fix on dc-larsen/socket-basics reproduce this against any scratch repo.

Notes for review

  • fix/changed-files-scope-observability is complementary: it makes this failure loud instead of silent. One interaction: its troubleshooting doc recommends git config --global --add safe.directory in a prior step, which cannot reach the container action. After this lands, that row can be dropped or swapped for the env-var form.
  • CHANGELOG entry added under Unreleased.

Note

Medium Risk
Changes core CI scoping and git integration: a failed diff now full-scans the repo (intentional but can surprise large repos), while shallow misconfiguration correctly fails fast instead of falling back.

Overview
Fixes changed_files diff-only mode resolving to zero files in the pre-built Docker GitHub Action: the container runs as root while the checkout is owned by the runner user, so git’s ownership check blocked every diff lookup and scanners skipped with a green run. Git subprocesses now mark the scan workspace as safe.directory via command-scope GIT_CONFIG_* env vars (no config files touched; existing user GIT_CONFIG_* workarounds are preserved). The same _git_env() wiring applies to repository/branch/commit and default-branch git discovery.

changed_files resolution failures are no longer treated like an empty diff: git stderr is logged, resolved scope is logged at INFO (file list at DEBUG), and when the diff cannot be resolved the run falls back to a full-repo scan with a warning instead of silently scanning nothing—except shallow checkouts that cannot resolve the base ref, which fail fast with a message to set fetch-depth: 0. Genuinely empty diffs (e.g. delete-only PRs) still skip scanners as before. Trivy/TruffleHog connectors treat None from _detect_git_changed_files as no changed files via or [].

Docs and CHANGELOG describe the fallback and shallow-checkout behavior.

Reviewed by Cursor Bugbot for commit b7c757a. Configure here.

…sses

The pre-built Docker action runs as root while the checkout at
GITHUB_WORKSPACE is owned by the runner user, so git's ownership check
(git 2.35.2+) refused the repository. changed_files diff-only mode then
resolved to zero files on every PR and the scanners silently skipped with
a green run. Git-based repository/branch/commit and default-branch
discovery failed the same way in local Docker runs. actions/checkout's
own safe.directory entry cannot help: it lands in the runner's global
config, which is not mounted into container actions.

Inject safe.directory for the scan workspace into the environment of
each git subprocess via command-scope GIT_CONFIG_* entries. No config
files are touched, and caller-provided GIT_CONFIG_* entries (including
the previously documented env-block workaround) are appended after, not
clobbered.

Tested:
- unit: new TestDubiousOwnership tests drive the real git ownership
  check via GIT_TEST_ASSUME_DIFFERENT_OWNER; they fail on the unpatched
  code and pass with the fix. TestGitEnv covers append-after-caller,
  garbage GIT_CONFIG_COUNT, and the GITHUB_WORKSPACE default. Full
  suite: 222 passed.
- container: on the published 3.0.0 and 2.2.1 images with a uid-1001
  checkout and a root process, unpatched runs skip with zero targets;
  patched runs resolve the PR diff and report the seeded finding, with
  and without a pre-existing user GIT_CONFIG_* block. Same-owner and
  delete-only-PR behavior unchanged.
@dc-larsen
David Larsen (dc-larsen) requested a review from a team as a code owner August 10, 2026 21:18
@lelia lelia self-assigned this Aug 10, 2026
@lelia

lelia commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

bugbot run

Comment thread socket_basics/core/config.py Outdated
Git ignores relative safe.directory values, so a relative --workspace
under an ownership mismatch still failed the check and the diff kept
resolving to empty. Resolve the path before writing the entry.

Regression tests: a relative --workspace now passes the end-to-end
ownership test, and TestGitEnv asserts the injected value is absolute.
Both fail without this change. Full suite: 224 passed. Re-ran the
container check on the published 3.0.0 image: unchanged, finding still
reported.
A git failure during scope resolution previously collapsed into the same
empty list as a genuinely empty diff, so any future breakage (beyond the
safe.directory fix) would again skip every scanner and report green.

- _detect_git_changed_files now captures git stderr (instead of DEVNULL),
  logs the failure reason, and returns None on failure vs [] for a truly
  empty diff. Ref-not-found is classified separately so the base-ref
  candidate loop still falls through, while unreadable-repo errors fail fast.
- On failed resolution the config layer falls back to a full-repo scan with
  a prominent warning, never a silent zero-file skip. Delete-only diffs keep
  the empty-scope skip (existing test still guards this).
- An unresolvable base ref in a PR context (e.g. shallow fetch) is now a
  failure rather than a quiet fall-through to the usually-empty staged diff.
- Resolved scope is logged: file count at INFO, full list at DEBUG (the
  customer ask from the report).
- Connector-internal staged-diff callers get 'or []' for the new contract.

Full suite: 231 passed.

Signed-off-by: lelia <2418071+lelia@users.noreply.github.com>
@lelia

lelia commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

bugbot run

@lelia

lelia commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

David Larsen (@dc-larsen) Heads up — I pushed 3e836bd onto your branch so the observability half from the support report ships in this same PR/release. Built directly on your work — the root-cause fix and the GIT_TEST_ASSUME_DIFFERENT_OWNER test approach are great.

What it adds: detection now distinguishes failed resolution (None) from a genuinely empty diff ([]); git stderr is captured and logged instead of discarded; ref-not-found stays a soft miss (your candidate-loop and delete-only behavior are preserved, tests still green) while unreadable-repo errors fail fast. On failure, the config layer falls back to a full-repo scan with a prominent warning instead of silently skipping every scanner — that also now applies to an unresolvable base ref in a PR context (e.g. shallow fetch), which previously fell through to the usually-empty staged diff. Resolved scope is logged every run (count at INFO, list at DEBUG) — the direct ask. Four connector-internal staged callers got or [] for the new contract. 7 new tests, full suite 231 passing, CHANGELOG extended.

One thing worth your eyes specifically: the full-scan-fallback and shallow-fetch-base cases are deliberate semantic changes from "skip silently" — flag if you know a customer scenario where that's the wrong call.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 3e836bd. Configure here.

@dc-larsen

Copy link
Copy Markdown
Contributor Author

Verified 3e836bd end to end on the container rig: the ownership case still resolves and alerts, delete-only PRs still skip, and the missing-base case warns with the real git error before full-scanning. 231 tests pass locally, and the stderr capture with the dubious-ownership hint is exactly what support needed.

On your question, two customer scenarios worth weighing, both from the account that hit the original bug:

  1. Very large repos choose diff-only mode partly because full-repo is the documented OOM path. Their main monorepo is 3.8 GB. A missing fetch-depth: 0 is deterministic, so with the fallback every PR in a misconfigured repo runs a full scan, and on a repo that size that's an OOM or timeout red check rather than a clean signal about the real problem.

  2. Their full-repo run reports 851 findings, 28 high/critical. In PR context the fallback exits non-zero on pre-existing findings, so one org-wide workflow-template mistake blocks every PR with a very large comment, and the red check reads as "you have hundreds of findings" rather than "add fetch-depth: 0".

Both are loud and discoverable, which beats the silent skip. If you want to blunt them, the deterministic base-unresolvable case could fail fast with a config error naming the one-line fix, keeping the full-scan fallback for the non-deterministic failures. Fail-toward-scanning is a defensible default for a security tool and the warning names the cause, so I'm fine shipping it as is.

The shallow-fetch change itself looks strictly better. The staged fallthrough in CI was almost always an empty diff, and local pre-commit runs have no GITHUB_BASE_REF so they keep the staged path.

lelia added 2 commits August 12, 2026 13:19
The full-scan fallback is deliberate fail-toward-scanning behavior, with two
known consequences on large repos (surprise full scans can be slow/OOM; the
scan reports pre-existing findings until the checkout misconfiguration —
usually a missing fetch-depth: 0 — is corrected). Document both, the fix, and
how to tell an empty diff from a failed lookup in the logs.

Signed-off-by: lelia <2418071+lelia@users.noreply.github.com>
A shallow checkout (missing fetch-depth: 0) can never resolve the PR base
ref, so the full-scan fallback would fire on every PR — slow or OOM-prone on
large monorepos, and reporting pre-existing findings instead of the actual
problem. That case is deterministic, so exit with a configuration error
naming the one-line fix (matching the existing SystemExit convention for
repository/branch discovery failures). Non-deterministic resolution failures
keep the full-scan fallback. Proposed by @dc-larsen in review.

Shallowness probed via git rev-parse --is-shallow-repository; tests fake it
by touching .git/shallow. Full suite: 236 passed.

Signed-off-by: lelia <2418071+lelia@users.noreply.github.com>
@lelia

lelia commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

David Larsen (@dc-larsen) thanks for running it end-to-end - and for raising the two real-world customer scenarios.

I went one better than documenting: your fail-fast suggestion is now in the PR (b7c757a). Shallow checkout + unresolvable base → configuration error naming the fetch-depth: 0 fix (via git rev-parse --is-shallow-repository, matching the existing SystemExit convention the repo/branch discovery paths use); everything non-deterministic keeps the full-scan fallback. Shallow-with-resolvable-base still diffs normally, delete-only still skips — tests cover all four quadrants, full suite at 236.

The tradeoffs are documented, too (a204ae7): both your scenarios in github-action.md + parameters.md, with the shallow carve-out noted, plus how to tell an empty diff from a failed lookup in the logs.

So the final behavior matrix: deterministic misconfig → precise error with the fix; unknown failure → loud full scan; empty diff → skip; and nothing, ever, silently scans zero files.

@lelia

lelia commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b7c757a. Configure here.

Comment thread socket_basics/core/config.py Outdated
pr_files = _diff_against_base(base)
if pr_files is None:
_fail_fast_if_shallow(base)
return pr_files

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fail-fast skips merge-base errors

Medium Severity

_fail_fast_if_shallow only runs when _diff_against_base returns None (soft ref miss). When the base tip exists but a shallow history makes A...HEAD fail with no merge base, that becomes a hard _GitScopeError, skips the shallow check, and still takes the full-repo fallback — the expensive every-PR path this change is meant to block after a common shallow base fetch.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b7c757a. Configure here.

lelia added 3 commits August 12, 2026 17:21
Bugbot: the fail-fast only ran on the soft ref-miss path, but the common
partial-fetch shape (base tip fetched, history disconnected) fails with
'A...HEAD: no merge base' — a hard error that skipped the shallow check and
took the full-scan fallback on every PR. Classify no-merge-base distinctly
and route both failure shapes through the shallow check; non-shallow
no-merge-base keeps the fallback (with a warning). Docs broadened to cover
both shapes. Full suite: 238 passed.

Signed-off-by: lelia <2418071+lelia@users.noreply.github.com>
An unresolvable diff scope previously widened to a full-repo scan. Both
that and skipping the scanners are dishonest outcomes: skipping exits
green having scanned nothing, so a passing check inspected no code and a
warning in a run log is not a signal anyone acts on; widening does the
expensive thing on every PR, which is precisely what requesting a diff
scope was avoiding, and it reports pre-existing findings rather than the
PR's own.

Diff-only scoping is an explicit instruction, so when it cannot be
honored the run now stops with a configuration error naming the
underlying git error. This generalizes the fail-fast already applied to
shallow checkouts, which keep their more specific fetch-depth message.

scan_all is the documented opt-in for the previous widening behavior: it
already meant "when the scope resolves to nothing, scan everything", so
it doubles as the fail-open escape hatch. It is now a declared action
input rather than env-only.
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.

2 participants