diff --git a/.github/workflows/build-server-image.yml b/.github/workflows/build-server-image.yml index ed430a19d1e..58e9ca2f3dd 100644 --- a/.github/workflows/build-server-image.yml +++ b/.github/workflows/build-server-image.yml @@ -11,6 +11,7 @@ on: - server/build/buildenv-tag.sh - server/.go-version - .nvmrc + - mise.toml - .github/workflows/build-server-image.yml pull_request: paths: @@ -19,6 +20,7 @@ on: - server/build/buildenv-tag.sh - server/.go-version - .nvmrc + - mise.toml - .github/workflows/build-server-image.yml workflow_dispatch: diff --git a/.github/workflows/webapp-ci.yml b/.github/workflows/webapp-ci.yml index dd456e1ac1e..e006798ad7c 100644 --- a/.github/workflows/webapp-ci.yml +++ b/.github/workflows/webapp-ci.yml @@ -37,6 +37,38 @@ jobs: run: | npm run check + check-lockfile: + permissions: + contents: read + runs-on: ubuntu-24.04 + timeout-minutes: 10 + defaults: + run: + working-directory: webapp + steps: + - name: ci/checkout-repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: ci/setup-node + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version-file: ".nvmrc" + - name: ci/check-lockfile + run: | + # `npm ci` accepts a lockfile that npm 10 has stripped of its "peer" + # markers, so it can't catch this. Regenerating and diffing can. + npm install --package-lock-only --ignore-scripts --no-audit --no-fund + + if ! git diff --quiet -- package-lock.json; then + echo "::error file=webapp/package-lock.json::package-lock.json is not what the pinned npm generates" + echo "Regenerate it using the Node version in .nvmrc:" + echo " cd webapp && npm install" + echo + git --no-pager diff -- package-lock.json + exit 1 + fi + check-i18n: permissions: contents: read diff --git a/.gitignore b/.gitignore index 73d60d5e5a7..0f5f1103741 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ +# Tracked, not ignored: mise.toml pins the Node this repo builds with, and +# it is common to ignore mise.toml globally for personal use elsewhere. +!mise.toml + logs .DS_Store node_modules diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000000..3831f6c57cb --- /dev/null +++ b/mise.toml @@ -0,0 +1,15 @@ +# Toolchain for developers using mise. CI does not read this file -- it installs +# Node from .nvmrc -- so this exists to give a working tree the same Node that +# CI, the buildenv images and webapp/package.json "engines" all agree on. +# +# npm is deliberately absent: it ships with Node, and 24.11.1 carries exactly +# the npm 11.6.2 that "engines" requires. Pinning Node exactly is therefore what +# pins npm exactly, and it is the same npm every CI job gets. Naming npm here +# too would add a second version to hold in step, with nothing enforcing the +# pairing, so a stale entry could hand you an npm that ships with no Node. +# +# A Node bump moves this alongside .nvmrc, both server/build/Dockerfile.buildenv*, +# "engines"/"packageManager" and webapp/package-lock.json. buildenv-tag.sh fails +# when this and .nvmrc disagree. +[tools] +node = "24.11.1" diff --git a/server/build/buildenv-tag.sh b/server/build/buildenv-tag.sh index d56857a11a2..db5d3895a8a 100755 --- a/server/build/buildenv-tag.sh +++ b/server/build/buildenv-tag.sh @@ -5,7 +5,8 @@ # # The tag comes from the Dockerfiles, since those are what actually determine # the image, and they are checked against .go-version and .nvmrc so the pins -# cannot drift apart unnoticed. +# cannot drift apart unnoticed. mise.toml pins Node for developers rather than +# for the image, but it is checked here too, for the same reason. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/../.." @@ -26,6 +27,11 @@ satisfies() { go_version=$(cat server/.go-version) node_spec=$(cat .nvmrc) +mise_version=$(awk -F'"' '$1 ~ /^node[[:space:]]*=[[:space:]]*$/ { print $2; exit }' mise.toml) +[[ -n "${mise_version}" ]] || fail "mise.toml: no node pin" +satisfies "${mise_version}" "${node_spec}" || + fail "mise.toml: node is ${mise_version}, but .nvmrc says ${node_spec}" + node_version="" for dockerfile in "${DOCKERFILES[@]}"; do from=$(awk '$1 == "FROM" { print $2; exit }' "${dockerfile}") diff --git a/webapp/.npmrc b/webapp/.npmrc index cffe8cdef13..3d853ee35d8 100644 --- a/webapp/.npmrc +++ b/webapp/.npmrc @@ -1 +1,6 @@ save-exact=true + +# Refuse to run under a toolchain that doesn't match "engines" in package.json. +# Without this, npm only warns (EBADENGINE) and then rewrites package-lock.json +# using the wrong npm's semantics -- npm 10 silently strips "peer" markers. +engine-strict=true diff --git a/webapp/package-lock.json b/webapp/package-lock.json index d0d1790e7be..e3f651cadb4 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -50,8 +50,8 @@ "webpack-dev-server": "5.2.2" }, "engines": { - "node": "^24", - "npm": "^11" + "node": "24.11.1", + "npm": "11.6.2" } }, "channels": { diff --git a/webapp/package.json b/webapp/package.json index 1dc481491f1..13909f50b11 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -2,9 +2,10 @@ "name": "@mattermost/webapp", "private": true, "engines": { - "node": "^24", - "npm": "^11" + "node": "24.11.1", + "npm": "11.6.2" }, + "packageManager": "npm@11.6.2", "scripts": { "postinstall": "patch-package && npm run build --workspace platform/types --workspace platform/client --workspace platform/shared --workspace platform/components", "build": "node scripts/build.mjs",