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
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@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" <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" <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