From bee98913580fda69a55202775cfaa82708ad7831 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 20 May 2026 12:02:42 +0100 Subject: [PATCH 1/6] Add build plugin to check Node versions --- build.mjs | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/build.mjs b/build.mjs index a0c3c06189..2659c65f47 100644 --- a/build.mjs +++ b/build.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import * as esbuild from "esbuild"; import { globSync } from "glob"; +import * as yaml from "js-yaml"; import pkg from "./package.json" with { type: "json" }; @@ -27,6 +28,57 @@ const cleanPlugin = { }, }; +/** A plugin that checks that the Node versions in all `action.yml` files are the same. */ +const checkNodeVersionsPlugin = { + name: "check-node-versions", + setup(build) { + build.onStart(async () => { + // Find all the `action.yml` files. We don't care about the stub in the repository root, + // since that is a `composite` action. + const actionSpecifications = globSync("*/action.yml"); + + // Track the Node versions we find for each file. + const nodeVersions = {}; + + // We will store the first Node version we find and use it to compare against the others. + // If there's any disagreement, we set `versionMismatch` to `true` and throw an error + // that includes all the discovered Node versions at the end. + let nodeVersion = undefined; + let versionMismatch = false; + + for (const actionSpecification of actionSpecifications) { + // Read the contents of the action.yml file. + const contents = await readFile(actionSpecification, "utf-8"); + const specification = yaml.load(contents); + + // Find the `runs.using` value in the specification. + const using = specification.runs.using; + if (using === undefined) { + throw new Error( + `Couldn't find 'runs.using' in ${actionSpecification}`, + ); + } + + if (nodeVersion === undefined) { + // First one we found: set it as the baseline. + nodeVersion = using; + } else if (nodeVersion !== using) { + // Disagreement: set `versionMismatch` to indicate that we should throw an error later. + versionMismatch = true; + } + nodeVersions[actionSpecification] = using; + } + + // Throw an error if there was a version mismatch. + if (versionMismatch) { + throw new Error( + `More than one node version used in 'action.yml' files: ${JSON.stringify(nodeVersions)}`, + ); + } + }); + }, +}; + /** * Copy defaults.json to the output directory since other projects depend on it. * @@ -78,7 +130,7 @@ const UPLOAD_LIB_SRC = "./src/upload-lib"; * * The virtual module additionally re-exports `upload-lib` under the `uploadLib` namespace so that * external consumers can access it via the small `lib/upload-lib.js` stub emitted below. - * + * * A tiny stub file is emitted for each Action entrypoint, and one for `upload-lib`. Each stub * imports the shared bundle and calls/re-exports from the respective entry point. * @@ -208,7 +260,13 @@ const context = await esbuild.context({ outdir: OUT_DIR, platform: "node", external: ["./entry-points"], - plugins: [cleanPlugin, copyDefaultsPlugin, entryPointsPlugin, onEndPlugin], + plugins: [ + cleanPlugin, + checkNodeVersionsPlugin, + copyDefaultsPlugin, + entryPointsPlugin, + onEndPlugin, + ], target: ["node20"], define: { __CODEQL_ACTION_VERSION__: JSON.stringify(pkg.version), From eeaa56bdbbf5339dfa9131a4463d7c2a44f15222 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 21 May 2026 00:00:51 +0100 Subject: [PATCH 2/6] Add some extra validation for `runs.using` --- build.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.mjs b/build.mjs index 2659c65f47..dea430ee43 100644 --- a/build.mjs +++ b/build.mjs @@ -53,12 +53,18 @@ const checkNodeVersionsPlugin = { // Find the `runs.using` value in the specification. const using = specification.runs.using; - if (using === undefined) { + if (using === undefined || using === null) { throw new Error( `Couldn't find 'runs.using' in ${actionSpecification}`, ); } + if (typeof using !== "string" || !using.startsWith("node")) { + throw new Error( + `Expected 'runs.using' to be a string starting with 'node' in ${actionSpecification}`, + ); + } + if (nodeVersion === undefined) { // First one we found: set it as the baseline. nodeVersion = using; From 5fb81ae73eea362cf29324d24fe2028a71a858f9 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 21 May 2026 00:06:40 +0100 Subject: [PATCH 3/6] Add `.nvmrc` --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..a45fd52cc5 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 From dda23c79a86f54b579955c7a9e81d9be23870713 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 21 May 2026 00:09:15 +0100 Subject: [PATCH 4/6] Write `.nvmrc` file in `build.mjs` --- build.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.mjs b/build.mjs index dea430ee43..410ac3d521 100644 --- a/build.mjs +++ b/build.mjs @@ -81,6 +81,13 @@ const checkNodeVersionsPlugin = { `More than one node version used in 'action.yml' files: ${JSON.stringify(nodeVersions)}`, ); } + + // Write the node version to `.nvmrc`. + writeFile( + join(__dirname, ".nvmrc"), + nodeVersion.substring("node".length) + "\n", + "utf-8", + ); }); }, }; From f34cadbf2a34d3ee64b9e42be22eb0f01f5433ff Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 21 May 2026 00:13:03 +0100 Subject: [PATCH 5/6] Update `check-node-version` to compare `.nvmrc` --- .github/workflows/pr-checks.yml | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index f240997030..fdf0dcac11 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -105,10 +105,10 @@ jobs: run: npx tsx --test check-node-version: - if: github.triggering_actor != 'dependabot[bot]' - name: Check Action Node versions + if: github.triggering_actor != 'dependabot[bot]' && startsWith(github.head_ref, 'backport-') + name: Check Action Node versions for Backport runs-on: ubuntu-latest - timeout-minutes: 45 + timeout-minutes: 5 env: BASE_REF: ${{ github.base_ref }} @@ -116,31 +116,30 @@ jobs: contents: read steps: - - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@v6 + with: + depth: 1 + - id: head-version - name: Verify all Actions use the same Node version + name: Determine Node version for HEAD run: | - NODE_VERSION=$(find . -name "action.yml" -exec yq -e '.runs.using' {} \; | grep node | sort | uniq) + NODE_VERSION=$(cat .nvmrc) echo "NODE_VERSION: ${NODE_VERSION}" - if [[ $(echo "$NODE_VERSION" | wc -l) -gt 1 ]]; then - echo "::error::More than one node version used in 'action.yml' files." - exit 1 - fi echo "node_version=${NODE_VERSION}" >> $GITHUB_OUTPUT - id: checkout-base name: 'Backport: Check out base ref' - if: ${{ startsWith(github.head_ref, 'backport-') }} uses: actions/checkout@v6 with: ref: ${{ env.BASE_REF }} + depth: 1 - name: 'Backport: Verify Node versions unchanged' - if: steps.checkout-base.outcome == 'success' env: HEAD_VERSION: ${{ steps.head-version.outputs.node_version }} run: | - BASE_VERSION=$(find . -name "action.yml" -exec yq -e '.runs.using' {} \; | grep node | sort | uniq) + BASE_VERSION=$(cat .nvmrc) echo "HEAD_VERSION: ${HEAD_VERSION}" echo "BASE_VERSION: ${BASE_VERSION}" if [[ "$BASE_VERSION" != "$HEAD_VERSION" ]]; then From 95152f814af0510c59f3424c4585332467f8c610 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 21 May 2026 10:54:04 +0100 Subject: [PATCH 6/6] Address review --- .github/workflows/pr-checks.yml | 14 ++++++++++++-- build.mjs | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index fdf0dcac11..a742b26361 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -119,11 +119,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 with: - depth: 1 + fetch-depth: 1 - id: head-version name: Determine Node version for HEAD run: | + if [[ ! -f ".nvmrc" ]]; then + echo "::error::Cannot find .nvmrc in the HEAD commit." + exit 1 + fi + NODE_VERSION=$(cat .nvmrc) echo "NODE_VERSION: ${NODE_VERSION}" echo "node_version=${NODE_VERSION}" >> $GITHUB_OUTPUT @@ -133,12 +138,17 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ env.BASE_REF }} - depth: 1 + fetch-depth: 1 - name: 'Backport: Verify Node versions unchanged' env: HEAD_VERSION: ${{ steps.head-version.outputs.node_version }} run: | + if [[ ! -f ".nvmrc" ]]; then + echo "::error::Cannot find .nvmrc in the base commit." + exit 1 + fi + BASE_VERSION=$(cat .nvmrc) echo "HEAD_VERSION: ${HEAD_VERSION}" echo "BASE_VERSION: ${BASE_VERSION}" diff --git a/build.mjs b/build.mjs index 410ac3d521..e6a17ae5c3 100644 --- a/build.mjs +++ b/build.mjs @@ -83,7 +83,7 @@ const checkNodeVersionsPlugin = { } // Write the node version to `.nvmrc`. - writeFile( + await writeFile( join(__dirname, ".nvmrc"), nodeVersion.substring("node".length) + "\n", "utf-8",