Skip to content

Reduce scan startup time in large repositories - #301

Draft
lelia wants to merge 7 commits into
mainfrom
lelia/reduce-scan-startup-time
Draft

Reduce scan startup time in large repositories#301
lelia wants to merge 7 commits into
mainfrom
lelia/reduce-scan-startup-time

Conversation

@lelia

@lelia lelia commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Scans spent most of their wall-clock time in local setup rather than in the API. Two phases dominated:

  1. Manifest discovery started a separate recursive rglob() traversal for every expanded manifest pattern, so each scan root was re-walked once per pattern. Excluded directories were filtered only after the walk had already descended into them, and .git was never excluded.
  2. Git initialization ran git fetch --all unconditionally, pulling every remote branch and tag before changed-file detection began.

This replaces both with single-pass equivalents, adds native Buildkite support, and adds stage-level timing so the remaining cost of a scan is visible in INFO logs.

Changes

Single-pass manifest discovery. One os.walk() per scan root instead of one traversal per pattern. Patterns are expanded and case-folded once, excluded directories (now including .git) are pruned before descent, and non-manifests are rejected on their basename before any path work happens.

Targeted Git fetching. git fetch --all is gone. Commit and branch metadata resolve locally, pull-request comparisons prefer refs already in the checkout, and a single base or head ref is fetched only when it's missing.

Native Buildkite context. Buildkite's own environment variables are recognised for commit, branch and pull-request range, and --scm github falls back to them for comment context. Buildkite jobs no longer need to shim GitHub Actions variable names.

Observability. INFO-level durations for CLI registration, organization setup, Git initialization, each fetch and the ref it requested, changed-file detection, pattern retrieval, and manifest discovery — plus discovery counters (files and directories visited, directories pruned, manifests found).

Behavior

Existing scan routing, output, and CLI behaviour are unchanged across GitHub Actions, GitLab CI, Bitbucket Pipelines, Buildkite and local execution. This does not skip full scans when no manifest changed.

Two intentional differences:

  • .git is excluded from manifest discovery. Manifest results are otherwise equivalent to the previous implementation.
  • Directory exclusions now honour globs. *.egg-info is in the default exclusion set but the previous exact-name matcher never applied it, so *.egg-info directories were being walked and their contents discovered. They are now excluded as intended.

One log line changed for accuracy: No Manifest files changed is now No supported manifest detected in the changed-file set; creating a full Socket report. The count-only Total files found: N line is retained for existing log consumers.

Validation

  • Matching-parity tests against the previous rglob implementation for every built-in ecosystem and pattern, covering case-insensitivity, brace expansion, nested patterns, dot-directories, exclusions, inclusions, symlinks, excluded ecosystems, multiple roots, sorting and deduplication. Asserts each root is walked exactly once and that excluded directories are pruned before traversal.
  • Git tests for local-ref preference, absence of an unconditional fetch, the targeted-fetch fallback, all four CI providers, and non-PR and detached-HEAD execution.
  • benchmarks/manifest_discovery.py is an opt-in developer benchmark (not a timing assertion in the suite) that compares old and new discovery on a synthetic large-monorepo fixture and fails on any result mismatch. It demonstrates one tree walk rather than one walk per pattern; it deliberately makes no promise about a fixed production speedup.
  • Full unit suite (467 passed), Ruff clean on all changed files, package build verified.

Still to do before this leaves draft: validate a prerelease build in an affected pipeline and compare the new stage timings against the reported baseline.

Ref: CE-379

lelia added 6 commits August 12, 2026 17:35
find_files() started a separate recursive rglob traversal for every expanded
manifest pattern, so a scan re-walked each root once per pattern and only
filtered excluded directories after descending into them. Replace that with one
os.walk() per scan root:

- Expand and case-fold all active patterns once, then match in memory.
- Prune excluded directories, including .git, before descending.
- Reject non-manifests on the basename alone (one set lookup plus one compiled
  glob alternation) before building a relative path or running a path match.
- Cache supported manifest patterns per Core instance, but only when the API
  lookup succeeds, so a transient failure does not pin the run to the smaller
  local fallback pattern set.
- Emit INFO durations for organization setup, pattern retrieval and discovery,
  with files/directories visited, directories pruned and manifests found.

Matching behaviour is unchanged apart from intentionally excluding .git
metadata. Adds parity tests against the previous rglob implementation for every
built-in ecosystem and pattern, covering case-insensitivity, brace expansion,
nested patterns, dot-directories, exclusions, inclusions, symlinks, excluded
ecosystems, multiple roots, sorting and deduplication, plus an opt-in benchmark
that asserts old/new result equality on a synthetic large-monorepo fixture.

Ref: CE-379
Git.__init__() ran `git fetch --all` on every invocation, pulling every remote
branch and tag before changed-file detection even began. Resolve commit and
branch metadata locally instead, and for pull-request comparisons prefer refs
already present in the checkout, fetching a single base or head ref only when it
is missing.

Also recognises Buildkite's native BUILDKITE_COMMIT, BUILDKITE_BRANCH,
BUILDKITE_PULL_REQUEST and BUILDKITE_PULL_REQUEST_BASE_BRANCH so Buildkite jobs
can calculate a complete base-to-head changed-file range without mapping their
environment onto GitHub Actions variable names. Buildkite is checked before
GitHub because some pipelines deliberately export GitHub-compatible variables.

Adds INFO durations for Git initialisation, changed-file detection and each
fetch, including the ref requested and why. Existing GitHub Actions, GitLab CI,
Bitbucket Pipelines and local behaviour is preserved; tests cover local-ref
preference, absence of an unconditional fetch, the targeted-fetch fallback, all
four CI providers, and non-PR and detached-HEAD execution.

Ref: CE-379
`--scm github` read its configuration solely from GITHUB_* variables, so
Buildkite users had to shim every one of them to get PR comments. Fall back to
Buildkite's own variables when the GITHUB_* equivalents are absent: PR number,
commit, branch, checkout path, commit message, build creator, and owner/repository
parsed from BUILDKITE_REPO (preferring the pipeline repository over a
contributor's fork). Explicit GITHUB_* and PR_NUMBER values still take priority,
and GitHub Enterprise remains configurable via GITHUB_API_URL.

A running Buildkite PR build maps to the supported `synchronize` comment path,
and a non-PR build maps to `push`, so event routing is unchanged. Default-branch
detection requires an actual branch name rather than treating two unset variables
as a match, which would otherwise mark any build as the default branch and
overwrite the repository baseline.

Ref: CE-379
The --sub-path routing pre-check walked every selected path to decide whether any
manifests existed, then discarded the result so scan creation walked the same
paths again. Retain and reuse it.

Apply --excluded-ecosystems before the pre-check rather than after, so every
find_files() call in a run sees the same ecosystem filter.

Add an INFO duration for CLI run registration, and replace the
"No Manifest files changed" line with wording that describes the decision being
made: no supported manifest was detected in the changed-file set, so a full
report is created. Scan-routing semantics are unchanged.

Ref: CE-379
Bumped via .hooks/sync_version.py so __init__.py, pyproject.toml and uv.lock
stay in sync, and moved the changelog entry under a 2.6.5 heading.

Ref: CE-379
@lelia
lelia deployed to socket-firewall August 13, 2026 00:43 — with GitHub Actions Active
@lelia lelia added publish-preview Publish a CLI preview to TestPyPI. publish-docker-preview Publish `socketdev/cli:pr-<number>` to DockerHub labels Aug 13, 2026
@github-actions

Copy link
Copy Markdown

🚀 CLI preview published: socketsecurity==2.6.5.dev3165619303101

pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple socketsecurity==2.6.5.dev3165619303101

TestPyPI's package index can take several minutes to expose a newly uploaded version.

The publish-docker job downloads the built wheel to ./dist, but the build step
omitted `context`, so docker/build-push-action used its default Git context.
Buildx then cloned the repository as the build context, where ./dist does not
exist, and `COPY dist/socketsecurity-*.whl` failed with
"lstat /dist: no such file or directory".

Set `context: .` so the build uses the workspace the artifact was downloaded
into. This also makes the job's existing trust boundary hold as documented: the
context is now the default-branch checkout rather than the pull-request ref, so
Dockerfile.preview is read from trusted code and the pull request still enters
the image only through the built wheel.

Pre-existing; the TestPyPI half of the workflow is unaffected.
@lelia lelia added publish-docker-preview Publish `socketdev/cli:pr-<number>` to DockerHub and removed publish-docker-preview Publish `socketdev/cli:pr-<number>` to DockerHub labels Aug 13, 2026
@lelia
lelia deployed to socket-firewall August 13, 2026 01:03 — with GitHub Actions Active
@github-actions

Copy link
Copy Markdown

🐳 Docker preview published: socketdev/cli:pr-301

This mutable tag is only created when a Docker preview is explicitly requested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

publish-docker-preview Publish `socketdev/cli:pr-<number>` to DockerHub publish-preview Publish a CLI preview to TestPyPI.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant