Skip to content

Commit f6a3dd5

Browse files
build: enforce hermetic, reproducible local↔CI parity
- 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>
1 parent 44a928b commit f6a3dd5

File tree

18 files changed

+463
-79
lines changed

18 files changed

+463
-79
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"lingo.dev": patch
3+
"@lingo.dev/_compiler": patch
4+
"@lingo.dev/_locales": patch
5+
"@lingo.dev/_sdk": patch
6+
"@lingo.dev/_spec": patch
7+
"@lingo.dev/_react": patch
8+
---
9+
10+
Adopt fully hermetic, reproducible builds with containerized local/CI parity; deterministic packaging and checksums; containerized PR/release workflows.

.github/workflows/lingodotdev.yml

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,31 @@ jobs:
4545
- name: Checkout
4646
uses: actions/checkout@v4
4747

48-
- name: Use Node.js
49-
uses: actions/setup-node@v2
50-
with:
51-
node-version: "20"
48+
- name: Build hermetic image
49+
run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 .
50+
51+
- name: Prepare pnpm store path
52+
run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV
5253

53-
- name: Lingo.dev
54-
uses: ./
54+
- name: Cache pnpm store
55+
uses: actions/cache@v3
5556
with:
56-
api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
57-
version: ${{ inputs.version }}
58-
pull-request: ${{ inputs['pull-request'] }}
59-
commit-message: ${{ inputs['commit-message'] }}
60-
pull-request-title: ${{ inputs['pull-request-title'] }}
61-
working-directory: ${{ inputs['working-directory'] }}
62-
process-own-commits: ${{ inputs['process-own-commits'] }}
63-
parallel: ${{ inputs.parallel }}
57+
path: ${{ env.REPRO_PNPM_STORE }}
58+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
59+
restore-keys: |
60+
${{ runner.os }}-pnpm-store-
61+
62+
- name: Lingo.dev (container)
6463
env:
6564
GH_TOKEN: ${{ github.token }}
65+
LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }}
66+
run: |
67+
REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh \
68+
npx lingo.dev@${{ inputs.version }} ci \
69+
--api-key "$LINGODOTDEV_API_KEY" \
70+
--pull-request "${{ inputs['pull-request'] }}" \
71+
--commit-message "${{ inputs['commit-message'] }}" \
72+
--pull-request-title "${{ inputs['pull-request-title'] }}" \
73+
--working-directory "${{ inputs['working-directory'] }}" \
74+
--process-own-commits "${{ inputs['process-own-commits'] }}" \
75+
--parallel ${{ inputs.parallel }}

.github/workflows/pr-check.yml

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,47 +30,38 @@ jobs:
3030
exit 0
3131
fi
3232
33-
- name: Use Node.js
34-
uses: actions/setup-node@v2
35-
with:
36-
node-version: 20.12.2
33+
- name: Build hermetic image
34+
run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 .
3735

38-
- name: Install pnpm
39-
uses: pnpm/action-setup@v4
40-
id: pnpm-install
41-
with:
42-
version: 9.12.3
43-
run_install: false
36+
- name: Prepare pnpm store path
37+
run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV
4438

45-
- name: Configure pnpm cache
46-
id: pnpm-cache
47-
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
48-
- uses: actions/cache@v3
39+
- name: Cache pnpm store
40+
uses: actions/cache@v3
4941
with:
50-
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
42+
path: ${{ env.REPRO_PNPM_STORE }}
5143
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
5244
restore-keys: |
5345
${{ runner.os }}-pnpm-store-
5446
55-
- name: Install deps
56-
run: pnpm install
47+
- name: Mark repo as safe for git inside container
48+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh git config --global --add safe.directory /workspace
5749

58-
- name: Setup
59-
run: |
60-
pnpm turbo telemetry disable
50+
- name: Install deps (container)
51+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile
6152

62-
- name: Configure Turbo cache
63-
uses: dtinth/setup-github-actions-caching-for-turbo@v1
53+
- name: Disable turbo telemetry (container)
54+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable
6455

65-
- name: Check formatting
66-
run: pnpm format:check
56+
- name: Check formatting (container)
57+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm format:check
6758

68-
- name: Build
69-
run: pnpm turbo build --force
59+
- name: Build (container)
60+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force
7061

71-
- name: Test
72-
run: pnpm turbo test --force
62+
- name: Test (container)
63+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test --force
7364

74-
- name: Require changeset to be present in PR
65+
- name: Require changeset to be present in PR (container)
7566
if: github.event.pull_request.user.login != 'dependabot[bot]'
76-
run: pnpm changeset status --since origin/main
67+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset status --since origin/main

.github/workflows/release.yml

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,61 +35,55 @@ jobs:
3535
exit 0
3636
fi
3737
38-
- name: Use Node.js
39-
uses: actions/setup-node@v2
40-
with:
41-
node-version: 20.12.2
38+
- name: Build hermetic image
39+
run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 .
4240

43-
- name: Install pnpm
44-
uses: pnpm/action-setup@v4
45-
id: pnpm-install
46-
with:
47-
version: 9.12.3
48-
run_install: false
41+
- name: Prepare pnpm store path
42+
run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV
4943

50-
- name: Configure pnpm cache
51-
id: pnpm-cache
52-
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
53-
- uses: actions/cache@v3
44+
- name: Cache pnpm store
45+
uses: actions/cache@v3
5446
with:
55-
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
47+
path: ${{ env.REPRO_PNPM_STORE }}
5648
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
5749
restore-keys: |
5850
${{ runner.os }}-pnpm-store-
5951
60-
- name: Install deps
61-
run: pnpm install
52+
- name: Install deps (container)
53+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm install --frozen-lockfile
6254

63-
- name: Lingo.dev
55+
- name: Lingo.dev (container)
6456
if: ${{ !inputs.skip_lingo }}
65-
uses: ./
66-
with:
67-
api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
68-
pull-request: true
69-
parallel: true
7057
env:
7158
GH_TOKEN: ${{ github.token }}
72-
73-
- name: Setup
59+
LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }}
7460
run: |
75-
pnpm turbo telemetry disable
61+
REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh \
62+
npx lingo.dev@latest ci \
63+
--api-key "$LINGODOTDEV_API_KEY" \
64+
--pull-request "true" \
65+
--commit-message "feat: update translations via @LingoDotDev" \
66+
--pull-request-title "feat: update translations via @LingoDotDev" \
67+
--working-directory "." \
68+
--process-own-commits "false" \
69+
--parallel true
7670
77-
- name: Configure Turbo cache
78-
uses: dtinth/setup-github-actions-caching-for-turbo@v1
71+
- name: Disable turbo telemetry (container)
72+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo telemetry disable
7973

80-
- name: Build
81-
run: pnpm turbo build --force
74+
- name: Build (container)
75+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo build --force
8276

83-
- name: Test
84-
run: pnpm turbo test --force
77+
- name: Test (container)
78+
run: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm turbo test --force
8579

8680
- name: Create Release Pull Request or Publish to npm
8781
id: changesets
8882
uses: changesets/action@v1
8983
with:
9084
title: "chore: bump package versions"
91-
version: pnpm changeset version
92-
publish: pnpm changeset publish
85+
version: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset version
86+
publish: REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh pnpm changeset publish
9387
commit: "chore: bump package version"
9488
env:
9589
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Reproducible Build
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
branches: [main]
7+
8+
jobs:
9+
reproducible:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Build hermetic image
20+
run: docker build -f Dockerfile.repro -t lingo-repro:20.12.2 .
21+
22+
- name: Prepare pnpm store path
23+
run: echo "REPRO_PNPM_STORE=${{ runner.temp }}/pnpm-store" >> $GITHUB_ENV
24+
25+
- name: Cache pnpm store
26+
uses: actions/cache@v3
27+
with:
28+
path: ${{ env.REPRO_PNPM_STORE }}
29+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
30+
restore-keys: |
31+
${{ runner.os }}-pnpm-store-
32+
33+
- name: Run hermetic build
34+
env:
35+
GIT_COMMIT: ${{ github.sha }}
36+
run: |
37+
REPRO_PNPM_STORE="${{ env.REPRO_PNPM_STORE }}" bash scripts/repro/exec.sh bash scripts/repro/build.sh
38+
39+
- name: Upload canonical artifact and checksums
40+
uses: actions/upload-artifact@v4
41+
with:
42+
name: reproducible-artifacts-${{ github.sha }}
43+
path: out/*

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
node_modules
55
.pnp
66
.pnp.js
7+
.pnpm-store/
78

89
# Local env files
910
.env

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pnpm-lock.yaml
33
packages/cli/demo/
44
build/
55
dist/
6+
.pnpm-store/
67
.react-router/
78
.turbo/
89
.next/

Dockerfile.repro

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
FROM node:20.12.2-bookworm-slim
2+
3+
# Pinned, hermetic build environment for reproducible builds
4+
ENV DEBIAN_FRONTEND=noninteractive \
5+
TZ=UTC \
6+
LC_ALL=C \
7+
LANG=C \
8+
PNPM_HOME=/usr/local/pnpm
9+
10+
RUN apt-get update \
11+
&& apt-get install -y --no-install-recommends \
12+
bash git ca-certificates tzdata coreutils findutils gawk curl xz-utils \
13+
tar gzip bzip2 \
14+
&& rm -rf /var/lib/apt/lists/*
15+
16+
ENV PATH=$PNPM_HOME:$PATH
17+
18+
# Pin pnpm via corepack
19+
RUN corepack enable \
20+
&& corepack prepare pnpm@9.12.3 --activate
21+
22+
WORKDIR /workspace
23+
24+
# Intentionally no COPY; repository will be bind-mounted at runtime
25+
26+
ENTRYPOINT ["bash", "-lc"]

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
"new": "changeset",
1010
"new:empty": "changeset --empty",
1111
"format": "prettier . --write",
12-
"format:check": "prettier . --check"
12+
"format:check": "prettier . --check",
13+
"repro:build": "bash scripts/repro/local.sh",
14+
"repro:exec": "bash scripts/repro/exec.sh",
15+
"repro:typecheck": "bash scripts/repro/exec.sh pnpm typecheck",
16+
"repro:test": "bash scripts/repro/exec.sh pnpm test",
17+
"repro:format:check": "bash scripts/repro/exec.sh pnpm format:check"
1318
},
1419
"devDependencies": {
1520
"@babel/generator": "^7.27.1",

packages/cli/tsup.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export default defineConfig({
2020
splitting: true,
2121
bundle: true,
2222
sourcemap: true,
23+
esbuildOptions(options) {
24+
options.legalComments = "none";
25+
options.absWorkingDir = process.cwd();
26+
},
2327
external: ["readline/promises", "@babel/traverse", "node-machine-id"],
2428
outExtension: (ctx) => ({
2529
js: ctx.format === "cjs" ? ".cjs" : ".mjs",

0 commit comments

Comments
 (0)