@codfish/actions 0.0.0-PR-58--24ced07

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.
@@ -0,0 +1,362 @@
1
+ name: npm-pr-version
2
+
3
+ description:
4
+ Publishes package with PR-specific version (0.0.0-PR-123--abc1234) using detected package manager (npm/yarn/pnpm) or
5
+ OIDC trusted publishing, and automatically comments on PR
6
+
7
+ inputs:
8
+ npm-token:
9
+ required: false
10
+ description:
11
+ Registry authentication token with publish permissions. If not provided, OIDC trusted publishing will be used.
12
+ tarball:
13
+ required: false
14
+ description:
15
+ Path to pre-built tarball to publish (e.g., '*.tgz'). When provided, publishes the tarball with --ignore-scripts
16
+ for security. Recommended for pull_request_target workflows to prevent execution of malicious lifecycle scripts.
17
+ comment:
18
+ required: false
19
+ default: 'true'
20
+ description: Whether to comment on the PR with the published version (true/false)
21
+ comment-tag:
22
+ required: false
23
+ default: npm-publish-pr
24
+ description: Tag to use for PR comments (for comment identification and updates)
25
+ dev:
26
+ required: false
27
+ default: 'false'
28
+ description: If true, use dev dependency install syntax in the PR comment (e.g. npm install -D, pnpm add -D).
29
+
30
+ outputs:
31
+ version:
32
+ description: Generated PR-specific version number (0.0.0-PR-{number}--{short-sha})
33
+ value: '${{ steps.publish.outputs.version }}'
34
+ package-name:
35
+ description: Package name from package.json
36
+ value: '${{ steps.publish.outputs.package-name }}'
37
+ error-message:
38
+ description: Error message if publish fails
39
+ value: '${{ steps.publish.outputs.error-message }}'
40
+
41
+ runs:
42
+ using: composite
43
+
44
+ steps:
45
+ - uses: codfish/actions/comment@v3
46
+ if: inputs.comment == 'true'
47
+ with:
48
+ message: ⏳ Publishing PR version...
49
+ upsert: true
50
+ tag: ${{ inputs.comment-tag }}
51
+
52
+ - name: Validate and publish to registry
53
+ id: publish
54
+ shell: bash
55
+ run: |
56
+ set +e # Don't exit on error so we can handle failures
57
+
58
+ # Initialize outputs for error handling
59
+ error_message=""
60
+ package_name=""
61
+ version=""
62
+ package_manager="npm"
63
+ tarball_mode=false
64
+ NPMRC_AUTH_FILE=""
65
+ REPACK_DIR=""
66
+
67
+ # Clean up on exit: remove temp auth file (never touch project .npmrc), temp files, repack dir
68
+ cleanup() {
69
+ local exit_code=$?
70
+ [ -n "$NPMRC_AUTH_FILE" ] && rm -f "$NPMRC_AUTH_FILE" 2>/dev/null || true
71
+ [ -n "$temp_pkg_json" ] && rm -f "$temp_pkg_json" 2>/dev/null || true
72
+ [ -n "$REPACK_DIR" ] && rm -rf "$REPACK_DIR" 2>/dev/null || true
73
+ return $exit_code
74
+ }
75
+ trap cleanup EXIT
76
+
77
+ # Detect if tarball mode is being used
78
+ if [ -n "$INPUT_TARBALL" ]; then
79
+ tarball_mode=true
80
+ echo "🔒 SECURE MODE: Using pre-built tarball (lifecycle scripts will NOT execute)"
81
+
82
+ # Expand glob patterns (e.g., *.tgz) to actual filename
83
+ shopt -s nullglob
84
+ tarball_files=($INPUT_TARBALL)
85
+ shopt -u nullglob
86
+
87
+ if [ ${#tarball_files[@]} -eq 0 ]; then
88
+ error_message="❌ ERROR: No tarball files found matching pattern: $INPUT_TARBALL"
89
+ echo "$error_message"
90
+ echo "error-message=$error_message" >> $GITHUB_OUTPUT
91
+ exit 1
92
+ elif [ ${#tarball_files[@]} -gt 1 ]; then
93
+ error_message="❌ ERROR: Multiple tarball files found matching pattern: $INPUT_TARBALL (found: ${tarball_files[*]}). Please specify a single tarball file."
94
+ echo "$error_message"
95
+ echo "error-message=$error_message" >> $GITHUB_OUTPUT
96
+ exit 1
97
+ fi
98
+
99
+ # Use the resolved tarball path
100
+ INPUT_TARBALL="${tarball_files[0]}"
101
+ echo "📦 Resolved tarball: $INPUT_TARBALL"
102
+ fi
103
+
104
+ # Function to extract relevant error message from npm output
105
+ extract_error() {
106
+ local output="$1"
107
+
108
+ # Extract npm error lines (lines starting with "npm error")
109
+ local error_lines=$(echo "$output" | grep "^npm error" | head -5)
110
+
111
+ # If we found error lines, use those
112
+ if [ -n "$error_lines" ]; then
113
+ echo "$error_lines" | tr '\n' ' ' | tr -s ' '
114
+ else
115
+ # Otherwise, take the last few lines (likely contains the error)
116
+ echo "$output" | tail -10 | tr '\n' ' ' | tr -s ' ' | cut -c1-500
117
+ fi
118
+ }
119
+
120
+ # Standardized publish error handler
121
+ handle_publish_error() {
122
+ local manager_name="$1"
123
+ local publish_output="$2"
124
+ local extracted_error=""
125
+
126
+ extracted_error=$(extract_error "$publish_output")
127
+ error_message="❌ Failed to publish with ${manager_name}: ${extracted_error}"
128
+ echo "Full output: $publish_output"
129
+ echo "Error message: $error_message"
130
+ echo "error-message=$error_message" >> $GITHUB_OUTPUT
131
+ exit 1
132
+ }
133
+
134
+ # In tarball mode: unpack, inject PR version, repack (so we publish a unique version)
135
+ # In normal mode, validate package.json
136
+ if [ "$tarball_mode" = true ]; then
137
+ # Validate tarball exists
138
+ if [ ! -f "$INPUT_TARBALL" ]; then
139
+ error_message="❌ ERROR: Tarball not found at path: $INPUT_TARBALL"
140
+ echo "$error_message"
141
+ echo "error-message=$error_message" >> $GITHUB_OUTPUT
142
+ exit 1
143
+ fi
144
+
145
+ echo "📦 Unpacking tarball and injecting PR version: $INPUT_TARBALL"
146
+
147
+ # Unpack to temp dir (npm pack format: top-level "package/" with package.json inside)
148
+ repack_dir=$(mktemp -d)
149
+ REPACK_DIR="$repack_dir"
150
+ tar -xzf "$INPUT_TARBALL" -C "$repack_dir"
151
+
152
+ if [ ! -f "$repack_dir/package/package.json" ]; then
153
+ error_message="❌ ERROR: Could not extract package.json from tarball (expected package/package.json)"
154
+ echo "$error_message"
155
+ echo "error-message=$error_message" >> $GITHUB_OUTPUT
156
+ exit 1
157
+ fi
158
+
159
+ package_name=$(jq -r '.name // empty' "$repack_dir/package/package.json")
160
+ if [ -z "$package_name" ] || [ "$package_name" = "null" ]; then
161
+ error_message="❌ ERROR: Tarball's package.json must have a 'name' field"
162
+ echo "$error_message"
163
+ echo "error-message=$error_message" >> $GITHUB_OUTPUT
164
+ exit 1
165
+ fi
166
+
167
+ # Generate PR-specific version (same format as normal mode) so we don't overwrite published versions
168
+ version="0.0.0-PR-${PR}--$(echo ${SHA} | cut -c -7)"
169
+ jq --arg v "$version" '.version = $v' "$repack_dir/package/package.json" > "$repack_dir/package/package.json.tmp" && mv "$repack_dir/package/package.json.tmp" "$repack_dir/package/package.json"
170
+
171
+ # Repack for publish (still secure: --ignore-scripts used when publishing)
172
+ (cd "$repack_dir" && tar -czf repack.tgz package)
173
+ TARBALL_TO_PUBLISH="$repack_dir/repack.tgz"
174
+
175
+ echo "📦 Tarball package: $package_name@$version (PR version)"
176
+ echo "package-name=$package_name" >> $GITHUB_OUTPUT
177
+ echo "version=$version" >> $GITHUB_OUTPUT
178
+ else
179
+ # Normal mode: validate package.json exists in current directory
180
+ if [ ! -f "package.json" ]; then
181
+ 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"
182
+ echo "$error_message"
183
+ echo "error-message=$(extract_error "$error_message")" >> $GITHUB_OUTPUT
184
+ exit 1
185
+ fi
186
+
187
+ # Validate package.json is valid JSON
188
+ if ! jq empty package.json 2>/dev/null; then
189
+ error_message="❌ ERROR: package.json is not valid JSON"
190
+ echo "$error_message"
191
+ echo "error-message=$(extract_error "$error_message")" >> $GITHUB_OUTPUT
192
+ exit 1
193
+ fi
194
+
195
+ # Check if package has a name
196
+ package_name=$(jq -r '.name // empty' package.json)
197
+ if [ -z "$package_name" ] || [ "$package_name" = "null" ]; then
198
+ error_message="❌ ERROR: package.json must have a 'name' field"
199
+ echo "$error_message"
200
+ echo "error-message=$(extract_error "$error_message")" >> $GITHUB_OUTPUT
201
+ exit 1
202
+ fi
203
+
204
+ # Output package name for use in error handling
205
+ echo "package-name=$package_name" >> $GITHUB_OUTPUT
206
+ fi
207
+
208
+ # Detect authentication mode and package manager
209
+ if [ -z "$INPUT_NPM_TOKEN" ]; then
210
+ echo "🔐 Using OIDC trusted publishing (no npm-token provided)"
211
+ echo "📦 Using npm for OIDC (--provenance requires npm)"
212
+
213
+ if [ -z "$ACTIONS_ID_TOKEN_REQUEST_URL" ]; then
214
+ error_message="❌ ERROR: OIDC token not available. Add 'permissions: { id-token: write }' to your workflow"
215
+ echo "$error_message"
216
+ echo "error-message=$error_message" >> $GITHUB_OUTPUT
217
+ exit 1
218
+ fi
219
+ fi
220
+
221
+ if [ "$INPUT_NPM_TOKEN" ]; then
222
+ echo "🔐 Using token-based authentication"
223
+
224
+ # Token mode: use a temp userconfig file so we never overwrite or delete the project's .npmrc.
225
+ # npm merges NPM_CONFIG_USERCONFIG with project .npmrc, so custom registry/scoped config is preserved.
226
+ export NODE_AUTH_TOKEN="$INPUT_NPM_TOKEN"
227
+ NPMRC_AUTH_FILE=$(mktemp)
228
+ echo "//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}" > "$NPMRC_AUTH_FILE"
229
+ export NPM_CONFIG_USERCONFIG="$NPMRC_AUTH_FILE"
230
+
231
+ # Detect package manager for token-based publishing
232
+ if [ -f "./yarn.lock" ]; then
233
+ package_manager="yarn"
234
+ echo "📦 Detected package manager: yarn"
235
+ elif [ -f "./pnpm-lock.yaml" ]; then
236
+ package_manager="pnpm"
237
+ echo "📦 Detected package manager: pnpm"
238
+ else
239
+ package_manager="npm"
240
+ echo "📦 Detected package manager: npm"
241
+ fi
242
+ fi
243
+
244
+ # Generate version (skip in tarball mode - already extracted)
245
+ if [ "$tarball_mode" = false ]; then
246
+ version="0.0.0-PR-${PR}--$(echo ${SHA} | cut -c -7)"
247
+ echo "📦 Publishing $package_name@$version with $package_manager"
248
+ echo "version=$version" >> $GITHUB_OUTPUT
249
+
250
+ # Update package.json version (all package managers support npm version)
251
+ version_output=$(npm version $version --no-git-tag-version 2>&1)
252
+ version_exit_code=$?
253
+ if [ $version_exit_code -ne 0 ]; then
254
+ error_message="❌ ERROR: Failed to update package version. Check if the version format is valid. Error: $version_output"
255
+ echo "$error_message"
256
+ echo "error-message=$(extract_error "$error_message")" >> $GITHUB_OUTPUT
257
+ exit 1
258
+ fi
259
+ else
260
+ echo "📦 Publishing $package_name@$version from tarball"
261
+ fi
262
+
263
+ # Publish package
264
+ if [ "$tarball_mode" = true ]; then
265
+ # SECURE TARBALL MODE: Publish repacked tarball (PR version injected) with --ignore-scripts
266
+ echo "🔒 Publishing tarball with --ignore-scripts (secure mode)"
267
+
268
+ if [ -z "$INPUT_NPM_TOKEN" ]; then
269
+ # OIDC mode with tarball
270
+ publish_output=$(npm publish "$TARBALL_TO_PUBLISH" --access public --tag pr --provenance --ignore-scripts 2>&1)
271
+ publish_exit_code=$?
272
+
273
+ if [ $publish_exit_code -ne 0 ]; then
274
+ handle_publish_error "OIDC (tarball)" "$publish_output"
275
+ fi
276
+ echo "✅ Successfully published $package_name@$version using OIDC (secure tarball mode)"
277
+ else
278
+ # Token mode with tarball - always use npm for tarball publishing
279
+ publish_output=$(npm publish "$TARBALL_TO_PUBLISH" --access public --tag pr --ignore-scripts 2>&1)
280
+ publish_exit_code=$?
281
+
282
+ if [ $publish_exit_code -ne 0 ]; then
283
+ handle_publish_error "npm (tarball)" "$publish_output"
284
+ fi
285
+ echo "✅ Successfully published $package_name@$version using npm (secure tarball mode)"
286
+ fi
287
+ else
288
+ # NORMAL MODE: Traditional publishing (INSECURE for pull_request_target)
289
+ if [ -z "$INPUT_NPM_TOKEN" ]; then
290
+ echo "📦 Publishing with OIDC trusted publishing..."
291
+
292
+ publish_output=$(npm publish --access public --tag pr --provenance 2>&1)
293
+ publish_exit_code=$?
294
+
295
+ if [ $publish_exit_code -ne 0 ]; then
296
+ handle_publish_error "OIDC" "$publish_output"
297
+ fi
298
+ echo "✅ Successfully published $package_name@$version using OIDC"
299
+ else
300
+ # Token mode: use detected package manager
301
+ case "$package_manager" in
302
+ "yarn")
303
+ publish_output=$(yarn publish --access public --tag pr --new-version $version --no-git-tag-version --skip-check-working-tree 2>&1)
304
+ publish_exit_code=$?
305
+ if [ $publish_exit_code -ne 0 ]; then
306
+ handle_publish_error "yarn" "$publish_output"
307
+ fi
308
+ ;;
309
+ "pnpm")
310
+ publish_output=$(pnpm publish --no-git-checks --access public --tag pr 2>&1)
311
+ publish_exit_code=$?
312
+ if [ $publish_exit_code -ne 0 ]; then
313
+ handle_publish_error "pnpm" "$publish_output"
314
+ fi
315
+ ;;
316
+ *)
317
+ publish_output=$(npm publish --access public --tag pr 2>&1)
318
+ publish_exit_code=$?
319
+ if [ $publish_exit_code -ne 0 ]; then
320
+ handle_publish_error "npm" "$publish_output"
321
+ fi
322
+ ;;
323
+ esac
324
+ echo "✅ Successfully published $package_name@$version using $package_manager"
325
+ fi
326
+ fi
327
+ env:
328
+ # CRITICAL: Use INPUT_NPM_TOKEN instead of NPM_TOKEN here to avoid
329
+ # setting NPM_TOKEN in the environment when empty (which could break OIDC)
330
+ INPUT_NPM_TOKEN: ${{ inputs.npm-token }}
331
+ INPUT_TARBALL: ${{ inputs.tarball }}
332
+ PR: ${{ github.event.number }}
333
+ SHA: ${{ github.event.pull_request.head.sha }}
334
+
335
+ - uses: codfish/actions/comment@v3
336
+ if: failure() && inputs.comment == 'true'
337
+ with:
338
+ message: |
339
+ ❌ **PR package publish failed!**
340
+
341
+ Error: ${{ steps.publish.outputs.error-message }}
342
+
343
+ 📋 [View workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.
344
+ upsert: true
345
+ tag: ${{ inputs.comment-tag }}
346
+
347
+ - uses: codfish/actions/comment@v3
348
+ if: success() && inputs.comment == 'true'
349
+ with:
350
+ message: |
351
+ ✅ **PR package published successfully!**
352
+
353
+ Install:
354
+
355
+ ${{ inputs.dev == 'true' && 'pnpm add -D ' || 'pnpm add ' }}${{ steps.publish.outputs.package-name }}@${{ steps.publish.outputs.version }}
356
+ ${{ inputs.dev == 'true' && 'npm install -D ' || 'npm install ' }}${{ steps.publish.outputs.package-name }}@${{ steps.publish.outputs.version }}
357
+ ${{ inputs.dev == 'true' && 'yarn add -D ' || 'yarn add ' }}${{ steps.publish.outputs.package-name }}@${{ steps.publish.outputs.version }}
358
+ ${{ inputs.dev == 'true' && 'bun add -d ' || 'bun add ' }}${{ steps.publish.outputs.package-name }}@${{ steps.publish.outputs.version }}
359
+
360
+ View on npm: https://www.npmjs.com/package/${{ steps.publish.outputs.package-name }}/v/${{ steps.publish.outputs.version }}
361
+ upsert: true
362
+ tag: ${{ inputs.comment-tag }}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@codfish/actions",
3
+ "type": "module",
4
+ "description": "Composite GitHub Actions for my projects.",
5
+ "author": "Chris O'Donnell <chris@codfish.dev>",
6
+ "license": "MIT",
7
+ "version": "0.0.0-PR-58--24ced07",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/codfish/actions.git"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "comment",
17
+ "npm-publish-pr",
18
+ "setup-node-and-install",
19
+ "bin",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "lint": "eslint .",
24
+ "fix": "eslint . --fix",
25
+ "format": "prettier --write \"**/*.{json,css,md,yml}\" --config ./node_modules/@codfish/eslint-config/prettier.js",
26
+ "test": "bash tests/scripts/test-runner.sh",
27
+ "test:integration": "bash tests/scripts/test-runner.sh integration",
28
+ "test:unit": "bash tests/scripts/test-runner.sh unit",
29
+ "docs:generate": "node bin/generate-docs.js",
30
+ "prepare": "husky"
31
+ },
32
+ "devDependencies": {
33
+ "@codfish/eslint-config": "12.3.0",
34
+ "bats": "^1.13.0",
35
+ "doctoc": "^2.2.1",
36
+ "eslint": "^9.39.2",
37
+ "husky": "^9.1.7",
38
+ "js-yaml": "^4.1.0",
39
+ "lint-staged": "^16.2.7",
40
+ "prettier": "^3.8.1"
41
+ },
42
+ "packageManager": "pnpm@10.29.3",
43
+ "commitlint": {
44
+ "extends": [
45
+ "./node_modules/@codfish/eslint-config/commitlint.js"
46
+ ]
47
+ },
48
+ "lint-staged": {
49
+ "*.{json,css}": [
50
+ "prettier --write --config ./node_modules/@codfish/eslint-config/prettier.js"
51
+ ],
52
+ "*.md": [
53
+ "prettier --write --config ./node_modules/@codfish/eslint-config/prettier.js",
54
+ "doctoc --title '## Table of Contents'"
55
+ ]
56
+ }
57
+ }
@@ -0,0 +1,184 @@
1
+ # setup-node-and-install
2
+
3
+ Sets up Node.js environment and installs dependencies with automatic package manager detection, intelligent caching, and
4
+ dynamic Node version detection via the `node-version` input, `.node-version`, `.nvmrc`, or `package.json` `volta.node`.
5
+
6
+ This action provides the following functionality:
7
+
8
+ - Automatically detects package manager (npm, yarn, or pnpm) from lockfiles
9
+ - Uses GitHub's official `setup-node` action (v6) with optimized caching
10
+ - **Upgrades npm to v11** (pinned to `^11.5.1` for OIDC trusted publishing support)
11
+ - Installs dependencies with appropriate commands based on detected package manager
12
+ - Supports `.node-version`, `.nvmrc`, and `package.json` `volta.node` for version specification
13
+ - Intelligent caching of node_modules when lockfiles are present
14
+
15
+ <!-- DOCTOC SKIP -->
16
+
17
+ ## Usage
18
+
19
+ See [action.yml](action.yml).
20
+
21
+ ```yml
22
+ steps:
23
+ - uses: actions/checkout@v6
24
+
25
+ # Will setup node, inferring node version from your codebase & installing your dependencies
26
+ - uses: codfish/actions/setup-node-and-install@v3
27
+
28
+ # Or if you want to be explicit
29
+ - uses: codfish/actions/setup-node-and-install@v3
30
+ with:
31
+ node-version: 24.4
32
+
33
+ - run: npm test
34
+ ```
35
+
36
+ The `node-version` input is optional. If not supplied, this action will attempt to resolve a version using, in order:
37
+
38
+ 1. `.node-version`, 2) `.nvmrc`, 3) `package.json` `volta.node`. If none are present, `actions/setup-node` runs without
39
+ an explicit version and will use its default behavior.
40
+
41
+ The `install-options` input is optional. If not supplied, the npm install commands will execute as defined without any
42
+ additional options.
43
+
44
+ **With `.nvmrc` file**
45
+
46
+ ```sh
47
+ # .nvmrc
48
+ v18.14.1
49
+ ```
50
+
51
+ ```yml
52
+ steps:
53
+ - uses: actions/checkout@v6
54
+ # will install Node v18.14.1
55
+ - uses: codfish/actions/setup-node-and-install@v3
56
+ - run: npm test
57
+ ```
58
+
59
+ **With `.node-version` file**
60
+
61
+ ```sh
62
+ # .node-version
63
+ 20.10.0
64
+ ```
65
+
66
+ ```yml
67
+ steps:
68
+ - uses: actions/checkout@v6
69
+ # will install Node v20.10.0
70
+ - uses: codfish/actions/setup-node-and-install@v3
71
+ - run: npm test
72
+ ```
73
+
74
+ ## Node Version Resolution Priority
75
+
76
+ When multiple version specification methods are present, the action uses this priority order:
77
+
78
+ 1. **Input parameter** (`node-version`) - highest priority
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
83
+
84
+ ## Inputs
85
+
86
+ <!-- start inputs -->
87
+
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 | `.` |
93
+ | `registry-url` | Optional registry URL to configure for publishing (e.g. "https://registry.npmjs.org/"). Creates .npmrc with NODE_AUTH_TOKEN placeholder. NOT recommended if using semantic-release (it handles auth independently). Only needed for publishing with manual npm publish or other non-semantic-release workflows. | No | - |
94
+ | `upgrade-npm` | Whether to upgrade npm to v11.5.1. This is required for OIDC trusted publishing but can be disabled if you want to shave off some run time and you are still using token-based authentication. | No | `true` |
95
+
96
+ <!-- end inputs -->
97
+
98
+ ## Package Manager Detection
99
+
100
+ The action automatically detects your package manager:
101
+
102
+ - **pnpm**: Detected when `pnpm-lock.yaml` exists
103
+ - **yarn**: Detected when `yarn.lock` exists
104
+ - **npm**: Detected when `package-lock.json` exists or as fallback
105
+
106
+ ## npm Version Upgrade
107
+
108
+ This action automatically upgrades npm to **v11** after Node.js setup (pinned to `^11.5.1`). This ensures:
109
+
110
+ - npm 11.5.1+ is available for **OIDC trusted publishing** support (required as of January 2026)
111
+ - Stable, predictable npm behavior across workflows
112
+ - Security fixes and improvements within the v11 release line
113
+ - No unexpected breaking changes from major version updates
114
+
115
+ The upgrade happens transparently and is logged in the workflow output. The version is pinned to prevent unexpected
116
+ breaking changes while still receiving patch and minor updates within v11.
117
+
118
+ ## Registry URL Configuration
119
+
120
+ The `registry-url` input configures npm authentication by creating a `.npmrc` file with a `NODE_AUTH_TOKEN` placeholder.
121
+ **In most cases, you should NOT set this parameter.**
122
+
123
+ ### When NOT to use registry-url (recommended)
124
+
125
+ **Skip this parameter if:**
126
+
127
+ - You're **only installing dependencies** (the primary use case for this action) - authentication is not needed for
128
+ public packages
129
+ - You're using **semantic-release** for publishing - it handles npm authentication independently and `registry-url` can
130
+ cause conflicts
131
+ ([semantic-release docs](https://semantic-release.gitbook.io/semantic-release/recipes/ci-configurations/github-actions#important-avoid-registry-url-in-setup-node))
132
+ - You're using **OIDC trusted publishing** with npm - the upgraded npm v11 handles this automatically
133
+
134
+ ### When to use registry-url
135
+
136
+ **Only set this parameter if:**
137
+
138
+ - You're publishing to npm using **manual `npm publish`** (not semantic-release)
139
+ - You need to authenticate to a **private npm registry**
140
+ - You're using **legacy token-based publishing** and need the `.npmrc` file created
141
+
142
+ ### Example with registry-url
143
+
144
+ ```yml
145
+ - uses: codfish/actions/setup-node-and-install@v3
146
+ with:
147
+ registry-url: 'https://registry.npmjs.org/'
148
+ env:
149
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
150
+
151
+ - run: npm publish
152
+ ```
153
+
154
+ ## Examples
155
+
156
+ ### With specific Node version
157
+
158
+ ```yml
159
+ - uses: codfish/actions/setup-node-and-install@v3
160
+ with:
161
+ node-version: '18'
162
+ ```
163
+
164
+ ### With pnpm in subdirectory
165
+
166
+ ```yml
167
+ - uses: codfish/actions/setup-node-and-install@v3
168
+ with:
169
+ working-directory: './frontend'
170
+ install-options: '--frozen-lockfile'
171
+ ```
172
+
173
+ ## Migrating
174
+
175
+ Replace multiple setup steps with this single action:
176
+
177
+ ```diff
178
+ - - uses: actions/setup-node@v4
179
+ - with:
180
+ - node-version-file: '.nvmrc'
181
+ - cache: 'npm'
182
+ - - run: npm ci --prefer-offline --no-audit
183
+ + - uses: codfish/actions/setup-node-and-install@v3
184
+ ```