--- name: shipping-build-artifacts description: Make the build step a real gate on what you actually distribute — build scripts that warn and exit 0 on a missing input, size checks with only an upper bound, hand-maintained file lists that drift from the entrypoints they must cover, committed bundles that go stale when only the source changes, GNU-only shell in release scripts that aborts on the other OS, and verification that runs against the source tree instead of the artifact. Use when writing or reviewing a build/package script, a `dist/` copy step, a release workflow that uploads a zip or installer, or a committed compiled asset. --- # Shipping Build Artifacts Lint, type-check, and tests run against the source tree. What users install is a *different* set of bytes — assembled by a script that most gates never look at, then uploaded by a workflow that trusts whatever the script left behind. Every failure below ships a broken or stale artifact under fully green CI. ## A script that warns and exits 0 is not a gate The shape is universal: a declared list of inputs, a copy loop, a friendly warning when one is missing. ```js for (const f of DIST_FILES) { if (!fs.existsSync(f)) { console.warn(`Warning: ${f} not found, skipping`); // build "succeeds" continue; } fs.copyFileSync(f, path.join("dist", f)); } ``` Move one required file aside and the script prints a line nobody reads, exits 0, and produces a `dist/` without it. Nothing downstream notices: the test job ran against the source tree, and the release job zips `dist/` and attaches it to a public release. The artifact is wholly non-functional — the entrypoint imports a file that isn't there — and the failure is discovered by users. ```js const missing = DIST_FILES.filter((f) => !fs.existsSync(f)); if (missing.length) { console.error(`Missing build inputs: ${missing.join(", ")}`); process.exit(1); } ``` The rule: inside a build script, `warn` may only describe something the artifact survives without. If you cannot say what still works when that file is absent, it is an error and the process must exit non-zero. ## Bound the artifact size on *both* sides A packaging check with only a ceiling — "fail if the zip exceeds 500 KB" — is a cost guard, not a correctness one. A build that silently dropped half its files is *smaller*, so it passes the only check that exists. ```js assert(bytes < 500 * 1024, "package too large"); assert(bytes > 20 * 1024, "package suspiciously small — inputs likely missing"); assert(entries.length === DIST_FILES.length, "package entry count mismatch"); ``` Better still, assert on contents rather than a proxy: list the archive's entries and compare against the set the entrypoints require. ## Derive the file list, or check it against the entrypoints `DIST_FILES` — like a build backend's `only-include`, or a hand-written `package_data` — is a *second* copy of "what this app is made of." The first copy is the manifest, the entry HTML, and the import graph. They drift in one direction: someone adds `utils.js`, references it from the popup, and forgets the copy list. The build stays green and the feature is dead in the packaged app. Either derive the list (bundle from the real entrypoints), or add a check that every path referenced by the manifest and by `