From a88bb34369a1cf2fec13d81c448f857f36a8539f Mon Sep 17 00:00:00 2001 From: Ismo Vuorinen Date: Fri, 21 Nov 2025 15:46:33 +0200 Subject: [PATCH] feature: inline actions (#359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. * refactor: inline Go detection into go-build Make go-build self-contained by inlining Go version detection logic, eliminating dependency on language-version-detect action. Changes: - Replace external language-version-detect call with inline script (~102 lines) - Detect Go version from: .tool-versions, Dockerfile, devcontainer.json, .go-version, go.mod - Use POSIX sh for maximum compatibility - Maintain same output contract (detected-version) - Fix sed to use POSIX-compliant extended regex (-E flag) - Fix go.mod parsing to clean version before validation Benefits: - Faster execution (no external action overhead) - Self-contained action - Reduced attack surface - Template for other language actions This is part of Phase 1 of the inlining campaign to improve performance and reduce internal dependencies. * refactor: inline .NET detection into csharp actions Replace language-version-detect dependency with inline version detection for all three C# actions (csharp-build, csharp-lint-check, csharp-publish). Detection logic checks (in priority order): - .tool-versions file (dotnet key) - Dockerfile (FROM dotnet: image) - devcontainer.json (dotnet: image) - global.json (.sdk.version field) Implementation details: - POSIX sh compliant with `set -eu` - Validates version format: X, X.Y, or X.Y.Z - Normalizes versions: strips 'v' prefix, whitespace, line endings - Uses `sed -E` for portable extended regex - Conditional jq usage with diagnostic messages - Maintains output contract (detected-version) Fixed issues from code review: - devcontainer.json sed regex: malformed wildcard ('. */' → '.*') - Dockerfile sed regex: removed unintended leading space (' \1' → '\1') - Added stderr diagnostics when jq is not found - Applied fixes to all three actions for consistency Changes: - csharp-build: ~100 lines of inline detection + jq diagnostics - csharp-lint-check: ~100 lines of inline detection + jq diagnostics - csharp-publish: ~100 lines of inline detection + jq diagnostics - All READMEs regenerated with action-docs Benefits: - Eliminates external dependency for .NET version detection - Reduces action initialization time - Improved debugging (diagnostic messages, all logic in one file) - Consistent with go-build pattern * refactor: inline Python detection into python-lint-fix Replace language-version-detect dependency with inline version detection for the Python linting action. Detection logic checks (in priority order): - .tool-versions file (python key) - Dockerfile (FROM python: image) - devcontainer.json (python: image) - .python-version file - pyproject.toml (requires-python field) Implementation details: - POSIX sh compliant with `set -eu` - Validates version format: X.Y or X.Y.Z - Normalizes versions: strips 'v' prefix, whitespace, line endings - Uses `sed -E` for portable extended regex (Dockerfile/devcontainer) - Uses basic sed for pyproject.toml (POSIX-compatible backslash escapes) - Conditional jq usage with diagnostic messages - Maintains output contract (detected-version) Changes: - python-lint-fix: ~110 lines of inline detection + jq diagnostics - README regenerated with action-docs Benefits: - Eliminates external dependency for Python version detection - Reduces action initialization time - Improved debugging (diagnostic messages, all logic in one file) - Consistent with go-build and csharp pattern * refactor: inline PHP detection into php-laravel-phpunit Replace language-version-detect dependency with inline version detection for the Laravel PHPUnit testing action. Detection logic checks (in priority order): - .tool-versions file (php key) - Dockerfile (FROM php: image) - devcontainer.json (php: image) - .php-version file - composer.json (require.php or config.platform.php fields) Implementation details: - POSIX sh compliant with `set -eu` - Validates version format: X.Y or X.Y.Z - Normalizes versions: strips 'v' prefix, whitespace, line endings - Uses `sed -E` for portable extended regex (Dockerfile/devcontainer) - Uses basic sed for composer.json (POSIX-compatible backslash escapes) - Conditional jq usage with diagnostic messages - Maintains output contract (detected-version) Changes: - php-laravel-phpunit: ~115 lines of inline detection + jq diagnostics - README regenerated with action-docs Benefits: - Eliminates external dependency for PHP version detection - Reduces action initialization time - Improved debugging (diagnostic messages, all logic in one file) - Consistent with go-build, csharp, and python-lint-fix pattern * refactor: inline Node.js version detection into node-setup 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. * refactor: remove deprecated version-file-parser action Remove version-file-parser after successful inlining into node-setup: - Delete version-file-parser action directory - Delete version-file-parser unit and integration tests - Remove version-file-parser references from spec_helper.sh - Remove version-file-parser path trigger from node-setup-test.yml - Regenerate action catalog (29 actions, down from 30) All version detection functionality now inlined into individual actions: - go-build: Go version detection - csharp-build/csharp-lint-check/csharp-publish: .NET version detection - python-lint-fix: Python version detection - php-laravel-phpunit: PHP version detection - node-setup: Node.js version detection and package manager detection Reduces external dependencies and improves initialization performance across all actions. * refactor: inline language-version-detect in pr-lint Inline version detection for PHP, Python, and Go directly into pr-lint to eliminate dependency on language-version-detect action and improve initialization performance. Changes: - PHP detection: .tool-versions, Dockerfile, devcontainer.json, .php-version, composer.json (default: 8.4) - Python detection: .tool-versions, Dockerfile, devcontainer.json, .python-version, pyproject.toml (default: 3.11) - Go detection: .tool-versions, Dockerfile, devcontainer.json, .go-version, go.mod (default: 1.24) All detection logic follows POSIX sh standard with set -eu and uses validate_version() and clean_version() helper functions for consistency. * docs: deprecate language-version-detect action Mark language-version-detect as deprecated now that all internal usages have been inlined. Inline version detection provides better performance by eliminating action initialization overhead. Changes: - Add DEPRECATED notice to action.yml description and metadata - Add deprecation warning banner to README with migration guidance - Reference existing actions with inline detection patterns Users should migrate to inlining version detection logic directly into their actions rather than using this composite action. See pr-lint, php-laravel-phpunit, python-lint-fix, and go-build for examples. This action will be removed in a future release. * refactor(go): remove redundant caching from Go actions Remove redundant common-cache usage in Go actions since setup-go with cache:true already provides comprehensive caching. Changes: - go-build: Removed duplicate common-cache step (setup-go caches ~/go/pkg/mod and ~/.cache/go-build automatically) - go-lint: Removed redundant ~/.cache/go-build from cache paths (kept ~/.cache/golangci-lint as it's linter-specific and not covered by setup-go) Performance improvements: - Eliminates duplicate caching operations - Reduces action initialization overhead - setup-go's native caching is more efficient and maintained setup-go with cache:true caches: - ~/go/pkg/mod (Go modules) - ~/.cache/go-build (Go build cache) * refactor(python): migrate to native setup-python caching Replace common-cache with native caching in Python actions for better performance and maintainability. python-lint-fix changes: - Add package manager detection (uv, poetry, pipenv, pip) - Use setup-python's native cache parameter dynamically - Remove redundant common-cache step - Support uv with pip-compatible caching - Enhanced cache-dependency-path to include all lock files ansible-lint-fix changes: - Add setup-python with native pip caching (Python 3.11) - Remove redundant common-cache step - Simplify dependency installation Benefits: - Native caching is more efficient and better maintained - Supports modern Python tooling (uv, poetry, pipenv) - Reduces common-cache dependencies from 11 to 7 actions - setup-python handles cache invalidation automatically setup-python cache types supported: pip, pipenv, poetry * refactor(csharp): migrate to native setup-dotnet caching Replace common-cache with native caching in C# actions for better performance and maintainability. csharp-build changes: - Add cache: true and cache-dependency-path to setup-dotnet - Remove redundant common-cache step - Simplify restore logic, remove cache-hit conditionals csharp-publish changes: - Add cache: true and cache-dependency-path to setup-dotnet - Remove redundant common-cache step - Simplify restore logic, use step-security/retry for restore Benefits: - Native caching is more efficient and better maintained - Reduces common-cache dependencies from 7 to 5 actions - setup-dotnet handles NuGet package caching automatically - Cleaner workflow without complex conditional logic Phase 2 complete: Reduced common-cache usage from 11 to 5 actions. * refactor(go-lint): replace common-cache with actions/cache Replace common-cache wrapper with direct actions/cache for golangci-lint caching. This simplifies the action and improves performance. Changes: - Replace ivuorinen/actions/common-cache with actions/cache@v4.3.0 - Use hashFiles() for cache key generation instead of manual SHA256 - Simplify from 10 lines to 9 lines of YAML Benefits: - Native GitHub Actions functionality (no wrapper overhead) - Better performance (no extra action call) - Matches official golangci-lint-action approach - Less maintenance (GitHub-maintained action) - Reduces common-cache usage from 5 to 4 actions Trade-off: - Cache key format changes (invalidates existing caches once) * refactor: eliminate common-cache, use actions/cache directly Replace common-cache wrapper with native actions/cache in npm-publish and php-composer, completing the caching optimization campaign. Changes: 1. npm-publish (lines 107-114): - Replace common-cache with actions/cache@v4.3.0 - Use hashFiles() for node_modules cache key - Support multiple lock files (package-lock, yarn.lock, pnpm, bun) 2. php-composer (lines 177-190): - Replace common-cache with actions/cache@v4.3.0 - Use multiline YAML for cleaner path configuration - Use hashFiles() for composer cache key - Support optional cache-directories input Benefits: - Native GitHub Actions functionality (no wrapper overhead) - Better performance (no extra action call) - Simpler maintenance (one less internal action) - Standard approach used by official actions - Built-in hashFiles() more efficient than manual sha256sum Result: - Eliminates all common-cache usage (reduced from 4 to 0 actions) - common-cache action can now be deprecated/removed - Completes caching optimization: 11 → 0 common-cache dependencies Campaign summary: - Phase 1: Inline language-version-detect - Phase 2: Migrate 6 actions to setup-* native caching - Phase 3: Replace go-lint common-cache with actions/cache - Phase 4: Eliminate remaining common-cache (npm, php) * refactor: migrate Node.js linters from common-cache to actions/cache Replace common-cache wrapper with native actions/cache@v4.3.0 in all Node.js linting actions. Changes: - biome-lint: Use actions/cache with direct hashFiles() - eslint-lint: Use actions/cache with direct hashFiles() - prettier-lint: Use actions/cache with direct hashFiles() - pr-lint: Use actions/cache with direct hashFiles() All actions now use: - Native GitHub Actions cache functionality - Multi-lock-file support (npm, yarn, pnpm, bun) - Two-level restore-keys for graceful fallback - OS-aware cache keys with runner.os Benefits: - No wrapper overhead - Native hashFiles() instead of manual SHA256 - Consistent caching pattern across all Node.js actions * refactor: remove common-cache action Delete common-cache action and all associated test files. All actions now use native actions/cache@v4.3.0 instead of the wrapper. Deleted: - common-cache/action.yml - common-cache/README.md - common-cache/rules.yml - common-cache/CustomValidator.py - _tests/unit/common-cache/validation.spec.sh - _tests/integration/workflows/common-cache-test.yml - validate-inputs/tests/test_common-cache_custom.py Action count: 28 → 27 * fix: improve cache key quality across actions Address cache key quality issues identified during code review. php-composer: - Remove unused cache-directories input and handling code - Simplify cache paths to vendor + ~/.composer/cache only - Eliminate empty path issue when cache-directories was default empty npm-publish: - Remove redundant -npm- segment from cache key - Change: runner.os-npm-publish-{manager}-npm-{hash} - To: runner.os-npm-publish-{manager}-{hash} go-lint: - Add ~/.cache/go-build to cached paths - Now caches both golangci-lint and Go build artifacts - Improves Go build performance Result: Cleaner cache keys and better caching coverage * docs: remove common-cache references from documentation and tooling Remove all remaining references to common-cache from project documentation, test workflows, and build tooling after action deletion. Updated: - CLAUDE.md: Remove from action catalog (28 → 27 actions) - README.md: Regenerate catalog without common-cache - SECURITY.md: Update caching optimization notes - Test workflows: Remove common-cache test references - spec_helper.sh: Remove common-cache test helpers - generate_listing.cjs: Remove from category/language mappings - update-validators.py: Remove custom validator entry * refactor: inline node-setup across Node.js actions Phase 6A: Remove node-setup abstraction layer and inline Node.js setup. Changes: - Replace node-setup calls with direct actions/setup-node@v6.0.0 - Inline package manager detection (lockfile-based) - Add Corepack enablement and package manager installation - Use Node.js 22 as default version Actions migrated (5): - prettier-lint: Inline Node.js setup + package manager detection - biome-lint: Inline Node.js setup + package manager detection - eslint-lint: Inline Node.js setup + package manager detection - pr-lint: Inline Node.js setup (conditional on package.json) - npm-publish: Inline Node.js setup + package manager detection Removed: - node-setup/action.yml (371 lines) - node-setup/README.md, rules.yml, CustomValidator.py - _tests/unit/node-setup/validation.spec.sh - _tests/integration/workflows/node-setup-test.yml - validate-inputs/tests/test_node-setup_custom.py Documentation updates: - CLAUDE.md: Remove node-setup from action list (26 actions) - generate_listing.cjs: Remove node-setup mappings - update-validators.py: Remove node-setup custom validator Result: 26 actions (down from 27), eliminated internal dependency layer. * refactor: consolidate PHP testing actions with Laravel detection Merge php-tests, php-laravel-phpunit, and php-composer into single php-tests action: Consolidation: - Merge three PHP actions into one with framework auto-detection - Add framework input (auto/laravel/generic) with artisan file detection - Inline PHP version detection from multiple sources - Inline Composer setup, caching, and dependency installation - Add conditional Laravel-specific setup steps Features: - Auto-detect Laravel via artisan file presence - PHP version detection from .tool-versions, Dockerfile, composer.json, etc. - Composer dependency management with retry logic and caching - Laravel setup: .env copy, key generation, permissions, SQLite database - Smart test execution: composer test for Laravel, direct PHPUnit for generic Outputs: - framework: Detected framework (laravel/generic) - php-version, composer-version, cache-hit: Setup metadata - test-status, tests-run, tests-passed: Test results Deleted: - php-laravel-phpunit/: Laravel-specific testing action - php-composer/: Composer dependency management action - Related test files and custom validators Updated: - CLAUDE.md: 26 → 24 actions - generate_listing.cjs: Remove php-laravel-phpunit, php-composer - validate-inputs: Remove php-laravel-phpunit custom validator Result: 3 actions → 1 action, maintained all functionality with simpler interface. * fix: correct sed pattern in go-build Dockerfile parsing Remove unintended space in sed replacement pattern that was extracting golang version from Dockerfile. Before: s/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/ \1/p After: s/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p The leading space in the replacement would have caused version strings to have unwanted whitespace, potentially breaking version validation. * fix: convert bash-specific syntax to POSIX sh in php-tests Replace bash-specific [[ ]] syntax with POSIX-compliant alternatives to adhere to CLAUDE.md standards (all scripts must be POSIX sh). Changes: - PHP version validation: Replace regex =~ with case statement matching X.Y and X.Y.Z patterns - Max retries validation: Replace regex =~ with case statement checking for non-digit characters - Email validation: Replace glob patterns with case statement matching *@*.* pattern - Username validation: Replace glob patterns with case statement detecting command injection characters (;, &&, |) All validation logic preserved, error messages unchanged. * fix: add missing max-retries input to csharp-publish Add missing max-retries input declaration that was being used by the step-security/retry step at line 171 but not defined in the inputs section. Changes: - Add max-retries input with default value of '3' - Add description for dependency restoration retry attempts - Regenerate README.md with updated inputs documentation This fixes undefined input reference in the Restore Dependencies step. * fix: remove misleading 'Restore Complete' step in csharp-publish Remove the 'Restore Complete' step that always printed 'Cache hit - skipping dotnet restore' even though restore always runs via the retry action. The message was misleading because: - Dependencies are always restored via step-security/retry - The message claimed restore was skipped, which was false - The step served no actual purpose The 'Restore Dependencies' step already provides appropriate output during execution, making this step redundant and confusing. * fix(csharp-publish): use NuGet lock files for cache hashing The cache-dependency-path was incorrectly targeting *.csproj files which don't represent dependency state. Update to target **/packages.lock.json for accurate cache key generation. This ensures: - Cache hits only when dependencies actually match - No false cache hits from project file changes - Correct behavior per setup-dotnet@v5 documentation * fix: escape dots in shell case patterns for literal period matching In shell case statements, unescaped dots match any character rather than literal periods. Escape all dots in version pattern matching to ensure correct semantic version validation (e.g., '8.3.1' not '8X3Y1'). Fixed in 9 actions: - go-build: validate_version function - csharp-build: validate_version function - csharp-lint-check: validate_version function - csharp-publish: validate_version function - php-tests: PHP version validation + validate_version function - python-lint-fix: validate_version function - pr-lint: 3x validate_version functions (Go, Node.js, Python) - language-version-detect: PHP, Python, Node.js, .NET, Go validation Changed patterns: [0-9]*.[0-9]* → [0-9]*\.[0-9]* Impact: More accurate version validation, prevents false matches * fix(csharp-build): use NuGet lock files for cache hashing The cache-dependency-path was incorrectly targeting *.csproj files which don't represent dependency state. Update to target **/packages.lock.json for accurate cache key generation, matching csharp-publish configuration. This ensures: - Cache hits only when dependencies actually match - No false cache hits from project file changes - Consistent caching behavior across C# actions * fix(php-tests): replace GNU grep with POSIX-compatible sed The Composer version detection used 'grep -oP' with \K which is GNU-specific and breaks portability on BSD/macOS systems. Replace with POSIX-compliant sed pattern that extracts version numbers from 'Composer version X.Y.Z'. Changed: - grep -oP 'Composer version \K[0-9]+\.[0-9]+\.[0-9]+' + sed -n 's/.*Composer version \([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' Maintains same behavior with empty string fallback on match failure. * fix: remove misleading 'Restore Complete' step in csharp-build The 'Restore Complete' step always printed 'Cache hit - skipping dotnet restore' even when no cache hit occurred and restore ran unconditionally via the retry action. Remove the step entirely to eliminate misleading log messages. Matches fix already applied to csharp-publish (821aef0). The 'Restore Dependencies' step already provides appropriate output. * fix(python-lint-fix): use literal cache values for setup-python@v6 The setup-python@v6 action requires literal cache values, not dynamic expressions. Split the single Setup Python step into three conditional steps with literal cache values ('pip', 'pipenv', 'poetry'). Changed: - Single step with 'cache: ${{ steps.package-manager.outputs.package-manager }}' + Three conditional steps each with literal cache values + Each step includes only relevant cache-dependency-path patterns Benefits: - Compatible with setup-python@v6 requirements - More precise cache-dependency-path per package manager - Maintains full functionality across pip, pipenv, poetry, and uv * fix(python-lint-fix): remove unreachable venv activation step The 'Activate Virtual Environment (Cache Hit)' step references non-existent steps.cache-pip.outputs.cache-hit, making its condition always false and the step unreachable dead code. Additionally, this step is redundant as all subsequent steps (Run flake8, Run autopep8, etc.) already activate the venv directly with '. .venv/bin/activate' in their run blocks. Removed lines 298-312 (15 lines of dead code). * fix: correct invalid step references and unsafe conditions prettier-lint/action.yml: - Fix 3 invalid references to steps.node-setup.outputs.package-manager - Should reference steps.detect-pm.outputs.package-manager (correct step ID) - Lines 327, 360, 417 terraform-lint-fix/action.yml: - Add safety checks to commit condition to prevent fromJSON failures - Now verifies: files found + auto-fix enabled + fixes made - Line 249 Note: eslint-lint/action.yml already has correct references (no changes needed) * fix(php-tests): improve PHPUnit output parsing robustness Address fragile test result parsing that fails with common PHPUnit formats: Before: - Line 467 pattern 'OK.*[0-9]+ tests' required plural, failing on single test - Pattern only matched success case, silently defaulting to 0 on failures - Didn't handle ERRORED!/FAILED! output or skipped tests After: - Pattern 1: Match 'OK (N test(s), M assertions)' - handles singular/plural - Pattern 2: Parse 'Tests: N' line for failures/errors, calculate passed - Handles: single test, failures, errors, mixed, skipped tests - Exit code still determines final status (line 489) * test: add comprehensive PHPUnit output parsing tests Add 20 unit tests covering all PHPUnit output format variations: Success cases (3): - Single test (singular 'test') - Multiple tests (plural 'tests') - Large test counts Failure cases (5): - Failures only - Errors only - Mixed failures and errors - All tests failing - Prevents negative passed count Edge cases (7): - Skipped tests (with/without OK prefix) - No parseable output (fallback to 0/0) - Empty output - Verbose output with noise - Full failure details - Risky tests Status tests (2): - Exit code 0 → success - Exit code non-zero → failure Helper function parse_phpunit_output() replicates action parsing logic for isolated testing. Also fix pre-existing test expecting underscores in output names: - test_status → test-status - tests_run → tests-run - tests_passed → tests-passed - coverage_path → framework (output doesn't exist) All 62 tests now pass (42 existing + 20 new) * fix(python-lint-fix): make pyproject.toml parsing POSIX/BSD compatible Fix portability issues in pyproject.toml version parsing: Line 173: - Before: grep -q '^\\[project\\]' (double-escaped, incorrect) - After: grep -q '^\[project\]' (single-escaped, correct) Line 174: - Before: '^\\s*' and sed with GNU-only '\+' quantifier - After: '^[[:space:]]*' (POSIX character class) and sed -E with '+' (extended regex) Changes ensure compatibility with: - macOS (BSD sed) - Linux (GNU sed) - Any POSIX-compliant shell environment sed -E enables extended regex mode where: - No backslashes needed for grouping () - '+' works directly (not '\+') - More readable and portable * fix(posix): Phase 1 - convert bash-specific syntax to POSIX sh Convert three simpler action files from bash to POSIX sh: terraform-lint-fix/action.yml: - Line 216: Change [[ to [ and == to = codeql-analysis/action.yml: - Change shell: bash to shell: sh (4 steps) - Add set -eu to all shell blocks - Convert [[ to [ and == to = (5 locations) - Fix $GITHUB_OUTPUT quoting sync-labels/action.yml: - Change shell: bash to shell: sh (2 steps) - Convert bash [[ tests to POSIX [ tests - Replace regex =~ with case pattern matching - Convert pipefail (bash-only) to POSIX set -eu All changes verified: - make lint: passed - Unit tests: 25/25 passed (100% coverage) - POSIX compliance: Confirmed for all three actions Part of comprehensive POSIX compatibility review. Next: Phase 2 (validation-heavy files) * fix(posix): Phase 2A - convert bash to POSIX sh (3 validation-heavy files) Convert three validation-heavy action files from bash to POSIX sh: release-monthly/action.yml: - Replace shell: bash → shell: sh (3 steps) - Replace set -euo pipefail → set -eu - Replace regex =~ with case pattern matching for version validation - Replace [[ ]] tests with [ ] tests and == with = - Use case statements for prefix validation - Fix &>/dev/null → >/dev/null 2>&1 (POSIX) compress-images/action.yml: - Replace shell: bash → shell: sh - Replace set -euo pipefail → set -eu - Convert all [[ ]] wildcard tests to case pattern matching - Convert regex =~ to case statements for numeric validation - Consolidated validation patterns biome-lint/action.yml: - Replace shell: bash → shell: sh (4 steps) - Replace set -euo pipefail → set -eu - Convert [[ ]] tests to case pattern matching - Convert regex =~ to case statements - Handle GitHub token expression validation with case - Email validation with case patterns - Username validation with case patterns for: - Invalid characters - Leading/trailing hyphens - Consecutive hyphens All changes maintain identical validation logic while using POSIX-compliant syntax. Next: eslint-lint and prettier-lint (similar patterns) * fix: convert eslint-lint and prettier-lint to POSIX sh (Phase 2B) Convert shell scripts from bash to POSIX sh for maximum compatibility: eslint-lint/action.yml: - Change all shell: bash → shell: sh - Replace set -euo pipefail with set -eu (pipefail not POSIX) - Convert [[ ]] tests to [ ] with = instead of == - Replace regex validation with case pattern matching - Convert boolean validation to case-insensitive patterns - Update version/path/email validation with case patterns prettier-lint/action.yml: - Change all shell: bash → shell: sh - Replace set -euo pipefail with set -eu - Convert [[ ]] tests to [ ] with = instead of == - Replace regex validation with case pattern matching - Convert boolean validation to case-insensitive patterns - Update version/path/email validation with case patterns All changes maintain identical functionality while ensuring compatibility with POSIX sh on all platforms. Part of comprehensive POSIX compatibility effort. Phase 2B completes 5/5 validation-heavy action files. * fix: convert docker-build and php-tests to POSIX sh (Phase 3) Convert complex shell scripts from bash to POSIX sh for maximum compatibility: docker-build/action.yml: - Convert all 12 shell blocks from bash to sh - Replace set -euo pipefail with set -eu (pipefail not POSIX) - Convert bash array parsing to POSIX positional parameters * Parse Build Arguments: IFS + set -- pattern * Parse Build Contexts: IFS + set -- pattern * Parse Secrets: IFS + set -- with case validation - Replace [[ wildcard tests with case patterns * Line 277: secret contains '=' check * Line 376: cache_to contains 'type=local' check - Convert [[ -z || -z ]] to separate [ ] tests - Change == to = in string comparisons php-tests/action.yml: - Convert all 11 shell blocks from bash to sh - Replace set -euo pipefail with set -eu - No bash-specific constructs (already POSIX-compatible logic) All changes maintain identical functionality while ensuring compatibility with POSIX sh on all platforms. Complex array handling in docker-build required careful conversion using positional parameters with IFS manipulation. Part of comprehensive POSIX compatibility effort. Phase 3 completes 2/2 complex action files with array handling. * fix: convert remaining actions to POSIX sh (Phase 4 - Final) Convert final shell scripts from bash to POSIX sh for maximum compatibility: csharp-build/action.yml (2 blocks): - Change shell: bash → shell: sh - Replace set -euo pipefail with set -eu csharp-lint-check/action.yml (3 blocks): - Change shell: bash → shell: sh - Replace set -euo pipefail with set -eu csharp-publish/action.yml (6 blocks): - Change shell: bash → shell: sh - Replace set -euo pipefail with set -eu go-build/action.yml (2 blocks): - Change shell: bash → shell: sh - Replace set -euo pipefail with set -eu python-lint-fix/action.yml: - Already using shell: sh (POSIX compliant) - No changes needed All Phase 4 files contained only isolated bash usage without bash-specific features ([[, =~, arrays, etc.), making conversion straightforward. All changes maintain identical functionality while ensuring compatibility with POSIX sh on all platforms. POSIX COMPATIBILITY PROJECT COMPLETE: - Phase 1: 3 files (terraform-lint-fix, codeql-analysis, sync-labels) - Phase 2: 5 files (release-monthly, compress-images, biome-lint, eslint-lint, prettier-lint) - Phase 3: 2 files (docker-build, php-tests) - complex array handling - Phase 4: 4 files (csharp-build, csharp-lint-check, csharp-publish, go-build) Total: 14/14 files converted (100% complete) All shell scripts now POSIX sh compatible across all platforms. * fix: address PR #359 review comments - Fix eslint-lint file extensions regex to support 1+ extensions (critical bug: default .js,.jsx,.ts,.tsx was rejected by validation) - Add NuGet caching to csharp-lint-check Setup .NET SDK step (matches csharp-build and csharp-publish configuration) * fix: address additional PR #359 review comments POSIX Compatibility: - csharp-lint-check: replace bash [[ ]] and =~ with POSIX [ ] and grep -qE (fixes validation block incompatible with sh shell declaration) ESLint Consistency: - eslint-lint: use detected package manager in check mode (adds pnpm/yarn/bun support, matching fix mode behavior) - eslint-lint: replace case pattern with grep-based validation (fixes regex that rejected valid multi-extension inputs like .js,.jsx,.ts,.tsx) * fix: use file-extensions input in eslint-lint commands The file-extensions input was defined, validated, and passed to env but never used in ESLint commands, causing ESLint to only lint .js files by default. Changes: - Add --ext flag to all check mode ESLint commands (pnpm/yarn/bun/npm) - Add FILE_EXTENSIONS to fix mode env section - Add --ext flag to all fix mode ESLint commands (pnpm/yarn/bun/npm) Now ESLint correctly lints all configured extensions (.js,.jsx,.ts,.tsx) * fix: strengthen eslint-lint version validation The previous case pattern allowed invalid versions like "1.0.0-" (trailing hyphen with no pre-release identifier) and didn't treat dots as literal characters. Changes: - Replace case pattern with grep-based regex validation - Pattern: ^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9]+([.-][a-zA-Z0-9]+)*)?$ - Requires at least one alphanumeric character after hyphen if present - Supports dot/dash-separated pre-release identifiers - Treats dots as literal characters throughout Valid: 8.57.0, 8.57.0-rc.1, 8.57.0-alpha.beta.1 Invalid: 8.57.0-, 8.57.0--, 8.57.0-., 8.57.0-alpha..1 * fix: strengthen eslint-lint email validation The previous case pattern *@*.* was overly permissive and would accept invalid emails like @@@, a@b., or user@@domain.com. Changes: - Replace case pattern with grep-based regex validation - Pattern: ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ - Requires proper structure: local-part@domain.tld - Local part: alphanumeric plus common email special chars - Domain: alphanumeric, dots, hyphens - TLD: at least 2 letters Valid: user@example.com, first.last@domain.co.uk Invalid: @@@, a@b., user@@domain.com, test@domain --- CLAUDE.md | 6 +- README.md | 117 ++-- SECURITY.md | 2 +- .../workflows/common-cache-test.yml | 471 ---------------- .../workflows/lint-fix-chain-test.yml | 1 - .../integration/workflows/node-setup-test.yml | 513 ------------------ .../workflows/version-file-parser-test.yml | 241 -------- _tests/unit/common-cache/validation.spec.sh | 168 ------ _tests/unit/node-setup/validation.spec.sh | 242 --------- _tests/unit/php-composer/validation.spec.sh | 407 -------------- .../php-laravel-phpunit/validation.spec.sh | 280 ---------- _tests/unit/php-tests/validation.spec.sh | 217 +++++++- _tests/unit/spec_helper.sh | 27 - .../version-file-parser/validation.spec.sh | 125 ----- ansible-lint-fix/action.yml | 11 +- biome-lint/action.yml | 150 +++-- codeql-analysis/action.yml | 26 +- common-cache/CustomValidator.py | 244 --------- common-cache/README.md | 72 --- common-cache/action.yml | 122 ----- common-cache/rules.yml | 42 -- compress-images/action.yml | 97 ++-- csharp-build/action.yml | 126 ++++- csharp-lint-check/action.yml | 118 +++- csharp-publish/README.md | 17 +- csharp-publish/action.yml | 153 ++++-- csharp-publish/rules.yml | 8 +- docker-build/action.yml | 125 +++-- eslint-lint/action.yml | 220 +++++--- generate_listing.cjs | 10 +- go-build/action.yml | 125 ++++- go-lint/action.yml | 15 +- language-version-detect/README.md | 4 +- language-version-detect/action.yml | 219 +++++++- language-version-detect/rules.yml | 3 +- node-setup/CustomValidator.py | 80 --- node-setup/README.md | 72 --- node-setup/action.yml | 242 --------- node-setup/rules.yml | 45 -- npm-publish/action.yml | 68 ++- php-composer/CustomValidator.py | 228 -------- php-composer/README.md | 94 ---- php-composer/action.yml | 228 -------- php-composer/rules.yml | 47 -- php-laravel-phpunit/CustomValidator.py | 134 ----- php-laravel-phpunit/README.md | 66 --- php-laravel-phpunit/action.yml | 135 ----- php-laravel-phpunit/rules.yml | 43 -- php-tests/README.md | 69 ++- php-tests/action.yml | 468 ++++++++++++++-- php-tests/rules.yml | 26 +- pr-lint/action.yml | 406 +++++++++++++- prettier-lint/action.yml | 199 +++++-- python-lint-fix/action.yml | 193 +++++-- release-monthly/action.yml | 71 ++- sync-labels/action.yml | 32 +- terraform-lint-fix/action.yml | 4 +- validate-inputs/scripts/update-validators.py | 10 - .../tests/test_common-cache_custom.py | 74 --- .../tests/test_node-setup_custom.py | 74 --- .../tests/test_php-composer_custom.py | 74 --- .../tests/test_php-laravel-phpunit_custom.py | 74 --- .../tests/test_version-file-parser_custom.py | 74 --- version-file-parser/CustomValidator.py | 115 ---- version-file-parser/README.md | 76 --- version-file-parser/action.yml | 365 ------------- version-file-parser/rules.yml | 42 -- 67 files changed, 2673 insertions(+), 5979 deletions(-) delete mode 100644 _tests/integration/workflows/common-cache-test.yml delete mode 100644 _tests/integration/workflows/node-setup-test.yml delete mode 100644 _tests/integration/workflows/version-file-parser-test.yml delete mode 100755 _tests/unit/common-cache/validation.spec.sh delete mode 100755 _tests/unit/node-setup/validation.spec.sh delete mode 100755 _tests/unit/php-composer/validation.spec.sh delete mode 100755 _tests/unit/php-laravel-phpunit/validation.spec.sh delete mode 100755 _tests/unit/version-file-parser/validation.spec.sh delete mode 100755 common-cache/CustomValidator.py delete mode 100644 common-cache/README.md delete mode 100644 common-cache/action.yml delete mode 100644 common-cache/rules.yml delete mode 100755 node-setup/CustomValidator.py delete mode 100644 node-setup/README.md delete mode 100644 node-setup/action.yml delete mode 100644 node-setup/rules.yml delete mode 100755 php-composer/CustomValidator.py delete mode 100644 php-composer/README.md delete mode 100644 php-composer/action.yml delete mode 100644 php-composer/rules.yml delete mode 100755 php-laravel-phpunit/CustomValidator.py delete mode 100644 php-laravel-phpunit/README.md delete mode 100644 php-laravel-phpunit/action.yml delete mode 100644 php-laravel-phpunit/rules.yml delete mode 100644 validate-inputs/tests/test_common-cache_custom.py delete mode 100644 validate-inputs/tests/test_node-setup_custom.py delete mode 100644 validate-inputs/tests/test_php-composer_custom.py delete mode 100644 validate-inputs/tests/test_php-laravel-phpunit_custom.py delete mode 100644 validate-inputs/tests/test_version-file-parser_custom.py delete mode 100755 version-file-parser/CustomValidator.py delete mode 100644 version-file-parser/README.md delete mode 100644 version-file-parser/action.yml delete mode 100644 version-file-parser/rules.yml diff --git a/CLAUDE.md b/CLAUDE.md index f56c435..c91349f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,11 +71,11 @@ Flat structure. Each action self-contained with `action.yml`. -**30 Actions**: Setup (node-setup, language-version-detect), Utilities (action-versioning, version-file-parser), +**24 Actions**: Setup (language-version-detect), Utilities (action-versioning, version-file-parser), Linting (ansible-lint-fix, biome-lint, csharp-lint-check, eslint-lint, go-lint, pr-lint, pre-commit, prettier-lint, python-lint-fix, terraform-lint-fix), -Testing (php-tests, php-laravel-phpunit, php-composer), Build (csharp-build, go-build, docker-build), +Testing (php-tests), Build (csharp-build, go-build, docker-build), Publishing (npm-publish, docker-publish, csharp-publish), -Repository (release-monthly, sync-labels, stale, compress-images, common-cache, codeql-analysis), +Repository (release-monthly, sync-labels, stale, compress-images, codeql-analysis), Validation (validate-inputs) ## Commands diff --git a/README.md b/README.md index fe57693..1172ab2 100644 --- a/README.md +++ b/README.md @@ -22,104 +22,94 @@ Each action is fully self-contained and can be used independently in any GitHub ## 📚 Action Catalog -This repository contains **30 reusable GitHub Actions** for CI/CD automation. +This repository contains **25 reusable GitHub Actions** for CI/CD automation. -### Quick Reference (30 Actions) +### Quick Reference (25 Actions) | Icon | Action | Category | Description | Key Features | |:----:|:-----------------------------------------------------|:-----------|:----------------------------------------------------------------|:---------------------------------------------| | 🔀 | [`action-versioning`][action-versioning] | Utilities | Automatically update SHA-pinned action references to match l... | Token auth, Outputs | -| 📦 | [`ansible-lint-fix`][ansible-lint-fix] | Linting | Lints and fixes Ansible playbooks, commits changes, and uplo... | Token auth, Outputs | -| ✅ | [`biome-lint`][biome-lint] | Linting | Run Biome linter in check or fix mode | Token auth, Outputs | +| 📦 | [`ansible-lint-fix`][ansible-lint-fix] | Linting | Lints and fixes Ansible playbooks, commits changes, and uplo... | Caching, Token auth, Outputs | +| ✅ | [`biome-lint`][biome-lint] | Linting | Run Biome linter in check or fix mode | Caching, Auto-detection, Token auth, Outputs | | 🛡️ | [`codeql-analysis`][codeql-analysis] | Repository | Run CodeQL security analysis for a single language with conf... | Auto-detection, Token auth, Outputs | -| 💾 | [`common-cache`][common-cache] | Repository | Standardized caching strategy for all actions | Caching, Outputs | | 🖼️ | [`compress-images`][compress-images] | Repository | Compress images on demand (workflow_dispatch), and at 11pm e... | Token auth, Outputs | -| 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Auto-detection, Token auth, Outputs | +| 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Caching, Auto-detection, Token auth, Outputs | | 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Auto-detection, Token auth, Outputs | -| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Auto-detection, Token auth, Outputs | +| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Caching, Auto-detection, Token auth, Outputs | | 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs | | ☁️ | [`docker-publish`][docker-publish] | Publishing | Simple wrapper to publish Docker images to GitHub Packages a... | Token auth, Outputs | -| ✅ | [`eslint-lint`][eslint-lint] | Linting | Run ESLint in check or fix mode with advanced configuration ... | Caching, Token auth, Outputs | +| ✅ | [`eslint-lint`][eslint-lint] | Linting | Run ESLint in check or fix mode with advanced configuration ... | Caching, Auto-detection, Token auth, Outputs | | 📦 | [`go-build`][go-build] | Build | Builds the Go project. | Caching, Auto-detection, Token auth, Outputs | | 📝 | [`go-lint`][go-lint] | Linting | Run golangci-lint with advanced configuration, caching, and ... | Caching, Token auth, Outputs | -| 📝 | [`language-version-detect`][language-version-detect] | Setup | Detects language version from project configuration files wi... | Auto-detection, Token auth, Outputs | -| 🖥️ | [`node-setup`][node-setup] | Setup | Sets up Node.js environment with version detection and packa... | Auto-detection, Token auth, Outputs | -| 📦 | [`npm-publish`][npm-publish] | Publishing | Publishes the package to the NPM registry with configurable ... | Token auth, Outputs | -| 🖥️ | [`php-composer`][php-composer] | Testing | Runs Composer install on a repository with advanced caching ... | Auto-detection, Token auth, Outputs | -| 💻 | [`php-laravel-phpunit`][php-laravel-phpunit] | Testing | Setup PHP, install dependencies, generate key, create databa... | Auto-detection, Token auth, Outputs | -| ✅ | [`php-tests`][php-tests] | Testing | Run PHPUnit tests on the repository | Token auth, Outputs | +| 📝 | [`language-version-detect`][language-version-detect] | Setup | DEPRECATED: This action is deprecated. Inline version detect... | Auto-detection, Token auth, Outputs | +| 📦 | [`npm-publish`][npm-publish] | Publishing | Publishes the package to the NPM registry with configurable ... | Caching, Auto-detection, Token auth, Outputs | +| ✅ | [`php-tests`][php-tests] | Testing | Run PHPUnit tests with optional Laravel setup and Composer d... | Caching, Auto-detection, Token auth, Outputs | | ✅ | [`pr-lint`][pr-lint] | Linting | Runs MegaLinter against pull requests | Caching, Auto-detection, Token auth, Outputs | | 📦 | [`pre-commit`][pre-commit] | Linting | Runs pre-commit on the repository and pushes the fixes back ... | Auto-detection, Token auth, Outputs | -| ✅ | [`prettier-lint`][prettier-lint] | Linting | Run Prettier in check or fix mode with advanced configuratio... | Caching, Token auth, Outputs | +| ✅ | [`prettier-lint`][prettier-lint] | Linting | Run Prettier in check or fix mode with advanced configuratio... | Caching, Auto-detection, Token auth, Outputs | | 📝 | [`python-lint-fix`][python-lint-fix] | Linting | Lints and fixes Python files, commits changes, and uploads S... | Caching, Auto-detection, Token auth, Outputs | | 📦 | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs | | 📦 | [`stale`][stale] | Repository | A GitHub Action to close stale issues and pull requests. | Token auth, Outputs | | 🏷️ | [`sync-labels`][sync-labels] | Repository | Sync labels from a YAML file to a GitHub repository | Token auth, Outputs | | 🖥️ | [`terraform-lint-fix`][terraform-lint-fix] | Linting | Lints and fixes Terraform files with advanced validation and... | Token auth, Outputs | | 🛡️ | [`validate-inputs`][validate-inputs] | Validation | Centralized Python-based input validation for GitHub Actions... | Token auth, Outputs | -| 📦 | [`version-file-parser`][version-file-parser] | Utilities | Universal parser for common version detection files (.tool-v... | Auto-detection, Outputs | ### Actions by Category -#### 🔧 Setup (2 actions) +#### 🔧 Setup (1 action) -| Action | Description | Languages | Features | -|:--------------------------------------------------------|:------------------------------------------------------|:--------------------------------|:------------------------------------| -| 📝 [`language-version-detect`][language-version-detect] | Detects language version from project configuratio... | PHP, Python, Go, .NET, Node.js | Auto-detection, Token auth, Outputs | -| 🖥️ [`node-setup`][node-setup] | Sets up Node.js environment with version detection... | Node.js, JavaScript, TypeScript | Auto-detection, Token auth, Outputs | +| Action | Description | Languages | Features | +|:--------------------------------------------------------|:------------------------------------------------------|:-------------------------------|:------------------------------------| +| 📝 [`language-version-detect`][language-version-detect] | DEPRECATED: This action is deprecated. Inline vers... | PHP, Python, Go, .NET, Node.js | Auto-detection, Token auth, Outputs | -#### 🛠️ Utilities (2 actions) +#### 🛠️ Utilities (1 action) -| Action | Description | Languages | Features | -|:------------------------------------------------|:------------------------------------------------------|:-------------------|:------------------------| -| 🔀 [`action-versioning`][action-versioning] | Automatically update SHA-pinned action references ... | GitHub Actions | Token auth, Outputs | -| 📦 [`version-file-parser`][version-file-parser] | Universal parser for common version detection file... | Multiple Languages | Auto-detection, Outputs | +| Action | Description | Languages | Features | +|:--------------------------------------------|:------------------------------------------------------|:---------------|:--------------------| +| 🔀 [`action-versioning`][action-versioning] | Automatically update SHA-pinned action references ... | GitHub Actions | Token auth, Outputs | #### 📝 Linting (10 actions) | Action | Description | Languages | Features | |:-----------------------------------------------|:------------------------------------------------------|:---------------------------------------------|:---------------------------------------------| -| 📦 [`ansible-lint-fix`][ansible-lint-fix] | Lints and fixes Ansible playbooks, commits changes... | Ansible, YAML | Token auth, Outputs | -| ✅ [`biome-lint`][biome-lint] | Run Biome linter in check or fix mode | JavaScript, TypeScript, JSON | Token auth, Outputs | +| 📦 [`ansible-lint-fix`][ansible-lint-fix] | Lints and fixes Ansible playbooks, commits changes... | Ansible, YAML | Caching, Token auth, Outputs | +| ✅ [`biome-lint`][biome-lint] | Run Biome linter in check or fix mode | JavaScript, TypeScript, JSON | Caching, Auto-detection, Token auth, Outputs | | 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Auto-detection, Token auth, Outputs | -| ✅ [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, Token auth, Outputs | +| ✅ [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, Auto-detection, Token auth, Outputs | | 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, Token auth, Outputs | | ✅ [`pr-lint`][pr-lint] | Runs MegaLinter against pull requests | Conventional Commits | Caching, Auto-detection, Token auth, Outputs | | 📦 [`pre-commit`][pre-commit] | Runs pre-commit on the repository and pushes the f... | Python, Multiple Languages | Auto-detection, Token auth, Outputs | -| ✅ [`prettier-lint`][prettier-lint] | Run Prettier in check or fix mode with advanced co... | JavaScript, TypeScript, Markdown, YAML, JSON | Caching, Token auth, Outputs | +| ✅ [`prettier-lint`][prettier-lint] | Run Prettier in check or fix mode with advanced co... | JavaScript, TypeScript, Markdown, YAML, JSON | Caching, Auto-detection, Token auth, Outputs | | 📝 [`python-lint-fix`][python-lint-fix] | Lints and fixes Python files, commits changes, and... | Python | Caching, Auto-detection, Token auth, Outputs | | 🖥️ [`terraform-lint-fix`][terraform-lint-fix] | Lints and fixes Terraform files with advanced vali... | Terraform, HCL | Token auth, Outputs | -#### 🧪 Testing (3 actions) +#### 🧪 Testing (1 action) -| Action | Description | Languages | Features | -|:------------------------------------------------|:------------------------------------------------------|:-------------|:------------------------------------| -| 🖥️ [`php-composer`][php-composer] | Runs Composer install on a repository with advance... | PHP | Auto-detection, Token auth, Outputs | -| 💻 [`php-laravel-phpunit`][php-laravel-phpunit] | Setup PHP, install dependencies, generate key, cre... | PHP, Laravel | Auto-detection, Token auth, Outputs | -| ✅ [`php-tests`][php-tests] | Run PHPUnit tests on the repository | PHP | Token auth, Outputs | +| Action | Description | Languages | Features | +|:---------------------------|:------------------------------------------------------|:-------------|:---------------------------------------------| +| ✅ [`php-tests`][php-tests] | Run PHPUnit tests with optional Laravel setup and ... | PHP, Laravel | Caching, Auto-detection, Token auth, Outputs | #### 🏗️ Build (3 actions) | Action | Description | Languages | Features | |:----------------------------------|:------------------------------------------------------|:----------|:---------------------------------------------| -| 📝 [`csharp-build`][csharp-build] | Builds and tests C# projects. | C#, .NET | Auto-detection, Token auth, Outputs | +| 📝 [`csharp-build`][csharp-build] | Builds and tests C# projects. | C#, .NET | Caching, Auto-detection, Token auth, Outputs | | 📦 [`docker-build`][docker-build] | Builds a Docker image for multiple architectures w... | Docker | Caching, Auto-detection, Token auth, Outputs | | 📦 [`go-build`][go-build] | Builds the Go project. | Go | Caching, Auto-detection, Token auth, Outputs | #### 🚀 Publishing (3 actions) -| Action | Description | Languages | Features | -|:--------------------------------------|:------------------------------------------------------|:-------------|:------------------------------------| -| 📦 [`csharp-publish`][csharp-publish] | Publishes a C# project to GitHub Packages. | C#, .NET | Auto-detection, Token auth, Outputs | -| ☁️ [`docker-publish`][docker-publish] | Simple wrapper to publish Docker images to GitHub ... | Docker | Token auth, Outputs | -| 📦 [`npm-publish`][npm-publish] | Publishes the package to the NPM registry with con... | Node.js, npm | Token auth, Outputs | +| Action | Description | Languages | Features | +|:--------------------------------------|:------------------------------------------------------|:-------------|:---------------------------------------------| +| 📦 [`csharp-publish`][csharp-publish] | Publishes a C# project to GitHub Packages. | C#, .NET | Caching, Auto-detection, Token auth, Outputs | +| ☁️ [`docker-publish`][docker-publish] | Simple wrapper to publish Docker images to GitHub ... | Docker | Token auth, Outputs | +| 📦 [`npm-publish`][npm-publish] | Publishes the package to the NPM registry with con... | Node.js, npm | Caching, Auto-detection, Token auth, Outputs | -#### 📦 Repository (6 actions) +#### 📦 Repository (5 actions) | Action | Description | Languages | Features | |:-----------------------------------------|:------------------------------------------------------|:--------------------------------------------------------|:------------------------------------| | 🛡️ [`codeql-analysis`][codeql-analysis] | Run CodeQL security analysis for a single language... | JavaScript, TypeScript, Python, Java, C#, C++, Go, Ruby | Auto-detection, Token auth, Outputs | -| 💾 [`common-cache`][common-cache] | Standardized caching strategy for all actions | Caching | Caching, Outputs | | 🖼️ [`compress-images`][compress-images] | Compress images on demand (workflow_dispatch), and... | Images, PNG, JPEG | Token auth, Outputs | | 📦 [`release-monthly`][release-monthly] | Creates a release for the current month, increment... | GitHub Actions | Token auth, Outputs | | 📦 [`stale`][stale] | A GitHub Action to close stale issues and pull req... | GitHub Actions | Token auth, Outputs | @@ -136,35 +126,30 @@ This repository contains **30 reusable GitHub Actions** for CI/CD automation. | Action | Caching | Auto-detection | Token auth | Outputs | |:-----------------------------------------------------|:-------:|:--------------:|:----------:|:-------:| | [`action-versioning`][action-versioning] | - | - | ✅ | ✅ | -| [`ansible-lint-fix`][ansible-lint-fix] | - | - | ✅ | ✅ | -| [`biome-lint`][biome-lint] | - | - | ✅ | ✅ | +| [`ansible-lint-fix`][ansible-lint-fix] | ✅ | - | ✅ | ✅ | +| [`biome-lint`][biome-lint] | ✅ | ✅ | ✅ | ✅ | | [`codeql-analysis`][codeql-analysis] | - | ✅ | ✅ | ✅ | -| [`common-cache`][common-cache] | ✅ | - | - | ✅ | | [`compress-images`][compress-images] | - | - | ✅ | ✅ | -| [`csharp-build`][csharp-build] | - | ✅ | ✅ | ✅ | +| [`csharp-build`][csharp-build] | ✅ | ✅ | ✅ | ✅ | | [`csharp-lint-check`][csharp-lint-check] | - | ✅ | ✅ | ✅ | -| [`csharp-publish`][csharp-publish] | - | ✅ | ✅ | ✅ | +| [`csharp-publish`][csharp-publish] | ✅ | ✅ | ✅ | ✅ | | [`docker-build`][docker-build] | ✅ | ✅ | ✅ | ✅ | | [`docker-publish`][docker-publish] | - | - | ✅ | ✅ | -| [`eslint-lint`][eslint-lint] | ✅ | - | ✅ | ✅ | +| [`eslint-lint`][eslint-lint] | ✅ | ✅ | ✅ | ✅ | | [`go-build`][go-build] | ✅ | ✅ | ✅ | ✅ | | [`go-lint`][go-lint] | ✅ | - | ✅ | ✅ | | [`language-version-detect`][language-version-detect] | - | ✅ | ✅ | ✅ | -| [`node-setup`][node-setup] | - | ✅ | ✅ | ✅ | -| [`npm-publish`][npm-publish] | - | - | ✅ | ✅ | -| [`php-composer`][php-composer] | - | ✅ | ✅ | ✅ | -| [`php-laravel-phpunit`][php-laravel-phpunit] | - | ✅ | ✅ | ✅ | -| [`php-tests`][php-tests] | - | - | ✅ | ✅ | +| [`npm-publish`][npm-publish] | ✅ | ✅ | ✅ | ✅ | +| [`php-tests`][php-tests] | ✅ | ✅ | ✅ | ✅ | | [`pr-lint`][pr-lint] | ✅ | ✅ | ✅ | ✅ | | [`pre-commit`][pre-commit] | - | ✅ | ✅ | ✅ | -| [`prettier-lint`][prettier-lint] | ✅ | - | ✅ | ✅ | +| [`prettier-lint`][prettier-lint] | ✅ | ✅ | ✅ | ✅ | | [`python-lint-fix`][python-lint-fix] | ✅ | ✅ | ✅ | ✅ | | [`release-monthly`][release-monthly] | - | - | ✅ | ✅ | | [`stale`][stale] | - | - | ✅ | ✅ | | [`sync-labels`][sync-labels] | - | - | ✅ | ✅ | | [`terraform-lint-fix`][terraform-lint-fix] | - | - | ✅ | ✅ | | [`validate-inputs`][validate-inputs] | - | - | ✅ | ✅ | -| [`version-file-parser`][version-file-parser] | - | ✅ | - | ✅ | ### Language Support @@ -174,7 +159,6 @@ This repository contains **30 reusable GitHub Actions** for CI/CD automation. | Ansible | [`ansible-lint-fix`][ansible-lint-fix] | | C# | [`codeql-analysis`][codeql-analysis], [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish] | | C++ | [`codeql-analysis`][codeql-analysis] | -| Caching | [`common-cache`][common-cache] | | Conventional Commits | [`pr-lint`][pr-lint] | | Docker | [`docker-build`][docker-build], [`docker-publish`][docker-publish] | | GitHub | [`sync-labels`][sync-labels] | @@ -185,17 +169,17 @@ This repository contains **30 reusable GitHub Actions** for CI/CD automation. | JPEG | [`compress-images`][compress-images] | | JSON | [`biome-lint`][biome-lint], [`prettier-lint`][prettier-lint] | | Java | [`codeql-analysis`][codeql-analysis] | -| JavaScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`node-setup`][node-setup], [`prettier-lint`][prettier-lint] | -| Laravel | [`php-laravel-phpunit`][php-laravel-phpunit] | +| JavaScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`prettier-lint`][prettier-lint] | +| Laravel | [`php-tests`][php-tests] | | Markdown | [`prettier-lint`][prettier-lint] | -| Multiple Languages | [`pre-commit`][pre-commit], [`version-file-parser`][version-file-parser] | -| Node.js | [`language-version-detect`][language-version-detect], [`node-setup`][node-setup], [`npm-publish`][npm-publish] | -| PHP | [`language-version-detect`][language-version-detect], [`php-composer`][php-composer], [`php-laravel-phpunit`][php-laravel-phpunit], [`php-tests`][php-tests] | +| Multiple Languages | [`pre-commit`][pre-commit] | +| Node.js | [`language-version-detect`][language-version-detect], [`npm-publish`][npm-publish] | +| PHP | [`language-version-detect`][language-version-detect], [`php-tests`][php-tests] | | PNG | [`compress-images`][compress-images] | | Python | [`codeql-analysis`][codeql-analysis], [`language-version-detect`][language-version-detect], [`pre-commit`][pre-commit], [`python-lint-fix`][python-lint-fix] | | Ruby | [`codeql-analysis`][codeql-analysis] | | Terraform | [`terraform-lint-fix`][terraform-lint-fix] | -| TypeScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`node-setup`][node-setup], [`prettier-lint`][prettier-lint] | +| TypeScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`prettier-lint`][prettier-lint] | | YAML | [`ansible-lint-fix`][ansible-lint-fix], [`prettier-lint`][prettier-lint], [`sync-labels`][sync-labels], [`validate-inputs`][validate-inputs] | | npm | [`npm-publish`][npm-publish] | @@ -223,7 +207,6 @@ All actions can be used independently in your workflows: [ansible-lint-fix]: ansible-lint-fix/README.md [biome-lint]: biome-lint/README.md [codeql-analysis]: codeql-analysis/README.md -[common-cache]: common-cache/README.md [compress-images]: compress-images/README.md [csharp-build]: csharp-build/README.md [csharp-lint-check]: csharp-lint-check/README.md @@ -234,10 +217,7 @@ All actions can be used independently in your workflows: [go-build]: go-build/README.md [go-lint]: go-lint/README.md [language-version-detect]: language-version-detect/README.md -[node-setup]: node-setup/README.md [npm-publish]: npm-publish/README.md -[php-composer]: php-composer/README.md -[php-laravel-phpunit]: php-laravel-phpunit/README.md [php-tests]: php-tests/README.md [pr-lint]: pr-lint/README.md [pre-commit]: pre-commit/README.md @@ -248,7 +228,6 @@ All actions can be used independently in your workflows: [sync-labels]: sync-labels/README.md [terraform-lint-fix]: terraform-lint-fix/README.md [validate-inputs]: validate-inputs/README.md -[version-file-parser]: version-file-parser/README.md --- diff --git a/SECURITY.md b/SECURITY.md index 1b941c0..0a9e4ef 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -231,7 +231,7 @@ When security issues are fixed: - Replaced custom Bun installation with official action - Replaced custom Trivy installation with official action - Added secret masking to 7 critical actions (including docker-publish) -- Optimized file hashing in common-cache +- Migrated from custom common-cache to official actions/cache - Status: ✅ Complete ### Phase 3: Documentation & Policy (2024) diff --git a/_tests/integration/workflows/common-cache-test.yml b/_tests/integration/workflows/common-cache-test.yml deleted file mode 100644 index 750c482..0000000 --- a/_tests/integration/workflows/common-cache-test.yml +++ /dev/null @@ -1,471 +0,0 @@ ---- -name: Integration Test - Common Cache -on: - workflow_dispatch: - push: - paths: - - 'common-cache/**' - - '_tests/integration/workflows/common-cache-test.yml' - -jobs: - test-common-cache-key-generation: - name: Test Cache Key Generation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test basic key generation - run: | - RUNNER_OS="Linux" - CACHE_TYPE="npm" - KEY_PREFIX="" - - cache_key="$RUNNER_OS" - [ -n "$CACHE_TYPE" ] && cache_key="${cache_key}-${CACHE_TYPE}" - - expected="Linux-npm" - if [[ "$cache_key" != "$expected" ]]; then - echo "❌ ERROR: Expected '$expected', got '$cache_key'" - exit 1 - fi - echo "✓ Basic cache key generation works" - - - name: Test key with prefix - run: | - RUNNER_OS="Linux" - CACHE_TYPE="npm" - KEY_PREFIX="node-20" - - cache_key="$RUNNER_OS" - [ -n "$KEY_PREFIX" ] && cache_key="${cache_key}-${KEY_PREFIX}" - [ -n "$CACHE_TYPE" ] && cache_key="${cache_key}-${CACHE_TYPE}" - - expected="Linux-node-20-npm" - if [[ "$cache_key" != "$expected" ]]; then - echo "❌ ERROR: Expected '$expected', got '$cache_key'" - exit 1 - fi - echo "✓ Cache key with prefix works" - - - name: Test OS-specific keys - run: | - for os in "Linux" "macOS" "Windows"; do - CACHE_TYPE="test" - cache_key="$os-$CACHE_TYPE" - if [[ ! "$cache_key" =~ ^(Linux|macOS|Windows)-test$ ]]; then - echo "❌ ERROR: Invalid key for OS $os: $cache_key" - exit 1 - fi - echo "✓ OS-specific key for $os: $cache_key" - done - - test-common-cache-file-hashing: - name: Test File Hashing - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Create test files - run: | - mkdir -p test-cache - cd test-cache - echo "content1" > file1.txt - echo "content2" > file2.txt - echo "content3" > file3.txt - - - name: Test single file hash - run: | - cd test-cache - file_hash=$(cat file1.txt | sha256sum | cut -d' ' -f1) - - if [[ ! "$file_hash" =~ ^[a-f0-9]{64}$ ]]; then - echo "❌ ERROR: Invalid hash format: $file_hash" - exit 1 - fi - echo "✓ Single file hash: $file_hash" - - - name: Test multiple file hash - run: | - cd test-cache - multi_hash=$(cat file1.txt file2.txt file3.txt | sha256sum | cut -d' ' -f1) - - if [[ ! "$multi_hash" =~ ^[a-f0-9]{64}$ ]]; then - echo "❌ ERROR: Invalid hash format: $multi_hash" - exit 1 - fi - echo "✓ Multiple file hash: $multi_hash" - - - name: Test hash changes with content - run: | - cd test-cache - - # Get initial hash - hash1=$(cat file1.txt | sha256sum | cut -d' ' -f1) - - # Modify file - echo "modified" > file1.txt - - # Get new hash - hash2=$(cat file1.txt | sha256sum | cut -d' ' -f1) - - if [[ "$hash1" == "$hash2" ]]; then - echo "❌ ERROR: Hash should change when content changes" - exit 1 - fi - echo "✓ Hash changes with content modification" - - - name: Test comma-separated file list processing - run: | - cd test-cache - - KEY_FILES="file1.txt,file2.txt,file3.txt" - IFS=',' read -ra FILES <<< "$KEY_FILES" - - existing_files=() - for file in "${FILES[@]}"; do - file=$(echo "$file" | xargs) - if [ -f "$file" ]; then - existing_files+=("$file") - fi - done - - if [ ${#existing_files[@]} -ne 3 ]; then - echo "❌ ERROR: Should find 3 files, found ${#existing_files[@]}" - exit 1 - fi - - echo "✓ Comma-separated file list processing works" - - - name: Test missing file handling - run: | - cd test-cache - - KEY_FILES="file1.txt,missing.txt,file2.txt" - IFS=',' read -ra FILES <<< "$KEY_FILES" - - existing_files=() - for file in "${FILES[@]}"; do - file=$(echo "$file" | xargs) - if [ -f "$file" ]; then - existing_files+=("$file") - fi - done - - if [ ${#existing_files[@]} -ne 2 ]; then - echo "❌ ERROR: Should find 2 files, found ${#existing_files[@]}" - exit 1 - fi - - echo "✓ Missing files correctly skipped" - - test-common-cache-env-vars: - name: Test Environment Variables - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test single env var inclusion - run: | - export NODE_VERSION="20.9.0" - ENV_VARS="NODE_VERSION" - - IFS=',' read -ra VARS <<< "$ENV_VARS" - env_hash="" - for var in "${VARS[@]}"; do - if [ -n "${!var}" ]; then - env_hash="${env_hash}-${var}-${!var}" - fi - done - - expected="-NODE_VERSION-20.9.0" - if [[ "$env_hash" != "$expected" ]]; then - echo "❌ ERROR: Expected '$expected', got '$env_hash'" - exit 1 - fi - echo "✓ Single env var inclusion works" - - - name: Test multiple env vars - run: | - export NODE_VERSION="20.9.0" - export PACKAGE_MANAGER="npm" - ENV_VARS="NODE_VERSION,PACKAGE_MANAGER" - - IFS=',' read -ra VARS <<< "$ENV_VARS" - env_hash="" - for var in "${VARS[@]}"; do - if [ -n "${!var}" ]; then - env_hash="${env_hash}-${var}-${!var}" - fi - done - - expected="-NODE_VERSION-20.9.0-PACKAGE_MANAGER-npm" - if [[ "$env_hash" != "$expected" ]]; then - echo "❌ ERROR: Expected '$expected', got '$env_hash'" - exit 1 - fi - echo "✓ Multiple env vars inclusion works" - - - name: Test undefined env var skipping - run: | - export NODE_VERSION="20.9.0" - ENV_VARS="NODE_VERSION,UNDEFINED_VAR" - - IFS=',' read -ra VARS <<< "$ENV_VARS" - env_hash="" - for var in "${VARS[@]}"; do - if [ -n "${!var}" ]; then - env_hash="${env_hash}-${var}-${!var}" - fi - done - - # Should only include NODE_VERSION - expected="-NODE_VERSION-20.9.0" - if [[ "$env_hash" != "$expected" ]]; then - echo "❌ ERROR: Expected '$expected', got '$env_hash'" - exit 1 - fi - echo "✓ Undefined env vars correctly skipped" - - test-common-cache-path-processing: - name: Test Path Processing - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test single path - run: | - CACHE_PATHS="~/.npm" - IFS=',' read -ra PATHS <<< "$CACHE_PATHS" - - if [ ${#PATHS[@]} -ne 1 ]; then - echo "❌ ERROR: Should have 1 path, got ${#PATHS[@]}" - exit 1 - fi - echo "✓ Single path processing works" - - - name: Test multiple paths - run: | - CACHE_PATHS="~/.npm,~/.yarn/cache,node_modules" - IFS=',' read -ra PATHS <<< "$CACHE_PATHS" - - if [ ${#PATHS[@]} -ne 3 ]; then - echo "❌ ERROR: Should have 3 paths, got ${#PATHS[@]}" - exit 1 - fi - echo "✓ Multiple paths processing works" - - - name: Test path with spaces (trimming) - run: | - CACHE_PATHS=" ~/.npm , ~/.yarn/cache , node_modules " - IFS=',' read -ra PATHS <<< "$CACHE_PATHS" - - trimmed_paths=() - for path in "${PATHS[@]}"; do - trimmed=$(echo "$path" | xargs) - trimmed_paths+=("$trimmed") - done - - # Check first path is trimmed - if [[ "${trimmed_paths[0]}" != "~/.npm" ]]; then - echo "❌ ERROR: Path not trimmed: '${trimmed_paths[0]}'" - exit 1 - fi - echo "✓ Path trimming works" - - test-common-cache-complete-key-generation: - name: Test Complete Key Generation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Create test files - run: | - mkdir -p test-complete - cd test-complete - echo "package-lock content" > package-lock.json - - - name: Test complete cache key with all components - run: | - cd test-complete - - RUNNER_OS="Linux" - CACHE_TYPE="npm" - KEY_PREFIX="node-20" - - # Generate file hash - files_hash=$(cat package-lock.json | sha256sum | cut -d' ' -f1) - - # Generate env hash - export NODE_VERSION="20.9.0" - env_hash="-NODE_VERSION-20.9.0" - - # Generate final key - cache_key="$RUNNER_OS" - [ -n "$KEY_PREFIX" ] && cache_key="${cache_key}-${KEY_PREFIX}" - [ -n "$CACHE_TYPE" ] && cache_key="${cache_key}-${CACHE_TYPE}" - [ -n "$files_hash" ] && cache_key="${cache_key}-${files_hash}" - [ -n "$env_hash" ] && cache_key="${cache_key}${env_hash}" - - echo "Generated cache key: $cache_key" - - # Verify structure - if [[ ! "$cache_key" =~ ^Linux-node-20-npm-[a-f0-9]{64}-NODE_VERSION-20\.9\.0$ ]]; then - echo "❌ ERROR: Invalid cache key structure: $cache_key" - exit 1 - fi - echo "✓ Complete cache key generation works" - - test-common-cache-restore-keys: - name: Test Restore Keys - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test single restore key - run: | - RESTORE_KEYS="Linux-npm-" - - if [[ -z "$RESTORE_KEYS" ]]; then - echo "❌ ERROR: Restore keys should not be empty" - exit 1 - fi - echo "✓ Single restore key: $RESTORE_KEYS" - - - name: Test multiple restore keys - run: | - RESTORE_KEYS="Linux-node-20-npm-,Linux-node-npm-,Linux-npm-" - - IFS=',' read -ra KEYS <<< "$RESTORE_KEYS" - if [ ${#KEYS[@]} -ne 3 ]; then - echo "❌ ERROR: Should have 3 restore keys, got ${#KEYS[@]}" - exit 1 - fi - echo "✓ Multiple restore keys work" - - test-common-cache-type-specific-scenarios: - name: Test Type-Specific Scenarios - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test NPM cache key - run: | - TYPE="npm" - FILES="package-lock.json" - PATHS="~/.npm,node_modules" - - echo "✓ NPM cache configuration valid" - echo " Type: $TYPE" - echo " Key files: $FILES" - echo " Paths: $PATHS" - - - name: Test Composer cache key - run: | - TYPE="composer" - FILES="composer.lock" - PATHS="~/.composer/cache,vendor" - - echo "✓ Composer cache configuration valid" - echo " Type: $TYPE" - echo " Key files: $FILES" - echo " Paths: $PATHS" - - - name: Test Go cache key - run: | - TYPE="go" - FILES="go.sum" - PATHS="~/go/pkg/mod,~/.cache/go-build" - - echo "✓ Go cache configuration valid" - echo " Type: $TYPE" - echo " Key files: $FILES" - echo " Paths: $PATHS" - - - name: Test Pip cache key - run: | - TYPE="pip" - FILES="requirements.txt" - PATHS="~/.cache/pip" - - echo "✓ Pip cache configuration valid" - echo " Type: $TYPE" - echo " Key files: $FILES" - echo " Paths: $PATHS" - - test-common-cache-edge-cases: - name: Test Edge Cases - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test empty prefix - run: | - KEY_PREFIX="" - cache_key="Linux" - [ -n "$KEY_PREFIX" ] && cache_key="${cache_key}-${KEY_PREFIX}" - - if [[ "$cache_key" != "Linux" ]]; then - echo "❌ ERROR: Empty prefix should not modify key" - exit 1 - fi - echo "✓ Empty prefix handling works" - - - name: Test no key files - run: | - KEY_FILES="" - files_hash="" - - if [ -n "$KEY_FILES" ]; then - echo "❌ ERROR: Should detect empty key files" - exit 1 - fi - echo "✓ No key files handling works" - - - name: Test no env vars - run: | - ENV_VARS="" - env_hash="" - - if [ -n "$ENV_VARS" ]; then - echo "❌ ERROR: Should detect empty env vars" - exit 1 - fi - echo "✓ No env vars handling works" - - integration-test-summary: - name: Integration Test Summary - runs-on: ubuntu-latest - needs: - - test-common-cache-key-generation - - test-common-cache-file-hashing - - test-common-cache-env-vars - - test-common-cache-path-processing - - test-common-cache-complete-key-generation - - test-common-cache-restore-keys - - test-common-cache-type-specific-scenarios - - test-common-cache-edge-cases - steps: - - name: Summary - run: | - echo "==========================================" - echo "Common Cache Integration Tests - PASSED" - echo "==========================================" - echo "" - echo "✓ Cache key generation tests" - echo "✓ File hashing tests" - echo "✓ Environment variable tests" - echo "✓ Path processing tests" - echo "✓ Complete key generation tests" - echo "✓ Restore keys tests" - echo "✓ Type-specific scenario tests" - echo "✓ Edge case tests" - echo "" - echo "All common-cache integration tests completed successfully!" diff --git a/_tests/integration/workflows/lint-fix-chain-test.yml b/_tests/integration/workflows/lint-fix-chain-test.yml index 3ca276b..86a7f8f 100644 --- a/_tests/integration/workflows/lint-fix-chain-test.yml +++ b/_tests/integration/workflows/lint-fix-chain-test.yml @@ -7,7 +7,6 @@ on: - 'eslint-lint/**' - 'prettier-lint/**' - 'node-setup/**' - - 'common-cache/**' - '_tests/integration/workflows/lint-fix-chain-test.yml' jobs: diff --git a/_tests/integration/workflows/node-setup-test.yml b/_tests/integration/workflows/node-setup-test.yml deleted file mode 100644 index 98d4e03..0000000 --- a/_tests/integration/workflows/node-setup-test.yml +++ /dev/null @@ -1,513 +0,0 @@ ---- -name: Integration Test - Node Setup -on: - workflow_dispatch: - push: - paths: - - 'node-setup/**' - - 'version-file-parser/**' - - 'common-cache/**' - - 'common-retry/**' - - '_tests/integration/workflows/node-setup-test.yml' - -jobs: - test-node-setup-version-validation: - name: Test Version Validation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test invalid default version format (alphabetic) - run: | - VERSION="abc" - if [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then - echo "❌ ERROR: Should reject alphabetic version" - exit 1 - fi - echo "✓ Alphabetic version correctly rejected" - - - name: Test invalid default version (too low) - run: | - VERSION="10" - major=$(echo "$VERSION" | cut -d'.' -f1) - if [ "$major" -lt 14 ] || [ "$major" -gt 30 ]; then - echo "✓ Version $VERSION correctly rejected (major < 14)" - else - echo "❌ ERROR: Should reject Node.js $VERSION" - exit 1 - fi - - - name: Test invalid default version (too high) - run: | - VERSION="50" - major=$(echo "$VERSION" | cut -d'.' -f1) - if [ "$major" -lt 14 ] || [ "$major" -gt 30 ]; then - echo "✓ Version $VERSION correctly rejected (major > 30)" - else - echo "❌ ERROR: Should reject Node.js $VERSION" - exit 1 - fi - - - name: Test valid version formats - run: | - for version in "20" "20.9" "20.9.0" "18" "22.1.0"; do - if [[ "$version" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then - major=$(echo "$version" | cut -d'.' -f1) - if [ "$major" -ge 14 ] && [ "$major" -le 30 ]; then - echo "✓ Version $version accepted" - else - echo "❌ ERROR: Version $version should be accepted" - exit 1 - fi - else - echo "❌ ERROR: Version $version format validation failed" - exit 1 - fi - done - - test-node-setup-package-manager-validation: - name: Test Package Manager Validation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test valid package managers - run: | - for pm in "npm" "yarn" "pnpm" "bun" "auto"; do - case "$pm" in - "npm"|"yarn"|"pnpm"|"bun"|"auto") - echo "✓ Package manager $pm accepted" - ;; - *) - echo "❌ ERROR: Valid package manager $pm rejected" - exit 1 - ;; - esac - done - - - name: Test invalid package manager - run: | - PM="invalid-pm" - case "$PM" in - "npm"|"yarn"|"pnpm"|"bun"|"auto") - echo "❌ ERROR: Invalid package manager should be rejected" - exit 1 - ;; - *) - echo "✓ Invalid package manager correctly rejected" - ;; - esac - - test-node-setup-url-validation: - name: Test URL Validation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test valid registry URLs - run: | - for url in "https://registry.npmjs.org" "http://localhost:4873" "https://npm.custom.com/"; do - if [[ "$url" == "https://"* ]] || [[ "$url" == "http://"* ]]; then - echo "✓ Registry URL $url accepted" - else - echo "❌ ERROR: Valid URL $url rejected" - exit 1 - fi - done - - - name: Test invalid registry URLs - run: | - for url in "ftp://registry.com" "not-a-url" "registry.com"; do - if [[ "$url" == "https://"* ]] || [[ "$url" == "http://"* ]]; then - echo "❌ ERROR: Invalid URL $url should be rejected" - exit 1 - else - echo "✓ Invalid URL $url correctly rejected" - fi - done - - test-node-setup-retries-validation: - name: Test Retries Validation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test valid retry counts - run: | - for retries in "1" "3" "5" "10"; do - if [[ "$retries" =~ ^[0-9]+$ ]] && [ "$retries" -gt 0 ] && [ "$retries" -le 10 ]; then - echo "✓ Max retries $retries accepted" - else - echo "❌ ERROR: Valid retry count $retries rejected" - exit 1 - fi - done - - - name: Test invalid retry counts - run: | - for retries in "0" "11" "abc" "-1"; do - if [[ "$retries" =~ ^[0-9]+$ ]] && [ "$retries" -gt 0 ] && [ "$retries" -le 10 ]; then - echo "❌ ERROR: Invalid retry count $retries should be rejected" - exit 1 - else - echo "✓ Invalid retry count $retries correctly rejected" - fi - done - - test-node-setup-boolean-validation: - name: Test Boolean Validation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test valid boolean values - run: | - for value in "true" "false"; do - if [[ "$value" == "true" ]] || [[ "$value" == "false" ]]; then - echo "✓ Boolean value $value accepted" - else - echo "❌ ERROR: Valid boolean $value rejected" - exit 1 - fi - done - - - name: Test invalid boolean values - run: | - for value in "yes" "no" "1" "0" "True" "FALSE" ""; do - if [[ "$value" != "true" ]] && [[ "$value" != "false" ]]; then - echo "✓ Invalid boolean value '$value' correctly rejected" - else - echo "❌ ERROR: Invalid boolean $value should be rejected" - exit 1 - fi - done - - test-node-setup-token-validation: - name: Test Auth Token Validation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test injection pattern detection - run: | - for token in "token;malicious" "token&&command" "token|pipe"; do - if [[ "$token" == *";"* ]] || [[ "$token" == *"&&"* ]] || [[ "$token" == *"|"* ]]; then - echo "✓ Injection pattern in token correctly detected" - else - echo "❌ ERROR: Should detect injection pattern in: $token" - exit 1 - fi - done - - - name: Test valid tokens - run: | - for token in "npm_AbCdEf1234567890" "github_pat_12345abcdef" "simple-token"; do - if [[ "$token" == *";"* ]] || [[ "$token" == *"&&"* ]] || [[ "$token" == *"|"* ]]; then - echo "❌ ERROR: Valid token should not be rejected: $token" - exit 1 - else - echo "✓ Valid token accepted" - fi - done - - test-node-setup-package-manager-resolution: - name: Test Package Manager Resolution - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test auto detection with detected PM - run: | - INPUT_PM="auto" - DETECTED_PM="pnpm" - - if [ "$INPUT_PM" = "auto" ]; then - if [ -n "$DETECTED_PM" ]; then - FINAL_PM="$DETECTED_PM" - else - FINAL_PM="npm" - fi - else - FINAL_PM="$INPUT_PM" - fi - - if [[ "$FINAL_PM" != "pnpm" ]]; then - echo "❌ ERROR: Should use detected PM (pnpm)" - exit 1 - fi - echo "✓ Auto-detected package manager correctly resolved" - - - name: Test auto detection without detected PM - run: | - INPUT_PM="auto" - DETECTED_PM="" - - if [ "$INPUT_PM" = "auto" ]; then - if [ -n "$DETECTED_PM" ]; then - FINAL_PM="$DETECTED_PM" - else - FINAL_PM="npm" - fi - else - FINAL_PM="$INPUT_PM" - fi - - if [[ "$FINAL_PM" != "npm" ]]; then - echo "❌ ERROR: Should default to npm" - exit 1 - fi - echo "✓ Defaults to npm when no detection" - - - name: Test explicit package manager - run: | - INPUT_PM="yarn" - DETECTED_PM="pnpm" - - if [ "$INPUT_PM" = "auto" ]; then - if [ -n "$DETECTED_PM" ]; then - FINAL_PM="$DETECTED_PM" - else - FINAL_PM="npm" - fi - else - FINAL_PM="$INPUT_PM" - fi - - if [[ "$FINAL_PM" != "yarn" ]]; then - echo "❌ ERROR: Should use explicit PM (yarn)" - exit 1 - fi - echo "✓ Explicit package manager correctly used" - - test-node-setup-feature-detection: - name: Test Feature Detection - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Create test package.json with ESM - run: | - mkdir -p test-esm - cd test-esm - cat > package.json <<'EOF' - { - "name": "test-esm", - "version": "1.0.0", - "type": "module" - } - EOF - - - name: Test ESM detection - run: | - cd test-esm - if command -v jq >/dev/null 2>&1; then - pkg_type=$(jq -r '.type // "commonjs"' package.json 2>/dev/null) - if [[ "$pkg_type" == "module" ]]; then - echo "✓ ESM support correctly detected" - else - echo "❌ ERROR: Should detect ESM support" - exit 1 - fi - else - echo "⚠️ jq not available, skipping ESM detection test" - echo "✓ ESM detection logic verified (jq would be required in actual action)" - fi - - - name: Create test with TypeScript - run: | - mkdir -p test-ts - cd test-ts - touch tsconfig.json - cat > package.json <<'EOF' - { - "name": "test-ts", - "devDependencies": { - "typescript": "^5.0.0" - } - } - EOF - - - name: Test TypeScript detection - run: | - cd test-ts - typescript_support="false" - if [ -f tsconfig.json ]; then - typescript_support="true" - fi - if [[ "$typescript_support" != "true" ]]; then - echo "❌ ERROR: Should detect TypeScript" - exit 1 - fi - echo "✓ TypeScript support correctly detected" - - - name: Create test with frameworks - run: | - mkdir -p test-frameworks - cd test-frameworks - cat > package.json <<'EOF' - { - "name": "test-frameworks", - "dependencies": { - "react": "^18.0.0", - "next": "^14.0.0" - } - } - EOF - - - name: Test framework detection - run: | - cd test-frameworks - if command -v jq >/dev/null 2>&1; then - has_next=$(jq -e '.dependencies.next or .devDependencies.next' package.json >/dev/null 2>&1 && echo "yes" || echo "no") - has_react=$(jq -e '.dependencies.react or .devDependencies.react' package.json >/dev/null 2>&1 && echo "yes" || echo "no") - - if [[ "$has_next" == "yes" ]] && [[ "$has_react" == "yes" ]]; then - echo "✓ Frameworks (Next.js, React) correctly detected" - else - echo "❌ ERROR: Should detect Next.js and React" - exit 1 - fi - else - echo "⚠️ jq not available, skipping framework detection test" - echo "✓ Framework detection logic verified (jq would be required in actual action)" - fi - - test-node-setup-security: - name: Test Security Measures - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test token sanitization - run: | - TOKEN="test-token - with-newline" - - # Should remove newlines - sanitized=$(echo "$TOKEN" | tr -d '\n\r') - - if [[ "$sanitized" == *$'\n'* ]] || [[ "$sanitized" == *$'\r'* ]]; then - echo "❌ ERROR: Newlines not removed" - exit 1 - fi - echo "✓ Token sanitization works correctly" - - - name: Test package manager sanitization - run: | - PM="npm - with-newline" - - # Should remove newlines - sanitized=$(echo "$PM" | tr -d '\n\r') - - if [[ "$sanitized" == *$'\n'* ]] || [[ "$sanitized" == *$'\r'* ]]; then - echo "❌ ERROR: Newlines not removed from PM" - exit 1 - fi - echo "✓ Package manager sanitization works correctly" - - test-node-setup-integration-workflow: - name: Test Integration Workflow - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Simulate complete workflow - run: | - echo "=== Simulating Node Setup Workflow ===" - - # 1. Validation - echo "Step 1: Validate inputs" - DEFAULT_VERSION="20" - PACKAGE_MANAGER="npm" - REGISTRY_URL="https://registry.npmjs.org" - CACHE="true" - INSTALL="true" - MAX_RETRIES="3" - echo "✓ Inputs validated" - - # 2. Version parsing - echo "Step 2: Parse Node.js version" - NODE_VERSION="20.9.0" - echo "✓ Version parsed: $NODE_VERSION" - - # 3. Package manager resolution - echo "Step 3: Resolve package manager" - if [ "$PACKAGE_MANAGER" = "auto" ]; then - FINAL_PM="npm" - else - FINAL_PM="$PACKAGE_MANAGER" - fi - echo "✓ Package manager resolved: $FINAL_PM" - - # 4. Setup Node.js - echo "Step 4: Setup Node.js $NODE_VERSION" - if command -v node >/dev/null 2>&1; then - echo "✓ Node.js available: $(node --version)" - fi - - # 5. Enable Corepack - echo "Step 5: Enable Corepack" - if command -v corepack >/dev/null 2>&1; then - echo "✓ Corepack available" - else - echo "⚠️ Corepack not available in test environment" - fi - - # 6. Cache dependencies - if [[ "$CACHE" == "true" ]]; then - echo "Step 6: Cache dependencies" - echo "✓ Would use common-cache action" - fi - - # 7. Install dependencies - if [[ "$INSTALL" == "true" ]]; then - echo "Step 7: Install dependencies" - echo "✓ Would run: $FINAL_PM install" - fi - - echo "=== Workflow simulation completed ===" - - integration-test-summary: - name: Integration Test Summary - runs-on: ubuntu-latest - needs: - - test-node-setup-version-validation - - test-node-setup-package-manager-validation - - test-node-setup-url-validation - - test-node-setup-retries-validation - - test-node-setup-boolean-validation - - test-node-setup-token-validation - - test-node-setup-package-manager-resolution - - test-node-setup-feature-detection - - test-node-setup-security - - test-node-setup-integration-workflow - steps: - - name: Summary - run: | - echo "==========================================" - echo "Node Setup Integration Tests - PASSED" - echo "==========================================" - echo "" - echo "✓ Version validation tests" - echo "✓ Package manager validation tests" - echo "✓ URL validation tests" - echo "✓ Retries validation tests" - echo "✓ Boolean validation tests" - echo "✓ Token validation tests" - echo "✓ Package manager resolution tests" - echo "✓ Feature detection tests" - echo "✓ Security measure tests" - echo "✓ Integration workflow tests" - echo "" - echo "All node-setup integration tests completed successfully!" diff --git a/_tests/integration/workflows/version-file-parser-test.yml b/_tests/integration/workflows/version-file-parser-test.yml deleted file mode 100644 index 2c2d83b..0000000 --- a/_tests/integration/workflows/version-file-parser-test.yml +++ /dev/null @@ -1,241 +0,0 @@ ---- -name: Test version-file-parser Integration -on: - workflow_dispatch: - push: - paths: - - 'version-file-parser/**' - - '_tests/integration/workflows/version-file-parser-test.yml' - -jobs: - test-version-file-parser: - runs-on: ubuntu-latest - strategy: - matrix: - test-case: - - name: 'Node.js project' - language: 'node' - tool-versions-key: 'nodejs' - dockerfile-image: 'node' - expected-version: '18.0.0' - setup-files: | - echo "18.17.0" > .nvmrc - cat > package.json <=18.0.0" } - } - EOF - touch package-lock.json - - - name: 'PHP project' - language: 'php' - tool-versions-key: 'php' - dockerfile-image: 'php' - expected-version: '8.1' - setup-files: | - cat > composer.json < .python-version - cat > pyproject.toml < go.mod < .tool-versions - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Clean up test files from previous runs - run: | - rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions - - - name: Setup test files - run: ${{ matrix.test-case.setup-files }} - - - name: Test version-file-parser - id: test-action - uses: ./version-file-parser - with: - language: ${{ matrix.test-case.language }} - tool-versions-key: ${{ matrix.test-case.tool-versions-key }} - dockerfile-image: ${{ matrix.test-case.dockerfile-image }} - default-version: '1.0.0' - - - name: Validate outputs - run: | - echo "Test case: ${{ matrix.test-case.name }}" - echo "Expected version: ${{ matrix.test-case.expected-version }}" - echo "Detected version: ${{ steps.test-action.outputs.detected-version }}" - echo "Package manager: ${{ steps.test-action.outputs.package-manager }}" - - # Validate that we got some version - if [[ -z "${{ steps.test-action.outputs.detected-version }}" ]]; then - echo "❌ ERROR: No version detected" - exit 1 - fi - - # Validate version format (basic semver check) - if ! echo "${{ steps.test-action.outputs.detected-version }}" | grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?'; then - echo "❌ ERROR: Invalid version format: ${{ steps.test-action.outputs.detected-version }}" - exit 1 - fi - - # Validate detected version matches expected version (not the fallback) - if [[ "${{ steps.test-action.outputs.detected-version }}" != "${{ matrix.test-case.expected-version }}" ]]; then - echo "❌ ERROR: Version mismatch" - echo "Expected: ${{ matrix.test-case.expected-version }}" - echo "Got: ${{ steps.test-action.outputs.detected-version }}" - exit 1 - fi - - echo "✅ Version validation passed" - - # Skip external reference test in local/CI environment to avoid auth issues - - name: Test external reference (info only) - run: | - echo "External reference test would use: ivuorinen/actions/version-file-parser@main" - echo "Skipping to avoid authentication issues in local testing" - - test-edge-cases: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Clean up test files from previous runs - run: | - rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions - - - name: Setup test files (package.json engines) - shell: bash - run: | - set -Eeuo pipefail - cat > package.json <<'EOF' - { - "name": "edge-case", - "engines": { "node": ">=18.0.0" } - } - EOF - echo "18.17.0" > .nvmrc - - - name: Test version detection from existing files - id: existing-version - uses: ./version-file-parser - with: - language: 'node' - tool-versions-key: 'nodejs' - dockerfile-image: 'node' - default-version: '20.0.0' - - - name: Validate existing version detection - run: | - # The action detects Node.js version from package.json engines field - # package.json >=18.0.0 is parsed as 18.0.0 - # Note: .nvmrc exists but package.json takes precedence in this implementation - expected_version="18.0.0" - detected_version="${{ steps.existing-version.outputs.detected-version }}" - - if [[ "$detected_version" != "$expected_version" ]]; then - echo "❌ ERROR: Version mismatch" - echo "Expected: $expected_version" - echo "Got: $detected_version" - exit 1 - fi - echo "✅ Existing version detection works correctly" - - - name: Clean up before invalid regex test - run: | - rm -f .nvmrc package.json package-lock.json - - - name: Test with invalid regex - id: invalid-regex - uses: ./version-file-parser - with: - language: 'node' - tool-versions-key: 'nodejs' - dockerfile-image: 'node' - validation-regex: 'invalid[regex' - default-version: '18.0.0' - continue-on-error: true - - - name: Validate regex error handling - run: | - echo "Testing regex error handling completed" - # Action should handle invalid regex gracefully - if [ "${{ steps.invalid-regex.outcome }}" != "failure" ]; then - echo "::error::Expected invalid-regex step to fail, but it was: ${{ steps.invalid-regex.outcome }}" - exit 1 - fi - echo "✅ Invalid regex properly failed as expected" - - test-dockerfile-parsing: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Clean up test files from previous runs - run: | - rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions Dockerfile - - - name: Create Dockerfile with Node.js - run: | - cat > Dockerfile <>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "node-version" "18.0.0" -The status should be success -End - -It "detects version from .nvmrc file" -create_mock_node_repo -echo "18.17.1" >.nvmrc - -# Mock action output -echo "node-version=18.17.1" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "node-version" "18.17.1" -The status should be success -End - -It "uses default version when none specified" -create_mock_node_repo -# Remove engines field simulation - -# Mock default version output -echo "node-version=20.0.0" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "node-version" "20.0.0" -The status should be success -End -End - -Context "when testing package manager detection" -BeforeEach "shellspec_setup_test_env 'package-manager-detection'" -AfterEach "shellspec_cleanup_test_env 'package-manager-detection'" - -It "detects bun from bun.lockb" -create_mock_node_repo -touch bun.lockb - -echo "package-manager=bun" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "package-manager" "bun" -The status should be success -End - -It "detects pnpm from pnpm-lock.yaml" -create_mock_node_repo -touch pnpm-lock.yaml - -echo "package-manager=pnpm" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "package-manager" "pnpm" -The status should be success -End - -It "detects yarn from yarn.lock" -create_mock_node_repo -touch yarn.lock - -echo "package-manager=yarn" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "package-manager" "yarn" -The status should be success -End - -It "detects npm from package-lock.json" -create_mock_node_repo -touch package-lock.json - -echo "package-manager=npm" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "package-manager" "npm" -The status should be success -End - -It "detects packageManager field from package.json" -create_mock_node_repo - -# Add packageManager field to package.json -cat >package.json <=18.0.0" - } -} -EOF - -echo "package-manager=pnpm" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "package-manager" "pnpm" -The status should be success -End -End - -Context "when testing Corepack integration" -BeforeEach "shellspec_setup_test_env 'corepack-test'" -AfterEach "shellspec_cleanup_test_env 'corepack-test'" - -It "enables Corepack when packageManager is specified" -create_mock_node_repo - -# Simulate packageManager field -cat >package.json <>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "corepack-enabled" "true" -The status should be success -End -End - -Context "when testing cache functionality" -BeforeEach "shellspec_setup_test_env 'cache-test'" -AfterEach "shellspec_cleanup_test_env 'cache-test'" - -It "reports cache hit when dependencies are cached" -create_mock_node_repo -touch package-lock.json -mkdir -p node_modules - -# Mock cache hit -echo "cache-hit=true" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "cache-hit" "true" -The status should be success -End - -It "reports cache miss when no cache exists" -create_mock_node_repo -touch package-lock.json - -# Mock cache miss -echo "cache-hit=false" >>"$GITHUB_OUTPUT" - -When call shellspec_validate_action_output "cache-hit" "false" -The status should be success -End -End - -Context "when testing output consistency" -It "produces all expected outputs" -When call test_action_outputs "$ACTION_DIR" "node-version" "18.0.0" "package-manager" "npm" -The status should be success -The stderr should include "Testing action outputs for: node-setup" -The stderr should include "Output test passed for: node-setup" -End -End -End diff --git a/_tests/unit/php-composer/validation.spec.sh b/_tests/unit/php-composer/validation.spec.sh deleted file mode 100755 index 4ba8ed0..0000000 --- a/_tests/unit/php-composer/validation.spec.sh +++ /dev/null @@ -1,407 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for php-composer action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "php-composer action" -ACTION_DIR="php-composer" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating php input" -It "accepts valid PHP version" -When call validate_input_python "php-composer" "php" "8.4" -The status should be success -End - -It "accepts PHP version with patch" -When call validate_input_python "php-composer" "php" "8.4.1" -The status should be success -End - -It "accepts PHP 7.4" -When call validate_input_python "php-composer" "php" "7.4" -The status should be success -End - -It "accepts PHP 8.0" -When call validate_input_python "php-composer" "php" "8.0" -The status should be success -End - -It "accepts PHP 8.1" -When call validate_input_python "php-composer" "php" "8.1" -The status should be success -End - -It "rejects PHP version too old" -When call validate_input_python "php-composer" "php" "5.5" -The status should be failure -End - -It "rejects invalid version format" -When call validate_input_python "php-composer" "php" "php8.4" -The status should be failure -End - -It "rejects version with command injection" -When call validate_input_python "php-composer" "php" "8.4; rm -rf /" -The status should be failure -End - -It "rejects empty version" -When call validate_input_python "php-composer" "php" "" -The status should be failure -End -End - -Context "when validating extensions input" -It "accepts valid PHP extensions" -When call validate_input_python "php-composer" "extensions" "mbstring, xml, zip" -The status should be success -End - -It "accepts single extension" -When call validate_input_python "php-composer" "extensions" "mbstring" -The status should be success -End - -It "accepts extensions without spaces" -When call validate_input_python "php-composer" "extensions" "mbstring,xml,zip" -The status should be success -End - -It "accepts extensions with underscores" -When call validate_input_python "php-composer" "extensions" "pdo_mysql, gd_jpeg" -The status should be success -End - -It "rejects extensions with special characters" -When call validate_input_python "php-composer" "extensions" "mbstring@xml" -The status should be failure -End - -It "rejects extensions with command injection" -When call validate_input_python "php-composer" "extensions" "mbstring; rm -rf /" -The status should be failure -End - -It "rejects empty extensions" -When call validate_input_python "php-composer" "extensions" "" -The status should be failure -End -End - -Context "when validating tools input" -It "accepts valid Composer tools" -When call validate_input_python "php-composer" "tools" "composer:v2" -The status should be success -End - -It "accepts multiple tools" -When call validate_input_python "php-composer" "tools" "composer:v2, phpunit:^9.0" -The status should be success -End - -It "accepts tools with version constraints" -When call validate_input_python "php-composer" "tools" "phpcs, phpstan:1.10" -The status should be success -End - -It "accepts tools with stability flags (@ allowed)" -When call validate_input_python "php-composer" "tools" "dev-master@dev" -The status should be success -End - -It "accepts tools with version and stability flag" -When call validate_input_python "php-composer" "tools" "monolog/monolog@dev" -The status should be success -End - -It "rejects tools with backticks" -When call validate_input_python "php-composer" "tools" "composer\`whoami\`" -The status should be failure -End - -It "rejects tools with command injection" -When call validate_input_python "php-composer" "tools" "composer; rm -rf /" -The status should be failure -End - -It "rejects empty tools" -When call validate_input_python "php-composer" "tools" "" -The status should be failure -End -End - -Context "when validating composer-version input" -It "accepts composer version 1" -When call validate_input_python "php-composer" "composer-version" "1" -The status should be success -End - -It "accepts composer version 2" -When call validate_input_python "php-composer" "composer-version" "2" -The status should be success -End - -It "rejects invalid composer version" -When call validate_input_python "php-composer" "composer-version" "3" -The status should be failure -End - -It "rejects non-numeric composer version" -When call validate_input_python "php-composer" "composer-version" "latest" -The status should be failure -End - -It "rejects empty composer version" -When call validate_input_python "php-composer" "composer-version" "" -The status should be failure -End -End - -Context "when validating stability input" -It "accepts stable" -When call validate_input_python "php-composer" "stability" "stable" -The status should be success -End - -It "accepts RC" -When call validate_input_python "php-composer" "stability" "RC" -The status should be success -End - -It "accepts beta" -When call validate_input_python "php-composer" "stability" "beta" -The status should be success -End - -It "accepts alpha" -When call validate_input_python "php-composer" "stability" "alpha" -The status should be success -End - -It "accepts dev" -When call validate_input_python "php-composer" "stability" "dev" -The status should be success -End - -It "rejects invalid stability" -When call validate_input_python "php-composer" "stability" "unstable" -The status should be failure -End - -It "rejects stability with injection" -When call validate_input_python "php-composer" "stability" "stable; rm -rf /" -The status should be failure -End -End - -Context "when validating cache-directories input" -It "accepts valid cache directory" -When call validate_input_python "php-composer" "cache-directories" "vendor/cache" -The status should be success -End - -It "accepts multiple cache directories" -When call validate_input_python "php-composer" "cache-directories" "vendor/cache, .cache" -The status should be success -End - -It "accepts directories with underscores and hyphens" -When call validate_input_python "php-composer" "cache-directories" "cache_dir, cache-dir" -The status should be success -End - -It "rejects path traversal" -When call validate_input_python "php-composer" "cache-directories" "../malicious" -The status should be failure -End - -It "rejects absolute paths" -When call validate_input_python "php-composer" "cache-directories" "/etc/passwd" -The status should be failure -End - -It "rejects directories with command injection" -When call validate_input_python "php-composer" "cache-directories" "cache; rm -rf /" -The status should be failure -End - -It "rejects empty cache directories" -When call validate_input_python "php-composer" "cache-directories" "" -The status should be failure -End -End - -Context "when validating token input" -It "accepts GitHub token expression" -When call validate_input_python "php-composer" "token" "\${{ github.token }}" -The status should be success -End - -It "accepts GitHub fine-grained token" -When call validate_input_python "php-composer" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890" -The status should be success -End - -It "accepts GitHub app token" -When call validate_input_python "php-composer" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890" -The status should be success -End - -It "rejects invalid token format" -When call validate_input_python "php-composer" "token" "invalid-token" -The status should be failure -End - -It "rejects empty token" -When call validate_input_python "php-composer" "token" "" -The status should be failure -End -End - -Context "when validating max-retries input" -It "accepts valid retry count" -When call validate_input_python "php-composer" "max-retries" "3" -The status should be success -End - -It "accepts minimum retries" -When call validate_input_python "php-composer" "max-retries" "1" -The status should be success -End - -It "accepts maximum retries" -When call validate_input_python "php-composer" "max-retries" "10" -The status should be success -End - -It "rejects zero retries" -When call validate_input_python "php-composer" "max-retries" "0" -The status should be failure -End - -It "rejects too many retries" -When call validate_input_python "php-composer" "max-retries" "11" -The status should be failure -End - -It "rejects non-numeric retries" -When call validate_input_python "php-composer" "max-retries" "many" -The status should be failure -End - -It "rejects negative retries" -When call validate_input_python "php-composer" "max-retries" "-1" -The status should be failure -End -End - -Context "when validating args input" -It "accepts valid Composer arguments" -When call validate_input_python "php-composer" "args" "--no-progress --prefer-dist" -The status should be success -End - -It "rejects empty args" -When call validate_input_python "php-composer" "args" "" -The status should be failure -End - -It "rejects args with command injection" -When call validate_input_python "php-composer" "args" "--no-progress; rm -rf /" -The status should be failure -End - -It "rejects args with pipe" -When call validate_input_python "php-composer" "args" "--no-progress | cat /etc/passwd" -The status should be failure -End -End - -Context "when checking action.yml structure" -It "has valid YAML syntax" -When call validate_action_yml_quiet "$ACTION_FILE" -The status should be success -End - -It "has correct action name" -name=$(get_action_name "$ACTION_FILE") -When call echo "$name" -The output should equal "Run Composer Install" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "php" -The output should include "extensions" -The output should include "tools" -The output should include "args" -The output should include "composer-version" -The output should include "stability" -The output should include "cache-directories" -The output should include "token" -The output should include "max-retries" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "lock" -The output should include "php-version" -The output should include "composer-version" -The output should include "cache-hit" -End -End - -Context "when testing input requirements" -It "requires php input" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php" "required" -The output should equal "required" -End - -It "has extensions as optional input" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "extensions" "optional" -The output should equal "optional" -End -End - -Context "when testing security validations" -It "validates against path traversal in cache directories" -When call validate_input_python "php-composer" "cache-directories" "../../etc/passwd" -The status should be failure -End - -It "validates against shell metacharacters in tools" -When call validate_input_python "php-composer" "tools" "composer && rm -rf /" -The status should be failure -End - -It "validates against backtick injection in args" -When call validate_input_python "php-composer" "args" "--no-progress \`whoami\`" -The status should be failure -End - -It "validates against variable expansion in extensions" -When call validate_input_python "php-composer" "extensions" "mbstring,\${HOME}" -The status should be failure -End -End - -Context "when testing PHP-specific validations" -It "validates PHP version boundaries" -When call validate_input_python "php-composer" "php" "10.0" -The status should be failure -End - -It "validates Composer version enum restriction" -When call validate_input_python "php-composer" "composer-version" "0" -The status should be failure -End - -It "validates stability enum values" -When call validate_input_python "php-composer" "stability" "experimental" -The status should be failure -End -End -End diff --git a/_tests/unit/php-laravel-phpunit/validation.spec.sh b/_tests/unit/php-laravel-phpunit/validation.spec.sh deleted file mode 100755 index cbc0c5b..0000000 --- a/_tests/unit/php-laravel-phpunit/validation.spec.sh +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for php-laravel-phpunit action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "php-laravel-phpunit action" -ACTION_DIR="php-laravel-phpunit" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating php-version input" -It "accepts latest" -When call validate_input_python "php-laravel-phpunit" "php-version" "latest" -The status should be success -End - -It "accepts valid PHP version" -When call validate_input_python "php-laravel-phpunit" "php-version" "8.4" -The status should be success -End - -It "accepts PHP version with patch" -When call validate_input_python "php-laravel-phpunit" "php-version" "8.4.1" -The status should be success -End - -It "accepts PHP 7.4" -When call validate_input_python "php-laravel-phpunit" "php-version" "7.4" -The status should be success -End - -It "accepts PHP 8.0" -When call validate_input_python "php-laravel-phpunit" "php-version" "8.0" -The status should be success -End - -It "rejects invalid version format" -When call validate_input_python "php-laravel-phpunit" "php-version" "php8.4" -The status should be failure -End - -It "rejects version with command injection" -When call validate_input_python "php-laravel-phpunit" "php-version" "8.4; rm -rf /" -The status should be failure -End - -It "accepts empty version (uses default)" -When call validate_input_python "php-laravel-phpunit" "php-version" "" -The status should be success -End -End - -Context "when validating php-version-file input" -It "accepts valid PHP version file" -When call validate_input_python "php-laravel-phpunit" "php-version-file" ".php-version" -The status should be success -End - -It "accepts custom version file" -When call validate_input_python "php-laravel-phpunit" "php-version-file" "custom-php-version" -The status should be success -End - -It "accepts version file with path" -When call validate_input_python "php-laravel-phpunit" "php-version-file" "config/.php-version" -The status should be success -End - -It "rejects path traversal in version file" -When call validate_input_python "php-laravel-phpunit" "php-version-file" "../../../etc/passwd" -The status should be failure -End - -It "rejects absolute path in version file" -When call validate_input_python "php-laravel-phpunit" "php-version-file" "/etc/passwd" -The status should be failure -End - -It "rejects version file with command injection" -When call validate_input_python "php-laravel-phpunit" "php-version-file" ".php-version; rm -rf /" -The status should be failure -End - -It "accepts empty version file" -When call validate_input_python "php-laravel-phpunit" "php-version-file" "" -The status should be success -End -End - -Context "when validating extensions input" -It "accepts valid PHP extensions" -When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring, intl, json" -The status should be success -End - -It "accepts single extension" -When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring" -The status should be success -End - -It "accepts extensions without spaces" -When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring,intl,json" -The status should be success -End - -It "accepts extensions with underscores" -When call validate_input_python "php-laravel-phpunit" "extensions" "pdo_sqlite, pdo_mysql" -The status should be success -End - -It "accepts extensions with numbers" -When call validate_input_python "php-laravel-phpunit" "extensions" "sqlite3, gd2" -The status should be success -End - -It "rejects extensions with special characters" -When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring@intl" -The status should be failure -End - -It "rejects extensions with command injection" -When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring; rm -rf /" -The status should be failure -End - -It "accepts empty extensions" -When call validate_input_python "php-laravel-phpunit" "extensions" "" -The status should be success -End -End - -Context "when validating coverage input" -It "accepts none coverage" -When call validate_input_python "php-laravel-phpunit" "coverage" "none" -The status should be success -End - -It "accepts xdebug coverage" -When call validate_input_python "php-laravel-phpunit" "coverage" "xdebug" -The status should be success -End - -It "accepts pcov coverage" -When call validate_input_python "php-laravel-phpunit" "coverage" "pcov" -The status should be success -End - -It "accepts xdebug3 coverage" -When call validate_input_python "php-laravel-phpunit" "coverage" "xdebug3" -The status should be success -End - -It "rejects invalid coverage driver" -When call validate_input_python "php-laravel-phpunit" "coverage" "invalid" -The status should be failure -End - -It "rejects coverage with command injection" -When call validate_input_python "php-laravel-phpunit" "coverage" "none; rm -rf /" -The status should be failure -End - -It "accepts empty coverage" -When call validate_input_python "php-laravel-phpunit" "coverage" "" -The status should be success -End -End - -Context "when validating token input" -It "accepts GitHub token expression" -When call validate_input_python "php-laravel-phpunit" "token" "\${{ github.token }}" -The status should be success -End - -It "accepts GitHub fine-grained token" -When call validate_input_python "php-laravel-phpunit" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890" -The status should be success -End - -It "accepts GitHub app token" -When call validate_input_python "php-laravel-phpunit" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890" -The status should be success -End - -It "rejects invalid token format" -When call validate_input_python "php-laravel-phpunit" "token" "invalid-token" -The status should be failure -End - -It "accepts empty token" -When call validate_input_python "php-laravel-phpunit" "token" "" -The status should be success -End -End - -Context "when checking action.yml structure" -It "has valid YAML syntax" -When call validate_action_yml_quiet "$ACTION_FILE" -The status should be success -End - -It "has correct action name" -name=$(get_action_name "$ACTION_FILE") -When call echo "$name" -The output should equal "Laravel Setup and Composer test" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "php-version" -The output should include "php-version-file" -The output should include "extensions" -The output should include "coverage" -The output should include "token" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "php-version" -The output should include "php-version-file" -The output should include "extensions" -The output should include "coverage" -End -End - -Context "when testing input requirements" -It "has all inputs as optional" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional" -The output should equal "none" -End - -It "has correct default php-version" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php-version" "default" -The output should equal "latest" -End - -It "has correct default php-version-file" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php-version-file" "default" -The output should equal ".php-version" -End -End - -Context "when testing security validations" -It "validates against path traversal in php-version-file" -When call validate_input_python "php-laravel-phpunit" "php-version-file" "../../etc/passwd" -The status should be failure -End - -It "validates against shell metacharacters in extensions" -When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring && rm -rf /" -The status should be failure -End - -It "validates against backtick injection in coverage" -When call validate_input_python "php-laravel-phpunit" "coverage" "none\`whoami\`" -The status should be failure -End - -It "validates against variable expansion in php-version" -When call validate_input_python "php-laravel-phpunit" "php-version" "8.4\${HOME}" -The status should be failure -End -End - -Context "when testing Laravel-specific validations" -It "validates coverage driver enum values" -When call validate_input_python "php-laravel-phpunit" "coverage" "invalid-driver" -The status should be failure -End - -It "validates php-version-file path safety" -When call validate_input_python "php-laravel-phpunit" "php-version-file" "/etc/shadow" -The status should be failure -End - -It "validates extensions format for Laravel requirements" -When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring, intl, json, pdo_sqlite, sqlite3" -The status should be success -End -End -End diff --git a/_tests/unit/php-tests/validation.spec.sh b/_tests/unit/php-tests/validation.spec.sh index 0903536..e5f112a 100755 --- a/_tests/unit/php-tests/validation.spec.sh +++ b/_tests/unit/php-tests/validation.spec.sh @@ -174,10 +174,10 @@ End It "defines expected outputs" When call get_action_outputs "$ACTION_FILE" -The output should include "test_status" -The output should include "tests_run" -The output should include "tests_passed" -The output should include "coverage_path" +The output should include "test-status" +The output should include "tests-run" +The output should include "tests-passed" +The output should include "framework" End End @@ -245,5 +245,214 @@ It "validates default email is secure" When call validate_input_python "php-tests" "email" "github-actions@github.com" The status should be success End + +# Helper function that replicates the PHPUnit output parsing logic from action.yml +parse_phpunit_output() { + local phpunit_output="$1" + local phpunit_exit_code="$2" + + local tests_run="0" + local tests_passed="0" + + # Pattern 1: "OK (N test(s), M assertions)" - success case (handles both singular and plural) + if echo "$phpunit_output" | grep -qE 'OK \([0-9]+ tests?,'; then + tests_run=$(echo "$phpunit_output" | grep -oE 'OK \([0-9]+ tests?,' | grep -oE '[0-9]+' | head -1) + tests_passed="$tests_run" + # Pattern 2: "Tests: N" line - failure/error/skipped case + elif echo "$phpunit_output" | grep -qE '^Tests:'; then + tests_run=$(echo "$phpunit_output" | grep -E '^Tests:' | grep -oE '[0-9]+' | head -1) + + # Calculate passed from failures and errors + failures=$(echo "$phpunit_output" | grep -oE 'Failures: [0-9]+' | grep -oE '[0-9]+' | head -1 || echo "0") + errors=$(echo "$phpunit_output" | grep -oE 'Errors: [0-9]+' | grep -oE '[0-9]+' | head -1 || echo "0") + tests_passed=$((tests_run - failures - errors)) + + # Ensure non-negative + if [ "$tests_passed" -lt 0 ]; then + tests_passed="0" + fi + fi + + # Determine status + local status + if [ "$phpunit_exit_code" -eq 0 ]; then + status="success" + else + status="failure" + fi + + # Output as KEY=VALUE format + echo "tests_run=$tests_run" + echo "tests_passed=$tests_passed" + echo "status=$status" +} + +Context "when parsing PHPUnit output" + # Success cases + It "parses single successful test" + output="OK (1 test, 2 assertions)" + When call parse_phpunit_output "$output" 0 + The line 1 of output should equal "tests_run=1" + The line 2 of output should equal "tests_passed=1" + The line 3 of output should equal "status=success" + End + + It "parses multiple successful tests" + output="OK (5 tests, 10 assertions)" + When call parse_phpunit_output "$output" 0 + The line 1 of output should equal "tests_run=5" + The line 2 of output should equal "tests_passed=5" + The line 3 of output should equal "status=success" + End + + It "parses successful tests with plural form" + output="OK (25 tests, 50 assertions)" + When call parse_phpunit_output "$output" 0 + The line 1 of output should equal "tests_run=25" + The line 2 of output should equal "tests_passed=25" + The line 3 of output should equal "status=success" + End + + # Failure cases + It "parses test failures" + output="FAILURES! +Tests: 5, Assertions: 10, Failures: 2." + When call parse_phpunit_output "$output" 1 + The line 1 of output should equal "tests_run=5" + The line 2 of output should equal "tests_passed=3" + The line 3 of output should equal "status=failure" + End + + It "parses test errors" + output="ERRORS! +Tests: 5, Assertions: 10, Errors: 1." + When call parse_phpunit_output "$output" 2 + The line 1 of output should equal "tests_run=5" + The line 2 of output should equal "tests_passed=4" + The line 3 of output should equal "status=failure" + End + + It "parses mixed failures and errors" + output="FAILURES! +Tests: 10, Assertions: 20, Failures: 2, Errors: 1." + When call parse_phpunit_output "$output" 1 + The line 1 of output should equal "tests_run=10" + The line 2 of output should equal "tests_passed=7" + The line 3 of output should equal "status=failure" + End + + It "handles all tests failing" + output="FAILURES! +Tests: 5, Assertions: 10, Failures: 5." + When call parse_phpunit_output "$output" 1 + The line 1 of output should equal "tests_run=5" + The line 2 of output should equal "tests_passed=0" + The line 3 of output should equal "status=failure" + End + + It "prevents negative passed count" + output="ERRORS! +Tests: 2, Assertions: 4, Failures: 1, Errors: 2." + When call parse_phpunit_output "$output" 2 + The line 1 of output should equal "tests_run=2" + The line 2 of output should equal "tests_passed=0" + The line 3 of output should equal "status=failure" + End + + # Skipped tests + It "parses skipped tests with success" + output="OK, but some tests were skipped! +Tests: 5, Assertions: 8, Skipped: 2." + When call parse_phpunit_output "$output" 0 + The line 1 of output should equal "tests_run=5" + The line 2 of output should equal "tests_passed=5" + The line 3 of output should equal "status=success" + End + + # Edge cases + It "handles no parseable output (fallback)" + output="Some random output without test info" + When call parse_phpunit_output "$output" 1 + The line 1 of output should equal "tests_run=0" + The line 2 of output should equal "tests_passed=0" + The line 3 of output should equal "status=failure" + End + + It "handles empty output" + output="" + When call parse_phpunit_output "$output" 0 + The line 1 of output should equal "tests_run=0" + The line 2 of output should equal "tests_passed=0" + The line 3 of output should equal "status=success" + End + + It "handles PHPUnit 10+ format with singular test" + output="OK (1 test, 3 assertions)" + When call parse_phpunit_output "$output" 0 + The line 1 of output should equal "tests_run=1" + The line 2 of output should equal "tests_passed=1" + The line 3 of output should equal "status=success" + End + + It "handles verbose output with noise" + output="PHPUnit 10.5.0 by Sebastian Bergmann and contributors. +Runtime: PHP 8.3.0 + +..... 5 / 5 (100%) + +Time: 00:00.123, Memory: 10.00 MB + +OK (5 tests, 10 assertions)" + When call parse_phpunit_output "$output" 0 + The line 1 of output should equal "tests_run=5" + The line 2 of output should equal "tests_passed=5" + The line 3 of output should equal "status=success" + End + + It "handles failure output with full details" + output="PHPUnit 10.5.0 by Sebastian Bergmann and contributors. + +..F.. 5 / 5 (100%) + +Time: 00:00.234, Memory: 12.00 MB + +FAILURES! +Tests: 5, Assertions: 10, Failures: 1." + When call parse_phpunit_output "$output" 1 + The line 1 of output should equal "tests_run=5" + The line 2 of output should equal "tests_passed=4" + The line 3 of output should equal "status=failure" + End + + # Status determination tests + It "marks as success when exit code is 0" + output="OK (3 tests, 6 assertions)" + When call parse_phpunit_output "$output" 0 + The line 3 of output should equal "status=success" + End + + It "marks as failure when exit code is non-zero" + output="OK (3 tests, 6 assertions)" + When call parse_phpunit_output "$output" 1 + The line 3 of output should equal "status=failure" + End + + It "handles skipped tests without OK prefix" + output="Tests: 5, Assertions: 8, Skipped: 2." + When call parse_phpunit_output "$output" 0 + The line 1 of output should equal "tests_run=5" + The line 2 of output should equal "tests_passed=5" + The line 3 of output should equal "status=success" + End + + It "handles risky tests output" + output="FAILURES! +Tests: 8, Assertions: 15, Failures: 1, Risky: 2." + When call parse_phpunit_output "$output" 1 + The line 1 of output should equal "tests_run=8" + The line 2 of output should equal "tests_passed=7" + The line 3 of output should equal "status=failure" + End +End End End diff --git a/_tests/unit/spec_helper.sh b/_tests/unit/spec_helper.sh index 9c4c806..22bd37f 100755 --- a/_tests/unit/spec_helper.sh +++ b/_tests/unit/spec_helper.sh @@ -92,10 +92,6 @@ setup_default_inputs() { "go-build" | "go-lint") [[ "$input_name" != "go-version" ]] && export INPUT_GO_VERSION="1.21" ;; - "common-cache") - [[ "$input_name" != "type" ]] && export INPUT_TYPE="npm" - [[ "$input_name" != "paths" ]] && export INPUT_PATHS="node_modules" - ;; "common-retry") [[ "$input_name" != "command" ]] && export INPUT_COMMAND="echo test" ;; @@ -114,11 +110,6 @@ setup_default_inputs() { "validate-inputs") [[ "$input_name" != "action-type" && "$input_name" != "action" && "$input_name" != "rules-file" && "$input_name" != "fail-on-error" ]] && export INPUT_ACTION_TYPE="test-action" ;; - "version-file-parser") - [[ "$input_name" != "language" ]] && export INPUT_LANGUAGE="node" - [[ "$input_name" != "tool-versions-key" ]] && export INPUT_TOOL_VERSIONS_KEY="nodejs" - [[ "$input_name" != "dockerfile-image" ]] && export INPUT_DOCKERFILE_IMAGE="node" - ;; "codeql-analysis") [[ "$input_name" != "language" ]] && export INPUT_LANGUAGE="javascript" [[ "$input_name" != "token" ]] && export INPUT_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" @@ -163,10 +154,6 @@ cleanup_default_inputs() { "go-build" | "go-lint") [[ "$input_name" != "go-version" ]] && unset INPUT_GO_VERSION ;; - "common-cache") - [[ "$input_name" != "type" ]] && unset INPUT_TYPE - [[ "$input_name" != "paths" ]] && unset INPUT_PATHS - ;; "common-retry") [[ "$input_name" != "command" ]] && unset INPUT_COMMAND ;; @@ -185,11 +172,6 @@ cleanup_default_inputs() { "validate-inputs") [[ "$input_name" != "action-type" && "$input_name" != "action" && "$input_name" != "rules-file" && "$input_name" != "fail-on-error" ]] && unset INPUT_ACTION_TYPE ;; - "version-file-parser") - [[ "$input_name" != "language" ]] && unset INPUT_LANGUAGE - [[ "$input_name" != "tool-versions-key" ]] && unset INPUT_TOOL_VERSIONS_KEY - [[ "$input_name" != "dockerfile-image" ]] && unset INPUT_DOCKERFILE_IMAGE - ;; "codeql-analysis") [[ "$input_name" != "language" ]] && unset INPUT_LANGUAGE [[ "$input_name" != "token" ]] && unset INPUT_TOKEN @@ -244,10 +226,6 @@ shellspec_mock_action_run() { action_name=$(basename "$action_dir") case "$action_name" in - "version-file-parser") - echo "detected-version=1.0.0" >>"$GITHUB_OUTPUT" - echo "package-manager=npm" >>"$GITHUB_OUTPUT" - ;; "node-setup") echo "node-version=18.0.0" >>"$GITHUB_OUTPUT" echo "package-manager=npm" >>"$GITHUB_OUTPUT" @@ -258,11 +236,6 @@ shellspec_mock_action_run() { echo "build-time=45" >>"$GITHUB_OUTPUT" echo "platforms=linux/amd64" >>"$GITHUB_OUTPUT" ;; - "common-cache") - echo "cache-hit=true" >>"$GITHUB_OUTPUT" - echo "cache-key=Linux-npm-abc123" >>"$GITHUB_OUTPUT" - echo "cache-paths=node_modules" >>"$GITHUB_OUTPUT" - ;; "common-file-check") echo "found=true" >>"$GITHUB_OUTPUT" ;; diff --git a/_tests/unit/version-file-parser/validation.spec.sh b/_tests/unit/version-file-parser/validation.spec.sh deleted file mode 100755 index f94fe6d..0000000 --- a/_tests/unit/version-file-parser/validation.spec.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for version-file-parser action validation and logic -# Framework is automatically loaded via spec_helper.sh - -Describe "version-file-parser action" - ACTION_DIR="version-file-parser" - ACTION_FILE="$ACTION_DIR/action.yml" - - Context "when validating language input" - It "accepts valid language input" - When call validate_input_python "version-file-parser" "language" "node" - The status should be success - End - It "accepts php language" - When call validate_input_python "version-file-parser" "language" "php" - The status should be success - End - It "accepts python language" - When call validate_input_python "version-file-parser" "language" "python" - The status should be success - End - It "accepts go language" - When call validate_input_python "version-file-parser" "language" "go" - The status should be success - End - It "rejects invalid language with special characters" - When call validate_input_python "version-file-parser" "language" "node; rm -rf /" - The status should be failure - End - It "rejects empty required inputs" - When call validate_input_python "version-file-parser" "language" "" - The status should be failure - End - End - - Context "when validating dockerfile-image input" - It "accepts valid dockerfile image" - When call validate_input_python "version-file-parser" "dockerfile-image" "node" - The status should be success - End - It "accepts php dockerfile image" - When call validate_input_python "version-file-parser" "dockerfile-image" "php" - The status should be success - End - It "accepts python dockerfile image" - When call validate_input_python "version-file-parser" "dockerfile-image" "python" - The status should be success - End - It "rejects injection in dockerfile image" - When call validate_input_python "version-file-parser" "dockerfile-image" "node;malicious" - The status should be failure - End - End - - Context "when validating optional inputs" - It "accepts valid validation regex" - When call validate_input_python "version-file-parser" "validation-regex" "^[0-9]+\.[0-9]+(\.[0-9]+)?$" - The status should be success - End - It "accepts valid default version" - When call validate_input_python "version-file-parser" "default-version" "18.0.0" - The status should be success - End - It "accepts tool versions key" - When call validate_input_python "version-file-parser" "tool-versions-key" "nodejs" - The status should be success - End - End - - Context "when checking action.yml structure" - It "has valid YAML syntax" - When call validate_action_yml_quiet "$ACTION_FILE" - The status should be success - End - - It "contains required metadata" - When call get_action_name "$ACTION_FILE" - The output should equal "Version File Parser" - End - - It "defines expected inputs" - When call get_action_inputs "$ACTION_FILE" - The output should include "language" - The output should include "tool-versions-key" - The output should include "dockerfile-image" - End - - It "defines expected outputs" - When call get_action_outputs "$ACTION_FILE" - The output should include "detected-version" - The output should include "package-manager" - End - End - - Context "when validating security" - It "rejects injection in language parameter" - When call validate_input_python "version-file-parser" "language" "node&&malicious" - The status should be failure - End - - It "rejects pipe injection in tool versions key" - When call validate_input_python "version-file-parser" "tool-versions-key" "nodejs|dangerous" - The status should be failure - End - - It "validates regex patterns safely" - When call validate_input_python "version-file-parser" "validation-regex" "^[0-9]+\.[0-9]+$" - The status should be success - End - - It "rejects malicious regex patterns" - When call validate_input_python "version-file-parser" "validation-regex" ".*; rm -rf /" - The status should be failure - End - End - - Context "when testing outputs" - It "produces all expected outputs consistently" - When call test_action_outputs "$ACTION_DIR" "language" "node" "dockerfile-image" "node" - The status should be success - The stderr should include "Testing action outputs for: version-file-parser" - The stderr should include "Output test passed for: version-file-parser" - End - End -End diff --git a/ansible-lint-fix/action.yml b/ansible-lint-fix/action.yml index b281bcb..4b9e871 100644 --- a/ansible-lint-fix/action.yml +++ b/ansible-lint-fix/action.yml @@ -73,15 +73,12 @@ runs: with: token: ${{ inputs.token || github.token }} - - name: Cache Python Dependencies + - name: Setup Python if: steps.check-files.outputs.files_found == 'true' - id: cache-pip - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: - type: 'pip' - paths: '~/.cache/pip' - key-files: 'requirements*.txt,pyproject.toml,setup.py,setup.cfg' - key-prefix: 'ansible-lint-fix' + python-version: '3.11' + cache: 'pip' - name: Install ansible-lint id: install-ansible-lint diff --git a/biome-lint/action.yml b/biome-lint/action.yml index 7e88fca..388b187 100644 --- a/biome-lint/action.yml +++ b/biome-lint/action.yml @@ -56,7 +56,7 @@ runs: steps: - name: Validate Inputs id: validate - shell: bash + shell: sh env: MODE: ${{ inputs.mode }} GITHUB_TOKEN: ${{ inputs.token }} @@ -65,7 +65,7 @@ runs: MAX_RETRIES: ${{ inputs.max-retries }} FAIL_ON_ERROR: ${{ inputs.fail-on-error }} run: | - set -euo pipefail + set -eu # Validate mode case "$MODE" in @@ -79,16 +79,26 @@ runs: esac # Validate GitHub token presence if provided - if [[ -n "$GITHUB_TOKEN" ]] && ! [[ "$GITHUB_TOKEN" =~ ^\$\{\{ ]]; then - echo "Using provided GitHub token" + if [ -n "$GITHUB_TOKEN" ]; then + case "$GITHUB_TOKEN" in + \$\{\{*) + # Token is a GitHub Actions expression, skip validation + ;; + *) + echo "Using provided GitHub token" + ;; + esac fi # Validate email format (basic check) - required for fix mode if [ "$MODE" = "fix" ]; then - if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" - exit 1 - fi + case "$EMAIL" in + *@*.*) ;; + *) + echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" + exit 1 + ;; + esac # Validate username format (GitHub canonical rules) username="$USERNAME" @@ -100,32 +110,45 @@ runs: fi # Check allowed characters (letters, digits, hyphens only) - if ! [[ "$username" =~ ^[a-zA-Z0-9-]+$ ]]; then - echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed" - exit 1 - fi + case "$username" in + *[!a-zA-Z0-9-]*) + echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed" + exit 1 + ;; + esac # Check doesn't start or end with hyphen - if [[ "$username" == -* ]] || [[ "$username" == *- ]]; then - echo "::error::Invalid username '$username'. Cannot start or end with hyphen" - exit 1 - fi + case "$username" in + -*|*-) + echo "::error::Invalid username '$username'. Cannot start or end with hyphen" + exit 1 + ;; + esac # Check no consecutive hyphens - if [[ "$username" == *--* ]]; then - echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" - exit 1 - fi + case "$username" in + *--*) + echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" + exit 1 + ;; + esac fi # Validate max retries (positive integer with reasonable upper limit) - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then + case "$MAX_RETRIES" in + ''|*[!0-9]*) + echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10" + exit 1 + ;; + esac + + if [ "$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 fail-on-error (boolean) - if [[ "$FAIL_ON_ERROR" != "true" ]] && [[ "$FAIL_ON_ERROR" != "false" ]]; then + if [ "$FAIL_ON_ERROR" != "true" ] && [ "$FAIL_ON_ERROR" != "false" ]; then echo "::error::Invalid fail-on-error value: '$FAIL_ON_ERROR'. Must be 'true' or 'false'" exit 1 fi @@ -137,26 +160,79 @@ runs: with: token: ${{ inputs.token || github.token }} - - name: Node Setup - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + - name: Detect Package Manager + id: detect-pm + shell: sh + run: | + set -eu + + # Detect package manager from lockfiles + if [ -f bun.lockb ]; then + package_manager="bun" + elif [ -f pnpm-lock.yaml ]; then + package_manager="pnpm" + elif [ -f yarn.lock ]; then + package_manager="yarn" + else + package_manager="npm" + fi + + printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT" + echo "Detected package manager: $package_manager" + + - name: Setup Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: '22' + + - name: Enable Corepack + shell: sh + run: | + set -eu + corepack enable + + - name: Install Package Manager + shell: sh + env: + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} + run: | + set -eu + + case "$PACKAGE_MANAGER" in + pnpm) + corepack prepare pnpm@latest --activate + ;; + yarn) + corepack prepare yarn@stable --activate + ;; + bun|npm) + # Bun installed separately, npm built-in + ;; + esac + + - name: Setup Bun + if: steps.detect-pm.outputs.package-manager == 'bun' + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: latest - name: Cache Node Dependencies id: cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'biome-lint-${{ inputs.mode }}-${{ steps.node-setup.outputs.package-manager }}' + path: node_modules + key: ${{ runner.os }}-biome-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }} + restore-keys: | + ${{ runner.os }}-biome-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}- + ${{ runner.os }}-biome-lint-${{ inputs.mode }}- - name: Install Biome - shell: bash + shell: sh env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} MAX_RETRIES: ${{ inputs.max-retries }} run: | - set -euo pipefail + set -eu # Check if biome is already installed if command -v biome >/dev/null 2>&1; then @@ -208,11 +284,11 @@ runs: - name: Run Biome Check if: inputs.mode == 'check' id: check - shell: bash + shell: sh env: FAIL_ON_ERROR: ${{ inputs.fail-on-error }} run: | - set -euo pipefail + set -eu echo "Running Biome check mode..." @@ -262,9 +338,9 @@ runs: - name: Run Biome Fix if: inputs.mode == 'fix' id: fix - shell: bash + shell: sh run: | - set -euo pipefail + set -eu echo "Running Biome fix mode..." diff --git a/codeql-analysis/action.yml b/codeql-analysis/action.yml index fb99da6..a45a14e 100644 --- a/codeql-analysis/action.yml +++ b/codeql-analysis/action.yml @@ -128,13 +128,14 @@ runs: skip-queries: ${{ inputs.skip-queries }} - name: Validate checkout safety - shell: bash + shell: sh env: CHECKOUT_REF: ${{ inputs.checkout-ref }} EVENT_NAME: ${{ github.event_name }} run: | + set -eu # Security check: Warn if checking out custom ref on pull_request_target - if [[ "$EVENT_NAME" == "pull_request_target" ]] && [[ -n "$CHECKOUT_REF" ]]; then + if [ "$EVENT_NAME" = "pull_request_target" ] && [ -n "$CHECKOUT_REF" ]; then echo "::warning::Using custom checkout-ref on pull_request_target is potentially unsafe" echo "::warning::Ensure the ref is validated before running untrusted code" fi @@ -147,28 +148,30 @@ runs: - name: Set analysis category id: set-category - shell: bash + shell: sh env: CATEGORY: ${{ inputs.category }} LANGUAGE: ${{ inputs.language }} run: | - if [[ -n "$CATEGORY" ]]; then + set -eu + if [ -n "$CATEGORY" ]; then category="$CATEGORY" else category="/language:$LANGUAGE" fi - echo "category=$category" >> $GITHUB_OUTPUT + echo "category=$category" >> "$GITHUB_OUTPUT" echo "Using analysis category: $category" - name: Set build mode id: set-build-mode - shell: bash + shell: sh env: BUILD_MODE: ${{ inputs.build-mode }} LANGUAGE: ${{ inputs.language }} run: | + set -eu build_mode="$BUILD_MODE" - if [[ -z "$build_mode" ]]; then + if [ -z "$build_mode" ]; then # Auto-detect build mode based on language case "$LANGUAGE" in javascript|python|ruby|actions) @@ -179,7 +182,7 @@ runs: ;; esac fi - echo "build-mode=$build_mode" >> $GITHUB_OUTPUT + echo "build-mode=$build_mode" >> "$GITHUB_OUTPUT" echo "Using build mode: $build_mode" - name: Initialize CodeQL @@ -211,7 +214,7 @@ runs: skip-queries: ${{ inputs.skip-queries }} - name: Summary - shell: bash + shell: sh env: LANGUAGE: ${{ inputs.language }} CATEGORY: ${{ steps.set-category.outputs.category }} @@ -221,14 +224,15 @@ runs: UPLOAD_RESULTS: ${{ inputs.upload-results }} OUTPUT: ${{ inputs.output }} run: | + set -eu echo "✅ CodeQL analysis completed for language: $LANGUAGE" echo "📊 Category: $CATEGORY" echo "🏗️ Build mode: $BUILD_MODE" echo "🔍 Queries: ${QUERIES:-default}" echo "📦 Packs: ${PACKS:-none}" - if [[ "$UPLOAD_RESULTS" == "true" ]]; then + if [ "$UPLOAD_RESULTS" = "true" ]; then echo "📤 Results uploaded to GitHub Security tab" fi - if [[ -n "$OUTPUT" ]]; then + if [ -n "$OUTPUT" ]; then echo "💾 SARIF saved to: $OUTPUT" fi diff --git a/common-cache/CustomValidator.py b/common-cache/CustomValidator.py deleted file mode 100755 index ebbddf7..0000000 --- a/common-cache/CustomValidator.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for common-cache action. - -This validator handles caching-specific validation including: -- Cache types (npm, composer, go, pip, maven, gradle) -- Cache paths (comma-separated list) -- Cache keys and restore keys -- Path validation with special handling for multiple paths -""" - -from __future__ import annotations - -from pathlib import Path -import sys - -# Add validate-inputs directory to path to import validators -validate_inputs_path = Path(__file__).parent.parent / "validate-inputs" -sys.path.insert(0, str(validate_inputs_path)) - -from validators.base import BaseValidator -from validators.file import FileValidator - - -class CustomValidator(BaseValidator): - """Custom validator for common-cache action. - - Provides validation for cache configuration. - """ - - def __init__(self, action_type: str = "common-cache") -> None: - """Initialize the common-cache validator.""" - super().__init__(action_type) - self.file_validator = FileValidator(action_type) - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate common-cache specific inputs. - - Args: - inputs: Dictionary of input names to values - - Returns: - True if all validations pass, False otherwise - """ - valid = True - - # Validate type (required) - if "type" in inputs: - valid &= self.validate_cache_type(inputs["type"]) - else: - # Type is required - self.add_error("Cache type is required") - valid = False - - # Validate paths (required) - if "paths" in inputs: - valid &= self.validate_cache_paths(inputs["paths"]) - else: - # Paths is required - self.add_error("Cache paths are required") - valid = False - - # Validate key-prefix (optional) - if inputs.get("key-prefix"): - valid &= self.validate_key_prefix(inputs["key-prefix"]) - - # Validate key-files (optional) - if inputs.get("key-files"): - valid &= self.validate_key_files(inputs["key-files"]) - - # Validate restore-keys (optional) - if inputs.get("restore-keys"): - valid &= self.validate_restore_keys(inputs["restore-keys"]) - - # Validate env-vars (optional) - if inputs.get("env-vars"): - valid &= self.validate_env_vars(inputs["env-vars"]) - - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs for common-cache. - - Returns: - List of required input names - """ - return ["type", "paths"] - - def get_validation_rules(self) -> dict: - """Get validation rules for common-cache. - - Returns: - Dictionary of validation rules - """ - return { - "type": "Cache type (npm, composer, go, pip, maven, gradle)", - "paths": "Comma-separated list of paths to cache", - "key-prefix": "Optional prefix for cache key", - "key-files": "Files to include in cache key hash", - "restore-keys": "Fallback cache keys to try", - } - - def validate_cache_type(self, cache_type: str) -> bool: - """Validate cache type. - - Args: - cache_type: Type of cache - - Returns: - True if valid, False otherwise - """ - # Check for empty - if not cache_type or not cache_type.strip(): - self.add_error("Cache type cannot be empty") - return False - - # Allow GitHub Actions expressions - if self.is_github_expression(cache_type): - return True - - # Note: The test says "accepts invalid cache type (no validation in action)" - # This suggests we should accept any value, not just the supported ones - # So we'll just validate for security issues, not restrict to specific types - - # Check for command injection using base validator - return self.validate_security_patterns(cache_type, "cache type") - - def validate_cache_paths(self, paths: str) -> bool: - """Validate cache paths (comma-separated). - - Args: - paths: Comma-separated paths - - Returns: - True if valid, False otherwise - """ - # Check for empty - if not paths or not paths.strip(): - self.add_error("Cache paths cannot be empty") - return False - - # Allow GitHub Actions expressions - if self.is_github_expression(paths): - return True - - # Split paths and validate each - path_list = [p.strip() for p in paths.split(",")] - - for path in path_list: - if not path: - continue - - # Use FileValidator for path validation - result = self.file_validator.validate_file_path(path, "paths") - # Propagate errors from file validator - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - - if not result: - return False - - return True - - def validate_key_prefix(self, key_prefix: str) -> bool: - """Validate cache key prefix. - - Args: - key_prefix: Key prefix - - Returns: - True if valid, False otherwise - """ - # Allow GitHub Actions expressions - if self.is_github_expression(key_prefix): - return True - - # Check for command injection using base validator - return self.validate_security_patterns(key_prefix, "key-prefix") - - def validate_key_files(self, key_files: str) -> bool: - """Validate key files (comma-separated). - - Args: - key_files: Comma-separated file paths - - Returns: - True if valid, False otherwise - """ - # Allow GitHub Actions expressions - if self.is_github_expression(key_files): - return True - - # Split files and validate each - file_list = [f.strip() for f in key_files.split(",")] - - for file_path in file_list: - if not file_path: - continue - - # Use FileValidator for path validation - result = self.file_validator.validate_file_path(file_path, "key-files") - # Propagate errors from file validator - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - - if not result: - return False - - return True - - def validate_restore_keys(self, restore_keys: str) -> bool: - """Validate restore keys. - - Args: - restore_keys: Restore keys specification - - Returns: - True if valid, False otherwise - """ - # Allow GitHub Actions expressions - if self.is_github_expression(restore_keys): - return True - - # Check for command injection using base validator - return self.validate_security_patterns(restore_keys, "restore-keys") - - def validate_env_vars(self, env_vars: str) -> bool: - """Validate environment variables. - - Args: - env_vars: Environment variables specification - - Returns: - True if valid, False otherwise - """ - # Allow GitHub Actions expressions - if self.is_github_expression(env_vars): - return True - - # Check for command injection using base validator - return self.validate_security_patterns(env_vars, "env-vars") diff --git a/common-cache/README.md b/common-cache/README.md deleted file mode 100644 index bfae5c9..0000000 --- a/common-cache/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# ivuorinen/actions/common-cache - -## Common Cache - -### Description - -Standardized caching strategy for all actions - -### Inputs - -| name | description | required | default | -|----------------|------------------------------------------------------|----------|---------| -| `type` |

Type of cache (npm, composer, go, pip, etc.)

| `true` | `""` | -| `paths` |

Paths to cache (comma-separated)

| `true` | `""` | -| `key-prefix` |

Custom prefix for cache key

| `false` | `""` | -| `key-files` |

Files to hash for cache key (comma-separated)

| `false` | `""` | -| `restore-keys` |

Fallback keys for cache restoration

| `false` | `""` | -| `env-vars` |

Environment variables to include in cache key

| `false` | `""` | - -### Outputs - -| name | description | -|---------------|-----------------------------| -| `cache-hit` |

Cache hit indicator

| -| `cache-key` |

Generated cache key

| -| `cache-paths` |

Resolved cache paths

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/common-cache@main - with: - type: - # Type of cache (npm, composer, go, pip, etc.) - # - # Required: true - # Default: "" - - paths: - # Paths to cache (comma-separated) - # - # Required: true - # Default: "" - - key-prefix: - # Custom prefix for cache key - # - # Required: false - # Default: "" - - key-files: - # Files to hash for cache key (comma-separated) - # - # Required: false - # Default: "" - - restore-keys: - # Fallback keys for cache restoration - # - # Required: false - # Default: "" - - env-vars: - # Environment variables to include in cache key - # - # Required: false - # Default: "" -``` diff --git a/common-cache/action.yml b/common-cache/action.yml deleted file mode 100644 index 23d6306..0000000 --- a/common-cache/action.yml +++ /dev/null @@ -1,122 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for reading cache contents ---- -name: Common Cache -description: 'Standardized caching strategy for all actions' -author: 'Ismo Vuorinen' - -branding: - icon: database - color: gray-dark - -inputs: - type: - description: 'Type of cache (npm, composer, go, pip, etc.)' - required: true - paths: - description: 'Paths to cache (comma-separated)' - required: true - key-prefix: - description: 'Custom prefix for cache key' - required: false - default: '' - key-files: - description: 'Files to hash for cache key (comma-separated)' - required: false - default: '' - restore-keys: - description: 'Fallback keys for cache restoration' - required: false - default: '' - env-vars: - description: 'Environment variables to include in cache key' - required: false - default: '' - -outputs: - cache-hit: - description: 'Cache hit indicator' - value: ${{ steps.cache.outputs.cache-hit }} - cache-key: - description: 'Generated cache key' - value: ${{ steps.prepare.outputs.cache-key }} - cache-paths: - description: 'Resolved cache paths' - value: ${{ steps.prepare.outputs.cache-paths }} - -runs: - using: composite - steps: - - id: prepare - shell: bash - env: - RUNNER_OS: ${{ runner.os }} - CACHE_TYPE: ${{ inputs.type }} - KEY_PREFIX: ${{ inputs.key-prefix }} - KEY_FILES: ${{ inputs.key-files }} - ENV_VARS: ${{ inputs.env-vars }} - CACHE_PATHS: ${{ inputs.paths }} - run: | - set -euo pipefail - - # Generate standardized cache key components - os_key="$RUNNER_OS" - type_key="$CACHE_TYPE" - prefix_key="$KEY_PREFIX" - - # Process file hashes - # Note: For simple glob patterns, hashFiles() function could be used directly - # in the cache key. This manual approach is used to support comma-separated - # file lists with complex cache key construction. - files_hash="" - if [ -n "$KEY_FILES" ]; then - IFS=',' read -ra FILES <<< "$KEY_FILES" - existing_files=() - for file in "${FILES[@]}"; do - # Trim whitespace - file=$(echo "$file" | xargs) - if [ -f "$file" ]; then - existing_files+=("$file") - fi - done - # Hash all files together for better performance - if [ ${#existing_files[@]} -gt 0 ]; then - files_hash=$(cat "${existing_files[@]}" | sha256sum | cut -d' ' -f1) - fi - fi - - # Process environment variables - env_hash="" - if [ -n "$ENV_VARS" ]; then - IFS=',' read -ra VARS <<< "$ENV_VARS" - for var in "${VARS[@]}"; do - if [ -n "${!var}" ]; then - env_hash="${env_hash}-${var}-${!var}" - fi - done - fi - - # Generate final cache key - cache_key="${os_key}" - [ -n "$prefix_key" ] && cache_key="${cache_key}-${prefix_key}" - [ -n "$type_key" ] && cache_key="${cache_key}-${type_key}" - [ -n "$files_hash" ] && cache_key="${cache_key}-${files_hash}" - [ -n "$env_hash" ] && cache_key="${cache_key}-${env_hash}" - - echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT - - # Process cache paths - IFS=',' read -ra PATHS <<< "$CACHE_PATHS" - cache_paths="" - for path in "${PATHS[@]}"; do - cache_paths="${cache_paths}${path}\n" - done - echo "cache-paths=${cache_paths}" >> $GITHUB_OUTPUT - - - id: cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ${{ steps.prepare.outputs.cache-paths }} - key: ${{ steps.prepare.outputs.cache-key }} - restore-keys: ${{ inputs.restore-keys }} diff --git a/common-cache/rules.yml b/common-cache/rules.yml deleted file mode 100644 index 5f5df4c..0000000 --- a/common-cache/rules.yml +++ /dev/null @@ -1,42 +0,0 @@ ---- -# Validation rules for common-cache action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 50% (3/6 inputs) -# -# This file defines validation rules for the common-cache GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: common-cache -description: Standardized caching strategy for all actions -generator_version: 1.0.0 -required_inputs: - - paths - - type -optional_inputs: - - env-vars - - key-files - - key-prefix - - restore-keys -conventions: - key-files: file_path - key-prefix: prefix - paths: file_path -overrides: {} -statistics: - total_inputs: 6 - validated_inputs: 3 - skipped_inputs: 0 - coverage_percentage: 50 -validation_coverage: 50 -auto_detected: true -manual_review_required: true -quality_indicators: - has_required_inputs: true - has_token_validation: false - has_version_validation: false - has_file_validation: true - has_security_validation: false diff --git a/compress-images/action.yml b/compress-images/action.yml index 7bb6d15..8d5178d 100644 --- a/compress-images/action.yml +++ b/compress-images/action.yml @@ -57,7 +57,7 @@ runs: steps: - name: Validate Inputs id: validate - shell: bash + shell: sh env: WORKING_DIRECTORY: ${{ inputs.working-directory }} IMAGE_QUALITY: ${{ inputs.image-quality }} @@ -67,7 +67,7 @@ runs: USERNAME: ${{ inputs.username }} GITHUB_TOKEN: ${{ inputs.token }} run: | - set -euo pipefail + set -eu # Validate working directory if [ ! -d "$WORKING_DIRECTORY" ]; then @@ -76,70 +76,73 @@ runs: fi # Validate path security (prevent absolute paths and path traversal) - if [[ "$WORKING_DIRECTORY" == "/"* ]] || [[ "$WORKING_DIRECTORY" == "~"* ]] || [[ "$WORKING_DIRECTORY" =~ ^[A-Za-z]:[/\\] ]]; then - echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Absolute paths not allowed" - exit 1 - fi + case "$WORKING_DIRECTORY" in + /*|~*|[A-Za-z]:*|*..*) + echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Absolute paths and path traversal not allowed" + exit 1 + ;; + esac - if [[ "$WORKING_DIRECTORY" == *".."* ]]; then - echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Path traversal not allowed" - exit 1 - fi - - # Validate image quality (0-100) - if ! [[ "$IMAGE_QUALITY" =~ ^[0-9]+$ ]]; then - echo "::error::Invalid image-quality: '$IMAGE_QUALITY'. Must be a number between 0 and 100" - exit 1 - fi + # Validate image quality (0-100) - must be numeric + case "$IMAGE_QUALITY" in + ''|*[!0-9]*) + echo "::error::Invalid image-quality: '$IMAGE_QUALITY'. Must be a number between 0 and 100" + exit 1 + ;; + esac if [ "$IMAGE_QUALITY" -lt 0 ] || [ "$IMAGE_QUALITY" -gt 100 ]; then echo "::error::Invalid image-quality: '$IMAGE_QUALITY'. Must be between 0 and 100" exit 1 fi - # Validate PNG quality (0-100) - if ! [[ "$PNG_QUALITY" =~ ^[0-9]+$ ]]; then - echo "::error::Invalid png-quality: '$PNG_QUALITY'. Must be a number between 0 and 100" - exit 1 - fi + # Validate PNG quality (0-100) - must be numeric + case "$PNG_QUALITY" in + ''|*[!0-9]*) + echo "::error::Invalid png-quality: '$PNG_QUALITY'. Must be a number between 0 and 100" + exit 1 + ;; + esac if [ "$PNG_QUALITY" -lt 0 ] || [ "$PNG_QUALITY" -gt 100 ]; then echo "::error::Invalid png-quality: '$PNG_QUALITY'. Must be between 0 and 100" exit 1 fi - # Validate ignore paths format (prevent command injection) - if [[ "$IGNORE_PATHS" == *";"* ]] || [[ "$IGNORE_PATHS" == *"&&"* ]] || \ - [[ "$IGNORE_PATHS" == *"|"* ]] || [[ "$IGNORE_PATHS" == *'`'* ]] || \ - [[ "$IGNORE_PATHS" == *'$('* ]] || [[ "$IGNORE_PATHS" == *'${'* ]] || \ - [[ "$IGNORE_PATHS" == *"<"* ]] || [[ "$IGNORE_PATHS" == *">"* ]]; then - echo "::error::Invalid ignore-paths: '$IGNORE_PATHS'. Command injection patterns not allowed" - exit 1 - fi - - # Validate ignore paths for path traversal - if [[ "$IGNORE_PATHS" == *".."* ]]; then - echo "::error::Invalid ignore-paths: '$IGNORE_PATHS'. Path traversal not allowed" - exit 1 - fi + # Validate ignore paths format (prevent command injection and path traversal) + case "$IGNORE_PATHS" in + *\;*|*\&\&*|*\|*|*\`*|*\$\(*|*\$\{*|*\<*|*\>*|*..\*) + echo "::error::Invalid ignore-paths: '$IGNORE_PATHS'. Command injection patterns and path traversal not allowed" + exit 1 + ;; + esac # Validate email format (basic check) - if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" - exit 1 - fi + case "$EMAIL" in + *@*.*) ;; + *) + echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" + exit 1 + ;; + esac # Validate username format (prevent command injection) - if [[ "$USERNAME" == *";"* ]] || [[ "$USERNAME" == *"&&"* ]] || [[ "$USERNAME" == *"|"* ]]; then - echo "::error::Invalid username: '$USERNAME'. Command injection patterns not allowed" - exit 1 - fi + case "$USERNAME" in + *\;*|*\&\&*|*\|*) + echo "::error::Invalid username: '$USERNAME'. Command injection patterns not allowed" + exit 1 + ;; + esac # Validate token format if provided (basic GitHub token pattern) - if [[ -n "$GITHUB_TOKEN" ]]; then - if ! [[ "$GITHUB_TOKEN" =~ ^gh[efpousr]_[a-zA-Z0-9]{36}$ ]]; then - echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters" - fi + if [ -n "$GITHUB_TOKEN" ]; then + case "$GITHUB_TOKEN" in + gh[efpousr]_?????????????????????????????????????) + ;; + *) + echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters" + ;; + esac fi - name: Checkout Repository diff --git a/csharp-build/action.yml b/csharp-build/action.yml index d435fed..c4213e9 100644 --- a/csharp-build/action.yml +++ b/csharp-build/action.yml @@ -50,27 +50,111 @@ runs: - name: Detect .NET SDK Version id: detect-dotnet-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'dotnet' - default-version: "${{ inputs.dotnet-version || '7.0' }}" + shell: sh + env: + DEFAULT_VERSION: "${{ inputs.dotnet-version || '7.0' }}" + 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]*\.[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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for dotnet..." >&2 + version=$(awk '/^dotnet[[: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 .NET 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 dotnet..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "dotnet:" | head -1 | \ + sed -n -E "s/.*dotnet:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET 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 dotnet..." >&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/.*dotnet:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping devcontainer.json parsing" >&2 + fi + fi + + # Parse global.json + if [ -z "$detected_version" ] && [ -f global.json ]; then + echo "Checking global.json..." >&2 + if command -v jq >/dev/null 2>&1; then + version=$(jq -r '.sdk.version // empty' global.json 2>/dev/null || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET version in global.json: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping global.json parsing" >&2 + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default .NET version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected .NET version: $detected_version" >&2 - name: Setup .NET SDK uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }} - - - name: Cache NuGet packages - id: cache-nuget - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'nuget' - paths: '~/.nuget/packages' - key-files: '**/*.csproj,**/*.props,**/*.targets' - key-prefix: 'csharp-build' + cache: true + cache-dependency-path: '**/packages.lock.json' - name: Restore Dependencies - if: steps.cache-nuget.outputs.cache-hit != 'true' uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 with: timeout_minutes: 10 @@ -79,17 +163,11 @@ runs: echo "Restoring .NET dependencies..." dotnet restore --verbosity normal - - name: Skip Restore (Cache Hit) - if: steps.cache-nuget.outputs.cache-hit == 'true' - shell: bash - run: | - echo "Cache hit - skipping dotnet restore" - - name: Build Solution id: build - shell: bash + shell: sh run: | - set -euo pipefail + set -eu echo "Building .NET solution..." if dotnet build --configuration Release --no-restore --verbosity normal; then echo "status=success" >> "$GITHUB_OUTPUT" @@ -102,9 +180,9 @@ runs: - name: Run Tests id: test - shell: bash + shell: sh run: | - set -euo pipefail + set -eu echo "Running .NET tests..." if find . -name "*.csproj" | xargs grep -lE "(Microsoft\.NET\.Test\.Sdk|xunit|nunit)" | head -1 | grep -q .; then if dotnet test --configuration Release --no-build \ diff --git a/csharp-lint-check/action.yml b/csharp-lint-check/action.yml index ac0a580..56ed777 100644 --- a/csharp-lint-check/action.yml +++ b/csharp-lint-check/action.yml @@ -36,15 +36,15 @@ runs: steps: - name: Validate Inputs id: validate - shell: bash + shell: sh env: DOTNET_VERSION: ${{ inputs.dotnet-version }} run: | - set -euo pipefail + set -eu # Validate .NET version format if provided - if [[ -n "$DOTNET_VERSION" ]]; then - if ! [[ "$DOTNET_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then + if [ -n "$DOTNET_VERSION" ]; then + if ! printf '%s' "$DOTNET_VERSION" | grep -qE '^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$'; then echo "::error::Invalid dotnet-version format: '$DOTNET_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 7.0, 8.0.100)" exit 1 fi @@ -66,28 +66,122 @@ runs: - name: Detect .NET SDK Version id: detect-dotnet-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'dotnet' - default-version: ${{ inputs.dotnet-version || '7.0' }} + shell: sh + env: + DEFAULT_VERSION: "${{ inputs.dotnet-version || '7.0' }}" + 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]*\.[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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for dotnet..." >&2 + version=$(awk '/^dotnet[[: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 .NET 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 dotnet..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "dotnet:" | head -1 | \ + sed -n -E "s/.*dotnet:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET 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 dotnet..." >&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/.*dotnet:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping devcontainer.json parsing" >&2 + fi + fi + + # Parse global.json + if [ -z "$detected_version" ] && [ -f global.json ]; then + echo "Checking global.json..." >&2 + if command -v jq >/dev/null 2>&1; then + version=$(jq -r '.sdk.version // empty' global.json 2>/dev/null || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET version in global.json: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping global.json parsing" >&2 + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default .NET version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected .NET version: $detected_version" >&2 - name: Setup .NET SDK uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }} + cache: true + cache-dependency-path: '**/packages.lock.json' - name: Install dotnet-format - shell: bash + shell: sh run: | - set -euo pipefail + set -eu dotnet tool install --global dotnet-format --version 7.0.1 - name: Run dotnet-format id: dotnet-format - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Initialize counters errors_count=0 diff --git a/csharp-publish/README.md b/csharp-publish/README.md index b425e4f..f41eac4 100644 --- a/csharp-publish/README.md +++ b/csharp-publish/README.md @@ -8,11 +8,12 @@ Publishes a C# project to GitHub Packages. ### Inputs -| name | description | required | default | -|------------------|----------------------------------------------------|----------|-------------| -| `dotnet-version` |

Version of .NET SDK to use.

| `false` | `""` | -| `namespace` |

GitHub namespace for the package.

| `true` | `ivuorinen` | -| `token` |

GitHub token with package write permissions

| `false` | `""` | +| name | description | required | default | +|------------------|--------------------------------------------------------------------|----------|-------------| +| `dotnet-version` |

Version of .NET SDK to use.

| `false` | `""` | +| `namespace` |

GitHub namespace for the package.

| `true` | `ivuorinen` | +| `token` |

GitHub token with package write permissions

| `false` | `""` | +| `max-retries` |

Maximum number of retry attempts for dependency restoration

| `false` | `3` | ### Outputs @@ -48,4 +49,10 @@ This action is a `composite` action. # # Required: false # Default: "" + + max-retries: + # Maximum number of retry attempts for dependency restoration + # + # Required: false + # Default: 3 ``` diff --git a/csharp-publish/action.yml b/csharp-publish/action.yml index 0310666..cb02611 100644 --- a/csharp-publish/action.yml +++ b/csharp-publish/action.yml @@ -22,6 +22,10 @@ inputs: token: description: 'GitHub token with package write permissions' required: false + max-retries: + description: 'Maximum number of retry attempts for dependency restoration' + required: false + default: '3' outputs: publish_status: @@ -38,7 +42,7 @@ runs: using: composite steps: - name: Mask Secrets - shell: bash + shell: sh env: API_KEY: ${{ inputs.token || github.token }} run: | @@ -60,57 +64,138 @@ runs: - name: Detect .NET SDK Version id: detect-dotnet-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'dotnet' - default-version: '7.0' + shell: sh + env: + DEFAULT_VERSION: '7.0' + 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]*\.[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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for dotnet..." >&2 + version=$(awk '/^dotnet[[: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 .NET 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 dotnet..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "dotnet:" | head -1 | \ + sed -n -E "s/.*dotnet:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET 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 dotnet..." >&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/.*dotnet:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping devcontainer.json parsing" >&2 + fi + fi + + # Parse global.json + if [ -z "$detected_version" ] && [ -f global.json ]; then + echo "Checking global.json..." >&2 + if command -v jq >/dev/null 2>&1; then + version=$(jq -r '.sdk.version // empty' global.json 2>/dev/null || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found .NET version in global.json: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping global.json parsing" >&2 + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default .NET version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected .NET version: $detected_version" >&2 - name: Setup .NET SDK uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: dotnet-version: ${{ inputs.dotnet-version || steps.detect-dotnet-version.outputs.detected-version }} - - - name: Cache NuGet packages - id: cache-nuget - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'nuget' - paths: '~/.nuget/packages' - key-files: '**/*.csproj,**/*.props,**/*.targets' - key-prefix: 'csharp-publish' + cache: true + cache-dependency-path: '**/packages.lock.json' - name: Restore Dependencies - shell: bash - env: - CACHE_HIT: ${{ steps.cache-nuget.outputs.cache-hit }} - run: | - set -euo pipefail - - # Always run dotnet restore to ensure project.assets.json is present - if [[ "$CACHE_HIT" == 'true' ]]; then - echo "Cache hit - running fast dotnet restore" - fi - dotnet restore + uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 + with: + timeout_minutes: 10 + max_attempts: ${{ inputs.max-retries }} + command: | + echo "Restoring .NET dependencies..." + dotnet restore --verbosity normal - name: Build Solution - shell: bash + shell: sh run: | - set -euo pipefail + set -eu dotnet build --configuration Release --no-restore - name: Pack Solution - shell: bash + shell: sh run: | - set -euo pipefail + set -eu dotnet pack --configuration Release --no-build --no-restore --output ./artifacts - name: Extract Package Version id: extract-version - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Find the newest .nupkg file by modification time and extract version PACKAGE_FILE=$(find ./artifacts -name "*.nupkg" -type f -printf '%T@ %p\n' | sort -rn | head -n 1 | cut -d' ' -f2-) @@ -126,12 +211,12 @@ runs: - name: Publish Package id: publish-package - shell: bash + shell: sh env: API_KEY: ${{ inputs.token || github.token }} NAMESPACE: ${{ inputs.namespace }} run: | - set -euo pipefail + set -eu PACKAGE_URL="https://github.com/$NAMESPACE/packages/nuget" printf '%s\n' "package_url=$PACKAGE_URL" >> "$GITHUB_OUTPUT" @@ -156,7 +241,7 @@ runs: - name: Set publish status output if: always() id: set-status - shell: bash + shell: sh env: PUBLISH_STATUS: ${{ steps.publish-package.outcome == 'success' && 'success' || 'failure' }} run: |- diff --git a/csharp-publish/rules.yml b/csharp-publish/rules.yml index 342f4f9..076848e 100644 --- a/csharp-publish/rules.yml +++ b/csharp-publish/rules.yml @@ -2,7 +2,7 @@ # Validation rules for csharp-publish action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 -# Coverage: 100% (3/3 inputs) +# Coverage: 100% (4/4 inputs) # # This file defines validation rules for the csharp-publish GitHub Action. # Rules are automatically applied by validate-inputs action when this @@ -17,15 +17,17 @@ required_inputs: - namespace optional_inputs: - dotnet-version + - max-retries - token conventions: dotnet-version: dotnet_version + max-retries: numeric_range_1_10 namespace: namespace_with_lookahead token: github_token overrides: {} statistics: - total_inputs: 3 - validated_inputs: 3 + total_inputs: 4 + validated_inputs: 4 skipped_inputs: 0 coverage_percentage: 100 validation_coverage: 100 diff --git a/docker-build/action.yml b/docker-build/action.yml index b831b68..50405e9 100644 --- a/docker-build/action.yml +++ b/docker-build/action.yml @@ -159,7 +159,7 @@ runs: parallel-builds: ${{ inputs.parallel-builds }} - name: Check Dockerfile Exists - shell: bash + shell: sh env: DOCKERFILE: ${{ inputs.dockerfile }} run: | @@ -186,12 +186,12 @@ runs: - name: Detect Available Platforms id: detect-platforms - shell: bash + shell: sh env: ARCHITECTURES: ${{ inputs.architectures }} AUTO_DETECT: ${{ inputs.auto-detect-platforms }} run: | - set -euo pipefail + set -eu # When auto-detect is enabled, try to detect available platforms if [ "$AUTO_DETECT" = "true" ]; then @@ -212,11 +212,11 @@ runs: - name: Determine Image Name id: image-name - shell: bash + shell: sh env: IMAGE_NAME: ${{ inputs.image-name }} run: | - set -euo pipefail + set -eu if [ -z "$IMAGE_NAME" ]; then repo_name=$(basename "${GITHUB_REPOSITORY}") @@ -227,16 +227,23 @@ runs: - name: Parse Build Arguments id: build-args - shell: bash + shell: sh env: BUILD_ARGS_INPUT: ${{ inputs.build-args }} run: | - set -euo pipefail + set -eu args="" if [ -n "$BUILD_ARGS_INPUT" ]; then - IFS=',' read -ra BUILD_ARGS <<< "$BUILD_ARGS_INPUT" - for arg in "${BUILD_ARGS[@]}"; do + # Save IFS and use comma as delimiter + old_ifs="$IFS" + IFS=',' + # Use set -- to load comma-separated values into positional parameters + set -- $BUILD_ARGS_INPUT + IFS="$old_ifs" + + # Iterate through positional parameters + for arg; do args="$args --build-arg $arg" done fi @@ -244,16 +251,23 @@ runs: - name: Parse Build Contexts id: build-contexts - shell: bash + shell: sh env: BUILD_CONTEXTS: ${{ inputs.build-contexts }} run: | - set -euo pipefail + set -eu contexts="" if [ -n "$BUILD_CONTEXTS" ]; then - IFS=',' read -ra CONTEXTS <<< "$BUILD_CONTEXTS" - for ctx in "${CONTEXTS[@]}"; do + # Save IFS and use comma as delimiter + old_ifs="$IFS" + IFS=',' + # Use set -- to load comma-separated values into positional parameters + set -- $BUILD_CONTEXTS + IFS="$old_ifs" + + # Iterate through positional parameters + for ctx; do contexts="$contexts --build-context $ctx" done fi @@ -261,36 +275,46 @@ runs: - name: Parse Secrets id: secrets - shell: bash + shell: sh env: INPUT_SECRETS: ${{ inputs.secrets }} run: | - set -euo pipefail + set -eu secrets="" if [ -n "$INPUT_SECRETS" ]; then - IFS=',' read -ra SECRETS <<< "$INPUT_SECRETS" - for secret in "${SECRETS[@]}"; do + # Save IFS and use comma as delimiter + old_ifs="$IFS" + IFS=',' + # Use set -- to load comma-separated values into positional parameters + set -- $INPUT_SECRETS + IFS="$old_ifs" + + # Iterate through positional parameters + for secret; do # Trim whitespace secret=$(echo "$secret" | xargs) - if [[ "$secret" == *"="* ]]; then - # Parse id=src format - id="${secret%%=*}" - src="${secret#*=}" + case "$secret" in + *=*) + # Parse id=src format + id="${secret%%=*}" + src="${secret#*=}" - # Validate id and src are not empty - if [[ -z "$id" || -z "$src" ]]; then - echo "::error::Invalid secret format: '$secret'. Expected 'id=src' where both id and src are non-empty" + # Validate id and src are not empty + if [ -z "$id" ] || [ -z "$src" ]; then + echo "::error::Invalid secret format: '$secret'. Expected 'id=src' where both id and src are non-empty" + exit 1 + fi + + secrets="$secrets --secret id=$id,src=$src" + ;; + *) + # Handle legacy format - treat as id only (error for now) + echo "::error::Invalid secret format: '$secret'. Expected 'id=src' format for Buildx compatibility" exit 1 - fi - - secrets="$secrets --secret id=$id,src=$src" - else - # Handle legacy format - treat as id only (error for now) - echo "::error::Invalid secret format: '$secret'. Expected 'id=src' format for Buildx compatibility" - exit 1 - fi + ;; + esac done fi echo "secrets=${secrets}" >> $GITHUB_OUTPUT @@ -305,7 +329,7 @@ runs: - name: Set up Build Cache id: cache - shell: bash + shell: sh env: CACHE_IMPORT: ${{ inputs.cache-import }} CACHE_FROM: ${{ inputs.cache-from }} @@ -314,7 +338,7 @@ runs: INPUT_TOKEN: ${{ inputs.token }} CACHE_MODE: ${{ inputs.cache-mode }} run: | - set -euo pipefail + set -eu # Use provided token or fall back to GITHUB_TOKEN TOKEN="${INPUT_TOKEN:-${GITHUB_TOKEN:-}}" @@ -335,7 +359,7 @@ runs: fi # Registry cache configuration for better performance (only if authenticated) - if [ "$PUSH" == "true" ] || [ -n "$TOKEN" ]; then + if [ "$PUSH" = "true" ] || [ -n "$TOKEN" ]; then normalized_repo=$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._\/-]/-/g') registry_cache_ref="ghcr.io/${normalized_repo}/cache:latest" cache_from="$cache_from --cache-from type=registry,ref=$registry_cache_ref" @@ -349,16 +373,21 @@ runs: # Also include local cache as fallback cache_from="$cache_from --cache-from type=local,src=/tmp/.buildx-cache" - if [[ "$cache_to" != *"type=local"* ]]; then - cache_to="$cache_to --cache-to type=local,dest=/tmp/.buildx-cache-new,mode=${cache_mode}" - fi + case "$cache_to" in + *"type=local"*) + # Already has local cache, don't add + ;; + *) + cache_to="$cache_to --cache-to type=local,dest=/tmp/.buildx-cache-new,mode=${cache_mode}" + ;; + esac echo "from=${cache_from}" >> $GITHUB_OUTPUT echo "to=${cache_to}" >> $GITHUB_OUTPUT - name: Build Multi-Architecture Docker Image id: build - shell: bash + shell: sh env: AUTO_DETECT_PLATFORMS: ${{ inputs.auto-detect-platforms }} DETECTED_PLATFORMS: ${{ steps.detect-platforms.outputs.platforms }} @@ -378,7 +407,7 @@ runs: DOCKERFILE: ${{ inputs.dockerfile }} CONTEXT: ${{ inputs.context }} run: | - set -euo pipefail + set -eu # Track build start time build_start=$(date +%s) @@ -518,9 +547,9 @@ runs: - name: Process Scan Results id: scan-output if: inputs.scan-image == 'true' && inputs.dry-run != 'true' - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Read and format scan results for output scan_results=$(cat trivy-results.json | jq -c '.') @@ -539,12 +568,12 @@ runs: - name: Sign Image id: sign if: inputs.sign-image == 'true' && inputs.push == 'true' && inputs.dry-run != 'true' - shell: bash + shell: sh env: IMAGE_NAME: ${{ steps.image-name.outputs.name }} IMAGE_TAG: ${{ inputs.tag }} run: | - set -euo pipefail + set -eu # Sign the image (using keyless signing with OIDC) export COSIGN_EXPERIMENTAL=1 @@ -555,13 +584,13 @@ runs: - name: Verify Build id: verify if: inputs.dry-run != 'true' - shell: bash + shell: sh env: PUSH: ${{ inputs.push }} IMAGE_NAME: ${{ steps.image-name.outputs.name }} IMAGE_TAG: ${{ inputs.tag }} run: | - set -euo pipefail + set -eu # Verify image exists if [ "$PUSH" == "true" ]; then @@ -584,9 +613,9 @@ runs: - name: Cleanup if: always() - shell: bash + shell: sh run: |- - set -euo pipefail + set -eu # Cleanup temporary files rm -rf /tmp/.buildx-cache* diff --git a/eslint-lint/action.yml b/eslint-lint/action.yml index 33c9ccd..d215cfd 100644 --- a/eslint-lint/action.yml +++ b/eslint-lint/action.yml @@ -97,7 +97,7 @@ runs: steps: - name: Validate Inputs id: validate - shell: bash + shell: sh env: MODE: ${{ inputs.mode }} WORKING_DIRECTORY: ${{ inputs.working-directory }} @@ -113,7 +113,7 @@ runs: EMAIL: ${{ inputs.email }} USERNAME: ${{ inputs.username }} run: | - set -euo pipefail + set -eu # Validate mode case "$MODE" in @@ -133,44 +133,54 @@ runs: fi # Validate working directory path security (prevent traversal) - if [[ "$WORKING_DIRECTORY" == *".."* ]]; then - echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed" - exit 1 - fi + case "$WORKING_DIRECTORY" in + *..*) + echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed" + exit 1 + ;; + esac # Validate ESLint version format - if [[ -n "$ESLINT_VERSION" ]] && [[ "$ESLINT_VERSION" != "latest" ]]; then - if ! [[ "$ESLINT_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?(-[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::Invalid eslint-version format: '$ESLINT_VERSION'. Expected format: X.Y.Z or 'latest' (e.g., 8.57.0, latest)" + if [ -n "$ESLINT_VERSION" ] && [ "$ESLINT_VERSION" != "latest" ]; then + if ! echo "$ESLINT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9]+([.-][a-zA-Z0-9]+)*)?$'; then + echo "::error::Invalid eslint-version format: '$ESLINT_VERSION'. Expected format: X.Y.Z or X.Y.Z-prerelease or 'latest' (e.g., 8.57.0, 8.57.0-rc.1, latest)" exit 1 fi fi # Validate config file path if not default - if [[ "$CONFIG_FILE" != ".eslintrc" ]] && [[ "$CONFIG_FILE" == *".."* ]]; then - echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed" - exit 1 + if [ "$CONFIG_FILE" != ".eslintrc" ]; then + case "$CONFIG_FILE" in + *..*) + echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed" + exit 1 + ;; + esac fi # Validate ignore file path if not default - if [[ "$IGNORE_FILE" != ".eslintignore" ]] && [[ "$IGNORE_FILE" == *".."* ]]; then - echo "::error::Invalid ignore file path: '$IGNORE_FILE'. Path traversal not allowed" - exit 1 + if [ "$IGNORE_FILE" != ".eslintignore" ]; then + case "$IGNORE_FILE" in + *..*) + echo "::error::Invalid ignore file path: '$IGNORE_FILE'. Path traversal not allowed" + exit 1 + ;; + esac fi - # Validate file extensions format - if ! [[ "$FILE_EXTENSIONS" =~ ^(\.[a-zA-Z0-9]+)(,\.[a-zA-Z0-9]+)*$ ]]; then + # Validate file extensions format (must start with . and contain letters/numbers) + if ! echo "$FILE_EXTENSIONS" | grep -Eq '^\.[a-zA-Z0-9]+(,\.[a-zA-Z0-9]+)*$'; then echo "::error::Invalid file extensions format: '$FILE_EXTENSIONS'. Expected format: .js,.jsx,.ts,.tsx" exit 1 fi # Validate boolean inputs validate_boolean() { - local value="$1" - local name="$2" + value="$1" + name="$2" - case "${value,,}" in - true|false) + case "$value" in + true|True|TRUE|false|False|FALSE) ;; *) echo "::error::Invalid boolean value for $name: '$value'. Expected: true or false" @@ -182,11 +192,13 @@ runs: validate_boolean "$CACHE" "cache" validate_boolean "$FAIL_ON_ERROR" "fail-on-error" - # Validate max warnings (positive integer) - if ! [[ "$MAX_WARNINGS" =~ ^[0-9]+$ ]]; then - echo "::error::Invalid max-warnings: '$MAX_WARNINGS'. Must be a non-negative integer" - exit 1 - fi + # Validate max warnings (non-negative integer) + case "$MAX_WARNINGS" in + ''|*[!0-9]*) + echo "::error::Invalid max-warnings: '$MAX_WARNINGS'. Must be a non-negative integer" + exit 1 + ;; + esac # Validate report format case "$REPORT_FORMAT" in @@ -199,15 +211,22 @@ runs: esac # Validate max retries - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then + case "$MAX_RETRIES" in + ''|*[!0-9]*) + echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10" + exit 1 + ;; + esac + + if [ "$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 email and username for fix mode if [ "$MODE" = "fix" ]; then - if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" + if ! echo "$EMAIL" | grep -Eq '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then + echo "::error::Invalid email format: '$EMAIL'. Expected valid email address (e.g., user@example.com)" exit 1 fi @@ -219,20 +238,26 @@ runs: exit 1 fi - if ! [[ "$username" =~ ^[a-zA-Z0-9-]+$ ]]; then - echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed" - exit 1 - fi + case "$username" in + *[!a-zA-Z0-9-]*) + echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed" + exit 1 + ;; + esac - if [[ "$username" == -* ]] || [[ "$username" == *- ]]; then - echo "::error::Invalid username '$username'. Cannot start or end with hyphen" - exit 1 - fi + case "$username" in + -*|*-) + echo "::error::Invalid username '$username'. Cannot start or end with hyphen" + exit 1 + ;; + esac - if [[ "$username" == *--* ]]; then - echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" - exit 1 - fi + case "$username" in + *--*) + echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" + exit 1 + ;; + esac fi echo "Input validation completed successfully" @@ -242,26 +267,79 @@ runs: with: token: ${{ inputs.token || github.token }} - - name: Node Setup - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + - name: Detect Package Manager + id: detect-pm + shell: sh + run: | + set -eu + + # Detect package manager from lockfiles + if [ -f bun.lockb ]; then + package_manager="bun" + elif [ -f pnpm-lock.yaml ]; then + package_manager="pnpm" + elif [ -f yarn.lock ]; then + package_manager="yarn" + else + package_manager="npm" + fi + + printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT" + echo "Detected package manager: $package_manager" + + - name: Setup Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: '22' + + - name: Enable Corepack + shell: sh + run: | + set -eu + corepack enable + + - name: Install Package Manager + shell: sh + env: + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} + run: | + set -eu + + case "$PACKAGE_MANAGER" in + pnpm) + corepack prepare pnpm@latest --activate + ;; + yarn) + corepack prepare yarn@stable --activate + ;; + bun|npm) + # Bun installed separately, npm built-in + ;; + esac + + - name: Setup Bun + if: steps.detect-pm.outputs.package-manager == 'bun' + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: latest - name: Cache Node Dependencies id: cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'eslint-lint-${{ inputs.mode }}-${{ steps.node-setup.outputs.package-manager }}' + path: node_modules + key: ${{ runner.os }}-eslint-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }} + restore-keys: | + ${{ runner.os }}-eslint-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}- + ${{ runner.os }}-eslint-lint-${{ inputs.mode }}- - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' - shell: bash + shell: sh env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} run: | - set -euo pipefail + set -eu echo "Installing dependencies using $PACKAGE_MANAGER..." @@ -289,10 +367,10 @@ runs: - name: Run ESLint Check if: inputs.mode == 'check' id: check - shell: bash + shell: sh working-directory: ${{ inputs.working-directory }} env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} ESLINT_VERSION: ${{ inputs.eslint-version }} CONFIG_FILE: ${{ inputs.config-file }} CACHE: ${{ inputs.cache }} @@ -301,12 +379,25 @@ runs: REPORT_FORMAT: ${{ inputs.report-format }} FILE_EXTENSIONS: ${{ inputs.file-extensions }} run: | - set -euo pipefail + set -eu echo "Running ESLint check mode..." - # Build ESLint command - eslint_cmd="npx eslint ." + # Build ESLint command based on package manager + case "$PACKAGE_MANAGER" in + "pnpm") + eslint_cmd="pnpm exec eslint . --ext $FILE_EXTENSIONS" + ;; + "yarn") + eslint_cmd="yarn eslint . --ext $FILE_EXTENSIONS" + ;; + "bun") + eslint_cmd="bunx eslint . --ext $FILE_EXTENSIONS" + ;; + "npm"|*) + eslint_cmd="npx eslint . --ext $FILE_EXTENSIONS" + ;; + esac # Add config file if specified if [ "$CONFIG_FILE" != ".eslintrc" ] && [ -f "$CONFIG_FILE" ]; then @@ -373,12 +464,13 @@ runs: - name: Run ESLint Fix if: inputs.mode == 'fix' id: fix - shell: bash + shell: sh working-directory: ${{ inputs.working-directory }} env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} + FILE_EXTENSIONS: ${{ inputs.file-extensions }} run: | - set -euo pipefail + set -eu echo "Running ESLint fix mode..." @@ -388,16 +480,16 @@ runs: # Run ESLint fix based on package manager case "$PACKAGE_MANAGER" in "pnpm") - pnpm exec eslint . --fix || true + pnpm exec eslint . --ext $FILE_EXTENSIONS --fix || true ;; "yarn") - yarn eslint . --fix || true + yarn eslint . --ext $FILE_EXTENSIONS --fix || true ;; "bun") - bunx eslint . --fix || true + bunx eslint . --ext $FILE_EXTENSIONS --fix || true ;; "npm"|*) - npx eslint . --fix || true + npx eslint . --ext $FILE_EXTENSIONS --fix || true ;; esac diff --git a/generate_listing.cjs b/generate_listing.cjs index 3bbd2b3..e3f1157 100755 --- a/generate_listing.cjs +++ b/generate_listing.cjs @@ -8,7 +8,6 @@ const { markdownTable } = require('markdown-table'); // Category mappings const CATEGORIES = { // Setup & Environment - 'node-setup': 'Setup', 'language-version-detect': 'Setup', // Utilities @@ -29,8 +28,6 @@ const CATEGORIES = { // Testing & Quality 'php-tests': 'Testing', - 'php-laravel-phpunit': 'Testing', - 'php-composer': 'Testing', // Build & Package 'csharp-build': 'Build', @@ -47,7 +44,6 @@ const CATEGORIES = { 'sync-labels': 'Repository', stale: 'Repository', 'compress-images': 'Repository', - 'common-cache': 'Repository', 'codeql-analysis': 'Repository', // Validation @@ -56,11 +52,8 @@ const CATEGORIES = { // Language support mappings const LANGUAGE_SUPPORT = { - 'node-setup': ['Node.js', 'JavaScript', 'TypeScript'], 'language-version-detect': ['PHP', 'Python', 'Go', '.NET', 'Node.js'], - 'php-tests': ['PHP'], - 'php-laravel-phpunit': ['PHP', 'Laravel'], - 'php-composer': ['PHP'], + 'php-tests': ['PHP', 'Laravel'], 'python-lint-fix': ['Python'], 'go-lint': ['Go'], 'go-build': ['Go'], @@ -85,7 +78,6 @@ const LANGUAGE_SUPPORT = { 'release-monthly': ['GitHub Actions'], stale: ['GitHub Actions'], 'compress-images': ['Images', 'PNG', 'JPEG'], - 'common-cache': ['Caching'], }; // Icon mapping for GitHub branding diff --git a/go-build/action.yml b/go-build/action.yml index c243a74..3bde6bc 100644 --- a/go-build/action.yml +++ b/go-build/action.yml @@ -54,10 +54,109 @@ runs: - name: Detect Go Version id: detect-go-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'go' - default-version: "${{ inputs.go-version || '1.21' }}" + shell: sh + env: + DEFAULT_VERSION: "${{ inputs.go-version || '1.24' }}" + 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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for golang..." >&2 + version=$(awk '/^golang[[: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 Go 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 golang..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "golang:" | head -1 | \ + sed -n -E "s/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Go 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 golang..." >&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/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?. */\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Go version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + fi + fi + + # Parse .go-version file + if [ -z "$detected_version" ] && [ -f .go-version ]; then + echo "Checking .go-version..." >&2 + version=$(tr -d '\r' < .go-version | head -1) + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Go version in .go-version: $version" >&2 + detected_version="$version" + fi + fi + fi + + # Parse go.mod + if [ -z "$detected_version" ] && [ -f go.mod ]; then + echo "Checking go.mod..." >&2 + version=$(grep -E '^go[[:space:]]+[0-9]' go.mod | awk '{print $2}' | head -1 || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Go version in go.mod: $version" >&2 + detected_version="$version" + fi + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default Go version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected Go version: $detected_version" >&2 - name: Setup Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 @@ -65,17 +164,7 @@ runs: go-version: ${{ steps.detect-go-version.outputs.detected-version }} cache: true - - name: Cache Go Dependencies - id: cache-go - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'go' - paths: '~/go/pkg/mod' - key-files: 'go.mod,go.sum' - key-prefix: 'go-build' - - name: Download Dependencies - if: steps.cache-go.outputs.cache-hit != 'true' uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 with: timeout_minutes: 10 @@ -87,11 +176,11 @@ runs: - name: Build Go Project id: build - shell: bash + shell: sh env: DESTINATION: ${{ inputs.destination }} run: | - set -euo pipefail + set -eu echo "Building Go project..." # Create destination directory @@ -126,9 +215,9 @@ runs: - name: Run Tests id: test - shell: bash + shell: sh run: | - set -euo pipefail + set -eu echo "Running Go tests..." if find . -name "*_test.go" | grep -q .; then # Check if race detector is supported on this platform diff --git a/go-lint/action.yml b/go-lint/action.yml index c04d00e..84bf3d4 100644 --- a/go-lint/action.yml +++ b/go-lint/action.yml @@ -215,16 +215,17 @@ runs: with: token: ${{ inputs.token || github.token }} - - name: Set up Cache + - name: Cache golangci-lint id: cache if: inputs.cache == 'true' - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - type: 'go' - paths: '~/.cache/golangci-lint,~/.cache/go-build' - key-prefix: 'golangci-${{ inputs.golangci-lint-version }}' - key-files: 'go.sum,${{ inputs.config-file }}' - restore-keys: '${{ runner.os }}-golangci-${{ inputs.golangci-lint-version }}-' + path: | + ~/.cache/golangci-lint + ~/.cache/go-build + key: ${{ runner.os }}-golangci-${{ inputs.golangci-lint-version }}-${{ hashFiles('go.sum', inputs.config-file) }} + restore-keys: | + ${{ runner.os }}-golangci-${{ inputs.golangci-lint-version }}- - name: Install golangci-lint shell: sh diff --git a/language-version-detect/README.md b/language-version-detect/README.md index 5f3a969..0b5ac2c 100644 --- a/language-version-detect/README.md +++ b/language-version-detect/README.md @@ -4,7 +4,7 @@ ### Description -Detects language version from project configuration files with support for PHP, Python, Go, and .NET. +DEPRECATED: This action is deprecated. Inline version detection directly in your actions instead. Detects language version from project configuration files with support for PHP, Python, Go, and .NET. ### Inputs @@ -28,7 +28,7 @@ This action is a `composite` action. ### Usage ```yaml -- uses: ivuorinen/actions/language-version-detect@v2025 +- uses: ivuorinen/actions/language-version-detect@main with: language: # Language to detect version for (php, python, go, dotnet) diff --git a/language-version-detect/action.yml b/language-version-detect/action.yml index 9848adc..b93b166 100644 --- a/language-version-detect/action.yml +++ b/language-version-detect/action.yml @@ -3,8 +3,9 @@ # - 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.' +description: 'DEPRECATED: This action is deprecated. Inline version detection directly in your actions instead. Detects language version from project configuration files with support for PHP, Python, Go, and .NET.' author: 'Ismo Vuorinen' +deprecated: true branding: icon: code @@ -80,7 +81,7 @@ runs: php) # Validate PHP version format (X.Y or X.Y.Z) case "$version" in - [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) + [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)" @@ -108,7 +109,7 @@ runs: python) # Validate Python version format case "$version" in - [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) + [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)" @@ -134,7 +135,7 @@ runs: go) # Validate Go version format case "$version" in - [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) + [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)" @@ -160,7 +161,7 @@ runs: dotnet) # Validate .NET version format case "$version" in - [0-9]* | [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) + [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)" @@ -186,11 +187,203 @@ runs: - name: Parse Language Version id: parse-version - uses: ivuorinen/actions/version-file-parser@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: ${{ inputs.language }} - tool-versions-key: ${{ inputs.language == 'go' && 'golang' || inputs.language }} - dockerfile-image: ${{ inputs.language == 'go' && 'golang' || inputs.language }} - version-file: ${{ inputs.language == 'php' && '.php-version' || inputs.language == 'python' && '.python-version' || inputs.language == 'go' && '.go-version' || '' }} - validation-regex: '^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$' - default-version: ${{ steps.validate.outputs.default_version || inputs.default-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 diff --git a/language-version-detect/rules.yml b/language-version-detect/rules.yml index 74fd787..dc600aa 100644 --- a/language-version-detect/rules.yml +++ b/language-version-detect/rules.yml @@ -11,7 +11,8 @@ schema_version: '1.0' action: language-version-detect -description: Detects language version from project configuration files with support for PHP, Python, Go, and .NET. +description: 'DEPRECATED: This action is deprecated. Inline version detection directly in your actions instead. Detects language + version from project configuration files with support for PHP, Python, Go, and .NET.' generator_version: 1.0.0 required_inputs: - language diff --git a/node-setup/CustomValidator.py b/node-setup/CustomValidator.py deleted file mode 100755 index b3b58eb..0000000 --- a/node-setup/CustomValidator.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for node-setup action.""" - -from __future__ import annotations - -from pathlib import Path -import sys - -# Add validate-inputs directory to path to import validators -validate_inputs_path = Path(__file__).parent.parent / "validate-inputs" -sys.path.insert(0, str(validate_inputs_path)) - -from validators.base import BaseValidator -from validators.version import VersionValidator - - -class CustomValidator(BaseValidator): - """Custom validator for node-setup action.""" - - def __init__(self, action_type: str = "node-setup") -> None: - """Initialize node-setup validator.""" - super().__init__(action_type) - self.version_validator = VersionValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate node-setup action inputs.""" - valid = True - - # Validate default-version if provided - if "default-version" in inputs: - value = inputs["default-version"] - - # Empty string should fail validation - if value == "": - self.add_error("Node version cannot be empty") - valid = False - elif value: - # Use the Node version validator - result = self.version_validator.validate_node_version(value, "default-version") - - # Propagate errors from the version validator - for error in self.version_validator.errors: - if error not in self.errors: - self.add_error(error) - - # Clear the version validator's errors after propagating - self.version_validator.clear_errors() - - if not result: - valid = False - - # Validate package-manager if provided - if "package-manager" in inputs: - value = inputs["package-manager"] - if value and value not in ["npm", "yarn", "pnpm", "bun"]: - self.add_error( - f"Invalid package manager: {value}. Must be one of: npm, yarn, pnpm, bun" - ) - valid = False - - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs.""" - return [] - - def get_validation_rules(self) -> dict: - """Get validation rules.""" - return { - "default-version": { - "type": "node_version", - "required": False, - "description": "Default Node.js version to use", - }, - "package-manager": { - "type": "string", - "required": False, - "description": "Package manager to use", - }, - } diff --git a/node-setup/README.md b/node-setup/README.md deleted file mode 100644 index fbac71a..0000000 --- a/node-setup/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# ivuorinen/actions/node-setup - -## Node Setup - -### Description - -Sets up Node.js environment with version detection and package manager configuration. - -### Inputs - -| name | description | required | default | -|-------------------|--------------------------------------------------------------------------|----------|------------------------------| -| `default-version` |

Default Node.js version to use if no configuration file is found.

| `false` | `22` | -| `package-manager` |

Node.js package manager to use (npm, yarn, pnpm, bun, auto)

| `false` | `auto` | -| `registry-url` |

Custom NPM registry URL

| `false` | `https://registry.npmjs.org` | -| `token` |

Auth token for private registry

| `false` | `""` | -| `node-mirror` |

Custom Node.js binary mirror

| `false` | `""` | -| `force-version` |

Force specific Node.js version regardless of config files

| `false` | `""` | - -### Outputs - -| name | description | -|-------------------|-------------------------------------| -| `node-version` |

Installed Node.js version

| -| `package-manager` |

Selected package manager

| -| `node-path` |

Path to Node.js installation

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/node-setup@main - with: - default-version: - # Default Node.js version to use if no configuration file is found. - # - # Required: false - # Default: 22 - - package-manager: - # Node.js package manager to use (npm, yarn, pnpm, bun, auto) - # - # Required: false - # Default: auto - - registry-url: - # Custom NPM registry URL - # - # Required: false - # Default: https://registry.npmjs.org - - token: - # Auth token for private registry - # - # Required: false - # Default: "" - - node-mirror: - # Custom Node.js binary mirror - # - # Required: false - # Default: "" - - force-version: - # Force specific Node.js version regardless of config files - # - # Required: false - # Default: "" -``` diff --git a/node-setup/action.yml b/node-setup/action.yml deleted file mode 100644 index 04a618b..0000000 --- a/node-setup/action.yml +++ /dev/null @@ -1,242 +0,0 @@ -# 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: Parse Node.js Version - id: version - uses: ivuorinen/actions/version-file-parser@0fa9a68f07a1260b321f814202658a6089a43d42 - 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" - - - 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 diff --git a/node-setup/rules.yml b/node-setup/rules.yml deleted file mode 100644 index efc7687..0000000 --- a/node-setup/rules.yml +++ /dev/null @@ -1,45 +0,0 @@ ---- -# Validation rules for node-setup action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 83% (5/6 inputs) -# -# This file defines validation rules for the node-setup GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: node-setup -description: Sets up Node.js environment with version detection and package manager configuration. -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - default-version - - force-version - - node-mirror - - package-manager - - registry-url - - token -conventions: - default-version: semantic_version - force-version: semantic_version - package-manager: boolean - registry-url: url - token: github_token -overrides: - package-manager: package_manager_enum -statistics: - total_inputs: 6 - validated_inputs: 5 - skipped_inputs: 0 - coverage_percentage: 83 -validation_coverage: 83 -auto_detected: true -manual_review_required: false -quality_indicators: - has_required_inputs: false - has_token_validation: true - has_version_validation: true - has_file_validation: false - has_security_validation: true diff --git a/npm-publish/action.yml b/npm-publish/action.yml index 9414d79..03a5bf1 100644 --- a/npm-publish/action.yml +++ b/npm-publish/action.yml @@ -100,24 +100,76 @@ runs: with: token: ${{ inputs.token || github.token }} + - name: Detect Package Manager + id: detect-pm + shell: sh + run: | + set -eu + + # Detect package manager from lockfiles + if [ -f bun.lockb ]; then + package_manager="bun" + elif [ -f pnpm-lock.yaml ]; then + package_manager="pnpm" + elif [ -f yarn.lock ]; then + package_manager="yarn" + else + package_manager="npm" + fi + + printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT" + echo "Detected package manager: $package_manager" + - name: Setup Node.js - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: '22' + + - name: Enable Corepack + shell: sh + run: | + set -eu + corepack enable + + - name: Install Package Manager + shell: sh + env: + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} + run: | + set -eu + + case "$PACKAGE_MANAGER" in + pnpm) + corepack prepare pnpm@latest --activate + ;; + yarn) + corepack prepare yarn@stable --activate + ;; + bun|npm) + # Bun installed separately, npm built-in + ;; + esac + + - name: Setup Bun + if: steps.detect-pm.outputs.package-manager == 'bun' + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: latest - name: Cache Node Dependencies id: cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'npm-publish-${{ steps.node-setup.outputs.package-manager }}' + path: node_modules + key: ${{ runner.os }}-npm-publish-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }} + restore-keys: | + ${{ runner.os }}-npm-publish-${{ steps.detect-pm.outputs.package-manager }}- - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' shell: sh env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} run: | set -eu diff --git a/php-composer/CustomValidator.py b/php-composer/CustomValidator.py deleted file mode 100755 index c01fb14..0000000 --- a/php-composer/CustomValidator.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for php-composer action.""" - -from __future__ import annotations - -from pathlib import Path -import re -import sys - -# Add validate-inputs directory to path to import validators -validate_inputs_path = Path(__file__).parent.parent / "validate-inputs" -sys.path.insert(0, str(validate_inputs_path)) - -from validators.base import BaseValidator -from validators.boolean import BooleanValidator -from validators.file import FileValidator -from validators.numeric import NumericValidator -from validators.security import SecurityValidator -from validators.token import TokenValidator -from validators.version import VersionValidator - - -class CustomValidator(BaseValidator): - """Custom validator for php-composer action.""" - - def __init__(self, action_type: str = "php-composer") -> None: - """Initialize php-composer validator.""" - super().__init__(action_type) - self.boolean_validator = BooleanValidator() - self.file_validator = FileValidator() - self.numeric_validator = NumericValidator() - self.security_validator = SecurityValidator() - self.token_validator = TokenValidator() - self.version_validator = VersionValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate php-composer action inputs.""" - valid = True - - # Validate required input: php - if "php" not in inputs or not inputs["php"]: - self.add_error("Input 'php' is required") - valid = False - elif inputs["php"]: - php_version = inputs["php"] - if not self.is_github_expression(php_version): - # PHP version validation with minimum version check - result = self.version_validator.validate_php_version(php_version, "php") - for error in self.version_validator.errors: - if error not in self.errors: - self.add_error(error) - self.version_validator.clear_errors() - if not result: - valid = False - elif php_version and not php_version.startswith("$"): - # Additional check for minimum PHP version (7.0) - try: - parts = php_version.split(".") - major = int(parts[0]) - minor = int(parts[1]) if len(parts) > 1 else 0 - if major < 7 or (major == 7 and minor < 0): - self.add_error("PHP version must be 7.0 or higher") - valid = False - except (ValueError, IndexError): - pass # Already handled by validate_php_version - - # Validate extensions (empty string is invalid) - if "extensions" in inputs: - extensions = inputs["extensions"] - if extensions == "": - self.add_error("Extensions cannot be empty string") - valid = False - elif extensions: - if not self.is_github_expression(extensions): - # Extensions should be comma-separated list (spaces allowed after commas) - if not re.match(r"^[a-zA-Z0-9_-]+(\s*,\s*[a-zA-Z0-9_-]+)*$", extensions): - self.add_error("Invalid extensions format: must be comma-separated list") - valid = False - - # Check for injection - result = self.security_validator.validate_no_injection(extensions, "extensions") - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False - - # Validate tools (empty string is invalid) - if "tools" in inputs: - tools = inputs["tools"] - if tools == "": - self.add_error("Tools cannot be empty string") - valid = False - elif tools: - if not self.is_github_expression(tools): - # Tools should be comma-separated list with optional version constraints - # Allow: letters, numbers, dash, underscore, colon, dot, caret, tilde, @, / - # @ symbol allows Composer stability flags like dev-master@dev - # / allows vendor/package format like monolog/monolog@dev - # spaces after commas - if not re.match( - r"^[a-zA-Z0-9_:.@/\-^~]+(\s*,\s*[a-zA-Z0-9_:.@/\-^~]+)*$", tools - ): - self.add_error("Invalid tools format: must be comma-separated list") - valid = False - - # Check for injection - result = self.security_validator.validate_no_injection(tools, "tools") - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False - - # Validate composer-version (empty string is invalid, only 1 or 2 accepted) - if "composer-version" in inputs: - composer_version = inputs["composer-version"] - if composer_version == "": - self.add_error("Composer version cannot be empty string") - valid = False - elif composer_version: - if not self.is_github_expression(composer_version) and composer_version not in [ - "1", - "2", - ]: - self.add_error("Composer version must be 1 or 2") - valid = False - - # Validate stability - if inputs.get("stability"): - stability = inputs["stability"] - if not self.is_github_expression(stability): - valid_stabilities = ["stable", "RC", "beta", "alpha", "dev", "snapshot"] - if stability not in valid_stabilities: - self.add_error( - f"Invalid stability: {stability}. " - f"Must be one of: {', '.join(valid_stabilities)}" - ) - valid = False - - # Check for injection - result = self.security_validator.validate_no_injection(stability, "stability") - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False - - # Validate cache-directories (empty string is invalid, accepts directory paths) - if "cache-directories" in inputs: - cache_dirs = inputs["cache-directories"] - if cache_dirs == "": - self.add_error("Cache directories cannot be empty string") - valid = False - elif cache_dirs: - if not self.is_github_expression(cache_dirs): - # Should be comma-separated list of directories - dirs = cache_dirs.split(",") - for dir_path in dirs: - dir_path = dir_path.strip() - if dir_path: - result = self.file_validator.validate_file_path( - dir_path, "cache-directories" - ) - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False - - # Validate token (empty string is invalid) - if "token" in inputs: - token = inputs["token"] - if token == "": - self.add_error("Token cannot be empty string") - valid = False - elif token: - result = self.token_validator.validate_github_token(token, required=False) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - if not result: - valid = False - - # Validate max-retries - if inputs.get("max-retries"): - result = self.numeric_validator.validate_numeric_range( - inputs["max-retries"], min_val=1, max_val=10, name="max-retries" - ) - for error in self.numeric_validator.errors: - if error not in self.errors: - self.add_error(error) - self.numeric_validator.clear_errors() - if not result: - valid = False - - # Validate args (empty string is invalid, checks for injection if provided) - if "args" in inputs: - args = inputs["args"] - if args == "": - self.add_error("Args cannot be empty string") - valid = False - elif args: - if not self.is_github_expression(args): - # Check for command injection patterns - result = self.security_validator.validate_no_injection(args, "args") - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False - - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs.""" - return ["php"] - - def get_validation_rules(self) -> dict: - """Get validation rules.""" - rules_path = Path(__file__).parent / "rules.yml" - return self.load_rules(rules_path) diff --git a/php-composer/README.md b/php-composer/README.md deleted file mode 100644 index e579bcc..0000000 --- a/php-composer/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# ivuorinen/actions/php-composer - -## Run Composer Install - -### Description - -Runs Composer install on a repository with advanced caching and configuration. - -### Inputs - -| name | description | required | default | -|---------------------|---------------------------------------------------------------|----------|-----------------------------------------------------| -| `php` |

PHP Version to use.

| `true` | `8.4` | -| `extensions` |

Comma-separated list of PHP extensions to install

| `false` | `mbstring, xml, zip, curl, json` | -| `tools` |

Comma-separated list of Composer tools to install

| `false` | `composer:v2` | -| `args` |

Arguments to pass to Composer.

| `false` | `--no-progress --prefer-dist --optimize-autoloader` | -| `composer-version` |

Composer version to use (1 or 2)

| `false` | `2` | -| `stability` |

Minimum stability (stable, RC, beta, alpha, dev)

| `false` | `stable` | -| `cache-directories` |

Additional directories to cache (comma-separated)

| `false` | `""` | -| `token` |

GitHub token for private repository access

| `false` | `""` | -| `max-retries` |

Maximum number of retry attempts for Composer commands

| `false` | `3` | - -### Outputs - -| name | description | -|--------------------|-------------------------------------------------| -| `lock` |

composer.lock or composer.json file hash

| -| `php-version` |

Installed PHP version

| -| `composer-version` |

Installed Composer version

| -| `cache-hit` |

Indicates if there was a cache hit

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/php-composer@main - with: - php: - # PHP Version to use. - # - # Required: true - # Default: 8.4 - - extensions: - # Comma-separated list of PHP extensions to install - # - # Required: false - # Default: mbstring, xml, zip, curl, json - - tools: - # Comma-separated list of Composer tools to install - # - # Required: false - # Default: composer:v2 - - args: - # Arguments to pass to Composer. - # - # Required: false - # Default: --no-progress --prefer-dist --optimize-autoloader - - composer-version: - # Composer version to use (1 or 2) - # - # Required: false - # Default: 2 - - stability: - # Minimum stability (stable, RC, beta, alpha, dev) - # - # Required: false - # Default: stable - - cache-directories: - # Additional directories to cache (comma-separated) - # - # Required: false - # Default: "" - - token: - # GitHub token for private repository access - # - # Required: false - # Default: "" - - max-retries: - # Maximum number of retry attempts for Composer commands - # - # Required: false - # Default: 3 -``` diff --git a/php-composer/action.yml b/php-composer/action.yml deleted file mode 100644 index 1854793..0000000 --- a/php-composer/action.yml +++ /dev/null @@ -1,228 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for checking out repository ---- -name: Run Composer Install -description: 'Runs Composer install on a repository with advanced caching and configuration.' -author: 'Ismo Vuorinen' - -branding: - icon: server - color: gray-dark - -inputs: - php: - description: 'PHP Version to use.' - required: true - default: '8.4' - extensions: - description: 'Comma-separated list of PHP extensions to install' - required: false - default: 'mbstring, xml, zip, curl, json' - tools: - description: 'Comma-separated list of Composer tools to install' - required: false - default: 'composer:v2' - args: - description: 'Arguments to pass to Composer.' - required: false - default: '--no-progress --prefer-dist --optimize-autoloader' - composer-version: - description: 'Composer version to use (1 or 2)' - required: false - default: '2' - stability: - description: 'Minimum stability (stable, RC, beta, alpha, dev)' - required: false - default: 'stable' - cache-directories: - description: 'Additional directories to cache (comma-separated)' - required: false - default: '' - token: - description: 'GitHub token for private repository access' - required: false - default: '' - max-retries: - description: 'Maximum number of retry attempts for Composer commands' - required: false - default: '3' - -outputs: - lock: - description: 'composer.lock or composer.json file hash' - value: ${{ steps.hash.outputs.lock }} - php-version: - description: 'Installed PHP version' - value: ${{ steps.php.outputs.version }} - composer-version: - description: 'Installed Composer version' - value: ${{ steps.composer.outputs.version }} - cache-hit: - description: 'Indicates if there was a cache hit' - value: ${{ steps.composer-cache.outputs.cache-hit }} - -runs: - using: composite - steps: - - name: Mask Secrets - shell: bash - env: - GITHUB_TOKEN: ${{ inputs.token || github.token }} - run: | - echo "::add-mask::$GITHUB_TOKEN" - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token || github.token }} - - - name: Validate Inputs - id: validate - uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - action-type: php-composer - - - name: Setup PHP - id: php - uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 - with: - php-version: ${{ inputs.php }} - extensions: ${{ inputs.extensions }} - tools: ${{ inputs.tools }} - coverage: none - ini-values: memory_limit=1G, max_execution_time=600 - fail-fast: true - - - name: Get Dependency Hashes - id: hash - shell: bash - env: - CACHE_DIRECTORIES: ${{ inputs.cache-directories }} - COMPOSER_LOCK_HASH: ${{ hashFiles('**/composer.lock') }} - COMPOSER_JSON_HASH: ${{ hashFiles('**/composer.json') }} - run: | - set -euo pipefail - - # Function to calculate directory hash - calculate_dir_hash() { - local dir=$1 - if [ -d "$dir" ]; then - find "$dir" -type f -exec sha256sum {} \; | sort | sha256sum | cut -d' ' -f1 - fi - } - - # Get composer.lock hash or composer.json hash - if [ -f composer.lock ]; then - echo "lock=$COMPOSER_LOCK_HASH" >> $GITHUB_OUTPUT - else - echo "lock=$COMPOSER_JSON_HASH" >> $GITHUB_OUTPUT - fi - - # Calculate additional directory hashes - if [ -n "$CACHE_DIRECTORIES" ]; then - IFS=',' read -ra DIRS <<< "$CACHE_DIRECTORIES" - for dir in "${DIRS[@]}"; do - dir_hash=$(calculate_dir_hash "$dir") - if [ -n "$dir_hash" ]; then - echo "${dir}_hash=$dir_hash" >> $GITHUB_OUTPUT - fi - done - fi - - - name: Configure Composer - id: composer - shell: bash - env: - GITHUB_TOKEN: ${{ inputs.token || github.token }} - STABILITY: ${{ inputs.stability }} - COMPOSER_VERSION: ${{ inputs.composer-version }} - run: | - set -euo pipefail - - # Configure Composer environment - composer config --global process-timeout 600 - composer config --global allow-plugins true - composer config --global github-oauth.github.com "$GITHUB_TOKEN" - - if [ "$STABILITY" != "stable" ]; then - composer config minimum-stability "$STABILITY" - fi - - # Verify Composer installation - composer_full_version=$(composer --version | grep -oP 'Composer version \K[0-9]+\.[0-9]+\.[0-9]+') - if [ -z "$composer_full_version" ]; then - echo "::error::Failed to detect Composer version" - exit 1 - fi - - # Extract major version for comparison - composer_major_version=${composer_full_version%%.*} - expected_version="$COMPOSER_VERSION" - - echo "Detected Composer version: $composer_full_version (major: $composer_major_version)" - - if [ "$composer_major_version" != "$expected_version" ]; then - echo "::error::Composer major version mismatch. Expected $expected_version.x, got $composer_full_version" - exit 1 - fi - - # Store full version for output - echo "version=$composer_full_version" >> "$GITHUB_OUTPUT" - - # Log Composer configuration - echo "Composer Configuration:" - composer config --list - - - name: Cache Composer packages - id: composer-cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'composer' - paths: vendor,~/.composer/cache${{ inputs.cache-directories != "" && format(",{0}", inputs.cache-directories) || "" }} - key-prefix: 'php-${{ inputs.php }}-composer-${{ inputs.composer-version }}' - key-files: 'composer.lock,composer.json' - restore-keys: | - ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ inputs.composer-version }}- - ${{ runner.os }}-php-${{ inputs.php }}-composer- - ${{ runner.os }}-php-${{ inputs.php }}- - - - name: Clear Composer Cache Before Final Attempt - if: steps.composer-cache.outputs.cache-hit != 'true' - shell: bash - run: | - set -euo pipefail - echo "Clearing Composer cache to ensure clean installation..." - composer clear-cache - - - name: Install Dependencies - uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 - with: - timeout_minutes: 10 - max_attempts: ${{ inputs.max-retries }} - retry_wait_seconds: 30 - command: composer install ${{ inputs.args }} - - - name: Verify Installation - shell: bash - run: | - set -euo pipefail - - # Verify vendor directory - if [ ! -d "vendor" ]; then - echo "::error::vendor directory not found" - exit 1 - fi - - # Verify autoloader - if [ ! -f "vendor/autoload.php" ]; then - echo "::error::autoload.php not found" - exit 1 - fi - - - name: Generate Optimized Autoloader - if: success() - shell: bash - run: |- - set -euo pipefail - composer dump-autoload --optimize --classmap-authoritative diff --git a/php-composer/rules.yml b/php-composer/rules.yml deleted file mode 100644 index c04f2cc..0000000 --- a/php-composer/rules.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -# Validation rules for php-composer action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 56% (5/9 inputs) -# -# This file defines validation rules for the php-composer GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: php-composer -description: Runs Composer install on a repository with advanced caching and configuration. -generator_version: 1.0.0 -required_inputs: - - php -optional_inputs: - - args - - cache-directories - - composer-version - - extensions - - max-retries - - stability - - token - - tools -conventions: - cache-directories: boolean - composer-version: semantic_version - max-retries: numeric_range_1_10 - php: semantic_version - token: github_token -overrides: {} -statistics: - total_inputs: 9 - validated_inputs: 5 - skipped_inputs: 0 - coverage_percentage: 56 -validation_coverage: 56 -auto_detected: true -manual_review_required: true -quality_indicators: - has_required_inputs: true - has_token_validation: true - has_version_validation: true - has_file_validation: false - has_security_validation: true diff --git a/php-laravel-phpunit/CustomValidator.py b/php-laravel-phpunit/CustomValidator.py deleted file mode 100755 index f198de5..0000000 --- a/php-laravel-phpunit/CustomValidator.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for php-laravel-phpunit action.""" - -from __future__ import annotations - -from pathlib import Path -import sys - -# Add validate-inputs directory to path to import validators -validate_inputs_path = Path(__file__).parent.parent / "validate-inputs" -sys.path.insert(0, str(validate_inputs_path)) - -from validators.base import BaseValidator -from validators.file import FileValidator -from validators.token import TokenValidator -from validators.version import VersionValidator - - -class CustomValidator(BaseValidator): - """Custom validator for php-laravel-phpunit action.""" - - def __init__(self, action_type: str = "php-laravel-phpunit") -> None: - """Initialize php-laravel-phpunit validator.""" - super().__init__(action_type) - self.version_validator = VersionValidator() - self.file_validator = FileValidator() - self.token_validator = TokenValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate php-laravel-phpunit action inputs.""" - valid = True - - # Validate php-version if provided and not empty - if inputs.get("php-version"): - value = inputs["php-version"] - # Special case: "latest" is allowed - if value != "latest": - result = self.version_validator.validate_php_version(value, "php-version") - - # Propagate errors from the version validator - for error in self.version_validator.errors: - if error not in self.errors: - self.add_error(error) - - self.version_validator.clear_errors() - - if not result: - valid = False - # Validate php-version-file if provided - if inputs.get("php-version-file"): - result = self.file_validator.validate_file_path( - inputs["php-version-file"], "php-version-file" - ) - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False - - # Validate extensions if provided - if inputs.get("extensions"): - value = inputs["extensions"] - # Basic validation for PHP extensions list - if ";" in value and not value.startswith("${{"): - self.add_error(f"Invalid extensions format in extensions: {value}") - valid = False - # Check for dangerous characters and invalid format (@ is not valid in PHP extensions) - if any(char in value for char in ["`", "$", "&", "|", ">", "<", "@", "\n", "\r"]): - self.add_error(f"Invalid characters in extensions: {value}") - valid = False - - # Validate coverage if provided - if inputs.get("coverage"): - value = inputs["coverage"] - # Valid coverage drivers for PHPUnit - valid_coverage = ["none", "xdebug", "xdebug3", "pcov"] - if value not in valid_coverage: - # Check for command injection attempts - if any(char in value for char in [";", "`", "$", "&", "|", ">", "<", "\n", "\r"]): - self.add_error(f"Command injection attempt in coverage: {value}") - valid = False - elif value and not value.startswith("${{"): - self.add_error( - f"Invalid coverage driver: {value}. " - f"Must be one of: {', '.join(valid_coverage)}" - ) - valid = False - - # Validate token if provided - if inputs.get("token"): - result = self.token_validator.validate_github_token(inputs["token"]) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - if not result: - valid = False - - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs.""" - return [] - - def get_validation_rules(self) -> dict: - """Get validation rules.""" - return { - "php-version": { - "type": "php_version", - "required": False, - "description": "PHP version to use", - }, - "php-version-file": { - "type": "file", - "required": False, - "description": "PHP version file", - }, - "extensions": { - "type": "string", - "required": False, - "description": "PHP extensions to install", - }, - "coverage": { - "type": "string", - "required": False, - "description": "Coverage driver", - }, - "token": { - "type": "token", - "required": False, - "description": "GitHub token", - }, - } diff --git a/php-laravel-phpunit/README.md b/php-laravel-phpunit/README.md deleted file mode 100644 index c21d2c7..0000000 --- a/php-laravel-phpunit/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# ivuorinen/actions/php-laravel-phpunit - -## Laravel Setup and Composer test - -### Description - -Setup PHP, install dependencies, generate key, create database and run composer test - -### Inputs - -| name | description | required | default | -|--------------------|-----------------------------------------------------------------------------------------------------------------------|----------|---------------------------------------------| -| `php-version` |

PHP Version to use, see https://github.com/marketplace/actions/setup-php-action#php-version-optional

| `false` | `latest` | -| `php-version-file` |

PHP Version file to use, see https://github.com/marketplace/actions/setup-php-action#php-version-file-optional

| `false` | `.php-version` | -| `extensions` |

PHP extensions to install, see https://github.com/marketplace/actions/setup-php-action#extensions-optional

| `false` | `mbstring, intl, json, pdo_sqlite, sqlite3` | -| `coverage` |

Specify code-coverage driver, see https://github.com/marketplace/actions/setup-php-action#coverage-optional

| `false` | `none` | -| `token` |

GitHub token for authentication

| `false` | `""` | - -### Outputs - -| name | description | -|--------------------|------------------------------------------------| -| `php-version` |

The PHP version that was setup

| -| `php-version-file` |

The PHP version file that was used

| -| `extensions` |

The PHP extensions that were installed

| -| `coverage` |

The code-coverage driver that was setup

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/php-laravel-phpunit@main - with: - php-version: - # PHP Version to use, see https://github.com/marketplace/actions/setup-php-action#php-version-optional - # - # Required: false - # Default: latest - - php-version-file: - # PHP Version file to use, see https://github.com/marketplace/actions/setup-php-action#php-version-file-optional - # - # Required: false - # Default: .php-version - - extensions: - # PHP extensions to install, see https://github.com/marketplace/actions/setup-php-action#extensions-optional - # - # Required: false - # Default: mbstring, intl, json, pdo_sqlite, sqlite3 - - coverage: - # Specify code-coverage driver, see https://github.com/marketplace/actions/setup-php-action#coverage-optional - # - # Required: false - # Default: none - - token: - # GitHub token for authentication - # - # Required: false - # Default: "" -``` diff --git a/php-laravel-phpunit/action.yml b/php-laravel-phpunit/action.yml deleted file mode 100644 index 724f51d..0000000 --- a/php-laravel-phpunit/action.yml +++ /dev/null @@ -1,135 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for checking out repository ---- -name: Laravel Setup and Composer test -description: 'Setup PHP, install dependencies, generate key, create database and run composer test' -author: 'Ismo Vuorinen' - -branding: - icon: 'terminal' - color: 'blue' - -inputs: - php-version: - description: 'PHP Version to use, see https://github.com/marketplace/actions/setup-php-action#php-version-optional' - required: false - default: 'latest' - php-version-file: - description: 'PHP Version file to use, see https://github.com/marketplace/actions/setup-php-action#php-version-file-optional' - required: false - default: '.php-version' - extensions: - description: 'PHP extensions to install, see https://github.com/marketplace/actions/setup-php-action#extensions-optional' - required: false - default: 'mbstring, intl, json, pdo_sqlite, sqlite3' - coverage: - description: 'Specify code-coverage driver, see https://github.com/marketplace/actions/setup-php-action#coverage-optional' - required: false - default: 'none' - token: - description: 'GitHub token for authentication' - required: false - default: '' - -outputs: - php-version: - description: 'The PHP version that was setup' - value: ${{ steps.setup-php.outputs.php-version }} - php-version-file: - description: 'The PHP version file that was used' - value: ${{ steps.setup-php.outputs.php-version-file }} - extensions: - description: 'The PHP extensions that were installed' - value: ${{ steps.setup-php.outputs.extensions }} - coverage: - description: 'The code-coverage driver that was setup' - value: ${{ steps.setup-php.outputs.coverage }} - -runs: - using: composite - steps: - - name: Mask Secrets - shell: bash - env: - GITHUB_TOKEN: ${{ inputs.token }} - run: | - if [ -n "$GITHUB_TOKEN" ]; then - echo "::add-mask::$GITHUB_TOKEN" - fi - - - name: Detect PHP Version - id: php-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'php' - default-version: ${{ inputs.php-version }} - - - uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 - id: setup-php - with: - php-version: ${{ steps.php-version.outputs.detected-version }} - extensions: ${{ inputs.extensions }} - coverage: ${{ inputs.coverage }} - - - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token != '' && inputs.token || github.token }} - - - name: 'Check file existence' - id: check_files - uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 - with: - files: 'package.json, artisan' - - - name: Copy .env - if: steps.check_files.outputs.files_exists == 'true' - shell: bash - run: | - set -euo pipefail - - php -r "file_exists('.env') || copy('.env.example', '.env');" - - - name: Install Dependencies - if: steps.check_files.outputs.files_exists == 'true' - shell: bash - run: | - set -euo pipefail - - composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - - - name: Generate key - if: steps.check_files.outputs.files_exists == 'true' - shell: bash - run: | - set -euo pipefail - - php artisan key:generate - - - name: Directory Permissions - if: steps.check_files.outputs.files_exists == 'true' - shell: bash - run: | - set -euo pipefail - - chmod -R 777 storage bootstrap/cache - - - name: Create Database - if: steps.check_files.outputs.files_exists == 'true' - shell: bash - run: | - set -euo pipefail - - mkdir -p database - touch database/database.sqlite - - - name: Execute composer test (Unit and Feature tests) - if: steps.check_files.outputs.files_exists == 'true' - shell: bash - env: - DB_CONNECTION: sqlite - DB_DATABASE: database/database.sqlite - run: |- - set -euo pipefail - - composer test diff --git a/php-laravel-phpunit/rules.yml b/php-laravel-phpunit/rules.yml deleted file mode 100644 index d3576b5..0000000 --- a/php-laravel-phpunit/rules.yml +++ /dev/null @@ -1,43 +0,0 @@ ---- -# Validation rules for php-laravel-phpunit action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (5/5 inputs) -# -# This file defines validation rules for the php-laravel-phpunit GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: php-laravel-phpunit -description: Setup PHP, install dependencies, generate key, create database and run composer test -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - coverage - - extensions - - php-version - - php-version-file - - token -conventions: - coverage: coverage_driver - extensions: php_extensions - php-version: semantic_version - php-version-file: file_path - token: github_token -overrides: {} -statistics: - total_inputs: 5 - validated_inputs: 5 - skipped_inputs: 0 - coverage_percentage: 100 -validation_coverage: 100 -auto_detected: true -manual_review_required: false -quality_indicators: - has_required_inputs: false - has_token_validation: true - has_version_validation: true - has_file_validation: true - has_security_validation: true diff --git a/php-tests/README.md b/php-tests/README.md index 992255f..08d64f2 100644 --- a/php-tests/README.md +++ b/php-tests/README.md @@ -4,24 +4,33 @@ ### Description -Run PHPUnit tests on the repository +Run PHPUnit tests with optional Laravel setup and Composer dependency management ### Inputs -| name | description | required | default | -|------------|----------------------------------------|----------|-----------------------------| -| `token` |

GitHub token for authentication

| `false` | `""` | -| `username` |

GitHub username for commits

| `false` | `github-actions` | -| `email` |

GitHub email for commits

| `false` | `github-actions@github.com` | +| name | description | required | default | +|-----------------|----------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------------| +| `framework` |

Framework detection mode (auto=detect Laravel via artisan, laravel=force Laravel, generic=no framework)

| `false` | `auto` | +| `php-version` |

PHP Version to use (latest, 8.4, 8.3, etc.)

| `false` | `latest` | +| `extensions` |

PHP extensions to install (comma-separated)

| `false` | `mbstring, intl, json, pdo_sqlite, sqlite3` | +| `coverage` |

Code-coverage driver (none, xdebug, pcov)

| `false` | `none` | +| `composer-args` |

Arguments to pass to Composer install

| `false` | `--no-progress --prefer-dist --optimize-autoloader` | +| `max-retries` |

Maximum number of retry attempts for Composer commands

| `false` | `3` | +| `token` |

GitHub token for authentication

| `false` | `""` | +| `username` |

GitHub username for commits

| `false` | `github-actions` | +| `email` |

GitHub email for commits

| `false` | `github-actions@github.com` | ### Outputs -| name | description | -|-----------------|--------------------------------------------------------| -| `test_status` |

Test execution status (success/failure/skipped)

| -| `tests_run` |

Number of tests executed

| -| `tests_passed` |

Number of tests passed

| -| `coverage_path` |

Path to coverage report

| +| name | description | +|--------------------|------------------------------------------------| +| `framework` |

Detected framework (laravel or generic)

| +| `php-version` |

The PHP version that was setup

| +| `composer-version` |

Installed Composer version

| +| `cache-hit` |

Indicates if there was a cache hit

| +| `test-status` |

Test execution status (success/failure)

| +| `tests-run` |

Number of tests executed

| +| `tests-passed` |

Number of tests passed

| ### Runs @@ -32,6 +41,42 @@ This action is a `composite` action. ```yaml - uses: ivuorinen/actions/php-tests@main with: + framework: + # Framework detection mode (auto=detect Laravel via artisan, laravel=force Laravel, generic=no framework) + # + # Required: false + # Default: auto + + php-version: + # PHP Version to use (latest, 8.4, 8.3, etc.) + # + # Required: false + # Default: latest + + extensions: + # PHP extensions to install (comma-separated) + # + # Required: false + # Default: mbstring, intl, json, pdo_sqlite, sqlite3 + + coverage: + # Code-coverage driver (none, xdebug, pcov) + # + # Required: false + # Default: none + + composer-args: + # Arguments to pass to Composer install + # + # Required: false + # Default: --no-progress --prefer-dist --optimize-autoloader + + max-retries: + # Maximum number of retry attempts for Composer commands + # + # Required: false + # Default: 3 + token: # GitHub token for authentication # diff --git a/php-tests/action.yml b/php-tests/action.yml index 1fdeb40..bc9d03e 100644 --- a/php-tests/action.yml +++ b/php-tests/action.yml @@ -3,7 +3,7 @@ # - contents: read # Required for checking out repository --- name: PHP Tests -description: Run PHPUnit tests on the repository +description: Run PHPUnit tests with optional Laravel setup and Composer dependency management author: Ismo Vuorinen branding: @@ -11,6 +11,30 @@ branding: color: green inputs: + framework: + description: 'Framework detection mode (auto=detect Laravel via artisan, laravel=force Laravel, generic=no framework)' + required: false + default: 'auto' + php-version: + description: 'PHP Version to use (latest, 8.4, 8.3, etc.)' + required: false + default: 'latest' + extensions: + description: 'PHP extensions to install (comma-separated)' + required: false + default: 'mbstring, intl, json, pdo_sqlite, sqlite3' + coverage: + description: 'Code-coverage driver (none, xdebug, pcov)' + required: false + default: 'none' + composer-args: + description: 'Arguments to pass to Composer install' + required: false + default: '--no-progress --prefer-dist --optimize-autoloader' + max-retries: + description: 'Maximum number of retry attempts for Composer commands' + required: false + default: '3' token: description: 'GitHub token for authentication' required: false @@ -25,56 +49,123 @@ inputs: default: 'github-actions@github.com' outputs: - test_status: - description: 'Test execution status (success/failure/skipped)' + framework: + description: 'Detected framework (laravel or generic)' + value: ${{ steps.detect-framework.outputs.framework }} + php-version: + description: 'The PHP version that was setup' + value: ${{ steps.setup-php.outputs.php-version }} + composer-version: + description: 'Installed Composer version' + value: ${{ steps.composer-config.outputs.version }} + cache-hit: + description: 'Indicates if there was a cache hit' + value: ${{ steps.composer-cache.outputs.cache-hit }} + test-status: + description: 'Test execution status (success/failure)' value: ${{ steps.test.outputs.status }} - tests_run: + tests-run: description: 'Number of tests executed' value: ${{ steps.test.outputs.tests_run }} - tests_passed: + tests-passed: description: 'Number of tests passed' value: ${{ steps.test.outputs.tests_passed }} - coverage_path: - description: 'Path to coverage report' - value: 'coverage.xml' runs: using: composite steps: - - name: Validate Inputs - id: validate - shell: bash + - name: Mask Secrets + shell: sh env: GITHUB_TOKEN: ${{ inputs.token }} + run: | + set -eu + if [ -n "$GITHUB_TOKEN" ]; then + echo "::add-mask::$GITHUB_TOKEN" + fi + + - name: Validate Inputs + id: validate + shell: sh + env: + FRAMEWORK: ${{ inputs.framework }} + PHP_VERSION: ${{ inputs.php-version }} + COVERAGE: ${{ inputs.coverage }} + MAX_RETRIES: ${{ inputs.max-retries }} EMAIL: ${{ inputs.email }} USERNAME: ${{ inputs.username }} run: | - set -euo pipefail + set -eu - # Validate GitHub token format (basic validation) - if [[ -n "$GITHUB_TOKEN" ]]; then - # Skip validation for GitHub expressions (they'll be resolved at runtime) - if ! [[ "$GITHUB_TOKEN" =~ ^gh[efpousr]_[a-zA-Z0-9]{36}$ ]] && ! [[ "$GITHUB_TOKEN" =~ ^\$\{\{ ]]; then - echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters" - fi + # Validate framework mode + case "$FRAMEWORK" in + auto|laravel|generic) + echo "Framework mode: $FRAMEWORK" + ;; + *) + echo "::error::Invalid framework: '$FRAMEWORK'. Must be 'auto', 'laravel', or 'generic'" + exit 1 + ;; + esac + + # Validate PHP version format + if [ "$PHP_VERSION" != "latest" ]; then + case "$PHP_VERSION" in + [0-9]*\.[0-9]*\.[0-9]*) + # X.Y.Z format (e.g., 8.3.0) + ;; + [0-9]*\.[0-9]*) + # X.Y format (e.g., 8.4) + ;; + *) + echo "::error::Invalid php-version format: '$PHP_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 8.4, 8.3.0)" + exit 1 + ;; + esac fi - # Validate email format (basic check) - if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" + # Validate coverage driver + case "$COVERAGE" in + none|xdebug|pcov) + ;; + *) + echo "::error::Invalid coverage driver: '$COVERAGE'. Must be 'none', 'xdebug', or 'pcov'" + exit 1 + ;; + esac + + # Validate max retries (must be digits only) + case "$MAX_RETRIES" in + *[!0-9]*) + echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10" + exit 1 + ;; + esac + # Validate max retries range + if [ "$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 username format (prevent command injection) - if [[ "$USERNAME" == *";"* ]] || [[ "$USERNAME" == *"&&"* ]] || [[ "$USERNAME" == *"|"* ]]; then - echo "::error::Invalid username: '$USERNAME'. Command injection patterns not allowed" - exit 1 - fi + # Validate email format (must contain @ and .) + case "$EMAIL" in + *@*.*) ;; + *) + echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" + exit 1 + ;; + esac - # Validate username length - username="$USERNAME" - if [ ${#username} -gt 39 ]; then - echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters" + # Validate username format (reject command injection patterns) + case "$USERNAME" in + *";"*|*"&&"*|*"|"*) + echo "::error::Invalid username: '$USERNAME'. Command injection patterns not allowed" + exit 1 + ;; + esac + + if [ ${#USERNAME} -gt 39 ]; then + echo "::error::Username too long: ${#USERNAME} characters. GitHub usernames are max 39 characters" exit 1 fi @@ -85,37 +176,328 @@ runs: with: token: ${{ inputs.token || github.token }} - - name: Composer Install - uses: ivuorinen/actions/php-composer@0fa9a68f07a1260b321f814202658a6089a43d42 + - name: Detect Framework + id: detect-framework + shell: sh + env: + FRAMEWORK_MODE: ${{ inputs.framework }} + run: | + set -eu + + framework="generic" + + if [ "$FRAMEWORK_MODE" = "laravel" ]; then + framework="laravel" + echo "Framework mode forced to Laravel" + elif [ "$FRAMEWORK_MODE" = "auto" ]; then + if [ -f "artisan" ]; then + framework="laravel" + echo "Detected Laravel framework (artisan file found)" + else + echo "No Laravel framework detected (no artisan file)" + fi + else + echo "Framework mode set to generic" + fi + + printf 'framework=%s\n' "$framework" >> "$GITHUB_OUTPUT" + + - name: Detect PHP Version + id: detect-php-version + shell: sh + env: + DEFAULT_VERSION: ${{ inputs.php-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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for php..." >&2 + version=$(awk '/^php[[: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 PHP 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 php..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "php:" | head -1 | \ + sed -n -E "s/.*php:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found PHP 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 php..." >&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/.*php:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found PHP version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping devcontainer.json parsing" >&2 + fi + fi + + # Parse .php-version file + if [ -z "$detected_version" ] && [ -f .php-version ]; then + echo "Checking .php-version..." >&2 + version=$(tr -d '\r' < .php-version | head -1) + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found PHP version in .php-version: $version" >&2 + detected_version="$version" + fi + fi + fi + + # Parse composer.json + if [ -z "$detected_version" ] && [ -f composer.json ]; then + echo "Checking composer.json..." >&2 + if 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" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found PHP version in composer.json: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping composer.json parsing" >&2 + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default PHP version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected PHP version: $detected_version" >&2 + + - name: Setup PHP + id: setup-php + uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 + with: + php-version: ${{ steps.detect-php-version.outputs.detected-version }} + extensions: ${{ inputs.extensions }} + coverage: ${{ inputs.coverage }} + ini-values: memory_limit=1G, max_execution_time=600 + fail-fast: true + + - name: Configure Composer + id: composer-config + shell: sh + env: + GITHUB_TOKEN: ${{ inputs.token || github.token }} + run: | + set -eu + + # Configure Composer environment + composer config --global process-timeout 600 + composer config --global allow-plugins true + composer config --global github-oauth.github.com "$GITHUB_TOKEN" + + # Verify Composer installation + composer_full_version=$(composer --version | sed -n 's/.*Composer version \([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' || echo "") + if [ -z "$composer_full_version" ]; then + echo "::error::Failed to detect Composer version" + exit 1 + fi + + echo "Detected Composer version: $composer_full_version" + printf 'version=%s\n' "$composer_full_version" >> "$GITHUB_OUTPUT" + + # Log Composer configuration + echo "Composer Configuration:" + composer config --list + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + vendor + ~/.composer/cache + key: ${{ runner.os }}-php-${{ steps.setup-php.outputs.php-version }}-composer-${{ hashFiles('composer.lock', 'composer.json') }} + restore-keys: | + ${{ runner.os }}-php-${{ steps.setup-php.outputs.php-version }}-composer- + ${{ runner.os }}-php-${{ steps.setup-php.outputs.php-version }}- + ${{ runner.os }}-php- + + - name: Clear Composer Cache Before Install + if: steps.composer-cache.outputs.cache-hit != 'true' + shell: sh + run: | + set -eu + echo "Clearing Composer cache to ensure clean installation..." + composer clear-cache + + - name: Install Composer Dependencies + uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 + with: + timeout_minutes: 10 + max_attempts: ${{ inputs.max-retries }} + retry_wait_seconds: 30 + command: composer install ${{ inputs.composer-args }} + + - name: Verify Composer Installation + shell: sh + run: | + set -eu + + # Verify vendor directory + if [ ! -d "vendor" ]; then + echo "::error::vendor directory not found" + exit 1 + fi + + # Verify autoloader + if [ ! -f "vendor/autoload.php" ]; then + echo "::error::autoload.php not found" + exit 1 + fi + + echo "✅ Composer installation verified" + + - name: Laravel Setup - Copy .env + if: steps.detect-framework.outputs.framework == 'laravel' + shell: sh + run: | + set -eu + php -r "file_exists('.env') || copy('.env.example', '.env');" + echo "✅ Laravel .env file configured" + + - name: Laravel Setup - Generate Key + if: steps.detect-framework.outputs.framework == 'laravel' + shell: sh + run: | + set -eu + php artisan key:generate + echo "✅ Laravel application key generated" + + - name: Laravel Setup - Directory Permissions + if: steps.detect-framework.outputs.framework == 'laravel' + shell: sh + run: | + set -eu + chmod -R 777 storage bootstrap/cache + echo "✅ Laravel directory permissions configured" + + - name: Laravel Setup - Create Database + if: steps.detect-framework.outputs.framework == 'laravel' + shell: sh + run: | + set -eu + mkdir -p database + touch database/database.sqlite + echo "✅ Laravel SQLite database created" - name: Run PHPUnit Tests id: test - shell: bash - run: |- - set -euo pipefail + shell: sh + env: + IS_LARAVEL: ${{ steps.detect-framework.outputs.framework == 'laravel' }} + DB_CONNECTION: sqlite + DB_DATABASE: database/database.sqlite + run: | + set -eu + + echo "Running PHPUnit tests..." # Run PHPUnit and capture results phpunit_exit_code=0 - phpunit_output=$(vendor/bin/phpunit --verbose 2>&1) || phpunit_exit_code=$? + if [ "$IS_LARAVEL" = "true" ] && [ -f "composer.json" ] && grep -q '"test"' composer.json; then + echo "Running Laravel tests via composer test..." + phpunit_output=$(composer test 2>&1) || phpunit_exit_code=$? + elif [ -f "vendor/bin/phpunit" ]; then + echo "Running PHPUnit directly..." + phpunit_output=$(vendor/bin/phpunit --verbose 2>&1) || phpunit_exit_code=$? + else + echo "::error::PHPUnit not found. Ensure Composer dependencies are installed." + exit 1 + fi echo "$phpunit_output" - # Parse test results from output - tests_run=$(echo "$phpunit_output" | grep -E "Tests:|tests" | head -1 | grep -oE '[0-9]+' | head -1 || echo "0") - tests_passed=$(echo "$phpunit_output" | grep -oE 'OK.*[0-9]+ tests' | grep -oE '[0-9]+' || echo "0") + # Parse test results from output - handle various PHPUnit formats + tests_run="0" + tests_passed="0" + + # Pattern 1: "OK (N test(s), M assertions)" - success case (handles both singular and plural) + if echo "$phpunit_output" | grep -qE 'OK \([0-9]+ tests?,'; then + tests_run=$(echo "$phpunit_output" | grep -oE 'OK \([0-9]+ tests?,' | grep -oE '[0-9]+' | head -1) + tests_passed="$tests_run" + # Pattern 2: "Tests: N" line - failure/error/skipped case + elif echo "$phpunit_output" | grep -qE '^Tests:'; then + tests_run=$(echo "$phpunit_output" | grep -E '^Tests:' | grep -oE '[0-9]+' | head -1) + + # Calculate passed from failures and errors + failures=$(echo "$phpunit_output" | grep -oE 'Failures: [0-9]+' | grep -oE '[0-9]+' | head -1 || echo "0") + errors=$(echo "$phpunit_output" | grep -oE 'Errors: [0-9]+' | grep -oE '[0-9]+' | head -1 || echo "0") + tests_passed=$((tests_run - failures - errors)) + + # Ensure non-negative + if [ "$tests_passed" -lt 0 ]; then + tests_passed="0" + fi + fi # Determine status if [ $phpunit_exit_code -eq 0 ]; then status="success" + echo "✅ Tests passed: $tests_passed/$tests_run" else status="failure" + echo "❌ Tests failed" fi # Output results - echo "tests_run=$tests_run" >> $GITHUB_OUTPUT - echo "tests_passed=$tests_passed" >> $GITHUB_OUTPUT - echo "status=$status" >> $GITHUB_OUTPUT - echo "coverage_path=coverage.xml" >> $GITHUB_OUTPUT + printf 'tests_run=%s\n' "$tests_run" >> "$GITHUB_OUTPUT" + printf 'tests_passed=%s\n' "$tests_passed" >> "$GITHUB_OUTPUT" + printf 'status=%s\n' "$status" >> "$GITHUB_OUTPUT" # Exit with original code to maintain test failure behavior exit $phpunit_exit_code diff --git a/php-tests/rules.yml b/php-tests/rules.yml index 057d08a..e5b66ec 100644 --- a/php-tests/rules.yml +++ b/php-tests/rules.yml @@ -2,7 +2,7 @@ # Validation rules for php-tests action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 -# Coverage: 100% (3/3 inputs) +# Coverage: 78% (7/9 inputs) # # This file defines validation rules for the php-tests GitHub Action. # Rules are automatically applied by validate-inputs action when this @@ -11,29 +11,39 @@ schema_version: '1.0' action: php-tests -description: Run PHPUnit tests on the repository +description: Run PHPUnit tests with optional Laravel setup and Composer dependency management generator_version: 1.0.0 required_inputs: [] optional_inputs: + - composer-args + - coverage - email + - extensions + - framework + - max-retries + - php-version - token - username conventions: + coverage: coverage_driver email: email + framework: boolean + max-retries: numeric_range_1_10 + php-version: semantic_version token: github_token username: username overrides: {} statistics: - total_inputs: 3 - validated_inputs: 3 + total_inputs: 9 + validated_inputs: 7 skipped_inputs: 0 - coverage_percentage: 100 -validation_coverage: 100 + coverage_percentage: 78 +validation_coverage: 78 auto_detected: true -manual_review_required: false +manual_review_required: true quality_indicators: has_required_inputs: false has_token_validation: true - has_version_validation: false + has_version_validation: true has_file_validation: false has_security_validation: true diff --git a/pr-lint/action.yml b/pr-lint/action.yml index 6c71909..5ed54b0 100644 --- a/pr-lint/action.yml +++ b/pr-lint/action.yml @@ -76,26 +76,81 @@ runs: printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" fi - - name: Setup Node.js environment + - name: Detect Package Manager if: steps.detect-node.outputs.found == 'true' - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + id: detect-pm + shell: sh + run: | + set -eu + + # Detect package manager from lockfiles + if [ -f bun.lockb ]; then + package_manager="bun" + elif [ -f pnpm-lock.yaml ]; then + package_manager="pnpm" + elif [ -f yarn.lock ]; then + package_manager="yarn" + else + package_manager="npm" + fi + + printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT" + echo "Detected package manager: $package_manager" + + - name: Setup Node.js + if: steps.detect-node.outputs.found == 'true' + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: '22' + + - name: Enable Corepack + if: steps.detect-node.outputs.found == 'true' + shell: sh + run: | + set -eu + corepack enable + + - name: Install Package Manager + if: steps.detect-node.outputs.found == 'true' + shell: sh + env: + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} + run: | + set -eu + + case "$PACKAGE_MANAGER" in + pnpm) + corepack prepare pnpm@latest --activate + ;; + yarn) + corepack prepare yarn@stable --activate + ;; + bun|npm) + # Bun installed separately, npm built-in + ;; + esac + + - name: Setup Bun + if: steps.detect-node.outputs.found == 'true' && steps.detect-pm.outputs.package-manager == 'bun' + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: latest - name: Cache Node Dependencies if: steps.detect-node.outputs.found == 'true' id: node-cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'pr-lint-${{ steps.node-setup.outputs.package-manager }}' + path: node_modules + key: ${{ runner.os }}-pr-lint-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }} + restore-keys: | + ${{ runner.os }}-pr-lint-${{ steps.detect-pm.outputs.package-manager }}- - name: Install Node Dependencies if: steps.detect-node.outputs.found == 'true' && steps.node-cache.outputs.cache-hit != 'true' shell: sh env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} run: | set -eu @@ -136,9 +191,118 @@ runs: - name: Detect PHP Version if: steps.detect-php.outputs.found == 'true' id: php-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'php' + shell: sh + env: + DEFAULT_VERSION: '8.4' + 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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for php..." >&2 + version=$(awk '/^php[[: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 PHP 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 php..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "php:" | head -1 | \ + sed -n -E "s/.*php:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found PHP 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 php..." >&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/.*php:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found PHP version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping devcontainer.json parsing" >&2 + fi + fi + + # Parse .php-version file + if [ -z "$detected_version" ] && [ -f .php-version ]; then + echo "Checking .php-version..." >&2 + version=$(tr -d '\r' < .php-version | head -1) + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found PHP version in .php-version: $version" >&2 + detected_version="$version" + fi + fi + fi + + # Parse composer.json + if [ -z "$detected_version" ] && [ -f composer.json ]; then + echo "Checking composer.json..." >&2 + if 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" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found PHP version in composer.json: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping composer.json parsing" >&2 + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default PHP version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected PHP version: $detected_version" >&2 - name: Setup PHP if: steps.detect-php.outputs.found == 'true' @@ -182,9 +346,113 @@ runs: - name: Detect Python Version if: steps.detect-python.outputs.found == 'true' id: python-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'python' + shell: sh + env: + DEFAULT_VERSION: '3.11' + 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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for python..." >&2 + version=$(awk '/^python[[: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 Python 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 python..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "python:" | head -1 | \ + sed -n -E "s/.*python:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Python 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 python..." >&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/.*python:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Python version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping devcontainer.json parsing" >&2 + fi + fi + + # Parse .python-version file + if [ -z "$detected_version" ] && [ -f .python-version ]; then + echo "Checking .python-version..." >&2 + version=$(tr -d '\r' < .python-version | head -1) + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Python version in .python-version: $version" >&2 + detected_version="$version" + fi + fi + fi + + # Parse pyproject.toml + if [ -z "$detected_version" ] && [ -f pyproject.toml ]; then + echo "Checking pyproject.toml..." >&2 + 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" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Python version in pyproject.toml: $version" >&2 + detected_version="$version" + fi + fi + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default Python version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected Python version: $detected_version" >&2 - name: Setup Python if: steps.detect-python.outputs.found == 'true' @@ -215,9 +483,111 @@ runs: - name: Detect Go Version if: steps.detect-go.outputs.found == 'true' id: go-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'go' + shell: sh + env: + DEFAULT_VERSION: '1.24' + 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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for golang..." >&2 + version=$(awk '/^golang[[: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 Go 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 golang..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "golang:" | head -1 | \ + sed -n -E "s/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Go 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 golang..." >&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/.*golang:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Go version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping devcontainer.json parsing" >&2 + fi + fi + + # Parse .go-version file + if [ -z "$detected_version" ] && [ -f .go-version ]; then + echo "Checking .go-version..." >&2 + version=$(tr -d '\r' < .go-version | head -1) + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Go version in .go-version: $version" >&2 + detected_version="$version" + fi + fi + fi + + # Parse go.mod + if [ -z "$detected_version" ] && [ -f go.mod ]; then + echo "Checking go.mod..." >&2 + version=$(grep -E '^go[[:space:]]+[0-9]' go.mod | awk '{print $2}' | head -1 || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Go version in go.mod: $version" >&2 + detected_version="$version" + fi + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default Go version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected Go version: $detected_version" >&2 - name: Setup Go if: steps.detect-go.outputs.found == 'true' diff --git a/prettier-lint/action.yml b/prettier-lint/action.yml index 0d98485..2bab959 100644 --- a/prettier-lint/action.yml +++ b/prettier-lint/action.yml @@ -91,7 +91,7 @@ runs: steps: - name: Validate Inputs id: validate - shell: bash + shell: sh env: MODE: ${{ inputs.mode }} WORKING_DIRECTORY: ${{ inputs.working-directory }} @@ -107,7 +107,7 @@ runs: EMAIL: ${{ inputs.email }} USERNAME: ${{ inputs.username }} run: | - set -euo pipefail + set -eu # Validate mode case "$MODE" in @@ -127,38 +127,52 @@ runs: fi # Validate working directory path security - if [[ "$WORKING_DIRECTORY" == *".."* ]]; then - echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed" - exit 1 - fi + case "$WORKING_DIRECTORY" in + *..*) + echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed" + exit 1 + ;; + esac # Validate Prettier version format - if [[ -n "$PRETTIER_VERSION" ]] && [[ "$PRETTIER_VERSION" != "latest" ]]; then - if ! [[ "$PRETTIER_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?(-[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::Invalid prettier-version format: '$PRETTIER_VERSION'. Expected format: X.Y.Z or 'latest'" - exit 1 - fi + if [ -n "$PRETTIER_VERSION" ] && [ "$PRETTIER_VERSION" != "latest" ]; then + case "$PRETTIER_VERSION" in + [0-9]*.[0-9]*|[0-9]*.[0-9]*.[0-9]*|[0-9]*.[0-9]*.[0-9]*-*) + ;; + *) + echo "::error::Invalid prettier-version format: '$PRETTIER_VERSION'. Expected format: X.Y.Z or 'latest'" + exit 1 + ;; + esac fi # Validate config file path - if [[ "$CONFIG_FILE" != ".prettierrc" ]] && [[ "$CONFIG_FILE" == *".."* ]]; then - echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed" - exit 1 + if [ "$CONFIG_FILE" != ".prettierrc" ]; then + case "$CONFIG_FILE" in + *..*) + echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed" + exit 1 + ;; + esac fi # Validate ignore file path - if [[ "$IGNORE_FILE" != ".prettierignore" ]] && [[ "$IGNORE_FILE" == *".."* ]]; then - echo "::error::Invalid ignore file path: '$IGNORE_FILE'. Path traversal not allowed" - exit 1 + if [ "$IGNORE_FILE" != ".prettierignore" ]; then + case "$IGNORE_FILE" in + *..*) + echo "::error::Invalid ignore file path: '$IGNORE_FILE'. Path traversal not allowed" + exit 1 + ;; + esac fi # Validate boolean inputs validate_boolean() { - local value="$1" - local name="$2" + value="$1" + name="$2" - case "${value,,}" in - true|false) + case "$value" in + true|True|TRUE|false|False|FALSE) ;; *) echo "::error::Invalid boolean value for $name: '$value'. Expected: true or false" @@ -181,17 +195,27 @@ runs: esac # Validate max retries - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then + case "$MAX_RETRIES" in + ''|*[!0-9]*) + echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10" + exit 1 + ;; + esac + + if [ "$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 email and username for fix mode if [ "$MODE" = "fix" ]; then - if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" - exit 1 - fi + case "$EMAIL" in + *@*.*) ;; + *) + echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" + exit 1 + ;; + esac username="$USERNAME" @@ -200,20 +224,26 @@ runs: exit 1 fi - if ! [[ "$username" =~ ^[a-zA-Z0-9-]+$ ]]; then - echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed" - exit 1 - fi + case "$username" in + *[!a-zA-Z0-9-]*) + echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed" + exit 1 + ;; + esac - if [[ "$username" == -* ]] || [[ "$username" == *- ]]; then - echo "::error::Invalid username '$username'. Cannot start or end with hyphen" - exit 1 - fi + case "$username" in + -*|*-) + echo "::error::Invalid username '$username'. Cannot start or end with hyphen" + exit 1 + ;; + esac - if [[ "$username" == *--* ]]; then - echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" - exit 1 - fi + case "$username" in + *--*) + echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" + exit 1 + ;; + esac fi echo "Input validation completed successfully" @@ -223,26 +253,79 @@ runs: with: token: ${{ inputs.token || github.token }} - - name: Node Setup - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + - name: Detect Package Manager + id: detect-pm + shell: sh + run: | + set -eu + + # Detect package manager from lockfiles + if [ -f bun.lockb ]; then + package_manager="bun" + elif [ -f pnpm-lock.yaml ]; then + package_manager="pnpm" + elif [ -f yarn.lock ]; then + package_manager="yarn" + else + package_manager="npm" + fi + + printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT" + echo "Detected package manager: $package_manager" + + - name: Setup Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: '22' + + - name: Enable Corepack + shell: sh + run: | + set -eu + corepack enable + + - name: Install Package Manager + shell: sh + env: + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} + run: | + set -eu + + case "$PACKAGE_MANAGER" in + pnpm) + corepack prepare pnpm@latest --activate + ;; + yarn) + corepack prepare yarn@stable --activate + ;; + bun|npm) + # Bun installed separately, npm built-in + ;; + esac + + - name: Setup Bun + if: steps.detect-pm.outputs.package-manager == 'bun' + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: latest - name: Cache Node Dependencies id: cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'prettier-lint-${{ inputs.mode }}-${{ steps.node-setup.outputs.package-manager }}' + path: node_modules + key: ${{ runner.os }}-prettier-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }} + restore-keys: | + ${{ runner.os }}-prettier-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}- + ${{ runner.os }}-prettier-lint-${{ inputs.mode }}- - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' - shell: bash + shell: sh env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} run: | - set -euo pipefail + set -eu echo "Installing dependencies using $PACKAGE_MANAGER..." @@ -269,12 +352,12 @@ runs: - name: Install Prettier Plugins if: inputs.plugins != '' - shell: bash + shell: sh env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} PLUGINS: ${{ inputs.plugins }} run: | - set -euo pipefail + set -eu echo "Installing Prettier plugins: $PLUGINS" @@ -301,16 +384,16 @@ runs: - name: Run Prettier Check if: inputs.mode == 'check' id: check - shell: bash + shell: sh working-directory: ${{ inputs.working-directory }} env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} CONFIG_FILE: ${{ inputs.config-file }} CACHE: ${{ inputs.cache }} FAIL_ON_ERROR: ${{ inputs.fail-on-error }} FILE_PATTERN: ${{ inputs.file-pattern }} run: | - set -euo pipefail + set -eu echo "Running Prettier check mode..." @@ -358,13 +441,13 @@ runs: - name: Run Prettier Fix if: inputs.mode == 'fix' id: fix - shell: bash + shell: sh working-directory: ${{ inputs.working-directory }} env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }} FILE_PATTERN: ${{ inputs.file-pattern }} run: | - set -euo pipefail + set -eu echo "Running Prettier fix mode..." diff --git a/python-lint-fix/action.yml b/python-lint-fix/action.yml index 56f4b81..837480e 100644 --- a/python-lint-fix/action.yml +++ b/python-lint-fix/action.yml @@ -84,12 +84,146 @@ runs: - name: Detect Python Version id: python-version - uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'python' - default-version: ${{ inputs.python-version }} + shell: sh + env: + DEFAULT_VERSION: "${{ inputs.python-version || '3.11' }}" + run: | + set -eu - - name: Setup Python + # 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="" + + # Parse .tool-versions file + if [ -f .tool-versions ]; then + echo "Checking .tool-versions for python..." >&2 + version=$(awk '/^python[[: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 Python 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 python..." >&2 + version=$(grep -iF "FROM" Dockerfile | grep -F "python:" | head -1 | \ + sed -n -E "s/.*python:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Python 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 python..." >&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/.*python:([0-9]+(\.[0-9]+)*)(-[^:]*)?.*/\1/p" || echo "") + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Python version in devcontainer: $version" >&2 + detected_version="$version" + fi + fi + else + echo "jq not found; skipping devcontainer.json parsing" >&2 + fi + fi + + # Parse .python-version file + if [ -z "$detected_version" ] && [ -f .python-version ]; then + echo "Checking .python-version..." >&2 + version=$(tr -d '\r' < .python-version | head -1) + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Python version in .python-version: $version" >&2 + detected_version="$version" + fi + fi + fi + + # Parse pyproject.toml + if [ -z "$detected_version" ] && [ -f pyproject.toml ]; then + echo "Checking pyproject.toml..." >&2 + if grep -q '^\[project\]' pyproject.toml; then + version=$(grep -A 20 '^\[project\]' pyproject.toml | grep -E '^[[:space:]]*requires-python[[:space:]]*=' | sed -n -E 's/[^0-9]*([0-9]+\.[0-9]+(\.[0-9]+)?).*/\1/p' | head -1) + if [ -n "$version" ]; then + version=$(clean_version "$version") + if validate_version "$version"; then + echo "Found Python version in pyproject.toml: $version" >&2 + detected_version="$version" + fi + fi + fi + fi + + # Use default version if nothing detected + if [ -z "$detected_version" ]; then + detected_version="$DEFAULT_VERSION" + echo "Using default Python version: $detected_version" >&2 + fi + + # Set output + printf 'detected-version=%s\n' "$detected_version" >> "$GITHUB_OUTPUT" + echo "Final detected Python version: $detected_version" >&2 + + - name: Detect Package Manager + id: package-manager + shell: sh + run: | + set -eu + + # Detect Python package manager based on lock files and config + package_manager="pip" + + if [ -f "uv.lock" ]; then + # uv uses pip-compatible caching, so we use 'pip' as cache type + package_manager="pip" + echo "Detected uv (using pip-compatible caching)" >&2 + elif [ -f "poetry.lock" ]; then + package_manager="poetry" + echo "Detected Poetry" >&2 + elif [ -f "Pipfile.lock" ] || [ -f "Pipfile" ]; then + package_manager="pipenv" + echo "Detected Pipenv" >&2 + elif [ -f "requirements.txt" ] || [ -f "requirements-dev.txt" ] || [ -f "setup.py" ] || [ -f "pyproject.toml" ]; then + package_manager="pip" + echo "Detected pip" >&2 + else + package_manager="pip" + echo "No package manager detected, defaulting to pip" >&2 + fi + + printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT" + echo "Using package manager: $package_manager" >&2 + + - name: Setup Python (pip) + if: steps.package-manager.outputs.package-manager == 'pip' uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ steps.python-version.outputs.detected-version }} @@ -99,6 +233,27 @@ runs: **/requirements-dev.txt **/pyproject.toml **/setup.py + **/uv.lock + + - name: Setup Python (pipenv) + if: steps.package-manager.outputs.package-manager == 'pipenv' + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: ${{ steps.python-version.outputs.detected-version }} + cache: 'pipenv' + cache-dependency-path: | + **/Pipfile + **/Pipfile.lock + + - name: Setup Python (poetry) + if: steps.package-manager.outputs.package-manager == 'poetry' + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: ${{ steps.python-version.outputs.detected-version }} + cache: 'poetry' + cache-dependency-path: | + **/poetry.lock + **/pyproject.toml - name: Check for Python Files id: check-files @@ -116,18 +271,8 @@ runs: fi printf '%s\n' "result=found" >> "$GITHUB_OUTPUT" - - name: Cache Python Dependencies - if: steps.check-files.outputs.result == 'found' - id: cache-pip - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'pip' - paths: '~/.cache/pip' - key-files: 'requirements*.txt,pyproject.toml,setup.py,setup.cfg' - key-prefix: 'python-lint-fix' - - name: Install Dependencies - if: steps.check-files.outputs.result == 'found' && steps.cache-pip.outputs.cache-hit != 'true' + if: steps.check-files.outputs.result == 'found' id: install shell: sh env: @@ -150,22 +295,6 @@ runs: flake8 --version || exit 1 autopep8 --version || exit 1 - - name: Activate Virtual Environment (Cache Hit) - if: steps.check-files.outputs.result == 'found' && steps.cache-pip.outputs.cache-hit == 'true' - shell: sh - env: - FLAKE8_VERSION: ${{ inputs.flake8-version }} - AUTOPEP8_VERSION: ${{ inputs.autopep8-version }} - run: | - set -eu - - # Create virtual environment if it doesn't exist from cache - if [ ! -d ".venv" ]; then - python -m venv .venv - . .venv/bin/activate - pip install "flake8==$FLAKE8_VERSION" "flake8-sarif==0.6.0" "autopep8==$AUTOPEP8_VERSION" - fi - - name: Run flake8 if: steps.check-files.outputs.result == 'found' id: lint diff --git a/release-monthly/action.yml b/release-monthly/action.yml index 19a6c70..4cb2ebc 100644 --- a/release-monthly/action.yml +++ b/release-monthly/action.yml @@ -39,13 +39,13 @@ runs: using: 'composite' steps: - name: Validate Inputs - shell: bash + shell: sh env: INPUT_TOKEN: ${{ inputs.token }} INPUT_DRY_RUN: ${{ inputs.dry-run }} INPUT_PREFIX: ${{ inputs.prefix }} run: | - set -euo pipefail + set -eu # Validate token if [ -z "$INPUT_TOKEN" ]; then @@ -59,12 +59,15 @@ runs: exit 1 fi - # Validate prefix format if provided + # Validate prefix format if provided (alphanumeric, dots, underscores, hyphens) if [ -n "$INPUT_PREFIX" ]; then - if ! [[ "$INPUT_PREFIX" =~ ^[a-zA-Z0-9_.-]*$ ]]; then - echo "::error::Invalid prefix format. Only alphanumeric characters, dots, underscores, and hyphens are allowed" - exit 1 - fi + # Use case pattern matching for validation + case "$INPUT_PREFIX" in + *[!a-zA-Z0-9_.-]*) + echo "::error::Invalid prefix format. Only alphanumeric characters, dots, underscores, and hyphens are allowed" + exit 1 + ;; + esac fi # Write validated values to GITHUB_ENV for use in subsequent steps @@ -82,21 +85,51 @@ runs: - name: Create Release id: create-release - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Use validated environment variables from GITHUB_ENV GITHUB_TOKEN="$VALIDATED_TOKEN" PREFIX="$VALIDATED_PREFIX" DRY_RUN="$VALIDATED_DRY_RUN" - # Function to validate version format + # Function to validate version format (YYYY.M.N or YYYY.MM.N) validate_version() { - local version=$1 - if ! [[ $version =~ ^[0-9]{4}\.[0-9]{1,2}\.[0-9]+$ ]]; then - echo "::error::Invalid version format: $version" - return 1 - fi + version=$1 + # Check basic format with case statement + case "$version" in + [0-9][0-9][0-9][0-9].[0-9].[0-9]*|[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9]*) + # Extract parts for detailed validation + year="${version%%.*}" + rest="${version#*.}" + month="${rest%%.*}" + patch="${rest#*.}" + + # Validate year (4 digits) + if [ ${#year} -ne 4 ]; then + echo "::error::Invalid version format: $version" + return 1 + fi + + # Validate month (1 or 2 digits) + if [ ${#month} -gt 2 ] || [ ${#month} -lt 1 ]; then + echo "::error::Invalid version format: $version" + return 1 + fi + + # Validate patch is numeric + case "$patch" in + ''|*[!0-9]*) + echo "::error::Invalid version format: $version" + return 1 + ;; + esac + ;; + *) + echo "::error::Invalid version format: $version" + return 1 + ;; + esac } # Function to get previous release tag with error handling @@ -129,7 +162,7 @@ runs: # Determine next release tag next_major_minor="$(date +'%Y').$(date +'%-m')" - if [ -n "$previous_tag" ] && [[ "${previous_major}.${previous_minor}" == "${next_major_minor}" ]]; then + if [ -n "$previous_tag" ] && [ "${previous_major}.${previous_minor}" = "${next_major_minor}" ]; then echo "Month release already exists for year, incrementing patch number by 1" next_patch="$((previous_patch + 1))" else @@ -170,16 +203,16 @@ runs: - name: Verify Release if: inputs.dry-run == 'false' - shell: bash + shell: sh env: RELEASE_TAG: ${{ steps.create-release.outputs.release_tag }} run: |- - set -euo pipefail + set -eu # Use validated environment variables from GITHUB_ENV GITHUB_TOKEN="$VALIDATED_TOKEN" # Verify the release was created - if ! gh release view "$RELEASE_TAG" &>/dev/null; then + if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then echo "::error::Failed to verify release creation" exit 1 fi diff --git a/sync-labels/action.yml b/sync-labels/action.yml index b878657..0e29c78 100644 --- a/sync-labels/action.yml +++ b/sync-labels/action.yml @@ -30,37 +30,43 @@ runs: steps: - name: Validate Inputs id: validate - shell: bash + shell: sh env: LABELS_FILE: ${{ inputs.labels }} GITHUB_TOKEN: ${{ inputs.token }} run: | - set -euo pipefail + set -eu # Validate labels file path format - if [[ "$LABELS_FILE" == *".."* ]] || [[ "$LABELS_FILE" == "/"* ]]; then - echo "::error::Invalid labels file path: '$LABELS_FILE'. Path traversal not allowed" - exit 1 - fi + case "$LABELS_FILE" in + *".."*|"/"*) + echo "::error::Invalid labels file path: '$LABELS_FILE'. Path traversal not allowed" + exit 1 + ;; + esac # Validate labels file extension - if ! [[ "$LABELS_FILE" =~ \.(yml|yaml)$ ]]; then - echo "::error::Invalid labels file extension: '$LABELS_FILE'. Expected .yml or .yaml file" - exit 1 - fi + case "$LABELS_FILE" in + *.yml|*.yaml) + ;; + *) + echo "::error::Invalid labels file extension: '$LABELS_FILE'. Expected .yml or .yaml file" + exit 1 + ;; + esac # Validate token is provided (basic check) - if [[ -z "$GITHUB_TOKEN" ]]; then + if [ -z "$GITHUB_TOKEN" ]; then echo "::error::GitHub token is required for label synchronization" exit 1 fi - name: ⤵️ Download latest labels definitions - shell: bash + shell: sh env: LABELS_FILE: ${{ inputs.labels }} run: | - set -euo pipefail + set -eu curl -s --retry 5 \ "https://raw.githubusercontent.com/ivuorinen/actions/main/sync-labels/labels.yml" \ diff --git a/terraform-lint-fix/action.yml b/terraform-lint-fix/action.yml index 575f619..82d64ca 100644 --- a/terraform-lint-fix/action.yml +++ b/terraform-lint-fix/action.yml @@ -213,7 +213,7 @@ runs: error_count=$(grep -c "level\": \"error\"" "$tflint_output" || echo 0) printf '%s\n' "error_count=$error_count" >> "$GITHUB_OUTPUT" - if [[ "$FAIL_ON_ERROR" == "true" ]]; then + if [ "$FAIL_ON_ERROR" = "true" ]; then echo "::error::Found $error_count linting errors" exit 1 fi @@ -246,7 +246,7 @@ runs: printf '%s\n' "fixed_count=$fixed_count" >> "$GITHUB_OUTPUT" - name: Commit Fixes - if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }} + if: steps.check-files.outputs.found == 'true' && inputs.auto-fix == 'true' && fromJSON(steps.fix.outputs.fixed_count) > 0 uses: stefanzweifel/git-auto-commit-action@be7095c202abcf573b09f20541e0ee2f6a3a9d9b # v5.0.1 with: commit_message: 'style: apply terraform formatting fixes' diff --git a/validate-inputs/scripts/update-validators.py b/validate-inputs/scripts/update-validators.py index 59e715a..9bf674f 100755 --- a/validate-inputs/scripts/update-validators.py +++ b/validate-inputs/scripts/update-validators.py @@ -304,10 +304,6 @@ class ValidationRuleGenerator: "cache-mode": "cache_mode", "sbom-format": "sbom_format", }, - "common-cache": { - "paths": "file_path", - "key-files": "file_path", - }, "common-file-check": { "file-pattern": "file_path", }, @@ -315,9 +311,6 @@ class ValidationRuleGenerator: "backoff-strategy": "backoff_strategy", "shell": "shell_type", }, - "node-setup": { - "package-manager": "package_manager_enum", - }, "docker-publish": { "registry": "registry_enum", "cache-mode": "cache_mode", @@ -338,9 +331,6 @@ class ValidationRuleGenerator: "file-pattern": "file_pattern", "plugins": "plugin_list", }, - "php-laravel-phpunit": { - "extensions": "php_extensions", - }, "codeql-analysis": { "language": "codeql_language", "queries": "codeql_queries", diff --git a/validate-inputs/tests/test_common-cache_custom.py b/validate-inputs/tests/test_common-cache_custom.py deleted file mode 100644 index b4cc5e3..0000000 --- a/validate-inputs/tests/test_common-cache_custom.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Tests for common-cache custom validator. - -Generated by generate-tests.py - Do not edit manually. -""" -# pylint: disable=invalid-name # Test file name matches action name - -import sys -from pathlib import Path - -# Add action directory to path to import custom validator -action_path = Path(__file__).parent.parent.parent / "common-cache" -sys.path.insert(0, str(action_path)) - -# pylint: disable=wrong-import-position -from CustomValidator import CustomValidator - - -class TestCustomCommonCacheValidator: - """Test cases for common-cache custom validator.""" - - def setup_method(self): - """Set up test fixtures.""" - self.validator = CustomValidator("common-cache") - - def teardown_method(self): - """Clean up after tests.""" - self.validator.clear_errors() - - def test_validate_inputs_valid(self): - """Test validation with valid inputs.""" - # TODO: Add specific valid inputs for common-cache - inputs = {} - result = self.validator.validate_inputs(inputs) - # Adjust assertion based on required inputs - assert isinstance(result, bool) - - def test_validate_inputs_invalid(self): - """Test validation with invalid inputs.""" - # TODO: Add specific invalid inputs for common-cache - inputs = {"invalid_key": "invalid_value"} - result = self.validator.validate_inputs(inputs) - # Custom validators may have specific validation rules - assert isinstance(result, bool) - - def test_required_inputs(self): - """Test required inputs detection.""" - required = self.validator.get_required_inputs() - assert isinstance(required, list) - # TODO: Assert specific required inputs for common-cache - - def test_validation_rules(self): - """Test validation rules.""" - rules = self.validator.get_validation_rules() - assert isinstance(rules, dict) - # TODO: Assert specific validation rules for common-cache - - def test_github_expressions(self): - """Test GitHub expression handling.""" - inputs = { - "test_input": "${{ github.token }}", - } - result = self.validator.validate_inputs(inputs) - assert isinstance(result, bool) - # GitHub expressions should generally be accepted - - def test_error_propagation(self): - """Test error propagation from sub-validators.""" - # Custom validators often use sub-validators - # Test that errors are properly propagated - inputs = {"test": "value"} - self.validator.validate_inputs(inputs) - # Check error handling - if self.validator.has_errors(): - assert len(self.validator.errors) > 0 diff --git a/validate-inputs/tests/test_node-setup_custom.py b/validate-inputs/tests/test_node-setup_custom.py deleted file mode 100644 index 440f0ce..0000000 --- a/validate-inputs/tests/test_node-setup_custom.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Tests for node-setup custom validator. - -Generated by generate-tests.py - Do not edit manually. -""" -# pylint: disable=invalid-name # Test file name matches action name - -import sys -from pathlib import Path - -# Add action directory to path to import custom validator -action_path = Path(__file__).parent.parent.parent / "node-setup" -sys.path.insert(0, str(action_path)) - -# pylint: disable=wrong-import-position -from CustomValidator import CustomValidator - - -class TestCustomNodeSetupValidator: - """Test cases for node-setup custom validator.""" - - def setup_method(self): - """Set up test fixtures.""" - self.validator = CustomValidator("node-setup") - - def teardown_method(self): - """Clean up after tests.""" - self.validator.clear_errors() - - def test_validate_inputs_valid(self): - """Test validation with valid inputs.""" - # TODO: Add specific valid inputs for node-setup - inputs = {} - result = self.validator.validate_inputs(inputs) - # Adjust assertion based on required inputs - assert isinstance(result, bool) - - def test_validate_inputs_invalid(self): - """Test validation with invalid inputs.""" - # TODO: Add specific invalid inputs for node-setup - inputs = {"invalid_key": "invalid_value"} - result = self.validator.validate_inputs(inputs) - # Custom validators may have specific validation rules - assert isinstance(result, bool) - - def test_required_inputs(self): - """Test required inputs detection.""" - required = self.validator.get_required_inputs() - assert isinstance(required, list) - # TODO: Assert specific required inputs for node-setup - - def test_validation_rules(self): - """Test validation rules.""" - rules = self.validator.get_validation_rules() - assert isinstance(rules, dict) - # TODO: Assert specific validation rules for node-setup - - def test_github_expressions(self): - """Test GitHub expression handling.""" - inputs = { - "test_input": "${{ github.token }}", - } - result = self.validator.validate_inputs(inputs) - assert isinstance(result, bool) - # GitHub expressions should generally be accepted - - def test_error_propagation(self): - """Test error propagation from sub-validators.""" - # Custom validators often use sub-validators - # Test that errors are properly propagated - inputs = {"test": "value"} - self.validator.validate_inputs(inputs) - # Check error handling - if self.validator.has_errors(): - assert len(self.validator.errors) > 0 diff --git a/validate-inputs/tests/test_php-composer_custom.py b/validate-inputs/tests/test_php-composer_custom.py deleted file mode 100644 index 8d22626..0000000 --- a/validate-inputs/tests/test_php-composer_custom.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Tests for php-composer custom validator. - -Generated by generate-tests.py - Do not edit manually. -""" -# pylint: disable=invalid-name # Test file name matches action name - -import sys -from pathlib import Path - -# Add action directory to path to import custom validator -action_path = Path(__file__).parent.parent.parent / "php-composer" -sys.path.insert(0, str(action_path)) - -# pylint: disable=wrong-import-position -from CustomValidator import CustomValidator - - -class TestCustomPhpComposerValidator: - """Test cases for php-composer custom validator.""" - - def setup_method(self): - """Set up test fixtures.""" - self.validator = CustomValidator("php-composer") - - def teardown_method(self): - """Clean up after tests.""" - self.validator.clear_errors() - - def test_validate_inputs_valid(self): - """Test validation with valid inputs.""" - # TODO: Add specific valid inputs for php-composer - inputs = {} - result = self.validator.validate_inputs(inputs) - # Adjust assertion based on required inputs - assert isinstance(result, bool) - - def test_validate_inputs_invalid(self): - """Test validation with invalid inputs.""" - # TODO: Add specific invalid inputs for php-composer - inputs = {"invalid_key": "invalid_value"} - result = self.validator.validate_inputs(inputs) - # Custom validators may have specific validation rules - assert isinstance(result, bool) - - def test_required_inputs(self): - """Test required inputs detection.""" - required = self.validator.get_required_inputs() - assert isinstance(required, list) - # TODO: Assert specific required inputs for php-composer - - def test_validation_rules(self): - """Test validation rules.""" - rules = self.validator.get_validation_rules() - assert isinstance(rules, dict) - # TODO: Assert specific validation rules for php-composer - - def test_github_expressions(self): - """Test GitHub expression handling.""" - inputs = { - "test_input": "${{ github.token }}", - } - result = self.validator.validate_inputs(inputs) - assert isinstance(result, bool) - # GitHub expressions should generally be accepted - - def test_error_propagation(self): - """Test error propagation from sub-validators.""" - # Custom validators often use sub-validators - # Test that errors are properly propagated - inputs = {"test": "value"} - self.validator.validate_inputs(inputs) - # Check error handling - if self.validator.has_errors(): - assert len(self.validator.errors) > 0 diff --git a/validate-inputs/tests/test_php-laravel-phpunit_custom.py b/validate-inputs/tests/test_php-laravel-phpunit_custom.py deleted file mode 100644 index 39e6b5a..0000000 --- a/validate-inputs/tests/test_php-laravel-phpunit_custom.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Tests for php-laravel-phpunit custom validator. - -Generated by generate-tests.py - Do not edit manually. -""" -# pylint: disable=invalid-name # Test file name matches action name - -import sys -from pathlib import Path - -# Add action directory to path to import custom validator -action_path = Path(__file__).parent.parent.parent / "php-laravel-phpunit" -sys.path.insert(0, str(action_path)) - -# pylint: disable=wrong-import-position -from CustomValidator import CustomValidator - - -class TestCustomPhpLaravelPhpunitValidator: - """Test cases for php-laravel-phpunit custom validator.""" - - def setup_method(self): - """Set up test fixtures.""" - self.validator = CustomValidator("php-laravel-phpunit") - - def teardown_method(self): - """Clean up after tests.""" - self.validator.clear_errors() - - def test_validate_inputs_valid(self): - """Test validation with valid inputs.""" - # TODO: Add specific valid inputs for php-laravel-phpunit - inputs = {} - result = self.validator.validate_inputs(inputs) - # Adjust assertion based on required inputs - assert isinstance(result, bool) - - def test_validate_inputs_invalid(self): - """Test validation with invalid inputs.""" - # TODO: Add specific invalid inputs for php-laravel-phpunit - inputs = {"invalid_key": "invalid_value"} - result = self.validator.validate_inputs(inputs) - # Custom validators may have specific validation rules - assert isinstance(result, bool) - - def test_required_inputs(self): - """Test required inputs detection.""" - required = self.validator.get_required_inputs() - assert isinstance(required, list) - # TODO: Assert specific required inputs for php-laravel-phpunit - - def test_validation_rules(self): - """Test validation rules.""" - rules = self.validator.get_validation_rules() - assert isinstance(rules, dict) - # TODO: Assert specific validation rules for php-laravel-phpunit - - def test_github_expressions(self): - """Test GitHub expression handling.""" - inputs = { - "test_input": "${{ github.token }}", - } - result = self.validator.validate_inputs(inputs) - assert isinstance(result, bool) - # GitHub expressions should generally be accepted - - def test_error_propagation(self): - """Test error propagation from sub-validators.""" - # Custom validators often use sub-validators - # Test that errors are properly propagated - inputs = {"test": "value"} - self.validator.validate_inputs(inputs) - # Check error handling - if self.validator.has_errors(): - assert len(self.validator.errors) > 0 diff --git a/validate-inputs/tests/test_version-file-parser_custom.py b/validate-inputs/tests/test_version-file-parser_custom.py deleted file mode 100644 index a9254b6..0000000 --- a/validate-inputs/tests/test_version-file-parser_custom.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Tests for version-file-parser custom validator. - -Generated by generate-tests.py - Do not edit manually. -""" -# pylint: disable=invalid-name # Test file name matches action name - -import sys -from pathlib import Path - -# Add action directory to path to import custom validator -action_path = Path(__file__).parent.parent.parent / "version-file-parser" -sys.path.insert(0, str(action_path)) - -# pylint: disable=wrong-import-position -from CustomValidator import CustomValidator - - -class TestCustomVersionFileParserValidator: - """Test cases for version-file-parser custom validator.""" - - def setup_method(self): - """Set up test fixtures.""" - self.validator = CustomValidator("version-file-parser") - - def teardown_method(self): - """Clean up after tests.""" - self.validator.clear_errors() - - def test_validate_inputs_valid(self): - """Test validation with valid inputs.""" - # TODO: Add specific valid inputs for version-file-parser - inputs = {} - result = self.validator.validate_inputs(inputs) - # Adjust assertion based on required inputs - assert isinstance(result, bool) - - def test_validate_inputs_invalid(self): - """Test validation with invalid inputs.""" - # TODO: Add specific invalid inputs for version-file-parser - inputs = {"invalid_key": "invalid_value"} - result = self.validator.validate_inputs(inputs) - # Custom validators may have specific validation rules - assert isinstance(result, bool) - - def test_required_inputs(self): - """Test required inputs detection.""" - required = self.validator.get_required_inputs() - assert isinstance(required, list) - # TODO: Assert specific required inputs for version-file-parser - - def test_validation_rules(self): - """Test validation rules.""" - rules = self.validator.get_validation_rules() - assert isinstance(rules, dict) - # TODO: Assert specific validation rules for version-file-parser - - def test_github_expressions(self): - """Test GitHub expression handling.""" - inputs = { - "test_input": "${{ github.token }}", - } - result = self.validator.validate_inputs(inputs) - assert isinstance(result, bool) - # GitHub expressions should generally be accepted - - def test_error_propagation(self): - """Test error propagation from sub-validators.""" - # Custom validators often use sub-validators - # Test that errors are properly propagated - inputs = {"test": "value"} - self.validator.validate_inputs(inputs) - # Check error handling - if self.validator.has_errors(): - assert len(self.validator.errors) > 0 diff --git a/version-file-parser/CustomValidator.py b/version-file-parser/CustomValidator.py deleted file mode 100755 index a969c5a..0000000 --- a/version-file-parser/CustomValidator.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for version-file-parser action.""" - -from __future__ import annotations - -from pathlib import Path -import sys - -# Add validate-inputs directory to path to import validators -validate_inputs_path = Path(__file__).parent.parent / "validate-inputs" -sys.path.insert(0, str(validate_inputs_path)) - -from validators.base import BaseValidator - - -class CustomValidator(BaseValidator): - """Custom validator for version-file-parser action.""" - - def __init__(self, action_type: str = "version-file-parser") -> None: - """Initialize version-file-parser validator.""" - super().__init__(action_type) - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate version-file-parser action inputs.""" - valid = True - - # Validate required input: language - if "language" not in inputs or not inputs["language"]: - self.add_error("Input 'language' is required") - valid = False - elif inputs["language"]: - # Validate language is one of the supported values - valid_languages = [ - "node", - "python", - "go", - "rust", - "ruby", - "php", - "java", - "dotnet", - "elixir", - ] - if inputs["language"] not in valid_languages: - self.add_error( - f"Invalid language: {inputs['language']}. " - f"Must be one of: {', '.join(valid_languages)}" - ) - valid = False - - # Validate dockerfile-image for injection - dockerfile_key = None - if "dockerfile-image" in inputs: - dockerfile_key = "dockerfile-image" - elif "dockerfile_image" in inputs: - dockerfile_key = "dockerfile_image" - - if dockerfile_key and inputs[dockerfile_key]: - value = inputs[dockerfile_key] - if ";" in value or "|" in value or "&" in value or "`" in value: - self.add_error("dockerfile-image contains potentially dangerous characters") - valid = False - - # Validate tool-versions-key for injection - tool_key = None - if "tool-versions-key" in inputs: - tool_key = "tool-versions-key" - elif "tool_versions_key" in inputs: - tool_key = "tool_versions_key" - - if tool_key and inputs[tool_key]: - value = inputs[tool_key] - if "|" in value or ";" in value or "&" in value or "`" in value: - self.add_error("tool-versions-key contains potentially dangerous characters") - valid = False - - # Validate validation-regex for malicious patterns - regex_key = None - if "validation-regex" in inputs: - regex_key = "validation-regex" - elif "validation_regex" in inputs: - regex_key = "validation_regex" - - if regex_key and inputs[regex_key]: - value = inputs[regex_key] - # Check for shell command injection in regex - if ";" in value or "|" in value or "`" in value or "rm " in value: - self.add_error("validation-regex contains potentially dangerous patterns") - valid = False - - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs.""" - return ["language"] - - def get_validation_rules(self) -> dict: - """Get validation rules.""" - return { - "language": { - "type": "string", - "required": True, - "description": "Language identifier", - }, - "tool-versions-key": { - "type": "string", - "required": False, - "description": "Key in .tool-versions", - }, - "dockerfile-image": { - "type": "string", - "required": False, - "description": "Dockerfile image name", - }, - } diff --git a/version-file-parser/README.md b/version-file-parser/README.md deleted file mode 100644 index c4b2708..0000000 --- a/version-file-parser/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# ivuorinen/actions/version-file-parser - -## Version File Parser - -### Description - -Universal parser for common version detection files (.tool-versions, Dockerfile, devcontainer.json, etc.) - -### Inputs - -| name | description | required | default | -|---------------------|------------------------------------------------------------------------------|----------|-------------------------------| -| `language` |

Programming language name (node, python, php, go, dotnet)

| `true` | `""` | -| `tool-versions-key` |

Key name in .tool-versions file (nodejs, python, php, golang, dotnet)

| `true` | `""` | -| `dockerfile-image` |

Docker image name pattern (node, python, php, golang, dotnet)

| `true` | `""` | -| `version-file` |

Language-specific version file (.nvmrc, .python-version, etc.)

| `false` | `""` | -| `validation-regex` |

Version validation regex pattern

| `false` | `^[0-9]+\.[0-9]+(\.[0-9]+)?$` | -| `default-version` |

Default version to use if no version is detected

| `false` | `""` | - -### Outputs - -| name | description | -|-------------------------|-----------------------------------------------------------------------------------| -| `tool-versions-version` |

Version found in .tool-versions

| -| `dockerfile-version` |

Version found in Dockerfile

| -| `devcontainer-version` |

Version found in devcontainer.json

| -| `version-file-version` |

Version found in language-specific version file

| -| `config-file-version` |

Version found in language config files (package.json, composer.json, etc.)

| -| `detected-version` |

Final detected version (first found or default)

| -| `package-manager` |

Detected package manager (npm, yarn, pnpm, composer, pip, poetry, etc.)

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/version-file-parser@main - with: - language: - # Programming language name (node, python, php, go, dotnet) - # - # Required: true - # Default: "" - - tool-versions-key: - # Key name in .tool-versions file (nodejs, python, php, golang, dotnet) - # - # Required: true - # Default: "" - - dockerfile-image: - # Docker image name pattern (node, python, php, golang, dotnet) - # - # Required: true - # Default: "" - - version-file: - # Language-specific version file (.nvmrc, .python-version, etc.) - # - # Required: false - # Default: "" - - validation-regex: - # Version validation regex pattern - # - # Required: false - # Default: ^[0-9]+\.[0-9]+(\.[0-9]+)?$ - - default-version: - # Default version to use if no version is detected - # - # Required: false - # Default: "" -``` diff --git a/version-file-parser/action.yml b/version-file-parser/action.yml deleted file mode 100644 index 39b3920..0000000 --- a/version-file-parser/action.yml +++ /dev/null @@ -1,365 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for reading version files ---- -name: Version File Parser -description: 'Universal parser for common version detection files (.tool-versions, Dockerfile, devcontainer.json, etc.)' -author: 'Ismo Vuorinen' - -branding: - icon: search - color: gray-dark - -inputs: - language: - description: 'Programming language name (node, python, php, go, dotnet)' - required: true - tool-versions-key: - description: 'Key name in .tool-versions file (nodejs, python, php, golang, dotnet)' - required: true - dockerfile-image: - description: 'Docker image name pattern (node, python, php, golang, dotnet)' - required: true - version-file: - description: 'Language-specific version file (.nvmrc, .python-version, etc.)' - required: false - validation-regex: - description: 'Version validation regex pattern' - required: false - default: '^[0-9]+\.[0-9]+(\.[0-9]+)?$' - default-version: - description: 'Default version to use if no version is detected' - required: false - -outputs: - tool-versions-version: - description: 'Version found in .tool-versions' - value: ${{ steps.parse.outputs.tool-versions-version }} - dockerfile-version: - description: 'Version found in Dockerfile' - value: ${{ steps.parse.outputs.dockerfile-version }} - devcontainer-version: - description: 'Version found in devcontainer.json' - value: ${{ steps.parse.outputs.devcontainer-version }} - version-file-version: - description: 'Version found in language-specific version file' - value: ${{ steps.parse.outputs.version-file-version }} - config-file-version: - description: 'Version found in language config files (package.json, composer.json, etc.)' - value: ${{ steps.parse.outputs.config-file-version }} - detected-version: - description: 'Final detected version (first found or default)' - value: ${{ steps.parse.outputs.detected-version }} - package-manager: - description: 'Detected package manager (npm, yarn, pnpm, composer, pip, poetry, etc.)' - value: ${{ steps.parse.outputs.package-manager }} - -runs: - using: composite - steps: - - name: Parse Version Files - id: parse - shell: bash - env: - VALIDATION_REGEX: ${{ inputs.validation-regex }} - LANGUAGE: ${{ inputs.language }} - TOOL_VERSIONS_KEY: ${{ inputs.tool-versions-key }} - DOCKERFILE_IMAGE: ${{ inputs.dockerfile-image }} - VERSION_FILE: ${{ inputs.version-file }} - DEFAULT_VERSION: ${{ inputs.default-version }} - run: |- - set -euo pipefail - - # Function to validate version format - validate_version() { - local version=$1 - local regex="$VALIDATION_REGEX" - - # Test regex validity - if ! bash -c "[[ 'test' =~ \${regex} ]]" 2>/dev/null; then - echo "::error::Invalid validation regex pattern: $regex" >&2 - exit 1 - fi - - # Validate version using safe regex matching with quoted variable - if [[ $version =~ ^${regex}$ ]]; then - return 0 - fi - return 1 - } - - # Function to clean version string - clean_version() { - echo "$1" | sed 's/^[vV]//' | tr -d ' \n\r' - } - - # Initialize outputs - echo "tool-versions-version=" >> $GITHUB_OUTPUT - echo "dockerfile-version=" >> $GITHUB_OUTPUT - echo "devcontainer-version=" >> $GITHUB_OUTPUT - echo "version-file-version=" >> $GITHUB_OUTPUT - echo "config-file-version=" >> $GITHUB_OUTPUT - echo "detected-version=" >> $GITHUB_OUTPUT - echo "package-manager=" >> $GITHUB_OUTPUT - - # Language detection patterns - language="$LANGUAGE" - - # 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 - echo "tool-versions-version=$version" >> $GITHUB_OUTPUT - fi - fi - fi - - # Parse Dockerfile - if [ -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 - echo "dockerfile-version=$version" >> $GITHUB_OUTPUT - fi - fi - fi - - # Parse devcontainer.json - if [ -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 - echo "devcontainer-version=$version" >> $GITHUB_OUTPUT - fi - fi - else - echo "jq not available, skipping devcontainer parsing" >&2 - fi - fi - - # Parse language-specific version file - if [ -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 - echo "version-file-version=$version" >> $GITHUB_OUTPUT - fi - fi - fi - - # Parse language-specific configuration files - config_version="" - detected_package_manager="" - - case "$language" in - "node") - # Check package.json - if [ -f package.json ] && 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" ] && validate_version "$version"; then - echo "Found Node.js version in package.json: $version" >&2 - config_version="$version" - 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 ] && command -v jq >/dev/null 2>&1; then - # Check packageManager field in package.json - 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 - ;; - - "php") - # Check composer.json - if [ -f composer.json ] && command -v jq >/dev/null 2>&1; then - # Try require.php first - 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 - # Try platform.php - 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 - config_version="$version" - fi - fi - - # Check phpunit.xml - if [ -z "$config_version" ]; then - phpunit_file="" - if [ -f phpunit.xml ]; then - phpunit_file="phpunit.xml" - elif [ -f phpunit.xml.dist ]; then - phpunit_file="phpunit.xml.dist" - fi - - if [ -n "$phpunit_file" ]; then - version=$(grep -o 'php[[:space:]]*version[[:space:]]*=[[:space:]]*"[^"]*"' "$phpunit_file" | sed -n 's/.*"\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\)".*/\1/p') - if [ -n "$version" ] && validate_version "$version"; then - echo "Found PHP version in $phpunit_file: $version" >&2 - config_version="$version" - fi - fi - fi - - # Detect package manager - if [ -f composer.json ]; then - detected_package_manager="composer" - fi - ;; - - "python") - # Check pyproject.toml - if [ -f pyproject.toml ]; then - # Try PEP 621 requires-python first (allow leading whitespace) - if command -v jq >/dev/null 2>&1 && grep -q '^\[project\]' pyproject.toml; then - # Convert TOML to JSON for PEP 621 parsing (basic approach) - 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 [project] requires-python: $version" >&2 - config_version="$version" - fi - fi - # Fallback to legacy python field if no PEP 621 version found - if [ -z "$config_version" ]; then - version=$(grep -E '^python[[:space:]]*=' pyproject.toml | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p') - if [ -n "$version" ] && validate_version "$version"; then - echo "Found Python version in pyproject.toml: $version" >&2 - config_version="$version" - fi - fi - fi - - # Check setup.py for python_requires - if [ -z "$config_version" ] && [ -f setup.py ]; then - version=$(grep -o 'python_requires[[:space:]]*=[[:space:]]*['\''"].*['\''"]' setup.py | sed -n 's/[^0-9]*\([0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\).*/\1/p') - if [ -n "$version" ] && validate_version "$version"; then - echo "Found Python version in setup.py: $version" >&2 - config_version="$version" - 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" - elif [ -f requirements.txt ]; then - detected_package_manager="pip" - 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 - config_version="$version" - fi - fi - - # Detect package manager - if [ -f go.mod ]; then - 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 - config_version="$version" - fi - fi - - # Check .csproj files - if [ -z "$config_version" ]; then - # Enable nullglob to handle case when no .csproj files exist - shopt -s nullglob - for csproj in *.csproj; do - if [ -f "$csproj" ]; then - # Handle both TargetFramework and TargetFrameworks, and handle -windows monikers - version=$(grep -oE 'net[0-9]+\.[0-9]+(-[a-z]+)?' "$csproj" | sed -n 's/.*net\([0-9]\+\.[0-9]\+\).*/\1/p' | head -1) - if [ -n "$version" ] && validate_version "$version"; then - echo "Found .NET version in $csproj: $version" >&2 - config_version="$version" - break - fi - fi - done - # Disable nullglob after use - shopt -u nullglob - fi - - # Detect package manager - detected_package_manager="dotnet" - ;; - esac - - # Set config-file-version output - if [ -n "$config_version" ]; then - echo "config-file-version=$config_version" >> $GITHUB_OUTPUT - fi - - # Set package-manager output - if [ -n "$detected_package_manager" ]; then - echo "package-manager=$detected_package_manager" >> $GITHUB_OUTPUT - fi - - # Determine final detected version with priority order - # Priority order: version-file > config-file > tool-versions > dockerfile > devcontainer > default - final_version=$(grep -E "^(version-file|config-file|tool-versions|dockerfile|devcontainer)-version=" $GITHUB_OUTPUT | tac | awk -F= 'NF>1 && $2!="" {print $2; exit}') - - # If no version found from any source, use default - if [ -z "$final_version" ] && [ -n "$DEFAULT_VERSION" ]; then - final_version="$DEFAULT_VERSION" - echo "Using default $LANGUAGE version: $final_version" >&2 - fi - - # Set final detected version - if [ -n "$final_version" ]; then - # Validate the final version against the regex - if ! validate_version "$final_version"; then - echo "::error::Detected version $final_version does not match validation regex" >&2 - exit 1 - fi - echo "detected-version=$final_version" >> $GITHUB_OUTPUT - echo "Final detected $LANGUAGE version: $final_version" >&2 - else - echo "No $LANGUAGE version detected" >&2 - fi diff --git a/version-file-parser/rules.yml b/version-file-parser/rules.yml deleted file mode 100644 index 7795a16..0000000 --- a/version-file-parser/rules.yml +++ /dev/null @@ -1,42 +0,0 @@ ---- -# Validation rules for version-file-parser action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 50% (3/6 inputs) -# -# This file defines validation rules for the version-file-parser GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: version-file-parser -description: Universal parser for common version detection files (.tool-versions, Dockerfile, devcontainer.json, etc.) -generator_version: 1.0.0 -required_inputs: - - dockerfile-image - - language - - tool-versions-key -optional_inputs: - - default-version - - validation-regex - - version-file -conventions: - default-version: semantic_version - validation-regex: regex_pattern - version-file: file_path -overrides: {} -statistics: - total_inputs: 6 - validated_inputs: 3 - skipped_inputs: 0 - coverage_percentage: 50 -validation_coverage: 50 -auto_detected: true -manual_review_required: true -quality_indicators: - has_required_inputs: true - has_token_validation: false - has_version_validation: true - has_file_validation: true - has_security_validation: false