From f6a3dd5855faefc814721a1a288cd559eb3b5db2 Mon Sep 17 00:00:00 2001 From: David Turnbull Date: Wed, 22 Oct 2025 13:21:18 +1100 Subject: [PATCH 1/5] =?UTF-8?q?build:=20enforce=20hermetic,=20reproducible?= =?UTF-8?q?=20local=E2=86=94CI=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use pinned Dockerfile.repro and containerized exec for all local/CI tasks - Deterministic packaging + checksums; update workflows, gitignore/prettierignore, and add changeset Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .changeset/reproducible-builds-parity.md | 10 ++ .github/workflows/lingodotdev.yml | 38 +++++--- .github/workflows/pr-check.yml | 51 ++++------ .github/workflows/release.yml | 62 ++++++------ .github/workflows/reproducible-build.yml | 43 +++++++++ .gitignore | 1 + .prettierignore | 1 + Dockerfile.repro | 26 +++++ package.json | 7 +- packages/cli/tsup.config.ts | 4 + packages/compiler/tsup.config.ts | 4 + packages/locales/tsup.config.ts | 4 + packages/sdk/tsup.config.ts | 4 + packages/spec/tsup.config.ts | 4 + readme/reproducible-builds.md | 117 +++++++++++++++++++++++ scripts/repro/build.sh | 71 ++++++++++++++ scripts/repro/exec.sh | 65 +++++++++++++ scripts/repro/local.sh | 30 ++++++ 18 files changed, 463 insertions(+), 79 deletions(-) create mode 100644 .changeset/reproducible-builds-parity.md create mode 100644 .github/workflows/reproducible-build.yml create mode 100644 Dockerfile.repro create mode 100644 readme/reproducible-builds.md create mode 100644 scripts/repro/build.sh create mode 100644 scripts/repro/exec.sh create mode 100644 scripts/repro/local.sh diff --git a/.changeset/reproducible-builds-parity.md b/.changeset/reproducible-builds-parity.md new file mode 100644 index 000000000..588eff47e --- /dev/null +++ b/.changeset/reproducible-builds-parity.md @@ -0,0 +1,10 @@ +--- +"lingo.dev": patch +"@lingo.dev/_compiler": patch +"@lingo.dev/_locales": patch +"@lingo.dev/_sdk": patch +"@lingo.dev/_spec": patch +"@lingo.dev/_react": patch +--- + +Adopt fully hermetic, reproducible builds with containerized local/CI parity; deterministic packaging and checksums; containerized PR/release workflows. diff --git a/.github/workflows/lingodotdev.yml b/.github/workflows/lingodotdev.yml index 7f8099a6e..109bbc1f3 100644 --- a/.github/workflows/lingodotdev.yml +++ b/.github/workflows/lingodotdev.yml @@ -45,21 +45,31 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version: "20" + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV - - name: Lingo.dev - uses: ./ + - name: Cache pnpm store + uses: actions/cache@v3 with: - api-key: ${{ secrets.LINGODOTDEV_API_KEY }} - version: ${{ inputs.version }} - pull-request: ${{ inputs['pull-request'] }} - commit-message: ${{ inputs['commit-message'] }} - pull-request-title: ${{ inputs['pull-request-title'] }} - working-directory: ${{ inputs['working-directory'] }} - process-own-commits: ${{ inputs['process-own-commits'] }} - parallel: ${{ inputs.parallel }} + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Lingo.dev (container) env: GH_TOKEN: ${{ github.token }} + LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }} + run: | + REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh \ + npx lingo.dev@${{ inputs.version }} ci \ + --api-key "$LINGODOTDEV_API_KEY" \ + --pull-request "${{ inputs['pull-request'] }}" \ + --commit-message "${{ inputs['commit-message'] }}" \ + --pull-request-title "${{ inputs['pull-request-title'] }}" \ + --working-directory "${{ inputs['working-directory'] }}" \ + --process-own-commits "${{ inputs['process-own-commits'] }}" \ + --parallel ${{ inputs.parallel }} diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index cd98f40b1..9e9fe42b1 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -30,47 +30,38 @@ jobs: exit 0 fi - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version: 20.12.2 + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . - - name: Install pnpm - uses: pnpm/action-setup@v4 - id: pnpm-install - with: - version: 9.12.3 - run_install: false + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV - - name: Configure pnpm cache - id: pnpm-cache - run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - name: Cache pnpm store + uses: actions/cache@v3 with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + path: ${{ env.REPRO_PNPM_STORE }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - - name: Install deps - run: pnpm install + - name: Mark repo as safe for git inside container + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh git config --global --add safe.directory /workspace - - name: Setup - run: | - pnpm turbo telemetry disable + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - name: Configure Turbo cache - uses: dtinth/setup-github-actions-caching-for-turbo@v1 + - name: Disable turbo telemetry (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable - - name: Check formatting - run: pnpm format:check + - name: Check formatting (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm format:check - - name: Build - run: pnpm turbo build --force + - name: Build (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force - - name: Test - run: pnpm turbo test --force + - name: Test (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test --force - - name: Require changeset to be present in PR + - name: Require changeset to be present in PR (container) if: github.event.pull_request.user.login != 'dependabot[bot]' - run: pnpm changeset status --since origin/main + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset status --since origin/main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17d09495a..31bfb035e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,61 +35,55 @@ jobs: exit 0 fi - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version: 20.12.2 + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . - - name: Install pnpm - uses: pnpm/action-setup@v4 - id: pnpm-install - with: - version: 9.12.3 - run_install: false + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV - - name: Configure pnpm cache - id: pnpm-cache - run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - name: Cache pnpm store + uses: actions/cache@v3 with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + path: ${{ env.REPRO_PNPM_STORE }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - - name: Install deps - run: pnpm install + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - name: Lingo.dev + - name: Lingo.dev (container) if: ${{ !inputs.skip_lingo }} - uses: ./ - with: - api-key: ${{ secrets.LINGODOTDEV_API_KEY }} - pull-request: true - parallel: true env: GH_TOKEN: ${{ github.token }} - - - name: Setup + LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }} run: | - pnpm turbo telemetry disable + REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh \ + npx lingo.dev@latest ci \ + --api-key "$LINGODOTDEV_API_KEY" \ + --pull-request "true" \ + --commit-message "feat: update translations via @LingoDotDev" \ + --pull-request-title "feat: update translations via @LingoDotDev" \ + --working-directory "." \ + --process-own-commits "false" \ + --parallel true - - name: Configure Turbo cache - uses: dtinth/setup-github-actions-caching-for-turbo@v1 + - name: Disable turbo telemetry (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable - - name: Build - run: pnpm turbo build --force + - name: Build (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force - - name: Test - run: pnpm turbo test --force + - name: Test (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test --force - name: Create Release Pull Request or Publish to npm id: changesets uses: changesets/action@v1 with: title: "chore: bump package versions" - version: pnpm changeset version - publish: pnpm changeset publish + version: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset version + publish: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset publish commit: "chore: bump package version" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reproducible-build.yml b/.github/workflows/reproducible-build.yml new file mode 100644 index 000000000..e5a4708e5 --- /dev/null +++ b/.github/workflows/reproducible-build.yml @@ -0,0 +1,43 @@ +name: Reproducible Build + +on: + workflow_dispatch: + pull_request: + branches: [main] + +jobs: + reproducible: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Run hermetic build + env: + GIT_COMMIT: ${{ github.sha }} + run: | + REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh bash scripts/repro/build.sh + + - name: Upload canonical artifact and checksums + uses: actions/upload-artifact@v4 + with: + name: reproducible-artifacts-${{ github.sha }} + path: out/* diff --git a/.gitignore b/.gitignore index ce8901476..a9191cbf2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules .pnp .pnp.js +.pnpm-store/ # Local env files .env diff --git a/.prettierignore b/.prettierignore index 857f16ab4..226393bff 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ pnpm-lock.yaml packages/cli/demo/ build/ dist/ +.pnpm-store/ .react-router/ .turbo/ .next/ diff --git a/Dockerfile.repro b/Dockerfile.repro new file mode 100644 index 000000000..da4114405 --- /dev/null +++ b/Dockerfile.repro @@ -0,0 +1,26 @@ +FROM node:20.12.2-bookworm-slim + +# Pinned, hermetic build environment for reproducible builds +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=UTC \ + LC_ALL=C \ + LANG=C \ + PNPM_HOME=/usr/local/pnpm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash git ca-certificates tzdata coreutils findutils gawk curl xz-utils \ + tar gzip bzip2 \ + && rm -rf /var/lib/apt/lists/* + +ENV PATH=$PNPM_HOME:$PATH + +# Pin pnpm via corepack +RUN corepack enable \ + && corepack prepare pnpm@9.12.3 --activate + +WORKDIR /workspace + +# Intentionally no COPY; repository will be bind-mounted at runtime + +ENTRYPOINT ["bash", "-lc"] diff --git a/package.json b/package.json index 13fe1c4c1..eacd5f14c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,12 @@ "new": "changeset", "new:empty": "changeset --empty", "format": "prettier . --write", - "format:check": "prettier . --check" + "format:check": "prettier . --check", + "repro:build": "bash scripts/repro/local.sh", + "repro:exec": "bash scripts/repro/exec.sh", + "repro:typecheck": "bash scripts/repro/exec.sh pnpm typecheck", + "repro:test": "bash scripts/repro/exec.sh pnpm test", + "repro:format:check": "bash scripts/repro/exec.sh pnpm format:check" }, "devDependencies": { "@babel/generator": "^7.27.1", diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index 7f00c50dd..4a7dcf482 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -20,6 +20,10 @@ export default defineConfig({ splitting: true, bundle: true, sourcemap: true, + esbuildOptions(options) { + options.legalComments = "none"; + options.absWorkingDir = process.cwd(); + }, external: ["readline/promises", "@babel/traverse", "node-machine-id"], outExtension: (ctx) => ({ js: ctx.format === "cjs" ? ".cjs" : ".mjs", diff --git a/packages/compiler/tsup.config.ts b/packages/compiler/tsup.config.ts index 24b199870..9b2506852 100644 --- a/packages/compiler/tsup.config.ts +++ b/packages/compiler/tsup.config.ts @@ -9,6 +9,10 @@ export default defineConfig({ dts: true, cjsInterop: true, splitting: true, + esbuildOptions(options) { + options.legalComments = "none"; + options.absWorkingDir = process.cwd(); + }, outExtension: (ctx) => ({ js: ctx.format === "cjs" ? ".cjs" : ".mjs", }), diff --git a/packages/locales/tsup.config.ts b/packages/locales/tsup.config.ts index c3b516243..72c63ae30 100644 --- a/packages/locales/tsup.config.ts +++ b/packages/locales/tsup.config.ts @@ -9,6 +9,10 @@ export default defineConfig({ dts: true, cjsInterop: true, splitting: false, + esbuildOptions(options) { + options.legalComments = "none"; + options.absWorkingDir = process.cwd(); + }, outExtension: (ctx) => ({ js: ctx.format === "cjs" ? ".cjs" : ".mjs", }), diff --git a/packages/sdk/tsup.config.ts b/packages/sdk/tsup.config.ts index 2d13ece73..c607f18b6 100644 --- a/packages/sdk/tsup.config.ts +++ b/packages/sdk/tsup.config.ts @@ -9,6 +9,10 @@ export default defineConfig({ dts: true, cjsInterop: true, splitting: true, + esbuildOptions(options) { + options.legalComments = "none"; + options.absWorkingDir = process.cwd(); + }, outExtension: (ctx) => ({ js: ctx.format === "cjs" ? ".cjs" : ".mjs", }), diff --git a/packages/spec/tsup.config.ts b/packages/spec/tsup.config.ts index 297ea8cb4..c8bb05d90 100644 --- a/packages/spec/tsup.config.ts +++ b/packages/spec/tsup.config.ts @@ -10,6 +10,10 @@ export default defineConfig({ dts: true, cjsInterop: true, splitting: true, + esbuildOptions(options) { + options.legalComments = "none"; + options.absWorkingDir = process.cwd(); + }, outExtension: (ctx) => ({ js: ctx.format === "cjs" ? ".cjs" : ".mjs", }), diff --git a/readme/reproducible-builds.md b/readme/reproducible-builds.md new file mode 100644 index 000000000..f03bc2247 --- /dev/null +++ b/readme/reproducible-builds.md @@ -0,0 +1,117 @@ +## Reproducible, Hermetic Builds (Full Local ↔ CI Parity) + +This guide explains how to run all dev and CI tasks inside the same pinned container and how to verify bit-for-bit identical artifacts. + +### What you get + +- One pinned container (`Dockerfile.repro`) used everywhere (local and CI) +- A parity runner (`scripts/repro/exec.sh`) for any command and a build entrypoint (`scripts/repro/build.sh`) +- Canonical packaging + checksums (`out/canonical.tar`, `out/SHA256SUMS`) and a manifest + +### Prerequisites + +- Docker installed and running + +--- + +## Local: run everything inside the container + +- Open a shell in the container at the repo root: + +```bash +pnpm repro:exec +``` + +- Common tasks (all run inside the container): + +```bash +pnpm repro:typecheck # pnpm typecheck in container +pnpm repro:test # pnpm test in container +pnpm repro:format:check # pnpm format:check in container +pnpm repro:exec -- pnpm turbo build --force +``` + +- Hermetic build that creates canonical artifacts: + +```bash +pnpm repro:build +``` + +Outputs: + +- `out/canonical.tar` — canonical, deterministically packaged bundle +- `out/SHA256SUMS` — SHA-256 checksum for verification +- `out/BUILD-MANIFEST.json` — metadata (commit, SDE, tool versions) + +How it works: + +1. Builds the pinned image (Node 20.12.2, pnpm 9.12.3, GNU tar, git) +2. Mounts your repo at `/workspace`, sets HOME/cache inside the container +3. Sets `SOURCE_DATE_EPOCH` from the commit time to stabilize timestamps +4. Installs deps with `pnpm install --frozen-lockfile` +5. Runs `pnpm turbo build` with deterministic env (no telemetry/daemon/remote cache) +6. Packages canonical artifacts with sorted names, fixed mtimes, numeric ownership + +Optional: speed up pnpm by caching the store locally across runs: + +```bash +export REPRO_PNPM_STORE="$HOME/.pnpm-store-lingo" +pnpm repro:exec -- pnpm install --frozen-lockfile +``` + +Common tokens are passed through automatically if set in your shell: `GH_TOKEN`, `GITHUB_TOKEN`, `NPM_TOKEN`, `LINGODOTDEV_API_KEY`. + +--- + +## CI: full parity with local + +All workflows now run inside the same container via `scripts/repro/exec.sh`: + +- PR checks: `.github/workflows/pr-check.yml` +- Release: `.github/workflows/release.yml` (including changesets version/publish) +- Lingo.dev: `.github/workflows/lingodotdev.yml` +- Reproducible build: `.github/workflows/reproducible-build.yml` + +CI also uses a cached pnpm store mounted into the container to keep builds fast. + +--- + +## Verify local vs CI artifacts + +1. Trigger or wait for the "Reproducible Build" workflow +2. Download the artifact bundle (`canonical.tar`, `SHA256SUMS`) +3. Compare with your local build: + +```bash +pnpm repro:build +cat out/SHA256SUMS +sha256sum out/canonical.tar # optional local recompute +``` + +The checksums must match for the same commit. + +--- + +## Notes on determinism + +- Time: `SOURCE_DATE_EPOCH` comes from the commit time +- Packaging: `gnu tar --sort=name --mtime=@$SOURCE_DATE_EPOCH --owner=0 --group=0 --numeric-owner` +- Dependencies: `pnpm install --frozen-lockfile` fails on drift +- Build tools: tsup/esbuild remove legal comments and normalize working directory + +--- + +## Updating pins + +1. Edit `Dockerfile.repro` (Node image or pnpm version via corepack) +2. Update image tag references if needed +3. Run `pnpm repro:build` locally; verify checksum stability for the same commit +4. Open a PR; CI will rebuild and upload artifacts for verification + +--- + +## Troubleshooting + +- Docker not running: Ensure your Docker daemon is running +- Lockfile errors: Only update dependencies intentionally, commit lockfile changes +- No build outputs: Ensure packages emit `packages/*/build`; the packager collects these directories diff --git a/scripts/repro/build.sh b/scripts/repro/build.sh new file mode 100644 index 000000000..9b768be0c --- /dev/null +++ b/scripts/repro/build.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +export TZ="UTC" +export LC_ALL="C" +export LANG="C" +export CI="${CI:-1}" + +# Ensure writable cache/home inside container regardless of UID:GID +export HOME="/tmp" +export XDG_CACHE_HOME="/tmp/.cache" +export COREPACK_HOME="/tmp/.corepack" + +GIT_COMMIT=${GIT_COMMIT:-"$(git rev-parse HEAD)"} +export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-"$(git log -1 --pretty=%ct "${GIT_COMMIT}")"} + +echo "Using commit: ${GIT_COMMIT}" +echo "SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH}" + +# Ensure pnpm is the pinned version via corepack +corepack enable >/dev/null 2>&1 || true +pnpm -v + +# Guardrails: lockfile must be respected +pnpm install --frozen-lockfile + +# Disable remote cache/telemetry for determinism +export TURBO_TELEMETRY_DISABLE=1 +export TURBO_REMOTE_CACHE_DISABLE=1 +export TURBO_NO_DAEMON=1 +export TURBO_CONCURRENCY=1 + +# Build +pnpm turbo build --force + +# Package canonical artifacts +mkdir -p out + +# Collect build directories +mapfile -t BUILD_DIRS < <(find packages -maxdepth 2 -type d -name build | sort) +if [ ${#BUILD_DIRS[@]} -eq 0 ]; then + echo "No build directories found under packages/*/build" >&2 + exit 1 +fi + +echo "Packaging canonical bundle from:" +printf ' - %s\n' "${BUILD_DIRS[@]}" + +CANONICAL_TAR="out/canonical.tar" + +tar --create \ + --file "${CANONICAL_TAR}" \ + --sort=name \ + --mtime=@"${SOURCE_DATE_EPOCH}" \ + --owner=0 --group=0 --numeric-owner \ + "${BUILD_DIRS[@]}" + +# Checksums and manifest +sha256sum "${CANONICAL_TAR}" > out/SHA256SUMS + +cat > out/BUILD-MANIFEST.json </dev/null 2>&1; then + echo "Building reproducible build image: ${IMAGE_TAG}" + docker build -f "${DOCKERFILE}" -t "${IMAGE_TAG}" . +fi + +UID_GID="$(id -u):$(id -g)" +GIT_COMMIT=${GIT_COMMIT:-"$(git rev-parse HEAD)"} + +if [ $# -eq 0 ]; then + echo "Opening interactive shell in container..." + DOCKER_ARGS=( + --rm -it + --user "${UID_GID}" + -e HOME="/tmp" + -e XDG_CACHE_HOME="/tmp/.cache" + -e COREPACK_HOME="/tmp/.corepack" + -e GIT_COMMIT="${GIT_COMMIT}" + -v "$(pwd)":/workspace + -w /workspace + --entrypoint bash + ) + # Pass through common tokens if present + [ -n "${GH_TOKEN:-}" ] && DOCKER_ARGS+=( -e GH_TOKEN ) + [ -n "${GITHUB_TOKEN:-}" ] && DOCKER_ARGS+=( -e GITHUB_TOKEN ) + [ -n "${NPM_TOKEN:-}" ] && DOCKER_ARGS+=( -e NPM_TOKEN ) + [ -n "${LINGODOTDEV_API_KEY:-}" ] && DOCKER_ARGS+=( -e LINGODOTDEV_API_KEY ) + if [ -n "${REPRO_PNPM_STORE:-}" ]; then + mkdir -p "${REPRO_PNPM_STORE}" + DOCKER_ARGS+=( -e PNPM_STORE_DIR=/pnpm-store -v "${REPRO_PNPM_STORE}:/pnpm-store" ) + fi + exec docker run "${DOCKER_ARGS[@]}" \ + "${IMAGE_TAG}" +else + CMD="$*" + echo "Running in container: ${CMD}" + DOCKER_ARGS=( + --rm + --user "${UID_GID}" + -e HOME="/tmp" + -e XDG_CACHE_HOME="/tmp/.cache" + -e COREPACK_HOME="/tmp/.corepack" + -e GIT_COMMIT="${GIT_COMMIT}" + -v "$(pwd)":/workspace + -w /workspace + --entrypoint bash + ) + # Pass through common tokens if present + [ -n "${GH_TOKEN:-}" ] && DOCKER_ARGS+=( -e GH_TOKEN ) + [ -n "${GITHUB_TOKEN:-}" ] && DOCKER_ARGS+=( -e GITHUB_TOKEN ) + [ -n "${NPM_TOKEN:-}" ] && DOCKER_ARGS+=( -e NPM_TOKEN ) + [ -n "${LINGODOTDEV_API_KEY:-}" ] && DOCKER_ARGS+=( -e LINGODOTDEV_API_KEY ) + if [ -n "${REPRO_PNPM_STORE:-}" ]; then + mkdir -p "${REPRO_PNPM_STORE}" + DOCKER_ARGS+=( -e PNPM_STORE_DIR=/pnpm-store -v "${REPRO_PNPM_STORE}:/pnpm-store" ) + fi + exec docker run "${DOCKER_ARGS[@]}" \ + "${IMAGE_TAG}" -lc "${CMD}" +fi diff --git a/scripts/repro/local.sh b/scripts/repro/local.sh new file mode 100644 index 000000000..70c6229e2 --- /dev/null +++ b/scripts/repro/local.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_TAG="lingo-repro:20.12.2" +DOCKERFILE="Dockerfile.repro" + +echo "Building reproducible build image: ${IMAGE_TAG}" +docker build -f "${DOCKERFILE}" -t "${IMAGE_TAG}" . + +GIT_COMMIT=$(git rev-parse HEAD) + +echo "Running hermetic build in container" +UID_GID="$(id -u):$(id -g)" +DOCKER_ARGS=( + --rm + --user "${UID_GID}" + -e GIT_COMMIT="${GIT_COMMIT}" + -v "$(pwd)":/workspace + -w /workspace + --entrypoint bash +) +if [ -n "${REPRO_PNPM_STORE:-}" ]; then + mkdir -p "${REPRO_PNPM_STORE}" + DOCKER_ARGS+=( -e PNPM_STORE_DIR=/pnpm-store -v "${REPRO_PNPM_STORE}:/pnpm-store" ) +fi +docker run "${DOCKER_ARGS[@]}" \ + "${IMAGE_TAG}" \ + -lc "scripts/repro/build.sh" + +echo "Done. Artifacts in ./out" From 83655889174fcb86c793d5e9b2881aa4ce8667b0 Mon Sep 17 00:00:00 2001 From: David Turnbull Date: Wed, 22 Oct 2025 13:29:18 +1100 Subject: [PATCH 2/5] chore: make container the default (drop repro: prefix) - Default scripts run inside Docker (build/typecheck/test/format) - Add shell, ci:install, build:canonical, host:* fallbacks - Update workflows to use default scripts; update README Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/pr-check.yml | 14 +++++++------- .github/workflows/release.yml | 8 ++++---- .github/workflows/reproducible-build.yml | 2 +- package.json | 24 ++++++++++++++---------- readme/reproducible-builds.md | 18 +++++++++--------- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 9e9fe42b1..7fcc155d4 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -45,23 +45,23 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Mark repo as safe for git inside container - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh git config --global --add safe.directory /workspace + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm shell git config --global --add safe.directory /workspace - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm ci:install - name: Disable turbo telemetry (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm turbo:telemetry:disable - name: Check formatting (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm format:check + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm format:check - name: Build (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm build -- --force - name: Test (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test --force + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm test - name: Require changeset to be present in PR (container) if: github.event.pull_request.user.login != 'dependabot[bot]' - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset status --since origin/main + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm shell pnpm changeset status --since origin/main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31bfb035e..f437211c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm ci:install - name: Lingo.dev (container) if: ${{ !inputs.skip_lingo }} @@ -69,13 +69,13 @@ jobs: --parallel true - name: Disable turbo telemetry (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm turbo:telemetry:disable - name: Build (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm build -- --force - name: Test (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test --force + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm test - name: Create Release Pull Request or Publish to npm id: changesets diff --git a/.github/workflows/reproducible-build.yml b/.github/workflows/reproducible-build.yml index e5a4708e5..071c57a10 100644 --- a/.github/workflows/reproducible-build.yml +++ b/.github/workflows/reproducible-build.yml @@ -34,7 +34,7 @@ jobs: env: GIT_COMMIT: ${{ github.sha }} run: | - REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh bash scripts/repro/build.sh + REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm build:canonical - name: Upload canonical artifact and checksums uses: actions/upload-artifact@v4 diff --git a/package.json b/package.json index eacd5f14c..8d1ec6afd 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,22 @@ "type": "module", "scripts": { "prepare": "husky", - "build": "turbo build", - "typecheck": "turbo typecheck", - "test": "turbo test", + "build": "bash scripts/repro/exec.sh pnpm turbo build", + "typecheck": "bash scripts/repro/exec.sh pnpm turbo typecheck", + "test": "bash scripts/repro/exec.sh pnpm turbo test", "new": "changeset", "new:empty": "changeset --empty", - "format": "prettier . --write", - "format:check": "prettier . --check", - "repro:build": "bash scripts/repro/local.sh", - "repro:exec": "bash scripts/repro/exec.sh", - "repro:typecheck": "bash scripts/repro/exec.sh pnpm typecheck", - "repro:test": "bash scripts/repro/exec.sh pnpm test", - "repro:format:check": "bash scripts/repro/exec.sh pnpm format:check" + "format": "bash scripts/repro/exec.sh pnpm prettier . --write", + "format:check": "bash scripts/repro/exec.sh pnpm prettier . --check", + "build:canonical": "bash scripts/repro/local.sh", + "shell": "bash scripts/repro/exec.sh", + "ci:install": "bash scripts/repro/exec.sh pnpm install --frozen-lockfile", + "turbo:telemetry:disable": "bash scripts/repro/exec.sh pnpm turbo telemetry disable", + "host:build": "turbo build", + "host:typecheck": "turbo typecheck", + "host:test": "turbo test", + "host:format": "prettier . --write", + "host:format:check": "prettier . --check" }, "devDependencies": { "@babel/generator": "^7.27.1", diff --git a/readme/reproducible-builds.md b/readme/reproducible-builds.md index f03bc2247..690f6715e 100644 --- a/readme/reproducible-builds.md +++ b/readme/reproducible-builds.md @@ -14,27 +14,27 @@ This guide explains how to run all dev and CI tasks inside the same pinned conta --- -## Local: run everything inside the container +## Local: run everything inside the container (default) - Open a shell in the container at the repo root: ```bash -pnpm repro:exec +pnpm run shell ``` - Common tasks (all run inside the container): ```bash -pnpm repro:typecheck # pnpm typecheck in container -pnpm repro:test # pnpm test in container -pnpm repro:format:check # pnpm format:check in container -pnpm repro:exec -- pnpm turbo build --force +pnpm typecheck # containerized typecheck +pnpm test # containerized tests +pnpm format:check # containerized formatting check +pnpm build -- --force # force rebuild inside container ``` - Hermetic build that creates canonical artifacts: ```bash -pnpm repro:build +pnpm build:canonical ``` Outputs: @@ -56,7 +56,7 @@ Optional: speed up pnpm by caching the store locally across runs: ```bash export REPRO_PNPM_STORE="$HOME/.pnpm-store-lingo" -pnpm repro:exec -- pnpm install --frozen-lockfile +pnpm ci:install ``` Common tokens are passed through automatically if set in your shell: `GH_TOKEN`, `GITHUB_TOKEN`, `NPM_TOKEN`, `LINGODOTDEV_API_KEY`. @@ -105,7 +105,7 @@ The checksums must match for the same commit. 1. Edit `Dockerfile.repro` (Node image or pnpm version via corepack) 2. Update image tag references if needed -3. Run `pnpm repro:build` locally; verify checksum stability for the same commit +3. Run `pnpm build:canonical` locally; verify checksum stability for the same commit 4. Open a PR; CI will rebuild and upload artifacts for verification --- From c5d7e74caf8faf8d27af473058e6d286a25dd76d Mon Sep 17 00:00:00 2001 From: David Turnbull Date: Wed, 22 Oct 2025 16:15:35 +1100 Subject: [PATCH 3/5] ci: fix GHA to not require host pnpm; run all steps via container exec - Replace pnpm host invocations with scripts/repro/exec.sh calls - Avoid recursive package scripts inside container (call turbo/prettier directly) --- .github/workflows/pr-check.yml | 14 +++++++------- .github/workflows/release.yml | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 7fcc155d4..3097eb5b7 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -45,23 +45,23 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Mark repo as safe for git inside container - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm shell git config --global --add safe.directory /workspace + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh git config --global --add safe.directory /workspace - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm ci:install + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - name: Disable turbo telemetry (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm turbo:telemetry:disable + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable - name: Check formatting (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm format:check + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm prettier . --check - name: Build (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm build -- --force + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force - name: Test (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm test + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test - name: Require changeset to be present in PR (container) if: github.event.pull_request.user.login != 'dependabot[bot]' - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm shell pnpm changeset status --since origin/main + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset status --since origin/main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f437211c9..51aa42f0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm ci:install + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - name: Lingo.dev (container) if: ${{ !inputs.skip_lingo }} @@ -69,13 +69,13 @@ jobs: --parallel true - name: Disable turbo telemetry (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm turbo:telemetry:disable + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable - name: Build (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm build -- --force + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force - name: Test (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm test + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test - name: Create Release Pull Request or Publish to npm id: changesets From 5fefe8c7e0a4fddacce11baeef3f9dd2562d6488 Mon Sep 17 00:00:00 2001 From: David Turnbull Date: Wed, 22 Oct 2025 16:20:35 +1100 Subject: [PATCH 4/5] ci: split PR checks into granular workflows Add workflows: format, typecheck, build, test, changeset. Remove reproducible-build workflow. Run steps inside container via scripts/repro/exec.sh. --- .../{reproducible-build.yml => ci-build.yml} | 19 ++++------ .../{pr-check.yml => ci-changeset.yml} | 33 ++-------------- .github/workflows/ci-format.yml | 38 +++++++++++++++++++ .github/workflows/ci-test.yml | 38 +++++++++++++++++++ .github/workflows/ci-typecheck.yml | 38 +++++++++++++++++++ 5 files changed, 125 insertions(+), 41 deletions(-) rename .github/workflows/{reproducible-build.yml => ci-build.yml} (64%) rename .github/workflows/{pr-check.yml => ci-changeset.yml} (55%) create mode 100644 .github/workflows/ci-format.yml create mode 100644 .github/workflows/ci-test.yml create mode 100644 .github/workflows/ci-typecheck.yml diff --git a/.github/workflows/reproducible-build.yml b/.github/workflows/ci-build.yml similarity index 64% rename from .github/workflows/reproducible-build.yml rename to .github/workflows/ci-build.yml index 071c57a10..a6672ff6b 100644 --- a/.github/workflows/reproducible-build.yml +++ b/.github/workflows/ci-build.yml @@ -1,12 +1,13 @@ -name: Reproducible Build +name: CI - Build on: workflow_dispatch: pull_request: + types: [opened, edited, synchronize] branches: [main] jobs: - reproducible: + build: runs-on: ubuntu-latest permissions: contents: read @@ -30,14 +31,8 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- - - name: Run hermetic build - env: - GIT_COMMIT: ${{ github.sha }} - run: | - REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" pnpm build:canonical + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - name: Upload canonical artifact and checksums - uses: actions/upload-artifact@v4 - with: - name: reproducible-artifacts-${{ github.sha }} - path: out/* + - name: Build (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force diff --git a/.github/workflows/pr-check.yml b/.github/workflows/ci-changeset.yml similarity index 55% rename from .github/workflows/pr-check.yml rename to .github/workflows/ci-changeset.yml index 3097eb5b7..07bed85c2 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/ci-changeset.yml @@ -1,17 +1,13 @@ -name: Check PR +name: CI - Changeset on: workflow_dispatch: pull_request: - types: - - opened - - edited - - synchronize - branches: - - main + types: [opened, edited, synchronize] + branches: [main] jobs: - check: + changeset: runs-on: ubuntu-latest permissions: contents: read @@ -19,17 +15,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{github.event.pull_request.head.sha}} fetch-depth: 0 - - name: Check for [skip i18n] - run: | - COMMIT_MESSAGE=$(git log -1 --pretty=%B) - if echo "$COMMIT_MESSAGE" | grep -iq '\[skip i18n\]'; then - echo "Skipping i18n checks due to [skip i18n] in commit message." - exit 0 - fi - - name: Build hermetic image run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . @@ -50,18 +37,6 @@ jobs: - name: Install deps (container) run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - name: Disable turbo telemetry (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable - - - name: Check formatting (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm prettier . --check - - - name: Build (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force - - - name: Test (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test - - name: Require changeset to be present in PR (container) if: github.event.pull_request.user.login != 'dependabot[bot]' run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset status --since origin/main diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml new file mode 100644 index 000000000..7e291bac1 --- /dev/null +++ b/.github/workflows/ci-format.yml @@ -0,0 +1,38 @@ +name: CI - Format + +on: + workflow_dispatch: + pull_request: + types: [opened, edited, synchronize] + branches: [main] + +jobs: + format: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + + - name: Check formatting (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm prettier . --check diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml new file mode 100644 index 000000000..912d19bd9 --- /dev/null +++ b/.github/workflows/ci-test.yml @@ -0,0 +1,38 @@ +name: CI - Test + +on: + workflow_dispatch: + pull_request: + types: [opened, edited, synchronize] + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + + - name: Test (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test diff --git a/.github/workflows/ci-typecheck.yml b/.github/workflows/ci-typecheck.yml new file mode 100644 index 000000000..d0ca70233 --- /dev/null +++ b/.github/workflows/ci-typecheck.yml @@ -0,0 +1,38 @@ +name: CI - Typecheck + +on: + workflow_dispatch: + pull_request: + types: [opened, edited, synchronize] + branches: [main] + +jobs: + typecheck: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + + - name: Typecheck (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo typecheck From f0f748830193d972a2e818042060e184388e26f3 Mon Sep 17 00:00:00 2001 From: David Turnbull Date: Wed, 22 Oct 2025 16:32:31 +1100 Subject: [PATCH 5/5] ci: consolidate into single workflow with parallel jobs Add ci.yml with format, typecheck, build, test, changeset jobs using containerized steps. --- .github/workflows/ci-build.yml | 38 ------- .github/workflows/ci-changeset.yml | 42 -------- .github/workflows/ci-format.yml | 38 ------- .github/workflows/ci-test.yml | 38 ------- .github/workflows/ci-typecheck.yml | 38 ------- .github/workflows/ci.yml | 162 +++++++++++++++++++++++++++++ 6 files changed, 162 insertions(+), 194 deletions(-) delete mode 100644 .github/workflows/ci-build.yml delete mode 100644 .github/workflows/ci-changeset.yml delete mode 100644 .github/workflows/ci-format.yml delete mode 100644 .github/workflows/ci-test.yml delete mode 100644 .github/workflows/ci-typecheck.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml deleted file mode 100644 index a6672ff6b..000000000 --- a/.github/workflows/ci-build.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: CI - Build - -on: - workflow_dispatch: - pull_request: - types: [opened, edited, synchronize] - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build hermetic image - run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . - - - name: Prepare pnpm store path - run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV - - - name: Cache pnpm store - uses: actions/cache@v3 - with: - path: ${{ env.REPRO_PNPM_STORE }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - - name: Build (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force diff --git a/.github/workflows/ci-changeset.yml b/.github/workflows/ci-changeset.yml deleted file mode 100644 index 07bed85c2..000000000 --- a/.github/workflows/ci-changeset.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: CI - Changeset - -on: - workflow_dispatch: - pull_request: - types: [opened, edited, synchronize] - branches: [main] - -jobs: - changeset: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build hermetic image - run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . - - - name: Prepare pnpm store path - run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV - - - name: Cache pnpm store - uses: actions/cache@v3 - with: - path: ${{ env.REPRO_PNPM_STORE }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Mark repo as safe for git inside container - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh git config --global --add safe.directory /workspace - - - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - - name: Require changeset to be present in PR (container) - if: github.event.pull_request.user.login != 'dependabot[bot]' - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset status --since origin/main diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml deleted file mode 100644 index 7e291bac1..000000000 --- a/.github/workflows/ci-format.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: CI - Format - -on: - workflow_dispatch: - pull_request: - types: [opened, edited, synchronize] - branches: [main] - -jobs: - format: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build hermetic image - run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . - - - name: Prepare pnpm store path - run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV - - - name: Cache pnpm store - uses: actions/cache@v3 - with: - path: ${{ env.REPRO_PNPM_STORE }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - - name: Check formatting (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm prettier . --check diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml deleted file mode 100644 index 912d19bd9..000000000 --- a/.github/workflows/ci-test.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: CI - Test - -on: - workflow_dispatch: - pull_request: - types: [opened, edited, synchronize] - branches: [main] - -jobs: - test: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build hermetic image - run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . - - - name: Prepare pnpm store path - run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV - - - name: Cache pnpm store - uses: actions/cache@v3 - with: - path: ${{ env.REPRO_PNPM_STORE }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - - name: Test (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test diff --git a/.github/workflows/ci-typecheck.yml b/.github/workflows/ci-typecheck.yml deleted file mode 100644 index d0ca70233..000000000 --- a/.github/workflows/ci-typecheck.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: CI - Typecheck - -on: - workflow_dispatch: - pull_request: - types: [opened, edited, synchronize] - branches: [main] - -jobs: - typecheck: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build hermetic image - run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . - - - name: Prepare pnpm store path - run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV - - - name: Cache pnpm store - uses: actions/cache@v3 - with: - path: ${{ env.REPRO_PNPM_STORE }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install deps (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile - - - name: Typecheck (container) - run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo typecheck diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..c92b9a52d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,162 @@ +name: CI + +on: + workflow_dispatch: + pull_request: + types: [opened, edited, synchronize] + branches: [main] + +jobs: + format: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + + - name: Check formatting (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm prettier . --check + + typecheck: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + + - name: Typecheck (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo typecheck + + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + + - name: Build (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force + + test: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + + - name: Test (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test + + changeset: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build hermetic image + run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 . + + - name: Prepare pnpm store path + run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ env.REPRO_PNPM_STORE }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Mark repo as safe for git inside container + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh git config --global --add safe.directory /workspace + + - name: Install deps (container) + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile + + - name: Require changeset to be present in PR (container) + if: github.event.pull_request.user.login != 'dependabot[bot]' + run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset status --since origin/main