Files
actions/node-setup/action.yml
Ismo Vuorinen 74968d942f chore: update action references for release v2025.10.26 (#312)
This commit updates all internal action references to point to the current
commit SHA in preparation for release v2025.10.26.
2025-10-27 00:00:02 +02:00

403 lines
15 KiB
YAML

# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
# permissions:
# - (none required) # Setup action, no repository writes
---
name: Node Setup
description: 'Sets up Node.js env with advanced version management, caching, and tooling.'
author: 'Ismo Vuorinen'
branding:
icon: server
color: green
inputs:
default-version:
description: 'Default Node.js version to use if no configuration file is found.'
required: false
default: '22'
package-manager:
description: 'Node.js package manager to use (npm, yarn, pnpm, bun, auto)'
required: false
default: 'auto'
registry-url:
description: 'Custom NPM registry URL'
required: false
default: 'https://registry.npmjs.org'
token:
description: 'Auth token for private registry'
required: false
cache:
description: 'Enable dependency caching'
required: false
default: 'true'
install:
description: 'Automatically install dependencies'
required: false
default: 'true'
node-mirror:
description: 'Custom Node.js binary mirror'
required: false
force-version:
description: 'Force specific Node.js version regardless of config files'
required: false
max-retries:
description: 'Maximum number of retry attempts for package manager operations'
required: false
default: '3'
outputs:
node-version:
description: 'Installed Node.js version'
value: ${{ steps.setup.outputs.node-version }}
package-manager:
description: 'Selected package manager'
value: ${{ steps.package-manager-resolution.outputs.final-package-manager }}
cache-hit:
description: 'Indicates if there was a cache hit'
value: ${{ steps.deps-cache.outputs.cache-hit }}
node-path:
description: 'Path to Node.js installation'
value: ${{ steps.setup.outputs.node-path }}
esm-support:
description: 'Whether ESM modules are supported'
value: ${{ steps.package-manager-resolution.outputs.esm-support }}
typescript-support:
description: 'Whether TypeScript is configured'
value: ${{ steps.package-manager-resolution.outputs.typescript-support }}
detected-frameworks:
description: 'Comma-separated list of detected frameworks'
value: ${{ steps.package-manager-resolution.outputs.detected-frameworks }}
runs:
using: composite
steps:
- name: Validate Inputs
id: validate
shell: bash
env:
DEFAULT_VERSION: ${{ inputs.default-version }}
FORCE_VERSION: ${{ inputs.force-version }}
PACKAGE_MANAGER: ${{ inputs.package-manager }}
REGISTRY_URL: ${{ inputs.registry-url }}
NODE_MIRROR: ${{ inputs.node-mirror }}
MAX_RETRIES: ${{ inputs.max-retries }}
CACHE: ${{ inputs.cache }}
INSTALL: ${{ inputs.install }}
AUTH_TOKEN: ${{ inputs.token }}
run: |
set -euo pipefail
# Validate default-version format
if [[ -n "$DEFAULT_VERSION" ]]; then
if ! [[ "$DEFAULT_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then
echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X or X.Y or X.Y.Z (e.g., 22, 20.9, 18.17.1)"
exit 1
fi
# Check for reasonable version range (prevent malicious inputs)
major_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f1)
if [ "$major_version" -lt 14 ] || [ "$major_version" -gt 30 ]; then
echo "::error::Invalid default-version: '$DEFAULT_VERSION'. Node.js major version should be between 14 and 30"
exit 1
fi
fi
# Validate force-version format if provided
if [[ -n "$FORCE_VERSION" ]]; then
if ! [[ "$FORCE_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then
echo "::error::Invalid force-version format: '$FORCE_VERSION'. Expected format: X or X.Y or X.Y.Z (e.g., 22, 20.9, 18.17.1)"
exit 1
fi
# Check for reasonable version range
major_version=$(echo "$FORCE_VERSION" | cut -d'.' -f1)
if [ "$major_version" -lt 14 ] || [ "$major_version" -gt 30 ]; then
echo "::error::Invalid force-version: '$FORCE_VERSION'. Node.js major version should be between 14 and 30"
exit 1
fi
fi
# Validate package-manager
case "$PACKAGE_MANAGER" in
"npm"|"yarn"|"pnpm"|"bun"|"auto")
# Valid package managers
;;
*)
echo "::error::Invalid package-manager: '$PACKAGE_MANAGER'. Must be one of: npm, yarn, pnpm, bun, auto"
exit 1
;;
esac
# Validate registry-url format (basic URL validation)
if [[ "$REGISTRY_URL" != "https://"* ]] && [[ "$REGISTRY_URL" != "http://"* ]]; then
echo "::error::Invalid registry-url: '$REGISTRY_URL'. Must be a valid HTTP/HTTPS URL"
exit 1
fi
# Validate node-mirror format if provided
if [[ -n "$NODE_MIRROR" ]]; then
if [[ "$NODE_MIRROR" != "https://"* ]] && [[ "$NODE_MIRROR" != "http://"* ]]; then
echo "::error::Invalid node-mirror: '$NODE_MIRROR'. Must be a valid HTTP/HTTPS URL"
exit 1
fi
fi
# Validate max retries (positive integer with reasonable upper limit)
if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then
echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10"
exit 1
fi
# Validate boolean inputs
if [[ "$CACHE" != "true" ]] && [[ "$CACHE" != "false" ]]; then
echo "::error::Invalid cache value: '$CACHE'. Must be 'true' or 'false'"
exit 1
fi
if [[ "$INSTALL" != "true" ]] && [[ "$INSTALL" != "false" ]]; then
echo "::error::Invalid install value: '$INSTALL'. Must be 'true' or 'false'"
exit 1
fi
# Validate auth token format if provided (basic check for NPM tokens)
if [[ -n "$AUTH_TOKEN" ]]; then
if [[ "$AUTH_TOKEN" == *";"* ]] || [[ "$AUTH_TOKEN" == *"&&"* ]] || [[ "$AUTH_TOKEN" == *"|"* ]]; then
echo "::error::Invalid token format: command injection patterns not allowed"
exit 1
fi
fi
echo "Input validation completed successfully"
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token || github.token }}
- name: Parse Node.js Version
id: version
uses: ivuorinen/actions/version-file-parser@e2222afff180ee77f330ef4325f60d6e85477c01
with:
language: 'node'
tool-versions-key: 'nodejs'
dockerfile-image: 'node'
version-file: '.nvmrc'
validation-regex: '^[0-9]+(\.[0-9]+)*$'
default-version: ${{ inputs.force-version != '' && inputs.force-version || inputs.default-version }}
- name: Resolve Package Manager
id: package-manager-resolution
shell: bash
env:
INPUT_PM: ${{ inputs.package-manager }}
DETECTED_PM: ${{ steps.version.outputs.package-manager }}
run: |
set -euo pipefail
input_pm="$INPUT_PM"
detected_pm="$DETECTED_PM"
final_pm=""
if [ "$input_pm" = "auto" ]; then
if [ -n "$detected_pm" ]; then
final_pm="$detected_pm"
echo "Auto-detected package manager: $final_pm"
else
final_pm="npm"
echo "No package manager detected, using default: $final_pm"
fi
else
final_pm="$input_pm"
echo "Using specified package manager: $final_pm"
fi
echo "final-package-manager=$final_pm" >> $GITHUB_OUTPUT
echo "Final package manager: $final_pm"
# Node.js feature detection
echo "Detecting Node.js features..."
# Detect ESM support
esm_support="false"
if [ -f package.json ] && command -v jq >/dev/null 2>&1; then
pkg_type=$(jq -r '.type // "commonjs"' package.json 2>/dev/null)
if [ "$pkg_type" = "module" ]; then
esm_support="true"
fi
fi
echo "esm-support=$esm_support" >> $GITHUB_OUTPUT
echo "ESM support: $esm_support"
# Detect TypeScript
typescript_support="false"
if [ -f tsconfig.json ] || [ -f package.json ]; then
if [ -f tsconfig.json ]; then
typescript_support="true"
elif command -v jq >/dev/null 2>&1; then
if jq -e '.devDependencies.typescript or .dependencies.typescript' package.json >/dev/null 2>&1; then
typescript_support="true"
fi
fi
fi
echo "typescript-support=$typescript_support" >> $GITHUB_OUTPUT
echo "TypeScript support: $typescript_support"
# Detect frameworks
frameworks=""
if [ -f package.json ] && command -v jq >/dev/null 2>&1; then
detected_frameworks=()
if jq -e '.dependencies.next or .devDependencies.next' package.json >/dev/null 2>&1; then
detected_frameworks+=("next")
fi
if jq -e '.dependencies.react or .devDependencies.react' package.json >/dev/null 2>&1; then
detected_frameworks+=("react")
fi
if jq -e '.dependencies.vue or .devDependencies.vue' package.json >/dev/null 2>&1; then
detected_frameworks+=("vue")
fi
if jq -e '.dependencies.svelte or .devDependencies.svelte' package.json >/dev/null 2>&1; then
detected_frameworks+=("svelte")
fi
if jq -e '.dependencies."@angular/core" or .devDependencies."@angular/core"' package.json >/dev/null 2>&1; then
detected_frameworks+=("angular")
fi
if [ ${#detected_frameworks[@]} -gt 0 ]; then
frameworks=$(IFS=','; echo "${detected_frameworks[*]}")
fi
fi
echo "detected-frameworks=$frameworks" >> $GITHUB_OUTPUT
echo "Detected frameworks: $frameworks"
- name: Setup Node.js
id: setup
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: ${{ steps.version.outputs.detected-version }}
registry-url: ${{ inputs.registry-url }}
cache: false
- name: Enable Corepack
id: corepack
shell: bash
run: |
set -euo pipefail
echo "Enabling Corepack for package manager management..."
corepack enable
echo "✅ Corepack enabled successfully"
- name: Set Auth Token
if: inputs.token != ''
shell: bash
env:
TOKEN: ${{ inputs.token }}
run: |
# Sanitize token by removing newlines to prevent env var injection
sanitized_token="$(echo "$TOKEN" | tr -d '\n\r')"
printf 'NODE_AUTH_TOKEN=%s\n' "$sanitized_token" >> "$GITHUB_ENV"
- name: Cache Dependencies
if: inputs.cache == 'true'
id: deps-cache
uses: ivuorinen/actions/common-cache@e2222afff180ee77f330ef4325f60d6e85477c01
with:
type: 'npm'
paths: '~/.npm,~/.yarn/cache,~/.pnpm-store,~/.bun/install/cache,node_modules'
key-prefix: 'node-${{ steps.version.outputs.detected-version }}-${{ steps.package-manager-resolution.outputs.final-package-manager }}'
key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb,.yarnrc.yml'
restore-keys: '${{ runner.os }}-node-${{ steps.version.outputs.detected-version }}-${{ steps.package-manager-resolution.outputs.final-package-manager }}-'
- name: Install Package Managers
if: inputs.install == 'true' && steps.deps-cache.outputs.cache-hit != 'true'
shell: bash
env:
PACKAGE_MANAGER: ${{ steps.package-manager-resolution.outputs.final-package-manager }}
run: |
set -euo pipefail
package_manager="$PACKAGE_MANAGER"
echo "Setting up package manager: $package_manager"
case "$package_manager" in
"pnpm")
echo "Installing PNPM via Corepack..."
corepack prepare pnpm@latest --activate
echo "✅ PNPM installed successfully"
;;
"yarn")
echo "Installing Yarn via Corepack..."
corepack prepare yarn@stable --activate
echo "✅ Yarn installed successfully"
;;
"bun")
# Bun installation handled by separate step below
echo "Bun will be installed via official setup-bun action"
;;
"npm")
echo "Using built-in NPM"
;;
*)
echo "::warning::Unknown package manager: $package_manager, using NPM"
;;
esac
- name: Setup Bun
if: inputs.install == 'true' && steps.package-manager-resolution.outputs.final-package-manager == 'bun'
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
with:
bun-version: latest
- name: Export Package Manager to Environment
if: inputs.install == 'true' && steps.deps-cache.outputs.cache-hit != 'true'
shell: bash
env:
PACKAGE_MANAGER: ${{ steps.package-manager-resolution.outputs.final-package-manager }}
run: |
# Sanitize package manager by removing newlines to prevent env var injection
sanitized_pm="$(echo "$PACKAGE_MANAGER" | tr -d '\n\r')"
printf 'PACKAGE_MANAGER=%s\n' "$sanitized_pm" >> "$GITHUB_ENV"
- name: Install Dependencies
if: inputs.install == 'true' && steps.deps-cache.outputs.cache-hit != 'true'
uses: ivuorinen/actions/common-retry@e2222afff180ee77f330ef4325f60d6e85477c01
with:
command: |
package_manager="$PACKAGE_MANAGER"
echo "Installing dependencies using $package_manager..."
case "$package_manager" in
"pnpm")
pnpm install --frozen-lockfile
;;
"yarn")
# Check for Yarn Berry/PnP configuration
if [ -f ".yarnrc.yml" ]; then
echo "Detected Yarn Berry configuration"
yarn install --immutable
else
echo "Using Yarn Classic"
yarn install --frozen-lockfile
fi
;;
"bun")
bun install
;;
"npm"|*)
npm ci
;;
esac
echo "✅ Dependencies installed successfully"
max-retries: ${{ inputs.max-retries }}
description: 'Installing Node.js dependencies'
- name: Set Final Outputs
shell: bash
env:
NODE_VERSION: ${{ steps.version.outputs.detected-version }}
PACKAGE_MANAGER: ${{ steps.package-manager-resolution.outputs.final-package-manager }}
run: |-
{
echo "node-version=$NODE_VERSION"
echo "package-manager=$PACKAGE_MANAGER"
echo "node-path=$(which node)"
} >> $GITHUB_OUTPUT