diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 076b35465..7605620fe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,9 @@ version: 2 updates: - # Only the `FROM` tag in Dockerfile is visible here. ENV NODE_VERSION and the - # corepack pin are not, and stay manual. Daily because a new Amazon Linux 2023 - # snapshot is a single ungrouped dependency, so a run without one is a no-op. + # Covers both `FROM` lines in Dockerfile. Daily because a new Amazon Linux + # 2023 snapshot is a single ungrouped dependency, so a run without one is a + # no-op. A Vite+ image bump should land with the matching vite-plus catalog + # bump, since the image's vp and the project's vite-plus are the same release. # # No `cooldown`: it needs a registry-assigned publication date, and Docker Hub's # tag_last_pushed is currently the only accepted source. ECR Public has none, so @@ -48,7 +49,7 @@ updates: - dependency-name: "@babel/core" update-types: ["version-update:semver-major"] # @types/node tracks the Node major it describes, and we target Node LTS - # (24, pinned in Dockerfile, .node-version, and the engines fields). A major bump + # (24, pinned in .node-version and the engines fields). A major bump # here would type against APIs the runtime does not have. Raise it by hand # when we move to the next LTS. - dependency-name: "@types/node" diff --git a/.github/workflows/test_build_docker.yml b/.github/workflows/test_build_docker.yml index 0bf28ba46..4ea74a9e7 100644 --- a/.github/workflows/test_build_docker.yml +++ b/.github/workflows/test_build_docker.yml @@ -41,6 +41,13 @@ jobs: run: | docker run --rm --entrypoint="" test-image openssl --version + - name: Ensure Node.js matches .node-version + run: | + expected="v$(cat .node-version)" + actual=$(docker run --rm --entrypoint="" test-image node --version) + [ "$actual" = "$expected" ] || { echo "FAIL: image has $actual, .node-version is $expected"; exit 1; } + echo "✓ node $actual" + - name: Verify build tooling is absent run: | docker run --rm --entrypoint="" test-image sh -c ' diff --git a/Dockerfile b/Dockerfile index 6e715bc17..18c3b2aec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,44 +1,36 @@ # syntax=docker/dockerfile:1 -# Pinned to a dated ECR Public snapshot for reproducible builds; bump to move forward. -# The tag also pins dnf repo metadata to that date, so the yum update below resolves -# against that snapshot and cannot reach anything published after it. -FROM public.ecr.aws/amazonlinux/amazonlinux:2023.12.20261013.0 AS os -ENV NODE_VERSION=24.21.0 - -# Node.js and pnpm for the build stages. The runtime copies only the node binary. -FROM os AS toolchain -RUN yum update -y && \ - yum install -y tar xz && \ - ARCH=$(uname -m) && \ - if [ "$ARCH" = "x86_64" ]; then NODE_ARCH="x64"; \ - elif [ "$ARCH" = "aarch64" ]; then NODE_ARCH="arm64"; \ - else echo "Unsupported architecture: $ARCH" && exit 1; fi && \ - curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz | tar -xJ -C /usr/local --strip-components=1 && \ - npm install --global corepack@0.36.0 && \ - corepack enable -WORKDIR /graph-explorer +# The Vite+ image builds with the Node.js in .node-version and the pnpm in +# packageManager. Pinned by digest; keep the tag on the vite-plus catalog version. +FROM ghcr.io/voidzero-dev/vite-plus:1.1.0@sha256:15dedad6ab1f100c096aaebac6b0fb40ccf1f3dd0dd0169141e2a56588489b60 AS toolchain +WORKDIR /app FROM toolchain AS build -# The image has no git, so skip installing the Vite+ commit hooks. +# The image has no repository to hook into, so skip the Vite+ commit hooks. ENV VP_GIT_HOOKS=0 -COPY . . -RUN pnpm install --frozen-lockfile && \ - pnpm build && \ - pnpm clean:dep && \ - chmod a+x ./process-environment.sh ./setup-ssl.sh ./docker-entrypoint.sh +COPY --chown=vp:vp . . +RUN vp install --frozen-lockfile && \ + vp run build && \ + rm -rf node_modules packages/*/node_modules && \ + chmod a+x ./process-environment.sh ./setup-ssl.sh ./docker-entrypoint.sh && \ + NODE_HOME="$(dirname "$(dirname "$(vp env which node | head -1)")")" && \ + mkdir /tmp/node && \ + cp "$NODE_HOME/bin/node" "$NODE_HOME/LICENSE" /tmp/node/ # Installed from the manifests alone, so the layer is reused until a # dependency changes and never holds devDependencies. FROM toolchain AS deps -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ -COPY packages/graph-explorer/package.json packages/graph-explorer/ -COPY packages/graph-explorer-proxy-server/package.json packages/graph-explorer-proxy-server/ -COPY packages/shared/package.json packages/shared/ -RUN pnpm install --prod --frozen-lockfile --ignore-scripts +COPY --chown=vp:vp package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc .node-version ./ +COPY --chown=vp:vp packages/graph-explorer/package.json packages/graph-explorer/ +COPY --chown=vp:vp packages/graph-explorer-proxy-server/package.json packages/graph-explorer-proxy-server/ +COPY --chown=vp:vp packages/shared/package.json packages/shared/ +RUN vp install --prod --frozen-lockfile --ignore-scripts +# Pinned to a dated ECR Public snapshot for reproducible builds; bump to move forward. +# The tag also pins dnf repo metadata to that date, so the yum update below resolves +# against that snapshot and cannot reach anything published after it. # openssl is for setup-ssl.sh. yum, dnf, and python3 come with the base image -# and are stripped; npm, pnpm, and corepack never reach this stage. -FROM os AS runtime +# and are stripped. +FROM public.ecr.aws/amazonlinux/amazonlinux:2023.12.20261013.0 AS runtime RUN yum update -y && \ yum install -y openssl && \ yum clean all && \ @@ -46,13 +38,13 @@ RUN yum update -y && \ rpm -qa 'dnf*' 'yum*' 'libdnf*' | while read pkg; do rpm -e --nodeps "$pkg"; done && \ rm -rf /var/cache/yum /var/cache/dnf # LICENSE carries the notices for the third-party code built into node. -COPY --from=toolchain /usr/local/bin/node /usr/local/bin/node -COPY --from=toolchain /usr/local/LICENSE /usr/local/LICENSE +COPY --from=build --chown=0:0 /tmp/node/node /usr/local/bin/node +COPY --from=build --chown=0:0 /tmp/node/LICENSE /usr/local/LICENSE ENV HOME=/graph-explorer WORKDIR /graph-explorer -COPY --from=deps /graph-explorer ./ -COPY --from=build /graph-explorer ./ +COPY --from=deps --chown=0:0 /app ./ +COPY --from=build --chown=0:0 /app ./ EXPOSE 443 EXPOSE 80 diff --git a/docs/development.md b/docs/development.md index 7e1b13dfe..f5266287c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -9,7 +9,7 @@ Build instructions and development setup for contributing to Graph Explorer. For ### Node Version -`.node-version` pins the Node.js version used for development and CI. Use [Vite+](https://viteplus.dev), [fnm](https://github.com/Schniz/fnm), or [nvm](https://github.com/nvm-sh/nvm) to switch to it: +`.node-version` pins the Node.js version used for development, CI, and the Docker image. Use [Vite+](https://viteplus.dev), [fnm](https://github.com/Schniz/fnm), or [nvm](https://github.com/nvm-sh/nvm) to switch to it: ```bash vp env use # Vite+ @@ -46,14 +46,8 @@ Three files carry the pnpm version itself: - `pnpm-lock.yaml`, which records the version under `packageManagerDependencies` plus an `integrity` for `pnpm` and for each `@pnpm/exe.*` platform package. `corepack use` regenerates this; never hand-edit it. Editing `packageManager` by hand still runs the version you typed, and rewrites these entries to match it, even under `--frozen-lockfile`. So the lockfile does not go stale, but the change lands in a commit nobody reviewed as a lockfile change. - The pnpm requirement at the top of this document. -One more pins a _different tool_ whose version is tied to the pnpm major, so it stays put on a patch or minor bump and moves only when the major changes: - -- `corepack@` in the `Dockerfile`. The container gets pnpm only through `corepack enable`, and pnpm 12 ships as a native executable, so a new pnpm major needs a Corepack release that can fetch and verify that binary. A Corepack release's notes name the pnpm majors it handles. - Run `corepack use pnpm@` from the repo root. It rewrites `packageManager` with a freshly computed integrity hash and then runs `pnpm install`, which updates the lockfile. Do not hand-write the hash. If `corepack` is not on your `PATH`, install it with `npm install -g corepack@`; Node 24 bundles Corepack but only behind `corepack enable`, and later Node lines drop it entirely. -On a major bump, raise the `Dockerfile`'s Corepack pin _before_ running `corepack use`, because the Corepack currently pinned is by definition the one that predates the new pnpm major. Nothing in the commands below builds the image, so a Corepack pin too old to fetch the new pnpm surfaces only when `test_build_docker.yml` runs on the pull request. Run `docker build .` locally if you would rather find out sooner. - Do not reach for `pnpm self-update`. It rewrites `packageManager` without the `+sha512` hash, throwing the integrity pin away. `corepack use` is the only command that writes a correct pin. Update this document to the same version, then confirm nothing shifted: @@ -64,7 +58,7 @@ pnpm checks pnpm test ``` -`.github/workflows/ci.yml` gets pnpm through Vite+ with [`voidzero-dev/setup-vp`](https://github.com/voidzero-dev/setup-vp), which reads the version from `packageManager`, so a pnpm bump needs no workflow edit. On a major bump, check the "Set up Vite+" step on the pull request: if the pinned Vite+ cannot run the new pnpm major, upgrade Vite+ first. +`.github/workflows/ci.yml` and the `Dockerfile` both get pnpm through Vite+, which reads the version from `packageManager`, so a pnpm bump needs no workflow or `Dockerfile` edit. CI uses [`voidzero-dev/setup-vp`](https://github.com/voidzero-dev/setup-vp), and the image builds in the Vite+ image. On a major bump, check the "Set up Vite+" step and the Docker build on the pull request: if the pinned Vite+ cannot run the new pnpm major, upgrade Vite+ first. No check in this repo compares the `+sha512` hash against anything, because every tool that installs pnpm already verifies the bytes it fetches and fails closed. Corepack and Vite+ both read the hash and refuse a `pnpm` package that does not match it. pnpm ignores the hash and instead checks each download against the `integrity` in `pnpm-lock.yaml` and against npm's registry signature, so a tampered lockfile stops the install with "its npm registry signature could not be verified" rather than passing quietly.