mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-31 03:40:54 +00:00
Replace version-file-parser dependency with ~140 lines of inline detection: - Detect from .nvmrc, package.json, .tool-versions, Dockerfile, devcontainer.json - Detect package manager from lock files (bun, pnpm, yarn, npm) - Use POSIX sh with set -eu for portability - Include validate_version() and clean_version() helper functions - Add diagnostic messages when jq unavailable Detection priority: .nvmrc > package.json > .tool-versions > Dockerfile > devcontainer > default Reduces external dependencies and improves initialization performance.
371 lines
13 KiB
YAML
371 lines
13 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 environment with version detection and package manager configuration.'
|
|
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
|
|
node-mirror:
|
|
description: 'Custom Node.js binary mirror'
|
|
required: false
|
|
force-version:
|
|
description: 'Force specific Node.js version regardless of config files'
|
|
required: false
|
|
|
|
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 }}
|
|
node-path:
|
|
description: 'Path to Node.js installation'
|
|
value: ${{ steps.final-outputs.outputs.node-path }}
|
|
|
|
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 }}
|
|
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 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@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
|
|
with:
|
|
token: ${{ inputs.token || github.token }}
|
|
|
|
- name: Detect Node.js Version
|
|
id: version
|
|
shell: sh
|
|
env:
|
|
DEFAULT_VERSION: "${{ inputs.force-version != '' && inputs.force-version || inputs.default-version }}"
|
|
run: |
|
|
set -eu
|
|
|
|
# Function to validate version format
|
|
validate_version() {
|
|
version=$1
|
|
case "$version" in
|
|
[0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Function to clean version string
|
|
clean_version() {
|
|
printf '%s' "$1" | sed 's/^[vV]//' | tr -d ' \n\r'
|
|
}
|
|
|
|
detected_version=""
|
|
detected_package_manager=""
|
|
|
|
# Parse .nvmrc file (highest priority for Node.js)
|
|
if [ -f .nvmrc ]; then
|
|
echo "Checking .nvmrc..." >&2
|
|
version=$(tr -d '\r' < .nvmrc | head -1)
|
|
if [ -n "$version" ]; then
|
|
version=$(clean_version "$version")
|
|
if validate_version "$version"; then
|
|
echo "Found Node.js version in .nvmrc: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Parse package.json
|
|
if [ -z "$detected_version" ] && [ -f package.json ]; then
|
|
echo "Checking package.json for Node.js version..." >&2
|
|
if command -v jq >/dev/null 2>&1; then
|
|
version=$(jq -r '.engines.node // empty' package.json 2>/dev/null | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p')
|
|
if [ -n "$version" ]; then
|
|
version=$(clean_version "$version")
|
|
if validate_version "$version"; then
|
|
echo "Found Node.js version in package.json: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
else
|
|
echo "jq not found; skipping package.json version parsing" >&2
|
|
fi
|
|
fi
|
|
|
|
# Parse .tool-versions file
|
|
if [ -z "$detected_version" ] && [ -f .tool-versions ]; then
|
|
echo "Checking .tool-versions for nodejs..." >&2
|
|
version=$(awk '/^nodejs[[:space:]]/ {gsub(/#.*/, ""); print $2; exit}' .tool-versions 2>/dev/null || echo "")
|
|
if [ -n "$version" ]; then
|
|
version=$(clean_version "$version")
|
|
if validate_version "$version"; then
|
|
echo "Found Node.js version in .tool-versions: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Parse Dockerfile
|
|
if [ -z "$detected_version" ] && [ -f Dockerfile ]; then
|
|
echo "Checking Dockerfile for node..." >&2
|
|
version=$(grep -iF "FROM" Dockerfile | grep -F "node:" | head -1 | \
|
|
sed -n -E "s/.*node:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "")
|
|
if [ -n "$version" ]; then
|
|
version=$(clean_version "$version")
|
|
if validate_version "$version"; then
|
|
echo "Found Node.js version in Dockerfile: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Parse devcontainer.json
|
|
if [ -z "$detected_version" ] && [ -f .devcontainer/devcontainer.json ]; then
|
|
echo "Checking devcontainer.json for node..." >&2
|
|
if command -v jq >/dev/null 2>&1; then
|
|
version=$(jq -r '.image // empty' .devcontainer/devcontainer.json 2>/dev/null | sed -n -E "s/.*node:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "")
|
|
if [ -n "$version" ]; then
|
|
version=$(clean_version "$version")
|
|
if validate_version "$version"; then
|
|
echo "Found Node.js version in devcontainer: $version" >&2
|
|
detected_version="$version"
|
|
fi
|
|
fi
|
|
else
|
|
echo "jq not found; skipping devcontainer.json parsing" >&2
|
|
fi
|
|
fi
|
|
|
|
# Detect package manager
|
|
if [ -f bun.lockb ]; then
|
|
detected_package_manager="bun"
|
|
elif [ -f pnpm-lock.yaml ]; then
|
|
detected_package_manager="pnpm"
|
|
elif [ -f yarn.lock ]; then
|
|
detected_package_manager="yarn"
|
|
elif [ -f package-lock.json ]; then
|
|
detected_package_manager="npm"
|
|
elif [ -f package.json ]; then
|
|
if command -v jq >/dev/null 2>&1; then
|
|
pkg_manager=$(jq -r '.packageManager // empty' package.json 2>/dev/null | sed 's/@.*//')
|
|
if [ -n "$pkg_manager" ]; then
|
|
detected_package_manager="$pkg_manager"
|
|
else
|
|
detected_package_manager="npm"
|
|
fi
|
|
else
|
|
detected_package_manager="npm"
|
|
fi
|
|
else
|
|
detected_package_manager="npm"
|
|
fi
|
|
|
|
# Use default version if nothing detected
|
|
if [ -z "$detected_version" ]; then
|
|
detected_version="$DEFAULT_VERSION"
|
|
echo "Using default Node.js version: $detected_version" >&2
|
|
fi
|
|
|
|
# Set outputs
|
|
printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT"
|
|
printf 'package-manager=%s\n' "$detected_package_manager" >> "$GITHUB_OUTPUT"
|
|
echo "Final detected Node.js version: $detected_version" >&2
|
|
echo "Detected package manager: $detected_package_manager" >&2
|
|
|
|
- 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"
|
|
|
|
- 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 }}
|
|
|
|
- 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: Setup Package Manager
|
|
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: steps.package-manager-resolution.outputs.final-package-manager == 'bun'
|
|
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
|
with:
|
|
bun-version: latest
|
|
|
|
- name: Set Final Outputs
|
|
id: final-outputs
|
|
shell: bash
|
|
run: |
|
|
echo "node-path=$(which node)" >> $GITHUB_OUTPUT
|