Skip to main content

Frontend asset build

Status: accepted on 2026-07-28.

Implementation status: implemented by the frontend asset producer on 2026-07-28. Compiled bundles are ignored; the deterministic producer, Python-only fail-closed consumer, commit-bound CI/release handoff, sealed sdist, SBOM/license evidence, and rollback contract described here are enforced in source and tests.

Context

Cockpit is authored in React and TypeScript under web. Its pinned npm graph produces a content-addressed, integrity-checked asset tree under the Python package. The installed Harness wheel serves that tree without Node.js or network access.

The compiled tree is currently tracked. The Harness sdist deliberately omits the frontend source and npm toolchain while retaining the compiled assets. It can therefore build an offline wheel without Node.js, but it cannot regenerate those assets or prove that they match the authored frontend revision.

The frontend asset producer must remove compiled JS, CSS, source maps, and generated declarations from Git without weakening these properties:

  • clean source can produce both wheel and sdist;
  • a wheel built from the sdist remains Node-free and serves Cockpit offline;
  • Gateway and Harness remain independently buildable;
  • missing, stale, or substituted assets fail the build instead of producing a wheel with a broken UI;
  • the producer toolchain, licenses, SBOM, output hashes, and release provenance are reviewable and reproducible.

For this decision, an offline build means that all locked Python and npm inputs or an already-produced verified asset artifact are available locally. It does not imply that an empty machine can reconstruct npm dependencies without a cache or supplied artifacts.

Options compared

CriterionPEP 517 Node build hookSeparate versioned asset packageCI-injected verified assets
Clean-clone buildImplicitly runs npm during Python buildRequires resolving two release artifactsExplicit producer step, then normal Python build
Offline Python buildRequires Node plus the npm cacheRequires the asset package locallyRequires only the verified asset tree
Node availabilityRequired by wheel and sdist producersRequired only by asset-package producerRequired only by the frontend producer job
Editable installHidden Node side effects during installVersion skew between editable Python and assetsExplicit local frontend build into an ignored staging tree
Wheel from sdistNeeds frontend source, Node, and npm inputs in the sdistNeeds another exact packageUses the sealed verified tree embedded in the sdist
Cache and reproducibilityPEP 517 cache hides a second package-manager graphGood per artifact, but two versions must stay alignedAsset digest is the cache key and Python input
Platform supportEvery Python build platform needs the Node toolchainRuntime is portable; release ordering is notProducer runs on the pinned platform; wheel remains pure Python
SBOM and licensesPython and npm evidence are coupled inside one opaque buildEvidence can be attached to the asset packageProducer publishes explicit npm SBOM/license evidence beside the tree
Release recoveryRe-run the mixed Python/Node buildRecover and republish the missing exact asset version firstRebuild or restore the commit-bound asset artifact, then rebuild Python
Repository contractKeeps two packages but makes PEP 517 invoke NodeAdds a third independently versioned distributionPreserves the two-member workspace and one Harness runtime artifact

PEP 517 Node build hook

Hatch build hooks can add ignored generated artifacts or force-include files in a target. They are suitable for validation and inclusion, but using one to run npm ci and Vite would make Node and the npm dependency graph implicit requirements of every isolated Python source build. PEP 517 build isolation does not describe or provision that non-Python graph. It would also make an sdist-to-wheel build depend on Node and frontend source, and would add hidden side effects to editable installs.

This option is rejected. The frontend asset producer may add a Python-only Hatch hook that validates and includes an already-produced tree; that is a consumer guard, not a Node build hook.

Separate versioned asset package

A dedicated asset wheel or archive gives the frontend an independent release identity, but GigaLoom does not need independent runtime rollout of its browser shell. The design would add a third distribution, exact-version coupling, release ordering, another offline-install input, and a new recovery path for asset/Python skew. The current two-member workspace and one installed Harness artifact are simpler and already provide the required runtime boundary.

This option is rejected for the current product. It may be reconsidered only if Cockpit gains a real independent release cadence or multiple Python consumers.

CI-injected verified assets

The frontend producer and Python package consumer remain separate build stages. The producer uses the pinned Node/npm graph to create one complete Cockpit tree. CI passes that tree as a commit-bound, content-addressed build artifact to the Python build, which validates and embeds it in the Harness wheel and sdist. The asset artifact is a build input, not a separately published runtime dependency.

This option is selected.

Decision

The frontend asset producer implements the following contract.

  1. Authored TypeScript, configuration, the npm lockfile, the canonical brand source, and deterministic producer scripts remain tracked. Compiled output is generated into an ignored staging tree at the existing Python package resource location so installed and editable loaders keep one path.
  2. One documented producer command starts from a clean staging tree, runs the pinned frontend build, and emits the existing per-file integrity manifest plus content-free provenance. Provenance binds the Git revision, frontend input digest, lockfile digest, canonical brand digest, Node/npm versions, output-tree digest, and SBOM/license evidence.
  3. CI and release workflows run the producer in a dedicated Node job and pass the exact tree and provenance to the Python artifact job. The transfer is hash-verified and tied to the same source revision. It does not fetch mutable “latest” assets.
  4. A Python-only Hatch consumer validates the manifest, all file sizes and hashes, the complete allowlisted tree, provenance, source revision when the authored source is present, and the absence of symlinks, path escapes, and unexpected files. Missing or stale input fails with the exact local recovery command.
  5. The verified tree is included in both direct wheels and sdists. The sdist remains a sealed Python source artifact: it contains the verified Cockpit tree and consumer metadata, not the frontend toolchain. Building a wheel from that sdist requires neither Node nor network access.
  6. Editable development uses the same ignored staging path. The explicit frontend producer command refreshes it atomically; the verifier rejects a source/provenance mismatch so old local output cannot shadow newer authored source.
  7. The Gateway build remains completely independent. The Harness build has two explicit modes: produce-and-package from a source checkout, or consume a previously verified tree supplied by the same CI/release run.
  8. Release evidence retains the asset digest, npm SBOM/license report, Python wheel/sdist hashes, and provenance attestation together. Rollback checks out the prior release source and either deterministically rebuilds or restores its commit-bound verified asset artifact before rebuilding the Python packages.

The consumer must never run npm, contact a registry, accept an unsigned or unbound mutable asset location, or silently build a wheel without Cockpit.

Spike evidence

The frontend architecture spike used a Git archive of the accepted asset-contract revision and removed compiled assets only inside that temporary copy.

  • npm ci --offline --ignore-scripts restored 301 packages from the local cache, and the production producer recreated 53 packaged asset files.
  • The normal frontend gate passed 37 test files and 137 tests. Two production builds produced the same asset-tree digest: 997003971b3db91353df7d11410f4250d468a596ee6ee1388ee342a3a5a6ec9b.
  • A direct injected Harness wheel and a Node-free wheel rebuilt from its sdist were byte-identical: 4d6773a1edef8a65f4de02edb75d8fc522aed474f9213895340f64d1f96f1af0.
  • The sdist retained the verified asset manifest and omitted the frontend toolchain.
  • Building the same temporary source with the asset directory empty currently succeeds and creates an incomplete wheel. This proves that the fail-closed Python consumer is mandatory before tracked output can be removed.

The local spike had Node.js 22.12 while the repository contract and CI require 22.13 or newer; npm reported that mismatch. The architecture decision does not accept that local runtime as release evidence. The asset producer must run reproducibility, platform, SBOM/license, and release recovery gates under the pinned toolchain.

Consequences

The Python package build stays deterministic, offline-capable, and Node-free when it consumes a verified tree. Frontend production becomes an explicit supply-chain stage with reviewable evidence rather than an implicit side effect or tracked source.

The cost is a new staging/verification contract and CI artifact handoff. The asset producer must implement and test that contract before deleting any tracked compiled file. Until then, the current bundle remains the rollback and packaging source.

References