From 099393d6d8589c522c33279bc742bdebe3206396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Costa?= Date: Sat, 17 Oct 2026 09:51:43 -0300 Subject: [PATCH 1/2] Pin @swc/core to 1.16.2 so the app boots read-only next.config.ts loads next-intl's plugin, which requires @swc/core at load time. From 1.16.12, @swc/core's native addon unpacks itself into a cache directory on first load (~/.cache, then /tmp) and refuses to load unless that directory is writable and executable. A container with a read-only root filesystem and a noexec /tmp has neither, so `next start` throws ERR_SWC_NATIVE_CACHE while loading the config; the web process exits and the start script takes the api and worker down with it. 1.16.2 is the last release that loads its addon in place, and the one the lockfile resolved before. It is inside next-intl 4.14.9's declared range (~1.16.0). Pointing SWC_NATIVE_BINDING_CACHE at an executable tmpfs would also work, but every deployment's compose file would need that mount; the pin fixes the image itself. Co-Authored-By: Claude Opus 5.5 --- pnpm-lock.yaml | 105 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 5 +++ 2 files changed, 58 insertions(+), 52 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b5cb9aa3..397169e25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: overrides: esbuild@<0.25.0: '>=0.25.0' deepmerge-ts@<8.0.0: '>=8.0.0' + '@swc/core': 1.16.2 importers: @@ -3332,86 +3333,86 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/core-darwin-arm64@1.16.18': - resolution: {integrity: sha512-ozgiCHyFDCwfXzTnaLR+i8WxEyt06GTudn9rPh08vj+u3Z8vMvt5Pi8T+BikyNXm7y++h6UMdqsnJuWrV+tk2A==} + '@swc/core-darwin-arm64@1.16.2': + resolution: {integrity: sha512-i/j0HNbnn79qnTVPicvay92Nark8fW8NQqn1e2mGERjUXNpBV0+SwQxlRpk2zBhn6laJ8PDI6Kn1nHZhnz3LCA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.16.18': - resolution: {integrity: sha512-tuwxXi+sjxxTAi7GP3NeQXQxOf4SXSiFxKAMYLBdoy7JqGYby6uSDXojcFoLOhSp7oiNGstnaFXxfbP0Hpro9w==} + '@swc/core-darwin-x64@1.16.2': + resolution: {integrity: sha512-HrwqHyEyHVXO3qTk8EkNK7/b6sOZSEoNh+pot6RdE5x0LbNqfo8LtJUvi3UTXr+5ja/o5HbJdW80eCXo+NjbiA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.16.18': - resolution: {integrity: sha512-SMdhmY1P9Kveh0MRj6vqQF98bsjnDa0lBBvkd/lcpxmjj5QHJOQAU0N3g6h7GPWpBMs3h2g3lHu/QTDn+uMsXg==} + '@swc/core-linux-arm-gnueabihf@1.16.2': + resolution: {integrity: sha512-MdXi83Z/gGp1LIrg+h7HKxiul/z/Bty/ZJSvYAFqDl9zteC1XLSAZdScquKtXPp50rdyXqritTDCqQBhwVfZKA==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.16.18': - resolution: {integrity: sha512-ep/mQyhblQYZ6tkfq0IJspcVSFl5jdywks3kejanrbB31uuujRD/QWOoGNp2L3lLRdAdQvgl/M3pA7g3hSuQug==} + '@swc/core-linux-arm64-gnu@1.16.2': + resolution: {integrity: sha512-/jcTmK6Ktz3owM3YtiKvjofV6p3VpHnYzTIrOGwDIOsDigRAAVuZ8east33wYO/7UTdKYFlyHNnJNT0WJqOA3Q==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [glibc] - '@swc/core-linux-arm64-musl@1.16.18': - resolution: {integrity: sha512-pkzGVxrAQu5+febW1a/Aiph3YJk0UiJKXoWbxnDVEIOZWkk5aETHTuyVsOQ3kOgn2dzNMvJVeqcoEQdH+ntqWQ==} + '@swc/core-linux-arm64-musl@1.16.2': + resolution: {integrity: sha512-4gFarKaFnlJTSlJYKmMhV4u+3YE4uYfiydpBoYjmgQhCf9lAieOq+WilZaK9vVSHeqLuQpTEiGULZqAdsRX5Dw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [musl] - '@swc/core-linux-ppc64-gnu@1.16.18': - resolution: {integrity: sha512-v9kiu4as1pJyQ8ESFTtMNydZ99m86QXLfAtm+XiKPKS93pXL80BCaXokAs9ebioGkIXgmwfbzUCdUC62H3JWkw==} + '@swc/core-linux-ppc64-gnu@1.16.2': + resolution: {integrity: sha512-syqSLGd6KlZ1PciNzs6bIUlhOuFztZufebOHaERjc4N4SqNZxyqYd4I+jj/EfOYnpe0kNjccn9HJLN1p5dz3+w==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] libc: [glibc] - '@swc/core-linux-s390x-gnu@1.16.18': - resolution: {integrity: sha512-vU7TKRJ26i86YQr4QY/5bdcH72KzkBbSSUfPlErW6d6KOR47BcVGlYZFTcMMYlmd3CVSXFHW18tBSila66YfoA==} + '@swc/core-linux-s390x-gnu@1.16.2': + resolution: {integrity: sha512-ZBBLK+ewGyXLzWeMS7wbKtWBdnif6etn7xvPY/iOfbdsjX/+bgkp1pQt2lWF2wlu2hXYZuhJ/tHZE/QR8/apzg==} engines: {node: '>=10'} cpu: [s390x] os: [linux] libc: [glibc] - '@swc/core-linux-x64-gnu@1.16.18': - resolution: {integrity: sha512-Ml6kbe0BoVJkSgmYPAEUudGcu84jaQeqDN8xeMumbwNpT0vlmm7241Ddp9XKCjQABvjomnXF0OiNOAfjBhNDNw==} + '@swc/core-linux-x64-gnu@1.16.2': + resolution: {integrity: sha512-LyHJgxCA4Tje0ysBMbEb0tt/ie8kgUKoFE3JAKFhpevmTmhYEoC0H9s47WuDsqiFckF1ITUguZIXJG6K5e0dvg==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [glibc] - '@swc/core-linux-x64-musl@1.16.18': - resolution: {integrity: sha512-TGU24+QmmS8y/hg0BLKgOU3W5TCcQC9bgGl8AB0R61gTNQdWdcmCehRLAJwlZfxtAml66xMJ6PWrX8NKrABOsA==} + '@swc/core-linux-x64-musl@1.16.2': + resolution: {integrity: sha512-PghXJlVM1cgtLfNUR1vxFo1z+PDRAe8cWAJlZZ7spmeiN7BospGXg/MHUg7oNSgwSX7Zo//YKv9P5yD9apsFJQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [musl] - '@swc/core-win32-arm64-msvc@1.16.18': - resolution: {integrity: sha512-S53hm7Amc3YV8t2BUT6R7J+zSnwiGarG+GnxMca4W0uMkxBZ3Gl6jBwEgb6YqrYjf2DzOuRfauztqptCO76a/w==} + '@swc/core-win32-arm64-msvc@1.16.2': + resolution: {integrity: sha512-StTOSefYBxemvNYYUI3UmO1a8y+hSPjjfHogC2TEHL+Z1PlEBim/XtLas5rS04jAzT9RrNmbtX911SZ42H9jSQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.16.18': - resolution: {integrity: sha512-+6/10+OJmU12zWkrPVG0QeQEr8VsWeF8gyXrOfHZMIfMOfz8/ULXVZPhV9GhGMKSWake1xEJj2IYCiFRS/xMpQ==} + '@swc/core-win32-ia32-msvc@1.16.2': + resolution: {integrity: sha512-fycER209DYIzsibpTMC+chND05OfOjgztWL9U8OE6/uUlsOUZH3eh98isBLEnOymYUhlJLEt5++W1+KL/FOh5Q==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.16.18': - resolution: {integrity: sha512-7LsVrIICAZWSajXT6DwO7PYSTnW9maamOCyQylaOh13Viptrgi/7hl9fP75SiHe+kscaJahGXcorgt4MOOh/1w==} + '@swc/core-win32-x64-msvc@1.16.2': + resolution: {integrity: sha512-cSd1z6ivSrJPVr+moVwOHWjeKy6TpO4/Shwcv5KCrKYXCccxwh4pRy1C3fDioNx2PF1jPZWHKZjtXt+Be9VbaQ==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.16.18': - resolution: {integrity: sha512-0cYQLkpcwUngg55TDwpwrrVOU+B6zh1NFbp+oS7dVFhmYkzaARelwugo/bzPJh1FrcnppmF+KTqioCGwyqbcEw==} + '@swc/core@1.16.2': + resolution: {integrity: sha512-95I4kiSMeveI/Mhi+tE4fiWcWLUMfzfKrk0jtr8LRMqHgOgq+xHS+zExkDqoO4b5OeeuXHMWVdD5MeP3X6sULw==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -8834,59 +8835,59 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/core-darwin-arm64@1.16.18': + '@swc/core-darwin-arm64@1.16.2': optional: true - '@swc/core-darwin-x64@1.16.18': + '@swc/core-darwin-x64@1.16.2': optional: true - '@swc/core-linux-arm-gnueabihf@1.16.18': + '@swc/core-linux-arm-gnueabihf@1.16.2': optional: true - '@swc/core-linux-arm64-gnu@1.16.18': + '@swc/core-linux-arm64-gnu@1.16.2': optional: true - '@swc/core-linux-arm64-musl@1.16.18': + '@swc/core-linux-arm64-musl@1.16.2': optional: true - '@swc/core-linux-ppc64-gnu@1.16.18': + '@swc/core-linux-ppc64-gnu@1.16.2': optional: true - '@swc/core-linux-s390x-gnu@1.16.18': + '@swc/core-linux-s390x-gnu@1.16.2': optional: true - '@swc/core-linux-x64-gnu@1.16.18': + '@swc/core-linux-x64-gnu@1.16.2': optional: true - '@swc/core-linux-x64-musl@1.16.18': + '@swc/core-linux-x64-musl@1.16.2': optional: true - '@swc/core-win32-arm64-msvc@1.16.18': + '@swc/core-win32-arm64-msvc@1.16.2': optional: true - '@swc/core-win32-ia32-msvc@1.16.18': + '@swc/core-win32-ia32-msvc@1.16.2': optional: true - '@swc/core-win32-x64-msvc@1.16.18': + '@swc/core-win32-x64-msvc@1.16.2': optional: true - '@swc/core@1.16.18(@swc/helpers@0.5.23)': + '@swc/core@1.16.2(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.28 optionalDependencies: - '@swc/core-darwin-arm64': 1.16.18 - '@swc/core-darwin-x64': 1.16.18 - '@swc/core-linux-arm-gnueabihf': 1.16.18 - '@swc/core-linux-arm64-gnu': 1.16.18 - '@swc/core-linux-arm64-musl': 1.16.18 - '@swc/core-linux-ppc64-gnu': 1.16.18 - '@swc/core-linux-s390x-gnu': 1.16.18 - '@swc/core-linux-x64-gnu': 1.16.18 - '@swc/core-linux-x64-musl': 1.16.18 - '@swc/core-win32-arm64-msvc': 1.16.18 - '@swc/core-win32-ia32-msvc': 1.16.18 - '@swc/core-win32-x64-msvc': 1.16.18 + '@swc/core-darwin-arm64': 1.16.2 + '@swc/core-darwin-x64': 1.16.2 + '@swc/core-linux-arm-gnueabihf': 1.16.2 + '@swc/core-linux-arm64-gnu': 1.16.2 + '@swc/core-linux-arm64-musl': 1.16.2 + '@swc/core-linux-ppc64-gnu': 1.16.2 + '@swc/core-linux-s390x-gnu': 1.16.2 + '@swc/core-linux-x64-gnu': 1.16.2 + '@swc/core-linux-x64-musl': 1.16.2 + '@swc/core-win32-arm64-msvc': 1.16.2 + '@swc/core-win32-ia32-msvc': 1.16.2 + '@swc/core-win32-x64-msvc': 1.16.2 '@swc/helpers': 0.5.23 '@swc/counter@0.1.3': {} @@ -10874,7 +10875,7 @@ snapshots: '@eloqnt/format-po': 0.1.1 '@formatjs/intl-localematcher': 0.8.14 '@parcel/watcher': 2.6.0 - '@swc/core': 1.16.18(@swc/helpers@0.5.23) + '@swc/core': 1.16.2(@swc/helpers@0.5.23) icu-minify: 4.14.9 negotiator: 1.1.0 next: 16.4.1(@opentelemetry/api@1.9.1)(@types/node@22.20.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 21b70ee80..13d42d8c4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,3 +14,8 @@ overrides: "esbuild@<0.25.0": ">=0.25.0" # Stack-exhaustion advisory in <8 (GHSA via mailparser→html-to-text). "deepmerge-ts@<8.0.0": ">=8.0.0" + # From 1.16.12, @swc/core unpacks its native addon into a cache directory + # on first load and refuses to load without a writable, executable one, + # which a read-only container with a noexec /tmp lacks. next.config.ts + # loads it through next-intl's plugin, so `next start` would die at boot. + "@swc/core": "1.16.2" From 13af7fe87b1d6123216e45dcfc208fe288093cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Costa?= Date: Sat, 17 Oct 2026 09:56:17 -0300 Subject: [PATCH 2/2] Boot the app image read-only in pull request CI The image job only built the image, so nothing ran it the way the compose files do: read-only root filesystem, non-root user, and Docker's default tmpfs mounts, which are noexec. A dependency that needs a writable or executable path outside those mounts passed CI and failed only once deployed, as @swc/core does from 1.16.12. The job now loads the image and starts it with the compose services' runtime flags, the same tmpfs paths, a throwaway Postgres and the four variables a boot needs (APP_BASE_URL included: auth refuses to start without it in production mode). It passes once the dashboard's /login and the API's /health both answer, and fails with the container's logs if the container stops or two minutes pass first. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10b0d1f7f..c252453a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,24 +40,54 @@ jobs: # `next build` (web, and docs, whose MDX compiles nowhere else) and the # Dockerfiles would otherwise first run after merge, in `build`. amd64 only, - # nothing pushed. Reads main's layer cache but never writes one: PR-scoped - # entries would crowd main's out of the repository's cache quota. + # nothing pushed; the app image is loaded locally to boot it. Reads main's + # layer cache but never writes one: PR-scoped entries would crowd main's out + # of the repository's cache quota. image: if: github.event_name == 'pull_request' runs-on: ubuntu-latest + services: + postgres: + image: postgres:17-alpine@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73 + env: + POSTGRES_USER: millionsend + POSTGRES_PASSWORD: millionsend + ports: ["5432:5432"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . - outputs: type=cacheonly + load: true + tags: millionsend:ci cache-from: type=gha,scope=app-amd64 - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: scripts/backup outputs: type=cacheonly cache-from: type=gha,scope=backup-amd64 + # Runs the image as the compose files do: read-only rootfs, non-root, + # and Docker's default tmpfs (noexec) at the same paths. A dependency + # that writes or executes anywhere else fails here instead of on deploy. + - name: Image boots read-only + run: | + docker run -d --name app --network host --init --read-only --user millionsend \ + --cap-drop ALL --security-opt no-new-privileges \ + --tmpfs /tmp --tmpfs /app/apps/web/.next/cache \ + --tmpfs /app/apps/docs/.next/cache --tmpfs /app/apps/docs/.source \ + -e DATABASE_URL=postgres://millionsend:millionsend@localhost:5432/millionsend \ + -e MASTER_ENCRYPTION_KEY="$(openssl rand -base64 32)" \ + -e BETTER_AUTH_SECRET="$(openssl rand -base64 32)" \ + -e APP_BASE_URL=http://localhost:3000 \ + millionsend:ci + for _ in $(seq 60); do + curl -fs -o /dev/null localhost:3000/login && curl -fs -o /dev/null localhost:3001/health && exit 0 + [ "$(docker inspect -f '{{.State.Running}}' app)" = true ] || break + sleep 2 + done + docker logs app + exit 1 # Whole-history secret scan with the pinned gitleaks image (the GitHub # action needs a license key for organization repos; the CLI does not).