Skip to content

deploy: .gcloudignore doesn't import .gitignore; gcloud app deploy uploads untracked files and trips GAE 10k-file cap #695

@bpowers

Description

@bpowers

Summary

.gcloudignore does not import .gitignore. There is no #!include:.gitignore directive — line 6 /.gitignore merely excludes the ignore file itself. As a result, gcloud app deploy (run by pnpm deploy:web) uploads every file present in the working tree that is not explicitly listed in .gcloudignore, completely independent of git tracking status.

This trips GAE Standard's hard 10,000-file deploy cap: pnpm deploy:web does a full pnpm clean + pnpm build and then aborts at the gcloud app deploy step, after the expensive clean+build has already run.

Evidence (measured on a working tree with git status --porcelain reporting ZERO untracked files)

$ git status --porcelain --ignored=no | grep -c '^??'
0
$ gcloud meta list-files-for-upload . | wc -l
144737            # > 14x the 10,000-file GAE Standard cap

Top-level breakdown of the upload set:

132863 third_party        # gitignored (.gitignore:19 `/third_party`), NOT in .gcloudignore
 10534 .venv-pysimlin     # gitignored (.gitignore:79 `.venv*`), NOT in .gcloudignore
  1115 env                # not gitignored, not in .gcloudignore
    96 docs
    31 .hypothesis
    29 .claude
    26 .agents
   ...

Two compounding root causes, both confirmed:

  1. .gcloudignore is independent of .gitignore. third_party/ and .venv-pysimlin/ are gitignored (so git status is clean), yet both are uploaded because .gcloudignore has no matching rule and no #!include:.gitignore.
  2. The /target/ rule is root-anchored. .gcloudignore line 13 /target/ only excludes the repo-root target/. Nested cargo target dirs escape it — third_party/codex/codex-rs/target exists, and 108536 of the uploaded paths contain /target/.

An operator's real working tree is worse: the finding that surfaced this measured ~177k untracked files (third_party ≈ 143k, .venv-pysimlin ≈ 10.5k, src/pysimlin/.venv ≈ 7.3k, .venv ≈ 5k, env ≈ 1.1k, plus playwright-report/, test-results/, simlin/, src/pysimlin/{venv,build,dist}).

Why it matters

  • pnpm deploy:web fails at the gcloud step for any operator whose tree contains these common gitignored build/dep artifacts (third_party checkouts, Python venvs, cargo targets) — i.e. essentially any developed working tree. The failure happens only after a full clean+build, so it is slow to discover and costs a rebuild to retry.
  • The documented pre-deploy checklist gives NO protection. docs/dev/deploy.md lists "git status is clean" as a checklist item, but the upload set is independent of git state — a clean tree here still uploads 144,737 files. Operators following the checklist have a false sense of safety.
  • Two verifiers rated this medium as a code matter, blocker as an operational matter (it can hard-block a production deploy).

Relationship to existing tech-debt items (related but distinct)

  • tech-debt.md fix edit box when zoomed in #39 "Web deploy uploads the whole monorepo and GAE installs the full dep set" — that item is about dependency-set bloat: GAE's Node buildpack running pnpm install against the root workspace and pulling in firebase-tools/@rsbuild/jest/etc. on the instance. This item is different: it's about the upload set including gitignored/untracked files and hitting the 10,000-file hard cap before the buildpack ever runs. The clean fix is shared (the staging-directory / self-contained-bundle approach described in fix edit box when zoomed in #39 would also bound the upload set), but the failure mode, the measurement, and the interim mitigation are distinct.
  • tech-debt.md make highlight of item clearer #40 "Web deploy mutates tracked public/" — unrelated (cleanup of build output written into the tracked tree).

Constraint on the obvious fix

Naively adding #!include:.gitignore to .gcloudignore would break the deploy the other way: it would EXCLUDE the gitignored-but-load-bearing build output the deploy depends on uploading (src/*/lib, src/engine/core/libsimlin.wasm, public/static, etc. — see docs/dev/deploy.md "What gets uploaded"). So the fix needs either explicit re-includes of those paths or, preferably, the staging-directory/self-contained-bundle approach already discussed in #39.

Suggested interim fix (not implemented here)

  1. Add explicit .gcloudignore exclusions for the high-count offenders: /third_party, /.venv*, /env, /simlin, /playwright-report, /test-results, /src/pysimlin/venv, /src/pysimlin/.venv, /src/pysimlin/build, /src/pysimlin/dist (and consider **/target/ to catch nested cargo targets, or per-path entries).
  2. Add a real pre-deploy gate in docs/dev/deploy.md and/or scripts/deploy-web.sh:
    gcloud meta list-files-for-upload . | wc -l   # must be < 10000
    This is the only check that actually reflects what gets uploaded; git status does not.

How it was discovered

Surfaced during a deploy-risk audit of the gcloud app deploy path. Confirmed in this repo by measuring gcloud meta list-files-for-upload . against a working tree with zero git-untracked files (still 144,737 files uploaded) and by confirming third_party/.venv-pysimlin are gitignored-but-uploaded and that the root-anchored /target/ rule lets third_party/codex/codex-rs/target through.

Components affected

  • .gcloudignore
  • scripts/deploy-web.sh (pnpm deploy:web)
  • docs/dev/deploy.md (pre-deploy checklist)

Metadata

Metadata

Assignees

No one assigned

    Labels

    ciCI, build pipeline, test hygiene

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions