Stop dev-script changes from rebuilding + recreating the prod NAS image
Why this exists¶
Editing a top-level scripts/* file (e.g. scripts/sync-taskboard.sh) was triggering a full
production NAS image rebuild + eop-app container recreate β for tooling the running app never uses.
Spotted while deploying T-047's chop fix: a one-line taskboard-helper change kicked off a ~5-minute
image build and a prod container restart. Two causes, both "scripts/ isn't treated as dev-only":
.github/workflows/nas-image.ymlruns on push tomainwithpaths-ignore: ['docs/**', '**.md']βscripts/**was not ignored, so a scripts-only push started a build..dockerignoredid not exclude top-levelscripts/(onlyservices/bank-login-service/scripts), soCOPY . .baked it into the image β the image digest changed β the deploy step (which recreates only whenOLD_ID != NEW_ID) recreated the container.
Investigation β is any scripts/* file needed at build time? NO.¶
package.json:build=next build,start=next start; nopostinstall/prepare/prebuildhook. Thenode scripts/*entries (dev:ocbc,ocbc:proxy,scan:footers,embed:fonts, β¦) are manual dev utilities, never run by build or start.- No app code imports from
scripts/(grepofapp/ lib/ pages/ components/forfrom 'β¦scripts/'/require('β¦scripts/')β nothing), and no runtime disk read ofscripts/. - The ONE build-time reference β
next.config.ts:146webpack loaderpath.resolve(__dirname, "scripts/replaceImportMetaLoader.js")β points at a file that is untracked and absent on disk (git ls-files+findβ nothing). Its rule only matchesβ¦/antd/node_modules/classnames/index.jsandβ¦/rc-pagination/node_modules/classnames/index.js, and both are absent (npm hoistsclassnamesto the rootnode_modules/classnames), so the rule never matches and the loader is never invoked.path.resolveonly builds a string (doesn't read the file), sonext buildsucceeds without it β which it already does, since CI (actions/checkout) never had the untracked file. Conclusion:scripts/has zero present build dependency.
The fix (both halves, kept in lock-step)¶
.dockerignoreβ exclude the dev tooling, re-include the one loader path for safety: β dev-script edits no longer change the image digest β deploy skips the recreate.nas-image.ymlpaths-ignoreβ addscripts/**(and!scripts/replaceImportMetaLoader.jsso a real loader change still rebuilds) β a scripts-only push doesn't build at all.- The loader is re-included in BOTH so the two never disagree: if it's ever restored as a real build input, it ships in the image AND its changes trigger a build.
Reasoning-pass verification¶
- docs-only push β matches
docs/**β skip build β no recreate. β (unchanged) - scripts-only push (e.g.
sync-taskboard.sh) β matchesscripts/**β skip build; and even if forced viaworkflow_dispatch, the image digest is unchanged (scripts/ excluded) β skip recreate. β - app-code push β no ignore match β build runs β image changes β recreate. β (unchanged)
- mixed app+script push β the app file doesn't match any ignore β build runs normally. β
- loader change (if it's ever created) β
!scripts/replaceImportMetaLoader.jsun-ignores it β build runs, and the re-include keeps it in the image. β - One-time cost: the commit that lands this change touches
.dockerignore(not ignored), so it rebuilds once and recreates once (the new image no longer containsscripts/); from then on, scripts-only pushes are free.
Landed + empirically confirmed (2026-06-24)¶
- Commit
ad9be2dcβmain. Itsnas-image.ymlrun all green, incl. step 6 build-push and step 7 "Deploy to NAS (pre-pull + recreate)". This is the empirical proof of the core claim: the image was built withscripts/EXCLUDED from the Docker context (the new.dockerignore) and the build still succeeded β so nothing inscripts/is a build dependency, exactly as analysed. The one-time recreate ran; prod now serves ascripts/-free image. - The remaining behaviour (a scripts-only push triggers no
Build NAS app imagerun, and a forcedworkflow_dispatchwould skip the recreate on an unchanged digest) is now in effect; it'll be visible on the next real scripts-only change.
Decision record¶
- β
Attestation (EOP Local Assistance (fork)): read
AGENTS.md; checked the board by scope β T-073/T-091/T-082 (the deploy tasks) are alldone, no open task covers this, so opened T-104. Source: EOP Local Assistance (fork) Β· session e527df5f-d3ea-4716-8076-b46319d9d830 (local Claude Code). - Proposed: EOP Local Assistance (fork) (spotted during the T-047 chop-fix deploy β a
sync-taskboard.shedit triggered a prod rebuild). - Approved: the owner started this as a background task (the chip spun off from that observation).
- Owner's framing (verbatim, 2026-06-24): "make dev-tooling script changes NOT churn the prod image"
β¦ "Preferred: add
scripts/⦠tonas-image.yml'spaths-ignore⦠BEFORE doing this, VERIFY noscripts/*file is needed at Docker build time ⦠And/or addscripts/to.dockerignore." Did exactly that, plus the loader-path verification the owner asked for. - Blast radius:
.dockerignore+nas-image.ymlonly β pure build/deploy config; no app code, no runtime behaviour change. The first deploy after this dropsscripts/from the image (slightly smaller image; nothing runtime reads it). Cross-cuts the deploy pipeline (T-091 auto-deploy, T-073 layer caching) β agents touching those should knowscripts/is now image-excluded + build-skipped.
Follow-up β DONE (2026-06-24)¶
next.config.ts referenced the untracked/vestigial webpack loader scripts/replaceImportMetaLoader.js
β a latent trap (if a dep bump gave antd/rc-pagination a nested classnames, the rule would match and the
build would fail on the missing loader). Resolved by removing the dead rule (the loader file, the
CLASSNAMES_FRAGMENTS test list, and the now-unused path import), keeping the standalone
module.parser.javascript.importMeta = true flag β which is the real import.meta handler (the loader
never ran: it was never committed, and its rule never matched with classnames hoisted). tsc + eslint clean;
next build re-verified by the CI image build on the landing commit. Owner approved ("proceed with that",
2026-06-24).