Skip to content

Commit e82730c

Browse files
authored
ci: upgrade action that decides what node version to use (#369)
Co-authored-by: Lukasz Gornicki <lpgornicki@gmail.com>
1 parent 932d8e2 commit e82730c

6 files changed

Lines changed: 195 additions & 10 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Overview
2+
3+
This action determines which Node.js version to use in your workflow. It's designed for use in centralized workflows where users need to control Node.js versions from their repository without modifying the workflow.
4+
5+
Maintainers can specify Node.js version via repository variable or `package.json` and `engines.node` value.
6+
7+
## How It Works
8+
9+
```mermaid
10+
flowchart TD
11+
Start([Action Starts]) --> CheckEnv{NODE_VERSION<br/>env variable set in repo settings?}
12+
CheckEnv -->|Yes| UseEnv[Use NODE_VERSION value]
13+
CheckEnv -->|No| CheckPackageJson{package.json<br/>exists in root?}
14+
15+
CheckPackageJson -->|Yes| CheckEngines{engines.node<br/>field exists?}
16+
CheckPackageJson -->|No| UseLock[Check package-lock.json]
17+
18+
CheckEngines -->|Yes| ParseSemver[Parse semver range<br/>with semver package]
19+
CheckEngines -->|No| UseLock
20+
21+
ParseSemver --> FilterLTS[Filter to LTS versions<br/>14, 16, 18, 20, 22, 24...]
22+
FilterLTS --> FindFirst[Find first LTS version<br/>satisfying range]
23+
FindFirst --> FoundLTS{Version found?}
24+
25+
FoundLTS -->|Yes| UseEngines[Use engines.node version]
26+
FoundLTS -->|No| UseLock
27+
28+
UseLock --> CheckLockVersion{package-lock.json<br/>lockfileVersion}
29+
CheckLockVersion -->|1| Use14[Use Node 14]
30+
CheckLockVersion -->|2| Use16[Use Node 16]
31+
CheckLockVersion -->|3| Use18[Use Node 18]
32+
CheckLockVersion -->|default| UseDefault[Use Node 16]
33+
34+
UseEnv --> Output([Output version])
35+
UseEngines --> Output
36+
Use14 --> Output
37+
Use16 --> Output
38+
Use18 --> Output
39+
UseDefault --> Output
40+
```
41+
42+
The action checks for Node.js version in this order:
43+
44+
1. **`node-version` input** (typically from `NODE_VERSION` repository variable) (highest priority)
45+
2. **`engines.node` field in `package.json`**
46+
3. **`package-lock.json` version** (fallback for backward compatibility)
47+
48+
## Usage
49+
50+
```yaml
51+
- name: Get Node version
52+
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master
53+
with:
54+
node-version: ${{ vars.NODE_VERSION }}
55+
id: nodeversion
56+
57+
- name: Setup Node.js
58+
uses: actions/setup-node@v4
59+
with:
60+
node-version: ${{ steps.nodeversion.outputs.version }}
61+
```
62+
63+
## How to Control Node.js Version
64+
65+
Users have two options to specify their desired Node.js version:
66+
67+
### Option 1: Repository Variable (Easiest)
68+
69+
Set a `NODE_VERSION` repository variable in GitHub:
70+
1. Go to repository **Settings → Secrets and variables → Actions → Variables tab**
71+
2. Click **New repository variable**
72+
3. Name: `NODE_VERSION`
73+
4. Value: `20` or `22` or any Node.js major version
74+
5. Click **Add variable**
75+
76+
### Option 2: package.json engines field
77+
78+
Add the `engines.node` field to `package.json` with a semver range:
79+
80+
```json
81+
{
82+
"name": "my-project",
83+
"version": "1.0.0",
84+
"engines": {
85+
"node": ">=20.0.0"
86+
}
87+
}
88+
```
89+
90+
The action uses the **semver** package to properly parse complex version ranges and automatically selects the **highest even-numbered (LTS) major version** that satisfies the range:
91+
92+
- `>=20.0.0` → `20`
93+
- `>=18.0.0 <22` → `18`
94+
- `>=18.10.3 <22` → `18`
95+
- `20.x` → `20`
96+
97+
**Note:** The action only considers LTS versions (even major numbers: 14, 16, 18, 20, 22, 24, etc.) and returns the major version number. The `actions/setup-node` action will automatically select the appropriate patch version.
98+
99+
## Outputs
100+
101+
| Output | Description |
102+
|--------|-------------|
103+
| `version` | Node.js version number to use with `actions/setup-node` |

.github/actions/get-node-version-from-package-lock/action.yml

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,85 @@
11
name: 'Specify Node version based on package-lock file version'
2-
description: 'This action reads package-lock.json file and determines Node.js version to use with actions/setup-node action'
2+
description: 'This action reads package-lock.json file and determines Node.js version to use with actions/setup-node action. Can be overridden via NODE_VERSION repository variable or engines.node in package.json'
3+
inputs:
4+
node-version:
5+
description: 'Node.js version override (e.g. from vars.NODE_VERSION)'
6+
required: false
37
outputs:
48
version:
59
description: 'Node.js version number to use with actions/setup-node action'
610
value: ${{ steps.getnode.outputs.version }}
711
runs:
812
using: "composite"
913
steps:
14+
- name: Install semver package
15+
shell: bash
16+
run: npm install --no-save semver@7.7.3
17+
1018
- name: Get Node version
1119
uses: actions/github-script@v7
1220
id: getnode
21+
env:
22+
# Map the composite action input to an environment variable for the script
23+
OVERRIDE_NODE_VERSION: ${{ inputs.node-version }}
1324
with:
1425
script: |
1526
const { resolve } = require('path');
27+
const { access, constants } = require('fs').promises;
28+
const semver = require('semver');
29+
30+
// Priority 1: Check for input override (e.g. vars.NODE_VERSION passed from caller)
31+
const envNodeVersion = process.env.OVERRIDE_NODE_VERSION;
32+
if (envNodeVersion) {
33+
core.info(`Using Node.js version from input override: ${envNodeVersion}`);
34+
core.setOutput('version', envNodeVersion);
35+
return;
36+
}
37+
38+
// Priority 2: Check for engines.node in package.json
39+
const packageJsonLocation = './package.json';
40+
try {
41+
//this is properly catched if the file does not exist
42+
await access(packageJsonLocation, constants.F_OK);
43+
const packageJson = require(resolve(packageJsonLocation));
44+
if (packageJson.engines?.node) {
45+
const enginesNode = packageJson.engines.node;
46+
core.info(`Found engines.node in package.json: ${enginesNode}`);
47+
48+
// Use semver to determine the best matching Node.js version
49+
// Check against LTS (even-numbered) major versions
50+
const ltsMajorVersions = [14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34];
51+
52+
// Find the first even major version that satisfies the range
53+
let selectedVersion = null;
54+
for (const major of ltsMajorVersions) {
55+
// Check if the major version range intersects with enginesNode
56+
// Use a high patch version to ensure we're checking the full major version range
57+
// Works well even with ranges like ">=16.10.3 <18" and picks 16
58+
if (semver.satisfies(`${major}.999.999`, enginesNode)) {
59+
selectedVersion = major;
60+
break;
61+
}
62+
}
63+
64+
if (selectedVersion) {
65+
const nodeVersion = selectedVersion.toString();
66+
core.info(`Using Node.js version ${nodeVersion} (satisfies ${enginesNode})`);
67+
core.setOutput('version', nodeVersion);
68+
return;
69+
} else {
70+
core.warning(`No even-numbered Node.js version satisfies range: ${enginesNode}. Falling back to package-lock.json detection.`);
71+
}
72+
}
73+
} catch (error) {
74+
// package.json doesn't exist or can't be read, continue to fallback
75+
if (error.code !== 'ENOENT') {
76+
core.warning(`Failed to read package.json engines field: ${error.message}`);
77+
}
78+
}
79+
80+
// Priority 3: Fallback to package-lock.json logic (backward compatibility)
1681
const packageLockLocation = './package-lock.json';
17-
core.info(`Location of the packae-lock verification script is: ${ resolve(packageLockLocation) }`)
82+
core.info(`Location of the package-lock verification script is: ${ resolve(packageLockLocation) }`)
1883
const packageLock = require(packageLockLocation);
1984
2085
const packageLockVersion = packageLock.lockfileVersion;
@@ -34,5 +99,6 @@ runs:
3499
nodeVersion = '16'
35100
break;
36101
}
37-
102+
103+
core.info(`Using Node.js version based on package-lock.json: ${nodeVersion}`);
38104
core.setOutput('version', nodeVersion);

.github/workflows/if-nodejs-pr-testing.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ jobs:
4949
run: test -e ./package.json && echo "exists=true" >> $GITHUB_OUTPUT || echo "exists=false" >> $GITHUB_OUTPUT
5050
shell: bash
5151
- if: steps.packagejson.outputs.exists == 'true'
52-
name: Check package-lock version
52+
name: Determine what node version to use
5353
# This workflow is from our own org repo and safe to reference by 'master'.
5454
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master # //NOSONAR
55+
with:
56+
node-version: ${{ vars.NODE_VERSION }}
5557
id: lockversion
5658
- if: steps.packagejson.outputs.exists == 'true'
5759
name: Setup Node.js

.github/workflows/if-nodejs-release.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ on:
1717
- alpha
1818
- next
1919

20+
permissions:
21+
contents: write # to be able to publish a GitHub release
22+
issues: write # to be able to comment on released issues
23+
pull-requests: write # to be able to comment on released pull requests
24+
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
25+
2026
jobs:
2127

2228
test-nodejs:
@@ -47,9 +53,11 @@ jobs:
4753
run: test -e ./package.json && echo "exists=true" >> $GITHUB_OUTPUT || echo "exists=false" >> $GITHUB_OUTPUT
4854
shell: bash
4955
- if: steps.packagejson.outputs.exists == 'true'
50-
name: Check package-lock version
56+
name: Determine what node version to use
5157
# This workflow is from our own org repo and safe to reference by 'master'.
5258
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master # //NOSONAR
59+
with:
60+
node-version: ${{ vars.NODE_VERSION }}
5361
id: lockversion
5462
- if: steps.packagejson.outputs.exists == 'true'
5563
name: Setup Node.js
@@ -94,15 +102,18 @@ jobs:
94102
run: test -e ./package.json && echo "exists=true" >> $GITHUB_OUTPUT || echo "exists=false" >> $GITHUB_OUTPUT
95103
shell: bash
96104
- if: steps.packagejson.outputs.exists == 'true'
97-
name: Check package-lock version
105+
name: Determine what node version to use
98106
# This workflow is from our own org repo and safe to reference by 'master'.
99107
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master # //NOSONAR
108+
with:
109+
node-version: ${{ vars.NODE_VERSION }}
100110
id: lockversion
101111
- if: steps.packagejson.outputs.exists == 'true'
102112
name: Setup Node.js
103113
uses: actions/setup-node@v4
104114
with:
105115
node-version: "${{ steps.lockversion.outputs.version }}"
116+
registry-url: "https://registry.npmjs.org"
106117
- if: steps.packagejson.outputs.exists == 'true'
107118
name: Install dependencies
108119
shell: bash
@@ -115,14 +126,13 @@ jobs:
115126
id: release
116127
env:
117128
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
118-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
119129
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
120130
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
121131
GIT_AUTHOR_NAME: asyncapi-bot
122132
GIT_AUTHOR_EMAIL: info@asyncapi.io
123133
GIT_COMMITTER_NAME: asyncapi-bot
124134
GIT_COMMITTER_EMAIL: info@asyncapi.io
125-
run: npx semantic-release@19.0.4
135+
run: npx semantic-release@25.0.2
126136
- if: failure() # Only, on failure, send a message on the 94_bot-failing-ci slack channel
127137
name: Report workflow run status to Slack
128138
uses: 8398a7/action-slack@28ba43ae48961b90635b50953d216767a6bea486 #using https://github.com/8398a7/action-slack/releases/tag/v3.16.2

.github/workflows/if-nodejs-version-bump.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ jobs:
2525
id: packagejson
2626
run: test -e ./package.json && echo "exists=true" >> $GITHUB_OUTPUT || echo "exists=false" >> $GITHUB_OUTPUT
2727
- if: steps.packagejson.outputs.exists == 'true'
28-
name: Check package-lock version
28+
name: Determine what node version to use
2929
# This workflow is from our own org repo and safe to reference by 'master'.
3030
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master # //NOSONAR
31+
with:
32+
node-version: ${{ vars.NODE_VERSION }}
3133
id: lockversion
3234
- if: steps.packagejson.outputs.exists == 'true'
3335
name: Setup Node.js

.github/workflows/update-docs-on-docs-commits.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ jobs:
2222
steps:
2323
- name: Checkout repo
2424
uses: actions/checkout@v4
25-
- name: Check package-lock version
25+
- name: Determine what node version to use
2626
# This workflow is from our own org repo and safe to reference by 'master'.
2727
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master # //NOSONAR
28+
with:
29+
node-version: ${{ vars.NODE_VERSION }}
2830
id: lockversion
2931
- name: Use Node.js
3032
uses: actions/setup-node@v4

0 commit comments

Comments
 (0)