mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 11:34:00 +00:00
* 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
580 lines
25 KiB
Python
Executable File
580 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""update-validators.py
|
|
|
|
Automatically generates validation rules for GitHub Actions
|
|
by scanning action.yml files and applying convention-based detection.
|
|
|
|
Usage:
|
|
python update-validators.py [--dry-run] [--action action-name]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml # pylint: disable=import-error
|
|
|
|
|
|
class ValidationRuleGenerator:
|
|
"""Generate validation rules for GitHub Actions automatically.
|
|
|
|
This class scans GitHub Action YAML files and generates validation rules
|
|
based on convention-based detection patterns and special case handling.
|
|
"""
|
|
|
|
def __init__(self, *, dry_run: bool = False, specific_action: str | None = None) -> None:
|
|
"""Initialize the validation rule generator.
|
|
|
|
Args:
|
|
dry_run: If True, show what would be generated without writing files
|
|
specific_action: If provided, only generate rules for this action
|
|
"""
|
|
self.dry_run = dry_run
|
|
self.specific_action = specific_action
|
|
self.actions_dir = Path(__file__).parent.parent.parent.resolve()
|
|
|
|
# Convention patterns for automatic detection
|
|
# Order matters - more specific patterns should come first
|
|
self.conventions = {
|
|
# CodeQL-specific patterns (high priority)
|
|
"codeql_language": re.compile(r"\blanguage\b", re.IGNORECASE),
|
|
"codeql_queries": re.compile(r"\bquer(y|ies)\b", re.IGNORECASE),
|
|
"codeql_packs": re.compile(r"\bpacks?\b", re.IGNORECASE),
|
|
"codeql_build_mode": re.compile(r"\bbuild[_-]?mode\b", re.IGNORECASE),
|
|
"codeql_config": re.compile(r"\bconfig\b", re.IGNORECASE),
|
|
"category_format": re.compile(r"\bcategor(y|ies)\b", re.IGNORECASE),
|
|
# GitHub token patterns (high priority)
|
|
"github_token": re.compile(
|
|
r"\b(github[_-]?token|gh[_-]?token|token|auth[_-]?token|api[_-]?key)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
# CalVer version patterns (high priority - check before semantic)
|
|
"calver_version": re.compile(
|
|
r"\b(release[_-]?tag|release[_-]?version|monthly[_-]?version|date[_-]?version)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
# Specific version types (high priority)
|
|
"dotnet_version": re.compile(r"\bdotnet[_-]?version\b", re.IGNORECASE),
|
|
"terraform_version": re.compile(r"\bterraform[_-]?version\b", re.IGNORECASE),
|
|
"node_version": re.compile(r"\bnode[_-]?version\b", re.IGNORECASE),
|
|
# Docker-specific patterns (high priority)
|
|
"docker_image_name": re.compile(r"\bimage[_-]?name\b", re.IGNORECASE),
|
|
"docker_tag": re.compile(r"\b(tags?|image[_-]?tags?)\b", re.IGNORECASE),
|
|
"docker_architectures": re.compile(
|
|
r"\b(arch|architecture|platform)s?\b",
|
|
re.IGNORECASE,
|
|
),
|
|
# Namespace with lookahead (specific pattern)
|
|
"namespace_with_lookahead": re.compile(r"\bnamespace\b", re.IGNORECASE),
|
|
# Numeric ranges (specific ranges)
|
|
"numeric_range_0_16": re.compile(
|
|
r"\b(parallel[_-]?builds?|builds?[_-]?parallel)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
"numeric_range_1_10": re.compile(
|
|
r"\b(retry|retries|attempt|attempts|max[_-]?retry)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
"numeric_range_1_128": re.compile(r"\bthreads?\b", re.IGNORECASE),
|
|
"numeric_range_256_32768": re.compile(r"\bram\b", re.IGNORECASE),
|
|
"numeric_range_0_100": re.compile(r"\b(quality|percent|percentage)\b", re.IGNORECASE),
|
|
# File and path patterns
|
|
"file_path": re.compile(
|
|
r"\b(paths?|files?|dir|directory|config|dockerfile"
|
|
r"|ignore[_-]?file|key[_-]?files?)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
"file_pattern": re.compile(r"\b(file[_-]?pattern|glob[_-]?pattern)\b", re.IGNORECASE),
|
|
"branch_name": re.compile(r"\b(branch|ref|base[_-]?branch)\b", re.IGNORECASE),
|
|
# User and identity patterns
|
|
"email": re.compile(r"\b(email|mail)\b", re.IGNORECASE),
|
|
"username": re.compile(r"\b(user|username|commit[_-]?user)\b", re.IGNORECASE),
|
|
# URL patterns (high priority)
|
|
"url": re.compile(r"\b(url|registry[_-]?url|api[_-]?url|endpoint)\b", re.IGNORECASE),
|
|
# Scope and namespace patterns
|
|
"scope": re.compile(r"\b(scope|namespace)\b", re.IGNORECASE),
|
|
# Security patterns for text content that could contain injection
|
|
"security_patterns": re.compile(
|
|
r"\b(changelog|notes|message|content|description|body|text|comment|summary|release[_-]?notes)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
# Regex pattern validation (ReDoS detection)
|
|
"regex_pattern": re.compile(
|
|
r"\b(regex|pattern|validation[_-]?regex|regex[_-]?pattern)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
# Additional validation types
|
|
"report_format": re.compile(r"\b(report[_-]?format|format)\b", re.IGNORECASE),
|
|
"plugin_list": re.compile(r"\b(plugins?|plugin[_-]?list)\b", re.IGNORECASE),
|
|
"prefix": re.compile(r"\b(prefix|tag[_-]?prefix)\b", re.IGNORECASE),
|
|
# Boolean patterns (broad, should be lower priority)
|
|
"boolean": re.compile(
|
|
r"\b(dry-?run|verbose|enable|disable|auto|skip|force|cache|provenance|sbom|scan|sign|fail[_-]?on[_-]?error|nightly)\b",
|
|
re.IGNORECASE,
|
|
),
|
|
# File extensions pattern
|
|
"file_extensions": re.compile(r"\b(file[_-]?extensions?|extensions?)\b", re.IGNORECASE),
|
|
# Registry pattern
|
|
"registry": re.compile(r"\bregistry\b", re.IGNORECASE),
|
|
# PHP-specific patterns
|
|
"php_extensions": re.compile(r"\b(extensions?|php[_-]?extensions?)\b", re.IGNORECASE),
|
|
"coverage_driver": re.compile(r"\b(coverage|coverage[_-]?driver)\b", re.IGNORECASE),
|
|
# Generic version pattern (lowest priority - catches remaining version fields)
|
|
"semantic_version": re.compile(r"\bversion\b", re.IGNORECASE),
|
|
}
|
|
|
|
# Special cases that need manual handling
|
|
self.special_cases = {
|
|
# CalVer fields that might not be detected
|
|
"release-tag": "calver_version",
|
|
# Flexible version fields (support both CalVer and SemVer)
|
|
"version": "flexible_version", # For github-release action
|
|
# File paths that might not be detected
|
|
"pre-commit-config": "file_path",
|
|
"config-file": "file_path",
|
|
"ignore-file": "file_path",
|
|
"readme-file": "file_path",
|
|
"working-directory": "file_path",
|
|
# Numeric fields that need positive integer validation
|
|
"days-before-stale": "positive_integer",
|
|
"days-before-close": "positive_integer",
|
|
# Version fields with specific types
|
|
"buildx-version": "semantic_version",
|
|
"buildkit-version": "semantic_version",
|
|
"tflint-version": "terraform_version",
|
|
"default-version": "semantic_version",
|
|
"force-version": "semantic_version",
|
|
"golangci-lint-version": "semantic_version",
|
|
"prettier-version": "semantic_version",
|
|
"eslint-version": "strict_semantic_version",
|
|
"flake8-version": "semantic_version",
|
|
"autopep8-version": "semantic_version",
|
|
"composer-version": "semantic_version",
|
|
# Tokens and passwords
|
|
"dockerhub-password": "github_token",
|
|
"npm_token": "github_token",
|
|
"password": "github_token",
|
|
# Complex fields that should skip validation
|
|
"build-args": None, # Can be empty
|
|
"context": None, # Default handled
|
|
"cache-from": None, # Complex cache syntax
|
|
"cache-export": None, # Complex cache syntax
|
|
"cache-import": None, # Complex cache syntax
|
|
"build-contexts": None, # Complex syntax
|
|
"secrets": None, # Complex syntax
|
|
"platform-build-args": None, # JSON format
|
|
"extensions": None, # PHP extensions list
|
|
"tools": None, # PHP tools list
|
|
"args": None, # Composer args
|
|
"stability": None, # Composer stability
|
|
"registry-url": "url", # URL format
|
|
"scope": "scope", # NPM scope
|
|
"plugins": None, # Prettier plugins
|
|
"file-extensions": "file_extensions", # File extension list
|
|
"file-pattern": None, # Glob pattern
|
|
"enable-linters": None, # Linter list
|
|
"disable-linters": None, # Linter list
|
|
"success-codes": None, # Exit code list
|
|
"retry-codes": None, # Exit code list
|
|
"ignore-paths": None, # Path patterns
|
|
"key-files": None, # Cache key files
|
|
"restore-keys": None, # Cache restore keys
|
|
"env-vars": None, # Environment variables
|
|
# Action-specific fields that need special handling
|
|
"type": None, # Cache type enum (npm, composer, go, etc.) - complex enum,
|
|
# skip validation
|
|
"paths": None, # File paths for caching (comma-separated) - complex format,
|
|
# skip validation
|
|
"command": None, # Shell command - complex format, skip validation for safety
|
|
"backoff-strategy": None, # Retry strategy enum - complex enum, skip validation
|
|
"shell": None, # Shell type enum - simple enum, skip validation
|
|
# Removed image-name and tag - now handled by docker_image_name and docker_tag patterns
|
|
# Numeric inputs with different ranges
|
|
"timeout": "numeric_range_1_3600", # Timeout should support higher values
|
|
"retry-delay": "numeric_range_1_300", # Retry delay should support higher values
|
|
"max-warnings": "numeric_range_0_10000",
|
|
# version-file-parser specific fields
|
|
"language": None, # Simple enum (node, php, python, go, dotnet)
|
|
"tool-versions-key": None, # Simple string (nodejs, python, php, golang, dotnet)
|
|
"dockerfile-image": None, # Simple string (node, python, php, golang, dotnet)
|
|
"validation-regex": "regex_pattern", # Regex pattern - validate for ReDoS
|
|
}
|
|
|
|
def get_action_directories(self) -> list[str]:
|
|
"""Get all action directories"""
|
|
entries = []
|
|
for item in self.actions_dir.iterdir():
|
|
if (
|
|
item.is_dir()
|
|
and not item.name.startswith(".")
|
|
and item.name != "validate-inputs"
|
|
and (item / "action.yml").exists()
|
|
):
|
|
entries.append(item.name)
|
|
return entries
|
|
|
|
def parse_action_file(self, action_name: str) -> dict[str, Any] | None:
|
|
"""Parse action.yml file to extract inputs"""
|
|
action_file = self.actions_dir / action_name / "action.yml"
|
|
|
|
try:
|
|
with action_file.open(encoding="utf-8") as f:
|
|
content = f.read()
|
|
action_data = yaml.safe_load(content)
|
|
|
|
return {
|
|
"name": action_data.get("name", action_name),
|
|
"description": action_data.get("description", ""),
|
|
"inputs": action_data.get("inputs", {}),
|
|
}
|
|
except Exception as error:
|
|
print(f"Failed to parse {action_file}: {error}")
|
|
return None
|
|
|
|
def detect_validation_type(self, input_name: str, input_data: dict[str, Any]) -> str | None:
|
|
"""Detect validation type based on input name and description"""
|
|
description = input_data.get("description", "")
|
|
|
|
# Check special cases first - highest priority
|
|
if input_name in self.special_cases:
|
|
return self.special_cases[input_name]
|
|
|
|
# Special handling for version fields that might be CalVer
|
|
# Check if description mentions calendar/date/monthly/release
|
|
if input_name == "version" and any(
|
|
word in description.lower() for word in ["calendar", "date", "monthly", "release"]
|
|
):
|
|
return "calver_version"
|
|
|
|
# Apply convention patterns in order (more specific first)
|
|
# Test input name first (highest confidence), then description
|
|
for validator, pattern in self.conventions.items():
|
|
if pattern.search(input_name):
|
|
return validator # Direct name match has highest confidence
|
|
|
|
# If no name match, try description
|
|
for validator, pattern in self.conventions.items():
|
|
if pattern.search(description):
|
|
return validator # Description match has lower confidence
|
|
|
|
return None # No validation detected
|
|
|
|
def sort_object_by_keys(self, obj: dict[str, Any]) -> dict[str, Any]:
|
|
"""Sort object keys alphabetically for consistent output"""
|
|
return {key: obj[key] for key in sorted(obj.keys())}
|
|
|
|
def generate_rules_for_action(self, action_name: str) -> dict[str, Any] | None:
|
|
"""Generate validation rules for a single action"""
|
|
action_data = self.parse_action_file(action_name)
|
|
if not action_data:
|
|
return None
|
|
|
|
required_inputs = []
|
|
optional_inputs = []
|
|
conventions = {}
|
|
overrides = {}
|
|
|
|
# Process each input
|
|
for input_name, input_data in action_data["inputs"].items():
|
|
is_required = input_data.get("required") in [True, "true"]
|
|
if is_required:
|
|
required_inputs.append(input_name)
|
|
else:
|
|
optional_inputs.append(input_name)
|
|
|
|
# Detect validation type
|
|
validation_type = self.detect_validation_type(input_name, input_data)
|
|
if validation_type:
|
|
conventions[input_name] = validation_type
|
|
|
|
# Handle action-specific overrides using data-driven approach
|
|
action_overrides = {
|
|
"php-version-detect": {"default-version": "php_version"},
|
|
"python-version-detect": {"default-version": "python_version"},
|
|
"python-version-detect-v2": {"default-version": "python_version"},
|
|
"dotnet-version-detect": {"default-version": "dotnet_version"},
|
|
"go-version-detect": {"default-version": "go_version"},
|
|
"npm-publish": {"package-version": "strict_semantic_version"},
|
|
"docker-build": {
|
|
"cache-mode": "cache_mode",
|
|
"sbom-format": "sbom_format",
|
|
},
|
|
"common-file-check": {
|
|
"file-pattern": "file_path",
|
|
},
|
|
"common-retry": {
|
|
"backoff-strategy": "backoff_strategy",
|
|
"shell": "shell_type",
|
|
},
|
|
"docker-publish": {
|
|
"registry": "registry_enum",
|
|
"cache-mode": "cache_mode",
|
|
"platforms": None, # Skip validation - complex platform format
|
|
},
|
|
"docker-publish-hub": {
|
|
"password": "docker_password",
|
|
},
|
|
"go-lint": {
|
|
"go-version": "go_version",
|
|
"timeout": "timeout_with_unit",
|
|
"only-new-issues": "boolean",
|
|
"enable-linters": "linter_list",
|
|
"disable-linters": "linter_list",
|
|
},
|
|
"prettier-check": {
|
|
"check-only": "boolean",
|
|
"file-pattern": "file_pattern",
|
|
"plugins": "plugin_list",
|
|
},
|
|
"codeql-analysis": {
|
|
"language": "codeql_language",
|
|
"queries": "codeql_queries",
|
|
"packs": "codeql_packs",
|
|
"config": "codeql_config",
|
|
"build-mode": "codeql_build_mode",
|
|
"source-root": "file_path",
|
|
"category": "category_format",
|
|
"token": "github_token",
|
|
"ram": "numeric_range_256_32768",
|
|
"threads": "numeric_range_1_128",
|
|
"output": "file_path",
|
|
"skip-queries": "boolean",
|
|
},
|
|
"biome-lint": {
|
|
"mode": "mode_enum",
|
|
},
|
|
"eslint-lint": {
|
|
"mode": "mode_enum",
|
|
},
|
|
"prettier-lint": {
|
|
"mode": "mode_enum",
|
|
},
|
|
}
|
|
|
|
if action_name in action_overrides:
|
|
# Apply overrides for existing conventions
|
|
overrides.update(
|
|
{
|
|
input_name: override_value
|
|
for input_name, override_value in action_overrides[action_name].items()
|
|
if input_name in conventions
|
|
},
|
|
)
|
|
# Add missing inputs from overrides to conventions
|
|
for input_name, override_value in action_overrides[action_name].items():
|
|
if input_name not in conventions and input_name in action_data["inputs"]:
|
|
conventions[input_name] = override_value
|
|
|
|
# Calculate statistics
|
|
total_inputs = len(action_data["inputs"])
|
|
validated_inputs = len(conventions)
|
|
skipped_inputs = sum(1 for v in overrides.values() if v is None)
|
|
coverage = round((validated_inputs / total_inputs) * 100) if total_inputs > 0 else 0
|
|
|
|
# Generate rules object with enhanced metadata
|
|
rules = {
|
|
"schema_version": "1.0",
|
|
"action": action_name,
|
|
"description": action_data["description"],
|
|
"generator_version": "1.0.0",
|
|
"required_inputs": sorted(required_inputs),
|
|
"optional_inputs": sorted(optional_inputs),
|
|
"conventions": self.sort_object_by_keys(conventions),
|
|
"overrides": self.sort_object_by_keys(overrides),
|
|
"statistics": {
|
|
"total_inputs": total_inputs,
|
|
"validated_inputs": validated_inputs,
|
|
"skipped_inputs": skipped_inputs,
|
|
"coverage_percentage": coverage,
|
|
},
|
|
"validation_coverage": coverage,
|
|
"auto_detected": True,
|
|
"manual_review_required": coverage < 80 or validated_inputs == 0,
|
|
"quality_indicators": {
|
|
"has_required_inputs": len(required_inputs) > 0,
|
|
"has_token_validation": "token" in conventions or "github-token" in conventions,
|
|
"has_version_validation": any("version" in v for v in conventions.values() if v),
|
|
"has_file_validation": any(v == "file_path" for v in conventions.values()),
|
|
"has_security_validation": any(
|
|
v in ["github_token", "security_patterns"] for v in conventions.values()
|
|
),
|
|
},
|
|
}
|
|
|
|
return rules
|
|
|
|
def write_rules_file(self, action_name: str, rules: dict[str, Any]) -> None:
|
|
"""Write rules to YAML file in action folder"""
|
|
rules_file = self.actions_dir / action_name / "rules.yml"
|
|
generator_version = rules.get("generator_version", "unknown")
|
|
schema_version = rules.get("schema_version", "unknown")
|
|
validation_coverage = rules.get("validation_coverage", 0)
|
|
validated_inputs = rules["statistics"].get("validated_inputs", 0)
|
|
total_inputs = rules["statistics"].get("total_inputs", 0)
|
|
|
|
header = f"""---
|
|
# Validation rules for {action_name} action
|
|
# Generated by update-validators.py v{generator_version} - DO NOT EDIT MANUALLY
|
|
# Schema version: {schema_version}
|
|
# Coverage: {validation_coverage}% ({validated_inputs}/{total_inputs} inputs)
|
|
#
|
|
# This file defines validation rules for the {action_name} GitHub Action.
|
|
# Rules are automatically applied by validate-inputs action when this
|
|
# action is used.
|
|
#
|
|
|
|
"""
|
|
|
|
# Use a custom yaml dumper to ensure proper indentation
|
|
class CustomYamlDumper(yaml.SafeDumper):
|
|
def increase_indent(self, flow: bool = False, *, indentless: bool = False) -> None: # noqa: FBT001, FBT002
|
|
return super().increase_indent(flow, indentless=indentless)
|
|
|
|
yaml_content = yaml.dump(
|
|
rules,
|
|
Dumper=CustomYamlDumper,
|
|
indent=2,
|
|
width=120,
|
|
default_flow_style=False,
|
|
allow_unicode=True,
|
|
sort_keys=False,
|
|
)
|
|
|
|
content = header + yaml_content
|
|
|
|
if self.dry_run:
|
|
print(f"[DRY RUN] Would write {rules_file}:")
|
|
print(content)
|
|
print("---")
|
|
else:
|
|
with rules_file.open("w", encoding="utf-8") as f:
|
|
f.write(content)
|
|
print(f"✅ Generated {rules_file}")
|
|
|
|
def generate_rules(self) -> None:
|
|
"""Generate rules for all actions or a specific action"""
|
|
print("🔍 Scanning for GitHub Actions...")
|
|
|
|
actions = self.get_action_directories()
|
|
filtered_actions = actions
|
|
|
|
if self.specific_action:
|
|
filtered_actions = [name for name in actions if name == self.specific_action]
|
|
if not filtered_actions:
|
|
print(f"❌ Action '{self.specific_action}' not found")
|
|
sys.exit(1)
|
|
|
|
print(f"📝 Found {len(actions)} actions, processing {len(filtered_actions)}:")
|
|
for name in filtered_actions:
|
|
print(f" - {name}")
|
|
print()
|
|
|
|
processed = 0
|
|
failed = 0
|
|
|
|
for action_name in filtered_actions:
|
|
try:
|
|
rules = self.generate_rules_for_action(action_name)
|
|
if rules:
|
|
self.write_rules_file(action_name, rules)
|
|
processed += 1
|
|
else:
|
|
print(f"⚠️ Failed to generate rules for {action_name}")
|
|
failed += 1
|
|
except Exception as error:
|
|
print(f"❌ Error processing {action_name}: {error}")
|
|
failed += 1
|
|
|
|
print()
|
|
print("📊 Summary:")
|
|
print(f" - Processed: {processed}")
|
|
print(f" - Failed: {failed}")
|
|
coverage = (
|
|
round((processed / (processed + failed)) * 100) if (processed + failed) > 0 else 0
|
|
)
|
|
print(f" - Coverage: {coverage}%")
|
|
|
|
if not self.dry_run and processed > 0:
|
|
print()
|
|
print(
|
|
"✨ Validation rules updated! Run 'git diff */rules.yml' to review changes.",
|
|
)
|
|
|
|
def validate_rules_files(self) -> bool:
|
|
"""Validate existing rules files"""
|
|
print("🔍 Validating existing rules files...")
|
|
|
|
# Find all rules.yml files in action directories
|
|
rules_files = []
|
|
for action_dir in self.actions_dir.iterdir():
|
|
if action_dir.is_dir() and not action_dir.name.startswith("."):
|
|
rules_file = action_dir / "rules.yml"
|
|
if rules_file.exists():
|
|
rules_files.append(rules_file)
|
|
|
|
valid = 0
|
|
invalid = 0
|
|
|
|
for rules_file in rules_files:
|
|
try:
|
|
with rules_file.open(encoding="utf-8") as f:
|
|
content = f.read()
|
|
rules = yaml.safe_load(content)
|
|
|
|
# Basic validation
|
|
required = ["action", "required_inputs", "optional_inputs", "conventions"]
|
|
missing = [field for field in required if field not in rules]
|
|
|
|
if missing:
|
|
print(f"⚠️ {rules_file.name}: Missing fields: {', '.join(missing)}")
|
|
invalid += 1
|
|
else:
|
|
valid += 1
|
|
except Exception as error:
|
|
print(f"❌ {rules_file.name}: {error}")
|
|
invalid += 1
|
|
|
|
print(f"✅ Validation complete: {valid} valid, {invalid} invalid")
|
|
return invalid == 0
|
|
|
|
|
|
def main() -> None:
|
|
"""CLI handling"""
|
|
parser = argparse.ArgumentParser(
|
|
description="Automatically generates validation rules for GitHub Actions",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python update-validators.py --dry-run
|
|
python update-validators.py --action csharp-publish
|
|
python update-validators.py --validate
|
|
""",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show what would be generated without writing files",
|
|
)
|
|
parser.add_argument("--action", metavar="NAME", help="Generate rules for specific action only")
|
|
parser.add_argument("--validate", action="store_true", help="Validate existing rules files")
|
|
|
|
args = parser.parse_args()
|
|
|
|
generator = ValidationRuleGenerator(dry_run=args.dry_run, specific_action=args.action)
|
|
|
|
if args.validate:
|
|
success = generator.validate_rules_files()
|
|
sys.exit(0 if success else 1)
|
|
else:
|
|
generator.generate_rules()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|