Build and release

The distro release workflow runs for version tags and manual dispatch. It builds several artifacts that have to agree on version and runtime contract. The Docker packaging step consumes already built STATIC and eBPF inputs; it does not compile them inside Alpine.

For the complete Linux/WSL musl toolchain and packaging sequence, use the local builder recipe. Verify published artifacts with the signature-verification guide.

Build inputs and toolchains

  1. Install the locked Node dependencies for the browser runtime. The JS builder stages Window, classic-worker and module-worker bundles and verifies a metadata-matched set. Cargo watches source/schema/build/lock inputs and embeds verified matching outputs.
  2. Build static_proxy for x86_64-unknown-linux-musl with Cargo. The native TLS dependency builds patched BoringSSL C/C++ sources, so CI wires Zig-backed musl-gcc and musl-g++ wrappers and a C++ runtime.
  3. Compile src/ebpf/ttl_editor.o separately with clang -target bpf, Linux/libbpf headers, and LLVM tools.
  4. Stage those two artifacts into the Alpine rootfs and write /opt/404/distro-version.

Release helpers validate artifact paths, stream SHA-256 hashes and reject duplicate artifact basenames. Signed artifacts establish provenance and byte identity, not browser privacy effectiveness. Profile publication hashes must cover the normalized committed JSON bytes; source/build checks and actual browser/proxy acceptance remain separate. See Profile maintenance and runtime limits.

The local build notes and build.sh describe equivalent builder prerequisites. A glibc Linux binary may fail inside Alpine; the explicit musl target matters.

Package the root filesystem

./distro/build.sh \
  --static-binary ./src/STATIC_proxy/target/x86_64-unknown-linux-musl/release/static_proxy \
  --ttl-object ./src/ebpf/ttl_editor.o \
  --version vX.Y.Z \
  --output ./dist/404-distro.tar.gz

The script builds a temporary Alpine image from Dockerfile.build, creates a container without running the application, exports its filesystem, and compresses it. The exported rootfs is what WSL later imports. The image’s CMD ["/bin/sh"] is not a WSL startup service; a launcher or desktop orchestration must start STATIC.

Manifest and signature

The workflow hashes the tarball, writes a manifest with version, sha256, artifact_path, and published_at, then signs the manifest bytes with the DISTRO_MANIFEST_SIGNING_KEY release secret. On a tagged release it publishes a stable distro/manifest.json and signature, plus versioned tarball and manifest objects in the public R2 update origin. GitHub Release assets include the tarball, manifest, signature, and Windows operator ZIP.

{
  "version": "vX.Y.Z",
  "sha256": "<tarball digest>",
  "artifact_path": "/distro/vX.Y.Z/404-distro.tar.gz",
  "published_at": "<timestamp>"
}

A SHA-256 digest in a manifest establishes a relationship only after the manifest’s signature and signing key are trusted. The desktop documentation describes an embedded public key for app verification; source confirmation of that app code remains outstanding. The operator ZIP includes a copy of the manifest and signature but its sample CLI hash check alone does not verify the signature.

What the Windows operator ZIP adds

The release workflow also packages 404-windows-x64.zip with 404-distro.tar.gz, profile JSON, a WSL runtime TOML, a control-token file, and copies of manifest/signature files. Its config uses file keystore mode inside WSL and legacy Windows roaming paths. The ZIP is a convenience layout around the rootfs, not a native Windows STATIC binary. See Provisioning and updates and the workflow source.

Local builder sequence

On a Linux builder with Docker, Rust and the musl target, Node, clang/LLVM, libbpf and Linux headers, compile the two inputs before packaging. The current BoringSSL build also needs a musl-capable C++ compiler and runtime; the CI workflow uses Zig-backed wrappers. These commands show the stages; configure equivalent musl C/C++ tooling for your machine before Cargo.

npm ci --prefix src/STATIC_proxy/build
npm run build --prefix src/STATIC_proxy/build
rustup target add x86_64-unknown-linux-musl
cargo build --release --locked \
  --manifest-path src/STATIC_proxy/Cargo.toml \
  --bin static_proxy --target x86_64-unknown-linux-musl
make -C src/ebpf clean all

The eBPF Makefile checks for clang, llvm-strip, tc, /usr/include/bpf/bpf_helpers.h, and /usr/include/linux/bpf.h. The distro build notes and scripts/setup-musl-toolchain.sh cover environment-specific setup.

Docker export and inspection

build.sh accepts --static-binary, --ttl-object, --version, --output, and --image-tag; its defaults are the musl release binary, src/ebpf/ttl_editor.o, dev, dist/404-distro.tar.gz, and 404-distro-build:local. It stages distro/rootfs/ and the two artifacts, writes the version file, builds a temporary image, and runs docker create then docker export | gzip -n. docker save produces an image archive and cannot substitute for the flat filesystem tarball expected by wsl --import.

tar -tzf dist/404-distro.tar.gz | head -100

Expect opt/404/static, opt/404/ttl_editor.o, opt/404/404-init.sh, opt/404/distro-version, and etc/wsl.conf. The Docker image is a packaging intermediate, not the distributed runtime.

CI publication and verification

The workflow runs on v* tags and manual dispatch. It uploads workflow artifacts; a tagged release also publishes stable manifest and signature objects, immutable versioned objects to the update origin, and GitHub Release assets. The stable manifest uses no-store caching while versioned tarballs can be immutable. Verify the public publication contract with ./scripts/verify-distro-publication.sh --base-url https://updates.404privacy.com --version vX.Y.Z after a release. This checks reachability and expected version; signature verification remains a separate consumer step.