diff --git a/.github/workflows/action-security.yml b/.github/workflows/action-security.yml index 96650df..049d633 100644 --- a/.github/workflows/action-security.yml +++ b/.github/workflows/action-security.yml @@ -41,7 +41,7 @@ jobs: - name: Check Required Configurations id: check-configs - shell: bash + shell: sh run: | # Initialize all flags as false { @@ -87,7 +87,7 @@ jobs: - name: Verify SARIF files id: verify-sarif - shell: bash + shell: sh run: | # Initialize outputs { diff --git a/.github/workflows/issue-stats.yml b/.github/workflows/issue-stats.yml index 77c019c..6035243 100644 --- a/.github/workflows/issue-stats.yml +++ b/.github/workflows/issue-stats.yml @@ -17,7 +17,7 @@ jobs: pull-requests: read steps: - name: Get dates for last month - shell: bash + shell: sh run: | # Calculate the first day of the previous month first_day=$(date -d "last month" +%Y-%m-01) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index a543bf9..0e599a3 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -47,6 +47,7 @@ concurrency: permissions: contents: read + packages: read # Required for private dependencies jobs: megalinter: @@ -56,8 +57,10 @@ jobs: permissions: actions: write + checks: write # Create and update check runs contents: write issues: write + packages: read # Access private packages pull-requests: write security-events: write statuses: write @@ -76,7 +79,7 @@ jobs: - name: Check MegaLinter Results id: check-results if: always() - shell: bash + shell: sh run: | printf '%s\n' "status=success" >> "$GITHUB_OUTPUT" @@ -108,7 +111,7 @@ jobs: - name: Prepare Git for Fixes if: steps.ml.outputs.has_updated_sources == 1 - shell: bash + shell: sh run: | sudo chown -Rc $UID .git/ git config --global user.name "fiximus" @@ -193,7 +196,7 @@ jobs: - name: Cleanup if: always() - shell: bash + shell: sh run: |- # Remove temporary files but keep reports find . -type f -name "megalinter.*" ! -name "megalinter-reports" -delete || true diff --git a/.github/workflows/security-suite.yml b/.github/workflows/security-suite.yml index 4c452fa..1be501c 100644 --- a/.github/workflows/security-suite.yml +++ b/.github/workflows/security-suite.yml @@ -43,7 +43,7 @@ jobs: - name: Fetch PR Base run: | - set -euo pipefail + set -eu # Fetch the base ref from base repository with authentication (works for private repos and forked PRs) # Using ref instead of SHA because git fetch requires ref names, not raw commit IDs # Use authenticated URL to avoid 403/404 on private repositories @@ -97,6 +97,9 @@ jobs: const fs = require('fs'); const path = require('path'); + // Unique marker to identify our bot comment + const SECURITY_COMMENT_MARKER = ''; + const findings = { permissions: [], actions: [], @@ -230,11 +233,40 @@ jobs: if (findings.permissions.length > 0) { const permSection = ['## ๐Ÿ” GitHub Actions Permissions Changes']; findings.permissions.forEach(change => { - permSection.push(`**${change.file}**:`); - permSection.push('```diff'); - permSection.push(`- ${change.old}`); - permSection.push(`+ ${change.new}`); - permSection.push('```'); + permSection.push(`\n**${change.file}**:`); + + // Parse permissions into lines + const oldLines = (change.old === 'None' ? [] : change.old.split('\n').map(l => l.trim()).filter(Boolean)); + const newLines = (change.new === 'None' ? [] : change.new.split('\n').map(l => l.trim()).filter(Boolean)); + + // Create sets for comparison + const oldSet = new Set(oldLines); + const newSet = new Set(newLines); + + // Find added, removed, and unchanged + const removed = oldLines.filter(l => !newSet.has(l)); + const added = newLines.filter(l => !oldSet.has(l)); + const unchanged = oldLines.filter(l => newSet.has(l)); + + // Only show diff if there are actual changes + if (removed.length > 0 || added.length > 0) { + permSection.push('```diff'); + + // Show removed permissions + removed.forEach(line => permSection.push(`- ${line}`)); + + // Show added permissions + added.forEach(line => permSection.push(`+ ${line}`)); + + permSection.push('```'); + + // Summary for context + if (unchanged.length > 0 && unchanged.length <= 3) { + permSection.push(`
Unchanged (${unchanged.length})\n\n${unchanged.map(l => `- ${l}`).join('\n')}\n
`); + } else if (unchanged.length > 3) { + permSection.push(`*${unchanged.length} permissions unchanged*`); + } + } }); sections.push(permSection.join('\n')); } @@ -314,15 +346,15 @@ jobs: // Export critical count as output core.setOutput('critical_issues', criticalCount.toString()); - // Generate final comment - let comment = '## โœ… Security Analysis\n\n'; + // Generate final comment with unique marker + let comment = `${SECURITY_COMMENT_MARKER}\n## โœ… Security Analysis\n\n`; if (sections.length === 0) { comment += 'No security issues detected in this PR.'; } else { comment += sections.join('\n\n'); } - // Find existing security comment + // Find existing security comment using unique marker const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, @@ -330,8 +362,7 @@ jobs: }); const existingComment = comments.find(comment => - comment.body.includes('Security Analysis') || - comment.body.includes('๐Ÿ” GitHub Actions Permissions') + comment.body && comment.body.includes(SECURITY_COMMENT_MARKER) ); if (existingComment) { diff --git a/.github/workflows/test-actions.yml b/.github/workflows/test-actions.yml index 64dc186..d2cb323 100644 --- a/.github/workflows/test-actions.yml +++ b/.github/workflows/test-actions.yml @@ -55,10 +55,10 @@ jobs: uses: ./.github/actions/setup-test-environment - name: Run unit tests - shell: bash + shell: sh run: | - if [[ "${{ github.event.inputs.test-type }}" == "unit" || "${{ github.event.inputs.test-type }}" == "all" || -z "${{ github.event.inputs.test-type }}" ]]; then - if [[ -n "${{ github.event.inputs.action-filter }}" ]]; then + if [ "${{ github.event.inputs.test-type }}" = "unit" ] || [ "${{ github.event.inputs.test-type }}" = "all" ] || [ -z "${{ github.event.inputs.test-type }}" ]; then + if [ -n "${{ github.event.inputs.action-filter }}" ]; then make test-action ACTION="${{ github.event.inputs.action-filter }}" else make test-unit @@ -68,7 +68,7 @@ jobs: fi - name: Generate SARIF report - shell: bash + shell: sh run: ./_tests/run-tests.sh --type unit --format sarif if: always() @@ -107,10 +107,10 @@ jobs: install-act: 'true' - name: Run integration tests - shell: bash + shell: sh run: | - if [[ "${{ github.event.inputs.test-type }}" == "integration" || "${{ github.event.inputs.test-type }}" == "all" || -z "${{ github.event.inputs.test-type }}" ]]; then - if [[ -n "${{ github.event.inputs.action-filter }}" ]]; then + if [ "${{ github.event.inputs.test-type }}" = "integration" ] || [ "${{ github.event.inputs.test-type }}" = "all" ] || [ -z "${{ github.event.inputs.test-type }}" ]; then + if [ -n "${{ github.event.inputs.action-filter }}" ]; then ./_tests/run-tests.sh --type integration --action "${{ github.event.inputs.action-filter }}" else make test-integration @@ -122,7 +122,7 @@ jobs: - name: Check for integration test reports id: check-integration-reports if: always() - shell: bash + shell: sh run: | if [ -d "_tests/reports/integration" ] && [ -n "$(find _tests/reports/integration -type f 2>/dev/null)" ]; then printf '%s\n' "reports-found=true" >> $GITHUB_OUTPUT @@ -176,9 +176,9 @@ jobs: - name: Comment coverage summary if: github.event_name == 'pull_request' - shell: bash + shell: sh run: | - if [[ -f _tests/coverage/summary.json ]]; then + if [ -f _tests/coverage/summary.json ]; then coverage=$(jq -r '.coverage_percent' _tests/coverage/summary.json) tested_actions=$(jq -r '.tested_actions' _tests/coverage/summary.json) total_actions=$(jq -r '.total_actions' _tests/coverage/summary.json) @@ -240,7 +240,7 @@ jobs: extra_args: --debug --only-verified - name: Scan shell scripts - shell: bash + shell: sh run: | # Scan all shell scripts in _tests/ find _tests/ -name "*.sh" -exec shellcheck -x {} \; || { @@ -270,7 +270,7 @@ jobs: path: test-results/ - name: Generate test summary - shell: bash + shell: sh run: | { echo "## ๐Ÿงช Test Results Summary" @@ -278,20 +278,20 @@ jobs: # Unit tests unit_count=$(find test-results -type f -path "*/unit/*.txt" | wc -l || true) - if [[ "${unit_count:-0}" -gt 0 ]]; then + if [ "${unit_count:-0}" -gt 0 ]; then echo "- **Unit Tests**: $unit_count action(s) tested" fi # Integration tests integration_count=$(find test-results -type f -path "*/integration/*.txt" | wc -l || true) - if [[ "${integration_count:-0}" -gt 0 ]]; then + if [ "${integration_count:-0}" -gt 0 ]; then echo "- **Integration Tests**: $integration_count action(s) tested" fi echo "" unit_success="${{ needs.unit-tests.result == 'success' }}" integration_ok="${{ needs.integration-tests.result == 'success' || needs.integration-tests.result == 'skipped' }}" - if [[ "$unit_success" == "true" && "$integration_ok" == "true" ]]; then + if [ "$unit_success" = "true" ] && [ "$integration_ok" = "true" ]; then status="โœ… All tests passed" else status="โŒ Some tests failed" @@ -307,7 +307,7 @@ jobs: - name: Fail if tests failed if: needs.unit-tests.result == 'failure' || needs.integration-tests.result == 'failure' - shell: bash + shell: sh run: |- echo "โŒ One or more test jobs failed" exit 1 diff --git a/.gitignore b/.gitignore index 1bbadfc..3e920b6 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,4 @@ tests/reports/**/*.json !uv.lock code-scanning-report-* *.sarif +TODO.md diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..fcd6df1 --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,2 @@ +node_modules/ +.worktrees/ diff --git a/.mega-linter.yml b/.mega-linter.yml index 6fe009e..88626fa 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -32,4 +32,4 @@ JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.json TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json FILTER_REGEX_EXCLUDE: > - (node_modules|\.automation/test|docs/json-schemas) + (node_modules|\.automation/test|docs/json-schemas|\.worktrees) diff --git a/.prettierignore b/.prettierignore index a7871f6..dcfa7a3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ .github/renovate.json .venv +.worktrees/ diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md index a79e279..b609af4 100644 --- a/.serena/memories/code_style_conventions.md +++ b/.serena/memories/code_style_conventions.md @@ -45,6 +45,32 @@ - macOS/Windows runners may lack Linux tools (jq, bc, specific GNU utils) - Always provide fallbacks or explicit installation steps +11. **NEVER** use `set-git-config` action - use direct git config or action parameters instead + - Git-related actions (`peter-evans/create-pull-request`, `stefanzweifel/git-auto-commit-action`) handle their own auth + - For direct git commands, configure git manually when needed: `git config user.name/user.email` + - Pattern for actions with git-auto-commit: + + ```yaml + - uses: stefanzweifel/git-auto-commit-action@SHA + with: + commit_user_name: ${{ inputs.username }} + commit_user_email: ${{ inputs.email }} + ``` + + - Pattern for actions with direct git commands: + + ```yaml + - shell: bash + run: | + git config user.name "${{ inputs.username }}" + git config user.email "${{ inputs.email }}" + git add . + git commit -m "message" + git push + ``` + + - Rationale: Avoids complexity, matches proven workflow pattern, no credential conflicts + ## EditorConfig Rules (.editorconfig) **CRITICAL**: EditorConfig violations are blocking errors and must be fixed always. diff --git a/.yamlignore b/.yamlignore index 1d17dae..435b6df 100644 --- a/.yamlignore +++ b/.yamlignore @@ -1 +1,2 @@ .venv +.worktrees/ diff --git a/.yamllint.yml b/.yamllint.yml index 065bc60..975adb6 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -1,6 +1,10 @@ --- extends: default +ignore: | + node_modules/ + .worktrees/ + rules: line-length: max: 200 diff --git a/CLAUDE.md b/CLAUDE.md index 60642b7..f56c435 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ **Core Memories** (read first for project understanding): -- `repository_overview` โ€“ 43 actions, categories, structure, status +- `repository_overview` โ€“ 30 actions, categories, structure, status - `validator_system` โ€“ Validation architecture, components, usage patterns - `development_standards` โ€“ Quality rules, workflows, security, completion checklist @@ -71,11 +71,11 @@ Flat structure. Each action self-contained with `action.yml`. -**43 Actions**: Setup (node-setup, set-git-config, php-version-detect, python-version-detect, python-version-detect-v2, go-version-detect, dotnet-version-detect), Utilities (version-file-parser, version-validator), -Linting (ansible-lint-fix, biome-check, biome-fix, csharp-lint-check, eslint-check, eslint-fix, go-lint, pr-lint, pre-commit, prettier-check, prettier-fix, python-lint-fix, terraform-lint-fix), +**30 Actions**: Setup (node-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), -Publishing (npm-publish, docker-publish, docker-publish-gh, docker-publish-hub, csharp-publish), -Repository (github-release, release-monthly, sync-labels, stale, compress-images, common-cache, common-file-check, common-retry, codeql-analysis), +Publishing (npm-publish, docker-publish, csharp-publish), +Repository (release-monthly, sync-labels, stale, compress-images, common-cache, codeql-analysis), Validation (validate-inputs) ## Commands diff --git a/Makefile b/Makefile index a470c88..7138446 100644 --- a/Makefile +++ b/Makefile @@ -217,7 +217,7 @@ check-version-refs: ## List all current SHA-pinned action references # Formatting targets format-markdown: ## Format markdown files @echo "$(BLUE)๐Ÿ“ Formatting markdown...$(RESET)" - @if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" 2>/dev/null; then \ + @if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" "#.worktrees" 2>/dev/null; then \ echo "$(GREEN)โœ… Markdown formatted$(RESET)"; \ else \ echo "$(YELLOW)โš ๏ธ Markdown formatting issues found$(RESET)" | tee -a $(LOG_FILE); \ @@ -269,7 +269,7 @@ format-python: ## Format Python files with ruff # Linting targets lint-markdown: ## Lint markdown files @echo "$(BLUE)๐Ÿ” Linting markdown...$(RESET)" - @if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules"; then \ + @if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" "#.worktrees"; then \ echo "$(GREEN)โœ… Markdown linting passed$(RESET)"; \ else \ echo "$(YELLOW)โš ๏ธ Markdown linting issues found$(RESET)" | tee -a $(LOG_FILE); \ @@ -291,7 +291,7 @@ lint-shell: ## Lint shell scripts echo " or: apt-get install shellcheck"; \ exit 1; \ fi - @if find . -name "*.sh" -not -path "./_tests/*" -exec shellcheck -x {} +; then \ + @if find . -name "*.sh" -not -path "./_tests/*" -not -path "./.worktrees/*" -exec shellcheck -x {} +; then \ echo "$(GREEN)โœ… Shell linting passed$(RESET)"; \ else \ echo "$(RED)โŒ Shell linting issues found$(RESET)"; \ @@ -340,7 +340,7 @@ check-tools: ## Check if required tools are available check-syntax: ## Check syntax of shell scripts and YAML files @echo "$(BLUE)๐Ÿ” Checking syntax...$(RESET)" @failed=0; \ - find . -name "*.sh" -print0 | while IFS= read -r -d '' file; do \ + find . -name "*.sh" -not -path "./_tests/*" -not -path "./.worktrees/*" -print0 | while IFS= read -r -d '' file; do \ if ! bash -n "$$file" 2>&1; then \ echo "$(RED)โŒ Syntax error in $$file$(RESET)" >&2; \ failed=1; \ @@ -721,7 +721,8 @@ docker-all: docker-build docker-test docker-push ## Build, test, and push Docker watch: ## Watch files and auto-format on changes (requires entr) @if command -v entr >/dev/null 2>&1; then \ echo "$(BLUE)๐Ÿ‘€ Watching for changes... (press Ctrl+C to stop)$(RESET)"; \ - find . -name "*.yml" -o -name "*.yaml" -o -name "*.md" -o -name "*.sh" | \ + find . \( -name "*.yml" -o -name "*.yaml" -o -name "*.md" -o -name "*.sh" \) \ + -not -path "./_tests/*" -not -path "./.worktrees/*" -not -path "./node_modules/*" | \ entr -c $(MAKE) format; \ else \ echo "$(RED)โŒ Error: entr not found. Install with: brew install entr$(RESET)"; \ diff --git a/README.md b/README.md index a808509..fe57693 100644 --- a/README.md +++ b/README.md @@ -22,94 +22,71 @@ Each action is fully self-contained and can be used independently in any GitHub ## ๐Ÿ“š Action Catalog -This repository contains **44 reusable GitHub Actions** for CI/CD automation. +This repository contains **30 reusable GitHub Actions** for CI/CD automation. -### Quick Reference (44 Actions) +### Quick Reference (30 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-check`][biome-check] | Linting | Run Biome check on the repository | Token auth, Outputs | -| โœ… | [`biome-fix`][biome-fix] | Linting | Run Biome fix on the repository | 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 | -| ๐Ÿ“ฆ | [`common-file-check`][common-file-check] | Repository | A reusable action to check if a specific file or type of fil... | Outputs | -| ๐Ÿ”„ | [`common-retry`][common-retry] | Repository | Standardized retry utility for network operations and flaky ... | 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-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 | -| ๐Ÿ“ฆ | [`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 | Publish a Docker image to GitHub Packages and Docker Hub. | Auto-detection, Token auth, Outputs | -| ๐Ÿ“ฆ | [`docker-publish-gh`][docker-publish-gh] | Publishing | Publishes a Docker image to GitHub Packages with advanced se... | Caching, Auto-detection, Token auth, Outputs | -| ๐Ÿ“ฆ | [`docker-publish-hub`][docker-publish-hub] | Publishing | Publishes a Docker image to Docker Hub with enhanced securit... | Caching, Auto-detection, Outputs | -| ๐Ÿ“ | [`dotnet-version-detect`][dotnet-version-detect] | Setup | Detects .NET SDK version from global.json or defaults to a s... | Auto-detection, Token auth, Outputs | -| โœ… | [`eslint-check`][eslint-check] | Linting | Run ESLint check on the repository with advanced configurati... | Caching, Token auth, Outputs | -| ๐Ÿ“ | [`eslint-fix`][eslint-fix] | Linting | Fixes ESLint violations in a project. | Token auth, Outputs | -| ๐Ÿท๏ธ | [`github-release`][github-release] | Repository | Creates a GitHub release with a version and changelog. | 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 | -| ๐Ÿ“ | [`go-version-detect`][go-version-detect] | Setup | Detects the Go version from the project's go.mod file or def... | Auto-detection, Token auth, Outputs | -| ๐Ÿ–ฅ๏ธ | [`node-setup`][node-setup] | Setup | Sets up Node.js env with advanced version management, cachin... | Caching, 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 | -| ๐Ÿ“ | [`php-version-detect`][php-version-detect] | Setup | Detects the PHP version from the project's composer.json, ph... | 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-check`][prettier-check] | Linting | Run Prettier check on the repository with advanced configura... | Caching, Token auth, Outputs | -| ๐Ÿ“ | [`prettier-fix`][prettier-fix] | Linting | Run Prettier to fix code style violations | 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 | -| ๐Ÿ“ | [`python-version-detect`][python-version-detect] | Setup | Detects Python version from project configuration files or d... | Auto-detection, Token auth, Outputs | -| ๐Ÿ“ | [`python-version-detect-v2`][python-version-detect-v2] | Setup | Detects Python version from project configuration files usin... | Auto-detection, Token auth, Outputs | -| ๐Ÿ“ฆ | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs | -| ๐Ÿ”€ | [`set-git-config`][set-git-config] | Setup | Sets Git configuration for actions. | 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 | -| โœ… | [`version-validator`][version-validator] | Utilities | Validates and normalizes version strings using customizable ... | Auto-detection, Outputs | +| 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 | +| ๐Ÿ›ก๏ธ | [`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-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 | +| ๐Ÿ“ฆ | [`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 | +| ๐Ÿ“ฆ | [`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 | +| โœ… | [`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 | +| ๐Ÿ“ | [`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 (7 actions) +#### ๐Ÿ”ง Setup (2 actions) -| Action | Description | Languages | Features | -|:----------------------------------------------------------|:------------------------------------------------------|:--------------------------------|:---------------------------------------------| -| ๐Ÿ“ [`dotnet-version-detect`][dotnet-version-detect] | Detects .NET SDK version from global.json or defau... | C#, .NET | Auto-detection, Token auth, Outputs | -| ๐Ÿ“ [`go-version-detect`][go-version-detect] | Detects the Go version from the project's go.mod f... | Go | Auto-detection, Token auth, Outputs | -| ๐Ÿ–ฅ๏ธ [`node-setup`][node-setup] | Sets up Node.js env with advanced version manageme... | Node.js, JavaScript, TypeScript | Caching, Auto-detection, Token auth, Outputs | -| ๐Ÿ“ [`php-version-detect`][php-version-detect] | Detects the PHP version from the project's compose... | PHP | Auto-detection, Token auth, Outputs | -| ๐Ÿ“ [`python-version-detect`][python-version-detect] | Detects Python version from project configuration ... | Python | Auto-detection, Token auth, Outputs | -| ๐Ÿ“ [`python-version-detect-v2`][python-version-detect-v2] | Detects Python version from project configuration ... | Python | Auto-detection, Token auth, Outputs | -| ๐Ÿ”€ [`set-git-config`][set-git-config] | Sets Git configuration for actions. | - | Token auth, Outputs | +| 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 | -#### ๐Ÿ› ๏ธ Utilities (3 actions) +#### ๐Ÿ› ๏ธ Utilities (2 actions) -| Action | Description | Languages | Features | -|:------------------------------------------------|:------------------------------------------------------|:----------------------------|:------------------------| -| ๐Ÿ”€ [`action-versioning`][action-versioning] | Automatically update SHA-pinned action references ... | - | Token auth, Outputs | -| ๐Ÿ“ฆ [`version-file-parser`][version-file-parser] | Universal parser for common version detection file... | Multiple Languages | Auto-detection, Outputs | -| โœ… [`version-validator`][version-validator] | Validates and normalizes version strings using cus... | Semantic Versioning, CalVer | Auto-detection, Outputs | +| 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 | -#### ๐Ÿ“ Linting (13 actions) +#### ๐Ÿ“ 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-check`][biome-check] | Run Biome check on the repository | JavaScript, TypeScript, JSON | Token auth, Outputs | -| โœ… [`biome-fix`][biome-fix] | Run Biome fix on the repository | JavaScript, TypeScript, JSON | Token auth, Outputs | +| โœ… [`biome-lint`][biome-lint] | Run Biome linter in check or fix mode | JavaScript, TypeScript, JSON | 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-check`][eslint-check] | Run ESLint check on the repository with advanced c... | JavaScript, TypeScript | Caching, Token auth, Outputs | -| ๐Ÿ“ [`eslint-fix`][eslint-fix] | Fixes ESLint violations in a project. | JavaScript, TypeScript | Token auth, Outputs | +| โœ… [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, 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-check`][prettier-check] | Run Prettier check on the repository with advanced... | JavaScript, TypeScript, Markdown, YAML, JSON | Caching, Token auth, Outputs | -| ๐Ÿ“ [`prettier-fix`][prettier-fix] | Run Prettier to fix code style violations | JavaScript, TypeScript, Markdown, YAML, JSON | 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 | | ๐Ÿ“ [`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 | @@ -129,29 +106,24 @@ This repository contains **44 reusable GitHub Actions** for CI/CD automation. | ๐Ÿ“ฆ [`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 (5 actions) +#### ๐Ÿš€ 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] | Publish a Docker image to GitHub Packages and Dock... | Docker | Auto-detection, Token auth, Outputs | -| ๐Ÿ“ฆ [`docker-publish-gh`][docker-publish-gh] | Publishes a Docker image to GitHub Packages with a... | Docker | Caching, Auto-detection, Token auth, Outputs | -| ๐Ÿ“ฆ [`docker-publish-hub`][docker-publish-hub] | Publishes a Docker image to Docker Hub with enhanc... | Docker | Caching, Auto-detection, 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 | 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 | -#### ๐Ÿ“ฆ Repository (9 actions) +#### ๐Ÿ“ฆ Repository (6 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, Outputs | -| ๐Ÿ“ฆ [`common-file-check`][common-file-check] | A reusable action to check if a specific file or t... | - | Outputs | -| ๐Ÿ”„ [`common-retry`][common-retry] | Standardized retry utility for network operations ... | - | Outputs | -| ๐Ÿ–ผ๏ธ [`compress-images`][compress-images] | Compress images on demand (workflow_dispatch), and... | - | Token auth, Outputs | -| ๐Ÿท๏ธ [`github-release`][github-release] | Creates a GitHub release with a version and change... | - | Outputs | -| ๐Ÿ“ฆ [`release-monthly`][release-monthly] | Creates a release for the current month, increment... | - | Token auth, Outputs | -| ๐Ÿ“ฆ [`stale`][stale] | A GitHub Action to close stale issues and pull req... | - | Token auth, Outputs | -| ๐Ÿท๏ธ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | YAML, GitHub | Token auth, Outputs | +| 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 | +| ๐Ÿท๏ธ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | YAML, GitHub | Token auth, Outputs | #### โœ… Validation (1 action) @@ -161,83 +133,71 @@ This repository contains **44 reusable GitHub Actions** for CI/CD automation. ### Feature Matrix -| Action | Caching | Auto-detection | Token auth | Outputs | -|:-------------------------------------------------------|:-------:|:--------------:|:----------:|:-------:| -| [`action-versioning`][action-versioning] | - | - | โœ… | โœ… | -| [`ansible-lint-fix`][ansible-lint-fix] | - | - | โœ… | โœ… | -| [`biome-check`][biome-check] | - | - | โœ… | โœ… | -| [`biome-fix`][biome-fix] | - | - | โœ… | โœ… | -| [`codeql-analysis`][codeql-analysis] | - | โœ… | โœ… | โœ… | -| [`common-cache`][common-cache] | โœ… | - | - | โœ… | -| [`common-file-check`][common-file-check] | - | - | - | โœ… | -| [`common-retry`][common-retry] | - | - | - | โœ… | -| [`compress-images`][compress-images] | - | - | โœ… | โœ… | -| [`csharp-build`][csharp-build] | - | โœ… | โœ… | โœ… | -| [`csharp-lint-check`][csharp-lint-check] | - | โœ… | โœ… | โœ… | -| [`csharp-publish`][csharp-publish] | - | โœ… | โœ… | โœ… | -| [`docker-build`][docker-build] | โœ… | โœ… | โœ… | โœ… | -| [`docker-publish`][docker-publish] | - | โœ… | โœ… | โœ… | -| [`docker-publish-gh`][docker-publish-gh] | โœ… | โœ… | โœ… | โœ… | -| [`docker-publish-hub`][docker-publish-hub] | โœ… | โœ… | - | โœ… | -| [`dotnet-version-detect`][dotnet-version-detect] | - | โœ… | โœ… | โœ… | -| [`eslint-check`][eslint-check] | โœ… | - | โœ… | โœ… | -| [`eslint-fix`][eslint-fix] | - | - | โœ… | โœ… | -| [`github-release`][github-release] | - | - | - | โœ… | -| [`go-build`][go-build] | โœ… | โœ… | โœ… | โœ… | -| [`go-lint`][go-lint] | โœ… | - | โœ… | โœ… | -| [`go-version-detect`][go-version-detect] | - | โœ… | โœ… | โœ… | -| [`node-setup`][node-setup] | โœ… | โœ… | โœ… | โœ… | -| [`npm-publish`][npm-publish] | - | - | โœ… | โœ… | -| [`php-composer`][php-composer] | - | โœ… | โœ… | โœ… | -| [`php-laravel-phpunit`][php-laravel-phpunit] | - | โœ… | โœ… | โœ… | -| [`php-tests`][php-tests] | - | - | โœ… | โœ… | -| [`php-version-detect`][php-version-detect] | - | โœ… | โœ… | โœ… | -| [`pr-lint`][pr-lint] | โœ… | โœ… | โœ… | โœ… | -| [`pre-commit`][pre-commit] | - | โœ… | โœ… | โœ… | -| [`prettier-check`][prettier-check] | โœ… | - | โœ… | โœ… | -| [`prettier-fix`][prettier-fix] | - | - | โœ… | โœ… | -| [`python-lint-fix`][python-lint-fix] | โœ… | โœ… | โœ… | โœ… | -| [`python-version-detect`][python-version-detect] | - | โœ… | โœ… | โœ… | -| [`python-version-detect-v2`][python-version-detect-v2] | - | โœ… | โœ… | โœ… | -| [`release-monthly`][release-monthly] | - | - | โœ… | โœ… | -| [`set-git-config`][set-git-config] | - | - | โœ… | โœ… | -| [`stale`][stale] | - | - | โœ… | โœ… | -| [`sync-labels`][sync-labels] | - | - | โœ… | โœ… | -| [`terraform-lint-fix`][terraform-lint-fix] | - | - | โœ… | โœ… | -| [`validate-inputs`][validate-inputs] | - | - | โœ… | โœ… | -| [`version-file-parser`][version-file-parser] | - | โœ… | - | โœ… | -| [`version-validator`][version-validator] | - | โœ… | - | โœ… | +| Action | Caching | Auto-detection | Token auth | Outputs | +|:-----------------------------------------------------|:-------:|:--------------:|:----------:|:-------:| +| [`action-versioning`][action-versioning] | - | - | โœ… | โœ… | +| [`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-lint-check`][csharp-lint-check] | - | โœ… | โœ… | โœ… | +| [`csharp-publish`][csharp-publish] | - | โœ… | โœ… | โœ… | +| [`docker-build`][docker-build] | โœ… | โœ… | โœ… | โœ… | +| [`docker-publish`][docker-publish] | - | - | โœ… | โœ… | +| [`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] | - | - | โœ… | โœ… | +| [`pr-lint`][pr-lint] | โœ… | โœ… | โœ… | โœ… | +| [`pre-commit`][pre-commit] | - | โœ… | โœ… | โœ… | +| [`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 -| Language | Actions | -|:---------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| .NET | [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish], [`dotnet-version-detect`][dotnet-version-detect] | -| 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], [`dotnet-version-detect`][dotnet-version-detect] | -| C++ | [`codeql-analysis`][codeql-analysis] | -| CalVer | [`version-validator`][version-validator] | -| Conventional Commits | [`pr-lint`][pr-lint] | -| Docker | [`docker-build`][docker-build], [`docker-publish`][docker-publish], [`docker-publish-gh`][docker-publish-gh], [`docker-publish-hub`][docker-publish-hub] | -| GitHub | [`sync-labels`][sync-labels] | -| GitHub Actions | [`validate-inputs`][validate-inputs] | -| Go | [`codeql-analysis`][codeql-analysis], [`go-build`][go-build], [`go-lint`][go-lint], [`go-version-detect`][go-version-detect] | -| HCL | [`terraform-lint-fix`][terraform-lint-fix] | -| JSON | [`biome-check`][biome-check], [`biome-fix`][biome-fix], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] | -| Java | [`codeql-analysis`][codeql-analysis] | -| JavaScript | [`biome-check`][biome-check], [`biome-fix`][biome-fix], [`codeql-analysis`][codeql-analysis], [`eslint-check`][eslint-check], [`eslint-fix`][eslint-fix], [`node-setup`][node-setup], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] | -| Laravel | [`php-laravel-phpunit`][php-laravel-phpunit] | -| Markdown | [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] | -| Multiple Languages | [`pre-commit`][pre-commit], [`version-file-parser`][version-file-parser] | -| Node.js | [`node-setup`][node-setup], [`npm-publish`][npm-publish] | -| PHP | [`php-composer`][php-composer], [`php-laravel-phpunit`][php-laravel-phpunit], [`php-tests`][php-tests], [`php-version-detect`][php-version-detect] | -| Python | [`codeql-analysis`][codeql-analysis], [`pre-commit`][pre-commit], [`python-lint-fix`][python-lint-fix], [`python-version-detect`][python-version-detect], [`python-version-detect-v2`][python-version-detect-v2] | -| Ruby | [`codeql-analysis`][codeql-analysis] | -| Semantic Versioning | [`version-validator`][version-validator] | -| Terraform | [`terraform-lint-fix`][terraform-lint-fix] | -| TypeScript | [`biome-check`][biome-check], [`biome-fix`][biome-fix], [`codeql-analysis`][codeql-analysis], [`eslint-check`][eslint-check], [`eslint-fix`][eslint-fix], [`node-setup`][node-setup], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] | -| YAML | [`ansible-lint-fix`][ansible-lint-fix], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix], [`sync-labels`][sync-labels], [`validate-inputs`][validate-inputs] | -| npm | [`npm-publish`][npm-publish] | +| Language | Actions | +|:---------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| .NET | [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish], [`language-version-detect`][language-version-detect] | +| 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] | +| GitHub Actions | [`action-versioning`][action-versioning], [`release-monthly`][release-monthly], [`stale`][stale], [`validate-inputs`][validate-inputs] | +| Go | [`codeql-analysis`][codeql-analysis], [`go-build`][go-build], [`go-lint`][go-lint], [`language-version-detect`][language-version-detect] | +| HCL | [`terraform-lint-fix`][terraform-lint-fix] | +| Images | [`compress-images`][compress-images] | +| 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] | +| 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] | +| 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] | +| YAML | [`ansible-lint-fix`][ansible-lint-fix], [`prettier-lint`][prettier-lint], [`sync-labels`][sync-labels], [`validate-inputs`][validate-inputs] | +| npm | [`npm-publish`][npm-publish] | ### Action Usage @@ -261,48 +221,34 @@ All actions can be used independently in your workflows: [action-versioning]: action-versioning/README.md [ansible-lint-fix]: ansible-lint-fix/README.md -[biome-check]: biome-check/README.md -[biome-fix]: biome-fix/README.md +[biome-lint]: biome-lint/README.md [codeql-analysis]: codeql-analysis/README.md [common-cache]: common-cache/README.md -[common-file-check]: common-file-check/README.md -[common-retry]: common-retry/README.md [compress-images]: compress-images/README.md [csharp-build]: csharp-build/README.md [csharp-lint-check]: csharp-lint-check/README.md [csharp-publish]: csharp-publish/README.md [docker-build]: docker-build/README.md [docker-publish]: docker-publish/README.md -[docker-publish-gh]: docker-publish-gh/README.md -[docker-publish-hub]: docker-publish-hub/README.md -[dotnet-version-detect]: dotnet-version-detect/README.md -[eslint-check]: eslint-check/README.md -[eslint-fix]: eslint-fix/README.md -[github-release]: github-release/README.md +[eslint-lint]: eslint-lint/README.md [go-build]: go-build/README.md [go-lint]: go-lint/README.md -[go-version-detect]: go-version-detect/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 -[php-version-detect]: php-version-detect/README.md [pr-lint]: pr-lint/README.md [pre-commit]: pre-commit/README.md -[prettier-check]: prettier-check/README.md -[prettier-fix]: prettier-fix/README.md +[prettier-lint]: prettier-lint/README.md [python-lint-fix]: python-lint-fix/README.md -[python-version-detect]: python-version-detect/README.md -[python-version-detect-v2]: python-version-detect-v2/README.md [release-monthly]: release-monthly/README.md -[set-git-config]: set-git-config/README.md [stale]: stale/README.md [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 -[version-validator]: version-validator/README.md --- diff --git a/_tests/framework/utils.sh b/_tests/framework/utils.sh index 37ca55f..842076c 100755 --- a/_tests/framework/utils.sh +++ b/_tests/framework/utils.sh @@ -57,6 +57,21 @@ get_action_name() { uv run "$script_dir/../shared/validation_core.py" --name "$action_file" } +# Check if an input is required in an action.yml file +is_input_required() { + local action_file="$1" + local input_name="$2" + local script_dir + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + # Get the 'required' property for the input + local required_status + required_status=$(uv run "$script_dir/../shared/validation_core.py" --property "$action_file" "$input_name" "required") + + # Return 0 (success) if input is required, 1 (failure) if optional + [[ $required_status == "required" ]] +} + # Test input validation using Python validation module test_input_validation() { local action_dir="$1" @@ -348,5 +363,5 @@ run_action_tests() { } # Export all functions -export -f validate_action_yml get_action_inputs get_action_outputs get_action_name +export -f validate_action_yml get_action_inputs get_action_outputs get_action_name is_input_required export -f test_input_validation test_action_outputs test_external_usage measure_action_time run_action_tests diff --git a/_tests/integration/workflows/github-release-test.yml b/_tests/integration/workflows/github-release-test.yml deleted file mode 100644 index 5f9ffe7..0000000 --- a/_tests/integration/workflows/github-release-test.yml +++ /dev/null @@ -1,440 +0,0 @@ ---- -name: Integration Test - GitHub Release -on: - workflow_dispatch: - push: - paths: - - 'github-release/**' - - '_tests/integration/workflows/github-release-test.yml' - -jobs: - test-github-release-validation: - name: Test Input Validation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test invalid version format - run: | - VERSION='not.a.version' - if [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Invalid version format should have failed" - exit 1 - fi - echo "โœ“ Invalid version format correctly rejected" - - - name: Test version with alphabetic characters - run: | - VERSION='abc.def.ghi' - if [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Alphabetic version should have failed" - exit 1 - fi - echo "โœ“ Alphabetic version correctly rejected" - - - name: Test valid version formats - run: | - for version in "1.2.3" "v1.2.3" "1.0.0-alpha" "2.0.0+build"; do - if ! [[ "$version" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Valid version '$version' should have passed" - exit 1 - fi - echo "โœ“ Valid version '$version' accepted" - done - - test-github-release-version-formats: - name: Test Version Format Support - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test basic SemVer (dry run) - run: | - echo "Testing basic SemVer format: 1.2.3" - VERSION="1.2.3" - if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Valid version rejected" - exit 1 - fi - echo "โœ“ Basic SemVer format accepted" - - - name: Test SemVer with v prefix - run: | - echo "Testing SemVer with v prefix: v1.2.3" - VERSION="v1.2.3" - if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Valid version rejected" - exit 1 - fi - echo "โœ“ SemVer with v prefix accepted" - - - name: Test prerelease version - run: | - echo "Testing prerelease version: 1.0.0-alpha.1" - VERSION="1.0.0-alpha.1" - if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Valid prerelease version rejected" - exit 1 - fi - echo "โœ“ Prerelease version accepted" - - - name: Test version with build metadata - run: | - echo "Testing version with build metadata: 1.0.0+build.123" - VERSION="1.0.0+build.123" - if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Valid build metadata version rejected" - exit 1 - fi - echo "โœ“ Version with build metadata accepted" - - - name: Test complex version - run: | - echo "Testing complex version: v2.1.0-rc.1+build.456" - VERSION="v2.1.0-rc.1+build.456" - if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Valid complex version rejected" - exit 1 - fi - echo "โœ“ Complex version accepted" - - test-github-release-tool-availability: - name: Test Tool Availability Checks - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test gh CLI detection logic - run: | - # Test the logic for checking gh availability - # In actual action, this would fail if gh is not available - if command -v gh >/dev/null 2>&1; then - echo "โœ“ gh CLI is available in this environment" - gh --version - else - echo "โš ๏ธ gh CLI not available in test environment (would fail in actual action)" - echo "โœ“ Tool detection logic works correctly" - fi - - - name: Test jq detection logic - run: | - # Test the logic for checking jq availability - # In actual action, this would fail if jq is not available - if command -v jq >/dev/null 2>&1; then - echo "โœ“ jq is available in this environment" - jq --version - else - echo "โš ๏ธ jq not available in test environment (would fail in actual action)" - echo "โœ“ Tool detection logic works correctly" - fi - - - name: Test tool requirement validation - run: | - # Verify the action correctly checks for required tools - REQUIRED_TOOLS=("gh" "jq") - echo "Testing tool requirement checks..." - - for tool in "${REQUIRED_TOOLS[@]}"; do - if command -v "$tool" >/dev/null 2>&1; then - echo " โœ“ $tool: available" - else - echo " โš ๏ธ $tool: not available (action would fail at this check)" - fi - done - - echo "โœ“ Tool requirement validation logic verified" - - - name: Test gh authentication logic - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Test authentication detection logic - if [[ -n "$GITHUB_TOKEN" ]]; then - echo "โœ“ GITHUB_TOKEN environment variable is set" - else - echo "โš ๏ธ GITHUB_TOKEN not set in test environment" - fi - - # Test gh auth status check (without requiring it to pass) - if command -v gh >/dev/null 2>&1; then - if gh auth status >/dev/null 2>&1; then - echo "โœ“ gh authentication successful" - else - echo "โš ๏ธ gh auth check failed (expected without proper setup)" - fi - else - echo "โš ๏ธ gh not available, skipping auth check" - fi - - echo "โœ“ Authentication detection logic verified" - - test-github-release-changelog-validation: - name: Test Changelog Validation - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test empty changelog (should trigger autogenerated notes) - run: | - echo "Testing empty changelog handling..." - CHANGELOG="" - if [[ -n "$CHANGELOG" ]]; then - echo "โŒ ERROR: Empty string should be empty" - exit 1 - fi - echo "โœ“ Empty changelog correctly detected" - - - name: Test normal changelog - run: | - echo "Testing normal changelog..." - CHANGELOG="## Features - - Added new feature - - Improved performance" - - if [[ -z "$CHANGELOG" ]]; then - echo "โŒ ERROR: Changelog should not be empty" - exit 1 - fi - - if [[ ${#CHANGELOG} -gt 10000 ]]; then - echo "โš ๏ธ Changelog is very long" - fi - - echo "โœ“ Normal changelog processed correctly" - - - name: Test very long changelog warning - run: | - echo "Testing very long changelog..." - # Create a changelog with >10000 characters - CHANGELOG=$(printf 'A%.0s' {1..10001}) - - if [[ ${#CHANGELOG} -le 10000 ]]; then - echo "โŒ ERROR: Test changelog should be >10000 chars" - exit 1 - fi - - echo "โœ“ Long changelog warning would trigger (${#CHANGELOG} characters)" - - test-github-release-changelog-types: - name: Test Changelog Content Types - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test multiline changelog - run: | - echo "Testing multiline changelog..." - CHANGELOG="## Version 1.2.3 - - ### Features - - Feature A - - Feature B - - ### Bug Fixes - - Fix #123 - - Fix #456" - - echo "โœ“ Multiline changelog supported" - - - name: Test changelog with special characters - run: | - echo "Testing changelog with special characters..." - CHANGELOG='## Release Notes - - Special chars: $, `, \, ", '\'', !, @, #, %, &, *, (, )' - - echo "โœ“ Special characters in changelog supported" - - - name: Test changelog with markdown - run: | - echo "Testing changelog with markdown..." - CHANGELOG="## Changes - - **Bold** and *italic* text - - - [x] Task completed - - [ ] Task pending - - \`\`\`bash - echo 'code block' - \`\`\` - - | Table | Header | - |-------|--------| - | Cell | Data |" - - echo "โœ“ Markdown in changelog supported" - - test-github-release-output-format: - name: Test Output Format - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Verify output structure (mock test) - run: | - echo "Testing output structure..." - - # Check if jq is available for this test - if ! command -v jq >/dev/null 2>&1; then - echo "โš ๏ธ jq not available, skipping JSON parsing test" - echo "โœ“ Output format validation logic verified (jq would be required in actual action)" - exit 0 - fi - - # Mock release info JSON (similar to gh release view output) - RELEASE_INFO='{ - "url": "https://github.com/owner/repo/releases/tag/v1.0.0", - "id": "RE_12345", - "uploadUrl": "https://uploads.github.com/repos/owner/repo/releases/12345/assets{?name,label}" - }' - - # Extract outputs - release_url=$(echo "$RELEASE_INFO" | jq -r '.url') - release_id=$(echo "$RELEASE_INFO" | jq -r '.id') - upload_url=$(echo "$RELEASE_INFO" | jq -r '.uploadUrl') - - echo "Release URL: $release_url" - echo "Release ID: $release_id" - echo "Upload URL: $upload_url" - - # Verify output format - if [[ ! "$release_url" =~ ^https://github\.com/.*/releases/tag/.* ]]; then - echo "โŒ ERROR: Invalid release URL format" - exit 1 - fi - - if [[ ! "$release_id" =~ ^RE_.* ]]; then - echo "โŒ ERROR: Invalid release ID format" - exit 1 - fi - - if [[ ! "$upload_url" =~ ^https://uploads\.github\.com/.* ]]; then - echo "โŒ ERROR: Invalid upload URL format" - exit 1 - fi - - echo "โœ“ Output format validation passed" - - test-github-release-integration-scenarios: - name: Test Integration Scenarios - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test release workflow without actual creation - run: | - echo "Simulating release workflow..." - - # Validate version - VERSION="v1.2.3-test" - if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "โŒ ERROR: Version validation failed" - exit 1 - fi - echo "โœ“ Version validation passed" - - # Check tool availability (non-fatal for test environment) - gh_available=false - jq_available=false - - if command -v gh >/dev/null 2>&1; then - echo "โœ“ gh CLI is available" - gh_available=true - else - echo "โš ๏ธ gh not available (would fail in actual action)" - fi - - if command -v jq >/dev/null 2>&1; then - echo "โœ“ jq is available" - jq_available=true - else - echo "โš ๏ธ jq not available (would fail in actual action)" - fi - - # Create mock changelog - CHANGELOG="Test release notes" - NOTES_FILE="$(mktemp)" - printf '%s' "$CHANGELOG" > "$NOTES_FILE" - - # Verify changelog file - if [[ ! -f "$NOTES_FILE" ]]; then - echo "โŒ ERROR: Changelog file not created" - exit 1 - fi - - CONTENT=$(cat "$NOTES_FILE") - if [[ "$CONTENT" != "$CHANGELOG" ]]; then - echo "โŒ ERROR: Changelog content mismatch" - exit 1 - fi - - rm -f "$NOTES_FILE" - - echo "โœ“ Release workflow simulation passed" - - - name: Test autogenerated changelog scenario - run: | - echo "Testing autogenerated changelog scenario..." - - VERSION="v2.0.0" - CHANGELOG="" - - if [[ -z "$CHANGELOG" ]]; then - echo "โœ“ Would use --generate-notes flag" - else - echo "โœ“ Would use custom changelog" - fi - - - name: Test custom changelog scenario - run: | - echo "Testing custom changelog scenario..." - - VERSION="v2.1.0" - CHANGELOG="## Custom Release Notes - - This release includes: - - Feature X - - Bug fix Y" - - if [[ -n "$CHANGELOG" ]]; then - echo "โœ“ Would use --notes-file with custom changelog" - else - echo "โœ“ Would use --generate-notes" - fi - - integration-test-summary: - name: Integration Test Summary - runs-on: ubuntu-latest - needs: - - test-github-release-validation - - test-github-release-version-formats - - test-github-release-tool-availability - - test-github-release-changelog-validation - - test-github-release-changelog-types - - test-github-release-output-format - - test-github-release-integration-scenarios - steps: - - name: Summary - run: | - echo "==========================================" - echo "GitHub Release Integration Tests - PASSED" - echo "==========================================" - echo "" - echo "โœ“ Input validation tests" - echo "โœ“ Version format tests" - echo "โœ“ Tool availability tests" - echo "โœ“ Changelog validation tests" - echo "โœ“ Changelog content tests" - echo "โœ“ Output format tests" - echo "โœ“ Integration scenario tests" - echo "" - echo "All github-release 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 d6a66b5..3ca276b 100644 --- a/_tests/integration/workflows/lint-fix-chain-test.yml +++ b/_tests/integration/workflows/lint-fix-chain-test.yml @@ -4,10 +4,8 @@ on: workflow_dispatch: push: paths: - - 'eslint-check/**' - - 'eslint-fix/**' - - 'prettier-check/**' - - 'prettier-fix/**' + - 'eslint-lint/**' + - 'prettier-lint/**' - 'node-setup/**' - 'common-cache/**' - '_tests/integration/workflows/lint-fix-chain-test.yml' @@ -64,14 +62,15 @@ jobs: node-version: '18' working-directory: './test-project' - - name: Test eslint-check (should find errors) + - name: Test eslint-lint check mode (should find errors) id: eslint-check - uses: ./eslint-check + uses: ./eslint-lint with: + mode: 'check' working-directory: './test-project' continue-on-error: true - - name: Validate eslint-check found issues + - name: Validate eslint-lint check found issues run: | echo "ESLint check outcome: ${{ steps.eslint-check.outcome }}" echo "Error count: ${{ steps.eslint-check.outputs.error-count }}" @@ -86,23 +85,24 @@ jobs: echo "โœ… ESLint check validated" - - name: Test eslint-fix (should fix issues) + - name: Test eslint-lint fix mode (should fix issues) id: eslint-fix - uses: ./eslint-fix + uses: ./eslint-lint with: + mode: 'fix' working-directory: './test-project' token: ${{ github.token }} email: 'test@example.com' username: 'test-user' - - name: Validate eslint-fix ran + - name: Validate eslint-lint fix ran run: | - echo "Fixed count: ${{ steps.eslint-fix.outputs.fixed-count }}" - echo "Files fixed: ${{ steps.eslint-fix.outputs.files-fixed }}" + echo "Errors fixed: ${{ steps.eslint-fix.outputs.errors-fixed }}" + echo "Files changed: ${{ steps.eslint-fix.outputs.files-changed }}" # Check that fixes were attempted - if [[ -n "${{ steps.eslint-fix.outputs.fixed-count }}" ]]; then - echo "โœ… ESLint fixed ${{ steps.eslint-fix.outputs.fixed-count }} issues" + if [[ -n "${{ steps.eslint-fix.outputs.errors-fixed }}" ]]; then + echo "โœ… ESLint fixed ${{ steps.eslint-fix.outputs.errors-fixed }} issues" else echo "โš ๏ธ No fix count reported (may be expected if no fixable issues)" fi @@ -156,10 +156,11 @@ jobs: node-version: '18' working-directory: './test-prettier' - - name: Test prettier-check (should find issues) + - name: Test prettier-lint check mode (should find issues) id: prettier-check - uses: ./prettier-check + uses: ./prettier-lint with: + mode: 'check' working-directory: './test-prettier' continue-on-error: true @@ -174,16 +175,17 @@ jobs: echo "โš ๏ธ WARNING: Expected Prettier to find formatting issues" fi - - name: Test prettier-fix (should fix issues) + - name: Test prettier-lint fix mode (should fix issues) id: prettier-fix - uses: ./prettier-fix + uses: ./prettier-lint with: + mode: 'fix' working-directory: './test-prettier' token: ${{ github.token }} email: 'test@example.com' username: 'test-user' - - name: Validate prettier-fix ran + - name: Validate prettier-lint fix ran run: | echo "Prettier fix completed" @@ -261,22 +263,25 @@ jobs: - name: Run ESLint check id: lint-check - uses: ./eslint-check + uses: ./eslint-lint with: + mode: 'check' working-directory: './test-chain' continue-on-error: true - name: Run Prettier check id: format-check - uses: ./prettier-check + uses: ./prettier-lint with: + mode: 'check' working-directory: './test-chain' continue-on-error: true - name: Run ESLint fix id: lint-fix - uses: ./eslint-fix + uses: ./eslint-lint with: + mode: 'fix' working-directory: './test-chain' token: ${{ github.token }} email: 'test@example.com' @@ -284,8 +289,9 @@ jobs: - name: Run Prettier fix id: format-fix - uses: ./prettier-fix + uses: ./prettier-lint with: + mode: 'fix' working-directory: './test-chain' token: ${{ github.token }} email: 'test@example.com' diff --git a/_tests/integration/workflows/pre-commit-test.yml b/_tests/integration/workflows/pre-commit-test.yml index 136a7f9..09f06c6 100644 --- a/_tests/integration/workflows/pre-commit-test.yml +++ b/_tests/integration/workflows/pre-commit-test.yml @@ -5,7 +5,6 @@ on: push: paths: - 'pre-commit/**' - - 'set-git-config/**' - 'validate-inputs/**' - '_tests/integration/workflows/pre-commit-test.yml' diff --git a/_tests/run-tests.sh b/_tests/run-tests.sh index 798a4d5..5db0172 100755 --- a/_tests/run-tests.sh +++ b/_tests/run-tests.sh @@ -151,14 +151,14 @@ discover_actions() { if [[ $action_name == *"$ACTION_FILTER"* ]]; then actions+=("$action_name") fi - done < <(find "${TEST_ROOT}/.." -mindepth 1 -maxdepth 1 -type d -name "*-*" | sort) + done < <(find "${TEST_ROOT}/.." -mindepth 2 -maxdepth 2 -type f -name "action.yml" -exec dirname {} \; | sort) else # All actions while IFS= read -r action_dir; do local action_name action_name=$(basename "$action_dir") actions+=("$action_name") - done < <(find "${TEST_ROOT}/.." -mindepth 1 -maxdepth 1 -type d -name "*-*" | sort) + done < <(find "${TEST_ROOT}/.." -mindepth 2 -maxdepth 2 -type f -name "action.yml" -exec dirname {} \; | sort) fi log_info "Discovered ${#actions[@]} actions to test: ${actions[*]}" diff --git a/_tests/unit/action-versioning/validation.spec.sh b/_tests/unit/action-versioning/validation.spec.sh new file mode 100755 index 0000000..001b329 --- /dev/null +++ b/_tests/unit/action-versioning/validation.spec.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env shellspec +# Unit tests for action-versioning action validation and logic +# Framework is automatically loaded via spec_helper.sh + +Describe "action-versioning action" +ACTION_DIR="action-versioning" +ACTION_FILE="$ACTION_DIR/action.yml" + +Context "when validating major-version input" +It "accepts valid year-based version (vYYYY)" +When call validate_input_python "action-versioning" "major-version" "v2025" +The status should be success +End + +It "accepts valid semantic version (v1)" +When call validate_input_python "action-versioning" "major-version" "v1" +The status should be success +End + +It "accepts valid semantic version (v2)" +When call validate_input_python "action-versioning" "major-version" "v2" +The status should be success +End + +It "accepts year-based version from 2020" +When call validate_input_python "action-versioning" "major-version" "v2020" +The status should be success +End + +It "accepts year-based version for 2030" +When call validate_input_python "action-versioning" "major-version" "v2030" +The status should be success +End + +It "rejects version without v prefix" +When call validate_input_python "action-versioning" "major-version" "2025" +The status should be failure +End + +It "rejects invalid version format" +When call validate_input_python "action-versioning" "major-version" "invalid" +The status should be failure +End + +It "rejects empty version" +When call validate_input_python "action-versioning" "major-version" "" +The status should be failure +End + +It "rejects version with command injection" +When call validate_input_python "action-versioning" "major-version" "v2025; rm -rf /" +The status should be failure +End +End + +Context "when validating token input" +It "accepts valid GitHub token (classic)" +When call validate_input_python "action-versioning" "token" "ghp_123456789012345678901234567890123456" +The status should be success +End + +It "accepts valid GitHub fine-grained token" +When call validate_input_python "action-versioning" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a" +The status should be success +End + +It "accepts empty token (optional input)" +When call validate_input_python "action-versioning" "token" "" +The status should be success +End + +It "rejects invalid token format" +When call validate_input_python "action-versioning" "token" "invalid-token" +The status should be failure +End + +It "rejects token with command injection" +When call validate_input_python "action-versioning" "token" "ghp_123456789012345678901234567890123456; rm -rf /" +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 "Action Versioning" +End + +It "defines expected inputs" +When call get_action_inputs "$ACTION_FILE" +The output should include "major-version" +The output should include "token" +End + +It "defines expected outputs" +When call get_action_outputs "$ACTION_FILE" +The output should include "updated" +The output should include "commit-sha" +The output should include "needs-annual-bump" +End +End + +Context "when testing input requirements" +It "requires major-version input" +When call is_input_required "$ACTION_FILE" "major-version" +The status should be success +End + +It "has token as optional input" +When call is_input_required "$ACTION_FILE" "token" +The status should be failure +End +End + +Context "when testing security validations" +It "validates against path traversal in major-version" +When call validate_input_python "action-versioning" "major-version" "v../../etc" +The status should be failure +End + +It "validates against shell metacharacters in major-version" +When call validate_input_python "action-versioning" "major-version" "v2025|echo" +The status should be failure +End + +It "validates against command substitution in major-version" +When call validate_input_python "action-versioning" "major-version" "v\$(whoami)" +The status should be failure +End + +It "validates against path traversal in token" +When call validate_input_python "action-versioning" "token" "../../../etc/passwd" +The status should be failure +End +End +End diff --git a/_tests/unit/biome-check/validation.spec.sh b/_tests/unit/biome-check/validation.spec.sh deleted file mode 100755 index 98c933b..0000000 --- a/_tests/unit/biome-check/validation.spec.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for biome-check action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "biome-check action" - ACTION_DIR="biome-check" - ACTION_FILE="$ACTION_DIR/action.yml" - - Context "when validating token input" - It "accepts personal access token" - When call validate_input_python "biome-check" "token" "ghp_123456789012345678901234567890123456" - The status should be success - End - It "accepts organization token" - When call validate_input_python "biome-check" "token" "gho_123456789012345678901234567890123456" - The status should be success - End - It "accepts user token" - When call validate_input_python "biome-check" "token" "ghu_123456789012345678901234567890123456" - The status should be success - End - It "accepts server token" - When call validate_input_python "biome-check" "token" "ghs_123456789012345678901234567890123456" - The status should be success - End - It "accepts refresh token" - When call validate_input_python "biome-check" "token" "ghr_123456789012345678901234567890123456" - The status should be success - End - End - - Context "when validating email input" - It "accepts valid email" - When call validate_input_python "biome-check" "email" "test@example.com" - The status should be success - End - It "rejects invalid email without @" - When call validate_input_python "biome-check" "email" "testexample.com" - The status should be failure - End - It "rejects invalid email without domain" - When call validate_input_python "biome-check" "email" "test@" - The status should be failure - End - End - - Context "when validating username input" - It "accepts valid username" - When call validate_input_python "biome-check" "username" "github-actions" - The status should be success - End - It "rejects semicolon injection" - When call validate_input_python "biome-check" "username" "user;rm -rf /" - The status should be failure - End - It "rejects ampersand injection" - When call validate_input_python "biome-check" "username" "user&&malicious" - The status should be failure - End - It "rejects pipe injection" - When call validate_input_python "biome-check" "username" "user|dangerous" - The status should be failure - End - It "rejects overly long username" - When call validate_input_python "biome-check" "username" "this-username-is-definitely-too-long-for-github-maximum-length-limit" - The status should be failure - End - End - - Context "when validating max-retries input" - It "accepts valid retry count" - When call validate_input_python "biome-check" "max-retries" "5" - The status should be success - End - It "rejects zero retries" - When call validate_input_python "biome-check" "max-retries" "0" - The status should be failure - End - It "rejects negative retries" - When call validate_input_python "biome-check" "max-retries" "-1" - The status should be failure - End - It "rejects retries above limit" - When call validate_input_python "biome-check" "max-retries" "15" - The status should be failure - End - It "rejects non-numeric retries" - When call validate_input_python "biome-check" "max-retries" "invalid" - 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 "Biome Check" - End - - It "defines expected inputs" - inputs=$(get_action_inputs "$ACTION_FILE") - When call echo "$inputs" - The output should include "token" - The output should include "username" - The output should include "email" - The output should include "max-retries" - End - - It "defines expected outputs" - outputs=$(get_action_outputs "$ACTION_FILE") - When call echo "$outputs" - The output should include "check_status" - The output should include "errors_count" - The output should include "warnings_count" - End - End - - Context "when validating security" - It "rejects command injection in token" - When call validate_input_python "biome-check" "token" "ghp_123;rm -rf /" - The status should be failure - End - - It "rejects command injection in email" - When call validate_input_python "biome-check" "email" "user@domain.com;rm -rf /" - The status should be failure - End - - It "validates all inputs for injection patterns" - When call validate_input_python "biome-check" "max-retries" "3;malicious" - The status should be failure - End - End - - Context "when testing outputs" - It "produces all expected outputs consistently" - When call test_action_outputs "$ACTION_DIR" "token" "ghp_123456789012345678901234567890123456" "username" "github-actions" "email" "test@example.com" "max-retries" "3" - The status should be success - The stderr should include "Testing action outputs for: biome-check" - The stderr should include "Output test passed for: biome-check" - End - End -End diff --git a/_tests/unit/biome-fix/validation.spec.sh b/_tests/unit/biome-fix/validation.spec.sh deleted file mode 100755 index d997985..0000000 --- a/_tests/unit/biome-fix/validation.spec.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for biome-fix action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "biome-fix action" - ACTION_DIR="biome-fix" - ACTION_FILE="$ACTION_DIR/action.yml" - - Context "when validating token input" - It "accepts personal access token" - When call validate_input_python "biome-fix" "token" "ghp_123456789012345678901234567890123456" - The status should be success - End - It "accepts organization token" - When call validate_input_python "biome-fix" "token" "gho_123456789012345678901234567890123456" - The status should be success - End - It "accepts user token" - When call validate_input_python "biome-fix" "token" "ghu_123456789012345678901234567890123456" - The status should be success - End - It "accepts server token" - When call validate_input_python "biome-fix" "token" "ghs_123456789012345678901234567890123456" - The status should be success - End - It "accepts refresh token" - When call validate_input_python "biome-fix" "token" "ghr_123456789012345678901234567890123456" - The status should be success - End - End - - Context "when validating email input" - It "accepts valid email" - When call validate_input_python "biome-fix" "email" "test@example.com" - The status should be success - End - It "rejects invalid email without @" - When call validate_input_python "biome-fix" "email" "testexample.com" - The status should be failure - End - It "rejects invalid email without domain" - When call validate_input_python "biome-fix" "email" "test@" - The status should be failure - End - End - - Context "when validating username input" - It "accepts valid username" - When call validate_input_python "biome-fix" "username" "github-actions" - The status should be success - End - It "rejects semicolon injection" - When call validate_input_python "biome-fix" "username" "user;rm -rf /" - The status should be failure - End - It "rejects ampersand injection" - When call validate_input_python "biome-fix" "username" "user&&malicious" - The status should be failure - End - It "rejects pipe injection" - When call validate_input_python "biome-fix" "username" "user|dangerous" - The status should be failure - End - It "rejects overly long username" - When call validate_input_python "biome-fix" "username" "this-username-is-definitely-too-long-for-github-maximum-length-limit" - The status should be failure - End - End - - Context "when validating max-retries input" - It "accepts valid retry count" - When call validate_input_python "biome-fix" "max-retries" "5" - The status should be success - End - It "rejects zero retries" - When call validate_input_python "biome-fix" "max-retries" "0" - The status should be failure - End - It "rejects negative retries" - When call validate_input_python "biome-fix" "max-retries" "-1" - The status should be failure - End - It "rejects retries above limit" - When call validate_input_python "biome-fix" "max-retries" "15" - The status should be failure - End - It "rejects non-numeric retries" - When call validate_input_python "biome-fix" "max-retries" "invalid" - 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 "Biome Fix" - End - - It "defines expected inputs" - inputs=$(get_action_inputs "$ACTION_FILE") - When call echo "$inputs" - The output should include "token" - The output should include "username" - The output should include "email" - The output should include "max-retries" - End - - It "defines expected outputs" - outputs=$(get_action_outputs "$ACTION_FILE") - When call echo "$outputs" - The output should include "files_changed" - The output should include "fix_status" - End - End - - Context "when validating security" - It "rejects command injection in token" - When call validate_input_python "biome-fix" "token" "ghp_123;rm -rf /" - The status should be failure - End - - It "rejects command injection in email" - When call validate_input_python "biome-fix" "email" "user@domain.com;rm -rf /" - The status should be failure - End - - It "validates all inputs for injection patterns" - When call validate_input_python "biome-fix" "max-retries" "3;malicious" - The status should be failure - End - End - - Context "when testing outputs" - It "produces all expected outputs consistently" - When call test_action_outputs "$ACTION_DIR" "token" "ghp_123456789012345678901234567890123456" "username" "github-actions" "email" "test@example.com" "max-retries" "3" - The status should be success - The stderr should include "Testing action outputs for: biome-fix" - The stderr should include "Output test passed for: biome-fix" - End - End -End diff --git a/_tests/unit/biome-lint/validation.spec.sh b/_tests/unit/biome-lint/validation.spec.sh new file mode 100755 index 0000000..b79e80a --- /dev/null +++ b/_tests/unit/biome-lint/validation.spec.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env shellspec +# Unit tests for biome-lint action validation and logic +# Framework is automatically loaded via spec_helper.sh + +Describe "biome-lint action" +ACTION_DIR="biome-lint" +ACTION_FILE="$ACTION_DIR/action.yml" + +Context "when validating mode input" +It "accepts check mode" +When call validate_input_python "biome-lint" "mode" "check" +The status should be success +End + +It "accepts fix mode" +When call validate_input_python "biome-lint" "mode" "fix" +The status should be success +End + +It "accepts empty mode (uses default)" +When call validate_input_python "biome-lint" "mode" "" +The status should be success +End + +It "rejects invalid mode" +When call validate_input_python "biome-lint" "mode" "invalid" +The status should be failure +End + +It "rejects mode with command injection" +When call validate_input_python "biome-lint" "mode" "check; rm -rf /" +The status should be failure +End +End + +Context "when validating token input" +It "accepts valid GitHub token (classic)" +When call validate_input_python "biome-lint" "token" "ghp_123456789012345678901234567890123456" +The status should be success +End + +It "accepts valid GitHub fine-grained token" +When call validate_input_python "biome-lint" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a" +The status should be success +End + +It "accepts empty token (optional)" +When call validate_input_python "biome-lint" "token" "" +The status should be success +End + +It "rejects invalid token format" +When call validate_input_python "biome-lint" "token" "invalid-token" +The status should be failure +End + +It "rejects token with command injection" +When call validate_input_python "biome-lint" "token" "ghp_123456789012345678901234567890123456; echo" +The status should be failure +End +End + +Context "when validating username input" +It "accepts valid username" +When call validate_input_python "biome-lint" "username" "github-actions" +The status should be success +End + +It "accepts username with hyphens" +When call validate_input_python "biome-lint" "username" "my-bot-user" +The status should be success +End + +It "accepts empty username (uses default)" +When call validate_input_python "biome-lint" "username" "" +The status should be success +End + +It "rejects username with command injection" +When call validate_input_python "biome-lint" "username" "user; rm -rf /" +The status should be failure +End +End + +Context "when validating email input" +It "accepts valid email" +When call validate_input_python "biome-lint" "email" "github-actions@github.com" +The status should be success +End + +It "accepts email with plus sign" +When call validate_input_python "biome-lint" "email" "user+bot@example.com" +The status should be success +End + +It "accepts email with subdomain" +When call validate_input_python "biome-lint" "email" "bot@ci.example.com" +The status should be success +End + +It "accepts empty email (uses default)" +When call validate_input_python "biome-lint" "email" "" +The status should be success +End + +It "rejects invalid email format" +When call validate_input_python "biome-lint" "email" "not-an-email" +The status should be failure +End + +It "rejects email with command injection" +When call validate_input_python "biome-lint" "email" "user@example.com; rm -rf /" +The status should be failure +End +End + +Context "when validating max-retries input" +It "accepts valid retry count (default)" +When call validate_input_python "biome-lint" "max-retries" "3" +The status should be success +End + +It "accepts retry count of 1" +When call validate_input_python "biome-lint" "max-retries" "1" +The status should be success +End + +It "accepts retry count of 10" +When call validate_input_python "biome-lint" "max-retries" "10" +The status should be success +End + +It "accepts empty max-retries (uses default)" +When call validate_input_python "biome-lint" "max-retries" "" +The status should be success +End + +It "rejects negative retry count" +When call validate_input_python "biome-lint" "max-retries" "-1" +The status should be failure +End + +It "rejects non-numeric retry count" +When call validate_input_python "biome-lint" "max-retries" "abc" +The status should be failure +End + +It "rejects retry count with command injection" +When call validate_input_python "biome-lint" "max-retries" "3; echo" +The status should be failure +End +End + +Context "when validating fail-on-error input" +It "accepts true" +When call validate_input_python "biome-lint" "fail-on-error" "true" +The status should be success +End + +It "accepts false" +When call validate_input_python "biome-lint" "fail-on-error" "false" +The status should be success +End + +It "accepts empty (uses default)" +When call validate_input_python "biome-lint" "fail-on-error" "" +The status should be success +End + +It "rejects invalid boolean value" +When call validate_input_python "biome-lint" "fail-on-error" "maybe" +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 "Biome Lint" +End + +It "defines expected inputs" +When call get_action_inputs "$ACTION_FILE" +The output should include "mode" +The output should include "token" +The output should include "username" +The output should include "email" +The output should include "max-retries" +The output should include "fail-on-error" +End + +It "defines expected outputs" +When call get_action_outputs "$ACTION_FILE" +The output should include "status" +The output should include "errors_count" +The output should include "warnings_count" +The output should include "files_changed" +End +End + +Context "when testing input requirements" +It "has all inputs as optional (with defaults)" +When call is_input_required "$ACTION_FILE" "mode" +The status should be failure +End +End + +Context "when testing security validations" +It "validates against path traversal" +When call validate_input_python "biome-lint" "username" "../../../etc" +The status should be failure +End + +It "validates against shell metacharacters" +When call validate_input_python "biome-lint" "email" "user@example.com|echo" +The status should be failure +End + +It "validates against command substitution" +When call validate_input_python "biome-lint" "username" "\$(whoami)" +The status should be failure +End +End +End diff --git a/_tests/unit/common-file-check/validation.spec.sh b/_tests/unit/common-file-check/validation.spec.sh deleted file mode 100755 index b5e4543..0000000 --- a/_tests/unit/common-file-check/validation.spec.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for common-file-check action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "common-file-check action" - ACTION_DIR="common-file-check" - ACTION_FILE="$ACTION_DIR/action.yml" - - Context "when validating file-pattern input" - It "accepts simple file pattern" - When call validate_input_python "common-file-check" "file-pattern" "package.json" - The status should be success - End - It "accepts glob pattern with wildcard" - When call validate_input_python "common-file-check" "file-pattern" "*.json" - The status should be success - End - It "accepts glob pattern with question mark" - When call validate_input_python "common-file-check" "file-pattern" "test?.js" - The status should be success - End - It "accepts nested path pattern" - When call validate_input_python "common-file-check" "file-pattern" "src/**/*.ts" - The status should be success - End - It "accepts pattern with braces" - When call validate_input_python "common-file-check" "file-pattern" "*.{js,ts}" - The status should be success - End - It "accepts pattern with brackets" - When call validate_input_python "common-file-check" "file-pattern" "[A-Z]*.txt" - The status should be success - End - It "rejects empty file pattern" - When call validate_input_python "common-file-check" "file-pattern" "" - The status should be failure - End - It "rejects path traversal" - When call validate_input_python "common-file-check" "file-pattern" "../../../etc/passwd" - The status should be failure - End - It "rejects command injection" - When call validate_input_python "common-file-check" "file-pattern" "*.json;rm -rf /" - 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 "Common File Check" - End - - It "defines expected inputs" - inputs=$(get_action_inputs "$ACTION_FILE") - When call echo "$inputs" - The output should include "file-pattern" - End - - It "defines expected outputs" - outputs=$(get_action_outputs "$ACTION_FILE") - When call echo "$outputs" - The output should include "found" - End - End - - Context "when validating security" - It "validates glob patterns safely" - When call validate_input_python "common-file-check" "file-pattern" "**/*.{js,ts,json}" - The status should be success - End - - It "rejects injection in glob patterns" - When call validate_input_python "common-file-check" "file-pattern" "*.js&&malicious" - The status should be failure - End - - It "rejects pipe injection in patterns" - When call validate_input_python "common-file-check" "file-pattern" "*.js|dangerous" - The status should be failure - End - End - - Context "when testing outputs" - It "produces all expected outputs consistently" - When call test_action_outputs "$ACTION_DIR" "file-pattern" "*.json" - The status should be success - The stderr should include "Testing action outputs for: common-file-check" - The stderr should include "Output test passed for: common-file-check" - End - End -End diff --git a/_tests/unit/common-retry/validation.spec.sh b/_tests/unit/common-retry/validation.spec.sh deleted file mode 100755 index f0d0119..0000000 --- a/_tests/unit/common-retry/validation.spec.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for common-retry action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "common-retry action" -ACTION_DIR="common-retry" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating max-retries input" -It "accepts minimum value (1)" -When call validate_input_python "common-retry" "max-retries" "1" -The status should be success -End -It "accepts maximum value (10)" -When call validate_input_python "common-retry" "max-retries" "10" -The status should be success -End -It "rejects below minimum" -When call validate_input_python "common-retry" "max-retries" "0" -The status should be failure -End -It "rejects above maximum" -When call validate_input_python "common-retry" "max-retries" "11" -The status should be failure -End -It "rejects non-numeric" -When call validate_input_python "common-retry" "max-retries" "invalid" -The status should be failure -End -End - -Context "when validating retry-delay input" -It "accepts minimum value (1)" -When call validate_input_python "common-retry" "retry-delay" "1" -The status should be success -End -It "accepts maximum value (300)" -When call validate_input_python "common-retry" "retry-delay" "300" -The status should be success -End -It "rejects below minimum" -When call validate_input_python "common-retry" "retry-delay" "0" -The status should be failure -End -It "rejects above maximum" -When call validate_input_python "common-retry" "retry-delay" "301" -The status should be failure -End -End - -Context "when validating backoff-strategy input" -It "accepts linear strategy" -When call validate_input_python "common-retry" "backoff-strategy" "linear" -The status should be success -End -It "accepts exponential strategy" -When call validate_input_python "common-retry" "backoff-strategy" "exponential" -The status should be success -End -It "accepts fixed strategy" -When call validate_input_python "common-retry" "backoff-strategy" "fixed" -The status should be success -End -It "rejects invalid strategy" -When call validate_input_python "common-retry" "backoff-strategy" "invalid" -The status should be failure -End -End - -Context "when validating timeout input" -It "accepts minimum value (1)" -When call validate_input_python "common-retry" "timeout" "1" -The status should be success -End -It "accepts maximum value (3600)" -When call validate_input_python "common-retry" "timeout" "3600" -The status should be success -End -It "rejects below minimum" -When call validate_input_python "common-retry" "timeout" "0" -The status should be failure -End -It "rejects above maximum" -When call validate_input_python "common-retry" "timeout" "3601" -The status should be failure -End -End - -Context "when validating working-directory input" -It "accepts current directory" -When call validate_input_python "common-retry" "working-directory" "." -The status should be success -End -It "accepts relative path" -When call validate_input_python "common-retry" "working-directory" "src/app" -The status should be success -End -It "rejects path traversal" -When call validate_input_python "common-retry" "working-directory" "../../../etc" -The status should be failure -End -End - -Context "when validating shell input" -It "accepts bash shell" -When call validate_input_python "common-retry" "shell" "bash" -The status should be success -End -It "accepts sh shell" -When call validate_input_python "common-retry" "shell" "sh" -The status should be success -End -It "rejects zsh shell" -When call validate_input_python "common-retry" "shell" "zsh" -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 "Common Retry" -End -End - -Context "when validating security" -It "rejects command injection with semicolon" -When call validate_input_python "common-retry" "command" "value; rm -rf /" -The status should be failure -End - -It "rejects command injection with ampersand" -When call validate_input_python "common-retry" "command" "value && malicious" -The status should be failure -End - -It "accepts valid success codes" -When call validate_input_python "common-retry" "success-codes" "0,1,2" -The status should be success -End - -It "rejects success codes with injection" -When call validate_input_python "common-retry" "success-codes" "0;rm -rf /" -The status should be failure -End - -It "accepts valid retry codes" -When call validate_input_python "common-retry" "retry-codes" "1,126,127" -The status should be success -End - -It "rejects retry codes with injection" -When call validate_input_python "common-retry" "retry-codes" "1;rm -rf /" -The status should be failure -End -End - -End diff --git a/_tests/unit/docker-publish-gh/validation.spec.sh b/_tests/unit/docker-publish-gh/validation.spec.sh deleted file mode 100755 index 8f4f9bc..0000000 --- a/_tests/unit/docker-publish-gh/validation.spec.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for docker-publish-gh action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "docker-publish-gh action" -ACTION_DIR="docker-publish-gh" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating inputs" -It "accepts valid image name" -When call validate_input_python "docker-publish-gh" "image-name" "myapp" -The status should be success -End -It "accepts valid GitHub token" -When call validate_input_python "docker-publish-gh" "token" "ghp_123456789012345678901234567890123456" -The status should be success -End -It "accepts valid tags" -When call validate_input_python "docker-publish-gh" "tags" "v1.0.0,latest" -The status should be success -End -It "rejects injection in token" -When call validate_input_python "docker-publish-gh" "token" "ghp_123;malicious" -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 match pattern "*Docker*" -End -End -End diff --git a/_tests/unit/docker-publish-hub/validation.spec.sh b/_tests/unit/docker-publish-hub/validation.spec.sh deleted file mode 100755 index 863f8eb..0000000 --- a/_tests/unit/docker-publish-hub/validation.spec.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for docker-publish-hub action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "docker-publish-hub action" -ACTION_DIR="docker-publish-hub" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating inputs" -It "accepts valid image name" -When call validate_input_python "docker-publish-hub" "image-name" "myapp" -The status should be success -End -It "accepts valid username" -When call validate_input_python "docker-publish-hub" "username" "dockeruser" -The status should be success -End -It "accepts valid password" -When call validate_input_python "docker-publish-hub" "password" "secretpassword123" -The status should be success -End -It "accepts valid tags" -When call validate_input_python "docker-publish-hub" "tags" "v1.0.0,latest" -The status should be success -End -It "rejects injection in username" -When call validate_input_python "docker-publish-hub" "username" "user;malicious" -The status should be failure -End -It "rejects injection in password" -When call validate_input_python "docker-publish-hub" "password" "pass;rm -rf /" -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 match pattern "*Docker*" -End -End -End diff --git a/_tests/unit/dotnet-version-detect/validation.spec.sh b/_tests/unit/dotnet-version-detect/validation.spec.sh deleted file mode 100755 index 42e5893..0000000 --- a/_tests/unit/dotnet-version-detect/validation.spec.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for dotnet-version-detect action validation and logic -# Framework is automatically loaded via spec_helper.sh - -Describe "dotnet-version-detect action" -ACTION_DIR="dotnet-version-detect" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating default-version input" -It "accepts valid dotnet version" -When call validate_input_python "dotnet-version-detect" "default-version" "8.0" -The status should be success -End -It "accepts full semantic version" -When call validate_input_python "dotnet-version-detect" "default-version" "8.0.0" -The status should be success -End -It "accepts dotnet 6 version" -When call validate_input_python "dotnet-version-detect" "default-version" "6.0.0" -The status should be success -End -It "accepts dotnet 7 version" -When call validate_input_python "dotnet-version-detect" "default-version" "7.0.0" -The status should be success -End -It "rejects invalid version format" -When call validate_input_python "dotnet-version-detect" "default-version" "invalid" -The status should be failure -End -It "rejects version with leading zeros" -When call validate_input_python "dotnet-version-detect" "default-version" "08.0.0" -The status should be failure -End -It "rejects unsupported version" -When call validate_input_python "dotnet-version-detect" "default-version" "2.0.0" -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 "Dotnet Version Detect" -End - -It "defines expected inputs" -inputs=$(get_action_inputs "$ACTION_FILE") -When call echo "$inputs" -The output should include "default-version" -End - -It "defines expected outputs" -outputs=$(get_action_outputs "$ACTION_FILE") -When call echo "$outputs" -The output should include "dotnet-version" -End -End - -Context "when validating security" -It "rejects injection in version" -When call validate_input_python "dotnet-version-detect" "default-version" "8.0;malicious" -The status should be failure -End - -It "validates version security" -When call validate_input_python "dotnet-version-detect" "default-version" "8.0&&malicious" -The status should be failure -End -End - -Context "when testing outputs" -It "produces all expected outputs consistently" -When call test_action_outputs "$ACTION_DIR" "default-version" "8.0" -The status should be success -The stderr should include "Testing action outputs for: dotnet-version-detect" -The stderr should include "Output test passed for: dotnet-version-detect" -End -End -End diff --git a/_tests/unit/eslint-check/validation.spec.sh b/_tests/unit/eslint-check/validation.spec.sh deleted file mode 100755 index 6aacc10..0000000 --- a/_tests/unit/eslint-check/validation.spec.sh +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for eslint-check action validation and logic -# Framework is automatically loaded via spec_helper.sh - -Describe "eslint-check action" - ACTION_DIR="eslint-check" - ACTION_FILE="$ACTION_DIR/action.yml" - - Context "when validating working-directory input" - It "accepts current directory" - When call validate_input_python "eslint-check" "working-directory" "." - The status should be success - End - It "accepts relative path" - When call validate_input_python "eslint-check" "working-directory" "src/frontend" - The status should be success - End - It "accepts nested directory" - When call validate_input_python "eslint-check" "working-directory" "packages/ui" - The status should be success - End - It "rejects path traversal" - When call validate_input_python "eslint-check" "working-directory" "../../../etc/passwd" - The status should be failure - End - It "rejects absolute paths" - When call validate_input_python "eslint-check" "working-directory" "/etc/passwd" - The status should be failure - End - It "rejects injection attempts" - When call validate_input_python "eslint-check" "working-directory" "src; rm -rf /" - The status should be failure - End - End - - Context "when validating eslint-version input" - It "accepts latest version" - When call validate_input_python "eslint-check" "eslint-version" "latest" - The status should be success - End - It "accepts semantic version" - When call validate_input_python "eslint-check" "eslint-version" "8.57.0" - The status should be success - End - It "accepts version with prerelease" - When call validate_input_python "eslint-check" "eslint-version" "9.0.0-alpha.0" - The status should be success - End - It "accepts older stable version" - When call validate_input_python "eslint-check" "eslint-version" "7.32.0" - The status should be success - End - It "rejects invalid version format" - When call validate_input_python "eslint-check" "eslint-version" "8.57" - The status should be failure - End - It "rejects version with letters" - When call validate_input_python "eslint-check" "eslint-version" "8.57.0a" - The status should be failure - End - It "rejects empty version" - When call validate_input_python "eslint-check" "eslint-version" "" - The status should be failure - End - End - - Context "when validating config-file input" - It "accepts default eslintrc" - When call validate_input_python "eslint-check" "config-file" ".eslintrc" - The status should be success - End - It "accepts eslintrc.json" - When call validate_input_python "eslint-check" "config-file" ".eslintrc.json" - The status should be success - End - It "accepts eslint.config.js" - When call validate_input_python "eslint-check" "config-file" "eslint.config.js" - The status should be success - End - It "accepts relative path config" - When call validate_input_python "eslint-check" "config-file" "config/eslint.json" - The status should be success - End - It "rejects path traversal" - When call validate_input_python "eslint-check" "config-file" "../../../malicious.js" - The status should be failure - End - It "rejects injection in config path" - When call validate_input_python "eslint-check" "config-file" "config.js;rm -rf /" - The status should be failure - End - End - - Context "when validating ignore-file input" - It "accepts default eslintignore" - When call validate_input_python "eslint-check" "ignore-file" ".eslintignore" - The status should be success - End - It "accepts custom ignore file" - When call validate_input_python "eslint-check" "ignore-file" "eslint-ignore.txt" - The status should be success - End - It "accepts relative path ignore file" - When call validate_input_python "eslint-check" "ignore-file" "config/.eslintignore" - The status should be success - End - It "rejects path traversal" - When call validate_input_python "eslint-check" "ignore-file" "../../sensitive.txt" - The status should be failure - End - End - - Context "when validating file-extensions input" - It "accepts default extensions" - When call validate_input_python "eslint-check" "file-extensions" ".js,.jsx,.ts,.tsx" - The status should be success - End - It "accepts single extension" - When call validate_input_python "eslint-check" "file-extensions" ".js" - The status should be success - End - It "accepts TypeScript extensions only" - When call validate_input_python "eslint-check" "file-extensions" ".ts,.tsx" - The status should be success - End - It "accepts Vue and JavaScript extensions" - When call validate_input_python "eslint-check" "file-extensions" ".js,.vue,.ts" - The status should be success - End - It "rejects extensions without dots" - When call validate_input_python "eslint-check" "file-extensions" "js,ts" - The status should be failure - End - It "rejects invalid extension format" - When call validate_input_python "eslint-check" "file-extensions" ".js;.ts" - The status should be failure - End - It "rejects extensions with special characters" - When call validate_input_python "eslint-check" "file-extensions" ".js,.t$" - The status should be failure - End - End - - Context "when validating boolean inputs" - It "accepts cache as true" - When call validate_input_python "eslint-check" "cache" "true" - The status should be success - End - It "accepts cache as false" - When call validate_input_python "eslint-check" "cache" "false" - The status should be success - End - It "accepts fail-on-error as true" - When call validate_input_python "eslint-check" "fail-on-error" "true" - The status should be success - End - It "accepts fail-on-error as false" - When call validate_input_python "eslint-check" "fail-on-error" "false" - The status should be success - End - It "rejects invalid boolean value" - When call validate_input_python "eslint-check" "cache" "maybe" - The status should be failure - End - It "rejects numeric boolean" - When call validate_input_python "eslint-check" "fail-on-error" "1" - The status should be failure - End - End - - Context "when validating numeric inputs" - It "accepts zero max-warnings" - When call validate_input_python "eslint-check" "max-warnings" "0" - The status should be success - End - It "accepts reasonable max-warnings" - When call validate_input_python "eslint-check" "max-warnings" "10" - The status should be success - End - It "accepts large max-warnings" - When call validate_input_python "eslint-check" "max-warnings" "1000" - The status should be success - End - It "accepts valid max-retries" - When call validate_input_python "eslint-check" "max-retries" "3" - The status should be success - End - It "accepts minimum retries" - When call validate_input_python "eslint-check" "max-retries" "1" - The status should be success - End - It "accepts maximum retries" - When call validate_input_python "eslint-check" "max-retries" "10" - The status should be success - End - It "rejects negative max-warnings" - When call validate_input_python "eslint-check" "max-warnings" "-1" - The status should be failure - End - It "rejects non-numeric max-warnings" - When call validate_input_python "eslint-check" "max-warnings" "many" - The status should be failure - End - It "rejects zero retries" - When call validate_input_python "eslint-check" "max-retries" "0" - The status should be failure - End - It "rejects retries above limit" - When call validate_input_python "eslint-check" "max-retries" "15" - The status should be failure - End - End - - Context "when validating report-format input" - It "accepts stylish format" - When call validate_input_python "eslint-check" "report-format" "stylish" - The status should be success - End - It "accepts json format" - When call validate_input_python "eslint-check" "report-format" "json" - The status should be success - End - It "accepts sarif format" - When call validate_input_python "eslint-check" "report-format" "sarif" - The status should be success - End - It "accepts checkstyle format" - When call validate_input_python "eslint-check" "report-format" "checkstyle" - The status should be success - End - It "accepts compact format" - When call validate_input_python "eslint-check" "report-format" "compact" - The status should be success - End - It "accepts html format" - When call validate_input_python "eslint-check" "report-format" "html" - The status should be success - End - It "accepts junit format" - When call validate_input_python "eslint-check" "report-format" "junit" - The status should be success - End - It "accepts tap format" - When call validate_input_python "eslint-check" "report-format" "tap" - The status should be success - End - It "accepts unix format" - When call validate_input_python "eslint-check" "report-format" "unix" - The status should be success - End - It "rejects invalid format" - When call validate_input_python "eslint-check" "report-format" "invalid" - The status should be failure - End - It "rejects empty format" - When call validate_input_python "eslint-check" "report-format" "" - 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 "ESLint Check" - End - - It "defines required inputs" - inputs=$(get_action_inputs "$ACTION_FILE") - When call echo "$inputs" - The output should include "working-directory" - The output should include "eslint-version" - The output should include "max-retries" - End - - It "defines optional inputs with defaults" - inputs=$(get_action_inputs "$ACTION_FILE") - When call echo "$inputs" - The output should include "config-file" - The output should include "ignore-file" - The output should include "file-extensions" - The output should include "cache" - The output should include "max-warnings" - The output should include "fail-on-error" - The output should include "report-format" - End - - It "defines expected outputs" - outputs=$(get_action_outputs "$ACTION_FILE") - When call echo "$outputs" - The output should include "error-count" - The output should include "warning-count" - The output should include "sarif-file" - The output should include "files-checked" - End - - It "has composite run type" - When call grep -q "using: composite" "$ACTION_FILE" - The status should be success - End - - It "includes input validation step" - When call grep -q "Validate Inputs" "$ACTION_FILE" - The status should be success - End - - It "uses node-setup action" - When call grep -q "./node-setup" "$ACTION_FILE" - The status should be success - End - - It "uses common-cache action" - When call grep -q "./common-cache" "$ACTION_FILE" - The status should be success - End - End - - Context "when validating security" - It "validates input paths to prevent injection" - When call validate_input_python "eslint-check" "working-directory" "../../../etc" - The status should be failure - End - - It "validates config file paths" - When call validate_input_python "eslint-check" "config-file" "../../malicious.js" - The status should be failure - End - - It "sanitizes file extensions input" - When call validate_input_python "eslint-check" "file-extensions" ".js;rm -rf /" - The status should be failure - End - End - - Context "when testing outputs" - It "produces all expected outputs" - When call test_action_outputs "$ACTION_DIR" "working-directory" "." "eslint-version" "latest" "max-retries" "3" - The status should be success - The stderr should include "Testing action outputs for: eslint-check" - The stderr should include "Output test passed for: eslint-check" - End - - It "outputs consistent error and warning counts" - When call test_action_outputs "$ACTION_DIR" "max-warnings" "0" "report-format" "sarif" - The status should be success - The stderr should include "Testing action outputs for: eslint-check" - The stderr should include "Output test passed for: eslint-check" - End - End -End diff --git a/_tests/unit/eslint-fix/validation.spec.sh b/_tests/unit/eslint-fix/validation.spec.sh deleted file mode 100755 index e05f3b2..0000000 --- a/_tests/unit/eslint-fix/validation.spec.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for eslint-fix action validation and logic -# Framework is automatically loaded via spec_helper.sh - -Describe "eslint-fix action" - ACTION_DIR="eslint-fix" - ACTION_FILE="$ACTION_DIR/action.yml" - - Context "when validating token input" - It "accepts valid GitHub token" - When call validate_input_python "eslint-fix" "token" "ghp_123456789012345678901234567890123456" - The status should be success - End - It "rejects injection in token" - When call validate_input_python "eslint-fix" "token" "token; rm -rf /" - The status should be failure - End - End - - Context "when validating username input" - It "accepts valid username" - When call validate_input_python "eslint-fix" "username" "github-actions" - The status should be success - End - It "rejects injection in username" - When call validate_input_python "eslint-fix" "username" "user; rm -rf /" - The status should be failure - End - End - - Context "when validating email input" - It "accepts valid email" - When call validate_input_python "eslint-fix" "email" "test@example.com" - The status should be success - End - It "rejects invalid email format" - When call validate_input_python "eslint-fix" "email" "invalid-email" - The status should be failure - End - End - - Context "when validating numeric inputs" - It "accepts valid max-retries" - When call validate_input_python "eslint-fix" "max-retries" "3" - The status should be success - End - It "accepts minimum retries" - When call validate_input_python "eslint-fix" "max-retries" "1" - The status should be success - End - It "accepts maximum retries" - When call validate_input_python "eslint-fix" "max-retries" "10" - The status should be success - End - It "rejects zero retries" - When call validate_input_python "eslint-fix" "max-retries" "0" - The status should be failure - End - It "rejects retries above limit" - When call validate_input_python "eslint-fix" "max-retries" "15" - 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 "ESLint Fix" - End - - It "defines required inputs" - inputs=$(get_action_inputs "$ACTION_FILE") - When call echo "$inputs" - The output should include "token" - The output should include "username" - The output should include "email" - The output should include "max-retries" - End - - It "defines expected outputs" - outputs=$(get_action_outputs "$ACTION_FILE") - When call echo "$outputs" - The output should include "files_changed" - The output should include "lint_status" - The output should include "errors_fixed" - End - End - - Context "when validating security" - It "validates token format" - When call validate_input_python "eslint-fix" "token" "invalid-token;rm -rf /" - The status should be failure - End - - It "validates email format" - When call validate_input_python "eslint-fix" "email" "invalid@email" - The status should be failure - End - End - - Context "when testing outputs" - It "produces all expected outputs" - When call test_action_outputs "$ACTION_DIR" "token" "ghp_test" "username" "test" "email" "test@example.com" "max-retries" "3" - The status should be success - The stderr should include "Testing action outputs for: eslint-fix" - The stderr should include "Output test passed for: eslint-fix" - End - End -End diff --git a/_tests/unit/eslint-lint/validation.spec.sh b/_tests/unit/eslint-lint/validation.spec.sh new file mode 100755 index 0000000..322aa25 --- /dev/null +++ b/_tests/unit/eslint-lint/validation.spec.sh @@ -0,0 +1,527 @@ +#!/usr/bin/env shellspec +# Unit tests for eslint-lint action validation and logic +# Framework is automatically loaded via spec_helper.sh + +Describe "eslint-lint action" +ACTION_DIR="eslint-lint" +ACTION_FILE="$ACTION_DIR/action.yml" + +Context "when validating mode input" +It "accepts check mode" +When call validate_input_python "eslint-lint" "mode" "check" +The status should be success +End + +It "accepts fix mode" +When call validate_input_python "eslint-lint" "mode" "fix" +The status should be success +End + +It "accepts empty mode (uses default)" +When call validate_input_python "eslint-lint" "mode" "" +The status should be success +End + +It "rejects invalid mode" +When call validate_input_python "eslint-lint" "mode" "invalid" +The status should be failure +End + +It "rejects mode with command injection" +When call validate_input_python "eslint-lint" "mode" "check; rm -rf /" +The status should be failure +End +End + +Context "when validating working-directory input" +It "accepts default directory" +When call validate_input_python "eslint-lint" "working-directory" "." +The status should be success +End + +It "accepts valid subdirectory" +When call validate_input_python "eslint-lint" "working-directory" "src" +The status should be success +End + +It "accepts empty working-directory (uses default)" +When call validate_input_python "eslint-lint" "working-directory" "" +The status should be success +End + +It "rejects path traversal" +When call validate_input_python "eslint-lint" "working-directory" "../../../etc" +The status should be failure +End + +It "rejects directory with command injection" +When call validate_input_python "eslint-lint" "working-directory" "src; rm -rf /" +The status should be failure +End +End + +Context "when validating eslint-version input" +It "accepts latest version" +When call validate_input_python "eslint-lint" "eslint-version" "latest" +The status should be success +End + +It "accepts semantic version" +When call validate_input_python "eslint-lint" "eslint-version" "8.57.0" +The status should be success +End + +It "accepts major.minor version" +When call validate_input_python "eslint-lint" "eslint-version" "8.57" +The status should be success +End + +It "accepts major version" +When call validate_input_python "eslint-lint" "eslint-version" "8" +The status should be success +End + +It "accepts version with pre-release" +When call validate_input_python "eslint-lint" "eslint-version" "9.0.0-beta.1" +The status should be success +End + +It "accepts empty version (uses default)" +When call validate_input_python "eslint-lint" "eslint-version" "" +The status should be success +End + +It "rejects invalid version format" +When call validate_input_python "eslint-lint" "eslint-version" "invalid" +The status should be failure +End + +It "rejects version with command injection" +When call validate_input_python "eslint-lint" "eslint-version" "8.57.0; echo" +The status should be failure +End +End + +Context "when validating config-file input" +It "accepts default config file" +When call validate_input_python "eslint-lint" "config-file" ".eslintrc" +The status should be success +End + +It "accepts custom config file" +When call validate_input_python "eslint-lint" "config-file" ".eslintrc.js" +The status should be success +End + +It "accepts config file in subdirectory" +When call validate_input_python "eslint-lint" "config-file" "config/eslint.config.js" +The status should be success +End + +It "accepts empty config-file (uses default)" +When call validate_input_python "eslint-lint" "config-file" "" +The status should be success +End + +It "rejects config file with path traversal" +When call validate_input_python "eslint-lint" "config-file" "../../../.eslintrc" +The status should be failure +End + +It "rejects config file with command injection" +When call validate_input_python "eslint-lint" "config-file" ".eslintrc; rm -rf /" +The status should be failure +End +End + +Context "when validating ignore-file input" +It "accepts default ignore file" +When call validate_input_python "eslint-lint" "ignore-file" ".eslintignore" +The status should be success +End + +It "accepts custom ignore file" +When call validate_input_python "eslint-lint" "ignore-file" ".eslintignore.custom" +The status should be success +End + +It "accepts empty ignore-file (uses default)" +When call validate_input_python "eslint-lint" "ignore-file" "" +The status should be success +End + +It "rejects ignore file with path traversal" +When call validate_input_python "eslint-lint" "ignore-file" "../../../.eslintignore" +The status should be failure +End + +It "rejects ignore file with command injection" +When call validate_input_python "eslint-lint" "ignore-file" ".eslintignore; echo" +The status should be failure +End +End + +Context "when validating file-extensions input" +It "accepts default extensions" +When call validate_input_python "eslint-lint" "file-extensions" ".js,.jsx,.ts,.tsx" +The status should be success +End + +It "accepts single extension" +When call validate_input_python "eslint-lint" "file-extensions" ".js" +The status should be success +End + +It "accepts multiple extensions" +When call validate_input_python "eslint-lint" "file-extensions" ".js,.ts,.mjs" +The status should be success +End + +It "accepts empty file-extensions (uses default)" +When call validate_input_python "eslint-lint" "file-extensions" "" +The status should be success +End + +It "rejects extensions without leading dot" +When call validate_input_python "eslint-lint" "file-extensions" "js,jsx" +The status should be failure +End + +It "rejects extensions with command injection" +When call validate_input_python "eslint-lint" "file-extensions" ".js; rm -rf /" +The status should be failure +End +End + +Context "when validating cache input" +It "accepts true" +When call validate_input_python "eslint-lint" "cache" "true" +The status should be success +End + +It "accepts false" +When call validate_input_python "eslint-lint" "cache" "false" +The status should be success +End + +It "accepts empty cache (uses default)" +When call validate_input_python "eslint-lint" "cache" "" +The status should be success +End + +It "rejects invalid boolean value" +When call validate_input_python "eslint-lint" "cache" "maybe" +The status should be failure +End +End + +Context "when validating max-warnings input" +It "accepts default value 0" +When call validate_input_python "eslint-lint" "max-warnings" "0" +The status should be success +End + +It "accepts positive integer" +When call validate_input_python "eslint-lint" "max-warnings" "10" +The status should be success +End + +It "accepts large number" +When call validate_input_python "eslint-lint" "max-warnings" "1000" +The status should be success +End + +It "accepts empty max-warnings (uses default)" +When call validate_input_python "eslint-lint" "max-warnings" "" +The status should be success +End + +It "rejects negative number" +When call validate_input_python "eslint-lint" "max-warnings" "-1" +The status should be failure +End + +It "rejects non-numeric value" +When call validate_input_python "eslint-lint" "max-warnings" "abc" +The status should be failure +End + +It "rejects max-warnings with command injection" +When call validate_input_python "eslint-lint" "max-warnings" "0; echo" +The status should be failure +End +End + +Context "when validating fail-on-error input" +It "accepts true" +When call validate_input_python "eslint-lint" "fail-on-error" "true" +The status should be success +End + +It "accepts false" +When call validate_input_python "eslint-lint" "fail-on-error" "false" +The status should be success +End + +It "accepts empty fail-on-error (uses default)" +When call validate_input_python "eslint-lint" "fail-on-error" "" +The status should be success +End + +It "rejects invalid boolean value" +When call validate_input_python "eslint-lint" "fail-on-error" "yes" +The status should be failure +End +End + +Context "when validating report-format input" +It "accepts stylish format" +When call validate_input_python "eslint-lint" "report-format" "stylish" +The status should be success +End + +It "accepts json format" +When call validate_input_python "eslint-lint" "report-format" "json" +The status should be success +End + +It "accepts sarif format" +When call validate_input_python "eslint-lint" "report-format" "sarif" +The status should be success +End + +It "accepts empty report-format (uses default)" +When call validate_input_python "eslint-lint" "report-format" "" +The status should be success +End + +It "rejects invalid format" +When call validate_input_python "eslint-lint" "report-format" "invalid" +The status should be failure +End + +It "rejects format with command injection" +When call validate_input_python "eslint-lint" "report-format" "json; rm -rf /" +The status should be failure +End +End + +Context "when validating max-retries input" +It "accepts default value 3" +When call validate_input_python "eslint-lint" "max-retries" "3" +The status should be success +End + +It "accepts retry count of 1" +When call validate_input_python "eslint-lint" "max-retries" "1" +The status should be success +End + +It "accepts retry count of 10" +When call validate_input_python "eslint-lint" "max-retries" "10" +The status should be success +End + +It "accepts empty max-retries (uses default)" +When call validate_input_python "eslint-lint" "max-retries" "" +The status should be success +End + +It "rejects zero retries" +When call validate_input_python "eslint-lint" "max-retries" "0" +The status should be failure +End + +It "rejects negative retry count" +When call validate_input_python "eslint-lint" "max-retries" "-1" +The status should be failure +End + +It "rejects retry count above 10" +When call validate_input_python "eslint-lint" "max-retries" "11" +The status should be failure +End + +It "rejects non-numeric retry count" +When call validate_input_python "eslint-lint" "max-retries" "abc" +The status should be failure +End + +It "rejects retry count with command injection" +When call validate_input_python "eslint-lint" "max-retries" "3; echo" +The status should be failure +End +End + +Context "when validating token input" +It "accepts valid GitHub token (classic)" +When call validate_input_python "eslint-lint" "token" "ghp_123456789012345678901234567890123456" +The status should be success +End + +It "accepts valid GitHub fine-grained token" +When call validate_input_python "eslint-lint" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a" +The status should be success +End + +It "accepts empty token (optional)" +When call validate_input_python "eslint-lint" "token" "" +The status should be success +End + +It "rejects invalid token format" +When call validate_input_python "eslint-lint" "token" "invalid-token" +The status should be failure +End + +It "rejects token with command injection" +When call validate_input_python "eslint-lint" "token" "ghp_123456789012345678901234567890123456; rm -rf /" +The status should be failure +End +End + +Context "when validating username input" +It "accepts valid username" +When call validate_input_python "eslint-lint" "username" "github-actions" +The status should be success +End + +It "accepts username with hyphens" +When call validate_input_python "eslint-lint" "username" "my-bot-user" +The status should be success +End + +It "accepts alphanumeric username" +When call validate_input_python "eslint-lint" "username" "user123" +The status should be success +End + +It "accepts empty username (uses default)" +When call validate_input_python "eslint-lint" "username" "" +The status should be success +End + +It "rejects username with command injection" +When call validate_input_python "eslint-lint" "username" "user; rm -rf /" +The status should be failure +End + +It "rejects username with special characters" +When call validate_input_python "eslint-lint" "username" "user@bot" +The status should be failure +End +End + +Context "when validating email input" +It "accepts valid email" +When call validate_input_python "eslint-lint" "email" "github-actions@github.com" +The status should be success +End + +It "accepts email with plus sign" +When call validate_input_python "eslint-lint" "email" "user+bot@example.com" +The status should be success +End + +It "accepts email with subdomain" +When call validate_input_python "eslint-lint" "email" "bot@ci.example.com" +The status should be success +End + +It "accepts empty email (uses default)" +When call validate_input_python "eslint-lint" "email" "" +The status should be success +End + +It "rejects invalid email format" +When call validate_input_python "eslint-lint" "email" "not-an-email" +The status should be failure +End + +It "rejects email with command injection" +When call validate_input_python "eslint-lint" "email" "user@example.com; rm -rf /" +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 "ESLint Lint" +End + +It "defines expected inputs" +When call get_action_inputs "$ACTION_FILE" +The output should include "mode" +The output should include "working-directory" +The output should include "eslint-version" +The output should include "config-file" +The output should include "ignore-file" +The output should include "file-extensions" +The output should include "cache" +The output should include "max-warnings" +The output should include "fail-on-error" +The output should include "report-format" +The output should include "max-retries" +The output should include "token" +The output should include "username" +The output should include "email" +End + +It "defines expected outputs" +When call get_action_outputs "$ACTION_FILE" +The output should include "status" +The output should include "error-count" +The output should include "warning-count" +The output should include "sarif-file" +The output should include "files-checked" +The output should include "files-changed" +The output should include "errors-fixed" +End +End + +Context "when testing input requirements" +It "has all inputs as optional (with defaults)" +When call is_input_required "$ACTION_FILE" "mode" +The status should be failure +End +End + +Context "when testing security validations" +It "validates against path traversal in working-directory" +When call validate_input_python "eslint-lint" "working-directory" "../../../etc" +The status should be failure +End + +It "validates against shell metacharacters in mode" +When call validate_input_python "eslint-lint" "mode" "check|echo" +The status should be failure +End + +It "validates against command substitution in config-file" +When call validate_input_python "eslint-lint" "config-file" "\$(whoami)" +The status should be failure +End + +It "validates against path traversal in token" +When call validate_input_python "eslint-lint" "token" "../../../etc/passwd" +The status should be failure +End + +It "validates against shell metacharacters in username" +When call validate_input_python "eslint-lint" "username" "user&whoami" +The status should be failure +End + +It "validates against command injection in email" +When call validate_input_python "eslint-lint" "email" "user@example.com\`whoami\`" +The status should be failure +End +End +End diff --git a/_tests/unit/github-release/validation.spec.sh b/_tests/unit/github-release/validation.spec.sh deleted file mode 100755 index e883f39..0000000 --- a/_tests/unit/github-release/validation.spec.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for github-release action validation and logic - -# Framework is automatically loaded via spec_helper.sh -# Using the centralized validate_input_python function from spec_helper.sh - -Describe "github-release action" -ACTION_DIR="github-release" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating version input" -It "accepts valid semantic version" -When call validate_input_python "github-release" "version" "1.2.3" -The status should be success -End - -It "accepts semantic version with v prefix" -When call validate_input_python "github-release" "version" "v1.2.3" -The status should be success -End - -It "accepts prerelease version" -When call validate_input_python "github-release" "version" "1.2.3-alpha" -The status should be success -End - -It "accepts build metadata version" -When call validate_input_python "github-release" "version" "1.2.3+build.1" -The status should be success -End - -It "accepts prerelease with build metadata" -When call validate_input_python "github-release" "version" "1.2.3-alpha.1+build.1" -The status should be success -End - -It "accepts CalVer format" -When call validate_input_python "github-release" "version" "2024.3.1" -The status should be success -End - -It "rejects invalid version format" -When call validate_input_python "github-release" "version" "invalid-version" -The status should be failure -End - -It "rejects version with command injection" -When call validate_input_python "github-release" "version" "1.2.3; rm -rf /" -The status should be failure -End - -It "rejects empty version" -When call validate_input_python "github-release" "version" "" -The status should be failure -End -End - -Context "when validating changelog input" -It "accepts empty changelog" -When call validate_input_python "github-release" "changelog" "" -The status should be success -End - -It "accepts normal changelog content" -When call validate_input_python "github-release" "changelog" "## What's Changed\n- Fixed bug #123\n- Added feature X" -The status should be success -End - -It "accepts changelog with special characters" -When call validate_input_python "github-release" "changelog" "Version 1.2.3\n\n- Bug fixes & improvements\n- Added @mention support" -The status should be success -End - -It "rejects changelog with command injection" -When call validate_input_python "github-release" "changelog" "Release notes; rm -rf /" -The status should be failure -End - -It "rejects changelog with shell expansion" -When call validate_input_python "github-release" "changelog" "Release \$(whoami) notes" -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 "GitHub Release" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "version" -The output should include "changelog" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "release_url" -The output should include "release_id" -The output should include "upload_url" -End -End - -Context "when testing input requirements" -It "requires version input" -inputs=$(get_action_inputs "$ACTION_FILE") -When call echo "$inputs" -The output should include "version" -End - -It "has changelog as optional input" -# Test that changelog has a default value in action.yml -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "changelog" "optional" -The output should equal "optional" -End -End - -Context "when testing security validations" -It "validates against path traversal in version" -When call validate_input_python "github-release" "version" "../1.2.3" -The status should be failure -End - -It "validates against shell metacharacters in version" -When call validate_input_python "github-release" "version" "1.2.3|echo" -The status should be failure -End - -It "validates against shell metacharacters in changelog" -When call validate_input_python "github-release" "changelog" "Release notes|echo test" -The status should be failure -End -End -End diff --git a/_tests/unit/go-version-detect/validation.spec.sh b/_tests/unit/go-version-detect/validation.spec.sh deleted file mode 100755 index c33ba76..0000000 --- a/_tests/unit/go-version-detect/validation.spec.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for go-version-detect action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "go-version-detect action" -ACTION_DIR="go-version-detect" -ACTION_FILE="$ACTION_DIR/action.yml" - -# Test version constants (update these when Go releases new versions) -CURRENT_STABLE_GO_VERSION="1.25" -CURRENT_STABLE_GO_PATCH="1.25.0" -PREVIOUS_GO_VERSION="1.24.0" -MIN_SUPPORTED_GO_VERSION="1.18" -MAX_SUPPORTED_GO_VERSION="1.30" -TOO_OLD_GO_VERSION="1.17" -TOO_NEW_GO_VERSION="1.31" - -Context "when validating default-version input" -It "accepts valid semantic version" -When call validate_input_python "go-version-detect" "default-version" "$CURRENT_STABLE_GO_VERSION" -The status should be success -End - -It "accepts semantic version with patch" -When call validate_input_python "go-version-detect" "default-version" "$PREVIOUS_GO_VERSION" -The status should be success -End - -It "accepts minimum supported Go version" -When call validate_input_python "go-version-detect" "default-version" "$MIN_SUPPORTED_GO_VERSION" -The status should be success -End - -It "accepts current stable Go version" -When call validate_input_python "go-version-detect" "default-version" "$CURRENT_STABLE_GO_PATCH" -The status should be success -End - -It "rejects version without minor" -When call validate_input_python "go-version-detect" "default-version" "1" -The status should be failure -End - -It "rejects invalid version format" -When call validate_input_python "go-version-detect" "default-version" "invalid-version" -The status should be failure -End - -It "rejects version with command injection" -When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}; rm -rf /" -The status should be failure -End - -It "rejects version with shell expansion" -When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\$(echo test)" -The status should be failure -End - -It "rejects major version other than 1" -When call validate_input_python "go-version-detect" "default-version" "2.0" -The status should be failure -End - -It "rejects too old minor version" -When call validate_input_python "go-version-detect" "default-version" "$TOO_OLD_GO_VERSION" -The status should be failure -End - -It "rejects too new minor version" -When call validate_input_python "go-version-detect" "default-version" "$TOO_NEW_GO_VERSION" -The status should be failure -End - -It "rejects empty version" -When call validate_input_python "go-version-detect" "default-version" "" -The status should be failure -End - -It "rejects version with leading v" -When call validate_input_python "go-version-detect" "default-version" "v${CURRENT_STABLE_GO_VERSION}" -The status should be failure -End - -It "rejects version with prerelease" -When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}-beta" -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 "Go Version Detect" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "default-version" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "go-version" -End -End - -Context "when testing input requirements" -It "has default-version as optional input" -# Test that default-version has a default value in action.yml -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional" -The output should equal "optional" -End - -It "has correct default version" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "default" -The output should equal "$CURRENT_STABLE_GO_VERSION" -End -End - -Context "when testing security validations" -It "validates against path traversal in version" -When call validate_input_python "go-version-detect" "default-version" "../${CURRENT_STABLE_GO_VERSION}" -The status should be failure -End - -It "validates against shell metacharacters in version" -When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}|echo" -The status should be failure -End - -It "validates against backtick injection" -When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\`whoami\`" -The status should be failure -End - -It "validates against variable expansion" -When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\${HOME}" -The status should be failure -End -End - -Context "when testing version range validation" -It "validates reasonable Go version range boundaries" -# Test boundary conditions for Go version validation -When call validate_input_python "go-version-detect" "default-version" "$TOO_OLD_GO_VERSION" -The status should be failure -End - -It "validates upper boundary" -When call validate_input_python "go-version-detect" "default-version" "$TOO_NEW_GO_VERSION" -The status should be failure -End - -It "validates exact boundary valid values" -When call validate_input_python "go-version-detect" "default-version" "$MIN_SUPPORTED_GO_VERSION" -The status should be success -End - -It "validates exact boundary valid values upper" -When call validate_input_python "go-version-detect" "default-version" "$MAX_SUPPORTED_GO_VERSION" -The status should be success -End -End -End diff --git a/_tests/unit/language-version-detect/validation.spec.sh b/_tests/unit/language-version-detect/validation.spec.sh new file mode 100755 index 0000000..489b6e5 --- /dev/null +++ b/_tests/unit/language-version-detect/validation.spec.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env shellspec +# Unit tests for language-version-detect action validation and logic +# Framework is automatically loaded via spec_helper.sh + +Describe "language-version-detect action" +ACTION_DIR="language-version-detect" +ACTION_FILE="$ACTION_DIR/action.yml" + +Context "when validating language input" +It "accepts php language" +When call validate_input_python "language-version-detect" "language" "php" +The status should be success +End + +It "accepts python language" +When call validate_input_python "language-version-detect" "language" "python" +The status should be success +End + +It "accepts go language" +When call validate_input_python "language-version-detect" "language" "go" +The status should be success +End + +It "accepts dotnet language" +When call validate_input_python "language-version-detect" "language" "dotnet" +The status should be success +End + +It "rejects invalid language" +When call validate_input_python "language-version-detect" "language" "javascript" +The status should be failure +End + +It "rejects empty language (required)" +When call validate_input_python "language-version-detect" "language" "" +The status should be failure +End + +It "rejects language with command injection" +When call validate_input_python "language-version-detect" "language" "php; rm -rf /" +The status should be failure +End + +It "rejects language with shell metacharacters" +When call validate_input_python "language-version-detect" "language" "php|echo" +The status should be failure +End +End + +Context "when validating default-version input for PHP" +It "accepts valid PHP version 8.4" +When call validate_input_python "language-version-detect" "default-version" "8.4" +The status should be success +End + +It "accepts valid PHP version 8.3" +When call validate_input_python "language-version-detect" "default-version" "8.3" +The status should be success +End + +It "accepts valid PHP version 7.4" +When call validate_input_python "language-version-detect" "default-version" "7.4" +The status should be success +End + +It "accepts valid PHP version with patch 8.3.1" +When call validate_input_python "language-version-detect" "default-version" "8.3.1" +The status should be success +End + +It "accepts empty default-version (uses language default)" +When call validate_input_python "language-version-detect" "default-version" "" +The status should be success +End + +It "rejects invalid PHP version format" +When call validate_input_python "language-version-detect" "default-version" "invalid" +The status should be failure +End +End + +Context "when validating default-version input for Python" +It "accepts valid Python version 3.12" +When call validate_input_python "language-version-detect" "default-version" "3.12" +The status should be success +End + +It "accepts valid Python version 3.11" +When call validate_input_python "language-version-detect" "default-version" "3.11" +The status should be success +End + +It "accepts valid Python version 3.10" +When call validate_input_python "language-version-detect" "default-version" "3.10" +The status should be success +End + +It "accepts valid Python version with patch 3.12.1" +When call validate_input_python "language-version-detect" "default-version" "3.12.1" +The status should be success +End + +It "accepts valid Python version 3.9" +When call validate_input_python "language-version-detect" "default-version" "3.9" +The status should be success +End + +It "accepts valid Python version 3.8" +When call validate_input_python "language-version-detect" "default-version" "3.8" +The status should be success +End +End + +Context "when validating default-version input for Go" +It "accepts valid Go version 1.21" +When call validate_input_python "language-version-detect" "default-version" "1.21" +The status should be success +End + +It "accepts valid Go version 1.20" +When call validate_input_python "language-version-detect" "default-version" "1.20" +The status should be success +End + +It "accepts valid Go version with patch 1.21.5" +When call validate_input_python "language-version-detect" "default-version" "1.21.5" +The status should be success +End + +It "accepts valid Go version 1.22" +When call validate_input_python "language-version-detect" "default-version" "1.22" +The status should be success +End +End + +Context "when validating default-version input for .NET" +It "accepts valid .NET version 7.0" +When call validate_input_python "language-version-detect" "default-version" "7.0" +The status should be success +End + +It "accepts valid .NET version 8.0" +When call validate_input_python "language-version-detect" "default-version" "8.0" +The status should be success +End + +It "accepts valid .NET version 6.0" +When call validate_input_python "language-version-detect" "default-version" "6.0" +The status should be success +End + +It "accepts valid .NET version with patch 7.0.1" +When call validate_input_python "language-version-detect" "default-version" "7.0.1" +The status should be success +End + +It "accepts valid .NET major version 7" +When call validate_input_python "language-version-detect" "default-version" "7" +The status should be success +End +End + +Context "when validating default-version input edge cases" +It "rejects version with v prefix" +When call validate_input_python "language-version-detect" "default-version" "v3.12" +The status should be failure +End + +It "rejects version with command injection" +When call validate_input_python "language-version-detect" "default-version" "3.12; rm -rf /" +The status should be failure +End + +It "rejects version with shell metacharacters" +When call validate_input_python "language-version-detect" "default-version" "3.12|echo" +The status should be failure +End + +It "rejects version with command substitution" +When call validate_input_python "language-version-detect" "default-version" "\$(whoami)" +The status should be failure +End + +It "rejects alphabetic version" +When call validate_input_python "language-version-detect" "default-version" "latest" +The status should be failure +End +End + +Context "when validating token input" +It "accepts valid GitHub token (classic)" +When call validate_input_python "language-version-detect" "token" "ghp_123456789012345678901234567890123456" +The status should be success +End + +It "accepts valid GitHub fine-grained token" +When call validate_input_python "language-version-detect" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a" +The status should be success +End + +It "accepts empty token (optional)" +When call validate_input_python "language-version-detect" "token" "" +The status should be success +End + +It "rejects invalid token format" +When call validate_input_python "language-version-detect" "token" "invalid-token" +The status should be failure +End + +It "rejects token with command injection" +When call validate_input_python "language-version-detect" "token" "ghp_123456789012345678901234567890123456; rm -rf /" +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 "Language Version Detect" +End + +It "defines expected inputs" +When call get_action_inputs "$ACTION_FILE" +The output should include "language" +The output should include "default-version" +The output should include "token" +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 testing input requirements" +It "requires language input" +When call is_input_required "$ACTION_FILE" "language" +The status should be success +End + +It "has default-version as optional input" +When call is_input_required "$ACTION_FILE" "default-version" +The status should be failure +End + +It "has token as optional input" +When call is_input_required "$ACTION_FILE" "token" +The status should be failure +End +End + +Context "when testing security validations" +It "validates against path traversal in language" +When call validate_input_python "language-version-detect" "language" "../../../etc" +The status should be failure +End + +It "validates against shell metacharacters in language" +When call validate_input_python "language-version-detect" "language" "php&whoami" +The status should be failure +End + +It "validates against command substitution in language" +When call validate_input_python "language-version-detect" "language" "php\`whoami\`" +The status should be failure +End + +It "validates against path traversal in default-version" +When call validate_input_python "language-version-detect" "default-version" "../../../etc" +The status should be failure +End + +It "validates against shell metacharacters in default-version" +When call validate_input_python "language-version-detect" "default-version" "3.12&echo" +The status should be failure +End + +It "validates against command substitution in default-version" +When call validate_input_python "language-version-detect" "default-version" "3.12\$(whoami)" +The status should be failure +End + +It "validates against path traversal in token" +When call validate_input_python "language-version-detect" "token" "../../../etc/passwd" +The status should be failure +End +End +End diff --git a/_tests/unit/php-version-detect/validation.spec.sh b/_tests/unit/php-version-detect/validation.spec.sh deleted file mode 100755 index f28110e..0000000 --- a/_tests/unit/php-version-detect/validation.spec.sh +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for php-version-detect action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "php-version-detect action" -ACTION_DIR="php-version-detect" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating default-version input" -It "accepts valid PHP version" -When call validate_input_python "php-version-detect" "default-version" "8.2" -The status should be success -End - -It "accepts PHP version with patch" -When call validate_input_python "php-version-detect" "default-version" "8.3.1" -The status should be success -End - -It "accepts PHP 7.4" -When call validate_input_python "php-version-detect" "default-version" "7.4" -The status should be success -End - -It "accepts PHP 8.0" -When call validate_input_python "php-version-detect" "default-version" "8.0" -The status should be success -End - -It "accepts PHP 8.1" -When call validate_input_python "php-version-detect" "default-version" "8.1" -The status should be success -End - -It "accepts PHP 8.4" -When call validate_input_python "php-version-detect" "default-version" "8.4" -The status should be success -End - -It "rejects PHP version too old" -When call validate_input_python "php-version-detect" "default-version" "5.6" -The status should be failure -End - -It "rejects PHP version too new" -When call validate_input_python "php-version-detect" "default-version" "10.0" -The status should be failure -End - -It "rejects invalid version format" -When call validate_input_python "php-version-detect" "default-version" "php8.2" -The status should be failure -End - -It "rejects version with command injection" -When call validate_input_python "php-version-detect" "default-version" "8.2; rm -rf /" -The status should be failure -End - -It "rejects version without minor" -When call validate_input_python "php-version-detect" "default-version" "8" -The status should be failure -End - -It "rejects empty version" -When call validate_input_python "php-version-detect" "default-version" "" -The status should be failure -End - -It "rejects version with v prefix" -When call validate_input_python "php-version-detect" "default-version" "v8.2" -The status should be failure -End - -It "accepts PHP 8.5 for future compatibility" -When call validate_input_python "php-version-detect" "default-version" "8.5" -The status should be success -End - -It "rejects unreasonably high minor version" -When call validate_input_python "php-version-detect" "default-version" "8.100" -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 "PHP Version Detect" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "default-version" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "php-version" -End -End - -Context "when testing input requirements" -It "has default-version as optional input" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional" -The output should equal "optional" -End - -It "has correct default version" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "default" -The output should equal "8.2" -End -End - -Context "when testing security validations" -It "validates against path traversal in version" -When call validate_input_python "php-version-detect" "default-version" "../8.2" -The status should be failure -End - -It "validates against shell metacharacters in version" -When call validate_input_python "php-version-detect" "default-version" "8.2|echo" -The status should be failure -End - -It "validates against backtick injection" -When call validate_input_python "php-version-detect" "default-version" "8.2\`whoami\`" -The status should be failure -End - -It "validates against variable expansion" -When call validate_input_python "php-version-detect" "default-version" "8.2\${HOME}" -The status should be failure -End -End - -Context "when testing PHP version range validation" -It "validates PHP 7 minor version boundaries" -When call validate_input_python "php-version-detect" "default-version" "7.0" -The status should be success -End - -It "validates PHP 7.4 specifically" -When call validate_input_python "php-version-detect" "default-version" "7.4" -The status should be success -End - -It "validates PHP 8 minor version boundaries" -When call validate_input_python "php-version-detect" "default-version" "8.0" -The status should be success -End - -It "validates PHP 8.4 boundary" -When call validate_input_python "php-version-detect" "default-version" "8.4" -The status should be success -End - -It "validates PHP 9 future version" -When call validate_input_python "php-version-detect" "default-version" "9.0" -The status should be success -End -End -End diff --git a/_tests/unit/prettier-check/validation.spec.sh b/_tests/unit/prettier-check/validation.spec.sh deleted file mode 100755 index d55dde3..0000000 --- a/_tests/unit/prettier-check/validation.spec.sh +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for prettier-check action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "prettier-check action" -ACTION_DIR="prettier-check" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating working-directory input" -It "accepts current directory" -When call validate_input_python "prettier-check" "working-directory" "." -The status should be success -End - -It "accepts relative directory" -When call validate_input_python "prettier-check" "working-directory" "src" -The status should be success -End - -It "accepts nested directory" -When call validate_input_python "prettier-check" "working-directory" "src/components" -The status should be success -End - -It "rejects path traversal" -When call validate_input_python "prettier-check" "working-directory" "../malicious" -The status should be failure -End - -It "rejects absolute paths" -When call validate_input_python "prettier-check" "working-directory" "/etc/passwd" -The status should be failure -End - -It "rejects directory with command injection" -When call validate_input_python "prettier-check" "working-directory" "src; rm -rf /" -The status should be failure -End -End - -Context "when validating prettier-version input" -It "accepts latest version" -When call validate_input_python "prettier-check" "prettier-version" "latest" -The status should be success -End - -It "accepts semantic version" -When call validate_input_python "prettier-check" "prettier-version" "3.0.0" -The status should be success -End - -It "accepts prerelease version" -When call validate_input_python "prettier-check" "prettier-version" "3.0.0-alpha" -The status should be success -End - -It "rejects invalid version format" -When call validate_input_python "prettier-check" "prettier-version" "v3.0.0" -The status should be failure -End - -It "rejects version with command injection" -When call validate_input_python "prettier-check" "prettier-version" "3.0.0; rm -rf /" -The status should be failure -End -End - -Context "when validating config-file input" -It "accepts valid config file" -When call validate_input_python "prettier-check" "config-file" ".prettierrc" -The status should be success -End - -It "accepts config file with extension" -When call validate_input_python "prettier-check" "config-file" ".prettierrc.json" -The status should be success -End - -It "accepts config file in subdirectory" -When call validate_input_python "prettier-check" "config-file" "config/.prettierrc" -The status should be success -End - -It "rejects path traversal in config file" -When call validate_input_python "prettier-check" "config-file" "../../../etc/passwd" -The status should be failure -End - -It "rejects absolute path in config file" -When call validate_input_python "prettier-check" "config-file" "/etc/passwd" -The status should be failure -End -End - -Context "when validating ignore-file input" -It "accepts valid ignore file" -When call validate_input_python "prettier-check" "ignore-file" ".prettierignore" -The status should be success -End - -It "accepts ignore file in subdirectory" -When call validate_input_python "prettier-check" "ignore-file" "config/.prettierignore" -The status should be success -End - -It "rejects path traversal in ignore file" -When call validate_input_python "prettier-check" "ignore-file" "../../../etc/passwd" -The status should be failure -End - -It "rejects absolute path in ignore file" -When call validate_input_python "prettier-check" "ignore-file" "/etc/passwd" -The status should be failure -End -End - -Context "when validating file-pattern input" -It "accepts valid glob pattern" -When call validate_input_python "prettier-check" "file-pattern" "**/*.{js,ts}" -The status should be success -End - -It "accepts simple file pattern" -When call validate_input_python "prettier-check" "file-pattern" "*.js" -The status should be success -End - -It "accepts multiple extensions" -When call validate_input_python "prettier-check" "file-pattern" "**/*.{js,jsx,ts,tsx,css}" -The status should be success -End - -It "rejects pattern with path traversal" -When call validate_input_python "prettier-check" "file-pattern" "../**/*.js" -The status should be failure -End - -It "rejects pattern with absolute path" -When call validate_input_python "prettier-check" "file-pattern" "/etc/**/*.conf" -The status should be failure -End -End - -Context "when validating boolean inputs" -It "accepts true for cache" -When call validate_input_python "prettier-check" "cache" "true" -The status should be success -End - -It "accepts false for cache" -When call validate_input_python "prettier-check" "cache" "false" -The status should be success -End - -It "rejects invalid cache value" -When call validate_input_python "prettier-check" "cache" "yes" -The status should be failure -End - -It "accepts true for fail-on-error" -When call validate_input_python "prettier-check" "fail-on-error" "true" -The status should be success -End - -It "accepts false for fail-on-error" -When call validate_input_python "prettier-check" "fail-on-error" "false" -The status should be success -End - -It "accepts true for check-only" -When call validate_input_python "prettier-check" "check-only" "true" -The status should be success -End - -It "accepts false for check-only" -When call validate_input_python "prettier-check" "check-only" "false" -The status should be success -End -End - -Context "when validating report-format input" -It "accepts json format" -When call validate_input_python "prettier-check" "report-format" "json" -The status should be success -End - -It "accepts sarif format" -When call validate_input_python "prettier-check" "report-format" "sarif" -The status should be success -End - -It "rejects invalid format" -When call validate_input_python "prettier-check" "report-format" "xml" -The status should be failure -End - -It "rejects empty format" -When call validate_input_python "prettier-check" "report-format" "" -The status should be failure -End -End - -Context "when validating max-retries input" -It "accepts valid retry count" -When call validate_input_python "prettier-check" "max-retries" "3" -The status should be success -End - -It "accepts minimum retries" -When call validate_input_python "prettier-check" "max-retries" "1" -The status should be success -End - -It "accepts maximum retries" -When call validate_input_python "prettier-check" "max-retries" "10" -The status should be success -End - -It "rejects zero retries" -When call validate_input_python "prettier-check" "max-retries" "0" -The status should be failure -End - -It "rejects too many retries" -When call validate_input_python "prettier-check" "max-retries" "11" -The status should be failure -End - -It "rejects non-numeric retries" -When call validate_input_python "prettier-check" "max-retries" "many" -The status should be failure -End -End - -Context "when validating plugins input" -It "accepts empty plugins" -When call validate_input_python "prettier-check" "plugins" "" -The status should be success -End - -It "accepts valid plugin name" -When call validate_input_python "prettier-check" "plugins" "prettier-plugin-java" -The status should be success -End - -It "accepts scoped plugin" -When call validate_input_python "prettier-check" "plugins" "@prettier/plugin-xml" -The status should be success -End - -It "accepts multiple plugins" -When call validate_input_python "prettier-check" "plugins" "plugin1,@scope/plugin2" -The status should be success -End - -It "rejects plugins with command injection" -When call validate_input_python "prettier-check" "plugins" "plugin1; rm -rf /" -The status should be failure -End - -It "rejects plugins with shell operators" -When call validate_input_python "prettier-check" "plugins" "plugin1 && malicious" -The status should be failure -End - -It "rejects plugins with pipe" -When call validate_input_python "prettier-check" "plugins" "plugin1 | 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 "Prettier Check" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "working-directory" -The output should include "prettier-version" -The output should include "config-file" -The output should include "ignore-file" -The output should include "file-pattern" -The output should include "cache" -The output should include "fail-on-error" -The output should include "report-format" -The output should include "max-retries" -The output should include "plugins" -The output should include "check-only" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "files-checked" -The output should include "unformatted-files" -The output should include "sarif-file" -The output should include "cache-hit" -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" "any" "all_optional" -The output should equal "none" -End -End - -Context "when testing security validations" -It "validates against path traversal in multiple inputs" -When call validate_input_python "prettier-check" "working-directory" "../../malicious" -The status should be failure -End - -It "validates against command injection in plugins" -When call validate_input_python "prettier-check" "plugins" "plugin\`whoami\`" -The status should be failure -End - -It "validates against shell expansion in file patterns" -When call validate_input_python "prettier-check" "file-pattern" "**/*.js\${HOME}" -The status should be failure -End -End -End diff --git a/_tests/unit/prettier-fix/validation.spec.sh b/_tests/unit/prettier-fix/validation.spec.sh deleted file mode 100755 index 475ff92..0000000 --- a/_tests/unit/prettier-fix/validation.spec.sh +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for prettier-fix action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "prettier-fix action" -ACTION_DIR="prettier-fix" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating token input" -It "accepts GitHub token expression" -When call validate_input_python "prettier-fix" "token" "\${{ github.token }}" -The status should be success -End - -It "accepts GitHub fine-grained token" -When call validate_input_python "prettier-fix" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890" -The status should be success -End - -It "accepts GitHub app token" -When call validate_input_python "prettier-fix" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890" -The status should be success -End - -It "accepts GitHub enterprise token" -When call validate_input_python "prettier-fix" "token" "ghe_abcdefghijklmnopqrstuvwxyz1234567890" -The status should be success -End - -It "rejects invalid token format" -When call validate_input_python "prettier-fix" "token" "invalid-token" -The status should be failure -End - -It "rejects token with command injection" -When call validate_input_python "prettier-fix" "token" "ghp_token; rm -rf /" -The status should be failure -End - -It "accepts empty token (uses default)" -When call validate_input_python "prettier-fix" "token" "" -The status should be success -End -End - -Context "when validating username input" -It "accepts valid GitHub username" -When call validate_input_python "prettier-fix" "username" "github-actions" -The status should be success -End - -It "accepts username with hyphens" -When call validate_input_python "prettier-fix" "username" "user-name" -The status should be success -End - -It "accepts username with numbers" -When call validate_input_python "prettier-fix" "username" "user123" -The status should be success -End - -It "accepts single character username" -When call validate_input_python "prettier-fix" "username" "a" -The status should be success -End - -It "accepts maximum length username" -When call validate_input_python "prettier-fix" "username" "abcdefghijklmnopqrstuvwxyz0123456789abc" -The status should be success -End - -It "rejects username too long" -When call validate_input_python "prettier-fix" "username" "abcdefghijklmnopqrstuvwxyz0123456789abcd" -The status should be failure -End - -It "rejects username with command injection" -When call validate_input_python "prettier-fix" "username" "user; rm -rf /" -The status should be failure -End - -It "rejects username with shell operators" -When call validate_input_python "prettier-fix" "username" "user && rm -rf /" -The status should be failure -End - -It "rejects username with pipe" -When call validate_input_python "prettier-fix" "username" "user | rm -rf /" -The status should be failure -End - -It "accepts empty username (uses default)" -When call validate_input_python "prettier-fix" "username" "" -The status should be success -End -End - -Context "when validating email input" -It "accepts valid email" -When call validate_input_python "prettier-fix" "email" "user@example.com" -The status should be success -End - -It "accepts email with subdomain" -When call validate_input_python "prettier-fix" "email" "user@mail.example.com" -The status should be success -End - -It "accepts email with plus sign" -When call validate_input_python "prettier-fix" "email" "user+tag@example.com" -The status should be success -End - -It "accepts email with numbers" -When call validate_input_python "prettier-fix" "email" "user123@example123.com" -The status should be success -End - -It "accepts email with hyphens" -When call validate_input_python "prettier-fix" "email" "user-name@example-domain.com" -The status should be success -End - -It "rejects email without at symbol" -When call validate_input_python "prettier-fix" "email" "userexample.com" -The status should be failure -End - -It "rejects email without domain" -When call validate_input_python "prettier-fix" "email" "user@" -The status should be failure -End - -It "rejects email without username" -When call validate_input_python "prettier-fix" "email" "@example.com" -The status should be failure -End - -It "rejects email without dot in domain" -When call validate_input_python "prettier-fix" "email" "user@example" -The status should be failure -End - -It "rejects email with spaces" -When call validate_input_python "prettier-fix" "email" "user @example.com" -The status should be failure -End - -It "rejects empty email" -When call validate_input_python "prettier-fix" "email" "" -The status should be failure -End -End - -Context "when validating max-retries input" -It "accepts valid retry count" -When call validate_input_python "prettier-fix" "max-retries" "3" -The status should be success -End - -It "accepts minimum retries" -When call validate_input_python "prettier-fix" "max-retries" "1" -The status should be success -End - -It "accepts maximum retries" -When call validate_input_python "prettier-fix" "max-retries" "10" -The status should be success -End - -It "rejects zero retries" -When call validate_input_python "prettier-fix" "max-retries" "0" -The status should be failure -End - -It "rejects too many retries" -When call validate_input_python "prettier-fix" "max-retries" "11" -The status should be failure -End - -It "rejects non-numeric retries" -When call validate_input_python "prettier-fix" "max-retries" "many" -The status should be failure -End - -It "rejects negative retries" -When call validate_input_python "prettier-fix" "max-retries" "-1" -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 "Prettier Fix" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "token" -The output should include "username" -The output should include "email" -The output should include "max-retries" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "files_changed" -The output should include "format_status" -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 token" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "token" "default" -The output should equal "\${{ github.token }}" -End - -It "has correct default username" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "username" "default" -The output should equal "github-actions" -End - -It "has correct default email" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "email" "default" -The output should equal "github-actions@github.com" -End -End - -Context "when testing security validations" -It "validates against command injection in username" -When call validate_input_python "prettier-fix" "username" "user\`whoami\`" -The status should be failure -End - -It "validates against shell metacharacters in email" -When call validate_input_python "prettier-fix" "email" "user@example.com; rm -rf /" -The status should be failure -End - -It "validates against variable expansion in token" -When call validate_input_python "prettier-fix" "token" "\${MALICIOUS_VAR}" -The status should be failure -End - -It "validates against backtick injection in email" -When call validate_input_python "prettier-fix" "email" "user@example.com\`echo test\`" -The status should be failure -End -End - -Context "when testing Prettier-specific validations" -It "validates username length boundaries for Git" -When call validate_input_python "prettier-fix" "username" "$(awk 'BEGIN{for(i=1;i<=40;i++)printf "a"}')" -The status should be failure -End - -It "validates email format for Git commits" -When call validate_input_python "prettier-fix" "email" "noreply@github.com" -The status should be success -End - -It "validates retry count boundaries" -When call validate_input_python "prettier-fix" "max-retries" "0" -The status should be failure -End - -It "validates default values are secure" -When call validate_input_python "prettier-fix" "username" "github-actions" -The status should be success -End -End -End diff --git a/_tests/unit/prettier-lint/validation.spec.sh b/_tests/unit/prettier-lint/validation.spec.sh new file mode 100755 index 0000000..cae74aa --- /dev/null +++ b/_tests/unit/prettier-lint/validation.spec.sh @@ -0,0 +1,515 @@ +#!/usr/bin/env shellspec +# Unit tests for prettier-lint action validation and logic +# Framework is automatically loaded via spec_helper.sh + +Describe "prettier-lint action" +ACTION_DIR="prettier-lint" +ACTION_FILE="$ACTION_DIR/action.yml" + +Context "when validating mode input" +It "accepts check mode" +When call validate_input_python "prettier-lint" "mode" "check" +The status should be success +End + +It "accepts fix mode" +When call validate_input_python "prettier-lint" "mode" "fix" +The status should be success +End + +It "accepts empty mode (uses default)" +When call validate_input_python "prettier-lint" "mode" "" +The status should be success +End + +It "rejects invalid mode" +When call validate_input_python "prettier-lint" "mode" "invalid" +The status should be failure +End + +It "rejects mode with command injection" +When call validate_input_python "prettier-lint" "mode" "check; rm -rf /" +The status should be failure +End +End + +Context "when validating working-directory input" +It "accepts default directory" +When call validate_input_python "prettier-lint" "working-directory" "." +The status should be success +End + +It "accepts valid subdirectory" +When call validate_input_python "prettier-lint" "working-directory" "src" +The status should be success +End + +It "accepts empty working-directory (uses default)" +When call validate_input_python "prettier-lint" "working-directory" "" +The status should be success +End + +It "rejects path traversal" +When call validate_input_python "prettier-lint" "working-directory" "../../../etc" +The status should be failure +End + +It "rejects directory with command injection" +When call validate_input_python "prettier-lint" "working-directory" "src; rm -rf /" +The status should be failure +End +End + +Context "when validating prettier-version input" +It "accepts latest version" +When call validate_input_python "prettier-lint" "prettier-version" "latest" +The status should be success +End + +It "accepts semantic version" +When call validate_input_python "prettier-lint" "prettier-version" "3.2.5" +The status should be success +End + +It "accepts major.minor version" +When call validate_input_python "prettier-lint" "prettier-version" "3.2" +The status should be success +End + +It "accepts major version" +When call validate_input_python "prettier-lint" "prettier-version" "3" +The status should be success +End + +It "accepts version with pre-release" +When call validate_input_python "prettier-lint" "prettier-version" "3.0.0-alpha.1" +The status should be success +End + +It "accepts empty version (uses default)" +When call validate_input_python "prettier-lint" "prettier-version" "" +The status should be success +End + +It "rejects invalid version format" +When call validate_input_python "prettier-lint" "prettier-version" "invalid" +The status should be failure +End + +It "rejects version with command injection" +When call validate_input_python "prettier-lint" "prettier-version" "3.2.5; echo" +The status should be failure +End +End + +Context "when validating config-file input" +It "accepts default config file" +When call validate_input_python "prettier-lint" "config-file" ".prettierrc" +The status should be success +End + +It "accepts custom config file" +When call validate_input_python "prettier-lint" "config-file" ".prettierrc.js" +The status should be success +End + +It "accepts config file in subdirectory" +When call validate_input_python "prettier-lint" "config-file" "config/prettier.config.js" +The status should be success +End + +It "accepts empty config-file (uses default)" +When call validate_input_python "prettier-lint" "config-file" "" +The status should be success +End + +It "rejects config file with path traversal" +When call validate_input_python "prettier-lint" "config-file" "../../../.prettierrc" +The status should be failure +End + +It "rejects config file with command injection" +When call validate_input_python "prettier-lint" "config-file" ".prettierrc; rm -rf /" +The status should be failure +End +End + +Context "when validating ignore-file input" +It "accepts default ignore file" +When call validate_input_python "prettier-lint" "ignore-file" ".prettierignore" +The status should be success +End + +It "accepts custom ignore file" +When call validate_input_python "prettier-lint" "ignore-file" ".prettierignore.custom" +The status should be success +End + +It "accepts empty ignore-file (uses default)" +When call validate_input_python "prettier-lint" "ignore-file" "" +The status should be success +End + +It "rejects ignore file with path traversal" +When call validate_input_python "prettier-lint" "ignore-file" "../../../.prettierignore" +The status should be failure +End + +It "rejects ignore file with command injection" +When call validate_input_python "prettier-lint" "ignore-file" ".prettierignore; echo" +The status should be failure +End +End + +Context "when validating file-pattern input" +It "accepts default pattern" +When call validate_input_python "prettier-lint" "file-pattern" "**/*.{js,jsx,ts,tsx,css,scss,json,md,yaml,yml}" +The status should be success +End + +It "accepts simple pattern" +When call validate_input_python "prettier-lint" "file-pattern" "**/*.js" +The status should be success +End + +It "accepts multiple patterns" +When call validate_input_python "prettier-lint" "file-pattern" "**/*.{js,ts}" +The status should be success +End + +It "accepts specific directory pattern" +When call validate_input_python "prettier-lint" "file-pattern" "src/**/*.js" +The status should be success +End + +It "accepts empty file-pattern (uses default)" +When call validate_input_python "prettier-lint" "file-pattern" "" +The status should be success +End + +It "rejects pattern with command injection" +When call validate_input_python "prettier-lint" "file-pattern" "**/*.js; rm -rf /" +The status should be failure +End +End + +Context "when validating cache input" +It "accepts true" +When call validate_input_python "prettier-lint" "cache" "true" +The status should be success +End + +It "accepts false" +When call validate_input_python "prettier-lint" "cache" "false" +The status should be success +End + +It "accepts empty cache (uses default)" +When call validate_input_python "prettier-lint" "cache" "" +The status should be success +End + +It "rejects invalid boolean value" +When call validate_input_python "prettier-lint" "cache" "maybe" +The status should be failure +End +End + +Context "when validating fail-on-error input" +It "accepts true" +When call validate_input_python "prettier-lint" "fail-on-error" "true" +The status should be success +End + +It "accepts false" +When call validate_input_python "prettier-lint" "fail-on-error" "false" +The status should be success +End + +It "accepts empty fail-on-error (uses default)" +When call validate_input_python "prettier-lint" "fail-on-error" "" +The status should be success +End + +It "rejects invalid boolean value" +When call validate_input_python "prettier-lint" "fail-on-error" "yes" +The status should be failure +End +End + +Context "when validating report-format input" +It "accepts json format" +When call validate_input_python "prettier-lint" "report-format" "json" +The status should be success +End + +It "accepts sarif format" +When call validate_input_python "prettier-lint" "report-format" "sarif" +The status should be success +End + +It "accepts empty report-format (uses default)" +When call validate_input_python "prettier-lint" "report-format" "" +The status should be success +End + +It "rejects invalid format" +When call validate_input_python "prettier-lint" "report-format" "invalid" +The status should be failure +End + +It "rejects format with command injection" +When call validate_input_python "prettier-lint" "report-format" "json; rm -rf /" +The status should be failure +End +End + +Context "when validating max-retries input" +It "accepts default value 3" +When call validate_input_python "prettier-lint" "max-retries" "3" +The status should be success +End + +It "accepts retry count of 1" +When call validate_input_python "prettier-lint" "max-retries" "1" +The status should be success +End + +It "accepts retry count of 10" +When call validate_input_python "prettier-lint" "max-retries" "10" +The status should be success +End + +It "accepts empty max-retries (uses default)" +When call validate_input_python "prettier-lint" "max-retries" "" +The status should be success +End + +It "rejects zero retries" +When call validate_input_python "prettier-lint" "max-retries" "0" +The status should be failure +End + +It "rejects negative retry count" +When call validate_input_python "prettier-lint" "max-retries" "-1" +The status should be failure +End + +It "rejects retry count above 10" +When call validate_input_python "prettier-lint" "max-retries" "11" +The status should be failure +End + +It "rejects non-numeric retry count" +When call validate_input_python "prettier-lint" "max-retries" "abc" +The status should be failure +End + +It "rejects retry count with command injection" +When call validate_input_python "prettier-lint" "max-retries" "3; echo" +The status should be failure +End +End + +Context "when validating plugins input" +It "accepts empty plugins (optional)" +When call validate_input_python "prettier-lint" "plugins" "" +The status should be success +End + +It "accepts single plugin" +When call validate_input_python "prettier-lint" "plugins" "prettier-plugin-tailwindcss" +The status should be success +End + +It "accepts multiple plugins" +When call validate_input_python "prettier-lint" "plugins" "prettier-plugin-tailwindcss,prettier-plugin-organize-imports" +The status should be success +End + +It "accepts scoped plugin" +When call validate_input_python "prettier-lint" "plugins" "@trivago/prettier-plugin-sort-imports" +The status should be success +End + +It "rejects plugins with command injection" +When call validate_input_python "prettier-lint" "plugins" "prettier-plugin-tailwindcss; rm -rf /" +The status should be failure +End +End + +Context "when validating token input" +It "accepts valid GitHub token (classic)" +When call validate_input_python "prettier-lint" "token" "ghp_123456789012345678901234567890123456" +The status should be success +End + +It "accepts valid GitHub fine-grained token" +When call validate_input_python "prettier-lint" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a" +The status should be success +End + +It "accepts empty token (optional)" +When call validate_input_python "prettier-lint" "token" "" +The status should be success +End + +It "rejects invalid token format" +When call validate_input_python "prettier-lint" "token" "invalid-token" +The status should be failure +End + +It "rejects token with command injection" +When call validate_input_python "prettier-lint" "token" "ghp_123456789012345678901234567890123456; rm -rf /" +The status should be failure +End +End + +Context "when validating username input" +It "accepts valid username" +When call validate_input_python "prettier-lint" "username" "github-actions" +The status should be success +End + +It "accepts username with hyphens" +When call validate_input_python "prettier-lint" "username" "my-bot-user" +The status should be success +End + +It "accepts alphanumeric username" +When call validate_input_python "prettier-lint" "username" "user123" +The status should be success +End + +It "accepts empty username (uses default)" +When call validate_input_python "prettier-lint" "username" "" +The status should be success +End + +It "rejects username with command injection" +When call validate_input_python "prettier-lint" "username" "user; rm -rf /" +The status should be failure +End + +It "rejects username with special characters" +When call validate_input_python "prettier-lint" "username" "user@bot" +The status should be failure +End +End + +Context "when validating email input" +It "accepts valid email" +When call validate_input_python "prettier-lint" "email" "github-actions@github.com" +The status should be success +End + +It "accepts email with plus sign" +When call validate_input_python "prettier-lint" "email" "user+bot@example.com" +The status should be success +End + +It "accepts email with subdomain" +When call validate_input_python "prettier-lint" "email" "bot@ci.example.com" +The status should be success +End + +It "accepts empty email (uses default)" +When call validate_input_python "prettier-lint" "email" "" +The status should be success +End + +It "rejects invalid email format" +When call validate_input_python "prettier-lint" "email" "not-an-email" +The status should be failure +End + +It "rejects email with command injection" +When call validate_input_python "prettier-lint" "email" "user@example.com; rm -rf /" +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 "Prettier Lint" +End + +It "defines expected inputs" +When call get_action_inputs "$ACTION_FILE" +The output should include "mode" +The output should include "working-directory" +The output should include "prettier-version" +The output should include "config-file" +The output should include "ignore-file" +The output should include "file-pattern" +The output should include "cache" +The output should include "fail-on-error" +The output should include "report-format" +The output should include "max-retries" +The output should include "plugins" +The output should include "token" +The output should include "username" +The output should include "email" +End + +It "defines expected outputs" +When call get_action_outputs "$ACTION_FILE" +The output should include "status" +The output should include "files-checked" +The output should include "unformatted-files" +The output should include "sarif-file" +The output should include "files-changed" +End +End + +Context "when testing input requirements" +It "has all inputs as optional (with defaults)" +When call is_input_required "$ACTION_FILE" "mode" +The status should be failure +End +End + +Context "when testing security validations" +It "validates against path traversal in working-directory" +When call validate_input_python "prettier-lint" "working-directory" "../../../etc" +The status should be failure +End + +It "validates against shell metacharacters in mode" +When call validate_input_python "prettier-lint" "mode" "check|echo" +The status should be failure +End + +It "validates against command substitution in config-file" +When call validate_input_python "prettier-lint" "config-file" "\$(whoami)" +The status should be failure +End + +It "validates against path traversal in token" +When call validate_input_python "prettier-lint" "token" "../../../etc/passwd" +The status should be failure +End + +It "validates against shell metacharacters in username" +When call validate_input_python "prettier-lint" "username" "user&whoami" +The status should be failure +End + +It "validates against command injection in email" +When call validate_input_python "prettier-lint" "email" "user@example.com\`whoami\`" +The status should be failure +End + +It "validates against command injection in plugins" +When call validate_input_python "prettier-lint" "plugins" "plugin1,plugin2; rm -rf /" +The status should be failure +End +End +End diff --git a/_tests/unit/python-version-detect-v2/validation.spec.sh b/_tests/unit/python-version-detect-v2/validation.spec.sh deleted file mode 100755 index c370e45..0000000 --- a/_tests/unit/python-version-detect-v2/validation.spec.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for python-version-detect-v2 action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "python-version-detect-v2 action" -ACTION_DIR="python-version-detect-v2" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating default-version input" -It "accepts valid Python version" -When call validate_input_python "python-version-detect-v2" "default-version" "3.11" -The status should be success -End - -It "accepts Python version with patch" -When call validate_input_python "python-version-detect-v2" "default-version" "3.11.5" -The status should be success -End - -It "accepts Python 3.8" -When call validate_input_python "python-version-detect-v2" "default-version" "3.8" -The status should be success -End - -It "accepts Python 3.12" -When call validate_input_python "python-version-detect-v2" "default-version" "3.12" -The status should be success -End - -It "rejects Python version too old" -When call validate_input_python "python-version-detect-v2" "default-version" "2.7" -The status should be failure -End - -It "rejects invalid version format" -When call validate_input_python "python-version-detect-v2" "default-version" "python3.11" -The status should be failure -End - -It "rejects version with command injection" -When call validate_input_python "python-version-detect-v2" "default-version" "3.11; rm -rf /" -The status should be failure -End - -It "rejects empty version" -When call validate_input_python "python-version-detect-v2" "default-version" "" -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 "Python Version Detect v2" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "default-version" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "python-version" -End -End - -Context "when testing input requirements" -It "has default-version as optional input" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional" -The output should equal "optional" -End -End - -Context "when testing security validations" -It "validates against path traversal in version" -When call validate_input_python "python-version-detect-v2" "default-version" "../3.11" -The status should be failure -End - -It "validates against shell metacharacters in version" -When call validate_input_python "python-version-detect-v2" "default-version" "3.11|echo" -The status should be failure -End - -It "validates against backtick injection" -When call validate_input_python "python-version-detect-v2" "default-version" "3.11\`whoami\`" -The status should be failure -End -End -End diff --git a/_tests/unit/python-version-detect/validation.spec.sh b/_tests/unit/python-version-detect/validation.spec.sh deleted file mode 100755 index 16aa8c8..0000000 --- a/_tests/unit/python-version-detect/validation.spec.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for python-version-detect action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "python-version-detect action" -ACTION_DIR="python-version-detect" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating default-version input" -It "accepts valid Python version" -When call validate_input_python "python-version-detect" "default-version" "3.11" -The status should be success -End - -It "accepts Python version with patch" -When call validate_input_python "python-version-detect" "default-version" "3.11.5" -The status should be success -End - -It "accepts Python 3.8" -When call validate_input_python "python-version-detect" "default-version" "3.8" -The status should be success -End - -It "accepts Python 3.12" -When call validate_input_python "python-version-detect" "default-version" "3.12" -The status should be success -End - -It "rejects Python version too old" -When call validate_input_python "python-version-detect" "default-version" "2.7" -The status should be failure -End - -It "rejects Python version too new" -When call validate_input_python "python-version-detect" "default-version" "4.0" -The status should be failure -End - -It "rejects invalid version format" -When call validate_input_python "python-version-detect" "default-version" "python3.11" -The status should be failure -End - -It "rejects version with command injection" -When call validate_input_python "python-version-detect" "default-version" "3.11; rm -rf /" -The status should be failure -End - -It "rejects version without minor" -When call validate_input_python "python-version-detect" "default-version" "3" -The status should be failure -End - -It "rejects empty version" -When call validate_input_python "python-version-detect" "default-version" "" -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 "Python Version Detect" -End - -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "default-version" -End - -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "python-version" -End -End - -Context "when testing input requirements" -It "has default-version as optional input" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional" -The output should equal "optional" -End -End - -Context "when testing security validations" -It "validates against path traversal in version" -When call validate_input_python "python-version-detect" "default-version" "../3.11" -The status should be failure -End - -It "validates against shell metacharacters in version" -When call validate_input_python "python-version-detect" "default-version" "3.11|echo" -The status should be failure -End - -It "validates against backtick injection" -When call validate_input_python "python-version-detect" "default-version" "3.11\`whoami\`" -The status should be failure -End -End -End diff --git a/_tests/unit/set-git-config/validation.spec.sh b/_tests/unit/set-git-config/validation.spec.sh deleted file mode 100755 index 821012f..0000000 --- a/_tests/unit/set-git-config/validation.spec.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for set-git-config action validation and logic -# Framework is automatically loaded via spec_helper.sh - -Describe "set-git-config action" - ACTION_DIR="set-git-config" - ACTION_FILE="$ACTION_DIR/action.yml" - - Context "when validating inputs (no validation logic in action)" - # NOTE: This action has no validation logic - all inputs are accepted - # The action simply passes through values and conditionally sets outputs - It "accepts valid token value" - When call validate_input_python "set-git-config" "token" "ghp_123456789012345678901234567890123456" - The status should be success - End - It "accepts any username value" - When call validate_input_python "set-git-config" "username" "any-username" - The status should be success - End - It "accepts valid email value" - When call validate_input_python "set-git-config" "email" "test@example.com" - The status should be success - End - It "accepts any is_fiximus value" - When call validate_input_python "set-git-config" "is_fiximus" "any-value" - 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 "Set Git Config" - End - - It "defines required inputs" - inputs=$(get_action_inputs "$ACTION_FILE") - When call echo "$inputs" - The output should include "token" - The output should include "username" - The output should include "email" - The output should include "is_fiximus" - End - - It "defines expected outputs" - outputs=$(get_action_outputs "$ACTION_FILE") - When call echo "$outputs" - The output should include "token" - The output should include "username" - The output should include "email" - The output should include "is_fiximus" - End - End - - Context "when testing outputs" - It "produces all expected outputs" - When call test_action_outputs "$ACTION_DIR" "token" "ghp_test" "username" "test" "email" "test@example.com" "is_fiximus" "false" - The status should be success - The stderr should include "Testing action outputs for: set-git-config" - The stderr should include "Output test passed for: set-git-config" - End - End -End diff --git a/_tests/unit/version-validator/validation.spec.sh b/_tests/unit/version-validator/validation.spec.sh deleted file mode 100755 index eae6788..0000000 --- a/_tests/unit/version-validator/validation.spec.sh +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env shellspec -# Unit tests for version-validator action validation and logic - -# Framework is automatically loaded via spec_helper.sh - -Describe "version-validator action" -ACTION_DIR="version-validator" -ACTION_FILE="$ACTION_DIR/action.yml" - -Context "when validating version input" -It "accepts valid semantic version" -When call validate_input_python "version-validator" "version" "1.2.3" -The status should be success -End -It "accepts semantic version with v prefix" -When call validate_input_python "version-validator" "version" "v1.2.3" -The status should be success -End -It "accepts prerelease version" -When call validate_input_python "version-validator" "version" "1.2.3-alpha" -The status should be success -End -It "accepts prerelease with number" -When call validate_input_python "version-validator" "version" "1.2.3-alpha.1" -The status should be success -End -It "accepts build metadata" -When call validate_input_python "version-validator" "version" "1.2.3+build.1" -The status should be success -End -It "accepts prerelease with build metadata" -When call validate_input_python "version-validator" "version" "1.2.3-alpha.1+build.1" -The status should be success -End -It "accepts CalVer format" -When call validate_input_python "version-validator" "version" "2024.3.1" -The status should be success -End -It "rejects invalid version format" -When call validate_input_python "version-validator" "version" "invalid.version" -The status should be failure -End -It "rejects version with command injection" -When call validate_input_python "version-validator" "version" "1.2.3; rm -rf /" -The status should be failure -End -It "rejects version with shell expansion" -When call validate_input_python "version-validator" "version" "1.2.3\$(whoami)" -The status should be failure -End -It "rejects empty version" -When call validate_input_python "version-validator" "version" "" -The status should be failure -End -End - -Context "when validating validation-regex input" -It "accepts valid regex pattern" -When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\.[0-9]+\.[0-9]+$" -The status should be success -End -It "accepts semantic version regex" -When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$" -The status should be success -End -It "accepts empty validation-regex (uses default)" -When call validate_input_python "version-validator" "validation-regex" "" -The status should be success -End -It "accepts valid regex patterns with quantifiers" -When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\\.[0-9]+$" -The status should be success -End -It "rejects regex with command injection" -When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$; rm -rf /" -The status should be failure -End -End - -Context "when validating ReDoS patterns" -It "rejects nested quantifiers (a+)+" -When call validate_input_python "version-validator" "validation-regex" "(a+)+" -The status should be failure -End -It "rejects nested quantifiers (a*)+" -When call validate_input_python "version-validator" "validation-regex" "(a*)+" -The status should be failure -End -It "rejects nested quantifiers (a+)*" -When call validate_input_python "version-validator" "validation-regex" "(a+)*" -The status should be failure -End -It "rejects nested quantifiers (a*)*" -When call validate_input_python "version-validator" "validation-regex" "(a*)*" -The status should be failure -End -It "rejects quantified groups (a+){2,5}" -When call validate_input_python "version-validator" "validation-regex" "(a+){2,5}" -The status should be failure -End -It "rejects consecutive quantifiers .*.* (ReDoS)" -When call validate_input_python "version-validator" "validation-regex" ".*.*" -The status should be failure -End -It "rejects consecutive quantifiers .*+ (ReDoS)" -When call validate_input_python "version-validator" "validation-regex" ".*+" -The status should be failure -End -It "rejects duplicate alternatives (a|a)+" -When call validate_input_python "version-validator" "validation-regex" "(a|a)+" -The status should be failure -End -It "rejects overlapping alternatives (a|ab)+" -When call validate_input_python "version-validator" "validation-regex" "(a|ab)+" -The status should be failure -End -It "accepts safe pattern with single quantifier" -When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$" -The status should be success -End -It "accepts safe pattern with character class" -When call validate_input_python "version-validator" "validation-regex" "^[a-zA-Z0-9]+$" -The status should be success -End -It "accepts safe pattern with optional group" -When call validate_input_python "version-validator" "validation-regex" "^[0-9]+(\\.[0-9]+)?$" -The status should be success -End -It "accepts safe alternation without repetition" -When call validate_input_python "version-validator" "validation-regex" "^(alpha|beta|gamma)$" -The status should be success -End -End - -Context "when validating language input" -It "accepts valid language name" -When call validate_input_python "version-validator" "language" "nodejs" -The status should be success -End -It "accepts version as language" -When call validate_input_python "version-validator" "language" "version" -The status should be success -End -It "accepts empty language (uses default)" -When call validate_input_python "version-validator" "language" "" -The status should be success -End -It "rejects language with command injection" -When call validate_input_python "version-validator" "language" "version; rm -rf /" -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 match pattern "*Version*" -End -It "defines expected inputs" -When call get_action_inputs "$ACTION_FILE" -The output should include "version" -The output should include "validation-regex" -The output should include "language" -End -It "defines expected outputs" -When call get_action_outputs "$ACTION_FILE" -The output should include "is-valid" -The output should include "validated-version" -The output should include "error-message" -End -End - -Context "when testing input requirements" -It "requires version input" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "version" "required" -The status should be success -The output should equal "required" -End -It "has validation-regex as optional input" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "validation-regex" "optional" -The status should be success -The output should equal "optional" -End -It "has language as optional input" -When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "language" "optional" -The status should be success -The output should equal "optional" -End -End - -Context "when testing security validations" -It "validates against path traversal in version" -When call validate_input_python "version-validator" "version" "../1.2.3" -The status should be failure -End -It "validates against shell metacharacters in version" -When call validate_input_python "version-validator" "version" "1.2.3|echo" -The status should be failure -End -It "validates against backtick injection in language" -When call validate_input_python "version-validator" "language" "version\`whoami\`" -The status should be failure -End -It "validates against variable expansion in version" -When call validate_input_python "version-validator" "version" "1.2.3\${HOME}" -The status should be failure -End -End - -Context "when testing version validation functionality" -It "validates semantic version format restrictions" -When call validate_input_python "version-validator" "version" "1.2" -The status should be success -End -It "validates regex pattern safety" -When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$" -The status should be success -End -It "validates language parameter format" -When call validate_input_python "version-validator" "language" "NODEJS" -The status should be success -End -It "validates complex version formats" -When call validate_input_python "version-validator" "version" "1.0.0-beta.1+exp.sha.5114f85" -The status should be success -End -End -End diff --git a/ansible-lint-fix/action.yml b/ansible-lint-fix/action.yml index 014033a..b281bcb 100644 --- a/ansible-lint-fix/action.yml +++ b/ansible-lint-fix/action.yml @@ -45,55 +45,19 @@ runs: steps: - name: Validate Inputs id: validate - shell: bash - env: - GITHUB_TOKEN: ${{ inputs.token }} - EMAIL: ${{ inputs.email }} - USERNAME: ${{ inputs.username }} - MAX_RETRIES: ${{ inputs.max-retries }} - run: | - set -euo pipefail - - # 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 - fi - - # Validate email format (basic check) - if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" - 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 username length - username="$USERNAME" - if [ ${#username} -gt 39 ]; then - echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters" - exit 1 - fi - - # Validate max retries (positive integer with reasonable upper limit) - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$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 - - echo "Input validation completed successfully" + uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 + with: + action-type: 'ansible-lint-fix' + token: ${{ inputs.token }} + email: ${{ inputs.email }} + username: ${{ inputs.username }} + max-retries: ${{ inputs.max-retries }} - name: Check for Ansible Files id: check-files - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Check for both .yml and .yaml files if find . \( -name "*.yml" -o -name "*.yaml" \) -type f | grep -q .; then @@ -122,18 +86,18 @@ runs: - name: Install ansible-lint id: install-ansible-lint if: steps.check-files.outputs.files_found == 'true' - uses: ivuorinen/actions/common-retry@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 with: + timeout_minutes: 5 + max_attempts: ${{ inputs.max-retries }} command: 'pip install ansible-lint==6.22.1' - max-retries: ${{ inputs.max-retries }} - description: 'Installing Python dependencies (ansible-lint)' - name: Run ansible-lint if: steps.check-files.outputs.files_found == 'true' id: lint - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Run ansible-lint and capture exit code if ansible-lint --write --parseable-severity --format sarif > ansible-lint.sarif; then @@ -159,28 +123,13 @@ runs: # Exit with the original ansible-lint exit code exit "$lint_exit_code" - - name: Set Git Config for Fixes - id: set-git-config - if: steps.check-files.outputs.files_found == 'true' - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} - - name: Commit Fixes if: steps.check-files.outputs.files_found == 'true' - shell: bash - run: | - set -euo pipefail - - if git diff --quiet; then - echo "No changes to commit." - else - git add . - git commit -m "fix: applied ansible lint fixes" - git push - fi + uses: stefanzweifel/git-auto-commit-action@be7095c202abcf573b09f20541e0ee2f6a3a9d9b # v5.0.1 + with: + commit_message: 'style: apply ansible lint fixes' + commit_user_name: ${{ inputs.username }} + commit_user_email: ${{ inputs.email }} - name: Upload SARIF Report if: steps.check-files.outputs.files_found == 'true' diff --git a/biome-check/README.md b/biome-check/README.md deleted file mode 100644 index ae6c975..0000000 --- a/biome-check/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# ivuorinen/actions/biome-check - -## Biome Check - -### Description - -Run Biome check on the repository - -### Inputs - -| name | description | required | default | -|---------------|--------------------------------------------------------------------|----------|-----------------------------| -| `token` |

GitHub token for authentication

| `false` | `${{ github.token }}` | -| `username` |

GitHub username for commits

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

GitHub email for commits

| `false` | `github-actions@github.com` | -| `max-retries` |

Maximum number of retry attempts for npm install operations

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

Check status (success/failure)

| -| `errors_count` |

Number of errors found

| -| `warnings_count` |

Number of warnings found

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/biome-check@main - with: - token: - # GitHub token for authentication - # - # Required: false - # Default: ${{ github.token }} - - username: - # GitHub username for commits - # - # Required: false - # Default: github-actions - - email: - # GitHub email for commits - # - # Required: false - # Default: github-actions@github.com - - max-retries: - # Maximum number of retry attempts for npm install operations - # - # Required: false - # Default: 3 -``` diff --git a/biome-check/rules.yml b/biome-check/rules.yml deleted file mode 100644 index d151c8c..0000000 --- a/biome-check/rules.yml +++ /dev/null @@ -1,41 +0,0 @@ ---- -# Validation rules for biome-check action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (4/4 inputs) -# -# This file defines validation rules for the biome-check GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: biome-check -description: Run Biome check on the repository -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - email - - max-retries - - token - - username -conventions: - email: email - max-retries: numeric_range_1_10 - token: github_token - username: username -overrides: {} -statistics: - total_inputs: 4 - validated_inputs: 4 - 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: false - has_file_validation: false - has_security_validation: true diff --git a/biome-fix/README.md b/biome-fix/README.md deleted file mode 100644 index d00a5b4..0000000 --- a/biome-fix/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# ivuorinen/actions/biome-fix - -## Biome Fix - -### Description - -Run Biome fix on the repository - -### Inputs - -| name | description | required | default | -|---------------|--------------------------------------------------------------------|----------|-----------------------------| -| `token` |

GitHub token for authentication

| `false` | `${{ github.token }}` | -| `username` |

GitHub username for commits

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

GitHub email for commits

| `false` | `github-actions@github.com` | -| `max-retries` |

Maximum number of retry attempts for npm install operations

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

Number of files changed by formatting

| -| `fix_status` |

Fix status (success/failure)

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/biome-fix@main - with: - token: - # GitHub token for authentication - # - # Required: false - # Default: ${{ github.token }} - - username: - # GitHub username for commits - # - # Required: false - # Default: github-actions - - email: - # GitHub email for commits - # - # Required: false - # Default: github-actions@github.com - - max-retries: - # Maximum number of retry attempts for npm install operations - # - # Required: false - # Default: 3 -``` diff --git a/biome-fix/action.yml b/biome-fix/action.yml deleted file mode 100644 index cdf6f67..0000000 --- a/biome-fix/action.yml +++ /dev/null @@ -1,204 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: write # Required for pushing fixes back to repository ---- -name: Biome Fix -description: Run Biome fix on the repository -author: Ismo Vuorinen - -branding: - icon: check-circle - color: green - -inputs: - token: - description: 'GitHub token for authentication' - required: false - default: ${{ github.token }} - username: - description: 'GitHub username for commits' - required: false - default: 'github-actions' - email: - description: 'GitHub email for commits' - required: false - default: 'github-actions@github.com' - max-retries: - description: 'Maximum number of retry attempts for npm install operations' - required: false - default: '3' - -outputs: - files_changed: - description: 'Number of files changed by formatting' - value: ${{ steps.fix.outputs.files_changed }} - fix_status: - description: 'Fix status (success/failure)' - value: ${{ steps.fix.outputs.status }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: sh - env: - GITHUB_TOKEN: ${{ inputs.token }} - EMAIL: ${{ inputs.email }} - USERNAME: ${{ inputs.username }} - MAX_RETRIES: ${{ inputs.max-retries }} - run: | - 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 ! echo "$GITHUB_TOKEN" | grep -Eq '^gh[efpousr]_[a-zA-Z0-9]{36}$' && ! echo "$GITHUB_TOKEN" | grep -q '^\${{'; then - echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters" - fi - fi - - # Validate email format (basic check) - case "$EMAIL" in - *@*.*) ;; - *) - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" - exit 1 - ;; - esac - - # Validate username format (prevent command injection) - if echo "$USERNAME" | grep -Eq '[;&|]'; then - echo "::error::Invalid username: '$USERNAME'. Command injection patterns not allowed" - exit 1 - fi - - # Validate username length - username="$USERNAME" - username_len=$(echo -n "$username" | wc -c | tr -d ' ') - if [ "$username_len" -gt 39 ]; then - echo "::error::Username too long: ${username_len} characters. GitHub usernames are max 39 characters" - exit 1 - fi - - # Validate max retries (positive integer with reasonable upper limit) - if ! echo "$MAX_RETRIES" | grep -Eq '^[0-9]+$' || [ "$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 - - echo "Input validation completed successfully" - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token }} - - - name: Set Git Config - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} - - - name: Node Setup - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 - - - name: Cache Node Dependencies - id: cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'biome-fix-${{ steps.node-setup.outputs.package-manager }}' - - - name: Install Biome - shell: sh - env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} - MAX_RETRIES: ${{ inputs.max-retries }} - run: | - set -eu - - # Check if biome is already installed - if command -v biome >/dev/null 2>&1; then - echo "โœ… Biome already installed: $(biome --version)" - exit 0 - fi - - echo "Installing Biome using $PACKAGE_MANAGER..." - - for attempt in $(seq 1 "$MAX_RETRIES"); do - echo "Attempt $attempt of $MAX_RETRIES" - - case "$PACKAGE_MANAGER" in - "pnpm") - if pnpm add -g @biomejs/biome; then - echo "โœ… Biome installed successfully with pnpm" - exit 0 - fi - ;; - "yarn") - if yarn global add @biomejs/biome; then - echo "โœ… Biome installed successfully with yarn" - exit 0 - fi - ;; - "bun") - if bun add -g @biomejs/biome; then - echo "โœ… Biome installed successfully with bun" - exit 0 - fi - ;; - "npm"|*) - if npm install -g @biomejs/biome; then - echo "โœ… Biome installed successfully with npm" - exit 0 - fi - ;; - esac - - if [ $attempt -lt "$MAX_RETRIES" ]; then - echo "โŒ Installation failed, retrying in 5 seconds..." - sleep 5 - fi - done - - echo "::error::Failed to install Biome after $MAX_RETRIES attempts" - exit 1 - - - name: Run Biome Fix - id: fix - shell: sh - run: | - set -eu - - echo "Running Biome fix..." - - # Run Biome fix and capture exit code - biome_exit_code=0 - biome check --write . || biome_exit_code=$? - - # Count changed files using git diff (strip whitespace from wc output) - files_changed=$(git diff --name-only | wc -l | tr -d ' ') - - # Set status based on biome check result and changes - if [ $biome_exit_code -eq 0 ] && [ "$files_changed" -eq 0 ]; then - status="success" - else - status="failure" - fi - - echo "files_changed=$files_changed" >> "$GITHUB_OUTPUT" - echo "status=$status" >> "$GITHUB_OUTPUT" - - echo "โœ… Biome fix completed. Files changed: $files_changed, Status: $status" - - - name: Push Fixes - if: success() - uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 - with: - commit_message: 'style: autofix Biome violations' - add_options: '-u' diff --git a/biome-fix/rules.yml b/biome-fix/rules.yml deleted file mode 100644 index cf2b54e..0000000 --- a/biome-fix/rules.yml +++ /dev/null @@ -1,41 +0,0 @@ ---- -# Validation rules for biome-fix action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (4/4 inputs) -# -# This file defines validation rules for the biome-fix GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: biome-fix -description: Run Biome fix on the repository -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - email - - max-retries - - token - - username -conventions: - email: email - max-retries: numeric_range_1_10 - token: github_token - username: username -overrides: {} -statistics: - total_inputs: 4 - validated_inputs: 4 - 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: false - has_file_validation: false - has_security_validation: true diff --git a/biome-lint/README.md b/biome-lint/README.md new file mode 100644 index 0000000..1d53978 --- /dev/null +++ b/biome-lint/README.md @@ -0,0 +1,73 @@ +# ivuorinen/actions/biome-lint + +## Biome Lint + +### Description + +Run Biome linter in check or fix mode + +### Inputs + +| name | description | required | default | +|-----------------|---------------------------------------------------------------------------------|----------|-----------------------------| +| `mode` |

Mode to run (check or fix)

| `false` | `check` | +| `token` |

GitHub token for authentication

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

GitHub username for commits (fix mode only)

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

GitHub email for commits (fix mode only)

| `false` | `github-actions@github.com` | +| `max-retries` |

Maximum number of retry attempts for npm install operations

| `false` | `3` | +| `fail-on-error` |

Whether to fail the action if linting errors are found (check mode only)

| `false` | `true` | + +### Outputs + +| name | description | +|------------------|---------------------------------------------------| +| `status` |

Overall status (success/failure)

| +| `errors_count` |

Number of errors found (check mode only)

| +| `warnings_count` |

Number of warnings found (check mode only)

| +| `files_changed` |

Number of files changed (fix mode only)

| + +### Runs + +This action is a `composite` action. + +### Usage + +```yaml +- uses: ivuorinen/actions/biome-lint@main + with: + mode: + # Mode to run (check or fix) + # + # Required: false + # Default: check + + token: + # GitHub token for authentication + # + # Required: false + # Default: "" + + username: + # GitHub username for commits (fix mode only) + # + # Required: false + # Default: github-actions + + email: + # GitHub email for commits (fix mode only) + # + # Required: false + # Default: github-actions@github.com + + max-retries: + # Maximum number of retry attempts for npm install operations + # + # Required: false + # Default: 3 + + fail-on-error: + # Whether to fail the action if linting errors are found (check mode only) + # + # Required: false + # Default: true +``` diff --git a/biome-check/action.yml b/biome-lint/action.yml similarity index 53% rename from biome-check/action.yml rename to biome-lint/action.yml index c3aea76..7e88fca 100644 --- a/biome-check/action.yml +++ b/biome-lint/action.yml @@ -1,10 +1,10 @@ # yaml-language-server: $schema=https://json.schemastore.org/github-action.json # permissions: -# - contents: read # Required for checking out repository -# - security-events: write # Required for uploading SARIF results +# - contents: write # Required for fix mode to push changes +# - security-events: write # Required for check mode to upload SARIF --- -name: Biome Check -description: Run Biome check on the repository +name: Biome Lint +description: Run Biome linter in check or fix mode author: Ismo Vuorinen branding: @@ -12,90 +12,110 @@ branding: color: green inputs: + mode: + description: 'Mode to run (check or fix)' + required: false + default: 'check' token: description: 'GitHub token for authentication' required: false - default: ${{ github.token }} + default: '' username: - description: 'GitHub username for commits' + description: 'GitHub username for commits (fix mode only)' required: false default: 'github-actions' email: - description: 'GitHub email for commits' + description: 'GitHub email for commits (fix mode only)' required: false default: 'github-actions@github.com' max-retries: description: 'Maximum number of retry attempts for npm install operations' required: false default: '3' + fail-on-error: + description: 'Whether to fail the action if linting errors are found (check mode only)' + required: false + default: 'true' outputs: - check_status: - description: 'Check status (success/failure)' - value: ${{ steps.check.outputs.status }} + status: + description: 'Overall status (success/failure)' + value: ${{ steps.check.outputs.status || steps.fix.outputs.status }} errors_count: - description: 'Number of errors found' + description: 'Number of errors found (check mode only)' value: ${{ steps.check.outputs.errors }} warnings_count: - description: 'Number of warnings found' + description: 'Number of warnings found (check mode only)' value: ${{ steps.check.outputs.warnings }} + files_changed: + description: 'Number of files changed (fix mode only)' + value: ${{ steps.fix.outputs.files_changed }} runs: using: composite steps: - - name: Validate Inputs (Centralized) - uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - action-type: biome-check - - - name: Validate Inputs (Additional) + - name: Validate Inputs id: validate shell: bash env: + MODE: ${{ inputs.mode }} GITHUB_TOKEN: ${{ inputs.token }} EMAIL: ${{ inputs.email }} USERNAME: ${{ inputs.username }} MAX_RETRIES: ${{ inputs.max-retries }} + FAIL_ON_ERROR: ${{ inputs.fail-on-error }} run: | set -euo pipefail - # Validate GitHub token presence (no format validation to avoid false warnings) + # Validate mode + case "$MODE" in + "check"|"fix") + echo "Mode: $MODE" + ;; + *) + echo "::error::Invalid mode: '$MODE'. Must be 'check' or 'fix'" + exit 1 + ;; + esac + + # Validate GitHub token presence if provided if [[ -n "$GITHUB_TOKEN" ]] && ! [[ "$GITHUB_TOKEN" =~ ^\$\{\{ ]]; then - # Token is present and not a GitHub expression, assume it's valid echo "Using provided GitHub token" fi - # Validate email format (basic check) - if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" - exit 1 - 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 - # Validate username format (GitHub canonical rules) - username="$USERNAME" + # Validate username format (GitHub canonical rules) + username="$USERNAME" - # Check length (GitHub limit) - if [ ${#username} -gt 39 ]; then - echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters" - exit 1 - fi + # Check length (GitHub limit) + if [ ${#username} -gt 39 ]; then + echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters" + exit 1 + 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 + # 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 - # 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 + # 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 - # Check no consecutive hyphens - if [[ "$username" == *--* ]]; then - echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" - exit 1 + # Check no consecutive hyphens + if [[ "$username" == *--* ]]; then + echo "::error::Invalid username '$username'. Consecutive hyphens not allowed" + exit 1 + fi fi # Validate max retries (positive integer with reasonable upper limit) @@ -104,19 +124,18 @@ runs: exit 1 fi + # Validate fail-on-error (boolean) + 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 + echo "Input validation completed successfully" - name: Checkout Repository uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta with: - token: ${{ inputs.token }} - - - name: Set Git Config - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} + token: ${{ inputs.token || github.token }} - name: Node Setup id: node-setup @@ -129,7 +148,7 @@ runs: type: 'npm' paths: 'node_modules' key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'biome-check-${{ steps.node-setup.outputs.package-manager }}' + key-prefix: 'biome-lint-${{ inputs.mode }}-${{ steps.node-setup.outputs.package-manager }}' - name: Install Biome shell: bash @@ -187,12 +206,15 @@ runs: exit 1 - name: Run Biome Check + if: inputs.mode == 'check' id: check shell: bash + env: + FAIL_ON_ERROR: ${{ inputs.fail-on-error }} run: | set -euo pipefail - echo "Running Biome check..." + echo "Running Biome check mode..." # Run Biome check with SARIF reporter biome_exit_code=0 @@ -218,21 +240,58 @@ runs: echo "status=success" >> "$GITHUB_OUTPUT" echo "errors=0" >> "$GITHUB_OUTPUT" echo "warnings=0" >> "$GITHUB_OUTPUT" + echo "โœ… Biome check completed successfully" else echo "status=failure" >> "$GITHUB_OUTPUT" echo "errors=$errors" >> "$GITHUB_OUTPUT" echo "warnings=$warnings" >> "$GITHUB_OUTPUT" - echo "::error::Biome check found $errors issues" fi - echo "โœ… Biome check completed" + # Exit with biome's exit code if fail-on-error is true + if [ "$FAIL_ON_ERROR" = "true" ]; then + exit $biome_exit_code + fi - # Exit with biome's exit code to fail the job on errors - exit $biome_exit_code - - - name: Upload Biome Results - if: always() + - name: Upload SARIF Report + if: inputs.mode == 'check' && always() uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: sarif_file: biome-report.sarif + + - name: Run Biome Fix + if: inputs.mode == 'fix' + id: fix + shell: bash + run: | + set -euo pipefail + + echo "Running Biome fix mode..." + + # Run Biome fix and capture exit code + biome_exit_code=0 + biome check --write . || biome_exit_code=$? + + # Count changed files using git diff + files_changed=$(git diff --name-only | wc -l | tr -d ' ') + + # Set status based on biome check result and changes + if [ $biome_exit_code -eq 0 ] && [ "$files_changed" -eq 0 ]; then + status="success" + echo "โœ… No changes needed" + else + status="failure" + echo "โš ๏ธ Fixed $files_changed file(s)" + fi + + printf '%s\n' "files_changed=$files_changed" >> "$GITHUB_OUTPUT" + printf '%s\n' "status=$status" >> "$GITHUB_OUTPUT" + + - name: Commit and Push Fixes + if: inputs.mode == 'fix' && success() + uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 + with: + commit_message: 'style: autofix Biome violations' + commit_user_name: ${{ inputs.username }} + commit_user_email: ${{ inputs.email }} + add_options: '-u' diff --git a/eslint-fix/rules.yml b/biome-lint/rules.yml similarity index 70% rename from eslint-fix/rules.yml rename to biome-lint/rules.yml index 7bdfd1b..c577002 100644 --- a/eslint-fix/rules.yml +++ b/biome-lint/rules.yml @@ -1,33 +1,37 @@ --- -# Validation rules for eslint-fix action +# Validation rules for biome-lint action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 -# Coverage: 100% (4/4 inputs) +# Coverage: 100% (6/6 inputs) # -# This file defines validation rules for the eslint-fix GitHub Action. +# This file defines validation rules for the biome-lint GitHub Action. # Rules are automatically applied by validate-inputs action when this # action is used. # schema_version: '1.0' -action: eslint-fix -description: Fixes ESLint violations in a project. +action: biome-lint +description: Run Biome linter in check or fix mode generator_version: 1.0.0 required_inputs: [] optional_inputs: - email + - fail-on-error - max-retries + - mode - token - username conventions: email: email + fail-on-error: boolean max-retries: numeric_range_1_10 + mode: mode_enum token: github_token username: username overrides: {} statistics: - total_inputs: 4 - validated_inputs: 4 + total_inputs: 6 + validated_inputs: 6 skipped_inputs: 0 coverage_percentage: 100 validation_coverage: 100 diff --git a/codeql-analysis/README.md b/codeql-analysis/README.md index bd19a6b..2cba76d 100644 --- a/codeql-analysis/README.md +++ b/codeql-analysis/README.md @@ -26,7 +26,6 @@ Run CodeQL security analysis for a single language with configurable query suite | `threads` |

Number of threads that can be used by CodeQL

| `false` | `""` | | `output` |

Path to save SARIF results

| `false` | `../results` | | `skip-queries` |

Build database but skip running queries

| `false` | `false` | -| `add-snippets` |

Add code snippets to SARIF output

| `false` | `false` | ### Outputs @@ -140,10 +139,4 @@ This action is a `composite` action. # # Required: false # Default: false - - add-snippets: - # Add code snippets to SARIF output - # - # Required: false - # Default: false ``` diff --git a/codeql-analysis/action.yml b/codeql-analysis/action.yml index d1c1101..fb99da6 100644 --- a/codeql-analysis/action.yml +++ b/codeql-analysis/action.yml @@ -90,11 +90,6 @@ inputs: required: false default: 'false' - add-snippets: - description: 'Add code snippets to SARIF output' - required: false - default: 'false' - outputs: language-analyzed: description: 'Language that was analyzed' @@ -131,7 +126,6 @@ runs: threads: ${{ inputs.threads }} output: ${{ inputs.output }} skip-queries: ${{ inputs.skip-queries }} - add-snippets: ${{ inputs.add-snippets }} - name: Validate checkout safety shell: bash @@ -214,7 +208,6 @@ runs: output: ${{ inputs.output }} ram: ${{ inputs.ram }} threads: ${{ inputs.threads }} - add-snippets: ${{ inputs.add-snippets }} skip-queries: ${{ inputs.skip-queries }} - name: Summary diff --git a/codeql-analysis/rules.yml b/codeql-analysis/rules.yml index 75daed3..86c3490 100644 --- a/codeql-analysis/rules.yml +++ b/codeql-analysis/rules.yml @@ -2,7 +2,7 @@ # Validation rules for codeql-analysis action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 -# Coverage: 94% (16/17 inputs) +# Coverage: 94% (15/16 inputs) # # This file defines validation rules for the codeql-analysis GitHub Action. # Rules are automatically applied by validate-inputs action when this @@ -16,7 +16,6 @@ generator_version: 1.0.0 required_inputs: - language optional_inputs: - - add-snippets - build-mode - category - checkout-ref @@ -33,7 +32,6 @@ optional_inputs: - upload-results - working-directory conventions: - add-snippets: boolean build-mode: codeql_build_mode category: category_format checkout-ref: branch_name @@ -62,8 +60,8 @@ overrides: threads: numeric_range_1_128 token: github_token statistics: - total_inputs: 17 - validated_inputs: 16 + total_inputs: 16 + validated_inputs: 15 skipped_inputs: 0 coverage_percentage: 94 validation_coverage: 94 diff --git a/common-file-check/CustomValidator.py b/common-file-check/CustomValidator.py deleted file mode 100755 index 1bcfb0e..0000000 --- a/common-file-check/CustomValidator.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for common-file-check action. - -This validator handles file checking validation including: -- File patterns with glob support (*, ?, **, {}, []) -- Path security validation -- Injection detection -""" - -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.boolean import BooleanValidator -from validators.file import FileValidator - - -class CustomValidator(BaseValidator): - """Custom validator for common-file-check action. - - Provides validation for file pattern checking. - """ - - def __init__(self, action_type: str = "common-file-check") -> None: - """Initialize the common-file-check validator.""" - super().__init__(action_type) - self.file_validator = FileValidator(action_type) - self.boolean_validator = BooleanValidator(action_type) - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate common-file-check specific inputs. - - Args: - inputs: Dictionary of input names to values - - Returns: - True if all validations pass, False otherwise - """ - valid = True - - # Validate file-pattern (required) - if "file-pattern" in inputs: - valid &= self.validate_file_pattern(inputs["file-pattern"]) - elif "file_pattern" in inputs: - valid &= self.validate_file_pattern(inputs["file_pattern"]) - else: - # File pattern is required - self.add_error("File pattern is required") - valid = False - - # Validate fail-on-missing (optional) - if inputs.get("fail-on-missing") or inputs.get("fail_on_missing"): - fail_on_missing = inputs.get("fail-on-missing", inputs.get("fail_on_missing")) - # Use BooleanValidator for boolean validation - result = self.boolean_validator.validate_optional_boolean( - fail_on_missing, "fail-on-missing" - ) - # Propagate errors - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - valid &= result - - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs for common-file-check. - - Returns: - List of required input names - """ - return ["file-pattern"] - - def get_validation_rules(self) -> dict: - """Get validation rules for common-file-check. - - Returns: - Dictionary of validation rules - """ - return { - "file-pattern": "File glob pattern to check", - "fail-on-missing": "Whether to fail if file is missing (true/false)", - } - - def validate_file_pattern(self, pattern: str) -> bool: - """Validate file pattern (glob pattern). - - Args: - pattern: File pattern with glob support - - Returns: - True if valid, False otherwise - """ - # Check for empty - if not pattern or not pattern.strip(): - self.add_error("File pattern cannot be empty") - return False - - # Allow GitHub Actions expressions - if self.is_github_expression(pattern): - return True - - # Use base validator's path security check - if not self.validate_path_security(pattern, "file-pattern"): - return False - - # Also check for command injection patterns using base validator - return self.validate_security_patterns(pattern, "file-pattern") diff --git a/common-file-check/README.md b/common-file-check/README.md deleted file mode 100644 index 18bbc22..0000000 --- a/common-file-check/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# ivuorinen/actions/common-file-check - -## Common File Check - -### Description - -A reusable action to check if a specific file or type of files exists in the repository. -Emits an output "found" which is true or false. - -### Inputs - -| name | description | required | default | -|----------------|-----------------------------------------|----------|---------| -| `file-pattern` |

Glob pattern for files to check.

| `true` | `""` | - -### Outputs - -| name | description | -|---------|----------------------------------------------------------------| -| `found` |

Indicates if the files matching the pattern were found.

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/common-file-check@main - with: - file-pattern: - # Glob pattern for files to check. - # - # Required: true - # Default: "" -``` diff --git a/common-file-check/action.yml b/common-file-check/action.yml deleted file mode 100644 index 4b2c14d..0000000 --- a/common-file-check/action.yml +++ /dev/null @@ -1,87 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for checking files in repository ---- -name: Common File Check -description: | - A reusable action to check if a specific file or type of files exists in the repository. - Emits an output "found" which is true or false. -author: 'Ismo Vuorinen' -branding: - icon: search - color: gray-dark - -inputs: - file-pattern: - description: 'Glob pattern for files to check.' - required: true - -outputs: - found: - description: 'Indicates if the files matching the pattern were found.' - value: ${{ steps.check-files.outputs.found }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - FILE_PATTERN: ${{ inputs.file-pattern }} - run: | - set -euo pipefail - - # Validate file pattern is not empty - if [[ -z "$FILE_PATTERN" ]]; then - echo "::error::file-pattern input is required and cannot be empty" - exit 1 - fi - - # Validate file pattern format (basic glob pattern validation) - pattern="$FILE_PATTERN" - - # Check for path traversal attempts - if [[ "$pattern" == *".."* ]]; then - echo "::error::Invalid file pattern: '$pattern'. Path traversal (..) not allowed" - exit 1 - fi - - # Check for absolute paths (should be relative patterns) - if [[ "$pattern" == /* ]]; then - echo "::error::Invalid file pattern: '$pattern'. Absolute paths not allowed, use relative patterns" - exit 1 - fi - - # Basic validation for dangerous patterns - if [[ "$pattern" == *";"* ]] || [[ "$pattern" == *"|"* ]] || [[ "$pattern" == *"&"* ]] || [[ "$pattern" == *"\$"* ]]; then - echo "::error::Invalid file pattern: '$pattern'. Command injection characters not allowed" - exit 1 - fi - - # Check for reasonable pattern length (prevent extremely long patterns) - if [ ${#pattern} -gt 255 ]; then - echo "::error::File pattern too long: ${#pattern} characters. Maximum allowed is 255 characters" - exit 1 - fi - - # Validate common glob pattern characters are safe - if ! [[ "$pattern" =~ ^[a-zA-Z0-9*?./_{}\[\]-]+$ ]]; then - echo "::warning::File pattern contains special characters: '$pattern'. Ensure this is intentional and safe" - fi - - echo "Validated file pattern: '$pattern'" - - - name: Check for Files - id: check-files - shell: bash - env: - FILE_PATTERN: ${{ inputs.file-pattern }} - run: |- - set -euo pipefail - - if find . -name "$FILE_PATTERN" | grep -q .; then - echo "found=true" >> $GITHUB_OUTPUT - else - echo "found=false" >> $GITHUB_OUTPUT - fi diff --git a/common-file-check/rules.yml b/common-file-check/rules.yml deleted file mode 100644 index d359757..0000000 --- a/common-file-check/rules.yml +++ /dev/null @@ -1,39 +0,0 @@ ---- -# Validation rules for common-file-check action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (1/1 inputs) -# -# This file defines validation rules for the common-file-check GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: common-file-check -description: 'A reusable action to check if a specific file or type of files exists in the repository. - - Emits an output "found" which is true or false. - - ' -generator_version: 1.0.0 -required_inputs: - - file-pattern -optional_inputs: [] -conventions: - file-pattern: file_path -overrides: {} -statistics: - total_inputs: 1 - validated_inputs: 1 - skipped_inputs: 0 - coverage_percentage: 100 -validation_coverage: 100 -auto_detected: true -manual_review_required: false -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/common-retry/CustomValidator.py b/common-retry/CustomValidator.py deleted file mode 100755 index e42569c..0000000 --- a/common-retry/CustomValidator.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for common-retry action.""" - -from __future__ import annotations - -from pathlib import Path -import sys -from typing import Any - -# 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.numeric import NumericValidator -from validators.security import SecurityValidator - - -class CustomValidator(BaseValidator): - """Custom validator for common-retry action.""" - - def __init__(self, action_type: str = "common-retry") -> None: - """Initialize common-retry validator.""" - super().__init__(action_type) - self.file_validator = FileValidator() - self.numeric_validator = NumericValidator() - self.security_validator = SecurityValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate common-retry action inputs.""" - valid = True - # Validate required inputs - if "command" not in inputs or not inputs["command"]: - self.add_error("Input 'command' is required") - valid = False - elif inputs["command"]: - # Validate command for security issues - result = self.security_validator.validate_no_injection(inputs["command"]) - 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 optional inputs - return self._validate_optionals(inputs=inputs, prev_valid=valid) - - def _validate_optionals(self, inputs: dict[str, Any], *, prev_valid: bool) -> bool: - """Validate optional inputs for common-retry action. - - Args: - inputs: Dictionary of input names and values - prev_valid: Previous validity state - Returns: - True if all optional validations pass, False otherwise - """ - valid = prev_valid - # Backoff strategy - fixed is the correct value, not constant - backoff_strategy = inputs.get("backoff-strategy") - backoff_strategies = ["exponential", "linear", "fixed"] - if backoff_strategy and backoff_strategy not in backoff_strategies: - self.add_error(f"Invalid backoff strategy: {inputs['backoff-strategy']}") - valid = False - # Max retries - max_retries = inputs.get("max-retries") - if max_retries: - result = self.numeric_validator.validate_numeric_range( - max_retries, min_val=1, max_val=10 - ) - 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 - # Retry delay - retry_delay = inputs.get("retry-delay") - if retry_delay: - result = self.numeric_validator.validate_numeric_range( - retry_delay, min_val=1, max_val=300 - ) - 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 - # Shell type - only bash and sh are allowed - shell = inputs.get("shell") - valid_shells = ["bash", "sh"] - if shell and shell not in valid_shells: - self.add_error(f"Invalid shell type: {inputs['shell']}") - valid = False - # Timeout - timeout = inputs.get("timeout") - if timeout: - result = self.numeric_validator.validate_numeric_range(timeout, min_val=1, max_val=3600) - 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 - # Working directory - working_directory = inputs.get("working-directory") - if working_directory: - result = self.file_validator.validate_file_path(working_directory) - 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 - # Description - description = inputs.get("description") - if description: - # Validate description for security patterns - result = self.security_validator.validate_no_injection(description) - 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 - # Success codes - validate for injection - success_codes = inputs.get("success-codes") - if success_codes: - result = self.security_validator.validate_no_injection(success_codes) - 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 - # Retry codes - validate for injection - retry_codes = inputs.get("retry-codes") - if retry_codes: - result = self.security_validator.validate_no_injection(retry_codes) - 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 ["command"] - - def get_validation_rules(self) -> dict: - """Get validation rules.""" - return { - "command": { - "type": "string", - "required": True, - "description": "Command to retry", - }, - "backoff-strategy": { - "type": "string", - "required": False, - "description": "Backoff strategy", - }, - "max-retries": { - "type": "numeric", - "required": False, - "description": "Maximum number of retries", - }, - "retry-delay": { - "type": "numeric", - "required": False, - "description": "Delay between retries", - }, - "shell": { - "type": "string", - "required": False, - "description": "Shell to use", - }, - "timeout": { - "type": "numeric", - "required": False, - "description": "Command timeout", - }, - } diff --git a/common-retry/README.md b/common-retry/README.md deleted file mode 100644 index e3c9aa2..0000000 --- a/common-retry/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# ivuorinen/actions/common-retry - -## Common Retry - -### Description - -Standardized retry utility for network operations and flaky commands - -### Inputs - -| name | description | required | default | -|---------------------|---------------------------------------------------------------------|----------|---------------------| -| `command` |

Command to execute with retry logic

| `true` | `""` | -| `max-retries` |

Maximum number of retry attempts

| `false` | `3` | -| `retry-delay` |

Initial delay between retries in seconds

| `false` | `5` | -| `backoff-strategy` |

Backoff strategy (linear, exponential, fixed)

| `false` | `exponential` | -| `timeout` |

Timeout for each attempt in seconds

| `false` | `300` | -| `working-directory` |

Working directory to execute command in

| `false` | `.` | -| `shell` |

Shell to use for command execution

| `false` | `bash` | -| `success-codes` |

Comma-separated list of success exit codes

| `false` | `0` | -| `retry-codes` |

Comma-separated list of exit codes that should trigger retry

| `false` | `1,2,124,126,127` | -| `description` |

Human-readable description of the operation for logging

| `false` | `Command execution` | - -### Outputs - -| name | description | -|-------------|---------------------------------------------------| -| `success` |

Whether the command succeeded (true/false)

| -| `attempts` |

Number of attempts made

| -| `exit-code` |

Final exit code of the command

| -| `duration` |

Total execution duration in seconds

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/common-retry@main - with: - command: - # Command to execute with retry logic - # - # Required: true - # Default: "" - - max-retries: - # Maximum number of retry attempts - # - # Required: false - # Default: 3 - - retry-delay: - # Initial delay between retries in seconds - # - # Required: false - # Default: 5 - - backoff-strategy: - # Backoff strategy (linear, exponential, fixed) - # - # Required: false - # Default: exponential - - timeout: - # Timeout for each attempt in seconds - # - # Required: false - # Default: 300 - - working-directory: - # Working directory to execute command in - # - # Required: false - # Default: . - - shell: - # Shell to use for command execution - # - # Required: false - # Default: bash - - success-codes: - # Comma-separated list of success exit codes - # - # Required: false - # Default: 0 - - retry-codes: - # Comma-separated list of exit codes that should trigger retry - # - # Required: false - # Default: 1,2,124,126,127 - - description: - # Human-readable description of the operation for logging - # - # Required: false - # Default: Command execution -``` diff --git a/common-retry/action.yml b/common-retry/action.yml deleted file mode 100644 index 9350d4e..0000000 --- a/common-retry/action.yml +++ /dev/null @@ -1,246 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - (none required) # Permissions depend on the command being executed ---- -name: Common Retry -description: 'Standardized retry utility for network operations and flaky commands' -author: 'Ismo Vuorinen' - -branding: - icon: refresh-cw - color: orange - -inputs: - command: - description: 'Command to execute with retry logic' - required: true - max-retries: - description: 'Maximum number of retry attempts' - required: false - default: '3' - retry-delay: - description: 'Initial delay between retries in seconds' - required: false - default: '5' - backoff-strategy: - description: 'Backoff strategy (linear, exponential, fixed)' - required: false - default: 'exponential' - timeout: - description: 'Timeout for each attempt in seconds' - required: false - default: '300' - working-directory: - description: 'Working directory to execute command in' - required: false - default: '.' - shell: - description: 'Shell to use for command execution' - required: false - default: 'bash' - success-codes: - description: 'Comma-separated list of success exit codes' - required: false - default: '0' - retry-codes: - description: 'Comma-separated list of exit codes that should trigger retry' - required: false - default: '1,2,124,126,127' - description: - description: 'Human-readable description of the operation for logging' - required: false - default: 'Command execution' - -outputs: - success: - description: 'Whether the command succeeded (true/false)' - value: ${{ steps.execute.outputs.success }} - attempts: - description: 'Number of attempts made' - value: ${{ steps.execute.outputs.attempts }} - exit-code: - description: 'Final exit code of the command' - value: ${{ steps.execute.outputs.exit-code }} - duration: - description: 'Total execution duration in seconds' - value: ${{ steps.execute.outputs.duration }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - MAX_RETRIES: ${{ inputs.max-retries }} - RETRY_DELAY: ${{ inputs.retry-delay }} - BACKOFF_STRATEGY: ${{ inputs.backoff-strategy }} - TIMEOUT: ${{ inputs.timeout }} - SHELL: ${{ inputs.shell }} - WORKING_DIRECTORY: ${{ inputs.working-directory }} - run: | - set -euo pipefail - - # Validate max-retries (1-10) - if ! [[ "$MAX_RETRIES" =~ ^[1-9]$|^10$ ]]; then - echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be 1-10" - exit 1 - fi - - # Validate retry-delay (1-300) - if ! [[ "$RETRY_DELAY" =~ ^[1-9][0-9]?$|^[12][0-9][0-9]$|^300$ ]]; then - echo "::error::Invalid retry-delay: '$RETRY_DELAY'. Must be 1-300 seconds" - exit 1 - fi - - # Validate backoff-strategy - if ! [[ "$BACKOFF_STRATEGY" =~ ^(linear|exponential|fixed)$ ]]; then - echo "::error::Invalid backoff-strategy: '$BACKOFF_STRATEGY'. Must be linear, exponential, or fixed" - exit 1 - fi - - # Validate timeout (1-3600) - if ! [[ "$TIMEOUT" =~ ^[1-9][0-9]?$|^[1-9][0-9][0-9]$|^[12][0-9][0-9][0-9]$|^3[0-5][0-9][0-9]$|^3600$ ]]; then - echo "::error::Invalid timeout: '$TIMEOUT'. Must be 1-3600 seconds" - exit 1 - fi - - # Validate shell (only bash supported due to script features) - if ! [[ "$SHELL" =~ ^bash$ ]]; then - echo "::error::Invalid shell: '$SHELL'. Must be bash (sh not supported due to pipefail requirement)" - exit 1 - fi - - # Validate working directory doesn't contain path traversal - if [[ "$WORKING_DIRECTORY" == *".."* ]]; then - echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Path traversal (..) not allowed" - exit 1 - fi - - echo "Input validation completed successfully" - - - name: Execute with Retry Logic - id: execute - shell: ${{ inputs.shell }} - working-directory: ${{ inputs.working-directory }} - env: - SUCCESS_CODES_INPUT: ${{ inputs.success-codes }} - RETRY_CODES_INPUT: ${{ inputs.retry-codes }} - MAX_RETRIES: ${{ inputs.max-retries }} - RETRY_DELAY: ${{ inputs.retry-delay }} - TIMEOUT: ${{ inputs.timeout }} - BACKOFF_STRATEGY: ${{ inputs.backoff-strategy }} - OPERATION_DESCRIPTION: ${{ inputs.description }} - COMMAND: ${{ inputs.command }} - run: |- - set -euo pipefail - - # Parse success and retry codes - IFS=',' read -ra SUCCESS_CODES <<< "$SUCCESS_CODES_INPUT" - IFS=',' read -ra RETRY_CODES <<< "$RETRY_CODES_INPUT" - - # Initialize variables - attempt=1 - max_attempts="$MAX_RETRIES" - base_delay="$RETRY_DELAY" - timeout_seconds="$TIMEOUT" - backoff_strategy="$BACKOFF_STRATEGY" - operation_description="$OPERATION_DESCRIPTION" - start_time=$(date +%s) - - echo "Starting retry execution: $operation_description" - echo "Command: $COMMAND" - echo "Max attempts: $max_attempts" - echo "Base delay: ${base_delay}s" - echo "Backoff strategy: $backoff_strategy" - echo "Timeout per attempt: ${timeout_seconds}s" - - # Function to check if exit code is in array - contains_code() { - local code=$1 - shift - local codes=("$@") - for c in "${codes[@]}"; do - if [[ "$c" == "$code" ]]; then - return 0 - fi - done - return 1 - } - - # Function to calculate delay based on backoff strategy - calculate_delay() { - local attempt_num=$1 - case "$backoff_strategy" in - "linear") - echo $((base_delay * attempt_num)) - ;; - "exponential") - echo $((base_delay * (2 ** (attempt_num - 1)))) - ;; - "fixed") - echo $base_delay - ;; - esac - } - - # Main retry loop - while [ $attempt -le $max_attempts ]; do - echo "Attempt $attempt of $max_attempts: $operation_description" - - # Execute command with timeout - exit_code=0 - if timeout "$timeout_seconds" bash -c "$COMMAND"; then - exit_code=0 - else - exit_code=$? - fi - - # Check if exit code indicates success - if contains_code "$exit_code" "${SUCCESS_CODES[@]}"; then - end_time=$(date +%s) - duration=$((end_time - start_time)) - echo "success=true" >> $GITHUB_OUTPUT - echo "attempts=$attempt" >> $GITHUB_OUTPUT - echo "exit-code=$exit_code" >> $GITHUB_OUTPUT - echo "duration=$duration" >> $GITHUB_OUTPUT - echo "โœ… Operation succeeded on attempt $attempt (exit code: $exit_code, duration: ${duration}s)" - exit 0 - fi - - # Check if we should retry this exit code - if ! contains_code "$exit_code" "${RETRY_CODES[@]}"; then - end_time=$(date +%s) - duration=$((end_time - start_time)) - echo "success=false" >> $GITHUB_OUTPUT - echo "attempts=$attempt" >> $GITHUB_OUTPUT - echo "exit-code=$exit_code" >> $GITHUB_OUTPUT - echo "duration=$duration" >> $GITHUB_OUTPUT - echo "::error::Operation failed with non-retryable exit code: $exit_code" - exit $exit_code - fi - - # Calculate delay for next attempt - if [ $attempt -lt $max_attempts ]; then - delay=$(calculate_delay $attempt) - max_delay=300 # Cap delay at 5 minutes - if [ $delay -gt $max_delay ]; then - delay=$max_delay - fi - - echo "โŒ Attempt $attempt failed (exit code: $exit_code). Waiting ${delay}s before retry..." - sleep $delay - fi - - attempt=$((attempt + 1)) - done - - # All attempts exhausted - end_time=$(date +%s) - duration=$((end_time - start_time)) - echo "success=false" >> $GITHUB_OUTPUT - echo "attempts=$max_attempts" >> $GITHUB_OUTPUT - echo "exit-code=$exit_code" >> $GITHUB_OUTPUT - echo "duration=$duration" >> $GITHUB_OUTPUT - echo "::error::Operation failed after $max_attempts attempts (final exit code: $exit_code, total duration: ${duration}s)" - exit $exit_code diff --git a/common-retry/rules.yml b/common-retry/rules.yml deleted file mode 100644 index 87b312b..0000000 --- a/common-retry/rules.yml +++ /dev/null @@ -1,50 +0,0 @@ ---- -# Validation rules for common-retry action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 70% (7/10 inputs) -# -# This file defines validation rules for the common-retry GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: common-retry -description: Standardized retry utility for network operations and flaky commands -generator_version: 1.0.0 -required_inputs: - - command -optional_inputs: - - backoff-strategy - - description - - max-retries - - retry-codes - - retry-delay - - shell - - success-codes - - timeout - - working-directory -conventions: - backoff-strategy: backoff_strategy - description: security_patterns - max-retries: numeric_range_1_10 - retry-delay: numeric_range_1_300 - shell: shell_type - timeout: numeric_range_1_3600 - working-directory: file_path -overrides: {} -statistics: - total_inputs: 10 - validated_inputs: 7 - skipped_inputs: 0 - coverage_percentage: 70 -validation_coverage: 70 -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: true diff --git a/compress-images/action.yml b/compress-images/action.yml index 2fd0923..7bb6d15 100644 --- a/compress-images/action.yml +++ b/compress-images/action.yml @@ -141,13 +141,6 @@ runs: echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters" fi fi - - name: Set Git Config - id: set-git-config - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} - name: Checkout Repository uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta @@ -169,7 +162,8 @@ runs: if: steps.calibre.outputs.markdown != '' uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: - title: Compressed Images Nightly + token: ${{ inputs.token }} + title: 'chore: compress images' branch-suffix: timestamp - commit-message: Compressed Images + commit-message: 'chore: compress images' body: ${{ steps.calibre.outputs.markdown }} diff --git a/csharp-build/action.yml b/csharp-build/action.yml index 5f772ce..d435fed 100644 --- a/csharp-build/action.yml +++ b/csharp-build/action.yml @@ -32,7 +32,7 @@ outputs: value: ${{ steps.test.outputs.status }} dotnet_version: description: 'Version of .NET SDK used' - value: ${{ steps.detect-dotnet-version.outputs.dotnet-version }} + value: ${{ steps.detect-dotnet-version.outputs.detected-version }} artifacts_path: description: 'Path to build artifacts' value: '**/bin/Release/**/*' @@ -50,14 +50,15 @@ runs: - name: Detect .NET SDK Version id: detect-dotnet-version - uses: ivuorinen/actions/dotnet-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 with: + language: 'dotnet' default-version: "${{ inputs.dotnet-version || '7.0' }}" - name: Setup .NET SDK uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: - dotnet-version: ${{ steps.detect-dotnet-version.outputs.dotnet-version }} + dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }} - name: Cache NuGet packages id: cache-nuget @@ -70,13 +71,13 @@ runs: - name: Restore Dependencies if: steps.cache-nuget.outputs.cache-hit != 'true' - uses: ivuorinen/actions/common-retry@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 with: + timeout_minutes: 10 + max_attempts: ${{ inputs.max-retries }} command: | echo "Restoring .NET dependencies..." dotnet restore --verbosity normal - max-retries: ${{ inputs.max-retries }} - description: 'Restoring .NET dependencies' - name: Skip Restore (Cache Hit) if: steps.cache-nuget.outputs.cache-hit == 'true' diff --git a/csharp-lint-check/action.yml b/csharp-lint-check/action.yml index 626b99e..ac0a580 100644 --- a/csharp-lint-check/action.yml +++ b/csharp-lint-check/action.yml @@ -66,14 +66,15 @@ runs: - name: Detect .NET SDK Version id: detect-dotnet-version - uses: ivuorinen/actions/dotnet-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 with: + language: 'dotnet' default-version: ${{ inputs.dotnet-version || '7.0' }} - name: Setup .NET SDK uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: - dotnet-version: ${{ steps.detect-dotnet-version.outputs.dotnet-version }} + dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }} - name: Install dotnet-format shell: bash diff --git a/csharp-publish/action.yml b/csharp-publish/action.yml index 3aa13c1..0310666 100644 --- a/csharp-publish/action.yml +++ b/csharp-publish/action.yml @@ -60,14 +60,15 @@ runs: - name: Detect .NET SDK Version id: detect-dotnet-version - uses: ivuorinen/actions/dotnet-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 with: + language: 'dotnet' default-version: '7.0' - name: Setup .NET SDK uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: - dotnet-version: ${{ inputs.dotnet-version || steps.detect-dotnet-version.outputs.dotnet-version }} + dotnet-version: ${{ inputs.dotnet-version || steps.detect-dotnet-version.outputs.detected-version }} - name: Cache NuGet packages id: cache-nuget diff --git a/docker-build/action.yml b/docker-build/action.yml index ec3f1cf..b831b68 100644 --- a/docker-build/action.yml +++ b/docker-build/action.yml @@ -120,7 +120,7 @@ outputs: value: ${{ steps.build.outputs.metadata }} platforms: description: 'Successfully built platforms' - value: ${{ steps.platforms.outputs.built }} + value: ${{ steps.detect-platforms.outputs.platforms }} platform-matrix: description: 'Build status per platform in JSON format' value: ${{ steps.build.outputs.platform-matrix }} @@ -186,22 +186,28 @@ runs: - name: Detect Available Platforms id: detect-platforms - if: inputs.auto-detect-platforms == 'true' shell: bash env: ARCHITECTURES: ${{ inputs.architectures }} + AUTO_DETECT: ${{ inputs.auto-detect-platforms }} run: | set -euo pipefail - # Get available platforms from buildx - available_platforms=$(docker buildx ls | grep -o 'linux/[^ ]*' | sort -u | tr '\n' ',' | sed 's/,$//') + # When auto-detect is enabled, try to detect available platforms + if [ "$AUTO_DETECT" = "true" ]; then + available_platforms=$(docker buildx ls | grep -o 'linux/[^ ]*' | sort -u | tr '\n' ',' | sed 's/,$//' || true) - if [ -n "$available_platforms" ]; then - echo "platforms=${available_platforms}" >> $GITHUB_OUTPUT - echo "Detected platforms: ${available_platforms}" + if [ -n "$available_platforms" ]; then + echo "platforms=${available_platforms}" >> $GITHUB_OUTPUT + echo "Detected platforms: ${available_platforms}" + else + echo "platforms=$ARCHITECTURES" >> $GITHUB_OUTPUT + echo "Using default platforms (detection failed): $ARCHITECTURES" + fi else + # Auto-detect disabled, use configured architectures echo "platforms=$ARCHITECTURES" >> $GITHUB_OUTPUT - echo "Using default platforms: $ARCHITECTURES" + echo "Using configured platforms: $ARCHITECTURES" fi - name: Determine Image Name diff --git a/docker-publish-gh/CustomValidator.py b/docker-publish-gh/CustomValidator.py deleted file mode 100755 index 15f28a9..0000000 --- a/docker-publish-gh/CustomValidator.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for docker-publish-gh 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.docker import DockerValidator -from validators.token import TokenValidator - - -class CustomValidator(BaseValidator): - """Custom validator for docker-publish-gh action.""" - - def __init__(self, action_type: str = "docker-publish-gh") -> None: - """Initialize docker-publish-gh validator.""" - super().__init__(action_type) - self.docker_validator = DockerValidator() - self.token_validator = TokenValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate docker-publish-gh action inputs.""" - valid = True - - # Validate required input: image-name - if "image-name" not in inputs or not inputs["image-name"]: - self.add_error("Input 'image-name' is required") - valid = False - elif inputs["image-name"]: - result = self.docker_validator.validate_image_name(inputs["image-name"], "image-name") - for error in self.docker_validator.errors: - if error not in self.errors: - self.add_error(error) - self.docker_validator.clear_errors() - if not result: - 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 ["image-name"] - - def get_validation_rules(self) -> dict: - """Get validation rules.""" - return { - "image-name": { - "type": "string", - "required": True, - "description": "Docker image name", - }, - "registry": { - "type": "string", - "required": False, - "description": "Docker registry", - }, - "username": { - "type": "string", - "required": False, - "description": "Registry username", - }, - "password": { - "type": "token", - "required": False, - "description": "Registry password", - }, - } diff --git a/docker-publish-gh/README.md b/docker-publish-gh/README.md deleted file mode 100644 index 0b741fc..0000000 --- a/docker-publish-gh/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# ivuorinen/actions/docker-publish-gh - -## Docker Publish to GitHub Packages - -### Description - -Publishes a Docker image to GitHub Packages with advanced security and reliability features. - -### Inputs - -| name | description | required | default | -|-------------------------|----------------------------------------------------------------------------------|----------|---------------------------| -| `image-name` |

The name of the Docker image to publish. Defaults to the repository name.

| `false` | `""` | -| `tags` |

Comma-separated list of tags for the Docker image.

| `true` | `""` | -| `platforms` |

Platforms to publish (comma-separated). Defaults to amd64 and arm64.

| `false` | `linux/amd64,linux/arm64` | -| `registry` |

GitHub Container Registry URL

| `false` | `ghcr.io` | -| `token` |

GitHub token with package write permissions

| `false` | `""` | -| `provenance` |

Enable SLSA provenance generation

| `false` | `true` | -| `sbom` |

Generate Software Bill of Materials

| `false` | `true` | -| `max-retries` |

Maximum number of retry attempts for publishing

| `false` | `3` | -| `retry-delay` |

Delay in seconds between retries

| `false` | `10` | -| `buildx-version` |

Specific Docker Buildx version to use

| `false` | `latest` | -| `cache-mode` |

Cache mode for build layers (min, max, or inline)

| `false` | `max` | -| `auto-detect-platforms` |

Automatically detect and build for all available platforms

| `false` | `false` | -| `scan-image` |

Scan published image for vulnerabilities

| `false` | `true` | -| `sign-image` |

Sign the published image with cosign

| `false` | `true` | -| `parallel-builds` |

Number of parallel platform builds (0 for auto)

| `false` | `0` | -| `verbose` |

Enable verbose logging

| `false` | `false` | - -### Outputs - -| name | description | -|-------------------|-------------------------------------------| -| `image-name` |

Full image name including registry

| -| `digest` |

The digest of the published image

| -| `tags` |

List of published tags

| -| `provenance` |

SLSA provenance attestation

| -| `sbom` |

SBOM document location

| -| `scan-results` |

Vulnerability scan results

| -| `platform-matrix` |

Build status per platform

| -| `build-time` |

Total build time in seconds

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/docker-publish-gh@main - with: - image-name: - # The name of the Docker image to publish. Defaults to the repository name. - # - # Required: false - # Default: "" - - tags: - # Comma-separated list of tags for the Docker image. - # - # Required: true - # Default: "" - - platforms: - # Platforms to publish (comma-separated). Defaults to amd64 and arm64. - # - # Required: false - # Default: linux/amd64,linux/arm64 - - registry: - # GitHub Container Registry URL - # - # Required: false - # Default: ghcr.io - - token: - # GitHub token with package write permissions - # - # Required: false - # Default: "" - - provenance: - # Enable SLSA provenance generation - # - # Required: false - # Default: true - - sbom: - # Generate Software Bill of Materials - # - # Required: false - # Default: true - - max-retries: - # Maximum number of retry attempts for publishing - # - # Required: false - # Default: 3 - - retry-delay: - # Delay in seconds between retries - # - # Required: false - # Default: 10 - - buildx-version: - # Specific Docker Buildx version to use - # - # Required: false - # Default: latest - - cache-mode: - # Cache mode for build layers (min, max, or inline) - # - # Required: false - # Default: max - - auto-detect-platforms: - # Automatically detect and build for all available platforms - # - # Required: false - # Default: false - - scan-image: - # Scan published image for vulnerabilities - # - # Required: false - # Default: true - - sign-image: - # Sign the published image with cosign - # - # Required: false - # Default: true - - parallel-builds: - # Number of parallel platform builds (0 for auto) - # - # Required: false - # Default: 0 - - verbose: - # Enable verbose logging - # - # Required: false - # Default: false -``` diff --git a/docker-publish-gh/action.yml b/docker-publish-gh/action.yml deleted file mode 100644 index 9b189e1..0000000 --- a/docker-publish-gh/action.yml +++ /dev/null @@ -1,495 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - packages: write # Required for publishing to GitHub Container Registry -# - contents: read # Required for checking out repository ---- -name: Docker Publish to GitHub Packages -description: 'Publishes a Docker image to GitHub Packages with advanced security and reliability features.' -author: 'Ismo Vuorinen' - -branding: - icon: 'package' - color: 'blue' - -inputs: - image-name: - description: 'The name of the Docker image to publish. Defaults to the repository name.' - required: false - tags: - description: 'Comma-separated list of tags for the Docker image.' - required: true - platforms: - description: 'Platforms to publish (comma-separated). Defaults to amd64 and arm64.' - required: false - default: 'linux/amd64,linux/arm64' - registry: - description: 'GitHub Container Registry URL' - required: false - default: 'ghcr.io' - token: - description: 'GitHub token with package write permissions' - required: false - default: '' - provenance: - description: 'Enable SLSA provenance generation' - required: false - default: 'true' - sbom: - description: 'Generate Software Bill of Materials' - required: false - default: 'true' - max-retries: - description: 'Maximum number of retry attempts for publishing' - required: false - default: '3' - retry-delay: - description: 'Delay in seconds between retries' - required: false - default: '10' - buildx-version: - description: 'Specific Docker Buildx version to use' - required: false - default: 'latest' - cache-mode: - description: 'Cache mode for build layers (min, max, or inline)' - required: false - default: 'max' - auto-detect-platforms: - description: 'Automatically detect and build for all available platforms' - required: false - default: 'false' - scan-image: - description: 'Scan published image for vulnerabilities' - required: false - default: 'true' - sign-image: - description: 'Sign the published image with cosign' - required: false - default: 'true' - parallel-builds: - description: 'Number of parallel platform builds (0 for auto)' - required: false - default: '0' - verbose: - description: 'Enable verbose logging' - required: false - default: 'false' - -outputs: - image-name: - description: 'Full image name including registry' - value: ${{ steps.metadata.outputs.full-name }} - digest: - description: 'The digest of the published image' - value: ${{ steps.publish.outputs.digest }} - tags: - description: 'List of published tags' - value: ${{ steps.metadata.outputs.tags }} - provenance: - description: 'SLSA provenance attestation' - value: ${{ steps.publish.outputs.provenance }} - sbom: - description: 'SBOM document location' - value: ${{ steps.publish.outputs.sbom }} - scan-results: - description: 'Vulnerability scan results' - value: ${{ steps.scan.outputs.results }} - platform-matrix: - description: 'Build status per platform' - value: ${{ steps.publish.outputs.platform-matrix }} - build-time: - description: 'Total build time in seconds' - value: ${{ steps.publish.outputs.build-time }} - -runs: - using: composite - steps: - - name: Mask Secrets - shell: bash - env: - INPUT_TOKEN: ${{ inputs.token }} - run: | - set -euo pipefail - # Use provided token or fall back to GITHUB_TOKEN - TOKEN="${INPUT_TOKEN:-${GITHUB_TOKEN:-}}" - if [ -n "$TOKEN" ]; then - echo "::add-mask::$TOKEN" - fi - - - name: Validate Inputs - id: validate - shell: bash - env: - IMAGE_NAME: ${{ inputs.image-name }} - TAGS: ${{ inputs.tags }} - PLATFORMS: ${{ inputs.platforms }} - run: | - set -euo pipefail - - # Validate image name format - if [ -n "$IMAGE_NAME" ]; then - if ! [[ "$IMAGE_NAME" =~ ^[a-z0-9]+(?:[._-][a-z0-9]+)*$ ]]; then - echo "::error::Invalid image name format" - exit 1 - fi - fi - - # Validate tags - IFS=',' read -ra TAG_ARRAY <<< "$TAGS" - for tag in "${TAG_ARRAY[@]}"; do - if ! [[ "$tag" =~ ^(v?[0-9]+\.[0-9]+\.[0-9]+(-[\w.]+)?(\+[\w.]+)?|latest|[a-zA-Z][-a-zA-Z0-9._]{0,127})$ ]]; then - echo "::error::Invalid tag format: $tag" - exit 1 - fi - done - - # Validate platforms - IFS=',' read -ra PLATFORM_ARRAY <<< "$PLATFORMS" - for platform in "${PLATFORM_ARRAY[@]}"; do - if ! [[ "$platform" =~ ^linux/(amd64|arm64|arm/v7|arm/v6|386|ppc64le|s390x)$ ]]; then - echo "::error::Invalid platform: $platform" - exit 1 - fi - done - - - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - with: - platforms: ${{ inputs.platforms }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - with: - version: ${{ inputs.buildx-version }} - platforms: ${{ inputs.platforms }} - buildkitd-flags: --debug - driver-opts: | - network=host - image=moby/buildkit:${{ inputs.buildx-version }} - - - name: Prepare Metadata - id: metadata - shell: bash - env: - IMAGE_NAME: ${{ inputs.image-name }} - REGISTRY: ${{ inputs.registry }} - TAGS: ${{ inputs.tags }} - REPO_OWNER: ${{ github.repository_owner }} - run: | - set -euo pipefail - - # Determine image name - if [ -z "$IMAGE_NAME" ]; then - image_name=$(basename $GITHUB_REPOSITORY) - else - image_name="$IMAGE_NAME" - fi - - # Output image name for reuse - echo "image-name=${image_name}" >> $GITHUB_OUTPUT - - # Normalize repository owner to lowercase for GHCR compatibility - repo_owner_lower=$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lower:]') - - # Construct full image name with registry - full_name="$REGISTRY/${repo_owner_lower}/${image_name}" - echo "full-name=${full_name}" >> $GITHUB_OUTPUT - - # Process tags - processed_tags="" - IFS=',' read -ra TAG_ARRAY <<< "$TAGS" - for tag in "${TAG_ARRAY[@]}"; do - processed_tags="${processed_tags}${full_name}:${tag}," - done - processed_tags=${processed_tags%,} - echo "tags=${processed_tags}" >> $GITHUB_OUTPUT - - - name: Log in to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 - with: - registry: ${{ inputs.registry }} - username: ${{ github.actor }} - password: ${{ inputs.token || github.token }} - - - name: Set up Cosign - if: inputs.provenance == 'true' || inputs.sign-image == 'true' - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 - - - name: Detect Available Platforms - id: detect-platforms - if: inputs.auto-detect-platforms == 'true' - shell: bash - env: - PLATFORMS: ${{ inputs.platforms }} - run: | - set -euo pipefail - - # Get available platforms from buildx - available_platforms=$(docker buildx ls | grep -o 'linux/[^ ]*' | sort -u | tr '\n' ',' | sed 's/,$//') - - if [ -n "$available_platforms" ]; then - echo "platforms=${available_platforms}" >> $GITHUB_OUTPUT - echo "Detected platforms: ${available_platforms}" - else - echo "platforms=$PLATFORMS" >> $GITHUB_OUTPUT - echo "Using default platforms: $PLATFORMS" - fi - - - name: Publish Image - id: publish - shell: bash - env: - DOCKER_BUILDKIT: 1 - AUTO_DETECT_PLATFORMS: ${{ inputs.auto-detect-platforms }} - DETECTED_PLATFORMS: ${{ steps.detect-platforms.outputs.platforms }} - DEFAULT_PLATFORMS: ${{ inputs.platforms }} - VERBOSE: ${{ inputs.verbose }} - MAX_RETRIES: ${{ inputs.max-retries }} - METADATA_TAGS: ${{ steps.metadata.outputs.tags }} - REGISTRY: ${{ inputs.registry }} - CACHE_MODE: ${{ inputs.cache-mode }} - PROVENANCE: ${{ inputs.provenance }} - SBOM: ${{ inputs.sbom }} - INPUT_TAGS: ${{ inputs.tags }} - FULL_IMAGE_NAME: ${{ steps.metadata.outputs.full-name }} - IMAGE_NAME: ${{ steps.metadata.outputs.image-name }} - RETRY_DELAY: ${{ inputs.retry-delay }} - REPO_OWNER: ${{ github.repository_owner }} - run: | - set -euo pipefail - - # Normalize repository owner to lowercase for GHCR compatibility - REPO_OWNER_LOWER=$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lower:]') - export REPO_OWNER_LOWER - - # Track build start time - build_start=$(date +%s) - - # Determine platforms - if [ "$AUTO_DETECT_PLATFORMS" == "true" ] && [ -n "$DETECTED_PLATFORMS" ]; then - platforms="$DETECTED_PLATFORMS" - else - platforms="$DEFAULT_PLATFORMS" - fi - - # Initialize platform matrix tracking - platform_matrix="{}" - - # Prepare verbose flag - verbose_flag="" - if [ "$VERBOSE" == "true" ]; then - verbose_flag="--progress=plain" - fi - - attempt=1 - max_attempts="$MAX_RETRIES" - - while [ $attempt -le $max_attempts ]; do - echo "Publishing attempt $attempt of $max_attempts" - - # Prepare tag arguments from comma-separated tags - tag_args="" - IFS=',' read -ra TAGS <<< "$METADATA_TAGS" - for tag in "${TAGS[@]}"; do - tag=$(echo "$tag" | xargs) # trim whitespace - tag_args="$tag_args --tag $tag" - done - - # Prepare provenance flag - provenance_flag="" - if [ "$PROVENANCE" == "true" ]; then - provenance_flag="--provenance=true" - fi - - # Prepare SBOM flag - sbom_flag="" - if [ "$SBOM" == "true" ]; then - sbom_flag="--sbom=true" - fi - - if docker buildx build \ - --platform=${platforms} \ - $tag_args \ - --push \ - --cache-from type=registry,ref="$REGISTRY/$REPO_OWNER_LOWER/cache:buildcache" \ - --cache-to type=registry,ref="$REGISTRY/$REPO_OWNER_LOWER/cache:buildcache",mode="$CACHE_MODE" \ - ${provenance_flag} \ - ${sbom_flag} \ - ${verbose_flag} \ - --metadata-file=/tmp/build-metadata.json \ - --label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ - --label "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ - --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ - --label "org.opencontainers.image.version=$INPUT_TAGS" \ - .; then - - # Get image digest - IFS=',' read -ra TAG_ARRAY <<< "$INPUT_TAGS" - digest=$(docker buildx imagetools inspect "$FULL_IMAGE_NAME:${TAG_ARRAY[0]}" --raw | jq -r '.digest // "unknown"' || echo "unknown") - echo "digest=${digest}" >> $GITHUB_OUTPUT - - # Calculate build time - build_end=$(date +%s) - build_time=$((build_end - build_start)) - echo "build-time=${build_time}" >> $GITHUB_OUTPUT - - # Build platform matrix - IFS=',' read -ra PLATFORM_ARRAY <<< "${platforms}" - platform_matrix="{" - for p in "${PLATFORM_ARRAY[@]}"; do - platform_matrix="${platform_matrix}\"${p}\":\"success\"," - done - platform_matrix="${platform_matrix%,}}" - echo "platform-matrix=${platform_matrix}" >> $GITHUB_OUTPUT - - # Generate attestations if enabled - if [[ "$PROVENANCE" == "true" ]]; then - echo "provenance=true" >> $GITHUB_OUTPUT - fi - - if [[ "$SBOM" == "true" ]]; then - sbom_path="$REGISTRY/$REPO_OWNER_LOWER/$IMAGE_NAME.sbom" - echo "sbom=${sbom_path}" >> $GITHUB_OUTPUT - fi - - break - fi - - attempt=$((attempt + 1)) - if [ $attempt -le $max_attempts ]; then - echo "Publish failed, waiting $RETRY_DELAY seconds before retry..." - sleep "$RETRY_DELAY" - else - echo "::error::Publishing failed after $max_attempts attempts" - exit 1 - fi - done - - - name: Scan Published Image - id: scan - if: inputs.scan-image == 'true' - shell: bash - env: - IMAGE_DIGEST: ${{ steps.publish.outputs.digest }} - FULL_IMAGE_NAME: ${{ steps.metadata.outputs.full-name }} - run: | - set -euo pipefail - - # Validate digest availability - if [ -z "$IMAGE_DIGEST" ] || [ "$IMAGE_DIGEST" == "unknown" ]; then - echo "::error::No valid image digest available for scanning" - exit 1 - fi - - # Install Trivy - wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - - echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list - sudo apt-get update && sudo apt-get install -y trivy - - # Scan the exact digest that was just built (not tags which could be stale) - trivy image \ - --severity HIGH,CRITICAL \ - --format json \ - --output /tmp/scan-results.json \ - "$FULL_IMAGE_NAME@${IMAGE_DIGEST}" - - # Output results - scan_results=$(cat /tmp/scan-results.json | jq -c '.') - echo "results=${scan_results}" >> $GITHUB_OUTPUT - - # Check for critical vulnerabilities - critical_count=$(cat /tmp/scan-results.json | jq '.Results[].Vulnerabilities[] | select(.Severity == "CRITICAL") | .VulnerabilityID' | wc -l) - if [ "$critical_count" -gt 0 ]; then - echo "::warning::Found $critical_count critical vulnerabilities in published image" - fi - - - name: Sign Published Image - id: sign - if: inputs.sign-image == 'true' - shell: bash - env: - IMAGE_DIGEST: ${{ steps.publish.outputs.digest }} - FULL_IMAGE_NAME: ${{ steps.metadata.outputs.full-name }} - run: | - set -euo pipefail - - # Validate digest availability - if [ -z "$IMAGE_DIGEST" ] || [ "$IMAGE_DIGEST" == "unknown" ]; then - echo "::error::No valid image digest available for signing" - exit 1 - fi - - # Sign the exact digest that was just built (not tags which could be stale) - echo "Signing $FULL_IMAGE_NAME@${IMAGE_DIGEST}" - - # Using keyless signing with OIDC - export COSIGN_EXPERIMENTAL=1 - cosign sign --yes "$FULL_IMAGE_NAME@${IMAGE_DIGEST}" - - echo "signature=signed" >> $GITHUB_OUTPUT - - - name: Verify Publication - id: verify - shell: bash - env: - IMAGE_DIGEST: ${{ steps.publish.outputs.digest }} - FULL_IMAGE_NAME: ${{ steps.metadata.outputs.full-name }} - AUTO_DETECT_PLATFORMS: ${{ inputs.auto-detect-platforms }} - DETECTED_PLATFORMS: ${{ steps.detect-platforms.outputs.platforms }} - DEFAULT_PLATFORMS: ${{ inputs.platforms }} - SIGN_IMAGE: ${{ inputs.sign-image }} - run: | - set -euo pipefail - - # Validate digest availability - if [ -z "$IMAGE_DIGEST" ] || [ "$IMAGE_DIGEST" == "unknown" ]; then - echo "::error::No valid image digest available for verification" - exit 1 - fi - - # Verify the exact digest that was just built - if ! docker buildx imagetools inspect "$FULL_IMAGE_NAME@${IMAGE_DIGEST}" >/dev/null 2>&1; then - echo "::error::Published image not found at digest: $IMAGE_DIGEST" - exit 1 - fi - - # Determine platforms to verify - if [ "$AUTO_DETECT_PLATFORMS" == "true" ] && [ -n "$DETECTED_PLATFORMS" ]; then - platforms="$DETECTED_PLATFORMS" - else - platforms="$DEFAULT_PLATFORMS" - fi - - # Verify platforms using the exact digest - IFS=',' read -ra PLATFORM_ARRAY <<< "${platforms}" - for platform in "${PLATFORM_ARRAY[@]}"; do - if ! docker buildx imagetools inspect "$FULL_IMAGE_NAME@${IMAGE_DIGEST}" | grep -q "$platform"; then - echo "::warning::Platform $platform not found in published image" - else - echo "โœ… Verified platform: $platform" - fi - done - - # Verify signature if signing was enabled (verify the digest) - if [ "$SIGN_IMAGE" == "true" ]; then - export COSIGN_EXPERIMENTAL=1 - if ! cosign verify --certificate-identity-regexp ".*" --certificate-oidc-issuer-regexp ".*" "$FULL_IMAGE_NAME@${IMAGE_DIGEST}" >/dev/null 2>&1; then - echo "::warning::Could not verify signature for digest: $IMAGE_DIGEST" - else - echo "โœ… Signature verified for digest: $IMAGE_DIGEST" - fi - fi - - - name: Clean up - if: always() - shell: bash - env: - REGISTRY: ${{ inputs.registry }} - run: |- - set -euo pipefail - - # Remove temporary files and cleanup Docker cache - docker buildx prune -f --keep-storage=10GB - - # Logout from registry - docker logout "$REGISTRY" diff --git a/docker-publish-gh/rules.yml b/docker-publish-gh/rules.yml deleted file mode 100644 index 16bbd01..0000000 --- a/docker-publish-gh/rules.yml +++ /dev/null @@ -1,65 +0,0 @@ ---- -# Validation rules for docker-publish-gh action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (16/16 inputs) -# -# This file defines validation rules for the docker-publish-gh GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: docker-publish-gh -description: Publishes a Docker image to GitHub Packages with advanced security and reliability features. -generator_version: 1.0.0 -required_inputs: - - tags -optional_inputs: - - auto-detect-platforms - - buildx-version - - cache-mode - - image-name - - max-retries - - parallel-builds - - platforms - - provenance - - registry - - retry-delay - - sbom - - scan-image - - sign-image - - token - - verbose -conventions: - auto-detect-platforms: docker_architectures - buildx-version: semantic_version - cache-mode: boolean - image-name: docker_image_name - max-retries: numeric_range_1_10 - parallel-builds: numeric_range_0_16 - platforms: docker_architectures - provenance: boolean - registry: registry - retry-delay: numeric_range_1_300 - sbom: boolean - scan-image: boolean - sign-image: boolean - tags: docker_tag - token: github_token - verbose: boolean -overrides: {} -statistics: - total_inputs: 16 - validated_inputs: 16 - skipped_inputs: 0 - coverage_percentage: 100 -validation_coverage: 100 -auto_detected: true -manual_review_required: false -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/docker-publish-hub/CustomValidator.py b/docker-publish-hub/CustomValidator.py deleted file mode 100755 index 0b978a5..0000000 --- a/docker-publish-hub/CustomValidator.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for docker-publish-hub 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.docker import DockerValidator -from validators.security import SecurityValidator -from validators.token import TokenValidator - - -class CustomValidator(BaseValidator): - """Custom validator for docker-publish-hub action.""" - - def __init__(self, action_type: str = "docker-publish-hub") -> None: - """Initialize docker-publish-hub validator.""" - super().__init__(action_type) - self.docker_validator = DockerValidator() - self.token_validator = TokenValidator() - self.security_validator = SecurityValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate docker-publish-hub action inputs.""" - valid = True - - # Validate required input: image-name - if "image-name" not in inputs or not inputs["image-name"]: - self.add_error("Input 'image-name' is required") - valid = False - elif inputs["image-name"]: - result = self.docker_validator.validate_image_name(inputs["image-name"], "image-name") - for error in self.docker_validator.errors: - if error not in self.errors: - self.add_error(error) - self.docker_validator.clear_errors() - if not result: - valid = False - - # Validate username for injection if provided - if inputs.get("username"): - result = self.security_validator.validate_no_injection(inputs["username"], "username") - 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 password if provided - if inputs.get("password"): - result = self.token_validator.validate_docker_token(inputs["password"], "password") - 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 ["image-name"] - - def get_validation_rules(self) -> dict: - """Get validation rules.""" - return { - "image-name": { - "type": "string", - "required": True, - "description": "Docker image name", - }, - "username": { - "type": "string", - "required": False, - "description": "Docker Hub username", - }, - "password": { - "type": "token", - "required": False, - "description": "Docker Hub password", - }, - } diff --git a/docker-publish-hub/README.md b/docker-publish-hub/README.md deleted file mode 100644 index e210b72..0000000 --- a/docker-publish-hub/README.md +++ /dev/null @@ -1,154 +0,0 @@ -# ivuorinen/actions/docker-publish-hub - -## Docker Publish to Docker Hub - -### Description - -Publishes a Docker image to Docker Hub with enhanced security and reliability features. - -### Inputs - -| name | description | required | default | -|--------------------------|----------------------------------------------------------------------------------|----------|---------------------------| -| `image-name` |

The name of the Docker image to publish. Defaults to the repository name.

| `false` | `""` | -| `tags` |

Comma-separated list of tags for the Docker image.

| `true` | `""` | -| `platforms` |

Platforms to publish (comma-separated). Defaults to amd64 and arm64.

| `false` | `linux/amd64,linux/arm64` | -| `username` |

Docker Hub username

| `true` | `""` | -| `password` |

Docker Hub password or access token

| `true` | `""` | -| `repository-description` |

Update Docker Hub repository description

| `false` | `""` | -| `readme-file` |

Path to README file to update on Docker Hub

| `false` | `README.md` | -| `provenance` |

Enable SLSA provenance generation

| `false` | `true` | -| `sbom` |

Generate Software Bill of Materials

| `false` | `true` | -| `max-retries` |

Maximum number of retry attempts for publishing

| `false` | `3` | -| `retry-delay` |

Delay in seconds between retries

| `false` | `10` | -| `buildx-version` |

Specific Docker Buildx version to use

| `false` | `latest` | -| `cache-mode` |

Cache mode for build layers (min, max, or inline)

| `false` | `max` | -| `auto-detect-platforms` |

Automatically detect and build for all available platforms

| `false` | `false` | -| `scan-image` |

Scan published image for vulnerabilities

| `false` | `true` | -| `sign-image` |

Sign the published image with cosign

| `false` | `false` | -| `verbose` |

Enable verbose logging

| `false` | `false` | - -### Outputs - -| name | description | -|-------------------|-------------------------------------------| -| `image-name` |

Full image name including registry

| -| `digest` |

The digest of the published image

| -| `tags` |

List of published tags

| -| `repo-url` |

Docker Hub repository URL

| -| `scan-results` |

Vulnerability scan results

| -| `platform-matrix` |

Build status per platform

| -| `build-time` |

Total build time in seconds

| -| `signature` |

Image signature if signing enabled

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/docker-publish-hub@main - with: - image-name: - # The name of the Docker image to publish. Defaults to the repository name. - # - # Required: false - # Default: "" - - tags: - # Comma-separated list of tags for the Docker image. - # - # Required: true - # Default: "" - - platforms: - # Platforms to publish (comma-separated). Defaults to amd64 and arm64. - # - # Required: false - # Default: linux/amd64,linux/arm64 - - username: - # Docker Hub username - # - # Required: true - # Default: "" - - password: - # Docker Hub password or access token - # - # Required: true - # Default: "" - - repository-description: - # Update Docker Hub repository description - # - # Required: false - # Default: "" - - readme-file: - # Path to README file to update on Docker Hub - # - # Required: false - # Default: README.md - - provenance: - # Enable SLSA provenance generation - # - # Required: false - # Default: true - - sbom: - # Generate Software Bill of Materials - # - # Required: false - # Default: true - - max-retries: - # Maximum number of retry attempts for publishing - # - # Required: false - # Default: 3 - - retry-delay: - # Delay in seconds between retries - # - # Required: false - # Default: 10 - - buildx-version: - # Specific Docker Buildx version to use - # - # Required: false - # Default: latest - - cache-mode: - # Cache mode for build layers (min, max, or inline) - # - # Required: false - # Default: max - - auto-detect-platforms: - # Automatically detect and build for all available platforms - # - # Required: false - # Default: false - - scan-image: - # Scan published image for vulnerabilities - # - # Required: false - # Default: true - - sign-image: - # Sign the published image with cosign - # - # Required: false - # Default: false - - verbose: - # Enable verbose logging - # - # Required: false - # Default: false -``` diff --git a/docker-publish-hub/action.yml b/docker-publish-hub/action.yml deleted file mode 100644 index 5a5888f..0000000 --- a/docker-publish-hub/action.yml +++ /dev/null @@ -1,500 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - packages: write # Required for publishing to Docker Hub -# - contents: read # Required for checking out repository ---- -name: Docker Publish to Docker Hub -description: 'Publishes a Docker image to Docker Hub with enhanced security and reliability features.' -author: 'Ismo Vuorinen' - -branding: - icon: 'package' - color: 'blue' - -inputs: - image-name: - description: 'The name of the Docker image to publish. Defaults to the repository name.' - required: false - tags: - description: 'Comma-separated list of tags for the Docker image.' - required: true - platforms: - description: 'Platforms to publish (comma-separated). Defaults to amd64 and arm64.' - required: false - default: 'linux/amd64,linux/arm64' - username: - description: 'Docker Hub username' - required: true - password: - description: 'Docker Hub password or access token' - required: true - repository-description: - description: 'Update Docker Hub repository description' - required: false - readme-file: - description: 'Path to README file to update on Docker Hub' - required: false - default: 'README.md' - provenance: - description: 'Enable SLSA provenance generation' - required: false - default: 'true' - sbom: - description: 'Generate Software Bill of Materials' - required: false - default: 'true' - max-retries: - description: 'Maximum number of retry attempts for publishing' - required: false - default: '3' - retry-delay: - description: 'Delay in seconds between retries' - required: false - default: '10' - buildx-version: - description: 'Specific Docker Buildx version to use' - required: false - default: 'latest' - cache-mode: - description: 'Cache mode for build layers (min, max, or inline)' - required: false - default: 'max' - auto-detect-platforms: - description: 'Automatically detect and build for all available platforms' - required: false - default: 'false' - scan-image: - description: 'Scan published image for vulnerabilities' - required: false - default: 'true' - sign-image: - description: 'Sign the published image with cosign' - required: false - default: 'false' - verbose: - description: 'Enable verbose logging' - required: false - default: 'false' - -outputs: - image-name: - description: 'Full image name including registry' - value: ${{ steps.metadata.outputs.full-name }} - digest: - description: 'The digest of the published image' - value: ${{ steps.publish.outputs.digest }} - tags: - description: 'List of published tags' - value: ${{ steps.metadata.outputs.tags }} - repo-url: - description: 'Docker Hub repository URL' - value: ${{ steps.metadata.outputs.repo-url }} - scan-results: - description: 'Vulnerability scan results' - value: ${{ steps.scan.outputs.results }} - platform-matrix: - description: 'Build status per platform' - value: ${{ steps.publish.outputs.platform-matrix }} - build-time: - description: 'Total build time in seconds' - value: ${{ steps.publish.outputs.build-time }} - signature: - description: 'Image signature if signing enabled' - value: ${{ steps.sign.outputs.signature }} - -runs: - using: composite - steps: - - name: Mask Secrets - shell: bash - env: - DOCKERHUB_PASSWORD: ${{ inputs.password }} - run: | - echo "::add-mask::$DOCKERHUB_PASSWORD" - - - name: Validate Inputs - id: validate - shell: bash - env: - IMAGE_NAME: ${{ inputs.image-name }} - TAGS: ${{ inputs.tags }} - PLATFORMS: ${{ inputs.platforms }} - DOCKERHUB_USERNAME: ${{ inputs.username }} - DOCKERHUB_PASSWORD: ${{ inputs.password }} - run: | - set -euo pipefail - - # Validate image name format - if [ -n "$IMAGE_NAME" ]; then - if ! [[ "$IMAGE_NAME" =~ ^[a-z0-9]+([._-][a-z0-9]+)*$ ]]; then - echo "::error::Invalid image name format" - exit 1 - fi - fi - - # Validate tags - IFS=',' read -ra TAG_ARRAY <<< "$TAGS" - for tag in "${TAG_ARRAY[@]}"; do - if ! [[ "$tag" =~ ^(v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._]+)?(\+[a-zA-Z0-9._]+)?|latest|[a-zA-Z][-a-zA-Z0-9._]{0,127})$ ]]; then - echo "::error::Invalid tag format: $tag" - exit 1 - fi - done - - # Validate platforms - IFS=',' read -ra PLATFORM_ARRAY <<< "$PLATFORMS" - for platform in "${PLATFORM_ARRAY[@]}"; do - if ! [[ "$platform" =~ ^linux/(amd64|arm64|arm/v7|arm/v6|386|ppc64le|s390x)$ ]]; then - echo "::error::Invalid platform: $platform" - exit 1 - fi - done - - # Validate credentials (without exposing them) - if [ -z "$DOCKERHUB_USERNAME" ] || [ -z "$DOCKERHUB_PASSWORD" ]; then - echo "::error::Docker Hub credentials are required" - exit 1 - fi - - - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - with: - platforms: ${{ inputs.platforms }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - with: - version: ${{ inputs.buildx-version }} - platforms: ${{ inputs.platforms }} - buildkitd-flags: --debug - driver-opts: | - network=host - image=moby/buildkit:${{ inputs.buildx-version }} - - - name: Prepare Metadata - id: metadata - shell: bash - env: - IMAGE_NAME: ${{ inputs.image-name }} - DOCKERHUB_USERNAME: ${{ inputs.username }} - TAGS: ${{ inputs.tags }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: | - set -euo pipefail - - # Determine image name - if [ -z "$IMAGE_NAME" ]; then - image_name=$(basename $GITHUB_REPOSITORY) - else - image_name="$IMAGE_NAME" - fi - - # Construct full image name - full_name="${DOCKERHUB_USERNAME}/${image_name}" - echo "full-name=${full_name}" >> $GITHUB_OUTPUT - - # Process tags - processed_tags="" - IFS=',' read -ra TAG_ARRAY <<< "$TAGS" - for tag in "${TAG_ARRAY[@]}"; do - processed_tags="${processed_tags}${full_name}:${tag}," - done - processed_tags=${processed_tags%,} - echo "tags=${processed_tags}" >> $GITHUB_OUTPUT - - # Generate repository URL - echo "repo-url=https://hub.docker.com/r/${full_name}" >> $GITHUB_OUTPUT - - - name: Log in to Docker Hub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 - with: - username: ${{ inputs.username }} - password: ${{ inputs.password }} - - - name: Set up Cosign - if: inputs.provenance == 'true' - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 - - - name: Update Docker Hub Description - if: inputs.repository-description != '' || inputs.readme-file != '' - shell: bash - env: - DOCKERHUB_USERNAME: ${{ inputs.username }} - DOCKERHUB_PASSWORD: ${{ inputs.password }} - REPO_DESCRIPTION: ${{ inputs.repository-description }} - README_FILE: ${{ inputs.readme-file }} - FULL_NAME: ${{ steps.metadata.outputs.full-name }} - run: | - set -euo pipefail - - # Install Docker Hub API client - pip install docker-hub-api - - # Update repository description - if [ -n "$REPO_DESCRIPTION" ]; then - docker-hub-api update-repo \ - --user "$DOCKERHUB_USERNAME" \ - --password "$DOCKERHUB_PASSWORD" \ - --name "$FULL_NAME" \ - --description "$REPO_DESCRIPTION" - fi - - # Update README - if [ -f "$README_FILE" ]; then - docker-hub-api update-repo \ - --user "$DOCKERHUB_USERNAME" \ - --password "$DOCKERHUB_PASSWORD" \ - --name "$FULL_NAME" \ - --full-description "$(cat "$README_FILE")" - fi - - - name: Detect Available Platforms - id: detect-platforms - if: inputs.auto-detect-platforms == 'true' - shell: bash - env: - DEFAULT_PLATFORMS: ${{ inputs.platforms }} - run: | - set -euo pipefail - - # Get available platforms from buildx - available_platforms=$(docker buildx ls | grep -o 'linux/[^ ]*' | sort -u | tr '\n' ',' | sed 's/,$//') - - if [ -n "$available_platforms" ]; then - echo "platforms=${available_platforms}" >> $GITHUB_OUTPUT - echo "Detected platforms: ${available_platforms}" - else - echo "platforms=$DEFAULT_PLATFORMS" >> $GITHUB_OUTPUT - echo "Using default platforms: $DEFAULT_PLATFORMS" - fi - - - name: Publish Image - id: publish - shell: bash - env: - DOCKER_BUILDKIT: 1 - AUTO_DETECT: ${{ inputs.auto-detect-platforms }} - DETECTED_PLATFORMS: ${{ steps.detect-platforms.outputs.platforms }} - DEFAULT_PLATFORMS: ${{ inputs.platforms }} - IMAGE_TAGS: ${{ steps.metadata.outputs.tags }} - DOCKERHUB_USERNAME: ${{ inputs.username }} - CACHE_MODE: ${{ inputs.cache-mode }} - ENABLE_PROVENANCE: ${{ inputs.provenance }} - ENABLE_SBOM: ${{ inputs.sbom }} - VERBOSE: ${{ inputs.verbose }} - MAX_RETRIES: ${{ inputs.max-retries }} - RETRY_DELAY: ${{ inputs.retry-delay }} - FULL_NAME: ${{ steps.metadata.outputs.full-name }} - TAGS: ${{ inputs.tags }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_SHA: ${{ github.sha }} - run: | - set -euo pipefail - - # Track build start time - build_start=$(date +%s) - - # Determine platforms - if [ "$AUTO_DETECT" == "true" ] && [ -n "$DETECTED_PLATFORMS" ]; then - platforms="$DETECTED_PLATFORMS" - else - platforms="$DEFAULT_PLATFORMS" - fi - - # Initialize platform matrix tracking - platform_matrix="{}" - - # Prepare verbose flag - verbose_flag="" - if [ "$VERBOSE" == "true" ]; then - verbose_flag="--progress=plain" - fi - - # Prepare optional flags - provenance_flag="" - if [ "$ENABLE_PROVENANCE" == "true" ]; then - provenance_flag="--provenance=true" - fi - - sbom_flag="" - if [ "$ENABLE_SBOM" == "true" ]; then - sbom_flag="--sbom=true" - fi - - attempt=1 - - while [ $attempt -le $MAX_RETRIES ]; do - echo "Publishing attempt $attempt of $MAX_RETRIES" - - if docker buildx build \ - --platform="${platforms}" \ - --tag "$IMAGE_TAGS" \ - --push \ - --cache-from "type=registry,ref=$DOCKERHUB_USERNAME/buildcache:latest" \ - --cache-to "type=registry,ref=$DOCKERHUB_USERNAME/buildcache:latest,mode=$CACHE_MODE" \ - $provenance_flag \ - $sbom_flag \ - ${verbose_flag} \ - --metadata-file=/tmp/build-metadata.json \ - --label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ - --label "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ - --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ - --label "org.opencontainers.image.version=$TAGS" \ - .; then - - # Get image digest - IFS=',' read -ra TAG_ARRAY <<< "$TAGS" - digest=$(docker buildx imagetools inspect "$FULL_NAME:${TAG_ARRAY[0]}" --raw | jq -r '.digest // "unknown"' || echo "unknown") - echo "digest=${digest}" >> $GITHUB_OUTPUT - - # Calculate build time - build_end=$(date +%s) - build_time=$((build_end - build_start)) - echo "build-time=${build_time}" >> $GITHUB_OUTPUT - - # Build platform matrix - IFS=',' read -ra PLATFORM_ARRAY <<< "${platforms}" - platform_matrix="{" - for p in "${PLATFORM_ARRAY[@]}"; do - platform_matrix="${platform_matrix}\"${p}\":\"success\"," - done - platform_matrix="${platform_matrix%,}}" - echo "platform-matrix=${platform_matrix}" >> $GITHUB_OUTPUT - - break - fi - - attempt=$((attempt + 1)) - if [ $attempt -le $MAX_RETRIES ]; then - echo "Publish failed, waiting $RETRY_DELAY seconds before retry..." - sleep "$RETRY_DELAY" - else - echo "::error::Publishing failed after $MAX_RETRIES attempts" - exit 1 - fi - done - - - name: Scan Published Image - id: scan - if: inputs.scan-image == 'true' - shell: bash - env: - FULL_NAME: ${{ steps.metadata.outputs.full-name }} - IMAGE_DIGEST: ${{ steps.publish.outputs.digest }} - run: | - set -euo pipefail - - # Install Trivy - wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - - echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list - sudo apt-get update && sudo apt-get install -y trivy - - # Scan the exact digest that was just built (not tags which could be stale) - trivy image \ - --severity HIGH,CRITICAL \ - --format json \ - --output /tmp/scan-results.json \ - "$FULL_NAME@${IMAGE_DIGEST}" - - # Output results - scan_results=$(cat /tmp/scan-results.json | jq -c '.') - echo "results=${scan_results}" >> $GITHUB_OUTPUT - - # Check for critical vulnerabilities - critical_count=$(cat /tmp/scan-results.json | jq '.Results[].Vulnerabilities[] | select(.Severity == "CRITICAL") | .VulnerabilityID' | wc -l) - if [ "$critical_count" -gt 0 ]; then - echo "::warning::Found $critical_count critical vulnerabilities in published image" - fi - - - name: Install Cosign - if: inputs.sign-image == 'true' - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 - - - name: Sign Published Image - id: sign - if: inputs.sign-image == 'true' - shell: bash - env: - FULL_NAME: ${{ steps.metadata.outputs.full-name }} - TAGS: ${{ inputs.tags }} - run: | - set -euo pipefail - - # Sign all tags - IFS=',' read -ra TAG_ARRAY <<< "$TAGS" - for tag in "${TAG_ARRAY[@]}"; do - echo "Signing $FULL_NAME:${tag}" - - # Using keyless signing with OIDC - export COSIGN_EXPERIMENTAL=1 - cosign sign --yes "$FULL_NAME:${tag}" - done - - echo "signature=signed" >> $GITHUB_OUTPUT - - - name: Verify Publication - id: verify - shell: bash - env: - FULL_NAME: ${{ steps.metadata.outputs.full-name }} - IMAGE_DIGEST: ${{ steps.publish.outputs.digest }} - AUTO_DETECT: ${{ inputs.auto-detect-platforms }} - DETECTED_PLATFORMS: ${{ steps.detect-platforms.outputs.platforms }} - DEFAULT_PLATFORMS: ${{ inputs.platforms }} - SIGN_IMAGE: ${{ inputs.sign-image }} - run: | - set -euo pipefail - - # Verify image existence and accessibility using exact digest - if [ -z "$IMAGE_DIGEST" ] || [ "$IMAGE_DIGEST" == "unknown" ]; then - echo "::error::No valid image digest available for verification" - exit 1 - fi - - # Verify the exact digest that was just built - if ! docker buildx imagetools inspect "$FULL_NAME@${IMAGE_DIGEST}" >/dev/null 2>&1; then - echo "::error::Published image not found at digest: $IMAGE_DIGEST" - exit 1 - fi - - echo "โœ… Verified image at digest: $IMAGE_DIGEST" - - # Determine platforms to verify - if [ "$AUTO_DETECT" == "true" ] && [ -n "$DETECTED_PLATFORMS" ]; then - platforms="$DETECTED_PLATFORMS" - else - platforms="$DEFAULT_PLATFORMS" - fi - - # Verify platforms using the exact digest - IFS=',' read -ra PLATFORM_ARRAY <<< "${platforms}" - for platform in "${PLATFORM_ARRAY[@]}"; do - if ! docker buildx imagetools inspect "$FULL_NAME@${IMAGE_DIGEST}" | grep -q "$platform"; then - echo "::warning::Platform $platform not found in published image" - else - echo "โœ… Verified platform: $platform" - fi - done - - # Verify signature if signing was enabled (use digest for verification) - if [ "$SIGN_IMAGE" == "true" ]; then - export COSIGN_EXPERIMENTAL=1 - if ! cosign verify --certificate-identity-regexp ".*" --certificate-oidc-issuer-regexp ".*" "$FULL_NAME@${IMAGE_DIGEST}" >/dev/null 2>&1; then - echo "::warning::Could not verify signature for digest ${IMAGE_DIGEST}" - else - echo "โœ… Verified signature for digest: $IMAGE_DIGEST" - fi - fi - - - name: Clean up - if: always() - shell: bash - run: |- - set -euo pipefail - - # Remove temporary files and cleanup Docker cache - docker buildx prune -f --keep-storage=10GB - - # Logout from Docker Hub - docker logout diff --git a/docker-publish-hub/rules.yml b/docker-publish-hub/rules.yml deleted file mode 100644 index f7882e4..0000000 --- a/docker-publish-hub/rules.yml +++ /dev/null @@ -1,68 +0,0 @@ ---- -# Validation rules for docker-publish-hub action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (17/17 inputs) -# -# This file defines validation rules for the docker-publish-hub GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: docker-publish-hub -description: Publishes a Docker image to Docker Hub with enhanced security and reliability features. -generator_version: 1.0.0 -required_inputs: - - password - - tags - - username -optional_inputs: - - auto-detect-platforms - - buildx-version - - cache-mode - - image-name - - max-retries - - platforms - - provenance - - readme-file - - repository-description - - retry-delay - - sbom - - scan-image - - sign-image - - verbose -conventions: - auto-detect-platforms: docker_architectures - buildx-version: semantic_version - cache-mode: boolean - image-name: docker_image_name - max-retries: numeric_range_1_10 - password: github_token - platforms: docker_architectures - provenance: boolean - readme-file: file_path - repository-description: security_patterns - retry-delay: numeric_range_1_300 - sbom: boolean - scan-image: boolean - sign-image: boolean - tags: docker_tag - username: username - verbose: boolean -overrides: - password: docker_password -statistics: - total_inputs: 17 - validated_inputs: 17 - skipped_inputs: 0 - coverage_percentage: 100 -validation_coverage: 100 -auto_detected: true -manual_review_required: false -quality_indicators: - has_required_inputs: true - has_token_validation: false - has_version_validation: true - has_file_validation: true - has_security_validation: true diff --git a/docker-publish/README.md b/docker-publish/README.md index 81eff07..b3d6386 100644 --- a/docker-publish/README.md +++ b/docker-publish/README.md @@ -4,37 +4,32 @@ ### Description -Publish a Docker image to GitHub Packages and Docker Hub. +Simple wrapper to publish Docker images to GitHub Packages and/or Docker Hub ### Inputs -| name | description | required | default | -|-------------------------|-------------------------------------------------------------------|----------|----------------------------------------| -| `registry` |

Registry to publish to (dockerhub, github, or both).

| `true` | `both` | -| `nightly` |

Is this a nightly build? (true or false)

| `false` | `false` | -| `platforms` |

Platforms to build for (comma-separated)

| `false` | `linux/amd64,linux/arm64,linux/arm/v7` | -| `auto-detect-platforms` |

Automatically detect and build for all available platforms

| `false` | `false` | -| `scan-image` |

Scan images for vulnerabilities

| `false` | `true` | -| `sign-image` |

Sign images with cosign

| `false` | `false` | -| `cache-mode` |

Cache mode for build layers (min, max, or inline)

| `false` | `max` | -| `buildx-version` |

Specific Docker Buildx version to use

| `false` | `latest` | -| `verbose` |

Enable verbose logging

| `false` | `false` | -| `dockerhub-username` |

Docker Hub username for authentication

| `false` | `""` | -| `dockerhub-password` |

Docker Hub password or access token for authentication

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

GitHub token for authentication

| `false` | `""` | +| name | description | required | default | +|----------------------|-------------------------------------------------------------------|----------|---------------------------| +| `registry` |

Registry to publish to (dockerhub, github, or both)

| `false` | `both` | +| `image-name` |

Docker image name (defaults to repository name)

| `false` | `""` | +| `tags` |

Comma-separated list of tags (e.g., latest,v1.0.0)

| `false` | `latest` | +| `platforms` |

Platforms to build for (comma-separated)

| `false` | `linux/amd64,linux/arm64` | +| `context` |

Build context path

| `false` | `.` | +| `dockerfile` |

Path to Dockerfile

| `false` | `Dockerfile` | +| `build-args` |

Build arguments (newline-separated KEY=VALUE pairs)

| `false` | `""` | +| `push` |

Whether to push the image

| `false` | `true` | +| `token` |

GitHub token for authentication (for GitHub registry)

| `false` | `""` | +| `dockerhub-username` |

Docker Hub username (required if publishing to Docker Hub)

| `false` | `""` | +| `dockerhub-token` |

Docker Hub token (required if publishing to Docker Hub)

| `false` | `""` | ### Outputs -| name | description | -|-------------------|-------------------------------------------------------| -| `registry` |

Registry where image was published

| -| `tags` |

Tags that were published

| -| `build-time` |

Total build time in seconds

| -| `platform-matrix` |

Build status per platform

| -| `scan-results` |

Vulnerability scan results if scanning enabled

| -| `image-id` |

Published image ID

| -| `image-digest` |

Published image digest

| -| `repository` |

Repository where image was published

| +| name | description | +|--------------|--------------------------------------| +| `image-name` |

Full image name with registry

| +| `tags` |

Tags that were published

| +| `digest` |

Image digest

| +| `metadata` |

Build metadata

| ### Runs @@ -46,73 +41,67 @@ This action is a `composite` action. - uses: ivuorinen/actions/docker-publish@main with: registry: - # Registry to publish to (dockerhub, github, or both). - # - # Required: true - # Default: both - - nightly: - # Is this a nightly build? (true or false) + # Registry to publish to (dockerhub, github, or both) # # Required: false - # Default: false + # Default: both + + image-name: + # Docker image name (defaults to repository name) + # + # Required: false + # Default: "" + + tags: + # Comma-separated list of tags (e.g., latest,v1.0.0) + # + # Required: false + # Default: latest platforms: # Platforms to build for (comma-separated) # # Required: false - # Default: linux/amd64,linux/arm64,linux/arm/v7 + # Default: linux/amd64,linux/arm64 - auto-detect-platforms: - # Automatically detect and build for all available platforms + context: + # Build context path # # Required: false - # Default: false + # Default: . - scan-image: - # Scan images for vulnerabilities + dockerfile: + # Path to Dockerfile + # + # Required: false + # Default: Dockerfile + + build-args: + # Build arguments (newline-separated KEY=VALUE pairs) + # + # Required: false + # Default: "" + + push: + # Whether to push the image # # Required: false # Default: true - sign-image: - # Sign images with cosign + token: + # GitHub token for authentication (for GitHub registry) # # Required: false - # Default: false - - cache-mode: - # Cache mode for build layers (min, max, or inline) - # - # Required: false - # Default: max - - buildx-version: - # Specific Docker Buildx version to use - # - # Required: false - # Default: latest - - verbose: - # Enable verbose logging - # - # Required: false - # Default: false + # Default: "" dockerhub-username: - # Docker Hub username for authentication + # Docker Hub username (required if publishing to Docker Hub) # # Required: false # Default: "" - dockerhub-password: - # Docker Hub password or access token for authentication - # - # Required: false - # Default: "" - - token: - # GitHub token for authentication + dockerhub-token: + # Docker Hub token (required if publishing to Docker Hub) # # Required: false # Default: "" diff --git a/docker-publish/action.yml b/docker-publish/action.yml index bf292a3..af2e816 100644 --- a/docker-publish/action.yml +++ b/docker-publish/action.yml @@ -2,9 +2,33 @@ # permissions: # - packages: write # Required for publishing to Docker registries # - contents: read # Required for checking out repository +# +# Security Considerations: +# +# Trust Model: This action should only be used in trusted workflows controlled by repository owners. +# Do not pass untrusted user input (e.g., PR labels, comments, external webhooks) to the `context` +# or `dockerfile` parameters. +# +# Input Validation: The action validates `context` and `dockerfile` inputs to prevent code injection attacks: +# +# - `context`: Must be a relative path (e.g., `.`, `./app`, `subdir/`). Absolute paths are rejected. +# Remote URLs trigger a warning and should only be used from trusted sources. +# - `dockerfile`: Must be a relative path (e.g., `Dockerfile`, `./docker/Dockerfile`). Absolute paths +# and URLs are rejected. +# +# These validations help prevent malicious actors from: +# - Building Docker images from arbitrary file system locations +# - Fetching malicious Dockerfiles from untrusted remote sources +# - Executing code injection attacks through build context manipulation +# +# Best Practices: +# 1. Only use hard-coded values or trusted workflow variables for `context` and `dockerfile` +# 2. Never accept these values from PR comments, labels, or external webhooks +# 3. Review workflow permissions before granting write access to this action +# 4. Use SHA-pinned action references: `ivuorinen/actions/docker-publish@` --- name: Docker Publish -description: Publish a Docker image to GitHub Packages and Docker Hub. +description: Simple wrapper to publish Docker images to GitHub Packages and/or Docker Hub author: Ismo Vuorinen branding: @@ -13,309 +37,225 @@ branding: inputs: registry: - description: 'Registry to publish to (dockerhub, github, or both).' - required: true - default: 'both' - nightly: - description: 'Is this a nightly build? (true or false)' + description: 'Registry to publish to (dockerhub, github, or both)' required: false - default: 'false' + default: 'both' + image-name: + description: 'Docker image name (defaults to repository name)' + required: false + tags: + description: 'Comma-separated list of tags (e.g., latest,v1.0.0)' + required: false + default: 'latest' platforms: description: 'Platforms to build for (comma-separated)' required: false - default: 'linux/amd64,linux/arm64,linux/arm/v7' - auto-detect-platforms: - description: 'Automatically detect and build for all available platforms' + default: 'linux/amd64,linux/arm64' + context: + description: 'Build context path' required: false - default: 'false' - scan-image: - description: 'Scan images for vulnerabilities' + default: '.' + dockerfile: + description: 'Path to Dockerfile' + required: false + default: 'Dockerfile' + build-args: + description: 'Build arguments (newline-separated KEY=VALUE pairs)' + required: false + push: + description: 'Whether to push the image' required: false default: 'true' - sign-image: - description: 'Sign images with cosign' - required: false - default: 'false' - cache-mode: - description: 'Cache mode for build layers (min, max, or inline)' - required: false - default: 'max' - buildx-version: - description: 'Specific Docker Buildx version to use' - required: false - default: 'latest' - verbose: - description: 'Enable verbose logging' - required: false - default: 'false' - dockerhub-username: - description: 'Docker Hub username for authentication' - required: false - dockerhub-password: - description: 'Docker Hub password or access token for authentication' - required: false token: - description: 'GitHub token for authentication' + description: 'GitHub token for authentication (for GitHub registry)' required: false default: '' + dockerhub-username: + description: 'Docker Hub username (required if publishing to Docker Hub)' + required: false + dockerhub-token: + description: 'Docker Hub token (required if publishing to Docker Hub)' + required: false outputs: - registry: - description: 'Registry where image was published' - value: ${{ steps.dest.outputs.reg }} + image-name: + description: 'Full image name with registry' + value: ${{ steps.meta.outputs.image-name }} tags: description: 'Tags that were published' - value: ${{ steps.tags.outputs.all-tags }} - build-time: - description: 'Total build time in seconds' - value: ${{ steps.build.outputs.build-time }} - platform-matrix: - description: 'Build status per platform' - value: ${{ steps.build.outputs.platform-matrix }} - scan-results: - description: 'Vulnerability scan results if scanning enabled' - value: ${{ steps.build.outputs.scan-results }} - image-id: - description: 'Published image ID' - value: ${{ steps.publish-dockerhub.outputs.image-id || steps.publish-github.outputs.image-id }} - image-digest: - description: 'Published image digest' - value: ${{ steps.publish-dockerhub.outputs.digest || steps.publish-github.outputs.digest }} - repository: - description: 'Repository where image was published' - value: ${{ steps.publish-dockerhub.outputs.repository || steps.publish-github.outputs.repository }} + value: ${{ steps.meta.outputs.tags }} + digest: + description: 'Image digest' + value: ${{ steps.build.outputs.digest }} + metadata: + description: 'Build metadata' + value: ${{ steps.build.outputs.metadata }} runs: using: composite steps: - - name: Mask Sensitive Inputs - shell: bash - env: - DOCKERHUB_PASSWORD: ${{ inputs.dockerhub-password }} - run: | - set -euo pipefail - - # Mask Docker Hub credentials to prevent exposure in logs - if [[ -n "${DOCKERHUB_PASSWORD}" ]]; then - echo "::add-mask::${DOCKERHUB_PASSWORD}" - fi - - name: Validate Inputs id: validate - shell: bash + shell: sh env: - REGISTRY: ${{ inputs.registry }} + INPUT_REGISTRY: ${{ inputs.registry }} + INPUT_DOCKERHUB_USERNAME: ${{ inputs.dockerhub-username }} + INPUT_DOCKERHUB_TOKEN: ${{ inputs.dockerhub-token }} + INPUT_TOKEN: ${{ inputs.token }} + INPUT_CONTEXT: ${{ inputs.context }} + INPUT_DOCKERFILE: ${{ inputs.dockerfile }} run: | - set -euo pipefail + set -eu # Validate registry input - if ! [[ "$REGISTRY" =~ ^(dockerhub|github|both)$ ]]; then - echo "::error::Invalid registry value. Must be 'dockerhub', 'github', or 'both'" - exit 1 - fi + case "$INPUT_REGISTRY" in + dockerhub|github|both) + ;; + *) + echo "::error::Invalid registry value. Must be 'dockerhub', 'github', or 'both'" + exit 1 + ;; + esac - - name: Determine Tags - id: tags - shell: bash - env: - NIGHTLY: ${{ inputs.nightly }} - RELEASE_TAG: ${{ github.event.release.tag_name }} - run: | - set -euo pipefail - - # Initialize variables - declare -a tag_array - - if [[ "$NIGHTLY" == "true" ]]; then - # Nightly build tags - current_date=$(date +'%Y%m%d-%H%M') - tag_array+=("nightly") - tag_array+=("nightly-${current_date}") - else - # Release tags - if [[ -n "$RELEASE_TAG" ]]; then - tag_array+=("$RELEASE_TAG") - tag_array+=("latest") - else - echo "::error::No release tag found and not a nightly build" + # Validate Docker Hub credentials if needed + if [ "$INPUT_REGISTRY" = "dockerhub" ] || [ "$INPUT_REGISTRY" = "both" ]; then + if [ -z "$INPUT_DOCKERHUB_USERNAME" ] || [ -z "$INPUT_DOCKERHUB_TOKEN" ]; then + echo "::error::Docker Hub username and token are required when publishing to Docker Hub" exit 1 fi fi - # Join tags with comma - tags=$(IFS=,; echo "${tag_array[*]}") - echo "all-tags=${tags}" >> "$GITHUB_OUTPUT" - echo "Generated tags: ${tags}" - - - name: Determine Publish Destination - id: dest - shell: bash - env: - REGISTRY: ${{ inputs.registry }} - run: | - set -euo pipefail - - if [[ "$REGISTRY" == "both" ]]; then - echo "reg=github,dockerhub" >> "$GITHUB_OUTPUT" - else - echo "reg=$REGISTRY" >> "$GITHUB_OUTPUT" + # Validate GitHub token if needed + if [ "$INPUT_REGISTRY" = "github" ] || [ "$INPUT_REGISTRY" = "both" ]; then + token="${INPUT_TOKEN:-${GITHUB_TOKEN:-}}" + if [ -z "$token" ]; then + echo "::error::GitHub token is required when publishing to GitHub Packages" + exit 1 + fi fi - echo "Publishing to: $REGISTRY" + # Validate context input for security + INPUT_CONTEXT="${INPUT_CONTEXT:-.}" + case "$INPUT_CONTEXT" in + .|./*|*/*) + # Relative paths are allowed + ;; + /*) + echo "::error::Context cannot be an absolute path: '$INPUT_CONTEXT'" + echo "::error::Use relative paths (e.g., '.', './app') to prevent code injection" + exit 1 + ;; + *://*) + echo "::warning::Context is a remote URL: '$INPUT_CONTEXT'" + echo "::warning::Ensure this URL is from a trusted source to prevent code injection" + ;; + esac - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token || github.token }} + # Validate dockerfile input for security + INPUT_DOCKERFILE="${INPUT_DOCKERFILE:-Dockerfile}" + case "$INPUT_DOCKERFILE" in + Dockerfile|*/Dockerfile|*.dockerfile|*/*.dockerfile) + # Common dockerfile patterns are allowed + ;; + /*) + echo "::error::Dockerfile path cannot be absolute: '$INPUT_DOCKERFILE'" + echo "::error::Use relative paths (e.g., 'Dockerfile', './docker/Dockerfile')" + exit 1 + ;; + *://*) + echo "::error::Dockerfile path cannot be a URL: '$INPUT_DOCKERFILE'" + exit 1 + ;; + esac - - name: Build Multi-Arch Docker Image - id: build - uses: ivuorinen/actions/docker-build@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - tag: ${{ steps.tags.outputs.all-tags }} - architectures: ${{ inputs.platforms }} - auto-detect-platforms: ${{ inputs.auto-detect-platforms }} - scan-image: ${{ inputs.scan-image }} - sign-image: ${{ inputs.sign-image }} - cache-mode: ${{ inputs.cache-mode }} - buildx-version: ${{ inputs.buildx-version }} - verbose: ${{ inputs.verbose }} - push: 'false' # Don't push during build, let publish actions handle it + echo "Input validation completed successfully" - - name: Publish to Docker Hub - id: publish-dockerhub - if: contains(steps.dest.outputs.reg, 'dockerhub') - uses: ivuorinen/actions/docker-publish-hub@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - tags: ${{ steps.tags.outputs.all-tags }} - platforms: ${{ inputs.platforms }} - auto-detect-platforms: ${{ inputs.auto-detect-platforms }} - scan-image: ${{ inputs.scan-image }} - sign-image: ${{ inputs.sign-image }} - cache-mode: ${{ inputs.cache-mode }} - buildx-version: ${{ inputs.buildx-version }} - verbose: ${{ inputs.verbose }} - username: ${{ inputs.dockerhub-username }} - password: ${{ inputs.dockerhub-password }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - - name: Publish to GitHub Packages - id: publish-github - if: contains(steps.dest.outputs.reg, 'github') - uses: ivuorinen/actions/docker-publish-gh@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - tags: ${{ steps.tags.outputs.all-tags }} - platforms: ${{ inputs.platforms }} - auto-detect-platforms: ${{ inputs.auto-detect-platforms }} - scan-image: ${{ inputs.scan-image }} - sign-image: ${{ inputs.sign-image }} - cache-mode: ${{ inputs.cache-mode }} - buildx-version: ${{ inputs.buildx-version }} - verbose: ${{ inputs.verbose }} - - - name: Verify Publications - id: verify - shell: bash + - name: Determine Image Names and Tags + id: meta + shell: sh env: - DEST_REG: ${{ steps.dest.outputs.reg }} - DOCKERHUB_IMAGE_NAME: ${{ steps.publish-dockerhub.outputs.image-name }} - DOCKERHUB_TAGS: ${{ steps.publish-dockerhub.outputs.tags }} - GITHUB_IMAGE_NAME: ${{ steps.publish-github.outputs.image-name }} - GITHUB_TAGS: ${{ steps.publish-github.outputs.tags }} - ALL_TAGS: ${{ steps.tags.outputs.all-tags }} + INPUT_REGISTRY: ${{ inputs.registry }} + INPUT_IMAGE_NAME: ${{ inputs.image-name }} + INPUT_TAGS: ${{ inputs.tags }} GITHUB_REPOSITORY: ${{ github.repository }} run: | - set -euo pipefail + set -eu - echo "Verifying publications..." - success=true + # Determine base image name + if [ -n "$INPUT_IMAGE_NAME" ]; then + base_name="$INPUT_IMAGE_NAME" + else + # Use repository name (lowercase) + base_name=$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]') + fi - # Split registry string into array - IFS=',' read -ra REGISTRIES <<< "$DEST_REG" + # Build full image names based on registry + image_names="" + case "$INPUT_REGISTRY" in + dockerhub) + image_names="docker.io/${base_name}" + ;; + github) + image_names="ghcr.io/${base_name}" + ;; + both) + image_names="docker.io/${base_name},ghcr.io/${base_name}" + ;; + esac - for registry in "${REGISTRIES[@]}"; do - echo "Checking ${registry} publication..." - case "${registry}" in - "dockerhub") - # Get actual image name from publish step output or fallback to repo-based name - image_name="$DOCKERHUB_IMAGE_NAME" - if [[ -z "$image_name" ]]; then - image_name="docker.io/$GITHUB_REPOSITORY" - fi - - # Get tags from publish step or fallback to metadata - tags="$DOCKERHUB_TAGS" - if [[ -z "$tags" ]]; then - tags="$ALL_TAGS" - fi - - IFS=',' read -ra TAGS <<< "$tags" - for tag in "${TAGS[@]}"; do - tag=$(echo "$tag" | xargs) # trim whitespace - if ! docker manifest inspect "${image_name}:${tag}" > /dev/null 2>&1; then - echo "::error::Failed to verify Docker Hub publication for ${tag}" - success=false - break - fi - done - if [[ "${success}" != "true" ]]; then - break - fi - ;; - "github") - # Get actual image name from publish step output or fallback to repo-based name - image_name="$GITHUB_IMAGE_NAME" - if [[ -z "$image_name" ]]; then - image_name="ghcr.io/$GITHUB_REPOSITORY" - fi - - # Get tags from publish step or fallback to metadata - tags="$GITHUB_TAGS" - if [[ -z "$tags" ]]; then - tags="$ALL_TAGS" - fi - - IFS=',' read -ra TAGS <<< "$tags" - for tag in "${TAGS[@]}"; do - tag=$(echo "$tag" | xargs) # trim whitespace - if ! docker manifest inspect "${image_name}:${tag}" > /dev/null 2>&1; then - echo "::error::Failed to verify GitHub Packages publication for ${tag}" - success=false - break - fi - done - if [[ "${success}" != "true" ]]; then - break - fi - ;; - esac + # Build full tags (image:tag format) + tags="" + IFS=',' + for image in $image_names; do + for tag in $INPUT_TAGS; do + tag=$(echo "$tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + if [ -n "$tags" ]; then + tags="$(printf '%s\n%s' "$tags" "${image}:${tag}")" + else + tags="${image}:${tag}" + fi + done done - if [[ "${success}" != "true" ]]; then - echo "::error::Publication verification failed" - exit 1 - fi + # Output results + printf 'image-name=%s\n' "$base_name" >> "$GITHUB_OUTPUT" + { + echo 'tags<> "$GITHUB_OUTPUT" - echo "All publications verified successfully" + echo "Image name: $base_name" + echo "Tags:" + echo "$tags" - - name: Cleanup - if: always() - shell: bash - env: - DEST_REG: ${{ steps.dest.outputs.reg }} - run: |- - set -euo pipefail + - name: Login to Docker Hub + if: inputs.registry == 'dockerhub' || inputs.registry == 'both' + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + username: ${{ inputs.dockerhub-username }} + password: ${{ inputs.dockerhub-token }} - echo "Cleaning up..." + - name: Login to GitHub Container Registry + if: inputs.registry == 'github' || inputs.registry == 'both' + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.token || github.token }} - # Remove any temporary files or caches - docker buildx prune -f --keep-storage=10GB - - # Remove any temporary authentication - if [[ "$DEST_REG" =~ "dockerhub" ]]; then - docker logout docker.io || true - fi - if [[ "$DEST_REG" =~ "github" ]]; then - docker logout ghcr.io || true - fi - - echo "Cleanup completed" + - name: Build and Push Docker Image + id: build + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + platforms: ${{ inputs.platforms }} + push: ${{ inputs.push }} + tags: ${{ steps.meta.outputs.tags }} + build-args: ${{ inputs.build-args }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/docker-publish/rules.yml b/docker-publish/rules.yml index 749504d..70aa1bc 100644 --- a/docker-publish/rules.yml +++ b/docker-publish/rules.yml @@ -2,7 +2,7 @@ # Validation rules for docker-publish action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 -# Coverage: 100% (12/12 inputs) +# Coverage: 73% (8/11 inputs) # # This file defines validation rules for the docker-publish GitHub Action. # Rules are automatically applied by validate-inputs action when this @@ -11,50 +11,44 @@ schema_version: '1.0' action: docker-publish -description: Publish a Docker image to GitHub Packages and Docker Hub. +description: Simple wrapper to publish Docker images to GitHub Packages and/or Docker Hub generator_version: 1.0.0 -required_inputs: - - registry +required_inputs: [] optional_inputs: - - auto-detect-platforms - - buildx-version - - cache-mode - - dockerhub-password + - build-args + - context + - dockerfile + - dockerhub-token - dockerhub-username - - nightly + - image-name - platforms - - scan-image - - sign-image + - push + - registry + - tags - token - - verbose conventions: - auto-detect-platforms: docker_architectures - buildx-version: semantic_version - cache-mode: boolean - dockerhub-password: github_token + dockerfile: file_path + dockerhub-token: github_token dockerhub-username: username - nightly: boolean + image-name: docker_image_name platforms: docker_architectures registry: registry - scan-image: boolean - sign-image: boolean + tags: docker_tag token: github_token - verbose: boolean overrides: - cache-mode: cache_mode platforms: null registry: registry_enum statistics: - total_inputs: 12 - validated_inputs: 12 + total_inputs: 11 + validated_inputs: 8 skipped_inputs: 1 - coverage_percentage: 100 -validation_coverage: 100 + coverage_percentage: 73 +validation_coverage: 73 auto_detected: true -manual_review_required: false +manual_review_required: true quality_indicators: - has_required_inputs: true + has_required_inputs: false has_token_validation: true - has_version_validation: true - has_file_validation: false + has_version_validation: false + has_file_validation: true has_security_validation: true diff --git a/dotnet-version-detect/README.md b/dotnet-version-detect/README.md deleted file mode 100644 index 60fb21a..0000000 --- a/dotnet-version-detect/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# ivuorinen/actions/dotnet-version-detect - -## Dotnet Version Detect - -### Description - -Detects .NET SDK version from global.json or defaults to a specified version. - -### Inputs - -| name | description | required | default | -|-------------------|---------------------------------------------------------------------|----------|---------| -| `default-version` |

Default .NET SDK version to use if global.json is not found.

| `true` | `7.0` | -| `token` |

GitHub token for authentication

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

Detected or default .NET SDK version.

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/dotnet-version-detect@main - with: - default-version: - # Default .NET SDK version to use if global.json is not found. - # - # Required: true - # Default: 7.0 - - token: - # GitHub token for authentication - # - # Required: false - # Default: "" -``` diff --git a/dotnet-version-detect/action.yml b/dotnet-version-detect/action.yml deleted file mode 100644 index debddc7..0000000 --- a/dotnet-version-detect/action.yml +++ /dev/null @@ -1,67 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for reading version files ---- -name: Dotnet Version Detect -description: 'Detects .NET SDK version from global.json or defaults to a specified version.' -author: 'Ismo Vuorinen' - -branding: - icon: code - color: blue - -inputs: - default-version: - description: 'Default .NET SDK version to use if global.json is not found.' - required: true - default: '7.0' - token: - description: 'GitHub token for authentication' - required: false - default: '' - -outputs: - dotnet-version: - description: 'Detected or default .NET SDK version.' - value: ${{ steps.parse-version.outputs.detected-version }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - DEFAULT_VERSION: ${{ inputs.default-version }} - run: | - set -euo pipefail - - # Validate default-version format - if ! [[ "$DEFAULT_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then - echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 7.0, 8.0.100)" - exit 1 - fi - - # Check for reasonable version range (prevent malicious inputs) - major_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f1) - if [ "$major_version" -lt 3 ] || [ "$major_version" -gt 20 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. Major version should be between 3 and 20" - exit 1 - fi - - echo "Input validation completed successfully" - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token || github.token }} - - - name: Parse .NET Version - id: parse-version - uses: ivuorinen/actions/version-file-parser@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'dotnet' - tool-versions-key: 'dotnet' - dockerfile-image: 'dotnet' - validation-regex: '^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$' - default-version: ${{ inputs.default-version }} diff --git a/dotnet-version-detect/rules.yml b/dotnet-version-detect/rules.yml deleted file mode 100644 index 65cad49..0000000 --- a/dotnet-version-detect/rules.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -# Validation rules for dotnet-version-detect action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (2/2 inputs) -# -# This file defines validation rules for the dotnet-version-detect GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: dotnet-version-detect -description: Detects .NET SDK version from global.json or defaults to a specified version. -generator_version: 1.0.0 -required_inputs: - - default-version -optional_inputs: - - token -conventions: - default-version: semantic_version - token: github_token -overrides: - default-version: dotnet_version -statistics: - total_inputs: 2 - validated_inputs: 2 - skipped_inputs: 0 - coverage_percentage: 100 -validation_coverage: 100 -auto_detected: true -manual_review_required: false -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/eslint-check/CustomValidator.py b/eslint-check/CustomValidator.py deleted file mode 100755 index f56f304..0000000 --- a/eslint-check/CustomValidator.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for eslint-check 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.version import VersionValidator - - -class CustomValidator(BaseValidator): - """Custom validator for eslint-check action.""" - - def __init__(self, action_type: str = "eslint-check") -> None: - """Initialize eslint-check validator.""" - super().__init__(action_type) - self.file_validator = FileValidator() - self.version_validator = VersionValidator() - self.boolean_validator = BooleanValidator() - self.numeric_validator = NumericValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate eslint-check action inputs.""" - valid = True - - # Validate working-directory if provided - if inputs.get("working-directory"): - result = self.file_validator.validate_file_path( - inputs["working-directory"], "working-directory" - ) - 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 eslint-version if provided - if "eslint-version" in inputs: - value = inputs["eslint-version"] - # Check for empty version - reject it - if value == "": - self.add_error("ESLint version cannot be empty") - valid = False - # Allow "latest" as a special case - elif value == "latest": - pass # Valid - # Validate as semantic version (eslint uses strict semantic versioning) - elif value and not value.startswith("${{"): - # ESLint requires full semantic version (X.Y.Z), not partial versions - if not re.match(r"^\d+\.\d+\.\d+", value): - self.add_error( - f"ESLint version must be a complete semantic version (X.Y.Z), got: {value}" - ) - valid = False - else: - result = self.version_validator.validate_semantic_version( - value, "eslint-version" - ) - 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 config-file if provided - if inputs.get("config-file"): - result = self.file_validator.validate_file_path(inputs["config-file"], "config-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 ignore-file if provided - if inputs.get("ignore-file"): - result = self.file_validator.validate_file_path(inputs["ignore-file"], "ignore-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 ignore-file if provided - if inputs.get("ignore-file"): - result = self.file_validator.validate_file_path(inputs["ignore-file"], "ignore-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 file-extensions if provided - if inputs.get("file-extensions"): - value = inputs["file-extensions"] - # Check for valid extension format - extensions = value.split(",") if "," in value else value.split() - for ext in extensions: - ext = ext.strip() - if ext and not ext.startswith("${{"): - # Extensions should start with a dot - if not ext.startswith("."): - self.add_error(f"Extension '{ext}' should start with a dot") - valid = False - # Check for invalid characters - elif not re.match(r"^\.[a-zA-Z0-9]+$", ext): - self.add_error(f"Invalid extension format: {ext}") - valid = False - - # Validate cache boolean - if inputs.get("cache"): - result = self.boolean_validator.validate_boolean(inputs["cache"], "cache") - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - if not result: - valid = False - - # Validate max-warnings numeric - if inputs.get("max-warnings"): - value = inputs["max-warnings"] - if value and not value.startswith("${{"): - try: - num_value = int(value) - if num_value < 0: - self.add_error(f"max-warnings cannot be negative: {value}") - valid = False - except ValueError: - self.add_error(f"max-warnings must be a number: {value}") - valid = False - - # Validate fail-on-error boolean - if inputs.get("fail-on-error"): - result = self.boolean_validator.validate_boolean( - inputs["fail-on-error"], "fail-on-error" - ) - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - if not result: - valid = False - - # Validate report-format - if "report-format" in inputs: - value = inputs["report-format"] - valid_formats = [ - "stylish", - "compact", - "json", - "junit", - "html", - "table", - "tap", - "unix", - "sarif", - "checkstyle", - ] - if value == "": - self.add_error("Report format cannot be empty") - valid = False - elif value and not value.startswith("${{"): - if value not in valid_formats: - self.add_error( - f"Invalid report format: {value}. " - f"Must be one of: {', '.join(valid_formats)}" - ) - valid = False - - # Validate max-retries - if inputs.get("max-retries"): - value = inputs["max-retries"] - if value and not value.startswith("${{"): - result = self.numeric_validator.validate_numeric_range_1_10(value, "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 - - 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 { - "working-directory": { - "type": "directory", - "required": False, - "description": "Working directory", - }, - "eslint-version": { - "type": "flexible_version", - "required": False, - "description": "ESLint version", - }, - "config-file": { - "type": "file", - "required": False, - "description": "ESLint config file", - }, - "ignore-file": { - "type": "file", - "required": False, - "description": "ESLint ignore file", - }, - "file-extensions": { - "type": "string", - "required": False, - "description": "File extensions to check", - }, - "cache": { - "type": "boolean", - "required": False, - "description": "Enable caching", - }, - "max-warnings": { - "type": "numeric", - "required": False, - "description": "Maximum warnings allowed", - }, - "fail-on-error": { - "type": "boolean", - "required": False, - "description": "Fail on error", - }, - "report-format": { - "type": "string", - "required": False, - "description": "Report format", - }, - "max-retries": { - "type": "numeric", - "required": False, - "description": "Maximum retry count", - }, - } diff --git a/eslint-check/README.md b/eslint-check/README.md deleted file mode 100644 index 05305b8..0000000 --- a/eslint-check/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# ivuorinen/actions/eslint-check - -## ESLint Check - -### Description - -Run ESLint check on the repository with advanced configuration and reporting - -### Inputs - -| name | description | required | default | -|---------------------|--------------------------------------------------|----------|---------------------| -| `working-directory` |

Directory containing files to lint

| `false` | `.` | -| `eslint-version` |

ESLint version to use

| `false` | `latest` | -| `config-file` |

Path to ESLint config file

| `false` | `.eslintrc` | -| `ignore-file` |

Path to ESLint ignore file

| `false` | `.eslintignore` | -| `file-extensions` |

File extensions to lint (comma-separated)

| `false` | `.js,.jsx,.ts,.tsx` | -| `cache` |

Enable ESLint caching

| `false` | `true` | -| `max-warnings` |

Maximum number of warnings allowed

| `false` | `0` | -| `fail-on-error` |

Fail workflow if issues are found

| `false` | `true` | -| `report-format` |

Output format (stylish, json, sarif)

| `false` | `sarif` | -| `max-retries` |

Maximum number of retry attempts

| `false` | `3` | -| `token` |

GitHub token for authentication

| `false` | `""` | - -### Outputs - -| name | description | -|-----------------|----------------------------------| -| `error-count` |

Number of errors found

| -| `warning-count` |

Number of warnings found

| -| `sarif-file` |

Path to SARIF report file

| -| `files-checked` |

Number of files checked

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/eslint-check@main - with: - working-directory: - # Directory containing files to lint - # - # Required: false - # Default: . - - eslint-version: - # ESLint version to use - # - # Required: false - # Default: latest - - config-file: - # Path to ESLint config file - # - # Required: false - # Default: .eslintrc - - ignore-file: - # Path to ESLint ignore file - # - # Required: false - # Default: .eslintignore - - file-extensions: - # File extensions to lint (comma-separated) - # - # Required: false - # Default: .js,.jsx,.ts,.tsx - - cache: - # Enable ESLint caching - # - # Required: false - # Default: true - - max-warnings: - # Maximum number of warnings allowed - # - # Required: false - # Default: 0 - - fail-on-error: - # Fail workflow if issues are found - # - # Required: false - # Default: true - - report-format: - # Output format (stylish, json, sarif) - # - # Required: false - # Default: sarif - - max-retries: - # Maximum number of retry attempts - # - # Required: false - # Default: 3 - - token: - # GitHub token for authentication - # - # Required: false - # Default: "" -``` diff --git a/eslint-check/action.yml b/eslint-check/action.yml deleted file mode 100644 index 7a9426f..0000000 --- a/eslint-check/action.yml +++ /dev/null @@ -1,438 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - security-events: write # Required for uploading SARIF results -# - contents: read # Required for checking out repository ---- -name: ESLint Check -description: 'Run ESLint check on the repository with advanced configuration and reporting' -author: Ismo Vuorinen - -branding: - icon: check-circle - color: blue - -inputs: - working-directory: - description: 'Directory containing files to lint' - required: false - default: '.' - eslint-version: - description: 'ESLint version to use' - required: false - default: 'latest' - config-file: - description: 'Path to ESLint config file' - required: false - default: '.eslintrc' - ignore-file: - description: 'Path to ESLint ignore file' - required: false - default: '.eslintignore' - file-extensions: - description: 'File extensions to lint (comma-separated)' - required: false - default: '.js,.jsx,.ts,.tsx' - cache: - description: 'Enable ESLint caching' - required: false - default: 'true' - max-warnings: - description: 'Maximum number of warnings allowed' - required: false - default: '0' - fail-on-error: - description: 'Fail workflow if issues are found' - required: false - default: 'true' - report-format: - description: 'Output format (stylish, json, sarif)' - required: false - default: 'sarif' - max-retries: - description: 'Maximum number of retry attempts' - required: false - default: '3' - token: - description: 'GitHub token for authentication' - required: false - default: '' - -outputs: - error-count: - description: 'Number of errors found' - value: ${{ steps.lint.outputs.error_count }} - warning-count: - description: 'Number of warnings found' - value: ${{ steps.lint.outputs.warning_count }} - sarif-file: - description: 'Path to SARIF report file' - value: ${{ steps.lint.outputs.sarif_file }} - files-checked: - description: 'Number of files checked' - value: ${{ steps.lint.outputs.files_checked }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - ESLINT_VERSION: ${{ inputs.eslint-version }} - CONFIG_FILE: ${{ inputs.config-file }} - IGNORE_FILE: ${{ inputs.ignore-file }} - FILE_EXTENSIONS: ${{ inputs.file-extensions }} - CACHE: ${{ inputs.cache }} - FAIL_ON_ERROR: ${{ inputs.fail-on-error }} - MAX_WARNINGS: ${{ inputs.max-warnings }} - REPORT_FORMAT: ${{ inputs.report-format }} - MAX_RETRIES: ${{ inputs.max-retries }} - run: | - set -euo pipefail - - # Validate working directory exists - if [ ! -d "$WORKING_DIRECTORY" ]; then - echo "::error::Working directory not found at '$WORKING_DIRECTORY'" - exit 1 - 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 - - # 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)" - 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 - 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 - fi - - # Validate file extensions format - if ! [[ "$FILE_EXTENSIONS" =~ ^(\.[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" - - case "${value,,}" in - true|false) - ;; - *) - echo "::error::Invalid boolean value for $name: '$value'. Expected: true or false" - exit 1 - ;; - esac - } - - 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 (e.g., 0, 10)" - exit 1 - fi - - # Validate report format enumerated values - case "$REPORT_FORMAT" in - stylish|json|sarif|checkstyle|compact|html|jslint-xml|junit|tap|unix) - ;; - *) - echo "::error::Invalid report-format: '$REPORT_FORMAT'. Allowed values: stylish, json, sarif, checkstyle, compact, html, jslint-xml, junit, tap, unix" - exit 1 - ;; - esac - - # Validate max retries (positive integer with reasonable upper limit) - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$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 - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token || github.token }} - - - name: Setup Node.js - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 - - - name: Cache Node Dependencies - id: cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'eslint-check-${{ steps.node-setup.outputs.package-manager }}' - - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} - MAX_RETRIES: ${{ inputs.max-retries }} - ESLINT_VERSION: ${{ inputs.eslint-version }} - run: | - set -euo pipefail - - cd "$WORKING_DIRECTORY" - - echo "Installing ESLint dependencies using $PACKAGE_MANAGER..." - - # Function to install with retries - install_with_retries() { - local attempt=1 - - while [ $attempt -le "$MAX_RETRIES" ]; do - echo "Installation attempt $attempt of $MAX_RETRIES" - - case "$PACKAGE_MANAGER" in - "pnpm") - if pnpm add -D \ - "eslint@$ESLINT_VERSION" \ - @typescript-eslint/parser \ - @typescript-eslint/eslint-plugin \ - @microsoft/eslint-formatter-sarif \ - eslint-plugin-import \ - eslint-config-prettier \ - typescript; then - return 0 - fi - ;; - "yarn") - if yarn add -D \ - "eslint@$ESLINT_VERSION" \ - @typescript-eslint/parser \ - @typescript-eslint/eslint-plugin \ - @microsoft/eslint-formatter-sarif \ - eslint-plugin-import \ - eslint-config-prettier \ - typescript; then - return 0 - fi - ;; - "bun") - if bun add -D \ - "eslint@$ESLINT_VERSION" \ - @typescript-eslint/parser \ - @typescript-eslint/eslint-plugin \ - @microsoft/eslint-formatter-sarif \ - eslint-plugin-import \ - eslint-config-prettier \ - typescript; then - return 0 - fi - ;; - "npm"|*) - if npm install \ - "eslint@$ESLINT_VERSION" \ - @typescript-eslint/parser \ - @typescript-eslint/eslint-plugin \ - @microsoft/eslint-formatter-sarif \ - eslint-plugin-import \ - eslint-config-prettier \ - typescript; then - return 0 - fi - ;; - esac - - attempt=$((attempt + 1)) - if [ $attempt -le "$MAX_RETRIES" ]; then - echo "Installation failed, waiting 10 seconds before retry..." - sleep 10 - fi - done - - echo "::error::Failed to install dependencies after $MAX_RETRIES attempts" - return 1 - } - - install_with_retries - - - name: Prepare ESLint Configuration - id: config - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - CONFIG_FILE: ${{ inputs.config-file }} - IGNORE_FILE: ${{ inputs.ignore-file }} - run: | - set -euo pipefail - - cd "$WORKING_DIRECTORY" - - # Create default config if none exists - if [ ! -f "$CONFIG_FILE" ]; then - echo "Creating default ESLint configuration..." - cat > "$CONFIG_FILE" < "$IGNORE_FILE" <> $GITHUB_OUTPUT - echo "warning_count=${warning_count}" >> $GITHUB_OUTPUT - - if [ "$FAIL_ON_ERROR" = "true" ] && [ $error_code -ne 0 ]; then - echo "::error::ESLint found ${error_count} errors and ${warning_count} warnings" - exit $error_code - fi - } - - # Count checked files - files_checked=$(find . -type f \( $(printf -- "-name *%s -o " "${EXTENSIONS[@]}") -false \) | wc -l) - echo "files_checked=${files_checked}" >> $GITHUB_OUTPUT - echo "sarif_file=reports/eslint.sarif" >> $GITHUB_OUTPUT - - - name: Upload ESLint Results - if: always() && inputs.report-format == 'sarif' - uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 - with: - sarif_file: ${{ inputs.working-directory }}/reports/eslint.sarif - category: eslint - - - name: Cache Cleanup - if: always() - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - run: |- - set -euo pipefail - - cd "$WORKING_DIRECTORY" - - # Clean up ESLint cache if it exists - if [ -f ".eslintcache" ]; then - rm .eslintcache - fi - - # Remove temporary files - rm -rf reports/ diff --git a/eslint-fix/README.md b/eslint-fix/README.md deleted file mode 100644 index 4f42047..0000000 --- a/eslint-fix/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# ivuorinen/actions/eslint-fix - -## ESLint Fix - -### Description - -Fixes ESLint violations in a project. - -### Inputs - -| name | description | required | default | -|---------------|--------------------------------------------------------------------|----------|-----------------------------| -| `token` |

GitHub token for authentication

| `false` | `${{ github.token }}` | -| `username` |

GitHub username for commits

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

GitHub email for commits

| `false` | `github-actions@github.com` | -| `max-retries` |

Maximum number of retry attempts for npm install operations

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

Number of files changed by ESLint

| -| `lint_status` |

Linting status (success/failure)

| -| `errors_fixed` |

Number of errors fixed

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/eslint-fix@main - with: - token: - # GitHub token for authentication - # - # Required: false - # Default: ${{ github.token }} - - username: - # GitHub username for commits - # - # Required: false - # Default: github-actions - - email: - # GitHub email for commits - # - # Required: false - # Default: github-actions@github.com - - max-retries: - # Maximum number of retry attempts for npm install operations - # - # Required: false - # Default: 3 -``` diff --git a/eslint-fix/action.yml b/eslint-fix/action.yml deleted file mode 100644 index 75ef011..0000000 --- a/eslint-fix/action.yml +++ /dev/null @@ -1,184 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: write # Required for pushing fixes back to repository ---- -name: ESLint Fix -description: Fixes ESLint violations in a project. -author: 'Ismo Vuorinen' - -branding: - icon: 'code' - color: 'blue' - -inputs: - token: - description: 'GitHub token for authentication' - required: false - default: ${{ github.token }} - username: - description: 'GitHub username for commits' - required: false - default: 'github-actions' - email: - description: 'GitHub email for commits' - required: false - default: 'github-actions@github.com' - max-retries: - description: 'Maximum number of retry attempts for npm install operations' - required: false - default: '3' - -outputs: - files_changed: - description: 'Number of files changed by ESLint' - value: ${{ steps.lint.outputs.files_changed }} - lint_status: - description: 'Linting status (success/failure)' - value: ${{ steps.lint.outputs.status }} - errors_fixed: - description: 'Number of errors fixed' - value: ${{ steps.lint.outputs.errors_fixed }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - action-type: 'eslint-fix' - token: ${{ inputs.token }} - email: ${{ inputs.email }} - username: ${{ inputs.username }} - max-retries: ${{ inputs.max-retries }} - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token }} - - - name: Set Git Config - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} - - - name: Node Setup - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 - - - name: Cache Node Dependencies - id: cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'eslint-fix-${{ steps.node-setup.outputs.package-manager }}' - - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - shell: bash - env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} - MAX_RETRIES: ${{ inputs.max-retries }} - run: | - set -euo pipefail - - echo "Installing dependencies using $PACKAGE_MANAGER..." - - for attempt in $(seq 1 "$MAX_RETRIES"); do - echo "Attempt $attempt of $MAX_RETRIES" - - case "$PACKAGE_MANAGER" in - "pnpm") - if pnpm install --frozen-lockfile; then - echo "โœ… Dependencies installed successfully with pnpm" - exit 0 - fi - ;; - "yarn") - if [ -f ".yarnrc.yml" ]; then - if yarn install --immutable; then - echo "โœ… Dependencies installed successfully with Yarn Berry" - exit 0 - fi - else - if yarn install --frozen-lockfile; then - echo "โœ… Dependencies installed successfully with Yarn Classic" - exit 0 - fi - fi - ;; - "bun") - if bun install --frozen-lockfile; then - echo "โœ… Dependencies installed successfully with Bun" - exit 0 - fi - ;; - "npm"|*) - if npm ci; then - echo "โœ… Dependencies installed successfully with npm" - exit 0 - fi - ;; - esac - - if [ $attempt -lt "$MAX_RETRIES" ]; then - echo "โŒ Installation failed, retrying in 5 seconds..." - sleep 5 - fi - done - - echo "::error::Failed to install dependencies after $MAX_RETRIES attempts" - exit 1 - - - name: Run ESLint Fix - id: lint - shell: bash - env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} - run: | - set -euo pipefail - - echo "Running ESLint fix with $PACKAGE_MANAGER..." - - # Count files before fix - files_before=$(git status --porcelain | wc -l || echo "0") - - # Run ESLint fix based on package manager - case "$PACKAGE_MANAGER" in - "pnpm") - pnpm exec eslint . --fix || true - ;; - "yarn") - yarn eslint . --fix || true - ;; - "bun") - bunx eslint . --fix || true - ;; - "npm"|*) - npx eslint . --fix || true - ;; - esac - - # Count files after fix - files_after=$(git status --porcelain | wc -l || echo "0") - files_changed=$((files_after - files_before)) - - # Get number of staged changes - errors_fixed=$(git diff --cached --numstat | wc -l || echo "0") - - echo "files_changed=$files_changed" >> $GITHUB_OUTPUT - echo "errors_fixed=$errors_fixed" >> $GITHUB_OUTPUT - echo "status=success" >> $GITHUB_OUTPUT - - echo "โœ… ESLint fix completed. Files changed: $files_changed, Errors fixed: $errors_fixed" - - - name: Push Fixes - if: always() - uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 - with: - commit_message: 'style: autofix ESLint violations' - add_options: '-u' diff --git a/eslint-lint/README.md b/eslint-lint/README.md new file mode 100644 index 0000000..994d310 --- /dev/null +++ b/eslint-lint/README.md @@ -0,0 +1,132 @@ +# ivuorinen/actions/eslint-lint + +## ESLint Lint + +### Description + +Run ESLint in check or fix mode with advanced configuration and reporting + +### Inputs + +| name | description | required | default | +|---------------------|-------------------------------------------------------------|----------|-----------------------------| +| `mode` |

Mode to run (check or fix)

| `false` | `check` | +| `working-directory` |

Directory containing files to lint

| `false` | `.` | +| `eslint-version` |

ESLint version to use

| `false` | `latest` | +| `config-file` |

Path to ESLint config file

| `false` | `.eslintrc` | +| `ignore-file` |

Path to ESLint ignore file

| `false` | `.eslintignore` | +| `file-extensions` |

File extensions to lint (comma-separated)

| `false` | `.js,.jsx,.ts,.tsx` | +| `cache` |

Enable ESLint caching

| `false` | `true` | +| `max-warnings` |

Maximum number of warnings allowed (check mode only)

| `false` | `0` | +| `fail-on-error` |

Fail workflow if issues are found (check mode only)

| `false` | `true` | +| `report-format` |

Output format for check mode (stylish, json, sarif)

| `false` | `sarif` | +| `max-retries` |

Maximum number of retry attempts

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

GitHub token for authentication

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

GitHub username for commits (fix mode only)

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

GitHub email for commits (fix mode only)

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

Overall status (success/failure)

| +| `error-count` |

Number of errors found (check mode only)

| +| `warning-count` |

Number of warnings found (check mode only)

| +| `sarif-file` |

Path to SARIF report file (check mode only)

| +| `files-checked` |

Number of files checked (check mode only)

| +| `files-changed` |

Number of files changed (fix mode only)

| +| `errors-fixed` |

Number of errors fixed (fix mode only)

| + +### Runs + +This action is a `composite` action. + +### Usage + +```yaml +- uses: ivuorinen/actions/eslint-lint@main + with: + mode: + # Mode to run (check or fix) + # + # Required: false + # Default: check + + working-directory: + # Directory containing files to lint + # + # Required: false + # Default: . + + eslint-version: + # ESLint version to use + # + # Required: false + # Default: latest + + config-file: + # Path to ESLint config file + # + # Required: false + # Default: .eslintrc + + ignore-file: + # Path to ESLint ignore file + # + # Required: false + # Default: .eslintignore + + file-extensions: + # File extensions to lint (comma-separated) + # + # Required: false + # Default: .js,.jsx,.ts,.tsx + + cache: + # Enable ESLint caching + # + # Required: false + # Default: true + + max-warnings: + # Maximum number of warnings allowed (check mode only) + # + # Required: false + # Default: 0 + + fail-on-error: + # Fail workflow if issues are found (check mode only) + # + # Required: false + # Default: true + + report-format: + # Output format for check mode (stylish, json, sarif) + # + # Required: false + # Default: sarif + + max-retries: + # Maximum number of retry attempts + # + # Required: false + # Default: 3 + + token: + # GitHub token for authentication + # + # Required: false + # Default: "" + + username: + # GitHub username for commits (fix mode only) + # + # Required: false + # Default: github-actions + + email: + # GitHub email for commits (fix mode only) + # + # Required: false + # Default: github-actions@github.com +``` diff --git a/eslint-lint/action.yml b/eslint-lint/action.yml new file mode 100644 index 0000000..33c9ccd --- /dev/null +++ b/eslint-lint/action.yml @@ -0,0 +1,424 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-action.json +# permissions: +# - contents: write # Required for fix mode to push changes +# - security-events: write # Required for check mode to upload SARIF +--- +name: ESLint Lint +description: 'Run ESLint in check or fix mode with advanced configuration and reporting' +author: Ismo Vuorinen + +branding: + icon: check-circle + color: blue + +inputs: + mode: + description: 'Mode to run (check or fix)' + required: false + default: 'check' + working-directory: + description: 'Directory containing files to lint' + required: false + default: '.' + eslint-version: + description: 'ESLint version to use' + required: false + default: 'latest' + config-file: + description: 'Path to ESLint config file' + required: false + default: '.eslintrc' + ignore-file: + description: 'Path to ESLint ignore file' + required: false + default: '.eslintignore' + file-extensions: + description: 'File extensions to lint (comma-separated)' + required: false + default: '.js,.jsx,.ts,.tsx' + cache: + description: 'Enable ESLint caching' + required: false + default: 'true' + max-warnings: + description: 'Maximum number of warnings allowed (check mode only)' + required: false + default: '0' + fail-on-error: + description: 'Fail workflow if issues are found (check mode only)' + required: false + default: 'true' + report-format: + description: 'Output format for check mode (stylish, json, sarif)' + required: false + default: 'sarif' + max-retries: + description: 'Maximum number of retry attempts' + required: false + default: '3' + token: + description: 'GitHub token for authentication' + required: false + default: '' + username: + description: 'GitHub username for commits (fix mode only)' + required: false + default: 'github-actions' + email: + description: 'GitHub email for commits (fix mode only)' + required: false + default: 'github-actions@github.com' + +outputs: + status: + description: 'Overall status (success/failure)' + value: ${{ steps.check.outputs.status || steps.fix.outputs.status }} + error-count: + description: 'Number of errors found (check mode only)' + value: ${{ steps.check.outputs.error_count }} + warning-count: + description: 'Number of warnings found (check mode only)' + value: ${{ steps.check.outputs.warning_count }} + sarif-file: + description: 'Path to SARIF report file (check mode only)' + value: ${{ steps.check.outputs.sarif_file }} + files-checked: + description: 'Number of files checked (check mode only)' + value: ${{ steps.check.outputs.files_checked }} + files-changed: + description: 'Number of files changed (fix mode only)' + value: ${{ steps.fix.outputs.files_changed }} + errors-fixed: + description: 'Number of errors fixed (fix mode only)' + value: ${{ steps.fix.outputs.errors_fixed }} + +runs: + using: composite + steps: + - name: Validate Inputs + id: validate + shell: bash + env: + MODE: ${{ inputs.mode }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + ESLINT_VERSION: ${{ inputs.eslint-version }} + CONFIG_FILE: ${{ inputs.config-file }} + IGNORE_FILE: ${{ inputs.ignore-file }} + FILE_EXTENSIONS: ${{ inputs.file-extensions }} + CACHE: ${{ inputs.cache }} + FAIL_ON_ERROR: ${{ inputs.fail-on-error }} + MAX_WARNINGS: ${{ inputs.max-warnings }} + REPORT_FORMAT: ${{ inputs.report-format }} + MAX_RETRIES: ${{ inputs.max-retries }} + EMAIL: ${{ inputs.email }} + USERNAME: ${{ inputs.username }} + run: | + set -euo pipefail + + # Validate mode + case "$MODE" in + "check"|"fix") + echo "Mode: $MODE" + ;; + *) + echo "::error::Invalid mode: '$MODE'. Must be 'check' or 'fix'" + exit 1 + ;; + esac + + # Validate working directory exists + if [ ! -d "$WORKING_DIRECTORY" ]; then + echo "::error::Working directory not found at '$WORKING_DIRECTORY'" + exit 1 + 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 + + # 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)" + 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 + 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 + fi + + # Validate file extensions format + if ! [[ "$FILE_EXTENSIONS" =~ ^(\.[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" + + case "${value,,}" in + true|false) + ;; + *) + echo "::error::Invalid boolean value for $name: '$value'. Expected: true or false" + exit 1 + ;; + esac + } + + 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 report format + case "$REPORT_FORMAT" in + stylish|json|sarif) + ;; + *) + echo "::error::Invalid report-format: '$REPORT_FORMAT'. Must be one of: stylish, json, sarif" + exit 1 + ;; + esac + + # Validate max retries + if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$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 + + # Validate username format (GitHub canonical rules) + username="$USERNAME" + + if [ ${#username} -gt 39 ]; then + echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters" + 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 + + if [[ "$username" == -* ]] || [[ "$username" == *- ]]; then + echo "::error::Invalid username '$username'. Cannot start or end with hyphen" + exit 1 + fi + + if [[ "$username" == *--* ]]; then + echo "::error::Invalid username '$username'. Consecutive hyphens 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: Node Setup + id: node-setup + uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + + - name: Cache Node Dependencies + id: cache + uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + 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 }}' + + - name: Install Dependencies + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + env: + PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + run: | + set -euo pipefail + + echo "Installing dependencies using $PACKAGE_MANAGER..." + + case "$PACKAGE_MANAGER" in + "pnpm") + pnpm install --frozen-lockfile + ;; + "yarn") + if [ -f ".yarnrc.yml" ]; then + yarn install --immutable + else + yarn install --frozen-lockfile + fi + ;; + "bun") + bun install --frozen-lockfile + ;; + "npm"|*) + npm ci + ;; + esac + + echo "โœ… Dependencies installed successfully" + + - name: Run ESLint Check + if: inputs.mode == 'check' + id: check + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + ESLINT_VERSION: ${{ inputs.eslint-version }} + CONFIG_FILE: ${{ inputs.config-file }} + CACHE: ${{ inputs.cache }} + MAX_WARNINGS: ${{ inputs.max-warnings }} + FAIL_ON_ERROR: ${{ inputs.fail-on-error }} + REPORT_FORMAT: ${{ inputs.report-format }} + FILE_EXTENSIONS: ${{ inputs.file-extensions }} + run: | + set -euo pipefail + + echo "Running ESLint check mode..." + + # Build ESLint command + eslint_cmd="npx eslint ." + + # Add config file if specified + if [ "$CONFIG_FILE" != ".eslintrc" ] && [ -f "$CONFIG_FILE" ]; then + eslint_cmd="$eslint_cmd --config $CONFIG_FILE" + fi + + # Add cache option + if [ "$CACHE" = "true" ]; then + eslint_cmd="$eslint_cmd --cache" + fi + + # Add max warnings + eslint_cmd="$eslint_cmd --max-warnings $MAX_WARNINGS" + + # Add format + if [ "$REPORT_FORMAT" = "sarif" ]; then + eslint_cmd="$eslint_cmd --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif" + else + eslint_cmd="$eslint_cmd --format $REPORT_FORMAT" + fi + + # Run ESLint and capture exit code + eslint_exit_code=0 + eval "$eslint_cmd" || eslint_exit_code=$? + + # Parse results + if [ "$REPORT_FORMAT" = "sarif" ] && [ -f eslint-results.sarif ]; then + error_count=$(jq '[.runs[]?.results[]? | select(.level == "error")] | length' eslint-results.sarif 2>/dev/null || echo "0") + warning_count=$(jq '[.runs[]?.results[]? | select(.level == "warning")] | length' eslint-results.sarif 2>/dev/null || echo "0") + files_checked=$(jq '[.runs[]?.results[]?.locations[]?.physicalLocation?.artifactLocation?.uri] | unique | length' eslint-results.sarif 2>/dev/null || echo "0") + sarif_file="eslint-results.sarif" + else + error_count="0" + warning_count="0" + files_checked="0" + sarif_file="" + fi + + # Set outputs + if [ $eslint_exit_code -eq 0 ]; then + echo "status=success" >> "$GITHUB_OUTPUT" + else + echo "status=failure" >> "$GITHUB_OUTPUT" + fi + + echo "error_count=$error_count" >> "$GITHUB_OUTPUT" + echo "warning_count=$warning_count" >> "$GITHUB_OUTPUT" + echo "files_checked=$files_checked" >> "$GITHUB_OUTPUT" + echo "sarif_file=$sarif_file" >> "$GITHUB_OUTPUT" + + echo "โœ… ESLint check completed: $error_count errors, $warning_count warnings" + + # Exit with eslint's exit code if fail-on-error is true + if [ "$FAIL_ON_ERROR" = "true" ]; then + exit $eslint_exit_code + fi + + - name: Upload SARIF Report + if: inputs.mode == 'check' && inputs.report-format == 'sarif' && always() + uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + with: + sarif_file: ${{ inputs.working-directory }}/eslint-results.sarif + + - name: Run ESLint Fix + if: inputs.mode == 'fix' + id: fix + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + run: | + set -euo pipefail + + echo "Running ESLint fix mode..." + + # Count files before fix + files_before=$(git status --porcelain | wc -l | tr -d ' ') + + # Run ESLint fix based on package manager + case "$PACKAGE_MANAGER" in + "pnpm") + pnpm exec eslint . --fix || true + ;; + "yarn") + yarn eslint . --fix || true + ;; + "bun") + bunx eslint . --fix || true + ;; + "npm"|*) + npx eslint . --fix || true + ;; + esac + + # Count files after fix + files_after=$(git status --porcelain | wc -l | tr -d ' ') + files_changed=$((files_after - files_before)) + + # Get number of errors fixed (approximate from diff) + errors_fixed=$(git diff --numstat | wc -l | tr -d ' ') + + printf '%s\n' "files_changed=$files_changed" >> "$GITHUB_OUTPUT" + printf '%s\n' "errors_fixed=$errors_fixed" >> "$GITHUB_OUTPUT" + printf '%s\n' "status=success" >> "$GITHUB_OUTPUT" + + echo "โœ… ESLint fix completed. Files changed: $files_changed, Errors fixed: $errors_fixed" + + - name: Commit and Push Fixes + if: inputs.mode == 'fix' && success() + uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 + with: + commit_message: 'style: autofix ESLint violations' + commit_user_name: ${{ inputs.username }} + commit_user_email: ${{ inputs.email }} + add_options: '-u' diff --git a/eslint-check/rules.yml b/eslint-lint/rules.yml similarity index 74% rename from eslint-check/rules.yml rename to eslint-lint/rules.yml index 48b08ba..ce75119 100644 --- a/eslint-check/rules.yml +++ b/eslint-lint/rules.yml @@ -1,47 +1,53 @@ --- -# Validation rules for eslint-check action +# Validation rules for eslint-lint action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 -# Coverage: 100% (11/11 inputs) +# Coverage: 100% (14/14 inputs) # -# This file defines validation rules for the eslint-check GitHub Action. +# This file defines validation rules for the eslint-lint GitHub Action. # Rules are automatically applied by validate-inputs action when this # action is used. # schema_version: '1.0' -action: eslint-check -description: Run ESLint check on the repository with advanced configuration and reporting +action: eslint-lint +description: Run ESLint in check or fix mode with advanced configuration and reporting generator_version: 1.0.0 required_inputs: [] optional_inputs: - cache - config-file + - email - eslint-version - fail-on-error - file-extensions - ignore-file - max-retries - max-warnings + - mode - report-format - token + - username - working-directory conventions: cache: boolean config-file: file_path + email: email eslint-version: strict_semantic_version fail-on-error: boolean file-extensions: file_extensions ignore-file: file_path max-retries: numeric_range_1_10 max-warnings: numeric_range_0_10000 + mode: mode_enum report-format: report_format token: github_token + username: username working-directory: file_path overrides: {} statistics: - total_inputs: 11 - validated_inputs: 11 + total_inputs: 14 + validated_inputs: 14 skipped_inputs: 0 coverage_percentage: 100 validation_coverage: 100 diff --git a/generate_listing.cjs b/generate_listing.cjs index 5a639f1..3bbd2b3 100755 --- a/generate_listing.cjs +++ b/generate_listing.cjs @@ -9,30 +9,21 @@ const { markdownTable } = require('markdown-table'); const CATEGORIES = { // Setup & Environment 'node-setup': 'Setup', - 'set-git-config': 'Setup', - 'php-version-detect': 'Setup', - 'python-version-detect': 'Setup', - 'python-version-detect-v2': 'Setup', - 'go-version-detect': 'Setup', - 'dotnet-version-detect': 'Setup', + 'language-version-detect': 'Setup', // Utilities 'action-versioning': 'Utilities', 'version-file-parser': 'Utilities', - 'version-validator': 'Utilities', // Linting & Formatting 'ansible-lint-fix': 'Linting', - 'biome-check': 'Linting', - 'biome-fix': 'Linting', + 'biome-lint': 'Linting', 'csharp-lint-check': 'Linting', - 'eslint-check': 'Linting', - 'eslint-fix': 'Linting', + 'eslint-lint': 'Linting', 'go-lint': 'Linting', 'pr-lint': 'Linting', 'pre-commit': 'Linting', - 'prettier-check': 'Linting', - 'prettier-fix': 'Linting', + 'prettier-lint': 'Linting', 'python-lint-fix': 'Linting', 'terraform-lint-fix': 'Linting', @@ -49,19 +40,14 @@ const CATEGORIES = { // Publishing 'npm-publish': 'Publishing', 'docker-publish': 'Publishing', - 'docker-publish-gh': 'Publishing', - 'docker-publish-hub': 'Publishing', 'csharp-publish': 'Publishing', // Repository Management - 'github-release': 'Repository', 'release-monthly': 'Repository', 'sync-labels': 'Repository', stale: 'Repository', 'compress-images': 'Repository', 'common-cache': 'Repository', - 'common-file-check': 'Repository', - 'common-retry': 'Repository', 'codeql-analysis': 'Repository', // Validation @@ -71,32 +57,23 @@ 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-version-detect': ['PHP'], 'python-lint-fix': ['Python'], - 'python-version-detect': ['Python'], - 'python-version-detect-v2': ['Python'], 'go-lint': ['Go'], 'go-build': ['Go'], - 'go-version-detect': ['Go'], 'csharp-lint-check': ['C#', '.NET'], 'csharp-build': ['C#', '.NET'], 'csharp-publish': ['C#', '.NET'], - 'dotnet-version-detect': ['C#', '.NET'], 'docker-build': ['Docker'], 'docker-publish': ['Docker'], - 'docker-publish-gh': ['Docker'], - 'docker-publish-hub': ['Docker'], 'terraform-lint-fix': ['Terraform', 'HCL'], 'ansible-lint-fix': ['Ansible', 'YAML'], - 'eslint-check': ['JavaScript', 'TypeScript'], - 'eslint-fix': ['JavaScript', 'TypeScript'], - 'prettier-check': ['JavaScript', 'TypeScript', 'Markdown', 'YAML', 'JSON'], - 'prettier-fix': ['JavaScript', 'TypeScript', 'Markdown', 'YAML', 'JSON'], - 'biome-check': ['JavaScript', 'TypeScript', 'JSON'], - 'biome-fix': ['JavaScript', 'TypeScript', 'JSON'], + 'eslint-lint': ['JavaScript', 'TypeScript'], + 'prettier-lint': ['JavaScript', 'TypeScript', 'Markdown', 'YAML', 'JSON'], + 'biome-lint': ['JavaScript', 'TypeScript', 'JSON'], 'npm-publish': ['Node.js', 'npm'], 'codeql-analysis': ['JavaScript', 'TypeScript', 'Python', 'Java', 'C#', 'C++', 'Go', 'Ruby'], 'validate-inputs': ['YAML', 'GitHub Actions'], @@ -104,7 +81,11 @@ const LANGUAGE_SUPPORT = { 'pr-lint': ['Conventional Commits'], 'sync-labels': ['YAML', 'GitHub'], 'version-file-parser': ['Multiple Languages'], - 'version-validator': ['Semantic Versioning', 'CalVer'], + 'action-versioning': ['GitHub Actions'], + 'release-monthly': ['GitHub Actions'], + stale: ['GitHub Actions'], + 'compress-images': ['Images', 'PNG', 'JPEG'], + 'common-cache': ['Caching'], }; // Icon mapping for GitHub branding diff --git a/github-release/README.md b/github-release/README.md deleted file mode 100644 index 20cec7f..0000000 --- a/github-release/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# ivuorinen/actions/github-release - -## GitHub Release - -### Description - -Creates a GitHub release with a version and changelog. - -### Inputs - -| name | description | required | default | -|-------------|------------------------------------------------------|----------|---------| -| `version` |

The version for the release.

| `true` | `""` | -| `changelog` |

The changelog or description for the release.

| `false` | `""` | - -### Outputs - -| name | description | -|---------------|---------------------------------------------------------| -| `release_url` |

URL of the created GitHub release

| -| `release_id` |

ID of the created GitHub release

| -| `upload_url` |

Upload URL for the created GitHub release assets

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/github-release@main - with: - version: - # The version for the release. - # - # Required: true - # Default: "" - - changelog: - # The changelog or description for the release. - # - # Required: false - # Default: "" -``` diff --git a/github-release/action.yml b/github-release/action.yml deleted file mode 100644 index af172ff..0000000 --- a/github-release/action.yml +++ /dev/null @@ -1,117 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: write # Required for creating releases ---- -name: GitHub Release -description: 'Creates a GitHub release with a version and changelog.' -author: 'Ismo Vuorinen' - -branding: - icon: 'tag' - color: 'blue' - -inputs: - version: - description: 'The version for the release.' - required: true - changelog: - description: 'The changelog or description for the release.' - required: false - default: '' - -outputs: - release_url: - description: 'URL of the created GitHub release' - value: ${{ steps.create-release.outputs.release_url || steps.create-release-custom.outputs.release_url }} - release_id: - description: 'ID of the created GitHub release' - value: ${{ steps.create-release.outputs.release_id || steps.create-release-custom.outputs.release_id }} - upload_url: - description: 'Upload URL for the created GitHub release assets' - value: ${{ steps.create-release.outputs.upload_url || steps.create-release-custom.outputs.upload_url }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - VERSION: ${{ inputs.version }} - CHANGELOG: ${{ inputs.changelog }} - run: | - set -euo pipefail - - # Validate version format (semantic versioning) - if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::Invalid version format: '$VERSION'. Expected semantic version (e.g., '1.2.3', 'v1.2.3-alpha', '1.2.3+build')" - exit 1 - fi - - # Validate changelog content (if provided) - if [[ -n "$CHANGELOG" ]] && [[ ${#CHANGELOG} -gt 10000 ]]; then - echo "::warning::Changelog is very long (${#CHANGELOG} characters). Consider using shorter release notes." - fi - - # Check if required tools are available - if ! command -v gh >/dev/null 2>&1; then - echo "::error::GitHub CLI (gh) is not available. Please ensure it's installed in the environment." - exit 1 - fi - if ! command -v jq >/dev/null 2>&1; then - echo "::error::jq is not available. Please ensure it's installed in the environment." - exit 1 - fi - - # Check GitHub authentication (requires GH_TOKEN or GITHUB_TOKEN with contents: write) - if ! gh auth status >/dev/null 2>&1; then - echo "::error::GitHub CLI (gh) is not authenticated. Ensure the workflow grants 'contents: write' and exports GITHUB_TOKEN (gh picks up GH_TOKEN/GITHUB_TOKEN)." - exit 1 - fi - - - name: Create GitHub Release with Autogenerated Changelog - id: create-release - if: ${{ inputs.changelog == '' }} - shell: bash - env: - VERSION: ${{ inputs.version }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: | - set -euo pipefail - - gh release create "$VERSION" \ - --repo="${GITHUB_REPOSITORY}" \ - --title="$VERSION" \ - --generate-notes - - # Get release info and set outputs - RELEASE_INFO=$(gh release view "$VERSION" --repo="${GITHUB_REPOSITORY}" --json url,id,uploadUrl) - echo "release_url=$(echo "$RELEASE_INFO" | jq -r '.url')" >> $GITHUB_OUTPUT - echo "release_id=$(echo "$RELEASE_INFO" | jq -r '.id')" >> $GITHUB_OUTPUT - echo "upload_url=$(echo "$RELEASE_INFO" | jq -r '.uploadUrl')" >> $GITHUB_OUTPUT - - - name: Create GitHub Release with Custom Changelog - id: create-release-custom - if: ${{ inputs.changelog != '' }} - shell: bash - env: - VERSION: ${{ inputs.version }} - CHANGELOG: ${{ inputs.changelog }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: |- - set -euo pipefail - - NOTES_FILE="$(mktemp)" - # Preserve exact content without allowing shell evaluation - printf '%s' "$CHANGELOG" > "$NOTES_FILE" - gh release create "$VERSION" \ - --repo="${GITHUB_REPOSITORY}" \ - --title="$VERSION" \ - --notes-file "$NOTES_FILE" - rm -f "$NOTES_FILE" - - # Get release info and set outputs - RELEASE_INFO=$(gh release view "$VERSION" --repo="${GITHUB_REPOSITORY}" --json url,id,uploadUrl) - echo "release_url=$(echo "$RELEASE_INFO" | jq -r '.url')" >> $GITHUB_OUTPUT - echo "release_id=$(echo "$RELEASE_INFO" | jq -r '.id')" >> $GITHUB_OUTPUT - echo "upload_url=$(echo "$RELEASE_INFO" | jq -r '.uploadUrl')" >> $GITHUB_OUTPUT diff --git a/github-release/rules.yml b/github-release/rules.yml deleted file mode 100644 index e55cd6d..0000000 --- a/github-release/rules.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -# Validation rules for github-release action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (2/2 inputs) -# -# This file defines validation rules for the github-release GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: github-release -description: Creates a GitHub release with a version and changelog. -generator_version: 1.0.0 -required_inputs: - - version -optional_inputs: - - changelog -conventions: - changelog: security_patterns - version: flexible_version -overrides: {} -statistics: - total_inputs: 2 - validated_inputs: 2 - skipped_inputs: 0 - coverage_percentage: 100 -validation_coverage: 100 -auto_detected: true -manual_review_required: false -quality_indicators: - has_required_inputs: true - has_token_validation: false - has_version_validation: true - has_file_validation: false - has_security_validation: true diff --git a/go-build/action.yml b/go-build/action.yml index 1f88fc4..c243a74 100644 --- a/go-build/action.yml +++ b/go-build/action.yml @@ -36,7 +36,7 @@ outputs: value: ${{ steps.test.outputs.status }} go_version: description: 'Version of Go used' - value: ${{ steps.detect-go-version.outputs.go-version }} + value: ${{ steps.detect-go-version.outputs.detected-version }} binary_path: description: 'Path to built binaries' value: ${{ inputs.destination }} @@ -54,14 +54,15 @@ runs: - name: Detect Go Version id: detect-go-version - uses: ivuorinen/actions/go-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 with: + language: 'go' default-version: "${{ inputs.go-version || '1.21' }}" - name: Setup Go uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: - go-version: ${{ steps.detect-go-version.outputs.go-version }} + go-version: ${{ steps.detect-go-version.outputs.detected-version }} cache: true - name: Cache Go Dependencies @@ -75,14 +76,14 @@ runs: - name: Download Dependencies if: steps.cache-go.outputs.cache-hit != 'true' - uses: ivuorinen/actions/common-retry@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 with: + timeout_minutes: 10 + max_attempts: ${{ inputs.max-retries }} command: | echo "Downloading Go dependencies..." go mod download go mod verify - max-retries: ${{ inputs.max-retries }} - description: 'Downloading Go modules' - name: Build Go Project id: build diff --git a/go-version-detect/CustomValidator.py b/go-version-detect/CustomValidator.py deleted file mode 100755 index 69a2d46..0000000 --- a/go-version-detect/CustomValidator.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for go-version-detect 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 go-version-detect action.""" - - def __init__(self, action_type: str = "go-version-detect") -> None: - """Initialize the validator.""" - super().__init__(action_type) - self.version_validator = VersionValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate go-version-detect specific inputs using existing validators.""" - valid = True - - # Validate default-version if provided - if "default-version" in inputs: - value = inputs["default-version"] - - # Empty string should fail validation for this action - if value == "": - self.add_error("Go version cannot be empty") - valid = False - elif value: - # Use the existing Go version validator - result = self.version_validator.validate_go_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 - - return valid - - def get_required_inputs(self) -> list[str]: - """Return list of required inputs.""" - return [] - - def get_validation_rules(self) -> dict: - """Return validation rules for this action.""" - return { - "default-version": { - "type": "go_version", - "required": False, - "description": "Default Go version to use", - } - } diff --git a/go-version-detect/README.md b/go-version-detect/README.md deleted file mode 100644 index 1ea0720..0000000 --- a/go-version-detect/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# ivuorinen/actions/go-version-detect - -## Go Version Detect - -### Description - -Detects the Go version from the project's go.mod file or defaults to a specified version. - -### Inputs - -| name | description | required | default | -|-------------------|----------------------------------------------------------|----------|---------| -| `default-version` |

Default Go version to use if go.mod is not found.

| `false` | `1.25` | -| `token` |

GitHub token for authentication

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

Detected or default Go version.

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/go-version-detect@main - with: - default-version: - # Default Go version to use if go.mod is not found. - # - # Required: false - # Default: 1.25 - - token: - # GitHub token for authentication - # - # Required: false - # Default: "" -``` diff --git a/go-version-detect/action.yml b/go-version-detect/action.yml deleted file mode 100644 index ebe6555..0000000 --- a/go-version-detect/action.yml +++ /dev/null @@ -1,75 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for reading version files ---- -name: Go Version Detect -description: "Detects the Go version from the project's go.mod file or defaults to a specified version." -author: 'Ismo Vuorinen' - -branding: - icon: code - color: blue - -inputs: - default-version: - description: 'Default Go version to use if go.mod is not found.' - required: false - default: '1.25' - token: - description: 'GitHub token for authentication' - required: false - default: '' - -outputs: - go-version: - description: 'Detected or default Go version.' - value: ${{ steps.parse-version.outputs.detected-version }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: sh - env: - DEFAULT_VERSION: ${{ inputs.default-version }} - run: | - set -eu - - # Validate default-version format - if ! echo "$DEFAULT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then - echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 1.22, 1.21.5)" - exit 1 - fi - - # Check for reasonable version range (prevent malicious inputs) - major_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f1) - if [ "$major_version" -ne 1 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. Go major version should be 1" - exit 1 - fi - - # Check minor version range - minor_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f2) - if [ "$minor_version" -lt 16 ] || [ "$minor_version" -gt 30 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. Go minor version should be between 16 and 30" - exit 1 - fi - - echo "Input validation completed successfully" - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token || github.token }} - - - name: Parse Go Version - id: parse-version - uses: ivuorinen/actions/version-file-parser@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'go' - tool-versions-key: 'golang' - dockerfile-image: 'golang' - version-file: '.go-version' - validation-regex: '^[0-9]+\.[0-9]+(\.[0-9]+)?$' - default-version: ${{ inputs.default-version }} diff --git a/go-version-detect/rules.yml b/go-version-detect/rules.yml deleted file mode 100644 index 8a96744..0000000 --- a/go-version-detect/rules.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -# Validation rules for go-version-detect action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (2/2 inputs) -# -# This file defines validation rules for the go-version-detect GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: go-version-detect -description: Detects the Go version from the project's go.mod file or defaults to a specified version. -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - default-version - - token -conventions: - default-version: semantic_version - token: github_token -overrides: - default-version: go_version -statistics: - total_inputs: 2 - validated_inputs: 2 - 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: false - has_security_validation: true diff --git a/language-version-detect/README.md b/language-version-detect/README.md new file mode 100644 index 0000000..5f3a969 --- /dev/null +++ b/language-version-detect/README.md @@ -0,0 +1,50 @@ +# ivuorinen/actions/language-version-detect + +## Language Version Detect + +### Description + +Detects language version from project configuration files with support for PHP, Python, Go, and .NET. + +### Inputs + +| name | description | required | default | +|-------------------|-----------------------------------------------------------------|----------|---------| +| `language` |

Language to detect version for (php, python, go, dotnet)

| `true` | `""` | +| `default-version` |

Default version to use if no version is detected

| `false` | `""` | +| `token` |

GitHub token for authentication

| `false` | `""` | + +### Outputs + +| name | description | +|--------------------|----------------------------------------------------------------------------| +| `detected-version` |

Detected or default language version

| +| `package-manager` |

Detected package manager (python: pip/poetry/pipenv, php: composer)

| + +### Runs + +This action is a `composite` action. + +### Usage + +```yaml +- uses: ivuorinen/actions/language-version-detect@v2025 + with: + language: + # Language to detect version for (php, python, go, dotnet) + # + # Required: true + # Default: "" + + default-version: + # Default version to use if no version is detected + # + # Required: false + # Default: "" + + token: + # GitHub token for authentication + # + # Required: false + # Default: "" +``` diff --git a/language-version-detect/action.yml b/language-version-detect/action.yml new file mode 100644 index 0000000..9848adc --- /dev/null +++ b/language-version-detect/action.yml @@ -0,0 +1,196 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-action.json +# permissions: +# - contents: read # Required for reading version files +--- +name: Language Version Detect +description: 'Detects language version from project configuration files with support for PHP, Python, Go, and .NET.' +author: 'Ismo Vuorinen' + +branding: + icon: code + color: blue + +inputs: + language: + description: 'Language to detect version for (php, python, go, dotnet)' + required: true + default-version: + description: 'Default version to use if no version is detected' + required: false + token: + description: 'GitHub token for authentication' + required: false + default: '' + +outputs: + detected-version: + description: 'Detected or default language version' + value: ${{ steps.parse-version.outputs.detected-version }} + package-manager: + description: 'Detected package manager (python: pip/poetry/pipenv, php: composer)' + value: ${{ steps.parse-version.outputs.package-manager }} + +runs: + using: composite + steps: + - name: Validate Inputs + id: validate + shell: sh + env: + LANGUAGE: ${{ inputs.language }} + DEFAULT_VERSION: ${{ inputs.default-version }} + run: | + set -eu + + # Validate language parameter + case "$LANGUAGE" in + php|python|go|dotnet) + ;; + *) + echo "::error::Invalid language: '$LANGUAGE'. Must be one of: php, python, go, dotnet" + exit 1 + ;; + esac + + # Set default version if not provided + if [ -z "$DEFAULT_VERSION" ]; then + case "$LANGUAGE" in + php) + default="8.4" + ;; + python) + default="3.12" + ;; + go) + default="1.21" + ;; + dotnet) + default="7.0" + ;; + esac + printf 'default_version=%s\n' "$default" >> "$GITHUB_OUTPUT" + else + printf 'default_version=%s\n' "$DEFAULT_VERSION" >> "$GITHUB_OUTPUT" + fi + + # Validate version format for specified language + version="${DEFAULT_VERSION:-$default}" + + case "$LANGUAGE" in + php) + # Validate PHP version format (X.Y or X.Y.Z) + case "$version" in + [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) + ;; + *) + echo "::error::Invalid PHP version format: '$version'. Expected format: X.Y or X.Y.Z (e.g., 8.4, 8.3.1)" + exit 1 + ;; + esac + + # Check for reasonable PHP version range + major_version=$(echo "$version" | cut -d'.' -f1) + if [ "$major_version" -lt 7 ] || [ "$major_version" -gt 9 ]; then + echo "::error::Invalid PHP version: '$version'. PHP major version should be between 7 and 9" + exit 1 + fi + + # Additional validation for PHP 8.x minor versions + if [ "$major_version" -eq 8 ]; then + minor_version=$(echo "$version" | cut -d'.' -f2) + if [ "$minor_version" -gt 4 ]; then + echo "::error::Invalid PHP 8 version: '$version'. PHP 8 minor version should be between 0 and 4" + exit 1 + fi + fi + ;; + + python) + # Validate Python version format + case "$version" in + [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) + ;; + *) + echo "::error::Invalid Python version format: '$version'. Expected format: X.Y or X.Y.Z (e.g., 3.12, 3.11.5)" + exit 1 + ;; + esac + + # Check Python major version + major_version=$(echo "$version" | cut -d'.' -f1) + if [ "$major_version" -ne 3 ]; then + echo "::error::Invalid Python version: '$version'. Python major version should be 3" + exit 1 + fi + + # Check Python minor version range + minor_version=$(echo "$version" | cut -d'.' -f2) + if [ "$minor_version" -lt 8 ] || [ "$minor_version" -gt 15 ]; then + echo "::error::Invalid Python version: '$version'. Python 3 minor version should be between 8 and 15" + exit 1 + fi + ;; + + go) + # Validate Go version format + case "$version" in + [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) + ;; + *) + echo "::error::Invalid Go version format: '$version'. Expected format: X.Y or X.Y.Z (e.g., 1.21, 1.21.5)" + exit 1 + ;; + esac + + # Check Go major version (must be 1) + major_version=$(echo "$version" | cut -d'.' -f1) + if [ "$major_version" -ne 1 ]; then + echo "::error::Invalid Go version: '$version'. Go major version should be 1" + exit 1 + fi + + # Check Go minor version range + minor_version=$(echo "$version" | cut -d'.' -f2) + if [ "$minor_version" -lt 16 ] || [ "$minor_version" -gt 30 ]; then + echo "::error::Invalid Go version: '$version'. Go minor version should be between 16 and 30" + exit 1 + fi + ;; + + dotnet) + # Validate .NET version format + case "$version" in + [0-9]* | [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) + ;; + *) + echo "::error::Invalid .NET version format: '$version'. Expected format: X, X.Y, or X.Y.Z (e.g., 7, 7.0, 7.0.1)" + exit 1 + ;; + esac + + # Check .NET major version range + major_version=$(echo "$version" | cut -d'.' -f1) + if [ "$major_version" -lt 3 ] || [ "$major_version" -gt 20 ]; then + echo "::error::Invalid .NET version: '$version'. .NET major version should be between 3 and 20" + exit 1 + fi + ;; + esac + + echo "Input validation completed successfully for $LANGUAGE version $version" + + - name: Checkout Repository + uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta + with: + token: ${{ inputs.token || github.token }} + + - name: Parse Language Version + id: parse-version + 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 }} diff --git a/version-validator/rules.yml b/language-version-detect/rules.yml similarity index 58% rename from version-validator/rules.yml rename to language-version-detect/rules.yml index 12db6d3..74fd787 100644 --- a/version-validator/rules.yml +++ b/language-version-detect/rules.yml @@ -1,26 +1,26 @@ --- -# Validation rules for version-validator action +# Validation rules for language-version-detect action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 # Coverage: 67% (2/3 inputs) # -# This file defines validation rules for the version-validator GitHub Action. +# This file defines validation rules for the language-version-detect GitHub Action. # Rules are automatically applied by validate-inputs action when this # action is used. # schema_version: '1.0' -action: version-validator -description: Validates and normalizes version strings using customizable regex patterns +action: language-version-detect +description: Detects language version from project configuration files with support for PHP, Python, Go, and .NET. generator_version: 1.0.0 required_inputs: - - version -optional_inputs: - language - - validation-regex +optional_inputs: + - default-version + - token conventions: - validation-regex: regex_pattern - version: flexible_version + default-version: semantic_version + token: github_token overrides: {} statistics: total_inputs: 3 @@ -32,7 +32,7 @@ auto_detected: true manual_review_required: true quality_indicators: has_required_inputs: true - has_token_validation: false + has_token_validation: true has_version_validation: true has_file_validation: false - has_security_validation: false + has_security_validation: true diff --git a/node-setup/README.md b/node-setup/README.md index 444d32a..fbac71a 100644 --- a/node-setup/README.md +++ b/node-setup/README.md @@ -4,7 +4,7 @@ ### Description -Sets up Node.js env with advanced version management, caching, and tooling. +Sets up Node.js environment with version detection and package manager configuration. ### Inputs @@ -14,23 +14,16 @@ Sets up Node.js env with advanced version management, caching, and tooling. | `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` | `""` | -| `cache` |

Enable dependency caching

| `false` | `true` | -| `install` |

Automatically install dependencies

| `false` | `true` | | `node-mirror` |

Custom Node.js binary mirror

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

Force specific Node.js version regardless of config files

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

Maximum number of retry attempts for package manager operations

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

Installed Node.js version

| -| `package-manager` |

Selected package manager

| -| `cache-hit` |

Indicates if there was a cache hit

| -| `node-path` |

Path to Node.js installation

| -| `esm-support` |

Whether ESM modules are supported

| -| `typescript-support` |

Whether TypeScript is configured

| -| `detected-frameworks` |

Comma-separated list of detected frameworks

| +| name | description | +|-------------------|-------------------------------------| +| `node-version` |

Installed Node.js version

| +| `package-manager` |

Selected package manager

| +| `node-path` |

Path to Node.js installation

| ### Runs @@ -65,18 +58,6 @@ This action is a `composite` action. # Required: false # Default: "" - cache: - # Enable dependency caching - # - # Required: false - # Default: true - - install: - # Automatically install dependencies - # - # Required: false - # Default: true - node-mirror: # Custom Node.js binary mirror # @@ -88,10 +69,4 @@ This action is a `composite` action. # # Required: false # Default: "" - - max-retries: - # Maximum number of retry attempts for package manager operations - # - # Required: false - # Default: 3 ``` diff --git a/node-setup/action.yml b/node-setup/action.yml index 86c2ffe..04a618b 100644 --- a/node-setup/action.yml +++ b/node-setup/action.yml @@ -3,7 +3,7 @@ # - (none required) # Setup action, no repository writes --- name: Node Setup -description: 'Sets up Node.js env with advanced version management, caching, and tooling.' +description: 'Sets up Node.js environment with version detection and package manager configuration.' author: 'Ismo Vuorinen' branding: @@ -26,24 +26,12 @@ inputs: token: description: 'Auth token for private registry' required: false - cache: - description: 'Enable dependency caching' - required: false - default: 'true' - install: - description: 'Automatically install dependencies' - required: false - default: 'true' node-mirror: description: 'Custom Node.js binary mirror' required: false force-version: description: 'Force specific Node.js version regardless of config files' required: false - max-retries: - description: 'Maximum number of retry attempts for package manager operations' - required: false - default: '3' outputs: node-version: @@ -52,21 +40,9 @@ outputs: package-manager: description: 'Selected package manager' value: ${{ steps.package-manager-resolution.outputs.final-package-manager }} - cache-hit: - description: 'Indicates if there was a cache hit' - value: ${{ steps.deps-cache.outputs.cache-hit }} node-path: description: 'Path to Node.js installation' - value: ${{ steps.setup.outputs.node-path }} - esm-support: - description: 'Whether ESM modules are supported' - value: ${{ steps.package-manager-resolution.outputs.esm-support }} - typescript-support: - description: 'Whether TypeScript is configured' - value: ${{ steps.package-manager-resolution.outputs.typescript-support }} - detected-frameworks: - description: 'Comma-separated list of detected frameworks' - value: ${{ steps.package-manager-resolution.outputs.detected-frameworks }} + value: ${{ steps.final-outputs.outputs.node-path }} runs: using: composite @@ -80,9 +56,6 @@ runs: PACKAGE_MANAGER: ${{ inputs.package-manager }} REGISTRY_URL: ${{ inputs.registry-url }} NODE_MIRROR: ${{ inputs.node-mirror }} - MAX_RETRIES: ${{ inputs.max-retries }} - CACHE: ${{ inputs.cache }} - INSTALL: ${{ inputs.install }} AUTH_TOKEN: ${{ inputs.token }} run: | set -euo pipefail @@ -142,23 +115,6 @@ runs: fi fi - # Validate max retries (positive integer with reasonable upper limit) - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$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 boolean inputs - if [[ "$CACHE" != "true" ]] && [[ "$CACHE" != "false" ]]; then - echo "::error::Invalid cache value: '$CACHE'. Must be 'true' or 'false'" - exit 1 - fi - - if [[ "$INSTALL" != "true" ]] && [[ "$INSTALL" != "false" ]]; then - echo "::error::Invalid install value: '$INSTALL'. Must be 'true' or 'false'" - exit 1 - 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 @@ -214,69 +170,12 @@ runs: echo "final-package-manager=$final_pm" >> $GITHUB_OUTPUT echo "Final package manager: $final_pm" - # Node.js feature detection - echo "Detecting Node.js features..." - - # Detect ESM support - esm_support="false" - if [ -f package.json ] && command -v jq >/dev/null 2>&1; then - pkg_type=$(jq -r '.type // "commonjs"' package.json 2>/dev/null) - if [ "$pkg_type" = "module" ]; then - esm_support="true" - fi - fi - echo "esm-support=$esm_support" >> $GITHUB_OUTPUT - echo "ESM support: $esm_support" - - # Detect TypeScript - typescript_support="false" - if [ -f tsconfig.json ] || [ -f package.json ]; then - if [ -f tsconfig.json ]; then - typescript_support="true" - elif command -v jq >/dev/null 2>&1; then - if jq -e '.devDependencies.typescript or .dependencies.typescript' package.json >/dev/null 2>&1; then - typescript_support="true" - fi - fi - fi - echo "typescript-support=$typescript_support" >> $GITHUB_OUTPUT - echo "TypeScript support: $typescript_support" - - # Detect frameworks - frameworks="" - if [ -f package.json ] && command -v jq >/dev/null 2>&1; then - detected_frameworks=() - if jq -e '.dependencies.next or .devDependencies.next' package.json >/dev/null 2>&1; then - detected_frameworks+=("next") - fi - if jq -e '.dependencies.react or .devDependencies.react' package.json >/dev/null 2>&1; then - detected_frameworks+=("react") - fi - if jq -e '.dependencies.vue or .devDependencies.vue' package.json >/dev/null 2>&1; then - detected_frameworks+=("vue") - fi - if jq -e '.dependencies.svelte or .devDependencies.svelte' package.json >/dev/null 2>&1; then - detected_frameworks+=("svelte") - fi - if jq -e '.dependencies."@angular/core" or .devDependencies."@angular/core"' package.json >/dev/null 2>&1; then - detected_frameworks+=("angular") - fi - - if [ ${#detected_frameworks[@]} -gt 0 ]; then - frameworks=$(IFS=','; echo "${detected_frameworks[*]}") - fi - fi - echo "detected-frameworks=$frameworks" >> $GITHUB_OUTPUT - echo "Detected frameworks: $frameworks" - - 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 }} - # Note: cache parameter removed for actions/setup-node@v6 compatibility - # Caching is handled separately via common-cache action (step: Cache Dependencies) - name: Enable Corepack id: corepack @@ -297,19 +196,7 @@ runs: sanitized_token="$(echo "$TOKEN" | tr -d '\n\r')" printf 'NODE_AUTH_TOKEN=%s\n' "$sanitized_token" >> "$GITHUB_ENV" - - name: Cache Dependencies - if: inputs.cache == 'true' - id: deps-cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'npm' - paths: '~/.npm,~/.yarn/cache,~/.pnpm-store,~/.bun/install/cache,node_modules' - key-prefix: 'node-${{ steps.version.outputs.detected-version }}-${{ steps.package-manager-resolution.outputs.final-package-manager }}' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb,.yarnrc.yml' - restore-keys: '${{ runner.os }}-node-${{ steps.version.outputs.detected-version }}-${{ steps.package-manager-resolution.outputs.final-package-manager }}-' - - - name: Install Package Managers - if: inputs.install == 'true' && steps.deps-cache.outputs.cache-hit != 'true' + - name: Setup Package Manager shell: bash env: PACKAGE_MANAGER: ${{ steps.package-manager-resolution.outputs.final-package-manager }} @@ -343,61 +230,13 @@ runs: esac - name: Setup Bun - if: inputs.install == 'true' && steps.package-manager-resolution.outputs.final-package-manager == '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: Export Package Manager to Environment - if: inputs.install == 'true' && steps.deps-cache.outputs.cache-hit != 'true' - shell: bash - env: - PACKAGE_MANAGER: ${{ steps.package-manager-resolution.outputs.final-package-manager }} - run: | - # Sanitize package manager by removing newlines to prevent env var injection - sanitized_pm="$(echo "$PACKAGE_MANAGER" | tr -d '\n\r')" - printf 'PACKAGE_MANAGER=%s\n' "$sanitized_pm" >> "$GITHUB_ENV" - - - name: Install Dependencies - if: inputs.install == 'true' && steps.deps-cache.outputs.cache-hit != 'true' - uses: ivuorinen/actions/common-retry@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - command: | - package_manager="$PACKAGE_MANAGER" - echo "Installing dependencies using $package_manager..." - case "$package_manager" in - "pnpm") - pnpm install --frozen-lockfile - ;; - "yarn") - # Check for Yarn Berry/PnP configuration - if [ -f ".yarnrc.yml" ]; then - echo "Detected Yarn Berry configuration" - yarn install --immutable - else - echo "Using Yarn Classic" - yarn install --frozen-lockfile - fi - ;; - "bun") - bun install - ;; - "npm"|*) - npm ci - ;; - esac - echo "โœ… Dependencies installed successfully" - max-retries: ${{ inputs.max-retries }} - description: 'Installing Node.js dependencies' - - name: Set Final Outputs + id: final-outputs shell: bash - env: - NODE_VERSION: ${{ steps.version.outputs.detected-version }} - PACKAGE_MANAGER: ${{ steps.package-manager-resolution.outputs.final-package-manager }} - run: |- - { - echo "node-version=$NODE_VERSION" - echo "package-manager=$PACKAGE_MANAGER" - echo "node-path=$(which node)" - } >> $GITHUB_OUTPUT + run: | + echo "node-path=$(which node)" >> $GITHUB_OUTPUT diff --git a/node-setup/rules.yml b/node-setup/rules.yml index b7516cc..efc7687 100644 --- a/node-setup/rules.yml +++ b/node-setup/rules.yml @@ -2,7 +2,7 @@ # Validation rules for node-setup action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 -# Coverage: 78% (7/9 inputs) +# 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 @@ -11,37 +11,32 @@ schema_version: '1.0' action: node-setup -description: Sets up Node.js env with advanced version management, caching, and tooling. +description: Sets up Node.js environment with version detection and package manager configuration. generator_version: 1.0.0 required_inputs: [] optional_inputs: - - cache - default-version - force-version - - install - - max-retries - node-mirror - package-manager - registry-url - token conventions: - cache: boolean default-version: semantic_version force-version: semantic_version - max-retries: numeric_range_1_10 package-manager: boolean registry-url: url token: github_token overrides: package-manager: package_manager_enum statistics: - total_inputs: 9 - validated_inputs: 7 + total_inputs: 6 + validated_inputs: 5 skipped_inputs: 0 - coverage_percentage: 78 -validation_coverage: 78 + coverage_percentage: 83 +validation_coverage: 83 auto_detected: true -manual_review_required: true +manual_review_required: false quality_indicators: has_required_inputs: false has_token_validation: true diff --git a/npm-publish/action.yml b/npm-publish/action.yml index ead34b9..9414d79 100644 --- a/npm-publish/action.yml +++ b/npm-publish/action.yml @@ -101,8 +101,49 @@ runs: token: ${{ inputs.token || github.token }} - name: Setup Node.js + id: node-setup uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + - name: Cache Node Dependencies + id: cache + uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + 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 }}' + + - name: Install Dependencies + if: steps.cache.outputs.cache-hit != 'true' + shell: sh + env: + PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + run: | + set -eu + + echo "Installing dependencies using $PACKAGE_MANAGER..." + + case "$PACKAGE_MANAGER" in + "pnpm") + pnpm install --frozen-lockfile + ;; + "yarn") + if [ -f ".yarnrc.yml" ]; then + yarn install --immutable + else + yarn install --frozen-lockfile + fi + ;; + "bun") + bun install --frozen-lockfile + ;; + "npm"|*) + npm ci + ;; + esac + + echo "โœ… Dependencies installed successfully" + - name: Authenticate NPM shell: sh env: diff --git a/php-composer/action.yml b/php-composer/action.yml index f69208e..1854793 100644 --- a/php-composer/action.yml +++ b/php-composer/action.yml @@ -196,12 +196,12 @@ runs: composer clear-cache - name: Install Dependencies - uses: ivuorinen/actions/common-retry@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 with: + timeout_minutes: 10 + max_attempts: ${{ inputs.max-retries }} + retry_wait_seconds: 30 command: composer install ${{ inputs.args }} - max-retries: ${{ inputs.max-retries }} - retry-delay: '30' - description: 'Installing PHP dependencies via Composer' - name: Verify Installation shell: bash diff --git a/php-laravel-phpunit/action.yml b/php-laravel-phpunit/action.yml index 1c9441f..724f51d 100644 --- a/php-laravel-phpunit/action.yml +++ b/php-laravel-phpunit/action.yml @@ -60,14 +60,15 @@ runs: - name: Detect PHP Version id: php-version - uses: ivuorinen/actions/php-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + 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.php-version }} + php-version: ${{ steps.php-version.outputs.detected-version }} extensions: ${{ inputs.extensions }} coverage: ${{ inputs.coverage }} diff --git a/php-tests/action.yml b/php-tests/action.yml index 1850d66..1fdeb40 100644 --- a/php-tests/action.yml +++ b/php-tests/action.yml @@ -85,13 +85,6 @@ runs: with: token: ${{ inputs.token || github.token }} - - name: Set Git Config - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token != '' && inputs.token || github.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} - - name: Composer Install uses: ivuorinen/actions/php-composer@0fa9a68f07a1260b321f814202658a6089a43d42 diff --git a/php-version-detect/CustomValidator.py b/php-version-detect/CustomValidator.py deleted file mode 100755 index 4d19066..0000000 --- a/php-version-detect/CustomValidator.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for php-version-detect 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 php-version-detect action.""" - - def __init__(self, action_type: str = "php-version-detect") -> None: - """Initialize php-version-detect validator.""" - super().__init__(action_type) - self.version_validator = VersionValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate php-version-detect 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("PHP version cannot be empty") - valid = False - elif value: - # Use the PHP version validator which handles version ranges - result = self.version_validator.validate_php_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 - - 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": "php_version", - "required": False, - "description": "Default PHP version to use", - } - } diff --git a/php-version-detect/README.md b/php-version-detect/README.md deleted file mode 100644 index e0027c7..0000000 --- a/php-version-detect/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# ivuorinen/actions/php-version-detect - -## PHP Version Detect - -### Description - -Detects the PHP version from the project's composer.json, phpunit.xml, or other configuration files. - -### Inputs - -| name | description | required | default | -|-------------------|--------------------------------------------------------------|----------|---------| -| `default-version` |

Default PHP version to use if no version is detected.

| `false` | `8.2` | -| `token` |

GitHub token for authentication

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

Detected or default PHP version.

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/php-version-detect@main - with: - default-version: - # Default PHP version to use if no version is detected. - # - # Required: false - # Default: 8.2 - - token: - # GitHub token for authentication - # - # Required: false - # Default: "" -``` diff --git a/php-version-detect/action.yml b/php-version-detect/action.yml deleted file mode 100644 index bf005b4..0000000 --- a/php-version-detect/action.yml +++ /dev/null @@ -1,77 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for reading version files ---- -name: PHP Version Detect -description: "Detects the PHP version from the project's composer.json, phpunit.xml, or other configuration files." -author: 'Ismo Vuorinen' - -branding: - icon: code - color: purple - -inputs: - default-version: - description: 'Default PHP version to use if no version is detected.' - required: false - default: '8.2' - token: - description: 'GitHub token for authentication' - required: false - default: '' - -outputs: - php-version: - description: 'Detected or default PHP version.' - value: ${{ steps.parse-version.outputs.detected-version }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - DEFAULT_VERSION: ${{ inputs.default-version }} - run: | - set -euo pipefail - - # Validate default-version format - if ! [[ "$DEFAULT_VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then - echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 8.2, 8.3.1)" - exit 1 - fi - - # Check for reasonable version range (prevent malicious inputs) - major_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f1) - if [ "$major_version" -lt 7 ] || [ "$major_version" -gt 9 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. PHP major version should be between 7 and 9" - exit 1 - fi - - # Check minor version range for PHP 8 - if [ "$major_version" -eq 8 ]; then - minor_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f2) - if [ "$minor_version" -lt 0 ] || [ "$minor_version" -gt 4 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. PHP 8 minor version should be between 0 and 4" - 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 PHP Version - id: parse-version - uses: ivuorinen/actions/version-file-parser@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'php' - tool-versions-key: 'php' - dockerfile-image: 'php' - version-file: '.php-version' - validation-regex: '^[0-9]+\.[0-9]+(\.[0-9]+)?$' - default-version: ${{ inputs.default-version }} diff --git a/php-version-detect/rules.yml b/php-version-detect/rules.yml deleted file mode 100644 index f6d0994..0000000 --- a/php-version-detect/rules.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -# Validation rules for php-version-detect action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (2/2 inputs) -# -# This file defines validation rules for the php-version-detect GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: php-version-detect -description: Detects the PHP version from the project's composer.json, phpunit.xml, or other configuration files. -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - default-version - - token -conventions: - default-version: semantic_version - token: github_token -overrides: - default-version: php_version -statistics: - total_inputs: 2 - validated_inputs: 2 - 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: false - has_security_validation: true diff --git a/pr-lint/action.yml b/pr-lint/action.yml index 0311cfb..6c71909 100644 --- a/pr-lint/action.yml +++ b/pr-lint/action.yml @@ -55,22 +55,12 @@ runs: with: token: ${{ inputs.token || github.token }} ref: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref_name }} + persist-credentials: false # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to # improve performance fetch-depth: 0 - # โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ - # โ”‚ Setup Git configuration โ”‚ - # โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - - name: Setup Git Config - id: git-config - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token || github.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} - # โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ # โ”‚ Install packages for linting โ”‚ # โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ @@ -78,42 +68,83 @@ runs: # Node.js tests if package.json exists - name: Detect package.json id: detect-node - shell: bash + shell: sh run: | - set -euo pipefail + set -eu if [ -f package.json ]; then - echo "found=true" >> $GITHUB_OUTPUT + printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" fi - name: Setup Node.js environment if: steps.detect-node.outputs.found == 'true' + id: node-setup uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + + - name: Cache Node Dependencies + if: steps.detect-node.outputs.found == 'true' + id: node-cache + uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 with: - install: true - cache: true + 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 }}' + + - 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 }} + run: | + set -eu + + echo "Installing dependencies using $PACKAGE_MANAGER..." + + case "$PACKAGE_MANAGER" in + "pnpm") + pnpm install --frozen-lockfile + ;; + "yarn") + if [ -f ".yarnrc.yml" ]; then + yarn install --immutable + else + yarn install --frozen-lockfile + fi + ;; + "bun") + bun install --frozen-lockfile + ;; + "npm"|*) + npm ci + ;; + esac + + echo "โœ… Dependencies installed successfully" # PHP tests if composer.json exists - name: Detect composer.json id: detect-php - shell: bash + shell: sh run: | - set -euo pipefail + set -eu if [ -f composer.json ]; then - echo "found=true" >> $GITHUB_OUTPUT + printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" fi - name: Detect PHP Version if: steps.detect-php.outputs.found == 'true' id: php-version - uses: ivuorinen/actions/php-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + with: + language: 'php' - name: Setup PHP if: steps.detect-php.outputs.found == 'true' uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 with: - php-version: ${{ steps.php-version.outputs.php-version }} + php-version: ${{ steps.php-version.outputs.detected-version }} tools: composer coverage: none env: @@ -121,74 +152,78 @@ runs: - name: Setup problem matchers for PHP if: steps.detect-php.outputs.found == 'true' - shell: bash + shell: sh env: RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | - set -euo pipefail + set -eu echo "::add-matcher::$RUNNER_TOOL_CACHE/php.json" - name: Install PHP dependencies if: steps.detect-php.outputs.found == 'true' - shell: bash + shell: sh run: | - set -euo pipefail + set -eu composer install --no-progress --prefer-dist --no-interaction # Python tests if requirements.txt exists - name: Detect requirements.txt id: detect-python - shell: bash + shell: sh run: | - set -euo pipefail + set -eu if [ -f requirements.txt ]; then - echo "found=true" >> $GITHUB_OUTPUT + printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" fi - name: Detect Python Version if: steps.detect-python.outputs.found == 'true' id: python-version - uses: ivuorinen/actions/python-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + with: + language: 'python' - name: Setup Python if: steps.detect-python.outputs.found == 'true' uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: - python-version: ${{ steps.python-version.outputs.python-version }} + python-version: ${{ steps.python-version.outputs.detected-version }} cache: 'pip' - name: Install Python dependencies if: steps.detect-python.outputs.found == 'true' - shell: bash + shell: sh run: | - set -euo pipefail + set -eu pip install -r requirements.txt # Go tests if go.mod exists - name: Detect go.mod id: detect-go - shell: bash + shell: sh run: | - set -euo pipefail + set -eu if [ -f go.mod ]; then - echo "found=true" >> $GITHUB_OUTPUT + printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" fi - name: Detect Go Version if: steps.detect-go.outputs.found == 'true' id: go-version - uses: ivuorinen/actions/go-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + with: + language: 'go' - name: Setup Go if: steps.detect-go.outputs.found == 'true' uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 with: - go-version: ${{ steps.go-version.outputs.go-version }} + go-version: ${{ steps.go-version.outputs.detected-version }} cache: true # โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ @@ -221,7 +256,7 @@ runs: contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref) }} - GITHUB_TOKEN: ${{ steps.git-config.outputs.token || inputs.token || github.token }} + GITHUB_TOKEN: ${{ inputs.token || github.token }} # Apply linter fixes configuration # @@ -245,7 +280,7 @@ runs: # Export env vars to make them available for subsequent expressions - name: Export Apply Fixes Variables - shell: bash + shell: sh run: | echo "APPLY_FIXES_EVENT=pull_request" >> "$GITHUB_ENV" echo "APPLY_FIXES_MODE=commit" >> "$GITHUB_ENV" @@ -263,7 +298,7 @@ runs: # Set APPLY_FIXES_IF var for use in future steps - name: Set APPLY_FIXES_IF var - shell: bash + shell: sh env: APPLY_FIXES_CONDITION: >- ${{ @@ -272,7 +307,7 @@ runs: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) }} run: | - set -euo pipefail + set -eu # Sanitize by removing newlines to prevent env var injection sanitized_condition="$(echo "$APPLY_FIXES_CONDITION" | tr -d '\n\r')" @@ -280,12 +315,12 @@ runs: # Set APPLY_FIXES_IF_* vars for use in future steps - name: Set APPLY_FIXES_IF_* vars - shell: bash + shell: sh env: APPLY_FIXES_IF_PR_CONDITION: ${{ env.APPLY_FIXES_IF == 'true' && env.APPLY_FIXES_MODE == 'pull_request' }} APPLY_FIXES_IF_COMMIT_CONDITION: ${{ env.APPLY_FIXES_IF == 'true' && env.APPLY_FIXES_MODE == 'commit' && (!contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref)) }} run: | - set -euo pipefail + set -eu # Sanitize by removing newlines to prevent env var injection sanitized_pr="$(echo "$APPLY_FIXES_IF_PR_CONDITION" | tr -d '\n\r')" @@ -301,19 +336,19 @@ runs: id: cpr if: env.APPLY_FIXES_IF_PR == 'true' with: - token: ${{ steps.git-config.outputs.token || inputs.token || github.token }} - commit-message: '[MegaLinter] Apply linters automatic fixes' - title: '[MegaLinter] Apply linters automatic fixes' + token: ${{ inputs.token || github.token }} + commit-message: 'style: apply linter fixes' + title: 'style: apply linter fixes' labels: bot - name: Create PR output if: env.APPLY_FIXES_IF_PR == 'true' - shell: bash + shell: sh env: PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }} PR_URL: ${{ steps.cpr.outputs.pull-request-url }} run: | - set -euo pipefail + set -eu echo "PR Number - $PR_NUMBER" echo "PR URL - $PR_URL" @@ -322,7 +357,7 @@ runs: # (for now works only on PR from same repository, not from forks) - name: Prepare commit if: env.APPLY_FIXES_IF_COMMIT == 'true' - shell: bash + shell: sh env: BRANCH_REF: >- ${{ @@ -331,7 +366,7 @@ runs: github.ref_name }} run: | - set -euo pipefail + set -eu # Fix .git directory ownership after MegaLinter container execution sudo chown -Rc "$UID" .git/ @@ -361,6 +396,6 @@ runs: github.head_ref || github.ref }} - commit_message: '[MegaLinter] Apply linters fixes' - commit_user_name: ${{ steps.git-config.outputs.username }} - commit_user_email: ${{ steps.git-config.outputs.email }} + commit_message: 'style: apply linter fixes' + commit_user_name: ${{ inputs.username }} + commit_user_email: ${{ inputs.email }} diff --git a/pre-commit/action.yml b/pre-commit/action.yml index 5165e95..0b5824c 100644 --- a/pre-commit/action.yml +++ b/pre-commit/action.yml @@ -57,26 +57,20 @@ runs: base-branch: ${{ inputs.base-branch }} email: ${{ inputs.commit_email }} username: ${{ inputs.commit_user }} - - name: Set Git Config - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token }} - username: ${{ inputs.commit_user }} - email: ${{ inputs.commit_email }} - name: Set option id: set-option - shell: bash + shell: sh env: BASE_BRANCH: ${{ inputs.base-branch }} run: | - set -euo pipefail + set -eu if [ -z "$BASE_BRANCH" ]; then - echo "option=--all-files" >> $GITHUB_OUTPUT + printf '%s\n' "option=--all-files" >> "$GITHUB_OUTPUT" exit 0 fi - echo "option=--from-ref $BASE_BRANCH --to-ref HEAD" >> $GITHUB_OUTPUT + printf '%s\n' "option=--from-ref $BASE_BRANCH --to-ref HEAD" >> "$GITHUB_OUTPUT" - name: Run pre-commit id: pre-commit @@ -92,4 +86,6 @@ runs: uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 with: commit_message: 'style(pre-commit): autofix' + commit_user_name: ${{ inputs.commit_user }} + commit_user_email: ${{ inputs.commit_email }} add_options: -u diff --git a/prettier-check/CustomValidator.py b/prettier-check/CustomValidator.py deleted file mode 100755 index 22bb33f..0000000 --- a/prettier-check/CustomValidator.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for prettier-check 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.conventions import ConventionBasedValidator -from validators.security import SecurityValidator -from validators.version import VersionValidator - - -class CustomValidator(BaseValidator): - """Custom validator for prettier-check action.""" - - def __init__(self, action_type: str = "prettier-check") -> None: - """Initialize prettier-check validator.""" - super().__init__(action_type) - self.convention_validator = ConventionBasedValidator(action_type) - self.security_validator = SecurityValidator() - self.version_validator = VersionValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate prettier-check action inputs.""" - valid = True - - # Use convention-based validation for most inputs - rules_path = Path(__file__).parent / "rules.yml" - self.convention_validator.rules = self.convention_validator.load_rules(rules_path) - - # Handle prettier-version specially (accepts "latest" or semantic version) - # Check both hyphenated and underscored versions since inputs can come either way - inputs_copy = inputs.copy() - prettier_version_key = None - if "prettier-version" in inputs: - prettier_version_key = "prettier-version" - elif "prettier_version" in inputs: - prettier_version_key = "prettier_version" - - if prettier_version_key: - value = inputs[prettier_version_key] - if value and value != "latest": - # Prettier versions should not have 'v' prefix (npm package versions) - if value.startswith("v"): - self.add_error( - f"{prettier_version_key}: Prettier version should not have 'v' prefix" - ) - valid = False - else: - # Must be a semantic version - result = self.version_validator.validate_semantic_version( - value, prettier_version_key - ) - 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 - # Remove both versions from inputs for convention validation - if "prettier-version" in inputs_copy: - del inputs_copy["prettier-version"] - if "prettier_version" in inputs_copy: - del inputs_copy["prettier_version"] - - # Validate plugins for security issues - if inputs_copy.get("plugins"): - # Check for command injection patterns - dangerous_patterns = [ - r"[;&|`$()]", # Shell operators - r"\$\{.*\}", # Variable expansion - r"\$\(.*\)", # Command substitution - ] - - for pattern in dangerous_patterns: - if re.search(pattern, inputs_copy["plugins"]): - self.add_error("plugins: Contains potentially dangerous characters or patterns") - valid = False - break - - # Remove plugins from inputs for convention validation - if "plugins" in inputs_copy: - del inputs_copy["plugins"] - - # Validate file-pattern for security issues - if inputs_copy.get("file-pattern"): - # Check for path traversal and shell expansion - if ".." in inputs_copy["file-pattern"]: - self.add_error("file-pattern: Path traversal detected") - valid = False - elif inputs_copy["file-pattern"].startswith("/"): - self.add_error("file-pattern: Absolute path not allowed") - valid = False - elif "$" in inputs_copy["file-pattern"]: - self.add_error("file-pattern: Shell expansion not allowed") - valid = False - - # Remove file-pattern from inputs for convention validation - if "file-pattern" in inputs_copy: - del inputs_copy["file-pattern"] - - # Validate report-format enum - if "report-format" in inputs_copy: - value = inputs_copy["report-format"] - if value == "": - self.add_error("report-format: Cannot be empty. Must be 'json' or 'sarif'") - valid = False - elif value not in ["json", "sarif"]: - self.add_error("report-format: Invalid format. Must be 'json' or 'sarif'") - valid = False - # Remove report-format from inputs for convention validation - if "report-format" in inputs_copy: - del inputs_copy["report-format"] - - # Use convention-based validation for remaining inputs - if not self.convention_validator.validate_inputs(inputs_copy): - for error in self.convention_validator.errors: - if error not in self.errors: - self.add_error(error) - 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.""" - rules_path = Path(__file__).parent / "rules.yml" - return self.load_rules(rules_path) diff --git a/prettier-check/action.yml b/prettier-check/action.yml deleted file mode 100644 index 3c838a4..0000000 --- a/prettier-check/action.yml +++ /dev/null @@ -1,458 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for reading repository files -# - security-events: write # Required for uploading SARIF reports ---- -name: Prettier Check -description: 'Run Prettier check on the repository with advanced configuration and reporting' -author: Ismo Vuorinen - -branding: - icon: check-circle - color: green - -inputs: - working-directory: - description: 'Directory containing files to check' - required: false - default: '.' - prettier-version: - description: 'Prettier version to use' - required: false - default: 'latest' - config-file: - description: 'Path to Prettier config file' - required: false - default: '.prettierrc' - ignore-file: - description: 'Path to Prettier ignore file' - required: false - default: '.prettierignore' - file-pattern: - description: 'Files to include (glob pattern)' - required: false - default: '**/*.{js,jsx,ts,tsx,css,scss,json,md,yaml,yml}' - cache: - description: 'Enable Prettier caching' - required: false - default: 'true' - fail-on-error: - description: 'Fail workflow if issues are found' - required: false - default: 'true' - report-format: - description: 'Output format (json, sarif)' - required: false - default: 'sarif' - max-retries: - description: 'Maximum number of retry attempts' - required: false - default: '3' - plugins: - description: 'Comma-separated list of Prettier plugins to install' - required: false - default: '' - check-only: - description: 'Only check for formatting issues without fixing' - required: false - default: 'true' - token: - description: 'GitHub token for authentication' - required: false - default: '' - -outputs: - files-checked: - description: 'Number of files checked' - value: ${{ steps.check.outputs.files_checked }} - unformatted-files: - description: 'Number of files with formatting issues' - value: ${{ steps.check.outputs.unformatted_files }} - sarif-file: - description: 'Path to SARIF report file' - value: ${{ steps.check.outputs.sarif_file }} - cache-hit: - description: 'Indicates if there was a cache hit' - value: ${{ steps.cache.outputs.cache-hit }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - PRETTIER_VERSION: ${{ inputs.prettier-version }} - CONFIG_FILE: ${{ inputs.config-file }} - IGNORE_FILE: ${{ inputs.ignore-file }} - FILE_PATTERN: ${{ inputs.file-pattern }} - CACHE: ${{ inputs.cache }} - FAIL_ON_ERROR: ${{ inputs.fail-on-error }} - CHECK_ONLY: ${{ inputs.check-only }} - REPORT_FORMAT: ${{ inputs.report-format }} - MAX_RETRIES: ${{ inputs.max-retries }} - PLUGINS: ${{ inputs.plugins }} - run: | - set -euo pipefail - - # Validate working directory - if [ ! -d "$WORKING_DIRECTORY" ]; then - echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Directory does not exist" - exit 1 - fi - - # Validate path security (prevent path traversal) - if [[ "$WORKING_DIRECTORY" == *".."* ]]; then - echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Path traversal not allowed" - exit 1 - fi - - # Validate prettier version format - if [[ "$PRETTIER_VERSION" != "latest" ]]; then - if ! [[ "$PRETTIER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::Invalid prettier-version: '$PRETTIER_VERSION'. Expected semantic version (e.g., 3.0.0) or 'latest'" - exit 1 - fi - fi - - # Validate config file path security - if [[ "$CONFIG_FILE" == *".."* ]] || [[ "$CONFIG_FILE" == "/"* ]]; then - echo "::error::Invalid config-file path: '$CONFIG_FILE'. Path traversal not allowed" - exit 1 - fi - - # Validate ignore file path security - if [[ "$IGNORE_FILE" == *".."* ]] || [[ "$IGNORE_FILE" == "/"* ]]; then - echo "::error::Invalid ignore-file path: '$IGNORE_FILE'. Path traversal not allowed" - exit 1 - fi - - # Validate file pattern format (basic safety check) - if [[ "$FILE_PATTERN" == *".."* ]] || [[ "$FILE_PATTERN" == "/"* ]]; then - echo "::error::Invalid file-pattern: '$FILE_PATTERN'. Absolute paths and path traversal not allowed" - exit 1 - fi - - # Validate boolean inputs - case "$CACHE" in - true|false) ;; - *) - echo "::error::Invalid cache value: '$CACHE'. Expected: true or false" - exit 1 - ;; - esac - - case "$FAIL_ON_ERROR" in - true|false) ;; - *) - echo "::error::Invalid fail-on-error value: '$FAIL_ON_ERROR'. Expected: true or false" - exit 1 - ;; - esac - - case "$CHECK_ONLY" in - true|false) ;; - *) - echo "::error::Invalid check-only value: '$CHECK_ONLY'. Expected: true or false" - exit 1 - ;; - esac - - # Validate report format - case "$REPORT_FORMAT" in - json|sarif) ;; - *) - echo "::error::Invalid report-format: '$REPORT_FORMAT'. Expected: json or sarif" - exit 1 - ;; - esac - - # Validate max-retries (positive integer) - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ]; then - echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer" - exit 1 - fi - - # Validate max-retries range - if [ "$MAX_RETRIES" -gt 10 ]; then - echo "::error::Invalid max-retries: '$MAX_RETRIES'. Maximum allowed is 10" - exit 1 - fi - - # Validate plugins format if provided - if [ -n "$PLUGINS" ]; then - # Check for basic npm package name format and prevent command injection - if ! [[ "$PLUGINS" =~ ^[a-zA-Z0-9@/._,-]+$ ]]; then - echo "::error::Invalid plugins format: '$PLUGINS'. Use comma-separated npm package names (e.g., plugin1,@scope/plugin2)" - exit 1 - fi - - # Check for suspicious patterns - if [[ "$PLUGINS" == *";"* ]] || [[ "$PLUGINS" == *"&&"* ]] || [[ "$PLUGINS" == *"|"* ]]; then - echo "::error::Invalid plugins format: '$PLUGINS'. Command injection patterns not allowed" - exit 1 - fi - fi - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token || github.token }} - - - name: Setup Node.js - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 - - - name: Set up Cache - id: cache - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - if: inputs.cache == 'true' - with: - type: 'npm' - paths: 'node_modules/.cache/prettier,.prettier-cache' - key-prefix: 'prettier-${{ steps.node-setup.outputs.package-manager }}' - key-files: package.json,package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb,${{ inputs.config-file }} - restore-keys: '${{ runner.os }}-prettier-' - - - name: Install Dependencies - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - PRETTIER_VERSION: ${{ inputs.prettier-version }} - PLUGINS: ${{ inputs.plugins }} - MAX_RETRIES: ${{ inputs.max-retries }} - run: | - set -euo pipefail - - cd "$WORKING_DIRECTORY" - - # Function to install with retries - install_with_retries() { - local attempt=1 - local max_attempts="$MAX_RETRIES" - - while [ $attempt -le $max_attempts ]; do - echo "Installation attempt $attempt of $max_attempts" - - # Install Prettier and base dependencies - if npm install \ - "prettier@$PRETTIER_VERSION" \ - @prettier/plugin-xml \ - prettier-plugin-packagejson \ - prettier-plugin-sh; then - - # Install additional plugins if specified - if [ -n "$PLUGINS" ]; then - IFS=',' read -ra PLUGIN_ARRAY <<< "$PLUGINS" - for plugin in "${PLUGIN_ARRAY[@]}"; do - if ! npm install "$plugin"; then - return 1 - fi - done - fi - - return 0 - fi - - attempt=$((attempt + 1)) - if [ $attempt -le $max_attempts ]; then - echo "Installation failed, waiting 10 seconds before retry..." - sleep 10 - fi - done - - echo "::error::Failed to install dependencies after $max_attempts attempts" - return 1 - } - - install_with_retries - - - name: Prepare Configuration - id: config - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - CONFIG_FILE: ${{ inputs.config-file }} - IGNORE_FILE: ${{ inputs.ignore-file }} - run: | - set -euo pipefail - - cd "$WORKING_DIRECTORY" - - # Create default config if none exists - if [ ! -f "$CONFIG_FILE" ]; then - echo "Creating default Prettier configuration..." - cat > "$CONFIG_FILE" < "$IGNORE_FILE" < "$output_file" - - local first=true - while IFS= read -r line; do - if [ "$first" = true ]; then - first=false - else - echo "," >> "$output_file" - fi - - echo "{ - \"level\": \"error\", - \"message\": { - \"text\": \"File is not formatted according to Prettier rules\" - }, - \"locations\": [ - { - \"physicalLocation\": { - \"artifactLocation\": { - \"uri\": \"$line\" - } - } - } - ] - }" >> "$output_file" - done < "$input_file" - - echo ']}]}' >> "$output_file" - } - - # Run Prettier - echo "Running Prettier check..." - unformatted_files=$(mktemp) - - if [ "$CHECK_ONLY" = "true" ]; then - npx prettier \ - --check \ - --config "$CONFIG_FILE" \ - --ignore-path "$IGNORE_FILE" \ - $cache_flags \ - --no-error-on-unmatched-pattern \ - "$FILE_PATTERN" 2>&1 | \ - grep -oE '[^ ]+\.[a-zA-Z]+$' > "$unformatted_files" || true - else - npx prettier \ - --write \ - --list-different \ - --config "$CONFIG_FILE" \ - --ignore-path "$IGNORE_FILE" \ - $cache_flags \ - --no-error-on-unmatched-pattern \ - "$FILE_PATTERN" > "$unformatted_files" || true - fi - - # Count files - files_checked=$(find . -type f -name "$FILE_PATTERN" -not -path "*/node_modules/*" | wc -l) - unformatted_count=$(wc -l < "$unformatted_files") - - echo "files_checked=${files_checked}" >> $GITHUB_OUTPUT - echo "unformatted_files=${unformatted_count}" >> $GITHUB_OUTPUT - - # Generate SARIF report if requested - if [ "$REPORT_FORMAT" = "sarif" ]; then - prettier_to_sarif "$unformatted_files" "reports/prettier.sarif" - echo "sarif_file=reports/prettier.sarif" >> $GITHUB_OUTPUT - fi - - # Clean up temporary file - rm "$unformatted_files" - - # Exit with error if issues found and fail-on-error is true - if [ "$FAIL_ON_ERROR" = "true" ] && [ "$unformatted_count" -gt 0 ]; then - echo "::error::Found $unformatted_count files with formatting issues" - exit 1 - fi - - - name: Upload Prettier Results - if: always() && inputs.report-format == 'sarif' - uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 - with: - sarif_file: ${{ inputs.working-directory }}/reports/prettier.sarif - category: prettier - - - name: Cleanup - if: always() - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - CACHE: ${{ inputs.cache }} - run: |- - set -euo pipefail - - cd "$WORKING_DIRECTORY" - - # Remove temporary files - rm -rf reports/ - - # Clean cache if exists and not being preserved - if [ "$CACHE" != "true" ]; then - rm -rf .prettier-cache - rm -rf node_modules/.cache/prettier - fi diff --git a/prettier-fix/CustomValidator.py b/prettier-fix/CustomValidator.py deleted file mode 100755 index 1cb2122..0000000 --- a/prettier-fix/CustomValidator.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for prettier-fix 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.network import NetworkValidator -from validators.numeric import NumericValidator -from validators.token import TokenValidator - - -class CustomValidator(BaseValidator): - """Custom validator for prettier-fix action.""" - - def __init__(self, action_type: str = "prettier-fix") -> None: - """Initialize prettier-fix validator.""" - super().__init__(action_type) - self.network_validator = NetworkValidator() - self.numeric_validator = NumericValidator() - self.token_validator = TokenValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate prettier-fix action inputs.""" - valid = True - # No required inputs - - # Validate optional input: username - if "username" in inputs: - username = inputs["username"] - if username: - # Check username length (GitHub usernames are max 39 characters) - if len(username) > 39: - self.add_error("Username is too long (max 39 characters)") - valid = False - # Check for command injection patterns - if ";" in username: - self.add_error("Username contains potentially dangerous character ';'") - valid = False - if "&&" in username or "&" in username: - self.add_error("Username contains potentially dangerous character '&'") - valid = False - if "|" in username: - self.add_error("Username contains potentially dangerous character '|'") - valid = False - if "`" in username: - self.add_error("Username contains potentially dangerous character '`'") - valid = False - if "$" in username: - self.add_error("Username contains potentially dangerous character '$'") - valid = False - - # Validate optional input: email - if "email" in inputs: - email = inputs["email"] - if not email or email.strip() == "": - # Empty email should fail validation - self.add_error("Email cannot be empty") - valid = False - else: - result = self.network_validator.validate_email(email, "email") - for error in self.network_validator.errors: - if error not in self.errors: - self.add_error(error) - self.network_validator.clear_errors() - if not result: - valid = False - # Additional security checks - if "`" in email: - self.add_error("Email contains potentially dangerous character '`'") - valid = False - # Validate optional input: max-retries (check both hyphenated and underscored) - max_retries_key = None - if "max-retries" in inputs: - max_retries_key = "max-retries" - elif "max_retries" in inputs: - max_retries_key = "max_retries" - - if max_retries_key: - result = self.numeric_validator.validate_numeric_range( - inputs[max_retries_key], min_val=1, max_val=10 - ) - 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 optional input: token - if inputs.get("token"): - token = inputs["token"] - # Check for variable expansion (but allow GitHub Actions expressions) - if "${" in token and not token.startswith("${{ ") and not token.endswith(" }}"): - self.add_error("Token contains potentially dangerous variable expansion '${}'") - valid = False - else: - 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 - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs.""" - return [] - - def get_validation_rules(self) -> dict: - """Get validation rules for this action.""" - rules_path = Path(__file__).parent / "rules.yml" - return self.load_rules(rules_path) diff --git a/prettier-fix/README.md b/prettier-fix/README.md deleted file mode 100644 index 83c5921..0000000 --- a/prettier-fix/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# ivuorinen/actions/prettier-fix - -## Prettier Fix - -### Description - -Run Prettier to fix code style violations - -### Inputs - -| name | description | required | default | -|---------------|--------------------------------------------------------------------|----------|-----------------------------| -| `token` |

GitHub token for authentication

| `false` | `${{ github.token }}` | -| `username` |

GitHub username for commits

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

GitHub email for commits

| `false` | `github-actions@github.com` | -| `max-retries` |

Maximum number of retry attempts for npm install operations

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

Number of files changed by Prettier

| -| `format_status` |

Formatting status (success/failure)

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/prettier-fix@main - with: - token: - # GitHub token for authentication - # - # Required: false - # Default: ${{ github.token }} - - username: - # GitHub username for commits - # - # Required: false - # Default: github-actions - - email: - # GitHub email for commits - # - # Required: false - # Default: github-actions@github.com - - max-retries: - # Maximum number of retry attempts for npm install operations - # - # Required: false - # Default: 3 -``` diff --git a/prettier-fix/action.yml b/prettier-fix/action.yml deleted file mode 100644 index 21c6fcf..0000000 --- a/prettier-fix/action.yml +++ /dev/null @@ -1,222 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: write # Required for committing and pushing formatting fixes ---- -name: Prettier Fix -description: Run Prettier to fix code style violations -author: 'Ismo Vuorinen' - -branding: - icon: 'code' - color: 'blue' - -inputs: - token: - description: 'GitHub token for authentication' - required: false - default: ${{ github.token }} - username: - description: 'GitHub username for commits' - required: false - default: 'github-actions' - email: - description: 'GitHub email for commits' - required: false - default: 'github-actions@github.com' - max-retries: - description: 'Maximum number of retry attempts for npm install operations' - required: false - default: '3' - -outputs: - files_changed: - description: 'Number of files changed by Prettier' - value: ${{ steps.format.outputs.files_changed }} - format_status: - description: 'Formatting status (success/failure)' - value: ${{ steps.format.outputs.status }} - -runs: - using: 'composite' - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - GITHUB_TOKEN: ${{ inputs.token }} - GITHUB_TOKEN_DEFAULT: ${{ github.token }} - EMAIL: ${{ inputs.email }} - USERNAME: ${{ inputs.username }} - MAX_RETRIES: ${{ inputs.max-retries }} - run: | - set -euo pipefail - - # Validate GitHub token format (basic validation) - if [[ -n "$GITHUB_TOKEN" ]] && [[ "$GITHUB_TOKEN" != "$GITHUB_TOKEN_DEFAULT" ]]; then - if ! [[ "$GITHUB_TOKEN" =~ ^gh[efpousr]_[a-zA-Z0-9]{36}$ ]]; then - echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters (ghp_, gho_, ghs_, ghe_, ghf_, ghu_, etc.)" - fi - fi - - # Validate email format (basic check) - if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then - echo "::error::Invalid email format: '$EMAIL'. Expected valid email address" - 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 username length - username="$USERNAME" - if [ ${#username} -gt 39 ]; then - echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters" - exit 1 - fi - - # Validate max retries (positive integer with reasonable upper limit) - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$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 - - echo "Input validation completed successfully" - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token }} - - - name: Set Git Config - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} - - - name: Node Setup - id: node-setup - uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 - - - name: Cache npm Dependencies - id: cache-npm - uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - type: 'npm' - paths: 'node_modules' - key-files: 'package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lockb' - key-prefix: 'prettier-fix-${{ steps.node-setup.outputs.package-manager }}' - - - name: Install Dependencies - if: steps.cache-npm.outputs.cache-hit != 'true' - shell: bash - env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} - MAX_RETRIES: ${{ inputs.max-retries }} - run: | - set -euo pipefail - - package_manager="$PACKAGE_MANAGER" - max_retries="$MAX_RETRIES" - - echo "Installing dependencies using $package_manager..." - - for attempt in $(seq 1 $max_retries); do - echo "Attempt $attempt of $max_retries" - - case "$package_manager" in - "pnpm") - if pnpm install --frozen-lockfile; then - echo "โœ… Dependencies installed successfully with pnpm" - exit 0 - fi - ;; - "yarn") - if [ -f ".yarnrc.yml" ]; then - if yarn install --immutable; then - echo "โœ… Dependencies installed successfully with Yarn Berry" - exit 0 - fi - else - if yarn install --frozen-lockfile; then - echo "โœ… Dependencies installed successfully with Yarn Classic" - exit 0 - fi - fi - ;; - "bun") - if bun install --frozen-lockfile; then - echo "โœ… Dependencies installed successfully with Bun" - exit 0 - fi - ;; - "npm"|*) - if npm ci; then - echo "โœ… Dependencies installed successfully with npm" - exit 0 - fi - ;; - esac - - if [ $attempt -lt $max_retries ]; then - echo "โŒ Installation failed, retrying in 5 seconds..." - sleep 5 - fi - done - - echo "::error::Failed to install dependencies after $max_retries attempts" - exit 1 - - - name: Run Prettier Fix - id: format - shell: bash - env: - PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} - run: | - set -euo pipefail - - package_manager="$PACKAGE_MANAGER" - - echo "Running Prettier fix with $package_manager..." - - # Count files before fix - files_before=$(git status --porcelain | wc -l || echo "0") - - # Run Prettier fix based on package manager - case "$package_manager" in - "pnpm") - pnpm exec prettier --write . - ;; - "yarn") - yarn prettier --write . - ;; - "bun") - bunx prettier --write . - ;; - "npm"|*) - npx prettier --write . - ;; - esac - - # Count files after fix - files_after=$(git status --porcelain | wc -l || echo "0") - - # Calculate absolute difference and set status - delta=$((files_after - files_before)) - files_changed=$((delta < 0 ? -delta : delta)) # Ensure non-negative - status=$([ "$files_changed" -eq 0 ] && echo success || echo failure) - - echo "files_changed=$files_changed" >> $GITHUB_OUTPUT - echo "status=$status" >> $GITHUB_OUTPUT - - echo "โœ… Prettier fix completed. Files changed: $files_changed, Status: $status" - - - name: Push Fixes - if: always() - uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 - with: - commit_message: 'style: autofix Prettier violations' - add_options: '-u' diff --git a/prettier-fix/rules.yml b/prettier-fix/rules.yml deleted file mode 100644 index 9859c44..0000000 --- a/prettier-fix/rules.yml +++ /dev/null @@ -1,41 +0,0 @@ ---- -# Validation rules for prettier-fix action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (4/4 inputs) -# -# This file defines validation rules for the prettier-fix GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: prettier-fix -description: Run Prettier to fix code style violations -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - email - - max-retries - - token - - username -conventions: - email: email - max-retries: numeric_range_1_10 - token: github_token - username: username -overrides: {} -statistics: - total_inputs: 4 - validated_inputs: 4 - 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: false - has_file_validation: false - has_security_validation: true diff --git a/prettier-check/README.md b/prettier-lint/README.md similarity index 61% rename from prettier-check/README.md rename to prettier-lint/README.md index cb39af3..41f9316 100644 --- a/prettier-check/README.md +++ b/prettier-lint/README.md @@ -1,36 +1,39 @@ -# ivuorinen/actions/prettier-check +# ivuorinen/actions/prettier-lint -## Prettier Check +## Prettier Lint ### Description -Run Prettier check on the repository with advanced configuration and reporting +Run Prettier in check or fix mode with advanced configuration and reporting ### Inputs | name | description | required | default | |---------------------|------------------------------------------------------------|----------|--------------------------------------------------| -| `working-directory` |

Directory containing files to check

| `false` | `.` | +| `mode` |

Mode to run (check or fix)

| `false` | `check` | +| `working-directory` |

Directory containing files to format

| `false` | `.` | | `prettier-version` |

Prettier version to use

| `false` | `latest` | | `config-file` |

Path to Prettier config file

| `false` | `.prettierrc` | | `ignore-file` |

Path to Prettier ignore file

| `false` | `.prettierignore` | | `file-pattern` |

Files to include (glob pattern)

| `false` | `**/*.{js,jsx,ts,tsx,css,scss,json,md,yaml,yml}` | | `cache` |

Enable Prettier caching

| `false` | `true` | -| `fail-on-error` |

Fail workflow if issues are found

| `false` | `true` | -| `report-format` |

Output format (json, sarif)

| `false` | `sarif` | +| `fail-on-error` |

Fail workflow if issues are found (check mode only)

| `false` | `true` | +| `report-format` |

Output format for check mode (json, sarif)

| `false` | `sarif` | | `max-retries` |

Maximum number of retry attempts

| `false` | `3` | | `plugins` |

Comma-separated list of Prettier plugins to install

| `false` | `""` | -| `check-only` |

Only check for formatting issues without fixing

| `false` | `true` | | `token` |

GitHub token for authentication

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

GitHub username for commits (fix mode only)

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

GitHub email for commits (fix mode only)

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

Number of files checked

| -| `unformatted-files` |

Number of files with formatting issues

| -| `sarif-file` |

Path to SARIF report file

| -| `cache-hit` |

Indicates if there was a cache hit

| +| name | description | +|---------------------|-----------------------------------------------------------------| +| `status` |

Overall status (success/failure)

| +| `files-checked` |

Number of files checked (check mode only)

| +| `unformatted-files` |

Number of files with formatting issues (check mode only)

| +| `sarif-file` |

Path to SARIF report file (check mode only)

| +| `files-changed` |

Number of files changed (fix mode only)

| ### Runs @@ -39,10 +42,16 @@ This action is a `composite` action. ### Usage ```yaml -- uses: ivuorinen/actions/prettier-check@main +- uses: ivuorinen/actions/prettier-lint@main with: + mode: + # Mode to run (check or fix) + # + # Required: false + # Default: check + working-directory: - # Directory containing files to check + # Directory containing files to format # # Required: false # Default: . @@ -78,13 +87,13 @@ This action is a `composite` action. # Default: true fail-on-error: - # Fail workflow if issues are found + # Fail workflow if issues are found (check mode only) # # Required: false # Default: true report-format: - # Output format (json, sarif) + # Output format for check mode (json, sarif) # # Required: false # Default: sarif @@ -101,15 +110,21 @@ This action is a `composite` action. # Required: false # Default: "" - check-only: - # Only check for formatting issues without fixing - # - # Required: false - # Default: true - token: # GitHub token for authentication # # Required: false # Default: "" + + username: + # GitHub username for commits (fix mode only) + # + # Required: false + # Default: github-actions + + email: + # GitHub email for commits (fix mode only) + # + # Required: false + # Default: github-actions@github.com ``` diff --git a/prettier-lint/action.yml b/prettier-lint/action.yml new file mode 100644 index 0000000..0d98485 --- /dev/null +++ b/prettier-lint/action.yml @@ -0,0 +1,393 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-action.json +# permissions: +# - contents: write # Required for fix mode to push changes +# - security-events: write # Required for check mode to upload SARIF +--- +name: Prettier Lint +description: 'Run Prettier in check or fix mode with advanced configuration and reporting' +author: Ismo Vuorinen + +branding: + icon: check-circle + color: green + +inputs: + mode: + description: 'Mode to run (check or fix)' + required: false + default: 'check' + working-directory: + description: 'Directory containing files to format' + required: false + default: '.' + prettier-version: + description: 'Prettier version to use' + required: false + default: 'latest' + config-file: + description: 'Path to Prettier config file' + required: false + default: '.prettierrc' + ignore-file: + description: 'Path to Prettier ignore file' + required: false + default: '.prettierignore' + file-pattern: + description: 'Files to include (glob pattern)' + required: false + default: '**/*.{js,jsx,ts,tsx,css,scss,json,md,yaml,yml}' + cache: + description: 'Enable Prettier caching' + required: false + default: 'true' + fail-on-error: + description: 'Fail workflow if issues are found (check mode only)' + required: false + default: 'true' + report-format: + description: 'Output format for check mode (json, sarif)' + required: false + default: 'sarif' + max-retries: + description: 'Maximum number of retry attempts' + required: false + default: '3' + plugins: + description: 'Comma-separated list of Prettier plugins to install' + required: false + default: '' + token: + description: 'GitHub token for authentication' + required: false + default: '' + username: + description: 'GitHub username for commits (fix mode only)' + required: false + default: 'github-actions' + email: + description: 'GitHub email for commits (fix mode only)' + required: false + default: 'github-actions@github.com' + +outputs: + status: + description: 'Overall status (success/failure)' + value: ${{ steps.check.outputs.status || steps.fix.outputs.status }} + files-checked: + description: 'Number of files checked (check mode only)' + value: ${{ steps.check.outputs.files_checked }} + unformatted-files: + description: 'Number of files with formatting issues (check mode only)' + value: ${{ steps.check.outputs.unformatted_files }} + sarif-file: + description: 'Path to SARIF report file (check mode only)' + value: ${{ steps.check.outputs.sarif_file }} + files-changed: + description: 'Number of files changed (fix mode only)' + value: ${{ steps.fix.outputs.files_changed }} + +runs: + using: composite + steps: + - name: Validate Inputs + id: validate + shell: bash + env: + MODE: ${{ inputs.mode }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + PRETTIER_VERSION: ${{ inputs.prettier-version }} + CONFIG_FILE: ${{ inputs.config-file }} + IGNORE_FILE: ${{ inputs.ignore-file }} + FILE_PATTERN: ${{ inputs.file-pattern }} + CACHE: ${{ inputs.cache }} + FAIL_ON_ERROR: ${{ inputs.fail-on-error }} + REPORT_FORMAT: ${{ inputs.report-format }} + MAX_RETRIES: ${{ inputs.max-retries }} + PLUGINS: ${{ inputs.plugins }} + EMAIL: ${{ inputs.email }} + USERNAME: ${{ inputs.username }} + run: | + set -euo pipefail + + # Validate mode + case "$MODE" in + "check"|"fix") + echo "Mode: $MODE" + ;; + *) + echo "::error::Invalid mode: '$MODE'. Must be 'check' or 'fix'" + exit 1 + ;; + esac + + # Validate working directory + if [ ! -d "$WORKING_DIRECTORY" ]; then + echo "::error::Working directory not found at '$WORKING_DIRECTORY'" + exit 1 + 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 + + # 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 + 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 + 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 + fi + + # Validate boolean inputs + validate_boolean() { + local value="$1" + local name="$2" + + case "${value,,}" in + true|false) + ;; + *) + echo "::error::Invalid boolean value for $name: '$value'. Expected: true or false" + exit 1 + ;; + esac + } + + validate_boolean "$CACHE" "cache" + validate_boolean "$FAIL_ON_ERROR" "fail-on-error" + + # Validate report format + case "$REPORT_FORMAT" in + json|sarif) + ;; + *) + echo "::error::Invalid report-format: '$REPORT_FORMAT'. Must be one of: json, sarif" + exit 1 + ;; + esac + + # Validate max retries + if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$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 + + username="$USERNAME" + + if [ ${#username} -gt 39 ]; then + echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters" + 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 + + if [[ "$username" == -* ]] || [[ "$username" == *- ]]; then + echo "::error::Invalid username '$username'. Cannot start or end with hyphen" + exit 1 + fi + + if [[ "$username" == *--* ]]; then + echo "::error::Invalid username '$username'. Consecutive hyphens 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: Node Setup + id: node-setup + uses: ivuorinen/actions/node-setup@0fa9a68f07a1260b321f814202658a6089a43d42 + + - name: Cache Node Dependencies + id: cache + uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 + 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 }}' + + - name: Install Dependencies + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + env: + PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + run: | + set -euo pipefail + + echo "Installing dependencies using $PACKAGE_MANAGER..." + + case "$PACKAGE_MANAGER" in + "pnpm") + pnpm install --frozen-lockfile + ;; + "yarn") + if [ -f ".yarnrc.yml" ]; then + yarn install --immutable + else + yarn install --frozen-lockfile + fi + ;; + "bun") + bun install --frozen-lockfile + ;; + "npm"|*) + npm ci + ;; + esac + + echo "โœ… Dependencies installed successfully" + + - name: Install Prettier Plugins + if: inputs.plugins != '' + shell: bash + env: + PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + PLUGINS: ${{ inputs.plugins }} + run: | + set -euo pipefail + + echo "Installing Prettier plugins: $PLUGINS" + + # Convert comma-separated list to space-separated + plugins_list=$(echo "$PLUGINS" | tr ',' ' ') + + case "$PACKAGE_MANAGER" in + "pnpm") + pnpm add -D $plugins_list + ;; + "yarn") + yarn add -D $plugins_list + ;; + "bun") + bun add -D $plugins_list + ;; + "npm"|*) + npm install --save-dev $plugins_list + ;; + esac + + echo "โœ… Plugins installed successfully" + + - name: Run Prettier Check + if: inputs.mode == 'check' + id: check + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + PACKAGE_MANAGER: ${{ steps.node-setup.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 + + echo "Running Prettier check mode..." + + # Build Prettier command + prettier_cmd="npx prettier --check \"$FILE_PATTERN\"" + + # Add config file if specified and exists + if [ "$CONFIG_FILE" != ".prettierrc" ] && [ -f "$CONFIG_FILE" ]; then + prettier_cmd="$prettier_cmd --config $CONFIG_FILE" + fi + + # Add cache option + if [ "$CACHE" = "true" ]; then + prettier_cmd="$prettier_cmd --cache" + fi + + # Run Prettier and capture results + prettier_exit_code=0 + files_checked=0 + unformatted_files=0 + + if eval "$prettier_cmd" > prettier-output.txt 2>&1; then + prettier_exit_code=0 + echo "status=success" >> "$GITHUB_OUTPUT" + else + prettier_exit_code=$? + unformatted_files=$(grep -c "^" prettier-output.txt 2>/dev/null || echo "0") + echo "status=failure" >> "$GITHUB_OUTPUT" + fi + + # Count total files checked + files_checked=$(eval "npx prettier --list-different \"$FILE_PATTERN\"" 2>/dev/null | wc -l | tr -d ' ' || echo "0") + + echo "files_checked=$files_checked" >> "$GITHUB_OUTPUT" + echo "unformatted_files=$unformatted_files" >> "$GITHUB_OUTPUT" + echo "sarif_file=prettier-results.sarif" >> "$GITHUB_OUTPUT" + + echo "โœ… Prettier check completed: $unformatted_files unformatted files out of $files_checked checked" + + # Exit with prettier's exit code if fail-on-error is true + if [ "$FAIL_ON_ERROR" = "true" ]; then + exit $prettier_exit_code + fi + + - name: Run Prettier Fix + if: inputs.mode == 'fix' + id: fix + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }} + FILE_PATTERN: ${{ inputs.file-pattern }} + run: | + set -euo pipefail + + echo "Running Prettier fix mode..." + + # Count files before fix + files_before=$(git status --porcelain | wc -l | tr -d ' ') + + # Run Prettier fix + npx prettier --write "$FILE_PATTERN" || true + + # Count files after fix + files_after=$(git status --porcelain | wc -l | tr -d ' ') + files_changed=$((files_after - files_before)) + + printf '%s\n' "files_changed=$files_changed" >> "$GITHUB_OUTPUT" + printf '%s\n' "status=success" >> "$GITHUB_OUTPUT" + + echo "โœ… Prettier fix completed. Files changed: $files_changed" + + - name: Commit and Push Fixes + if: inputs.mode == 'fix' && success() + uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 + with: + commit_message: 'style: autofix Prettier formatting' + commit_user_name: ${{ inputs.username }} + commit_user_email: ${{ inputs.email }} + add_options: '-u' diff --git a/prettier-check/rules.yml b/prettier-lint/rules.yml similarity index 69% rename from prettier-check/rules.yml rename to prettier-lint/rules.yml index b2a990b..f0b820e 100644 --- a/prettier-check/rules.yml +++ b/prettier-lint/rules.yml @@ -1,52 +1,54 @@ --- -# Validation rules for prettier-check action +# Validation rules for prettier-lint action # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Schema version: 1.0 -# Coverage: 100% (12/12 inputs) +# Coverage: 86% (12/14 inputs) # -# This file defines validation rules for the prettier-check GitHub Action. +# This file defines validation rules for the prettier-lint GitHub Action. # Rules are automatically applied by validate-inputs action when this # action is used. # schema_version: '1.0' -action: prettier-check -description: Run Prettier check on the repository with advanced configuration and reporting +action: prettier-lint +description: Run Prettier in check or fix mode with advanced configuration and reporting generator_version: 1.0.0 required_inputs: [] optional_inputs: - cache - - check-only - config-file + - email - fail-on-error - file-pattern - ignore-file - max-retries + - mode - plugins - prettier-version - report-format - token + - username - working-directory conventions: cache: boolean - check-only: boolean config-file: file_path + email: email fail-on-error: boolean - file-pattern: file_pattern ignore-file: file_path max-retries: numeric_range_1_10 - plugins: plugin_list + mode: mode_enum prettier-version: semantic_version report-format: report_format token: github_token + username: username working-directory: file_path overrides: {} statistics: - total_inputs: 12 + total_inputs: 14 validated_inputs: 12 skipped_inputs: 0 - coverage_percentage: 100 -validation_coverage: 100 + coverage_percentage: 86 +validation_coverage: 86 auto_detected: true manual_review_required: false quality_indicators: diff --git a/pyproject.toml b/pyproject.toml index 2865a25..69a3c7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,15 @@ target-version = "py310" line-length = 100 indent-width = 4 +# Exclude directories from linting +exclude = [ + ".git", + ".venv", + "node_modules", + ".worktrees", + "__pycache__", +] + [tool.ruff.lint] # Enable comprehensive rule sets select = [ diff --git a/python-lint-fix/action.yml b/python-lint-fix/action.yml index 4de1c37..56f4b81 100644 --- a/python-lint-fix/action.yml +++ b/python-lint-fix/action.yml @@ -64,89 +64,18 @@ runs: steps: - name: Validate Inputs id: validate - shell: bash - env: - PYTHON_VERSION: ${{ inputs.python-version }} - FLAKE8_VERSION: ${{ inputs.flake8-version }} - AUTOPEP8_VERSION: ${{ inputs.autopep8-version }} - WORKING_DIRECTORY: ${{ inputs.working-directory }} - MAX_RETRIES: ${{ inputs.max-retries }} - FAIL_ON_ERROR: ${{ inputs.fail-on-error }} - EMAIL: ${{ inputs.email }} - USERNAME: ${{ inputs.username }} - GITHUB_TOKEN: ${{ inputs.token }} - run: | - set -euo pipefail - - # Validate Python version format - if ! [[ "$PYTHON_VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then - echo "::error::Invalid python-version: '$PYTHON_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 3.11, 3.11.5)" - exit 1 - fi - - # Validate flake8 version format (semantic versioning) - if ! [[ "$FLAKE8_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::Invalid flake8-version: '$FLAKE8_VERSION'. Expected semantic version (e.g., 7.0.0)" - exit 1 - fi - - # Validate autopep8 version format (semantic versioning) - if ! [[ "$AUTOPEP8_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then - echo "::error::Invalid autopep8-version: '$AUTOPEP8_VERSION'. Expected semantic version (e.g., 2.0.4)" - exit 1 - fi - - # Validate working directory - if [ ! -d "$WORKING_DIRECTORY" ]; then - echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Directory does not exist" - exit 1 - fi - - # Validate path security (prevent path traversal) - if [[ "$WORKING_DIRECTORY" == *".."* ]]; then - echo "::error::Invalid working-directory: '$WORKING_DIRECTORY'. Path traversal not allowed" - exit 1 - fi - - # Validate max-retries (positive integer) - if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ]; then - echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer" - exit 1 - fi - - # Validate max-retries range - if [ "$MAX_RETRIES" -gt 10 ]; then - echo "::error::Invalid max-retries: '$MAX_RETRIES'. Maximum allowed is 10" - exit 1 - fi - - # Validate boolean inputs - case "$FAIL_ON_ERROR" in - true|false) ;; - *) - echo "::error::Invalid fail-on-error value: '$FAIL_ON_ERROR'. Expected: true or false" - 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 - - # 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 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 - fi + uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 + with: + action-type: 'python-lint-fix' + token: ${{ inputs.token }} + email: ${{ inputs.email }} + username: ${{ inputs.username }} + python-version: ${{ inputs.python-version }} + flake8-version: ${{ inputs.flake8-version }} + autopep8-version: ${{ inputs.autopep8-version }} + working-directory: ${{ inputs.working-directory }} + max-retries: ${{ inputs.max-retries }} + fail-on-error: ${{ inputs.fail-on-error }} - name: Checkout Repository uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta @@ -155,14 +84,15 @@ runs: - name: Detect Python Version id: python-version - uses: ivuorinen/actions/python-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 + uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 with: + language: 'python' default-version: ${{ inputs.python-version }} - name: Setup Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: - python-version: ${{ steps.python-version.outputs.python-version }} + python-version: ${{ steps.python-version.outputs.detected-version }} cache: 'pip' cache-dependency-path: | **/requirements.txt @@ -172,19 +102,19 @@ runs: - name: Check for Python Files id: check-files - shell: bash + shell: sh env: WORKING_DIRECTORY: ${{ inputs.working-directory }} run: | - set -euo pipefail + set -eu cd "$WORKING_DIRECTORY" if ! find . -name "*.py" -type f -not -path "*/\.*" | grep -q .; then echo "No Python files found. Skipping lint and fix." - echo "result=skipped" >> $GITHUB_OUTPUT + printf '%s\n' "result=skipped" >> "$GITHUB_OUTPUT" exit 0 fi - echo "result=found" >> $GITHUB_OUTPUT + printf '%s\n' "result=found" >> "$GITHUB_OUTPUT" - name: Cache Python Dependencies if: steps.check-files.outputs.result == 'found' @@ -199,45 +129,22 @@ runs: - name: Install Dependencies if: steps.check-files.outputs.result == 'found' && steps.cache-pip.outputs.cache-hit != 'true' id: install - shell: bash + shell: sh env: MAX_RETRIES: ${{ inputs.max-retries }} FLAKE8_VERSION: ${{ inputs.flake8-version }} AUTOPEP8_VERSION: ${{ inputs.autopep8-version }} run: | - set -euo pipefail - - function install_with_retry() { - local package=$1 - local version=$2 - local attempt=1 - local max_attempts="$MAX_RETRIES" - - while [ $attempt -le $max_attempts ]; do - echo "Installing $package==$version (Attempt $attempt of $max_attempts)" - if pip install "$package==$version"; then - return 0 - fi - - attempt=$((attempt + 1)) - if [ $attempt -le $max_attempts ]; then - echo "Installation failed, waiting 5 seconds before retry..." - sleep 5 - fi - done - - echo "::error::Failed to install $package after $max_attempts attempts" - return 1 - } + set -eu # Create virtual environment python -m venv .venv - source .venv/bin/activate + . .venv/bin/activate - # Install dependencies with retry logic - install_with_retry flake8 "$FLAKE8_VERSION" - install_with_retry flake8-sarif 0.6.0 - install_with_retry autopep8 "$AUTOPEP8_VERSION" + # Install dependencies (pip has built-in retry logic) + pip install "flake8==$FLAKE8_VERSION" + pip install flake8-sarif==0.6.0 + pip install "autopep8==$AUTOPEP8_VERSION" # Verify installations flake8 --version || exit 1 @@ -245,31 +152,31 @@ runs: - name: Activate Virtual Environment (Cache Hit) if: steps.check-files.outputs.result == 'found' && steps.cache-pip.outputs.cache-hit == 'true' - shell: bash + shell: sh env: FLAKE8_VERSION: ${{ inputs.flake8-version }} AUTOPEP8_VERSION: ${{ inputs.autopep8-version }} run: | - set -euo pipefail + set -eu # Create virtual environment if it doesn't exist from cache if [ ! -d ".venv" ]; then python -m venv .venv - source .venv/bin/activate + . .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 - shell: bash + shell: sh env: WORKING_DIRECTORY: ${{ inputs.working-directory }} FAIL_ON_ERROR: ${{ inputs.fail-on-error }} run: | - set -euo pipefail + set -eu - source .venv/bin/activate + . .venv/bin/activate cd "$WORKING_DIRECTORY" # Create temporary directory for reports @@ -280,28 +187,28 @@ runs: if ! flake8 --format=sarif --output-file=reports/flake8.sarif .; then error_count=$(grep -c "level\": \"error\"" reports/flake8.sarif || echo 0) echo "Found $error_count linting errors" - echo "error_count=$error_count" >> $GITHUB_OUTPUT + printf '%s\n' "error_count=$error_count" >> "$GITHUB_OUTPUT" - if [[ "$FAIL_ON_ERROR" == "true" ]]; then + if [ "$FAIL_ON_ERROR" = "true" ]; then echo "::error::Linting failed with $error_count errors" - echo "result=failure" >> $GITHUB_OUTPUT + printf '%s\n' "result=failure" >> "$GITHUB_OUTPUT" exit 1 fi fi - echo "result=success" >> $GITHUB_OUTPUT - echo "error_count=$error_count" >> $GITHUB_OUTPUT + printf '%s\n' "result=success" >> "$GITHUB_OUTPUT" + printf '%s\n' "error_count=$error_count" >> "$GITHUB_OUTPUT" - name: Run autopep8 Fix if: steps.check-files.outputs.result == 'found' id: fix - shell: bash + shell: sh env: WORKING_DIRECTORY: ${{ inputs.working-directory }} run: | - set -euo pipefail + set -eu - source .venv/bin/activate + . .venv/bin/activate cd "$WORKING_DIRECTORY" # Create temporary file for tracking changes @@ -318,55 +225,19 @@ runs: # Count fixed files fixed_count=$(wc -l < /tmp/changed_files || echo 0) echo "Fixed $fixed_count files" - echo "fixed_count=$fixed_count" >> $GITHUB_OUTPUT + printf '%s\n' "fixed_count=$fixed_count" >> "$GITHUB_OUTPUT" # Cleanup rm /tmp/changed_files - - name: Set Git Config for Fixes - if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }} - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} - - name: Commit Fixes if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }} - shell: bash - env: - WORKING_DIRECTORY: ${{ inputs.working-directory }} - MAX_RETRIES: ${{ inputs.max-retries }} - FIXED_COUNT: ${{ steps.fix.outputs.fixed_count }} - run: | - set -euo pipefail - - cd "$WORKING_DIRECTORY" - - # Commit changes with retry logic - attempt=1 - max_attempts="$MAX_RETRIES" - - while [ $attempt -le $max_attempts ]; do - echo "Attempting to commit and push changes (Attempt $attempt of $max_attempts)" - - git add . - git commit -m "fix: applied python lint fixes to $FIXED_COUNT files" - - if git pull --rebase && git push; then - echo "Successfully pushed changes" - break - fi - - attempt=$((attempt + 1)) - if [ $attempt -le $max_attempts ]; then - echo "Push failed, waiting 5 seconds before retry..." - sleep 5 - else - echo "::error::Failed to push changes after $max_attempts attempts" - exit 1 - fi - done + uses: stefanzweifel/git-auto-commit-action@be7095c202abcf573b09f20541e0ee2f6a3a9d9b # v5.0.1 + with: + commit_message: 'style: apply python lint fixes' + commit_user_name: ${{ inputs.username }} + commit_user_email: ${{ inputs.email }} + file_pattern: '*.py' - name: Upload SARIF Report if: steps.check-files.outputs.result == 'found' @@ -377,9 +248,9 @@ runs: - name: Cleanup if: always() - shell: bash + shell: sh run: |- - set -euo pipefail + set -eu # Remove virtual environment rm -rf .venv diff --git a/python-version-detect-v2/CustomValidator.py b/python-version-detect-v2/CustomValidator.py deleted file mode 100755 index 8e47204..0000000 --- a/python-version-detect-v2/CustomValidator.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for python-version-detect-v2 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 python-version-detect-v2 action.""" - - def __init__(self, action_type: str = "python-version-detect-v2") -> None: - """Initialize python-version-detect-v2 validator.""" - super().__init__(action_type) - self.version_validator = VersionValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate python-version-detect-v2 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("Python version cannot be empty") - valid = False - elif value: - # Use the Python version validator which handles version ranges - result = self.version_validator.validate_python_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 - - 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": "python_version", - "required": False, - "description": "Default Python version to use", - } - } diff --git a/python-version-detect-v2/README.md b/python-version-detect-v2/README.md deleted file mode 100644 index 535007d..0000000 --- a/python-version-detect-v2/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# ivuorinen/actions/python-version-detect-v2 - -## Python Version Detect v2 - -### Description - -Detects Python version from project configuration files using enhanced detection logic. - -### Inputs - -| name | description | required | default | -|-------------------|-----------------------------------------------------------------|----------|---------| -| `default-version` |

Default Python version to use if no version is detected.

| `false` | `3.12` | -| `token` |

GitHub token for authentication

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

Detected or default Python version.

| -| `package-manager` |

Detected Python package manager (pip, poetry, pipenv).

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/python-version-detect-v2@main - with: - default-version: - # Default Python version to use if no version is detected. - # - # Required: false - # Default: 3.12 - - token: - # GitHub token for authentication - # - # Required: false - # Default: "" -``` diff --git a/python-version-detect-v2/action.yml b/python-version-detect-v2/action.yml deleted file mode 100644 index e784720..0000000 --- a/python-version-detect-v2/action.yml +++ /dev/null @@ -1,82 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for reading version files ---- -name: Python Version Detect v2 -description: 'Detects Python version from project configuration files using enhanced detection logic.' -author: 'Ismo Vuorinen' - -branding: - icon: code - color: blue - -inputs: - default-version: - description: 'Default Python version to use if no version is detected.' - required: false - default: '3.12' - token: - description: 'GitHub token for authentication' - required: false - default: '' - -outputs: - python-version: - description: 'Detected or default Python version.' - value: ${{ steps.parse-version.outputs.detected-version }} - package-manager: - description: 'Detected Python package manager (pip, poetry, pipenv).' - value: ${{ steps.parse-version.outputs.package-manager }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: sh - env: - DEFAULT_VERSION: ${{ inputs.default-version }} - run: | - set -eu - - # Validate default-version format - case "$DEFAULT_VERSION" in - [0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*) - ;; - *) - echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 3.12, 3.11.5)" - exit 1 - ;; - esac - - # Check for reasonable version range (prevent malicious inputs) - major_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f1) - if [ "$major_version" -ne 3 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. Python major version should be 3" - exit 1 - fi - - # Check minor version range for Python 3 - minor_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f2) - if [ "$minor_version" -lt 8 ] || [ "$minor_version" -gt 15 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. Python 3 minor version should be between 8 and 15" - exit 1 - fi - - echo "Input validation completed successfully" - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token || github.token }} - - - name: Parse Python Version - id: parse-version - uses: ivuorinen/actions/version-file-parser@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'python' - tool-versions-key: 'python' - dockerfile-image: 'python' - version-file: '.python-version' - validation-regex: '^[0-9]+\.[0-9]+(\.[0-9]+)?$' - default-version: ${{ inputs.default-version }} diff --git a/python-version-detect-v2/rules.yml b/python-version-detect-v2/rules.yml deleted file mode 100644 index 2071ca3..0000000 --- a/python-version-detect-v2/rules.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -# Validation rules for python-version-detect-v2 action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (2/2 inputs) -# -# This file defines validation rules for the python-version-detect-v2 GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: python-version-detect-v2 -description: Detects Python version from project configuration files using enhanced detection logic. -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - default-version - - token -conventions: - default-version: semantic_version - token: github_token -overrides: - default-version: python_version -statistics: - total_inputs: 2 - validated_inputs: 2 - 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: false - has_security_validation: true diff --git a/python-version-detect/CustomValidator.py b/python-version-detect/CustomValidator.py deleted file mode 100755 index 31144c7..0000000 --- a/python-version-detect/CustomValidator.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for python-version-detect 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 python-version-detect action.""" - - def __init__(self, action_type: str = "python-version-detect") -> None: - """Initialize python-version-detect validator.""" - super().__init__(action_type) - self.version_validator = VersionValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate python-version-detect 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("Python version cannot be empty") - valid = False - elif value: - # Use the Python version validator which handles version ranges - result = self.version_validator.validate_python_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 - - 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": "python_version", - "required": False, - "description": "Default Python version to use", - } - } diff --git a/python-version-detect/README.md b/python-version-detect/README.md deleted file mode 100644 index d80c12e..0000000 --- a/python-version-detect/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# ivuorinen/actions/python-version-detect - -## Python Version Detect - -### Description - -Detects Python version from project configuration files or defaults to a specified version. - -### Inputs - -| name | description | required | default | -|-------------------|-----------------------------------------------------------------|----------|---------| -| `default-version` |

Default Python version to use if no version is detected.

| `false` | `3.12` | -| `token` |

GitHub token for authentication

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

Detected or default Python version.

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/python-version-detect@main - with: - default-version: - # Default Python version to use if no version is detected. - # - # Required: false - # Default: 3.12 - - token: - # GitHub token for authentication - # - # Required: false - # Default: "" -``` diff --git a/python-version-detect/action.yml b/python-version-detect/action.yml deleted file mode 100644 index 912d864..0000000 --- a/python-version-detect/action.yml +++ /dev/null @@ -1,75 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: read # Required for reading version files ---- -name: Python Version Detect -description: 'Detects Python version from project configuration files or defaults to a specified version.' -author: 'Ismo Vuorinen' - -branding: - icon: code - color: blue - -inputs: - default-version: - description: 'Default Python version to use if no version is detected.' - required: false - default: '3.12' - token: - description: 'GitHub token for authentication' - required: false - default: '' - -outputs: - python-version: - description: 'Detected or default Python version.' - value: ${{ steps.parse-version.outputs.detected-version }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate - shell: bash - env: - DEFAULT_VERSION: ${{ inputs.default-version }} - run: | - set -euo pipefail - - # Validate default-version format - if ! [[ "$DEFAULT_VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then - echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 3.12, 3.11.5)" - exit 1 - fi - - # Check for reasonable version range (prevent malicious inputs) - major_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f1) - if [ "$major_version" -ne 3 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. Python major version should be 3" - exit 1 - fi - - # Check minor version range for Python 3 - minor_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f2) - if [ "$minor_version" -lt 8 ] || [ "$minor_version" -gt 15 ]; then - echo "::error::Invalid default-version: '$DEFAULT_VERSION'. Python 3 minor version should be between 8 and 15" - exit 1 - fi - - echo "Input validation completed successfully" - - - name: Checkout Repository - uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - with: - token: ${{ inputs.token || github.token }} - - - name: Parse Python Version - id: parse-version - uses: ivuorinen/actions/version-file-parser@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - language: 'python' - tool-versions-key: 'python' - dockerfile-image: 'python' - version-file: '.python-version' - validation-regex: '^[0-9]+\.[0-9]+(\.[0-9]+)?$' - default-version: ${{ inputs.default-version }} diff --git a/python-version-detect/rules.yml b/python-version-detect/rules.yml deleted file mode 100644 index d1e7c18..0000000 --- a/python-version-detect/rules.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -# Validation rules for python-version-detect action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 100% (2/2 inputs) -# -# This file defines validation rules for the python-version-detect GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: python-version-detect -description: Detects Python version from project configuration files or defaults to a specified version. -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - default-version - - token -conventions: - default-version: semantic_version - token: github_token -overrides: - default-version: python_version -statistics: - total_inputs: 2 - validated_inputs: 2 - 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: false - has_security_validation: true diff --git a/set-git-config/CustomValidator.py b/set-git-config/CustomValidator.py deleted file mode 100755 index f909172..0000000 --- a/set-git-config/CustomValidator.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for set-git-config 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.network import NetworkValidator -from validators.token import TokenValidator - - -class CustomValidator(BaseValidator): - """Custom validator for set-git-config action.""" - - def __init__(self, action_type: str = "set-git-config") -> None: - """Initialize set-git-config validator.""" - super().__init__(action_type) - self.network_validator = NetworkValidator() - self.token_validator = TokenValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate set-git-config action inputs.""" - valid = True - # No required inputs - # Validate optional input: email - if inputs.get("email"): - result = self.network_validator.validate_email(inputs["email"], "email") - for error in self.network_validator.errors: - if error not in self.errors: - self.add_error(error) - self.network_validator.clear_errors() - if not result: - valid = False - # Validate optional input: token - if inputs.get("token"): - result = self.token_validator.validate_github_token(inputs["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 - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs.""" - return [] - - 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/set-git-config/README.md b/set-git-config/README.md deleted file mode 100644 index 296e30a..0000000 --- a/set-git-config/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# ivuorinen/actions/set-git-config - -## Set Git Config - -### Description - -Sets Git configuration for actions. - -### Inputs - -| name | description | required | default | -|--------------|----------------------------------------|----------|-----------------------------| -| `token` |

GitHub token for authentication

| `false` | `${{ github.token }}` | -| `username` |

GitHub username for commits.

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

GitHub email for commits.

| `false` | `github-actions@github.com` | -| `is_fiximus` |

Whether to use the Fiximus bot.

| `false` | `false` | - -### Outputs - -| name | description | -|--------------|----------------------------------------| -| `token` |

GitHub token.

| -| `username` |

GitHub username for commits.

| -| `email` |

GitHub email for commits.

| -| `is_fiximus` |

Whether to use the Fiximus bot.

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/set-git-config@main - with: - token: - # GitHub token for authentication - # - # Required: false - # Default: ${{ github.token }} - - username: - # GitHub username for commits. - # - # Required: false - # Default: github-actions - - email: - # GitHub email for commits. - # - # Required: false - # Default: github-actions@github.com - - is_fiximus: - # Whether to use the Fiximus bot. - # - # Required: false - # Default: false -``` diff --git a/set-git-config/action.yml b/set-git-config/action.yml deleted file mode 100644 index 1d3dc71..0000000 --- a/set-git-config/action.yml +++ /dev/null @@ -1,102 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - contents: write # Required for git configuration and operations ---- -name: Set Git Config -description: 'Sets Git configuration for actions.' -author: 'Ismo Vuorinen' - -branding: - icon: git-commit - color: gray-dark - -inputs: - token: - description: 'GitHub token for authentication' - required: false - default: ${{ github.token }} - username: - description: 'GitHub username for commits.' - default: 'github-actions' - email: - description: 'GitHub email for commits.' - default: 'github-actions@github.com' - is_fiximus: - description: 'Whether to use the Fiximus bot.' - required: false - default: 'false' - -outputs: - token: - description: 'GitHub token.' - value: ${{ steps.bot.outputs.token }} - username: - description: 'GitHub username for commits.' - value: ${{ steps.bot.outputs.username }} - email: - description: 'GitHub email for commits.' - value: ${{ steps.bot.outputs.email }} - is_fiximus: - description: 'Whether to use the Fiximus bot.' - value: ${{ steps.bot.outputs.is_fiximus }} - -runs: - using: composite - steps: - - name: Check for FIXIMUS_TOKEN - id: bot - shell: bash - env: - INPUT_TOKEN: ${{ inputs.token }} - INPUT_USERNAME: ${{ inputs.username }} - INPUT_EMAIL: ${{ inputs.email }} - INPUT_IS_FIXIMUS: ${{ inputs.is_fiximus }} - run: | - set -euo pipefail - - # Use printf to safely write outputs (prevents injection) - printf 'token=%s\n' "${INPUT_TOKEN}" >> "$GITHUB_OUTPUT" - printf 'username=%s\n' "${INPUT_USERNAME}" >> "$GITHUB_OUTPUT" - printf 'email=%s\n' "${INPUT_EMAIL}" >> "$GITHUB_OUTPUT" - printf 'is_fiximus=%s\n' "${INPUT_IS_FIXIMUS}" >> "$GITHUB_OUTPUT" - - # Determine final values - FINAL_TOKEN="$INPUT_TOKEN" - FINAL_USERNAME="$INPUT_USERNAME" - FINAL_EMAIL="$INPUT_EMAIL" - - if [ "$INPUT_IS_FIXIMUS" != "false" ]; then - FINAL_USERNAME="fiximus" - FINAL_EMAIL="github-bot@ivuorinen.net" - printf 'username=%s\n' "fiximus" >> "$GITHUB_OUTPUT" - printf 'email=%s\n' "github-bot@ivuorinen.net" >> "$GITHUB_OUTPUT" - fi - - # Write validated values to GITHUB_ENV for safe use in subsequent steps - { - echo "VALIDATED_GIT_TOKEN=$FINAL_TOKEN" - echo "VALIDATED_GIT_USERNAME=$FINAL_USERNAME" - echo "VALIDATED_GIT_EMAIL=$FINAL_EMAIL" - } >> "$GITHUB_ENV" - - - name: Configure Git - shell: bash - run: |- - set -euo pipefail - # Use validated environment variables from GITHUB_ENV - GITHUB_TOKEN="$VALIDATED_GIT_TOKEN" - GIT_USERNAME="$VALIDATED_GIT_USERNAME" - GIT_EMAIL="$VALIDATED_GIT_EMAIL" - - # Store token in variable to avoid repeated exposure - TOKEN="$GITHUB_TOKEN" - - git config --local --unset-all http.https://github.com/.extraheader || true - git config --local \ - --add "url.https://x-access-token:${TOKEN}@github.com/.insteadOf" \ - "https://github.com/" - git config --local \ - --add "url.https://x-access-token:${TOKEN}@github.com/.insteadOf" \ - 'git@github.com:' - git config --local user.name "$GIT_USERNAME" - git config --local user.email "$GIT_EMAIL" diff --git a/set-git-config/rules.yml b/set-git-config/rules.yml deleted file mode 100644 index fd61444..0000000 --- a/set-git-config/rules.yml +++ /dev/null @@ -1,40 +0,0 @@ ---- -# Validation rules for set-git-config action -# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY -# Schema version: 1.0 -# Coverage: 75% (3/4 inputs) -# -# This file defines validation rules for the set-git-config GitHub Action. -# Rules are automatically applied by validate-inputs action when this -# action is used. -# - -schema_version: '1.0' -action: set-git-config -description: Sets Git configuration for actions. -generator_version: 1.0.0 -required_inputs: [] -optional_inputs: - - email - - is_fiximus - - token - - username -conventions: - email: email - token: github_token - username: username -overrides: {} -statistics: - total_inputs: 4 - validated_inputs: 3 - skipped_inputs: 0 - coverage_percentage: 75 -validation_coverage: 75 -auto_detected: true -manual_review_required: true -quality_indicators: - has_required_inputs: false - has_token_validation: true - has_version_validation: false - has_file_validation: false - has_security_validation: true diff --git a/terraform-lint-fix/action.yml b/terraform-lint-fix/action.yml index 79e170a..575f619 100644 --- a/terraform-lint-fix/action.yml +++ b/terraform-lint-fix/action.yml @@ -89,7 +89,7 @@ runs: max-retries: ${{ inputs.max-retries }} - name: Write Validated Inputs to Environment - shell: bash + shell: sh env: INPUT_WORKING_DIR: ${{ inputs.working-directory }} INPUT_CONFIG: ${{ inputs.config-file }} @@ -97,7 +97,7 @@ runs: INPUT_FAIL: ${{ inputs.fail-on-error }} INPUT_RETRIES: ${{ inputs.max-retries }} run: | - set -euo pipefail + set -eu # Write validated inputs to GITHUB_ENV for safe use in shell contexts { echo "VALIDATED_WORKING_DIR=$INPUT_WORKING_DIR" @@ -109,9 +109,9 @@ runs: - name: Check for Terraform Files id: check-files - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Use validated environment variable WORKING_DIRECTORY="$VALIDATED_WORKING_DIR" @@ -120,11 +120,11 @@ runs: # Check for Terraform files if ! find . -name "*.tf" -o -name "*.tfvars" | grep -q .; then echo "No Terraform files found. Skipping lint and fix." - echo "found=false" >> $GITHUB_OUTPUT + printf '%s\n' "found=false" >> "$GITHUB_OUTPUT" exit 0 fi - echo "found=true" >> $GITHUB_OUTPUT + printf '%s\n' "found=true" >> "$GITHUB_OUTPUT" - name: Setup Terraform if: steps.check-files.outputs.found == 'true' @@ -135,9 +135,9 @@ runs: - name: Validate Terraform Syntax if: steps.check-files.outputs.found == 'true' - shell: bash + shell: sh run: | - set -euo pipefail + set -eu echo "Validating Terraform file syntax..." for file in $(find . -name "*.tf" -o -name "*.tfvars"); do if ! terraform fmt -check=true "$file" >/dev/null 2>&1; then @@ -145,48 +145,25 @@ runs: fi done - - name: Install TFLint + - name: Setup TFLint if: steps.check-files.outputs.found == 'true' - shell: bash + uses: terraform-linters/setup-tflint@19a52fbac37dacb22a09518e4ef6ee234f2d4987 # v4.0.0 + with: + tflint_version: ${{ inputs.tflint-version }} + + - name: Initialize TFLint + if: steps.check-files.outputs.found == 'true' + shell: sh run: | - set -euo pipefail - # Use validated environment variable - MAX_RETRIES="$VALIDATED_RETRIES" - - # Function to install TFLint with retries - install_tflint() { - local attempt=1 - local max_attempts="$MAX_RETRIES" - - while [ $attempt -le $max_attempts ]; do - echo "Installing TFLint (Attempt $attempt of $max_attempts)" - - if curl -sSL "https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh" | bash; then - echo "TFLint installed successfully" - return 0 - fi - - attempt=$((attempt + 1)) - if [ $attempt -le $max_attempts ]; then - echo "Installation failed, waiting 10 seconds before retry..." - sleep 10 - fi - done - - echo "::error::Failed to install TFLint after $max_attempts attempts" - return 1 - } - - install_tflint - + set -eu # Initialize TFLint plugins tflint --init - name: Configure TFLint if: steps.check-files.outputs.found == 'true' - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Use validated environment variable CONFIG_FILE="$VALIDATED_CONFIG" @@ -213,9 +190,9 @@ runs: - name: Run TFLint if: steps.check-files.outputs.found == 'true' id: lint - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Use validated environment variables WORKING_DIRECTORY="$VALIDATED_WORKING_DIR" CONFIG_FILE="$VALIDATED_CONFIG" @@ -234,7 +211,7 @@ runs: --no-color \ . > "$tflint_output"; then error_count=$(grep -c "level\": \"error\"" "$tflint_output" || echo 0) - echo "error_count=$error_count" >> $GITHUB_OUTPUT + printf '%s\n' "error_count=$error_count" >> "$GITHUB_OUTPUT" if [[ "$FAIL_ON_ERROR" == "true" ]]; then echo "::error::Found $error_count linting errors" @@ -242,14 +219,14 @@ runs: fi fi - echo "sarif_file=$tflint_output" >> $GITHUB_OUTPUT + printf '%s\n' "sarif_file=$tflint_output" >> "$GITHUB_OUTPUT" - name: Run Terraform Format if: steps.check-files.outputs.found == 'true' && inputs.auto-fix == 'true' id: fix - shell: bash + shell: sh run: | - set -euo pipefail + set -eu # Use validated environment variable WORKING_DIRECTORY="$VALIDATED_WORKING_DIR" @@ -266,39 +243,16 @@ runs: fi done - echo "fixed_count=$fixed_count" >> $GITHUB_OUTPUT - - - name: Set Git Config for Fixes - if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }} - uses: ivuorinen/actions/set-git-config@0fa9a68f07a1260b321f814202658a6089a43d42 - with: - token: ${{ inputs.token || github.token }} - username: ${{ inputs.username }} - email: ${{ inputs.email }} + printf '%s\n' "fixed_count=$fixed_count" >> "$GITHUB_OUTPUT" - name: Commit Fixes if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }} - shell: bash - env: - FIXED_COUNT: ${{ steps.fix.outputs.fixed_count }} - run: | - set -euo pipefail - # Use validated environment variable and output - WORKING_DIRECTORY="$VALIDATED_WORKING_DIR" - - cd "$WORKING_DIRECTORY" - - if git diff --quiet; then - echo "No changes to commit." - else - git add . - git commit -m "fix: applied terraform formatting fixes to $FIXED_COUNT files" - git push || { - echo "Push failed, pulling latest changes..." - git pull --rebase - git push - } - fi + uses: stefanzweifel/git-auto-commit-action@be7095c202abcf573b09f20541e0ee2f6a3a9d9b # v5.0.1 + with: + commit_message: 'style: apply terraform formatting fixes' + commit_user_name: ${{ inputs.username }} + commit_user_email: ${{ inputs.email }} + file_pattern: '*.tf *.tfvars' - name: Upload SARIF Report if: steps.check-files.outputs.found == 'true' && inputs.format == 'sarif' @@ -309,9 +263,9 @@ runs: - name: Cleanup if: always() - shell: bash + shell: sh run: |- - set -euo pipefail + set -eu # Remove temporary files rm -rf .terraform/ diff --git a/validate-inputs/README.md b/validate-inputs/README.md index 8bdbade..371e406 100644 --- a/validate-inputs/README.md +++ b/validate-inputs/README.md @@ -8,56 +8,56 @@ Centralized Python-based input validation for GitHub Actions with PCRE regex sup ### Inputs -| name | description | required | default | -|---------------------|------------------------------------------------------------------------------------|----------|---------| -| `action` |

Action name to validate (alias for action-type)

| `false` | `""` | -| `action-type` |

Type of action to validate (e.g., csharp-publish, docker-build, eslint-fix)

| `false` | `""` | -| `rules-file` |

Path to validation rules file

| `false` | `""` | -| `fail-on-error` |

Whether to fail on validation errors

| `false` | `true` | -| `token` |

GitHub token for authentication

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

Namespace/username for validation

| `false` | `""` | -| `email` |

Email address for validation

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

Username for validation

| `false` | `""` | -| `dotnet-version` |

.NET version string

| `false` | `""` | -| `terraform-version` |

Terraform version string

| `false` | `""` | -| `tflint-version` |

TFLint version string

| `false` | `""` | -| `node-version` |

Node.js version string

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

Force version override

| `false` | `""` | -| `default-version` |

Default version fallback

| `false` | `""` | -| `image-name` |

Docker image name

| `false` | `""` | -| `tag` |

Docker image tag

| `false` | `""` | -| `architectures` |

Target architectures

| `false` | `""` | -| `dockerfile` |

Dockerfile path

| `false` | `""` | -| `context` |

Docker build context

| `false` | `""` | -| `build-args` |

Docker build arguments

| `false` | `""` | -| `buildx-version` |

Docker Buildx version

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

Maximum retry attempts

| `false` | `""` | -| `image-quality` |

Image quality percentage

| `false` | `""` | -| `png-quality` |

PNG quality percentage

| `false` | `""` | -| `parallel-builds` |

Number of parallel builds

| `false` | `""` | -| `days-before-stale` |

Number of days before marking as stale

| `false` | `""` | -| `days-before-close` |

Number of days before closing stale items

| `false` | `""` | -| `pre-commit-config` |

Pre-commit configuration file path

| `false` | `""` | -| `base-branch` |

Base branch name

| `false` | `""` | -| `dry-run` |

Dry run mode

| `false` | `""` | -| `is_fiximus` |

Use Fiximus bot

| `false` | `""` | -| `prefix` |

Release tag prefix

| `false` | `""` | -| `language` |

Language to analyze (for CodeQL)

| `false` | `""` | -| `queries` |

CodeQL queries to run

| `false` | `""` | -| `packs` |

CodeQL query packs

| `false` | `""` | -| `config-file` |

CodeQL configuration file path

| `false` | `""` | -| `config` |

CodeQL configuration YAML string

| `false` | `""` | -| `build-mode` |

Build mode for compiled languages

| `false` | `""` | -| `source-root` |

Source code root directory

| `false` | `""` | -| `category` |

Analysis category

| `false` | `""` | -| `checkout-ref` |

Git reference to checkout

| `false` | `""` | -| `working-directory` |

Working directory for analysis

| `false` | `""` | -| `upload-results` |

Upload results to GitHub Security

| `false` | `""` | -| `ram` |

Memory in MB for CodeQL

| `false` | `""` | -| `threads` |

Number of threads for CodeQL

| `false` | `""` | -| `output` |

Output path for SARIF results

| `false` | `""` | -| `skip-queries` |

Skip running queries

| `false` | `""` | -| `add-snippets` |

Add code snippets to SARIF

| `false` | `""` | +| name | description | required | default | +|---------------------|-------------------------------------------------------------------------------------|----------|---------| +| `action` |

Action name to validate (alias for action-type)

| `false` | `""` | +| `action-type` |

Type of action to validate (e.g., csharp-publish, docker-build, eslint-lint)

| `false` | `""` | +| `rules-file` |

Path to validation rules file

| `false` | `""` | +| `fail-on-error` |

Whether to fail on validation errors

| `false` | `true` | +| `token` |

GitHub token for authentication

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

Namespace/username for validation

| `false` | `""` | +| `email` |

Email address for validation

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

Username for validation

| `false` | `""` | +| `dotnet-version` |

.NET version string

| `false` | `""` | +| `terraform-version` |

Terraform version string

| `false` | `""` | +| `tflint-version` |

TFLint version string

| `false` | `""` | +| `node-version` |

Node.js version string

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

Force version override

| `false` | `""` | +| `default-version` |

Default version fallback

| `false` | `""` | +| `image-name` |

Docker image name

| `false` | `""` | +| `tag` |

Docker image tag

| `false` | `""` | +| `architectures` |

Target architectures

| `false` | `""` | +| `dockerfile` |

Dockerfile path

| `false` | `""` | +| `context` |

Docker build context

| `false` | `""` | +| `build-args` |

Docker build arguments

| `false` | `""` | +| `buildx-version` |

Docker Buildx version

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

Maximum retry attempts

| `false` | `""` | +| `image-quality` |

Image quality percentage

| `false` | `""` | +| `png-quality` |

PNG quality percentage

| `false` | `""` | +| `parallel-builds` |

Number of parallel builds

| `false` | `""` | +| `days-before-stale` |

Number of days before marking as stale

| `false` | `""` | +| `days-before-close` |

Number of days before closing stale items

| `false` | `""` | +| `pre-commit-config` |

Pre-commit configuration file path

| `false` | `""` | +| `base-branch` |

Base branch name

| `false` | `""` | +| `dry-run` |

Dry run mode

| `false` | `""` | +| `is_fiximus` |

Use Fiximus bot

| `false` | `""` | +| `prefix` |

Release tag prefix

| `false` | `""` | +| `language` |

Language to analyze (for CodeQL)

| `false` | `""` | +| `queries` |

CodeQL queries to run

| `false` | `""` | +| `packs` |

CodeQL query packs

| `false` | `""` | +| `config-file` |

CodeQL configuration file path

| `false` | `""` | +| `config` |

CodeQL configuration YAML string

| `false` | `""` | +| `build-mode` |

Build mode for compiled languages

| `false` | `""` | +| `source-root` |

Source code root directory

| `false` | `""` | +| `category` |

Analysis category

| `false` | `""` | +| `checkout-ref` |

Git reference to checkout

| `false` | `""` | +| `working-directory` |

Working directory for analysis

| `false` | `""` | +| `upload-results` |

Upload results to GitHub Security

| `false` | `""` | +| `ram` |

Memory in MB for CodeQL

| `false` | `""` | +| `threads` |

Number of threads for CodeQL

| `false` | `""` | +| `output` |

Output path for SARIF results

| `false` | `""` | +| `skip-queries` |

Skip running queries

| `false` | `""` | +| `add-snippets` |

Add code snippets to SARIF

| `false` | `""` | ### Outputs @@ -85,7 +85,7 @@ This action is a `composite` action. # Default: "" action-type: - # Type of action to validate (e.g., csharp-publish, docker-build, eslint-fix) + # Type of action to validate (e.g., csharp-publish, docker-build, eslint-lint) # # Required: false # Default: "" diff --git a/validate-inputs/action.yml b/validate-inputs/action.yml index 4d1c055..e9d73c8 100644 --- a/validate-inputs/action.yml +++ b/validate-inputs/action.yml @@ -15,7 +15,7 @@ inputs: description: 'Action name to validate (alias for action-type)' required: false action-type: - description: 'Type of action to validate (e.g., csharp-publish, docker-build, eslint-fix)' + description: 'Type of action to validate (e.g., csharp-publish, docker-build, eslint-lint)' required: false rules-file: description: 'Path to validation rules file' diff --git a/validate-inputs/scripts/update-validators.py b/validate-inputs/scripts/update-validators.py index 92770f5..59e715a 100755 --- a/validate-inputs/scripts/update-validators.py +++ b/validate-inputs/scripts/update-validators.py @@ -354,7 +354,15 @@ class ValidationRuleGenerator: "threads": "numeric_range_1_128", "output": "file_path", "skip-queries": "boolean", - "add-snippets": "boolean", + }, + "biome-lint": { + "mode": "mode_enum", + }, + "eslint-lint": { + "mode": "mode_enum", + }, + "prettier-lint": { + "mode": "mode_enum", }, } diff --git a/validate-inputs/tests/test_integration.py b/validate-inputs/tests/test_integration.py index 70566d0..9c3aad1 100644 --- a/validate-inputs/tests/test_integration.py +++ b/validate-inputs/tests/test_integration.py @@ -52,9 +52,9 @@ class TestValidatorIntegration: def test_validator_script_success(self): """Test validator script execution with valid inputs.""" env_vars = { - "INPUT_ACTION_TYPE": "github-release", - "INPUT_VERSION": "1.2.3", - "INPUT_CHANGELOG": "Release notes", + "INPUT_ACTION_TYPE": "release-monthly", + "INPUT_TOKEN": "github_pat_" + "a" * 71, + "INPUT_PREFIX": "v", } result = self.run_validator(env_vars) @@ -65,9 +65,9 @@ class TestValidatorIntegration: def test_validator_script_failure(self): """Test validator script execution with invalid inputs.""" env_vars = { - "INPUT_ACTION_TYPE": "github-release", - "INPUT_VERSION": "invalid-version", - "INPUT_CHANGELOG": "Release notes", + "INPUT_ACTION_TYPE": "release-monthly", + "INPUT_TOKEN": "invalid-token", + "INPUT_PREFIX": "v", } result = self.run_validator(env_vars) @@ -78,22 +78,21 @@ class TestValidatorIntegration: def test_validator_script_missing_required(self): """Test validator script with missing required inputs.""" env_vars = { - "INPUT_ACTION_TYPE": "github-release", - # Missing required INPUT_VERSION - "INPUT_CHANGELOG": "Release notes", + "INPUT_ACTION_TYPE": "release-monthly", + # Missing required INPUT_TOKEN + "INPUT_PREFIX": "v", } result = self.run_validator(env_vars) assert result.returncode == 1 - assert "Required input 'version' is missing" in result.stderr + assert "Required input 'token' is missing" in result.stderr - def test_validator_script_calver_validation(self): - """Test validator script with CalVer version.""" + def test_validator_script_semver_validation(self): + """Test validator script with semantic version.""" env_vars = { - "INPUT_ACTION_TYPE": "github-release", - "INPUT_VERSION": "2024.3.1", - "INPUT_CHANGELOG": "Release notes", + "INPUT_ACTION_TYPE": "action-versioning", + "INPUT_MAJOR_VERSION": "v2025", } result = self.run_validator(env_vars) @@ -101,18 +100,17 @@ class TestValidatorIntegration: assert result.returncode == 0 assert "All input validation checks passed" in result.stderr - def test_validator_script_invalid_calver(self): - """Test validator script with invalid CalVer version.""" + def test_validator_script_invalid_semver(self): + """Test validator script with invalid semantic version.""" env_vars = { - "INPUT_ACTION_TYPE": "github-release", - "INPUT_VERSION": "2024.13.1", # Invalid month - "INPUT_CHANGELOG": "Release notes", + "INPUT_ACTION_TYPE": "action-versioning", + "INPUT_MAJOR_VERSION": "invalid.version", # Invalid version } result = self.run_validator(env_vars) assert result.returncode == 1 - assert "Invalid CalVer format" in result.stderr + assert "Input validation failed" in result.stderr def test_validator_script_docker_build(self): """Test validator script with docker-build action.""" @@ -239,8 +237,8 @@ class TestValidatorIntegration: def test_validator_script_output_file_creation(self): """Test that validator script creates GitHub output file.""" env_vars = { - "INPUT_ACTION_TYPE": "github-release", - "INPUT_VERSION": "1.2.3", + "INPUT_ACTION_TYPE": "release-monthly", + "INPUT_TOKEN": "github_pat_" + "a" * 71, } result = self.run_validator(env_vars) @@ -259,8 +257,8 @@ class TestValidatorIntegration: """Test validator script handles exceptions gracefully.""" # Test with invalid GITHUB_OUTPUT path to trigger exception env_vars = { - "INPUT_ACTION_TYPE": "github-release", - "INPUT_VERSION": "1.2.3", + "INPUT_ACTION_TYPE": "release-monthly", + "INPUT_TOKEN": "github_pat_" + "a" * 71, "GITHUB_OUTPUT": "/invalid/path/that/does/not/exist", } @@ -272,9 +270,9 @@ class TestValidatorIntegration: @pytest.mark.parametrize( "action_type,inputs,expected_success", [ - ("github-release", {"version": "1.2.3"}, True), - ("github-release", {"version": "2024.3.1"}, True), - ("github-release", {"version": "invalid"}, False), + ("release-monthly", {"token": "github_pat_" + "a" * 71}, True), + ("release-monthly", {"token": "github_pat_" + "a" * 71, "prefix": "v"}, True), + ("release-monthly", {"token": "invalid"}, False), ("docker-build", {"context": ".", "image-name": "app", "tag": "latest"}, True), ( "docker-build", diff --git a/validate-inputs/tests/test_set-git-config_custom.py b/validate-inputs/tests/test_set-git-config_custom.py deleted file mode 100644 index 49de20d..0000000 --- a/validate-inputs/tests/test_set-git-config_custom.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Tests for set-git-config 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 / "set-git-config" -sys.path.insert(0, str(action_path)) - -# pylint: disable=wrong-import-position -from CustomValidator import CustomValidator - - -class TestCustomSetGitConfigValidator: - """Test cases for set-git-config custom validator.""" - - def setup_method(self): - """Set up test fixtures.""" - self.validator = CustomValidator("set-git-config") - - 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 set-git-config - 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 set-git-config - 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 set-git-config - - def test_validation_rules(self): - """Test validation rules.""" - rules = self.validator.get_validation_rules() - assert isinstance(rules, dict) - # TODO: Assert specific validation rules for set-git-config - - 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/validators/conventions.py b/validate-inputs/validators/conventions.py index 01a73ab..45a5087 100644 --- a/validate-inputs/validators/conventions.py +++ b/validate-inputs/validators/conventions.py @@ -212,6 +212,7 @@ class ConventionBasedValidator(BaseValidator): "format": "report_format", "output_format": "report_format", "report_format": "report_format", + "mode": "mode_enum", } return exact_matches.get(name_lower) @@ -556,7 +557,7 @@ class ConventionBasedValidator(BaseValidator): return self._validator_modules["codeql"], f"validate_{validator_type}" # PHP-specific validators - if validator_type in ["php_extensions", "coverage_driver"]: + if validator_type in ["php_extensions", "coverage_driver", "mode_enum"]: # Return self for PHP-specific validation methods return self, f"_validate_{validator_type}" @@ -637,3 +638,23 @@ class ConventionBasedValidator(BaseValidator): return False return True + + def _validate_mode_enum(self, value: str, input_name: str) -> bool: + """Validate mode enum for linting actions. + + Args: + value: The mode value + input_name: The input name for error messages + + Returns: + True if valid, False otherwise + """ + valid_modes = ["check", "fix"] + + if value and value not in valid_modes: + self.add_error( + f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_modes)}" + ) + return False + + return True diff --git a/version-validator/CustomValidator.py b/version-validator/CustomValidator.py deleted file mode 100755 index 537a6f5..0000000 --- a/version-validator/CustomValidator.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -"""Custom validator for version-validator 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.security import SecurityValidator -from validators.version import VersionValidator - - -class CustomValidator(BaseValidator): - """Custom validator for version-validator action.""" - - def __init__(self, action_type: str = "version-validator") -> None: - """Initialize version-validator validator.""" - super().__init__(action_type) - self.version_validator = VersionValidator() - self.security_validator = SecurityValidator() - - def validate_inputs(self, inputs: dict[str, str]) -> bool: - """Validate version-validator action inputs.""" - valid = True - # Validate required input: version - if "version" not in inputs or not inputs["version"]: - self.add_error("Input 'version' is required") - valid = False - elif inputs["version"]: - result = self.version_validator.validate_flexible_version(inputs["version"], "version") - 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 optional input: validation-regex - # Check both underscore and dash versions - regex_value = inputs.get("validation-regex") or inputs.get("validation_regex") - if regex_value: - result = self.security_validator.validate_regex_pattern(regex_value, "validation-regex") - 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 optional input: language (accept any value) - if "language" in inputs and inputs.get("language"): - # Basic check that it's not malicious - lang_value = inputs["language"] - if ";" in lang_value or "$(" in lang_value or "`" in lang_value: - self.add_error("language contains potentially dangerous characters") - valid = False - - return valid - - def get_required_inputs(self) -> list[str]: - """Get list of required inputs.""" - return ["version"] - - 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/version-validator/README.md b/version-validator/README.md deleted file mode 100644 index 5bd4c98..0000000 --- a/version-validator/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# ivuorinen/actions/version-validator - -## Version Validator - -### Description - -Validates and normalizes version strings using customizable regex patterns - -### Inputs - -| name | description | required | default | -|--------------------|-----------------------------------------|----------|--------------------------------------------------------------------| -| `version` |

Version string to validate

| `true` | `""` | -| `validation-regex` |

Regex pattern for validation

| `false` | `^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$` | -| `language` |

Language name for error messages

| `false` | `version` | - -### Outputs - -| name | description | -|---------------------|------------------------------------------------------------| -| `is-valid` |

Boolean indicating if version is valid (true/false)

| -| `validated-version` |

Cleaned/normalized version string

| -| `error-message` |

Error message if validation fails

| - -### Runs - -This action is a `composite` action. - -### Usage - -```yaml -- uses: ivuorinen/actions/version-validator@main - with: - version: - # Version string to validate - # - # Required: true - # Default: "" - - validation-regex: - # Regex pattern for validation - # - # Required: false - # Default: ^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ - - language: - # Language name for error messages - # - # Required: false - # Default: version -``` diff --git a/version-validator/action.yml b/version-validator/action.yml deleted file mode 100644 index 25181c2..0000000 --- a/version-validator/action.yml +++ /dev/null @@ -1,117 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-action.json -# permissions: -# - (none required) # Read-only validation action ---- -name: Version Validator -description: 'Validates and normalizes version strings using customizable regex patterns' -author: 'Ismo Vuorinen' - -branding: - icon: check-circle - color: green - -inputs: - version: - description: 'Version string to validate' - required: true - validation-regex: - description: 'Regex pattern for validation' - required: false - default: '^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$' - language: - description: 'Language name for error messages' - required: false - default: 'version' - -outputs: - is-valid: - description: 'Boolean indicating if version is valid (true/false)' - value: ${{ steps.validate.outputs.is-valid }} - validated-version: - description: 'Cleaned/normalized version string' - value: ${{ steps.validate.outputs.validated-version }} - error-message: - description: 'Error message if validation fails' - value: ${{ steps.validate.outputs.error-message }} - -runs: - using: composite - steps: - - name: Validate Inputs - id: validate-inputs - shell: bash - env: - VERSION: ${{ inputs.version }} - VALIDATION_REGEX: ${{ inputs.validation-regex }} - LANGUAGE: ${{ inputs.language }} - run: | - set -euo pipefail - - # Validate version input is not empty - if [[ -z "$VERSION" ]]; then - echo "::error::Version input cannot be empty" - exit 1 - fi - - # Validate version string doesn't contain dangerous characters - if [[ "$VERSION" == *";"* ]] || [[ "$VERSION" == *"&&"* ]] || \ - [[ "$VERSION" == *"|"* ]] || [[ "$VERSION" == *"`"* ]] || [[ "$VERSION" == *"$"* ]]; then - echo "::error::Invalid version string: '$VERSION'. Potentially dangerous characters detected" - exit 1 - fi - - # Validate validation-regex is not empty and doesn't contain dangerous patterns - if [[ -z "$VALIDATION_REGEX" ]]; then - echo "::error::Validation regex cannot be empty" - exit 1 - fi - - # Basic check for regex safety (prevent ReDoS) - if [[ "$VALIDATION_REGEX" == *".*.*"* ]] || [[ "$VALIDATION_REGEX" == *".+.+"* ]]; then - echo "::warning::Validation regex may be vulnerable to ReDoS attacks" - fi - - # Validate language parameter - if [[ "$LANGUAGE" == *";"* ]] || [[ "$LANGUAGE" == *"&&"* ]] || [[ "$LANGUAGE" == *"|"* ]]; then - echo "::error::Invalid language parameter: '$LANGUAGE'. Command injection patterns not allowed" - exit 1 - fi - - echo "Input validation completed successfully" - - - name: Validate Version - id: validate - shell: bash - env: - VERSION: ${{ inputs.version }} - VALIDATION_REGEX: ${{ inputs.validation-regex }} - LANGUAGE: ${{ inputs.language }} - run: |- - set -euo pipefail - - input_version="$VERSION" - regex="$VALIDATION_REGEX" - language="$LANGUAGE" - - # Clean the version string - cleaned_version=$(echo "$input_version" | sed -e 's/^[vV]//' | tr -d ' ' | tr -d '\n' | tr -d '\r') - - # Validate the version - if [[ $cleaned_version =~ $regex ]]; then - { - echo "is-valid=true" - printf 'validated-version=%s\n' "$cleaned_version" - echo "error-message=" - } >> "$GITHUB_OUTPUT" - echo "โœ… Valid $language version: $cleaned_version" >&2 - else - error_msg="Invalid $language version format: '$input_version' (cleaned: '$cleaned_version'). Expected pattern: $regex" - # Sanitize error message by removing newlines to prevent GITHUB_OUTPUT injection - error_msg_sanitized="$(echo "$error_msg" | tr -d '\n\r')" - { - echo "is-valid=false" - echo "validated-version=" - printf 'error-message=%s\n' "$error_msg_sanitized" - } >> "$GITHUB_OUTPUT" - echo "โŒ $error_msg" >&2 - fi