Files
actions/language-version-detect/action.yml
Ismo Vuorinen d22d5c7ee9 refactor: make language-version-detect self-contained
Inline version-file-parser logic into language-version-detect to eliminate
external dependency and make the action fully self-contained.

Changes:
- Replace external call to version-file-parser with inline parsing script
- Use POSIX sh for maximum compatibility
- Streamlined version detection logic focusing on 4 supported languages
- Priority: .tool-versions > Dockerfile > devcontainer.json > version files > config files > default

Benefits:
- No external action dependencies
- Faster execution (no action setup overhead)
- Easier to maintain and test
- Reduced surface area for security issues

The action now handles all version detection inline while maintaining
the same outputs and functionality.
2025-11-20 08:59:23 +02:00

389 lines
14 KiB
YAML

# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
# permissions:
# - contents: read # Required for reading version files
---
name: Language Version Detect
description: 'Detects language version from project configuration files with support for PHP, Python, Go, and .NET.'
author: 'Ismo Vuorinen'
branding:
icon: code
color: blue
inputs:
language:
description: 'Language to detect version for (php, python, go, dotnet)'
required: true
default-version:
description: 'Default version to use if no version is detected'
required: false
token:
description: 'GitHub token for authentication'
required: false
default: ''
outputs:
detected-version:
description: 'Detected or default language version'
value: ${{ steps.parse-version.outputs.detected-version }}
package-manager:
description: 'Detected package manager (python: pip/poetry/pipenv, php: composer)'
value: ${{ steps.parse-version.outputs.package-manager }}
runs:
using: composite
steps:
- name: Validate Inputs
id: validate
shell: sh
env:
LANGUAGE: ${{ inputs.language }}
DEFAULT_VERSION: ${{ inputs.default-version }}
run: |
set -eu
# Validate language parameter
case "$LANGUAGE" in
php|python|go|dotnet)
;;
*)
echo "::error::Invalid language: '$LANGUAGE'. Must be one of: php, python, go, dotnet"
exit 1
;;
esac
# Set default version if not provided
if [ -z "$DEFAULT_VERSION" ]; then
case "$LANGUAGE" in
php)
default="8.4"
;;
python)
default="3.12"
;;
go)
default="1.21"
;;
dotnet)
default="7.0"
;;
esac
printf 'default_version=%s\n' "$default" >> "$GITHUB_OUTPUT"
else
printf 'default_version=%s\n' "$DEFAULT_VERSION" >> "$GITHUB_OUTPUT"
fi
# Validate version format for specified language
version="${DEFAULT_VERSION:-$default}"
case "$LANGUAGE" in
php)
# Validate PHP version format (X.Y or X.Y.Z)
case "$version" in
[0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*)
;;
*)
echo "::error::Invalid PHP version format: '$version'. Expected format: X.Y or X.Y.Z (e.g., 8.4, 8.3.1)"
exit 1
;;
esac
# Check for reasonable PHP version range
major_version=$(echo "$version" | cut -d'.' -f1)
if [ "$major_version" -lt 7 ] || [ "$major_version" -gt 9 ]; then
echo "::error::Invalid PHP version: '$version'. PHP major version should be between 7 and 9"
exit 1
fi
# Additional validation for PHP 8.x minor versions
if [ "$major_version" -eq 8 ]; then
minor_version=$(echo "$version" | cut -d'.' -f2)
if [ "$minor_version" -gt 4 ]; then
echo "::error::Invalid PHP 8 version: '$version'. PHP 8 minor version should be between 0 and 4"
exit 1
fi
fi
;;
python)
# Validate Python version format
case "$version" in
[0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*)
;;
*)
echo "::error::Invalid Python version format: '$version'. Expected format: X.Y or X.Y.Z (e.g., 3.12, 3.11.5)"
exit 1
;;
esac
# Check Python major version
major_version=$(echo "$version" | cut -d'.' -f1)
if [ "$major_version" -ne 3 ]; then
echo "::error::Invalid Python version: '$version'. Python major version should be 3"
exit 1
fi
# Check Python minor version range
minor_version=$(echo "$version" | cut -d'.' -f2)
if [ "$minor_version" -lt 8 ] || [ "$minor_version" -gt 15 ]; then
echo "::error::Invalid Python version: '$version'. Python 3 minor version should be between 8 and 15"
exit 1
fi
;;
go)
# Validate Go version format
case "$version" in
[0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*)
;;
*)
echo "::error::Invalid Go version format: '$version'. Expected format: X.Y or X.Y.Z (e.g., 1.21, 1.21.5)"
exit 1
;;
esac
# Check Go major version (must be 1)
major_version=$(echo "$version" | cut -d'.' -f1)
if [ "$major_version" -ne 1 ]; then
echo "::error::Invalid Go version: '$version'. Go major version should be 1"
exit 1
fi
# Check Go minor version range
minor_version=$(echo "$version" | cut -d'.' -f2)
if [ "$minor_version" -lt 16 ] || [ "$minor_version" -gt 30 ]; then
echo "::error::Invalid Go version: '$version'. Go minor version should be between 16 and 30"
exit 1
fi
;;
dotnet)
# Validate .NET version format
case "$version" in
[0-9]* | [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*)
;;
*)
echo "::error::Invalid .NET version format: '$version'. Expected format: X, X.Y, or X.Y.Z (e.g., 7, 7.0, 7.0.1)"
exit 1
;;
esac
# Check .NET major version range
major_version=$(echo "$version" | cut -d'.' -f1)
if [ "$major_version" -lt 3 ] || [ "$major_version" -gt 20 ]; then
echo "::error::Invalid .NET version: '$version'. .NET major version should be between 3 and 20"
exit 1
fi
;;
esac
echo "Input validation completed successfully for $LANGUAGE version $version"
- name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
with:
token: ${{ inputs.token || github.token }}
- name: Parse Language Version
id: parse-version
shell: sh
env:
LANGUAGE: ${{ inputs.language }}
DEFAULT_VERSION: ${{ steps.validate.outputs.default_version || inputs.default-version }}
run: |
set -eu
# Map language to tool-versions key and dockerfile image
case "$LANGUAGE" in
go)
TOOL_VERSIONS_KEY="golang"
DOCKERFILE_IMAGE="golang"
VERSION_FILE=".go-version"
;;
php)
TOOL_VERSIONS_KEY="php"
DOCKERFILE_IMAGE="php"
VERSION_FILE=".php-version"
;;
python)
TOOL_VERSIONS_KEY="python"
DOCKERFILE_IMAGE="python"
VERSION_FILE=".python-version"
;;
dotnet)
TOOL_VERSIONS_KEY="dotnet"
DOCKERFILE_IMAGE="dotnet"
VERSION_FILE=""
;;
esac
# Function to validate version format
validate_version() {
version=$1
# Use case pattern matching for POSIX compatibility
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'
}
# Initialize outputs
printf 'detected-version=\n' >> "$GITHUB_OUTPUT"
printf 'package-manager=\n' >> "$GITHUB_OUTPUT"
detected_version=""
detected_package_manager=""
# Parse .tool-versions file
if [ -f .tool-versions ]; then
echo "Checking .tool-versions for $TOOL_VERSIONS_KEY..." >&2
version=$(awk "/^$TOOL_VERSIONS_KEY[[: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 $LANGUAGE 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 $DOCKERFILE_IMAGE..." >&2
version=$(grep -iF "FROM" Dockerfile | grep -F "$DOCKERFILE_IMAGE:" | head -1 | \
sed -n "s/.*$DOCKERFILE_IMAGE:\([0-9]\+\(\.[0-9]\+\)*\)\(-[^:]*\)\?.*/\1/p" || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found $LANGUAGE 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 $DOCKERFILE_IMAGE..." >&2
if command -v jq >/dev/null 2>&1; then
version=$(jq -r '.image // empty' .devcontainer/devcontainer.json 2>/dev/null | sed -n "s/.*$DOCKERFILE_IMAGE:\([0-9]\+\(\.[0-9]\+\)*\)\(-[^:]*\)\?.*/\1/p" || echo "")
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found $LANGUAGE version in devcontainer: $version" >&2
detected_version="$version"
fi
fi
fi
fi
# Parse language-specific version file
if [ -z "$detected_version" ] && [ -n "$VERSION_FILE" ] && [ -f "$VERSION_FILE" ]; then
echo "Checking $VERSION_FILE..." >&2
version=$(tr -d '\r' < "$VERSION_FILE" | head -1)
if [ -n "$version" ]; then
version=$(clean_version "$version")
if validate_version "$version"; then
echo "Found $LANGUAGE version in $VERSION_FILE: $version" >&2
detected_version="$version"
fi
fi
fi
# Parse language-specific configuration files
if [ -z "$detected_version" ]; then
case "$LANGUAGE" in
php)
# Check composer.json
if [ -f composer.json ] && command -v jq >/dev/null 2>&1; then
version=$(jq -r '.require.php // empty' composer.json 2>/dev/null | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p')
if [ -z "$version" ]; then
version=$(jq -r '.config.platform.php // empty' composer.json 2>/dev/null | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p')
fi
if [ -n "$version" ] && validate_version "$version"; then
echo "Found PHP version in composer.json: $version" >&2
detected_version="$version"
fi
fi
# Detect package manager
if [ -f composer.json ]; then
detected_package_manager="composer"
fi
;;
python)
# Check pyproject.toml
if [ -f pyproject.toml ]; then
if grep -q '^\[project\]' pyproject.toml; then
version=$(grep -A 20 '^\[project\]' pyproject.toml | grep -E '^\s*requires-python[[:space:]]*=' | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p' | head -1)
if [ -n "$version" ] && validate_version "$version"; then
echo "Found Python version in pyproject.toml: $version" >&2
detected_version="$version"
fi
fi
fi
# Detect package manager
if [ -f pyproject.toml ] && grep -q '\[tool\.poetry\]' pyproject.toml; then
detected_package_manager="poetry"
elif [ -f Pipfile ]; then
detected_package_manager="pipenv"
else
detected_package_manager="pip"
fi
;;
go)
# Check go.mod
if [ -f go.mod ]; then
version=$(grep -E '^go[[:space:]]+[0-9]' go.mod | awk '{print $2}' | head -1 || echo "")
if [ -n "$version" ] && validate_version "$version"; then
echo "Found Go version in go.mod: $version" >&2
detected_version="$version"
fi
detected_package_manager="go"
fi
;;
dotnet)
# Check global.json
if [ -f global.json ] && command -v jq >/dev/null 2>&1; then
version=$(jq -r '.sdk.version // empty' global.json 2>/dev/null || echo "")
if [ -n "$version" ] && validate_version "$version"; then
echo "Found .NET version in global.json: $version" >&2
detected_version="$version"
fi
fi
detected_package_manager="dotnet"
;;
esac
fi
# Use default version if nothing detected
if [ -z "$detected_version" ]; then
if [ -n "$DEFAULT_VERSION" ]; then
detected_version="$DEFAULT_VERSION"
echo "Using default $LANGUAGE version: $detected_version" >&2
else
echo "No $LANGUAGE version detected and no default provided" >&2
fi
fi
# Set outputs
if [ -n "$detected_version" ]; then
printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT"
echo "Final detected $LANGUAGE version: $detected_version" >&2
fi
if [ -n "$detected_package_manager" ]; then
printf 'package-manager=%s\n' "$detected_package_manager" >> "$GITHUB_OUTPUT"
echo "Detected package manager: $detected_package_manager" >&2
fi