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