* fix(pack): notarize macOS builds only once * perf(pack): separate type validation and defer bounded Windows caches * test(e2e): bound BuildKit cleanup job away from pack cache save The deferred Windows pack-cache job checkouts the repo, so slicing clean-closed-pr-buildkit to EOF made the no-checkout assertion fail. Generated-By: looper 0.16.1 (runner=fixer, agent=omp) * test(ci): bound cache cleanup assertions to their job * fix(ci): preserve cache provenance when the release branch advances * perf(ci): reduce compression work in Windows cache handoff * fix(ci): align Windows cache restore and persistence paths * perf(ci): retain latest complete Windows artifacts for repeat builds * fix(ci): keep newer pack caches when ancestor saves finish later Prefix pruning in cache-maintenance used cache creation time, so an older ancestor finishing after a newer same-ref release deleted that newer cache. Prune only when the validated source SHA is the current commit. Generated-By: looper 0.16.1 (runner=fixer, agent=omp)
8.6 KiB
tools-pack build cache contract
This file is the source of truth for the tools-pack build-graph cache: the
cache under --cache-dir that stores packaged build artifacts as
content-addressed nodes. Read it before changing any node key, adding a node,
or changing what a node writes.
It records current state only, not change history — same convention as
specs/current/ci.md.
Out of scope: the GitHub Actions cache that wraps this store. That layer is
owned by .github/; it restores and saves the store as an opaque directory and
carries no correctness obligation (see Why coarse restore is safe below).
Cache model
A node is { id, key, outputs, build, invalidate }, acquired through
ToolPackCache (src/cache.ts).
keyHash = hash(node.id + "\n" + node.key); the entry lives atentries/<node.id>/<keyHash>/with amanifest.json.- An entry is accepted only when
manifest.key === node.keyexactly. A mismatch is reported askey mismatchand the node rebuilds (cache.ts:270-282). There is no fuzzy or prefix matching inside the store. invalidateis an additional per-node veto applied to an otherwise-valid entry (for examplewin.packaged-appre-validates its native rebuild output).materializecopies entry outputs to their workspace locations. Steps that run on the materialization path execute on both the hit and the miss path.
Why coarse restore is safe
Because acceptance is an exact node.key comparison, a restored store is a
pool of candidates, not an authority. Entries that do not match are ignored.
An outer layer may therefore restore a broader or older store than strictly
requested without risking a wrong build — it only affects how much work is
skipped, never what is produced.
This property is what the surrounding CI caching depends on. Do not weaken it by introducing prefix or best-effort matching inside the store.
Node inventory
The build-graph cache is almost entirely Windows-specific.
| Node | Platform |
|---|---|
<platform>.workspace-build |
all |
win.resource-tree |
win |
win.workspace-tarballs |
win |
win.packaged-app |
win |
win.electron-builder-dir |
win |
win.nsis-payload-base |
win |
win.nsis-payload-overlay |
win |
win.nsis-installer |
win |
win.portable-zip |
win |
win.launcher-payload-base |
win |
win.launcher-payload |
win |
mac and linux have <platform>.workspace-build only.
The workspace build delegates Web type validation to independent same-commit
CI/release checks (next typegen plus the full Web tsconfig, including tests).
The build recipe explicitly sets OD_WEB_TYPECHECK=external; this recipe is
part of the workspace node key. Local ordinary next build still checks its
production-only tsconfig. Declaration outputs remain part of the cache contract.
Determinant rules
R1 — A node key must cover every input that determines the node's output.
Inputs include file content, configuration values, tool versions, and process
environment. An input that is read by build but absent from key is a
defect, not an optimization.
R2 — A node key must carry the key of every upstream node it consumes. Re-deriving an upstream node's own inputs is not a substitute: upstream keys carry inputs that are not file content (see R3), so re-derivation silently drops them.
Existing links: win.workspace-tarballs carries workspaceBuildKey;
win.packaged-app carries tarballsKey; win.electron-builder-dir carries
packagedAppKey and resourceTreeKey; win.nsis-installer carries
basePayloadKey and overlayPayloadKey; win.launcher-payload carries
sourceKey.
R3 — Build outputs are never direct key inputs. hashPackageSourcePath
excludes dist, .next, out, node_modules, and .od. A node that
consumes another node's build output must obtain that output's identity
through R2, not by hashing the output tree.
R4 — A node key must not restate a list that already exists as a constant. Derive key inputs from the constant instead. Two independent restatements of the same list drift silently and produce stale entries with no signal.
R5 — Every declared input needs a witness. See Changing a cache node.
Materialization-time parameters
Some values deliberately do not enter node keys. They are stamped onto the output every time the node is materialized, so cached content stays parameter-agnostic and is specialized on the way out.
Current materialization-time parameters:
- App version.
win.packaged-appomits it. It is applied through electron-builderextraMetadata.version, then rewritten on materialization byrewriteUnpackedAppPackageVersionandrewriteWinExecutableVersion, then verified byassertMaterializedUnpackedVersionConsistency— a fail-closed check over the apppackage.jsonversion, theopen-design-config.jsonappVersion, and the Windows executable fixed file version. - Namespace / channel and runtime endpoints.
win.electron-builder-diromits them.open-design-config.json— which carriesnamespace,amrProfile,telemetryRelayUrl,updateMetadataUrl,posthogKey/posthogHost,webOutputMode, andnamespaceBaseRoot— is regenerated on the materialization path bywritePackagedConfig.
The downstream win.nsis-payload-overlay, win.nsis-installer,
win.portable-zip, and win.launcher-payload nodes carry namespace and the
full packagedVersion in their keys, because their content includes the
already-stamped payload. win.nsis-payload-base instead carries only
versionCore: its content excludes Open Design.exe,
resources/app/package.json, and resources/open-design-config.json, which
are assigned to the version-bearing overlay.
win.launcher-payload-base is the exception: its key carries namespace, but
version identity reaches it only indirectly through the upstream sourceKey;
the final win.launcher-payload archive explicitly carries the
version-bearing manifest and configBody.
Requirement. A value may be a materialization-time parameter only when both hold:
- it is re-applied unconditionally on the materialization path, so the hit and miss paths converge; and
- the applied value is verified by a fail-closed assertion.
Adding a materialization-time parameter without (2) is not permitted.
Known asymmetry: app version satisfies (2). The other regenerated config fields —
namespace,amrProfile,telemetryRelayUrl,updateMetadataUrl,posthogKey,posthogHost,webOutputMode,namespaceBaseRoot, and the packaged entrypoint fields — currently satisfy only (1): they are rewritten but not asserted.
Signing boundary
Signing material never enters cache content. resolveWinSigningCacheKey
(src/win/sign.ts) contributes only the certificate SHA-1, digest algorithm,
timestamp algorithm, and timestamp URL, and appears in the keys of
win.nsis-payload-overlay, win.nsis-installer, and win.portable-zip.
win.nsis-payload-base correctly omits signing: it is built before
ensureSignedUnpacked(), while the overlay is built after. Keep that ordering
when changing the payload split.
Confidence tiers
Borrowed from specs/current/ci.md.
certain— the key provably covers the node's full input closure, and a witness test demonstrates it (see below). Onlycertainnodes may participate in cross-run cache reuse policies built on top of this store.- low confidence — anything else.
Grading is fail-closed: a node is low confidence until a witness proves otherwise. A newly added node is low confidence by default.
Declared low-confidence points
Recorded so that grading stays checkable. These are known and accepted; do not extend them.
<platform>.workspace-build— file mode (executable bit) is not hashed byhashPackageSourcePath.win.launcher-payload— theseed: "nsis-base"branch takes content from the NSIS base payload but carries only the literal"nsis-base", notWIN_ARCHIVE_CACHE_VERSION. Bumping that constant without bumping the launcher payload cache versions mismatches.
Changing a cache node
- Read this file and
AGENTS.mdin this directory. - If the change alters what
buildreads or writes, update the key in the same commit. - Bump the node's
schemaVersion/ cache-version constant whenever key semantics change. - Add or update the node's witness test in
tests/. A witness proves both halves:- mutating each declared input changes the key;
- mutating a known non-input (for example a package's
disttree) leaves the key unchanged.
- Never introduce prefix or best-effort matching inside the store.