@codfish/actions 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,6 @@ jobs:
24
24
  contents: read
25
25
  pull-requests: read
26
26
  issues: read
27
- id-token: write
28
27
 
29
28
  steps:
30
29
  - name: Checkout repository
package/AGENT.md CHANGED
@@ -62,7 +62,8 @@ This project uses **pnpm** as the package manager. All commands should use pnpm:
62
62
  - `comment` - Creates or updates pull request comments with intelligent upsert functionality using unique tags
63
63
  - **IMPORTANT**: Any job using the comment action must include `permissions: pull-requests: write`
64
64
  - `setup-node-and-install` - Sets up Node.js environment and installs dependencies with automatic package manager
65
- detection, intelligent caching, and .nvmrc/.node-version support
65
+ detection, intelligent caching, and dynamic Node version detection via input, `.node-version`, `.nvmrc`, or
66
+ `package.json` `volta.node`. Validation is relaxed; the action no longer fails when no version is detected.
66
67
 
67
68
  ## Testing
68
69
 
@@ -127,3 +128,22 @@ The project implements multiple security measures:
127
128
  - **Vulnerability auditing**: Regular pnpm audit checks
128
129
  - **Note**: Dependency review requires GitHub Advanced Security (available free on public repos, paid feature for
129
130
  private repos)
131
+
132
+ ## Code Quality and File Editing Rules
133
+
134
+ ### Bats File Editing Rules
135
+
136
+ **CRITICAL**: After editing any `.bats` file, ALWAYS check for and remove trailing spaces:
137
+
138
+ 1. Run: `grep -n " $" path/to/file.bats`
139
+ 2. If any trailing spaces are found, remove them immediately
140
+ 3. Bats files are NOT automatically formatted by eslint/prettier, so manual cleanup is required
141
+ 4. Trailing spaces in bats files can cause test execution issues
142
+
143
+ ### General File Editing Guidelines
144
+
145
+ - Do what has been asked; nothing more, nothing less
146
+ - NEVER create files unless they're absolutely necessary for achieving your goal
147
+ - ALWAYS prefer editing an existing file to creating a new one
148
+ - NEVER proactively create documentation files (\*.md) or README files. Only create documentation files if explicitly
149
+ requested by the User
package/README.md CHANGED
@@ -67,7 +67,7 @@ automatically comments on PR
67
67
 
68
68
  | Input | Description | Required | Default |
69
69
  | -------------- | ----------------------------------------------------------------------------------- | -------- | ---------------- |
70
- | `npm-token` | Registry authentication token with publish permissions (works with npm/yarn/pnpm) | No | - |
70
+ | `npm-token` | Registry authentication token with publish permissions (works with npm/yarn/pnpm) | Yes | - |
71
71
  | `github-token` | GitHub token with pull request comment permissions (typically secrets.GITHUB_TOKEN) | Yes | - |
72
72
  | `comment` | Whether to comment on the PR with the published version (true/false) | No | `true` |
73
73
  | `comment-tag` | Tag to use for PR comments (for comment identification and updates) | No | `npm-publish-pr` |
@@ -101,16 +101,21 @@ steps:
101
101
  ### [setup-node-and-install](./setup-node-and-install/)
102
102
 
103
103
  Sets up Node.js environment and installs dependencies with automatic package manager detection (npm/pnpm/yarn),
104
- intelligent caching, and .nvmrc/.node-version support
104
+ intelligent caching, and version detection via input, .node-version, .nvmrc, or package.json volta.node
105
105
 
106
106
  **Inputs:**
107
107
 
108
- | Input | Description | Required | Default |
109
- | ------------------- | ----------------------------------------------------------------------------------------------------- | -------- | ------- |
110
- | `node-version` | Node.js version to install (e.g. '24', 'lts/\*'). Defaults to .nvmrc or .node-version file if present | No | - |
111
- | `cache-key-suffix` | Additional suffix for cache key to enable multiple caches per workflow | No | - |
112
- | `install-options` | Extra command-line options to pass to npm/pnpm/yarn install | No | - |
113
- | `working-directory` | Directory containing package.json and lockfile | No | `.` |
108
+ | Input | Description | Required | Default |
109
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------- |
110
+ | `node-version` | Node.js version to install (e.g. "24", "lts/\*"). Precedence: node-version input > .node-version > .nvmrc > package.json volta.node. | No | - |
111
+ | `install-options` | Extra command-line options to pass to npm/pnpm/yarn install. | No | - |
112
+ | `working-directory` | Directory containing package.json and lockfile. | No | `.` |
113
+
114
+ **Outputs:**
115
+
116
+ | Output | Description |
117
+ | ----------- | -------------------------------------------------- |
118
+ | `cache-hit` | Whether the dependency cache was hit (true/false). |
114
119
 
115
120
  **Usage:**
116
121
 
@@ -1,4 +1,4 @@
1
- # npm-pr-version
1
+ # npm-publish-pr
2
2
 
3
3
  Publishes packages with PR-specific version numbers for testing in downstream applications before merging. Automatically
4
4
  detects your package manager (npm, yarn, or pnpm) and uses the appropriate publish command. The action generates
@@ -102,7 +102,7 @@ The package is published under the `pr` tag, so it won't interfere with your reg
102
102
 
103
103
  | Input | Description | Required | Default |
104
104
  | -------------- | ----------------------------------------------------------------------------------- | -------- | ---------------- |
105
- | `npm-token` | Registry authentication token with publish permissions (works with npm/yarn/pnpm) | No | - |
105
+ | `npm-token` | Registry authentication token with publish permissions (works with npm/yarn/pnpm) | Yes | - |
106
106
  | `github-token` | GitHub token with pull request comment permissions (typically secrets.GITHUB_TOKEN) | Yes | - |
107
107
  | `comment` | Whether to comment on the PR with the published version (true/false) | No | `true` |
108
108
  | `comment-tag` | Tag to use for PR comments (for comment identification and updates) | No | `npm-publish-pr` |
@@ -6,7 +6,7 @@ description:
6
6
 
7
7
  inputs:
8
8
  npm-token:
9
- required: false
9
+ required: true
10
10
  description: Registry authentication token with publish permissions (works with npm/yarn/pnpm)
11
11
  github-token:
12
12
  required: true
@@ -53,11 +53,18 @@ runs:
53
53
  package_name=""
54
54
  version=""
55
55
 
56
+ # Function to sanitize error messages for GitHub output
57
+ sanitize_error() {
58
+ local message="$1"
59
+ # Replace newlines with spaces, remove extra whitespace, truncate if too long
60
+ echo "$message" | tr '\n' ' ' | tr -s ' ' | cut -c1-500
61
+ }
62
+
56
63
  # Validate package.json exists
57
64
  if [ ! -f "package.json" ]; then
58
65
  error_message="❌ ERROR: package.json not found in current directory. Make sure you're running this action in a directory with a package.json file"
59
66
  echo "$error_message"
60
- echo "error-message=$error_message" >> $GITHUB_OUTPUT
67
+ echo "error-message=$(sanitize_error "$error_message")" >> $GITHUB_OUTPUT
61
68
  exit 1
62
69
  fi
63
70
 
@@ -65,7 +72,7 @@ runs:
65
72
  if ! jq empty package.json 2>/dev/null; then
66
73
  error_message="❌ ERROR: package.json is not valid JSON"
67
74
  echo "$error_message"
68
- echo "error-message=$error_message" >> $GITHUB_OUTPUT
75
+ echo "error-message=$(sanitize_error "$error_message")" >> $GITHUB_OUTPUT
69
76
  exit 1
70
77
  fi
71
78
 
@@ -74,7 +81,7 @@ runs:
74
81
  if [ -z "$package_name" ] || [ "$package_name" = "null" ]; then
75
82
  error_message="❌ ERROR: package.json must have a 'name' field"
76
83
  echo "$error_message"
77
- echo "error-message=$error_message" >> $GITHUB_OUTPUT
84
+ echo "error-message=$(sanitize_error "$error_message")" >> $GITHUB_OUTPUT
78
85
  exit 1
79
86
  fi
80
87
 
@@ -104,29 +111,29 @@ runs:
104
111
  if [ $version_exit_code -ne 0 ]; then
105
112
  error_message="❌ ERROR: Failed to update package version. Check if the version format is valid. Error: $version_output"
106
113
  echo "$error_message"
107
- echo "error-message=$error_message" >> $GITHUB_OUTPUT
114
+ echo "error-message=$(sanitize_error "$error_message")" >> $GITHUB_OUTPUT
108
115
  exit 1
109
116
  fi
110
117
 
111
118
  # Publish package based on detected package manager
112
119
  case "$package_manager" in
113
120
  "yarn")
114
- publish_output=$(yarn publish --access public --tag pr --new-version $version --no-git-tag-version 2>&1)
121
+ publish_output=$(yarn publish --access public --tag pr --new-version $version --no-git-tag-version --skip-check-working-tree 2>&1)
115
122
  publish_exit_code=$?
116
123
  if [ $publish_exit_code -ne 0 ]; then
117
124
  error_message="❌ ERROR: Failed to publish package with yarn. Error: $publish_output"
118
125
  echo "$error_message"
119
- echo "error-message=$error_message" >> $GITHUB_OUTPUT
126
+ echo "error-message=$(sanitize_error "$error_message")" >> $GITHUB_OUTPUT
120
127
  exit 1
121
128
  fi
122
129
  ;;
123
130
  "pnpm")
124
- publish_output=$(pnpm publish --access public --tag pr 2>&1)
131
+ publish_output=$(pnpm publish --no-git-checks --access public --tag pr 2>&1)
125
132
  publish_exit_code=$?
126
133
  if [ $publish_exit_code -ne 0 ]; then
127
134
  error_message="❌ ERROR: Failed to publish package with pnpm. Error: $publish_output"
128
135
  echo "$error_message"
129
- echo "error-message=$error_message" >> $GITHUB_OUTPUT
136
+ echo "error-message=$(sanitize_error "$error_message")" >> $GITHUB_OUTPUT
130
137
  exit 1
131
138
  fi
132
139
  ;;
@@ -136,7 +143,7 @@ runs:
136
143
  if [ $publish_exit_code -ne 0 ]; then
137
144
  error_message="❌ ERROR: Failed to publish package with npm. Error: $publish_output"
138
145
  echo "$error_message"
139
- echo "error-message=$error_message" >> $GITHUB_OUTPUT
146
+ echo "error-message=$(sanitize_error "$error_message")" >> $GITHUB_OUTPUT
140
147
  exit 1
141
148
  fi
142
149
  ;;
@@ -156,7 +163,7 @@ runs:
156
163
 
157
164
  Error: ${{ steps.publish.outputs.error-message }}
158
165
 
159
- Please check the workflow logs for more details.
166
+ 📋 [View workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.
160
167
  upsert: true
161
168
  tag: ${{ inputs.comment-tag }}
162
169
 
@@ -166,6 +173,6 @@ runs:
166
173
  message: |
167
174
  ✅ **PR package published successfully!**
168
175
 
169
- Install with: `npm install ${{ steps.publish.outputs.package-name }}@${{ steps.publish.outputs.version }}`
176
+ Install with: <code>npm install ${{ steps.publish.outputs.package-name }}@${{ steps.publish.outputs.version }}</code>
170
177
  upsert: true
171
178
  tag: ${{ inputs.comment-tag }}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "description": "Composite GitHub Actions for my projects.",
5
5
  "author": "Chris O'Donnell <chris@codfish.dev>",
6
6
  "license": "MIT",
7
- "version": "1.1.0",
7
+ "version": "2.0.0",
8
8
  "publishConfig": {
9
9
  "access": "public"
10
10
  },
@@ -1,14 +1,14 @@
1
1
  # setup-node-and-install
2
2
 
3
3
  Sets up Node.js environment and installs dependencies with automatic package manager detection, intelligent caching, and
4
- .nvmrc/.node-version support.
4
+ dynamic Node version detection via the `node-version` input, `.node-version`, `.nvmrc`, or `package.json` `volta.node`.
5
5
 
6
6
  This action provides the following functionality:
7
7
 
8
8
  - Automatically detects package manager (npm, yarn, or pnpm) from lockfiles
9
9
  - Uses GitHub's official `setup-node` action with optimized caching
10
10
  - Installs dependencies with appropriate commands based on detected package manager
11
- - Supports .nvmrc and .node-version files for version specification
11
+ - Supports `.node-version`, `.nvmrc`, and `package.json` `volta.node` for version specification
12
12
  - Intelligent caching of node_modules when lockfiles are present
13
13
 
14
14
  <!-- DOCTOC SKIP -->
@@ -30,8 +30,10 @@ steps:
30
30
  - run: npm test
31
31
  ```
32
32
 
33
- The `node-version` input is optional. If not supplied, this action will attempt to use an `.nvmrc` file in your project.
34
- If neither is supplied, it will fail your workflow.
33
+ The `node-version` input is optional. If not supplied, this action will attempt to resolve a version using, in order:
34
+
35
+ 1. `.node-version`, 2) `.nvmrc`, 3) `package.json` `volta.node`. If none are present, `actions/setup-node` runs without
36
+ an explicit version and will use its default behavior.
35
37
 
36
38
  The `cache-key-suffix` input is optional. If not supplied, no suffix will be applied to the cache key used to restore
37
39
  cache in subsequent workflow runs.
@@ -69,25 +71,25 @@ steps:
69
71
  - run: npm test
70
72
  ```
71
73
 
72
- ## Node Version File Priority
74
+ ## Node Version Resolution Priority
73
75
 
74
76
  When multiple version specification methods are present, the action uses this priority order:
75
77
 
76
78
  1. **Input parameter** (`node-version`) - highest priority
77
- 2. **`.nvmrc` file** - takes precedence over .node-version
78
- 3. **`.node-version` file** - used if no .nvmrc exists
79
- 4. **Error** - if none of the above are present
79
+ 2. **`.node-version` file**
80
+ 3. **`.nvmrc` file**
81
+ 4. **`package.json` `volta.node` property**
82
+ 5. **`actions/setup-node` default behavior** when no version is specified
80
83
 
81
84
  ## Inputs
82
85
 
83
86
  <!-- start inputs -->
84
87
 
85
- | Input | Description | Required | Default |
86
- | ------------------- | ----------------------------------------------------------------------------------------------------- | -------- | ------- |
87
- | `node-version` | Node.js version to install (e.g. '24', 'lts/\*'). Defaults to .nvmrc or .node-version file if present | No | - |
88
- | `cache-key-suffix` | Additional suffix for cache key to enable multiple caches per workflow | No | - |
89
- | `install-options` | Extra command-line options to pass to npm/pnpm/yarn install | No | - |
90
- | `working-directory` | Directory containing package.json and lockfile | No | `.` |
88
+ | Input | Description | Required | Default |
89
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------- |
90
+ | `node-version` | Node.js version to install (e.g. "24", "lts/\*"). Precedence: node-version input > .node-version > .nvmrc > package.json volta.node. | No | - |
91
+ | `install-options` | Extra command-line options to pass to npm/pnpm/yarn install. | No | - |
92
+ | `working-directory` | Directory containing package.json and lockfile. | No | `.` |
91
93
 
92
94
  <!-- end inputs -->
93
95
 
@@ -2,39 +2,35 @@ name: setup-node-and-install
2
2
 
3
3
  description:
4
4
  Sets up Node.js environment and installs dependencies with automatic package manager detection (npm/pnpm/yarn),
5
- intelligent caching, and .nvmrc/.node-version support
5
+ intelligent caching, and version detection via input, .node-version, .nvmrc, or package.json volta.node
6
6
 
7
7
  inputs:
8
8
  node-version:
9
- description: Node.js version to install (e.g. '24', 'lts/*'). Defaults to .nvmrc or .node-version file if present
9
+ description:
10
+ 'Node.js version to install (e.g. "24", "lts/*"). Precedence: node-version input > .node-version > .nvmrc >
11
+ package.json volta.node.'
10
12
  required: false
11
- cache-key-suffix:
12
- description: Additional suffix for cache key to enable multiple caches per workflow
13
- default: ''
14
13
  install-options:
15
- description: Extra command-line options to pass to npm/pnpm/yarn install
14
+ description: Extra command-line options to pass to npm/pnpm/yarn install.
16
15
  default: ''
17
16
  working-directory:
18
- description: Directory containing package.json and lockfile
17
+ description: Directory containing package.json and lockfile.
19
18
  default: .
20
19
 
21
- outputs: {}
20
+ outputs:
21
+ cache-hit:
22
+ description: Whether the dependency cache was hit (true/false).
23
+ value: "${{ steps.setup-node.outputs.cache-hit == 'true' && 'true' || 'false' }}"
22
24
 
23
25
  runs:
24
26
  using: composite
25
27
 
26
28
  steps:
27
- - name: Validate inputs and detect package manager
28
- id: setup
29
+ - name: Validate environment and detect package manager
30
+ id: detect-package-manager
29
31
  working-directory: ${{ inputs.working-directory }}
30
32
  shell: bash
31
33
  run: |
32
- # Validate working directory exists
33
- if [ ! -d "." ]; then
34
- echo "❌ ERROR: Working directory '${{ inputs.working-directory }}' does not exist"
35
- exit 1
36
- fi
37
-
38
34
  # Validate package.json exists
39
35
  if [ ! -f "./package.json" ]; then
40
36
  echo "❌ ERROR: package.json not found in '${{ inputs.working-directory }}'"
@@ -42,48 +38,6 @@ runs:
42
38
  exit 1
43
39
  fi
44
40
 
45
- # Validate package.json is valid JSON
46
- if ! jq empty package.json 2>/dev/null; then
47
- echo "❌ ERROR: package.json is not valid JSON"
48
- exit 1
49
- fi
50
-
51
- # Check Node version requirements (.nvmrc, .node-version, or input)
52
- if [[ ! -f "./.nvmrc" && ! -f "./.node-version" && -z "$INPUT_NODE_VERSION" ]]; then
53
- echo "node-version-missing=true" >> $GITHUB_OUTPUT
54
- exit
55
- fi
56
-
57
- # Validate .nvmrc format if it exists
58
- if [ -f "./.nvmrc" ]; then
59
- nvmrc_version=$(cat .nvmrc | tr -d '\n\r' | xargs)
60
- if [ -z "$nvmrc_version" ]; then
61
- echo "❌ ERROR: .nvmrc file is empty"
62
- exit 1
63
- fi
64
- echo "📋 Found .nvmrc with Node version: $nvmrc_version"
65
- fi
66
-
67
- # Validate .node-version format if it exists (and no .nvmrc)
68
- if [ -f "./.node-version" ] && [ ! -f "./.nvmrc" ]; then
69
- node_version_file=$(cat .node-version | tr -d '\n\r' | xargs)
70
- if [ -z "$node_version_file" ]; then
71
- echo "❌ ERROR: .node-version file is empty"
72
- exit 1
73
- fi
74
- echo "📋 Found .node-version with Node version: $node_version_file"
75
- fi
76
-
77
- # Show priority info if both files exist
78
- if [ -f "./.nvmrc" ] && [ -f "./.node-version" ]; then
79
- echo "📋 Both .nvmrc and .node-version found, .nvmrc takes priority"
80
- fi
81
-
82
- # Validate node-version input format if provided
83
- if [ -n "$INPUT_NODE_VERSION" ]; then
84
- echo "📋 Using Node version from input: $INPUT_NODE_VERSION"
85
- fi
86
-
87
41
  # Detect package manager based on lockfiles (including yarn)
88
42
  if [ -f "./pnpm-lock.yaml" ]; then
89
43
  echo "package-manager=pnpm" >> $GITHUB_OUTPUT
@@ -106,115 +60,134 @@ runs:
106
60
  echo "lockfile-path=" >> $GITHUB_OUTPUT
107
61
  echo "📦 No lockfile found, defaulting to: npm"
108
62
  fi
109
- env:
110
- INPUT_NODE_VERSION: ${{ inputs.node-version }}
111
-
112
- - if: steps.setup.outputs.node-version-missing == 'true'
113
- uses: actions/github-script@v7
114
- with:
115
- script: |
116
- core.setFailed('You need to create an .nvmrc file, .node-version file, or pass a value in the `node-version` input.')
117
63
 
118
64
  - name: Install pnpm
119
- if: steps.setup.outputs.package-manager == 'pnpm'
65
+ if: steps.detect-package-manager.outputs.package-manager == 'pnpm'
120
66
  uses: pnpm/action-setup@v4
121
67
  with:
122
68
  run_install: false
123
69
 
124
- - name: Setup node with .nvmrc
125
- if: ${{ inputs.node-version != '' || hashFiles(format('{0}/.nvmrc', inputs.working-directory)) != '' }}
126
- uses: actions/setup-node@v5
127
- id: setup-node
128
- with:
129
- # use detected package manager cache if a lockfile is present
130
- cache: ${{ steps.setup.outputs.lockfile-exists == 'true' && steps.setup.outputs.package-manager || '' }}
131
- # supplying a node-version input will override the .nvmrc file and give a warning, but that's to be expected.
132
- cache-dependency-path: ${{ inputs.working-directory }}
133
- node-version: ${{ inputs.node-version }}
134
- node-version-file: '${{ inputs.working-directory }}/.nvmrc'
135
- registry-url: 'https://registry.npmjs.org'
136
-
137
- - name: Read .node-version file
138
- if:
139
- ${{ inputs.node-version == '' && hashFiles(format('{0}/.nvmrc', inputs.working-directory)) == '' &&
140
- hashFiles(format('{0}/.node-version', inputs.working-directory)) != '' }}
141
- id: read-node-version
70
+ # Detect Node.js version to use (input > .node-version > .nvmrc > package.json volta.node)
71
+ - name: Detect node version
72
+ id: detect-node-version
142
73
  working-directory: ${{ inputs.working-directory }}
143
74
  shell: bash
144
75
  run: |
145
- node_version=$(cat .node-version | tr -d '\n\r' | xargs)
146
- echo "node-version=$node_version" >> $GITHUB_OUTPUT
147
- echo "📋 Read Node version from .node-version: $node_version"
148
-
149
- - name: Setup node with .node-version
150
- if:
151
- ${{ inputs.node-version == '' && hashFiles(format('{0}/.nvmrc', inputs.working-directory)) == '' &&
152
- hashFiles(format('{0}/.node-version', inputs.working-directory)) != '' }}
76
+ resolved_version=""
77
+
78
+ if [ -n "${INPUT_NODE_VERSION}" ]; then
79
+ resolved_version="$INPUT_NODE_VERSION"
80
+ echo "📋 Using Node version from input: $resolved_version"
81
+ elif [ -f "./.node-version" ]; then
82
+ file_version=$(cat ./.node-version | tr -d '\n\r' | xargs)
83
+ if [ -n "$file_version" ]; then
84
+ resolved_version="$file_version"
85
+ echo "📋 Using Node version from .node-version: $resolved_version"
86
+ fi
87
+ fi
88
+
89
+ if [ -z "$resolved_version" ] && [ -f "./.nvmrc" ]; then
90
+ nvmrc_version=$(cat ./.nvmrc | tr -d '\n\r' | xargs)
91
+ if [ -n "$nvmrc_version" ]; then
92
+ resolved_version="$nvmrc_version"
93
+ echo "📋 Using Node version from .nvmrc: $resolved_version"
94
+ fi
95
+ fi
96
+
97
+ if [ -z "$resolved_version" ]; then
98
+ volta_node=$(jq -r '.volta.node // empty' package.json 2>/dev/null || true)
99
+ if [ -n "$volta_node" ]; then
100
+ resolved_version="$volta_node"
101
+ echo "📋 Using Node version from package.json volta.node: $resolved_version"
102
+ fi
103
+ fi
104
+
105
+ echo "version=$resolved_version" >> $GITHUB_OUTPUT
106
+ env:
107
+ INPUT_NODE_VERSION: ${{ inputs.node-version }}
108
+
109
+ - name: Setup Node.js
153
110
  uses: actions/setup-node@v5
154
- id: setup-node-alt
111
+ id: setup-node
155
112
  with:
156
- # use detected package manager cache if a lockfile is present
157
- cache: ${{ steps.setup.outputs.lockfile-exists == 'true' && steps.setup.outputs.package-manager || '' }}
113
+ # use detected package manager cache
114
+ cache: ${{ steps.detect-package-manager.outputs.package-manager }}
158
115
  cache-dependency-path: ${{ inputs.working-directory }}
159
- node-version: ${{ steps.read-node-version.outputs.node-version }}
116
+ node-version: ${{ steps.detect-node-version.outputs.version }}
160
117
  registry-url: 'https://registry.npmjs.org'
161
118
 
162
- # Install dependencies with pnpm
119
+ # apply `./node_modules` cache only if a lockfile is present
120
+ # Will remove the need to run install commands twice. Risk reduced by using a very specific cache key.
121
+ # Cache wont be used if the lockfile changes, package manager, node version, or OS changes.
122
+ - name: Setup node_modules dependency cache
123
+ if: steps.detect-package-manager.outputs.lockfile-exists == 'true'
124
+ uses: actions/cache@v4
125
+ id: cache
126
+ with:
127
+ path: ${{ inputs.working-directory }}/node_modules
128
+ key:
129
+ ${{ runner.os }}-node_modules-${{ steps.detect-package-manager.outputs.package-manager }}-${{
130
+ steps.setup-node.outputs.node-version }}-${{ hashFiles(format('{0}/{1}', inputs.working-directory,
131
+ steps.detect-package-manager.outputs.lockfile-path)) }}
132
+
163
133
  - name: Install dependencies with pnpm
164
- if: steps.setup.outputs.package-manager == 'pnpm'
134
+ if: steps.detect-package-manager.outputs.package-manager == 'pnpm' && steps.cache.outputs.cache-hit != 'true'
165
135
  working-directory: ${{ inputs.working-directory }}
166
136
  shell: bash
167
137
  run: |
168
138
  echo "🔧 Installing dependencies with pnpm..."
169
- if ! pnpm install $INPUT_INSTALL_OPTIONS; then
139
+ if [ "$LOCKFILE_EXISTS" = "true" ]; then
140
+ INPUT_INSTALL_OPTIONS="--frozen-lockfile $INPUT_INSTALL_OPTIONS"
141
+ else
142
+ echo "⚠️ Warning: No lockfile found, not using --frozen-lockfile"
143
+ fi
144
+ if ! pnpm i $INPUT_INSTALL_OPTIONS; then
170
145
  echo "❌ ERROR: pnpm install failed"
171
146
  exit 1
172
147
  fi
173
148
  echo "✅ pnpm install completed successfully"
174
149
  env:
175
150
  INPUT_INSTALL_OPTIONS: ${{ inputs.install-options }}
151
+ LOCKFILE_EXISTS: ${{ steps.detect-package-manager.outputs.lockfile-exists }}
176
152
 
177
- # Install dependencies with yarn
178
153
  - name: Install dependencies with yarn
179
- if: steps.setup.outputs.package-manager == 'yarn'
154
+ if: steps.detect-package-manager.outputs.package-manager == 'yarn' && steps.cache.outputs.cache-hit != 'true'
180
155
  working-directory: ${{ inputs.working-directory }}
181
156
  shell: bash
182
157
  run: |
183
158
  echo "🔧 Installing dependencies with yarn..."
184
- if ! yarn install --frozen-lockfile $INPUT_INSTALL_OPTIONS; then
159
+ if [ "$LOCKFILE_EXISTS" = "true" ]; then
160
+ INPUT_INSTALL_OPTIONS="--frozen-lockfile $INPUT_INSTALL_OPTIONS"
161
+ else
162
+ echo "⚠️ Warning: No lockfile found, results may vary"
163
+ fi
164
+ if ! yarn install $INPUT_INSTALL_OPTIONS; then
185
165
  echo "❌ ERROR: yarn install failed"
186
166
  exit 1
187
167
  fi
188
168
  echo "✅ yarn install completed successfully"
189
169
  env:
190
170
  INPUT_INSTALL_OPTIONS: ${{ inputs.install-options }}
171
+ LOCKFILE_EXISTS: ${{ steps.detect-package-manager.outputs.lockfile-exists }}
191
172
 
192
- # Install dependencies with npm (with lockfile)
193
- - name: Install dependencies with npm (with lockfile)
194
- if: steps.setup.outputs.lockfile-exists == 'true' && steps.setup.outputs.package-manager == 'npm'
173
+ - name: Install dependencies with npm
174
+ if: steps.detect-package-manager.outputs.package-manager == 'npm' && steps.cache.outputs.cache-hit != 'true'
195
175
  working-directory: ${{ inputs.working-directory }}
196
176
  shell: bash
197
177
  run: |
198
- echo "🔧 Installing dependencies with npm ci..."
199
- if ! npm ci --prefer-offline --no-audit $INPUT_INSTALL_OPTIONS; then
200
- echo "❌ ERROR: npm ci failed"
201
- exit 1
178
+ NPM_CMD=""
179
+ if [ "$LOCKFILE_EXISTS" = "true" ]; then
180
+ NPM_CMD="ci"
181
+ else
182
+ echo "⚠️ Warning: No lockfile found, versions may vary"
183
+ NPM_CMD="install"
202
184
  fi
203
- echo " npm ci completed successfully"
204
- env:
205
- INPUT_INSTALL_OPTIONS: ${{ inputs.install-options }}
206
-
207
- - name: Install dependencies with npm (without lockfile)
208
- if: steps.setup.outputs.lockfile-exists == 'false' && steps.setup.outputs.package-manager == 'npm'
209
- working-directory: ${{ inputs.working-directory }}
210
- shell: bash
211
- run: |
212
- echo "🔧 Installing dependencies with npm install..."
213
- echo "⚠️ Warning: No lockfile found, versions may vary"
214
- if ! npm install --no-save --prefer-offline --no-audit $INPUT_INSTALL_OPTIONS; then
215
- echo "❌ ERROR: npm install failed"
185
+ echo "🔧 Installing dependencies with npm $NPM_CMD..."
186
+ if ! npm $NPM_CMD --no-save --prefer-offline --no-audit $INPUT_INSTALL_OPTIONS; then
187
+ echo "❌ ERROR: npm $NPM_CMD failed"
216
188
  exit 1
217
189
  fi
218
- echo "✅ npm install completed successfully"
190
+ echo "✅ npm $NPM_CMD completed successfully"
219
191
  env:
220
192
  INPUT_INSTALL_OPTIONS: ${{ inputs.install-options }}
193
+ LOCKFILE_EXISTS: ${{ steps.detect-package-manager.outputs.lockfile-exists }}