Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/reproducible-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Reproducible build

# Verifiable release pipeline (parallel to the Netlify deployment):
# on every version tag, build the bundle in the pinned environment,
# fingerprint it, and publish artifact + hash on the GitHub Release.
# Anyone can check the fingerprint with: distribution/verify.sh <tag> <hash>

on:
push:
tags: ['v*']
workflow_dispatch: {}

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Verifiable build (pinned environment)
run: bash distribution/build.sh HEAD

- name: Determinism check — rebuild from scratch and compare
run: |
HASH1="$(cat distribution/out/BUILDHASH)"
cp distribution/out/BUILDMANIFEST /tmp/manifest1
bash distribution/build.sh --no-cache HEAD
HASH2="$(cat distribution/out/BUILDHASH)"
echo "first: $HASH1"
echo "second: $HASH2"
if [ "$HASH1" != "$HASH2" ]; then
echo "::error::Build is not reproducible on this runner"
diff /tmp/manifest1 distribution/out/BUILDMANIFEST || true
exit 1
fi

- name: Publish release with artifact + fingerprint
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
run: |
HASH="$(cat distribution/out/BUILDHASH)"
cp distribution/out/BUILDHASH distribution/out/BUILDHASH.txt
gh release create "${GITHUB_REF_NAME}" \
--title "${GITHUB_REF_NAME}" \
--notes "BUILDHASH: \`${HASH}\`

Verify independently:
\`\`\`
git clone https://github.com/${GITHUB_REPOSITORY} && cd homebase-app
distribution/verify.sh ${GITHUB_REF_NAME} ${HASH}
\`\`\`" \
distribution/out/homebase-build.tar.gz \
distribution/out/BUILDHASH.txt \
distribution/out/BUILDMANIFEST
43 changes: 43 additions & 0 deletions distribution/Dockerfile.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Verifiable production build environment for Homebase.
#
# Pinned by DIGEST, not tag: a tag ("node:22.12.0") is a moving label the
# registry can repoint; the digest is the sha256 of the image itself and
# can never refer to different bytes. node 22.12.0 matches .nvmrc.
FROM node@sha256:35531c52ce27b6575d69755c73e65d4468dba93a25644eed56dc12879cae9213

WORKDIR /app

# CI=false: CRA escalates the repo's pre-existing lint warnings to errors
# under CI=true; lint policy is enforced elsewhere (lint-staged / lint:ci),
# not by the verifiable build.
ENV CI=false \
HUSKY=0

# Dependencies first (better layer caching for local iteration)
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --network-timeout 600000

COPY . .

# SOURCE_DATE_EPOCH: the commit timestamp of the source being built.
# Build tools that would otherwise stamp "now" into output use this
# instead, so wall-clock time stops being an input to the build.
ARG SOURCE_DATE_EPOCH
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}

# The canonical public env is the only .env a verifiable build may see.
RUN cp distribution/env.build .env

# Source maps embed builder-machine paths; the verifiable artifact omits
# them (transparency comes from reproducibility, not from maps).
ENV GENERATE_SOURCEMAP=false

RUN yarn build

# Deterministic artifact + hash, computed inside the pinned environment:
# - tar: sorted entries, fixed mtime, no owners - gzip -n: no timestamp
# - BUILDHASH: sha256 over the sorted per-file sha256s of build/
RUN cd /app && \
find build -type f -print0 | sort -z | xargs -0 sha256sum | tee /app/BUILDMANIFEST | sha256sum | cut -d' ' -f1 > /app/BUILDHASH && \
tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" --owner=0 --group=0 --numeric-owner -cf - build | gzip -n > /app/homebase-build.tar.gz && \
echo "BUILDHASH: $(cat /app/BUILDHASH)"
55 changes: 55 additions & 0 deletions distribution/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Verifiable builds

This directory makes Homebase releases **reproducible**: the same git ref
always produces a byte-identical bundle, on any machine. That turns "the
deployed app matches the public source" from a promise into a check.

**Why it matters here:** for a governance platform, control of the frontend
is a governance question — a tampered bundle can misrepresent what users
sign, without any trace in the source repository. Reproducibility is the
foundation for distributing Homebase through channels that don't require
trusting any single host or maintainer (content-addressed hosting, release
registries, independent build attestation). It runs **in parallel** with the
existing hosted deployment, which continues unchanged.

## How determinism is achieved

| Leak | Fix |
|---|---|
| Toolchain drift (node/yarn versions) | Builder image pinned by **digest** in `Dockerfile.build` (node 22.12.0, matching `.nvmrc`) |
| Machine paths in output | Build always runs at `/app` inside the container; source maps disabled for the artifact |
| Timestamps | `SOURCE_DATE_EPOCH` = the commit's timestamp; `gzip -n`; fixed tar mtimes |
| Uncommitted/untracked files | Docker context is `git archive <ref>` — only committed code can enter a build |
| Environment variance | `env.build` is the canonical, committed, public-only production env — the only `.env` a verifiable build sees |

## Usage

```bash
# Build the current commit; artifact + fingerprint land in distribution/out/
distribution/build.sh

# Verify a release against its published hash (from the GitHub Release page)
distribution/verify.sh v1.2.3 <expected-sha256>
```

`BUILDHASH` is the sha256 over the sorted per-file sha256s of `build/`
(`BUILDMANIFEST` holds the per-file list, so any mismatch can be localized
to the exact file).

## Release flow

Pushing a `v*` tag triggers `.github/workflows/reproducible-build.yml`,
which builds, **rebuilds from scratch and fails unless both hashes match**,
then publishes the artifact, `BUILDHASH.txt`, and the manifest on the
GitHub Release.

## Boundaries (honest ones)

- `env.build` must never contain a secret. A browser bundle cannot keep
secrets; anything here is public the moment the app ships.
- Reproducibility certifies the *bundle*, not the *services* it talks to —
the app still depends on the indexer/API endpoints listed in `env.build`.
- The hosted deployment (Netlify) builds separately and is not expected to
match `BUILDHASH`; verifiable distribution channels (content-addressed
mirrors) are the next phase and will serve exactly the fingerprinted
artifact.
49 changes: 49 additions & 0 deletions distribution/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Verifiable production build.
#
# Builds ONLY committed code: the Docker context is `git archive HEAD`, so
# uncommitted changes and untracked files cannot enter the artifact. The
# environment is fully pinned by distribution/Dockerfile.build.
#
# Usage: distribution/build.sh [--no-cache] [git-ref]
# git-ref defaults to HEAD. Output lands in distribution/out/:
# build/ the deployable bundle
# homebase-build.tar.gz deterministic tarball of build/
# BUILDHASH sha256 fingerprint of the bundle
# BUILDMANIFEST per-file sha256s (for pinpointing any mismatch)
set -euo pipefail

NO_CACHE=""
REF="HEAD"
for arg in "$@"; do
case "$arg" in
--no-cache) NO_CACHE="--no-cache" ;;
*) REF="$arg" ;;
esac
done

cd "$(git rev-parse --show-toplevel)"

# Commit timestamp of the ref being built — the only "time" the build sees.
SDE="$(git log -1 --format=%ct "$REF")"
COMMIT="$(git rev-parse "$REF")"

echo "Building $REF ($COMMIT, SOURCE_DATE_EPOCH=$SDE)"

git archive --format=tar "$REF" | docker build $NO_CACHE \
-f distribution/Dockerfile.build \
--build-arg SOURCE_DATE_EPOCH="$SDE" \
-t homebase-verifiable-build:latest \
-

cid="$(docker create homebase-verifiable-build:latest true)"
trap 'docker rm -f "$cid" >/dev/null' EXIT
rm -rf distribution/out
mkdir -p distribution/out
docker cp "$cid":/app/build distribution/out/build
docker cp "$cid":/app/homebase-build.tar.gz distribution/out/
docker cp "$cid":/app/BUILDHASH distribution/out/
docker cp "$cid":/app/BUILDMANIFEST distribution/out/

echo "commit: $COMMIT"
echo "BUILDHASH: $(cat distribution/out/BUILDHASH)"
21 changes: 21 additions & 0 deletions distribution/env.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Canonical PUBLIC build environment for verifiable production builds.
# Every value here is public by definition — it ships inside the browser
# bundle. Secrets must never appear in this file (a frontend bundle cannot
# keep a secret anyway).
#
# This file is the single source of truth for what a verifiable Homebase
# build talks to. Changing any value changes the build hash.
REACT_APP_ENV=PROD
REACT_APP_NETWORK=mainnet
REACT_APP_URL=https://tezos-homebase.io
REACT_APP_V2_URL=https://v2.tezos-homebase.io
REACT_APP_HASURA_URL=https://v3-homebase-indexer.tezos-homebase.io/v1/graphql
REACT_APP_HASURA_URL_V2=https://v2-homebase-indexer.w3api.dev/v1/graphql
REACT_APP_LITE_API_URL=https://homebase-backend.netlify.app
REACT_APP_DAO_DEPLOYER_API=https://homebase-dao-deployer.tezos-homebase.io
REACT_APP_BASEDAO_DOCKERISED_URL=https://v3-basedao-dockerised.herokuapp.com/steps
REACT_APP_CORS_PROXY_URL=https://dorg-cors-proxy.herokuapp.com
REACT_APP_POSTHOG_KEY=phc_o6mUWsxRDEbXyHLuyCfkAploVj0azEyV64DGlF7fvqd
REACT_APP_POSTHOG_HOST=https://us.i.posthog.com
REACT_APP_IS_NOT_TESTING=true
REACT_APP_IGNORE_DAO_CYCLE_CHECK=false
35 changes: 35 additions & 0 deletions distribution/verify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Independently verify a Homebase release.
#
# Rebuilds the given git ref from source in the pinned environment and
# compares the resulting fingerprint against an expected hash (e.g. the
# BUILDHASH published on the GitHub Release, or — in later phases — the
# hash recorded in the on-chain release registry).
#
# Usage: distribution/verify.sh <git-ref> <expected-sha256>
#
# Exit codes: 0 = MATCH (the source provably produces the published bundle)
# 1 = MISMATCH (investigate: toolchain drift or tampering)
set -euo pipefail

if [ $# -ne 2 ]; then
echo "usage: $0 <git-ref> <expected-sha256>" >&2
exit 2
fi
REF="$1"
EXPECTED="$2"

"$(dirname "$0")/build.sh" --no-cache "$REF"

ACTUAL="$(cat "$(git rev-parse --show-toplevel)/distribution/out/BUILDHASH")"

echo
echo "expected: $EXPECTED"
echo "actual: $ACTUAL"
if [ "$ACTUAL" = "$EXPECTED" ]; then
echo "MATCH — the published bundle is exactly this source."
else
echo "MISMATCH — do not trust the published bundle until explained." >&2
echo "Compare distribution/out/BUILDMANIFEST against the published one to locate differing files." >&2
exit 1
fi
Loading