Compare commits

..

1 Commits

Author SHA1 Message Date
0fa9a68f07 chore: update action references for release v2025.10.26
This commit updates all internal action references to point to the current
commit SHA in preparation for release v2025.10.26.
2025-10-26 23:56:09 +02:00
171 changed files with 11896 additions and 5195 deletions

View File

@@ -17,7 +17,7 @@ runs:
using: composite using: composite
steps: steps:
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1
with: with:
enable-cache: true enable-cache: true
@@ -33,7 +33,7 @@ runs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version: '24' node-version: '22'
cache: npm cache: npm
- name: Install Node dependencies - name: Install Node dependencies

View File

@@ -35,13 +35,13 @@ jobs:
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Check Required Configurations - name: Check Required Configurations
id: check-configs id: check-configs
shell: sh shell: bash
run: | run: |
# Initialize all flags as false # Initialize all flags as false
{ {
@@ -87,7 +87,7 @@ jobs:
- name: Verify SARIF files - name: Verify SARIF files
id: verify-sarif id: verify-sarif
shell: sh shell: bash
run: | run: |
# Initialize outputs # Initialize outputs
{ {
@@ -117,21 +117,21 @@ jobs:
- name: Upload Trivy results - name: Upload Trivy results
if: steps.verify-sarif.outputs.has_trivy == 'true' if: steps.verify-sarif.outputs.has_trivy == 'true'
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
sarif_file: 'trivy-results.sarif' sarif_file: 'trivy-results.sarif'
category: 'trivy' category: 'trivy'
- name: Upload Gitleaks results - name: Upload Gitleaks results
if: steps.verify-sarif.outputs.has_gitleaks == 'true' if: steps.verify-sarif.outputs.has_gitleaks == 'true'
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
sarif_file: 'gitleaks-report.sarif' sarif_file: 'gitleaks-report.sarif'
category: 'gitleaks' category: 'gitleaks'
- name: Archive security reports - name: Archive security reports
if: always() if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: security-reports-${{ github.run_id }} name: security-reports-${{ github.run_id }}
path: | path: |

View File

@@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
@@ -49,7 +49,7 @@ jobs:
- name: Extract metadata - name: Extract metadata
id: meta id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with: with:
images: ghcr.io/${{ github.repository_owner }}/actions images: ghcr.io/${{ github.repository_owner }}/actions
tags: | tags: |

View File

@@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Run CodeQL Analysis - name: Run CodeQL Analysis
uses: ./codeql-analysis uses: ./codeql-analysis

View File

@@ -34,18 +34,18 @@ jobs:
steps: # Add languages used in your actions steps: # Add languages used in your actions
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: security-and-quality queries: security-and-quality
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -12,6 +12,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1

View File

@@ -1,4 +1,3 @@
---
name: Monthly issue metrics name: Monthly issue metrics
on: on:
workflow_dispatch: workflow_dispatch:
@@ -17,7 +16,7 @@ jobs:
pull-requests: read pull-requests: read
steps: steps:
- name: Get dates for last month - name: Get dates for last month
shell: sh shell: bash
run: | run: |
# Calculate the first day of the previous month # Calculate the first day of the previous month
first_day=$(date -d "last month" +%Y-%m-01) first_day=$(date -d "last month" +%Y-%m-01)
@@ -30,7 +29,7 @@ jobs:
echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV"
- name: Run issue-metrics tool - name: Run issue-metrics tool
uses: github/issue-metrics@637a24e71b78bc10881e61972b19ea9ff736e14a # v3.25.2 uses: github/issue-metrics@c640329f02bd24b12b91d51cd385f0b1c25cefb9 # v3.25.1
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SEARCH_QUERY: 'repo:ivuorinen/actions is:issue created:${{ env.last_month }} -reason:"not planned"' SEARCH_QUERY: 'repo:ivuorinen/actions is:issue created:${{ env.last_month }} -reason:"not planned"'

View File

@@ -20,7 +20,7 @@ jobs:
version: ${{ steps.daily-version.outputs.version }} version: ${{ steps.daily-version.outputs.version }}
steps: steps:
- uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Create tag if necessary - name: Create tag if necessary
uses: fregante/daily-version-action@fb1a60b7c4daf1410cd755e360ebec3901e58588 # v2.1.3 uses: fregante/daily-version-action@fb1a60b7c4daf1410cd755e360ebec3901e58588 # v2.1.3

View File

@@ -47,7 +47,6 @@ concurrency:
permissions: permissions:
contents: read contents: read
packages: read # Required for private dependencies
jobs: jobs:
megalinter: megalinter:
@@ -57,17 +56,15 @@ jobs:
permissions: permissions:
actions: write actions: write
checks: write # Create and update check runs
contents: write contents: write
issues: write issues: write
packages: read # Access private packages
pull-requests: write pull-requests: write
security-events: write security-events: write
statuses: write statuses: write
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }} token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@@ -79,7 +76,7 @@ jobs:
- name: Check MegaLinter Results - name: Check MegaLinter Results
id: check-results id: check-results
if: always() if: always()
shell: sh shell: bash
run: | run: |
printf '%s\n' "status=success" >> "$GITHUB_OUTPUT" printf '%s\n' "status=success" >> "$GITHUB_OUTPUT"
@@ -94,7 +91,7 @@ jobs:
- name: Upload Reports - name: Upload Reports
if: always() if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: MegaLinter reports name: MegaLinter reports
path: | path: |
@@ -104,14 +101,14 @@ jobs:
- name: Upload SARIF Report - name: Upload SARIF Report
if: always() && hashFiles('megalinter-reports/sarif/*.sarif') if: always() && hashFiles('megalinter-reports/sarif/*.sarif')
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
sarif_file: megalinter-reports/sarif sarif_file: megalinter-reports/sarif
category: megalinter category: megalinter
- name: Prepare Git for Fixes - name: Prepare Git for Fixes
if: steps.ml.outputs.has_updated_sources == 1 if: steps.ml.outputs.has_updated_sources == 1
shell: sh shell: bash
run: | run: |
sudo chown -Rc $UID .git/ sudo chown -Rc $UID .git/
git config --global user.name "fiximus" git config --global user.name "fiximus"
@@ -196,7 +193,7 @@ jobs:
- name: Cleanup - name: Cleanup
if: always() if: always()
shell: sh shell: bash
run: |- run: |-
# Remove temporary files but keep reports # Remove temporary files but keep reports
find . -type f -name "megalinter.*" ! -name "megalinter-reports" -delete || true find . -type f -name "megalinter.*" ! -name "megalinter-reports" -delete || true

View File

@@ -16,7 +16,7 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 - uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with: with:
generate_release_notes: true generate_release_notes: true

View File

@@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Checkout PR - name: Checkout PR
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
fetch-depth: 0 fetch-depth: 0
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -43,7 +43,7 @@ jobs:
- name: Fetch PR Base - name: Fetch PR Base
run: | run: |
set -eu set -euo pipefail
# Fetch the base ref from base repository with authentication (works for private repos and forked PRs) # 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 # 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 # Use authenticated URL to avoid 403/404 on private repositories
@@ -97,9 +97,6 @@ jobs:
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
// Unique marker to identify our bot comment
const SECURITY_COMMENT_MARKER = '<!-- security-analysis-bot-comment -->';
const findings = { const findings = {
permissions: [], permissions: [],
actions: [], actions: [],
@@ -233,40 +230,11 @@ jobs:
if (findings.permissions.length > 0) { if (findings.permissions.length > 0) {
const permSection = ['## 🔐 GitHub Actions Permissions Changes']; const permSection = ['## 🔐 GitHub Actions Permissions Changes'];
findings.permissions.forEach(change => { findings.permissions.forEach(change => {
permSection.push(`\n**${change.file}**:`); permSection.push(`**${change.file}**:`);
permSection.push('```diff');
// Parse permissions into lines permSection.push(`- ${change.old}`);
const oldLines = (change.old === 'None' ? [] : change.old.split('\n').map(l => l.trim()).filter(Boolean)); permSection.push(`+ ${change.new}`);
const newLines = (change.new === 'None' ? [] : change.new.split('\n').map(l => l.trim()).filter(Boolean)); permSection.push('```');
// 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(`<details><summary>Unchanged (${unchanged.length})</summary>\n\n${unchanged.map(l => `- ${l}`).join('\n')}\n</details>`);
} else if (unchanged.length > 3) {
permSection.push(`<sub>*${unchanged.length} permissions unchanged*</sub>`);
}
}
}); });
sections.push(permSection.join('\n')); sections.push(permSection.join('\n'));
} }
@@ -346,15 +314,15 @@ jobs:
// Export critical count as output // Export critical count as output
core.setOutput('critical_issues', criticalCount.toString()); core.setOutput('critical_issues', criticalCount.toString());
// Generate final comment with unique marker // Generate final comment
let comment = `${SECURITY_COMMENT_MARKER}\n## ✅ Security Analysis\n\n`; let comment = '## ✅ Security Analysis\n\n';
if (sections.length === 0) { if (sections.length === 0) {
comment += 'No security issues detected in this PR.'; comment += 'No security issues detected in this PR.';
} else { } else {
comment += sections.join('\n\n'); comment += sections.join('\n\n');
} }
// Find existing security comment using unique marker // Find existing security comment
const { data: comments } = await github.rest.issues.listComments({ const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
@@ -362,7 +330,8 @@ jobs:
}); });
const existingComment = comments.find(comment => const existingComment = comments.find(comment =>
comment.body && comment.body.includes(SECURITY_COMMENT_MARKER) comment.body.includes('Security Analysis') ||
comment.body.includes('🔐 GitHub Actions Permissions')
); );
if (existingComment) { if (existingComment) {

View File

@@ -35,6 +35,6 @@ jobs:
steps: steps:
- name: ⤵️ Checkout Repository - name: ⤵️ Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: ⤵️ Sync Latest Labels Definitions - name: ⤵️ Sync Latest Labels Definitions
uses: ./sync-labels uses: ./sync-labels

View File

@@ -49,16 +49,16 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup test environment - name: Setup test environment
uses: ./.github/actions/setup-test-environment uses: ./.github/actions/setup-test-environment
- name: Run unit tests - name: Run unit tests
shell: sh shell: bash
run: | run: |
if [ "${{ github.event.inputs.test-type }}" = "unit" ] || [ "${{ github.event.inputs.test-type }}" = "all" ] || [ -z "${{ github.event.inputs.test-type }}" ]; then if [[ "${{ github.event.inputs.test-type }}" == "unit" || "${{ github.event.inputs.test-type }}" == "all" || -z "${{ github.event.inputs.test-type }}" ]]; then
if [ -n "${{ github.event.inputs.action-filter }}" ]; then if [[ -n "${{ github.event.inputs.action-filter }}" ]]; then
make test-action ACTION="${{ github.event.inputs.action-filter }}" make test-action ACTION="${{ github.event.inputs.action-filter }}"
else else
make test-unit make test-unit
@@ -68,19 +68,19 @@ jobs:
fi fi
- name: Generate SARIF report - name: Generate SARIF report
shell: sh shell: bash
run: ./_tests/run-tests.sh --type unit --format sarif run: ./_tests/run-tests.sh --type unit --format sarif
if: always() if: always()
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
if: always() && hashFiles('_tests/reports/test-results.sarif') != '' if: always() && hashFiles('_tests/reports/test-results.sarif') != ''
with: with:
sarif_file: _tests/reports/test-results.sarif sarif_file: _tests/reports/test-results.sarif
category: github-actions-tests category: github-actions-tests
- name: Upload unit test results - name: Upload unit test results
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always() if: always()
with: with:
name: unit-test-results name: unit-test-results
@@ -99,7 +99,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup test environment - name: Setup test environment
uses: ./.github/actions/setup-test-environment uses: ./.github/actions/setup-test-environment
@@ -107,10 +107,10 @@ jobs:
install-act: 'true' install-act: 'true'
- name: Run integration tests - name: Run integration tests
shell: sh shell: bash
run: | run: |
if [ "${{ github.event.inputs.test-type }}" = "integration" ] || [ "${{ github.event.inputs.test-type }}" = "all" ] || [ -z "${{ github.event.inputs.test-type }}" ]; then if [[ "${{ github.event.inputs.test-type }}" == "integration" || "${{ github.event.inputs.test-type }}" == "all" || -z "${{ github.event.inputs.test-type }}" ]]; then
if [ -n "${{ github.event.inputs.action-filter }}" ]; then if [[ -n "${{ github.event.inputs.action-filter }}" ]]; then
./_tests/run-tests.sh --type integration --action "${{ github.event.inputs.action-filter }}" ./_tests/run-tests.sh --type integration --action "${{ github.event.inputs.action-filter }}"
else else
make test-integration make test-integration
@@ -122,7 +122,7 @@ jobs:
- name: Check for integration test reports - name: Check for integration test reports
id: check-integration-reports id: check-integration-reports
if: always() if: always()
shell: sh shell: bash
run: | run: |
if [ -d "_tests/reports/integration" ] && [ -n "$(find _tests/reports/integration -type f 2>/dev/null)" ]; then if [ -d "_tests/reports/integration" ] && [ -n "$(find _tests/reports/integration -type f 2>/dev/null)" ]; then
printf '%s\n' "reports-found=true" >> $GITHUB_OUTPUT printf '%s\n' "reports-found=true" >> $GITHUB_OUTPUT
@@ -133,7 +133,7 @@ jobs:
fi fi
- name: Upload integration test results - name: Upload integration test results
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always() && steps.check-integration-reports.outputs.reports-found == 'true' if: always() && steps.check-integration-reports.outputs.reports-found == 'true'
with: with:
name: integration-test-results name: integration-test-results
@@ -156,7 +156,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup test environment - name: Setup test environment
uses: ./.github/actions/setup-test-environment uses: ./.github/actions/setup-test-environment
@@ -167,7 +167,7 @@ jobs:
run: make test-coverage run: make test-coverage
- name: Upload coverage report - name: Upload coverage report
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: coverage-report name: coverage-report
path: _tests/coverage/ path: _tests/coverage/
@@ -176,9 +176,9 @@ jobs:
- name: Comment coverage summary - name: Comment coverage summary
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
shell: sh shell: bash
run: | run: |
if [ -f _tests/coverage/summary.json ]; then if [[ -f _tests/coverage/summary.json ]]; then
coverage=$(jq -r '.coverage_percent' _tests/coverage/summary.json) coverage=$(jq -r '.coverage_percent' _tests/coverage/summary.json)
tested_actions=$(jq -r '.tested_actions' _tests/coverage/summary.json) tested_actions=$(jq -r '.tested_actions' _tests/coverage/summary.json)
total_actions=$(jq -r '.total_actions' _tests/coverage/summary.json) total_actions=$(jq -r '.total_actions' _tests/coverage/summary.json)
@@ -224,7 +224,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup test environment - name: Setup test environment
uses: ./.github/actions/setup-test-environment uses: ./.github/actions/setup-test-environment
@@ -240,7 +240,7 @@ jobs:
extra_args: --debug --only-verified extra_args: --debug --only-verified
- name: Scan shell scripts - name: Scan shell scripts
shell: sh shell: bash
run: | run: |
# Scan all shell scripts in _tests/ # Scan all shell scripts in _tests/
find _tests/ -name "*.sh" -exec shellcheck -x {} \; || { find _tests/ -name "*.sh" -exec shellcheck -x {} \; || {
@@ -263,14 +263,14 @@ jobs:
steps: steps:
- name: Download test results - name: Download test results
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
pattern: '*-test-results' pattern: '*-test-results'
merge-multiple: true merge-multiple: true
path: test-results/ path: test-results/
- name: Generate test summary - name: Generate test summary
shell: sh shell: bash
run: | run: |
{ {
echo "## 🧪 Test Results Summary" echo "## 🧪 Test Results Summary"
@@ -278,20 +278,20 @@ jobs:
# Unit tests # Unit tests
unit_count=$(find test-results -type f -path "*/unit/*.txt" | wc -l || true) unit_count=$(find test-results -type f -path "*/unit/*.txt" | wc -l || true)
if [ "${unit_count:-0}" -gt 0 ]; then if [[ "${unit_count:-0}" -gt 0 ]]; then
echo "- **Unit Tests**: $unit_count action(s) tested" echo "- **Unit Tests**: $unit_count action(s) tested"
fi fi
# Integration tests # Integration tests
integration_count=$(find test-results -type f -path "*/integration/*.txt" | wc -l || true) integration_count=$(find test-results -type f -path "*/integration/*.txt" | wc -l || true)
if [ "${integration_count:-0}" -gt 0 ]; then if [[ "${integration_count:-0}" -gt 0 ]]; then
echo "- **Integration Tests**: $integration_count action(s) tested" echo "- **Integration Tests**: $integration_count action(s) tested"
fi fi
echo "" echo ""
unit_success="${{ needs.unit-tests.result == 'success' }}" unit_success="${{ needs.unit-tests.result == 'success' }}"
integration_ok="${{ needs.integration-tests.result == 'success' || needs.integration-tests.result == 'skipped' }}" integration_ok="${{ needs.integration-tests.result == 'success' || needs.integration-tests.result == 'skipped' }}"
if [ "$unit_success" = "true" ] && [ "$integration_ok" = "true" ]; then if [[ "$unit_success" == "true" && "$integration_ok" == "true" ]]; then
status="✅ All tests passed" status="✅ All tests passed"
else else
status="❌ Some tests failed" status="❌ Some tests failed"
@@ -307,7 +307,7 @@ jobs:
- name: Fail if tests failed - name: Fail if tests failed
if: needs.unit-tests.result == 'failure' || needs.integration-tests.result == 'failure' if: needs.unit-tests.result == 'failure' || needs.integration-tests.result == 'failure'
shell: sh shell: bash
run: |- run: |-
echo "❌ One or more test jobs failed" echo "❌ One or more test jobs failed"
exit 1 exit 1

View File

@@ -24,7 +24,7 @@ jobs:
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
@@ -49,7 +49,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
if: steps.action-versioning.outputs.updated == 'true' if: steps.action-versioning.outputs.updated == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: update action references to ${{ steps.version.outputs.major }}' commit-message: 'chore: update action references to ${{ steps.version.outputs.major }}'
@@ -78,7 +78,7 @@ jobs:
- name: Check for Annual Bump - name: Check for Annual Bump
if: steps.action-versioning.outputs.needs-annual-bump == 'true' if: steps.action-versioning.outputs.needs-annual-bump == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with: with:
script: | script: |
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();

2
.gitignore vendored
View File

@@ -13,7 +13,6 @@
.cache .cache
.cache/ .cache/
.coverage .coverage
.worktrees/
.coverage.* .coverage.*
.docusaurus .docusaurus
.dynamodb/ .dynamodb/
@@ -84,4 +83,3 @@ tests/reports/**/*.json
!uv.lock !uv.lock
code-scanning-report-* code-scanning-report-*
*.sarif *.sarif
TODO.md

View File

@@ -1,2 +0,0 @@
node_modules/
.worktrees/

View File

@@ -32,4 +32,4 @@ JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.json
TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json
FILTER_REGEX_EXCLUDE: > FILTER_REGEX_EXCLUDE: >
(node_modules|\.automation/test|docs/json-schemas|\.worktrees) (node_modules|\.automation/test|docs/json-schemas)

2
.nvmrc
View File

@@ -1 +1 @@
24 v22

View File

@@ -14,7 +14,7 @@ repos:
types: [markdown, python, yaml] types: [markdown, python, yaml]
files: ^(docs/.*|README\.md|CONTRIBUTING\.md|CHANGELOG\.md|.*\.py|.*\.ya?ml)$ files: ^(docs/.*|README\.md|CONTRIBUTING\.md|CHANGELOG\.md|.*\.py|.*\.ya?ml)$
- repo: https://github.com/astral-sh/uv-pre-commit - repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.9.8 rev: 0.9.5
hooks: hooks:
- id: uv-lock - id: uv-lock
- id: uv-sync - id: uv-sync
@@ -44,7 +44,7 @@ repos:
args: [--autofix, --no-sort-keys] args: [--autofix, --no-sort-keys]
- repo: https://github.com/DavidAnson/markdownlint-cli2 - repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.19.0 rev: v0.18.1
hooks: hooks:
- id: markdownlint-cli2 - id: markdownlint-cli2
args: [--fix] args: [--fix]
@@ -55,7 +55,7 @@ repos:
- id: yamllint - id: yamllint
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.5 rev: v0.14.2
hooks: hooks:
# Run the linter with auto-fix # Run the linter with auto-fix
- id: ruff-check - id: ruff-check
@@ -84,18 +84,18 @@ repos:
args: ['-shellcheck='] args: ['-shellcheck=']
- repo: https://github.com/renovatebot/pre-commit-hooks - repo: https://github.com/renovatebot/pre-commit-hooks
rev: 42.6.2 rev: 41.149.2
hooks: hooks:
- id: renovate-config-validator - id: renovate-config-validator
- repo: https://github.com/bridgecrewio/checkov.git - repo: https://github.com/bridgecrewio/checkov.git
rev: '3.2.489' rev: '3.2.487'
hooks: hooks:
- id: checkov - id: checkov
args: args:
- '--quiet' - '--quiet'
- repo: https://github.com/gitleaks/gitleaks - repo: https://github.com/gitleaks/gitleaks
rev: v8.29.0 rev: v8.28.0
hooks: hooks:
- id: gitleaks - id: gitleaks

View File

@@ -1,3 +1,2 @@
.github/renovate.json .github/renovate.json
.venv .venv
.worktrees/

View File

@@ -45,32 +45,6 @@
- macOS/Windows runners may lack Linux tools (jq, bc, specific GNU utils) - macOS/Windows runners may lack Linux tools (jq, bc, specific GNU utils)
- Always provide fallbacks or explicit installation steps - Always provide fallbacks or explicit installation steps
11. **NEVER** use `set-git-config` action - use direct git config or action parameters instead
- Git-related actions (`peter-evans/create-pull-request`, `stefanzweifel/git-auto-commit-action`) handle their own auth
- For direct git commands, configure git manually when needed: `git config user.name/user.email`
- Pattern for actions with git-auto-commit:
```yaml
- uses: stefanzweifel/git-auto-commit-action@SHA
with:
commit_user_name: ${{ inputs.username }}
commit_user_email: ${{ inputs.email }}
```
- Pattern for actions with direct git commands:
```yaml
- shell: bash
run: |
git config user.name "${{ inputs.username }}"
git config user.email "${{ inputs.email }}"
git add .
git commit -m "message"
git push
```
- Rationale: Avoids complexity, matches proven workflow pattern, no credential conflicts
## EditorConfig Rules (.editorconfig) ## EditorConfig Rules (.editorconfig)
**CRITICAL**: EditorConfig violations are blocking errors and must be fixed always. **CRITICAL**: EditorConfig violations are blocking errors and must be fixed always.

View File

@@ -4,6 +4,10 @@
# * For JavaScript, use typescript # * For JavaScript, use typescript
# Special requirements: # Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder. # * csharp: Requires the presence of a .sln file in the project folder.
language: bash
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true ignore_all_files_in_gitignore: true
# list of additional paths to ignore # list of additional paths to ignore
# same syntax as gitignore, so you can use * and ** # same syntax as gitignore, so you can use * and **
@@ -62,8 +66,3 @@ excluded_tools: []
initial_prompt: '' initial_prompt: ''
project_name: 'actions' project_name: 'actions'
languages:
- bash
- python
included_optional_tools: []
encoding: utf-8

View File

@@ -1,2 +1 @@
.venv .venv
.worktrees/

View File

@@ -1,10 +1,6 @@
--- ---
extends: default extends: default
ignore: |
node_modules/
.worktrees/
rules: rules:
line-length: line-length:
max: 200 max: 200

View File

@@ -39,7 +39,7 @@
**Core Memories** (read first for project understanding): **Core Memories** (read first for project understanding):
- `repository_overview` 30 actions, categories, structure, status - `repository_overview` 43 actions, categories, structure, status
- `validator_system` Validation architecture, components, usage patterns - `validator_system` Validation architecture, components, usage patterns
- `development_standards` Quality rules, workflows, security, completion checklist - `development_standards` Quality rules, workflows, security, completion checklist
@@ -71,11 +71,11 @@
Flat structure. Each action self-contained with `action.yml`. Flat structure. Each action self-contained with `action.yml`.
**30 Actions**: Setup (node-setup, language-version-detect), Utilities (action-versioning, version-file-parser), **43 Actions**: Setup (node-setup, set-git-config, php-version-detect, python-version-detect, python-version-detect-v2, go-version-detect, dotnet-version-detect), Utilities (version-file-parser, version-validator),
Linting (ansible-lint-fix, biome-lint, csharp-lint-check, eslint-lint, go-lint, pr-lint, pre-commit, prettier-lint, python-lint-fix, terraform-lint-fix), Linting (ansible-lint-fix, biome-check, biome-fix, csharp-lint-check, eslint-check, eslint-fix, go-lint, pr-lint, pre-commit, prettier-check, prettier-fix, python-lint-fix, terraform-lint-fix),
Testing (php-tests, php-laravel-phpunit, php-composer), Build (csharp-build, go-build, docker-build), Testing (php-tests, php-laravel-phpunit, php-composer), Build (csharp-build, go-build, docker-build),
Publishing (npm-publish, docker-publish, csharp-publish), Publishing (npm-publish, docker-publish, docker-publish-gh, docker-publish-hub, csharp-publish),
Repository (release-monthly, sync-labels, stale, compress-images, common-cache, codeql-analysis), Repository (github-release, release-monthly, sync-labels, stale, compress-images, common-cache, common-file-check, common-retry, codeql-analysis),
Validation (validate-inputs) Validation (validate-inputs)
## Commands ## Commands

View File

@@ -1,7 +1,7 @@
# Makefile for GitHub Actions repository # Makefile for GitHub Actions repository
# Provides organized task management with parallel execution capabilities # Provides organized task management with parallel execution capabilities
.PHONY: help all docs update-catalog lint format check clean install-tools test test-unit test-integration test-coverage generate-tests generate-tests-dry test-generate-tests docker-build docker-push docker-test docker-login docker-all release release-dry release-prep release-tag release-undo update-version-refs bump-major-version check-version-refs .PHONY: help all docs lint format check clean install-tools test test-unit test-integration test-coverage generate-tests generate-tests-dry test-generate-tests docker-build docker-push docker-test docker-login docker-all release update-version-refs bump-major-version check-version-refs
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
# Colors for output # Colors for output
@@ -43,7 +43,7 @@ help: ## Show this help message
@echo " make check # Quick syntax checks" @echo " make check # Quick syntax checks"
# Main targets # Main targets
all: install-tools update-validators docs update-catalog format lint precommit ## Generate docs, format, lint, and run pre-commit all: install-tools update-validators docs format lint precommit ## Generate docs, format, lint, and run pre-commit
@echo "$(GREEN)✅ All tasks completed successfully$(RESET)" @echo "$(GREEN)✅ All tasks completed successfully$(RESET)"
docs: ## Generate documentation for all actions docs: ## Generate documentation for all actions
@@ -66,16 +66,6 @@ docs: ## Generate documentation for all actions
done; \ done; \
[ $$failed -eq 0 ] && echo "$(GREEN)✅ All documentation updated successfully$(RESET)" || { echo "$(RED)$$failed documentation updates failed$(RESET)"; exit 1; } [ $$failed -eq 0 ] && echo "$(GREEN)✅ All documentation updated successfully$(RESET)" || { echo "$(RED)$$failed documentation updates failed$(RESET)"; exit 1; }
update-catalog: ## Update action catalog in README.md
@echo "$(BLUE)📚 Updating action catalog...$(RESET)"
@if command -v npm >/dev/null 2>&1; then \
npm run update-catalog; \
else \
echo "$(RED)❌ npm not found. Please install Node.js$(RESET)"; \
exit 1; \
fi
@echo "$(GREEN)✅ Action catalog updated$(RESET)"
update-validators: ## Update validation rules for all actions update-validators: ## Update validation rules for all actions
@echo "$(BLUE)🔧 Updating validation rules...$(RESET)" @echo "$(BLUE)🔧 Updating validation rules...$(RESET)"
@if command -v uv >/dev/null 2>&1; then \ @if command -v uv >/dev/null 2>&1; then \
@@ -159,36 +149,12 @@ fix-local-refs-dry: ## Preview local action reference fixes (dry run)
release: ## Create a new release with version tags (usage: make release [VERSION=v2025.10.18]) release: ## Create a new release with version tags (usage: make release [VERSION=v2025.10.18])
@VERSION_TO_USE=$$(if [ -n "$(VERSION)" ]; then echo "$(VERSION)"; else date +v%Y.%m.%d; fi); \ @VERSION_TO_USE=$$(if [ -n "$(VERSION)" ]; then echo "$(VERSION)"; else date +v%Y.%m.%d; fi); \
echo "$(BLUE)🚀 Creating release $$VERSION_TO_USE...$(RESET)"; \ echo "$(BLUE)🚀 Creating release $$VERSION_TO_USE...$(RESET)"; \
sh _tools/release.sh "$$VERSION_TO_USE" sh _tools/release.sh "$$VERSION_TO_USE"; \
echo "$(GREEN)✅ Release created$(RESET)"; \
release-dry: ## Preview release without making changes (usage: make release-dry VERSION=v2025.11.01) echo ""; \
@if [ -z "$(VERSION)" ]; then \ echo "$(YELLOW)Next steps:$(RESET)"; \
VERSION_TO_USE=$$(date +v%Y.%m.%d); \ echo " 1. Review changes: git show HEAD"; \
else \ echo " 2. Push tags: git push origin main --tags --force-with-lease"
VERSION_TO_USE="$(VERSION)"; \
fi; \
echo "$(BLUE)🔍 Previewing release $$VERSION_TO_USE (dry run)...$(RESET)"; \
sh _tools/release.sh --dry-run "$$VERSION_TO_USE"
release-prep: ## Update action refs and commit (no tags) (usage: make release-prep [VERSION=v2025.11.01])
@VERSION_TO_USE=$$(if [ -n "$(VERSION)" ]; then echo "$(VERSION)"; else date +v%Y.%m.%d; fi); \
echo "$(BLUE)🔧 Preparing release $$VERSION_TO_USE...$(RESET)"; \
sh _tools/release.sh --prep-only "$$VERSION_TO_USE"; \
echo "$(GREEN)✅ Preparation complete$(RESET)"; \
echo "$(YELLOW)Next: make release-tag VERSION=$$VERSION_TO_USE$(RESET)"
release-tag: ## Create tags only (assumes prep done) (usage: make release-tag VERSION=v2025.11.01)
@if [ -z "$(VERSION)" ]; then \
echo "$(RED)❌ Error: VERSION parameter required for release-tag$(RESET)"; \
echo "Usage: make release-tag VERSION=v2025.11.01"; \
exit 1; \
fi; \
echo "$(BLUE)🏷️ Creating tags for release $(VERSION)...$(RESET)"; \
sh _tools/release.sh --tag-only "$(VERSION)"
release-undo: ## Rollback the most recent release (delete tags and reset HEAD)
@echo "$(BLUE)🔙 Rolling back release...$(RESET)"; \
sh _tools/release-undo.sh
update-version-refs: ## Update all action references to a specific version tag (usage: make update-version-refs MAJOR=v2025) update-version-refs: ## Update all action references to a specific version tag (usage: make update-version-refs MAJOR=v2025)
@if [ -z "$(MAJOR)" ]; then \ @if [ -z "$(MAJOR)" ]; then \
@@ -217,7 +183,7 @@ check-version-refs: ## List all current SHA-pinned action references
# Formatting targets # Formatting targets
format-markdown: ## Format markdown files format-markdown: ## Format markdown files
@echo "$(BLUE)📝 Formatting markdown...$(RESET)" @echo "$(BLUE)📝 Formatting markdown...$(RESET)"
@if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" "#.worktrees" 2>/dev/null; then \ @if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" 2>/dev/null; then \
echo "$(GREEN)✅ Markdown formatted$(RESET)"; \ echo "$(GREEN)✅ Markdown formatted$(RESET)"; \
else \ else \
echo "$(YELLOW)⚠️ Markdown formatting issues found$(RESET)" | tee -a $(LOG_FILE); \ echo "$(YELLOW)⚠️ Markdown formatting issues found$(RESET)" | tee -a $(LOG_FILE); \
@@ -269,7 +235,7 @@ format-python: ## Format Python files with ruff
# Linting targets # Linting targets
lint-markdown: ## Lint markdown files lint-markdown: ## Lint markdown files
@echo "$(BLUE)🔍 Linting markdown...$(RESET)" @echo "$(BLUE)🔍 Linting markdown...$(RESET)"
@if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" "#.worktrees"; then \ @if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules"; then \
echo "$(GREEN)✅ Markdown linting passed$(RESET)"; \ echo "$(GREEN)✅ Markdown linting passed$(RESET)"; \
else \ else \
echo "$(YELLOW)⚠️ Markdown linting issues found$(RESET)" | tee -a $(LOG_FILE); \ echo "$(YELLOW)⚠️ Markdown linting issues found$(RESET)" | tee -a $(LOG_FILE); \
@@ -291,7 +257,7 @@ lint-shell: ## Lint shell scripts
echo " or: apt-get install shellcheck"; \ echo " or: apt-get install shellcheck"; \
exit 1; \ exit 1; \
fi fi
@if find . -name "*.sh" -not -path "./_tests/*" -not -path "./.worktrees/*" -exec shellcheck -x {} +; then \ @if find . -name "*.sh" -not -path "./_tests/*" -exec shellcheck -x {} +; then \
echo "$(GREEN)✅ Shell linting passed$(RESET)"; \ echo "$(GREEN)✅ Shell linting passed$(RESET)"; \
else \ else \
echo "$(RED)❌ Shell linting issues found$(RESET)"; \ echo "$(RED)❌ Shell linting issues found$(RESET)"; \
@@ -340,7 +306,7 @@ check-tools: ## Check if required tools are available
check-syntax: ## Check syntax of shell scripts and YAML files check-syntax: ## Check syntax of shell scripts and YAML files
@echo "$(BLUE)🔍 Checking syntax...$(RESET)" @echo "$(BLUE)🔍 Checking syntax...$(RESET)"
@failed=0; \ @failed=0; \
find . -name "*.sh" -not -path "./_tests/*" -not -path "./.worktrees/*" -print0 | while IFS= read -r -d '' file; do \ find . -name "*.sh" -print0 | while IFS= read -r -d '' file; do \
if ! bash -n "$$file" 2>&1; then \ if ! bash -n "$$file" 2>&1; then \
echo "$(RED)❌ Syntax error in $$file$(RESET)" >&2; \ echo "$(RED)❌ Syntax error in $$file$(RESET)" >&2; \
failed=1; \ failed=1; \
@@ -721,8 +687,7 @@ docker-all: docker-build docker-test docker-push ## Build, test, and push Docker
watch: ## Watch files and auto-format on changes (requires entr) watch: ## Watch files and auto-format on changes (requires entr)
@if command -v entr >/dev/null 2>&1; then \ @if command -v entr >/dev/null 2>&1; then \
echo "$(BLUE)👀 Watching for changes... (press Ctrl+C to stop)$(RESET)"; \ echo "$(BLUE)👀 Watching for changes... (press Ctrl+C to stop)$(RESET)"; \
find . \( -name "*.yml" -o -name "*.yaml" -o -name "*.md" -o -name "*.sh" \) \ find . -name "*.yml" -o -name "*.yaml" -o -name "*.md" -o -name "*.sh" | \
-not -path "./_tests/*" -not -path "./.worktrees/*" -not -path "./node_modules/*" | \
entr -c $(MAKE) format; \ entr -c $(MAKE) format; \
else \ else \
echo "$(RED)❌ Error: entr not found. Install with: brew install entr$(RESET)"; \ echo "$(RED)❌ Error: entr not found. Install with: brew install entr$(RESET)"; \

318
README.md
View File

@@ -22,71 +22,92 @@ Each action is fully self-contained and can be used independently in any GitHub
## 📚 Action Catalog ## 📚 Action Catalog
This repository contains **30 reusable GitHub Actions** for CI/CD automation. This repository contains **43 reusable GitHub Actions** for CI/CD automation.
### Quick Reference (30 Actions) ### Quick Reference (43 Actions)
| Icon | Action | Category | Description | Key Features | | Icon | Action | Category | Description | Key Features |
|:----:|:-----------------------------------------------------|:-----------|:----------------------------------------------------------------|:---------------------------------------------| |:----:|:-------------------------------------------------------|:-----------|:----------------------------------------------------------------|:---------------------------------------------|
| 🔀 | [`action-versioning`][action-versioning] | Utilities | Automatically update SHA-pinned action references to match l... | Token auth, Outputs | | 📦 | [`ansible-lint-fix`][ansible-lint-fix] | Linting | Lints and fixes Ansible playbooks, commits changes, and uplo... | Token auth, Outputs |
| 📦 | [`ansible-lint-fix`][ansible-lint-fix] | Linting | Lints and fixes Ansible playbooks, commits changes, and uplo... | Token auth, Outputs | | | [`biome-check`][biome-check] | Linting | Run Biome check on the repository | Token auth, Outputs |
| ✅ | [`biome-lint`][biome-lint] | Linting | Run Biome linter in check or fix mode | Token auth, Outputs | | ✅ | [`biome-fix`][biome-fix] | Linting | Run Biome fix on the repository | Token auth, Outputs |
| 🛡️ | [`codeql-analysis`][codeql-analysis] | Repository | Run CodeQL security analysis for a single language with conf... | Auto-detection, Token auth, Outputs | | 🛡️ | [`codeql-analysis`][codeql-analysis] | Other | Run CodeQL security analysis for a single language with conf... | Auto-detection, Token auth, Outputs |
| 💾 | [`common-cache`][common-cache] | Repository | Standardized caching strategy for all actions | Caching, Outputs | | 💾 | [`common-cache`][common-cache] | Repository | Standardized caching strategy for all actions | Caching, Outputs |
| 🖼️ | [`compress-images`][compress-images] | Repository | Compress images on demand (workflow_dispatch), and at 11pm e... | Token auth, Outputs | | 📦 | [`common-file-check`][common-file-check] | Repository | A reusable action to check if a specific file or type of fil... | Outputs |
| 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Auto-detection, Token auth, Outputs | | 🔄 | [`common-retry`][common-retry] | Repository | Standardized retry utility for network operations and flaky ... | Outputs |
| 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Auto-detection, Token auth, Outputs | | 🖼️ | [`compress-images`][compress-images] | Repository | Compress images on demand (workflow_dispatch), and at 11pm e... | Token auth, Outputs |
| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Auto-detection, Token auth, Outputs | | 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Auto-detection, Outputs |
| 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs | | 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Auto-detection, Outputs |
| ☁️ | [`docker-publish`][docker-publish] | Publishing | Simple wrapper to publish Docker images to GitHub Packages a... | Token auth, Outputs | | 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Auto-detection, Token auth, Outputs |
| | [`eslint-lint`][eslint-lint] | Linting | Run ESLint in check or fix mode with advanced configuration ... | Caching, Token auth, Outputs | | 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`go-build`][go-build] | Build | Builds the Go project. | Caching, Auto-detection, Token auth, Outputs | | ☁️ | [`docker-publish`][docker-publish] | Publishing | Publish a Docker image to GitHub Packages and Docker Hub. | Auto-detection, Outputs |
| 📝 | [`go-lint`][go-lint] | Linting | Run golangci-lint with advanced configuration, caching, and ... | Caching, Token auth, Outputs | | 📦 | [`docker-publish-gh`][docker-publish-gh] | Publishing | Publishes a Docker image to GitHub Packages with advanced se... | Caching, Auto-detection, Token auth, Outputs |
| 📝 | [`language-version-detect`][language-version-detect] | Setup | Detects language version from project configuration files wi... | Auto-detection, Token auth, Outputs | | 📦 | [`docker-publish-hub`][docker-publish-hub] | Publishing | Publishes a Docker image to Docker Hub with enhanced securit... | Caching, Auto-detection, Outputs |
| 🖥️ | [`node-setup`][node-setup] | Setup | Sets up Node.js environment with version detection and packa... | Auto-detection, Token auth, Outputs | | 📝 | [`dotnet-version-detect`][dotnet-version-detect] | Setup | Detects .NET SDK version from global.json or defaults to a s... | Auto-detection, Outputs |
| 📦 | [`npm-publish`][npm-publish] | Publishing | Publishes the package to the NPM registry with configurable ... | Token auth, Outputs | | | [`eslint-check`][eslint-check] | Linting | Run ESLint check on the repository with advanced configurati... | Caching, Outputs |
| 🖥️ | [`php-composer`][php-composer] | Testing | Runs Composer install on a repository with advanced caching ... | Auto-detection, Token auth, Outputs | | 📝 | [`eslint-fix`][eslint-fix] | Linting | Fixes ESLint violations in a project. | Token auth, Outputs |
| 💻 | [`php-laravel-phpunit`][php-laravel-phpunit] | Testing | Setup PHP, install dependencies, generate key, create databa... | Auto-detection, Token auth, Outputs | | 🏷️ | [`github-release`][github-release] | Repository | Creates a GitHub release with a version and changelog. | Outputs |
| | [`php-tests`][php-tests] | Testing | Run PHPUnit tests on the repository | Token auth, Outputs | | 📦 | [`go-build`][go-build] | Build | Builds the Go project. | Caching, Auto-detection, Outputs |
| | [`pr-lint`][pr-lint] | Linting | Runs MegaLinter against pull requests | Caching, Auto-detection, Token auth, Outputs | | 📝 | [`go-lint`][go-lint] | Linting | Run golangci-lint with advanced configuration, caching, and ... | Caching, Outputs |
| 📦 | [`pre-commit`][pre-commit] | Linting | Runs pre-commit on the repository and pushes the fixes back ... | Auto-detection, Token auth, Outputs | | 📝 | [`go-version-detect`][go-version-detect] | Setup | Detects the Go version from the project's go.mod file or def... | Auto-detection, Outputs |
| | [`prettier-lint`][prettier-lint] | Linting | Run Prettier in check or fix mode with advanced configuratio... | Caching, Token auth, Outputs | | 🖥️ | [`node-setup`][node-setup] | Setup | Sets up Node.js env with advanced version management, cachin... | Caching, Auto-detection, Token auth, Outputs |
| 📝 | [`python-lint-fix`][python-lint-fix] | Linting | Lints and fixes Python files, commits changes, and uploads S... | Caching, Auto-detection, Token auth, Outputs | | 📦 | [`npm-publish`][npm-publish] | Publishing | Publishes the package to the NPM registry with configurable ... | Outputs |
| 📦 | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs | | 🖥️ | [`php-composer`][php-composer] | Testing | Runs Composer install on a repository with advanced caching ... | Auto-detection, Token auth, Outputs |
| 📦 | [`stale`][stale] | Repository | A GitHub Action to close stale issues and pull requests. | Token auth, Outputs | | 💻 | [`php-laravel-phpunit`][php-laravel-phpunit] | Testing | Setup PHP, install dependencies, generate key, create databa... | Auto-detection, Token auth, Outputs |
| 🏷️ | [`sync-labels`][sync-labels] | Repository | Sync labels from a YAML file to a GitHub repository | Token auth, Outputs | | | [`php-tests`][php-tests] | Testing | Run PHPUnit tests on the repository | Token auth, Outputs |
| 🖥️ | [`terraform-lint-fix`][terraform-lint-fix] | Linting | Lints and fixes Terraform files with advanced validation and... | Token auth, Outputs | | 📝 | [`php-version-detect`][php-version-detect] | Setup | Detects the PHP version from the project's composer.json, ph... | Auto-detection, Outputs |
| 🛡️ | [`validate-inputs`][validate-inputs] | Validation | Centralized Python-based input validation for GitHub Actions... | Token auth, Outputs | | | [`pr-lint`][pr-lint] | Linting | Runs MegaLinter against pull requests | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`version-file-parser`][version-file-parser] | Utilities | Universal parser for common version detection files (.tool-v... | Auto-detection, Outputs | | 📦 | [`pre-commit`][pre-commit] | Linting | Runs pre-commit on the repository and pushes the fixes back ... | Auto-detection, Token auth, Outputs |
| ✅ | [`prettier-check`][prettier-check] | Linting | Run Prettier check on the repository with advanced configura... | Caching, Outputs |
| 📝 | [`prettier-fix`][prettier-fix] | Linting | Run Prettier to fix code style violations | Token auth, Outputs |
| 📝 | [`python-lint-fix`][python-lint-fix] | Linting | Lints and fixes Python files, commits changes, and uploads S... | Caching, Auto-detection, Token auth, Outputs |
| 📝 | [`python-version-detect`][python-version-detect] | Setup | Detects Python version from project configuration files or d... | Auto-detection, Outputs |
| 📝 | [`python-version-detect-v2`][python-version-detect-v2] | Setup | Detects Python version from project configuration files usin... | Auto-detection, Outputs |
| 📦 | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs |
| 🔀 | [`set-git-config`][set-git-config] | Setup | Sets Git configuration for actions. | Token auth, Outputs |
| 📦 | [`stale`][stale] | Repository | A GitHub Action to close stale issues and pull requests. | Token auth, Outputs |
| 🏷️ | [`sync-labels`][sync-labels] | Repository | Sync labels from a YAML file to a GitHub repository | Token auth, Outputs |
| 🖥️ | [`terraform-lint-fix`][terraform-lint-fix] | Linting | Lints and fixes Terraform files with advanced validation and... | Token auth, Outputs |
| 🛡️ | [`validate-inputs`][validate-inputs] | Other | Centralized Python-based input validation for GitHub Actions... | Token auth, Outputs |
| 📦 | [`version-file-parser`][version-file-parser] | Utilities | Universal parser for common version detection files (.tool-v... | Auto-detection, Outputs |
| ✅ | [`version-validator`][version-validator] | Utilities | Validates and normalizes version strings using customizable ... | Auto-detection, Outputs |
### Actions by Category ### Actions by Category
#### 🔧 Setup (2 actions) #### 🔧 Setup (7 actions)
| Action | Description | Languages | Features | | Action | Description | Languages | Features |
|:--------------------------------------------------------|:------------------------------------------------------|:--------------------------------|:------------------------------------| |:----------------------------------------------------------|:------------------------------------------------------|:--------------------------------|:---------------------------------------------|
| 📝 [`language-version-detect`][language-version-detect] | Detects language version from project configuratio... | PHP, Python, Go, .NET, Node.js | Auto-detection, Token auth, Outputs | | 📝 [`dotnet-version-detect`][dotnet-version-detect] | Detects .NET SDK version from global.json or defau... | C#, .NET | Auto-detection, Outputs |
| 🖥️ [`node-setup`][node-setup] | Sets up Node.js environment with version detection... | Node.js, JavaScript, TypeScript | Auto-detection, Token auth, Outputs | | 📝 [`go-version-detect`][go-version-detect] | Detects the Go version from the project's go.mod f... | Go | Auto-detection, Outputs |
| 🖥️ [`node-setup`][node-setup] | Sets up Node.js env with advanced version manageme... | Node.js, JavaScript, TypeScript | Caching, Auto-detection, Token auth, Outputs |
| 📝 [`php-version-detect`][php-version-detect] | Detects the PHP version from the project's compose... | PHP | Auto-detection, Outputs |
| 📝 [`python-version-detect`][python-version-detect] | Detects Python version from project configuration ... | Python | Auto-detection, Outputs |
| 📝 [`python-version-detect-v2`][python-version-detect-v2] | Detects Python version from project configuration ... | Python | Auto-detection, Outputs |
| 🔀 [`set-git-config`][set-git-config] | Sets Git configuration for actions. | - | Token auth, Outputs |
#### 🛠️ Utilities (2 actions) #### 🛠️ Utilities (2 actions)
| Action | Description | Languages | Features | | Action | Description | Languages | Features |
|:------------------------------------------------|:------------------------------------------------------|:-------------------|:------------------------| |:------------------------------------------------|:------------------------------------------------------|:----------|:------------------------|
| 🔀 [`action-versioning`][action-versioning] | Automatically update SHA-pinned action references ... | GitHub Actions | Token auth, Outputs | | 📦 [`version-file-parser`][version-file-parser] | Universal parser for common version detection file... | - | Auto-detection, Outputs |
| 📦 [`version-file-parser`][version-file-parser] | Universal parser for common version detection file... | Multiple Languages | Auto-detection, Outputs | | [`version-validator`][version-validator] | Validates and normalizes version strings using cus... | - | Auto-detection, Outputs |
#### 📝 Linting (10 actions) #### 📝 Linting (13 actions)
| Action | Description | Languages | Features | | Action | Description | Languages | Features |
|:-----------------------------------------------|:------------------------------------------------------|:---------------------------------------------|:---------------------------------------------| |:-----------------------------------------------|:------------------------------------------------------|:---------------------------------------------|:---------------------------------------------|
| 📦 [`ansible-lint-fix`][ansible-lint-fix] | Lints and fixes Ansible playbooks, commits changes... | Ansible, YAML | Token auth, Outputs | | 📦 [`ansible-lint-fix`][ansible-lint-fix] | Lints and fixes Ansible playbooks, commits changes... | Ansible, YAML | Token auth, Outputs |
| ✅ [`biome-lint`][biome-lint] | Run Biome linter in check or fix mode | JavaScript, TypeScript, JSON | Token auth, Outputs | | ✅ [`biome-check`][biome-check] | Run Biome check on the repository | JavaScript, TypeScript, JSON | Token auth, Outputs |
| 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Auto-detection, Token auth, Outputs | | [`biome-fix`][biome-fix] | Run Biome fix on the repository | JavaScript, TypeScript, JSON | Token auth, Outputs |
| [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, Token auth, Outputs | | 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Auto-detection, Outputs |
| 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, Token auth, Outputs | | [`eslint-check`][eslint-check] | Run ESLint check on the repository with advanced c... | JavaScript, TypeScript | Caching, Outputs |
| [`pr-lint`][pr-lint] | Runs MegaLinter against pull requests | Conventional Commits | Caching, Auto-detection, Token auth, Outputs | | 📝 [`eslint-fix`][eslint-fix] | Fixes ESLint violations in a project. | JavaScript, TypeScript | Token auth, Outputs |
| 📦 [`pre-commit`][pre-commit] | Runs pre-commit on the repository and pushes the f... | Python, Multiple Languages | Auto-detection, Token auth, Outputs | | 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, Outputs |
| ✅ [`prettier-lint`][prettier-lint] | Run Prettier in check or fix mode with advanced co... | JavaScript, TypeScript, Markdown, YAML, JSON | Caching, Token auth, Outputs | | ✅ [`pr-lint`][pr-lint] | Runs MegaLinter against pull requests | - | Caching, Auto-detection, Token auth, Outputs |
| 📦 [`pre-commit`][pre-commit] | Runs pre-commit on the repository and pushes the f... | - | Auto-detection, Token auth, Outputs |
| ✅ [`prettier-check`][prettier-check] | Run Prettier check on the repository with advanced... | JavaScript, TypeScript, Markdown, YAML, JSON | Caching, Outputs |
| 📝 [`prettier-fix`][prettier-fix] | Run Prettier to fix code style violations | JavaScript, TypeScript, Markdown, YAML, JSON | Token auth, Outputs |
| 📝 [`python-lint-fix`][python-lint-fix] | Lints and fixes Python files, commits changes, and... | Python | Caching, Auto-detection, Token auth, Outputs | | 📝 [`python-lint-fix`][python-lint-fix] | Lints and fixes Python files, commits changes, and... | Python | Caching, Auto-detection, Token auth, Outputs |
| 🖥️ [`terraform-lint-fix`][terraform-lint-fix] | Lints and fixes Terraform files with advanced vali... | Terraform, HCL | Token auth, Outputs | | 🖥️ [`terraform-lint-fix`][terraform-lint-fix] | Lints and fixes Terraform files with advanced vali... | Terraform, HCL | Token auth, Outputs |
@@ -102,102 +123,102 @@ This repository contains **30 reusable GitHub Actions** for CI/CD automation.
| Action | Description | Languages | Features | | Action | Description | Languages | Features |
|:----------------------------------|:------------------------------------------------------|:----------|:---------------------------------------------| |:----------------------------------|:------------------------------------------------------|:----------|:---------------------------------------------|
| 📝 [`csharp-build`][csharp-build] | Builds and tests C# projects. | C#, .NET | Auto-detection, Token auth, Outputs | | 📝 [`csharp-build`][csharp-build] | Builds and tests C# projects. | C#, .NET | Auto-detection, Outputs |
| 📦 [`docker-build`][docker-build] | Builds a Docker image for multiple architectures w... | Docker | Caching, Auto-detection, Token auth, Outputs | | 📦 [`docker-build`][docker-build] | Builds a Docker image for multiple architectures w... | Docker | Caching, Auto-detection, Token auth, Outputs |
| 📦 [`go-build`][go-build] | Builds the Go project. | Go | Caching, Auto-detection, Token auth, Outputs | | 📦 [`go-build`][go-build] | Builds the Go project. | Go | Caching, Auto-detection, Outputs |
#### 🚀 Publishing (3 actions) #### 🚀 Publishing (5 actions)
| Action | Description | Languages | Features | | Action | Description | Languages | Features |
|:--------------------------------------|:------------------------------------------------------|:-------------|:------------------------------------| |:----------------------------------------------|:------------------------------------------------------|:-------------|:---------------------------------------------|
| 📦 [`csharp-publish`][csharp-publish] | Publishes a C# project to GitHub Packages. | C#, .NET | Auto-detection, Token auth, Outputs | | 📦 [`csharp-publish`][csharp-publish] | Publishes a C# project to GitHub Packages. | C#, .NET | Auto-detection, Token auth, Outputs |
| ☁️ [`docker-publish`][docker-publish] | Simple wrapper to publish Docker images to GitHub ... | Docker | Token auth, Outputs | | ☁️ [`docker-publish`][docker-publish] | Publish a Docker image to GitHub Packages and Dock... | Docker | Auto-detection, Outputs |
| 📦 [`npm-publish`][npm-publish] | Publishes the package to the NPM registry with con... | Node.js, npm | Token auth, Outputs | | 📦 [`docker-publish-gh`][docker-publish-gh] | Publishes a Docker image to GitHub Packages with a... | Docker | Caching, Auto-detection, Token auth, Outputs |
| 📦 [`docker-publish-hub`][docker-publish-hub] | Publishes a Docker image to Docker Hub with enhanc... | Docker | Caching, Auto-detection, Outputs |
| 📦 [`npm-publish`][npm-publish] | Publishes the package to the NPM registry with con... | Node.js, npm | Outputs |
#### 📦 Repository (6 actions) #### 📦 Repository (8 actions)
| Action | Description | Languages | Features | | Action | Description | Languages | Features |
|:-----------------------------------------|:------------------------------------------------------|:--------------------------------------------------------|:------------------------------------| |:--------------------------------------------|:------------------------------------------------------|:----------|:--------------------|
| 🛡️ [`codeql-analysis`][codeql-analysis] | Run CodeQL security analysis for a single language... | JavaScript, TypeScript, Python, Java, C#, C++, Go, Ruby | Auto-detection, Token auth, Outputs | | 💾 [`common-cache`][common-cache] | Standardized caching strategy for all actions | - | Caching, Outputs |
| 💾 [`common-cache`][common-cache] | Standardized caching strategy for all actions | Caching | Caching, Outputs | | 📦 [`common-file-check`][common-file-check] | A reusable action to check if a specific file or t... | - | Outputs |
| 🖼️ [`compress-images`][compress-images] | Compress images on demand (workflow_dispatch), and... | Images, PNG, JPEG | Token auth, Outputs | | 🔄 [`common-retry`][common-retry] | Standardized retry utility for network operations ... | - | Outputs |
| 📦 [`release-monthly`][release-monthly] | Creates a release for the current month, increment... | GitHub Actions | Token auth, Outputs | | 🖼️ [`compress-images`][compress-images] | Compress images on demand (workflow_dispatch), and... | - | Token auth, Outputs |
| 📦 [`stale`][stale] | A GitHub Action to close stale issues and pull req... | GitHub Actions | Token auth, Outputs | | 🏷️ [`github-release`][github-release] | Creates a GitHub release with a version and change... | - | Outputs |
| 🏷️ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | YAML, GitHub | Token auth, Outputs | | 📦 [`release-monthly`][release-monthly] | Creates a release for the current month, increment... | - | Token auth, Outputs |
| 📦 [`stale`][stale] | A GitHub Action to close stale issues and pull req... | - | Token auth, Outputs |
#### ✅ Validation (1 action) | 🏷️ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | - | Token auth, Outputs |
| Action | Description | Languages | Features |
|:-----------------------------------------|:------------------------------------------------------|:---------------------|:--------------------|
| 🛡️ [`validate-inputs`][validate-inputs] | Centralized Python-based input validation for GitH... | YAML, GitHub Actions | Token auth, Outputs |
### Feature Matrix ### Feature Matrix
| Action | Caching | Auto-detection | Token auth | Outputs | | Action | Caching | Auto-detection | Token auth | Outputs |
|:-----------------------------------------------------|:-------:|:--------------:|:----------:|:-------:| |:-------------------------------------------------------|:-------:|:--------------:|:----------:|:-------:|
| [`action-versioning`][action-versioning] | - | - | ✅ | ✅ | | [`ansible-lint-fix`][ansible-lint-fix] | - | - | ✅ | ✅ |
| [`ansible-lint-fix`][ansible-lint-fix] | - | - | ✅ | ✅ | | [`biome-check`][biome-check] | - | - | ✅ | ✅ |
| [`biome-lint`][biome-lint] | - | - | ✅ | ✅ | | [`biome-fix`][biome-fix] | - | - | ✅ | ✅ |
| [`codeql-analysis`][codeql-analysis] | - | ✅ | ✅ | ✅ | | [`codeql-analysis`][codeql-analysis] | - | ✅ | ✅ | ✅ |
| [`common-cache`][common-cache] | ✅ | - | - | ✅ | | [`common-cache`][common-cache] | ✅ | - | - | ✅ |
| [`compress-images`][compress-images] | - | - | | ✅ | | [`common-file-check`][common-file-check] | - | - | - | ✅ |
| [`csharp-build`][csharp-build] | - | | | ✅ | | [`common-retry`][common-retry] | - | - | - | ✅ |
| [`csharp-lint-check`][csharp-lint-check] | - | | ✅ | ✅ | | [`compress-images`][compress-images] | - | - | ✅ | ✅ |
| [`csharp-publish`][csharp-publish] | - | ✅ | | ✅ | | [`csharp-build`][csharp-build] | - | ✅ | - | ✅ |
| [`docker-build`][docker-build] | | ✅ | | ✅ | | [`csharp-lint-check`][csharp-lint-check] | - | ✅ | - | ✅ |
| [`docker-publish`][docker-publish] | - | - | ✅ | ✅ | | [`csharp-publish`][csharp-publish] | - | | ✅ | ✅ |
| [`eslint-lint`][eslint-lint] | ✅ | - | ✅ | ✅ | | [`docker-build`][docker-build] | ✅ | | ✅ | ✅ |
| [`go-build`][go-build] | | ✅ | | ✅ | | [`docker-publish`][docker-publish] | - | ✅ | - | ✅ |
| [`go-lint`][go-lint] | ✅ | - | ✅ | ✅ | | [`docker-publish-gh`][docker-publish-gh] | ✅ | | ✅ | ✅ |
| [`language-version-detect`][language-version-detect] | - | ✅ | | ✅ | | [`docker-publish-hub`][docker-publish-hub] | | ✅ | - | ✅ |
| [`node-setup`][node-setup] | - | ✅ | | ✅ | | [`dotnet-version-detect`][dotnet-version-detect] | - | ✅ | - | ✅ |
| [`npm-publish`][npm-publish] | - | - | | ✅ | | [`eslint-check`][eslint-check] | | - | - | ✅ |
| [`php-composer`][php-composer] | - | | ✅ | ✅ | | [`eslint-fix`][eslint-fix] | - | - | ✅ | ✅ |
| [`php-laravel-phpunit`][php-laravel-phpunit] | - | | | ✅ | | [`github-release`][github-release] | - | - | - | ✅ |
| [`php-tests`][php-tests] | - | - | | ✅ | | [`go-build`][go-build] | | | - | ✅ |
| [`pr-lint`][pr-lint] | ✅ | | | ✅ | | [`go-lint`][go-lint] | ✅ | - | - | ✅ |
| [`pre-commit`][pre-commit] | - | ✅ | | ✅ | | [`go-version-detect`][go-version-detect] | - | ✅ | - | ✅ |
| [`prettier-lint`][prettier-lint] | ✅ | - | ✅ | ✅ | | [`node-setup`][node-setup] | ✅ | | ✅ | ✅ |
| [`python-lint-fix`][python-lint-fix] | | | | ✅ | | [`npm-publish`][npm-publish] | - | - | - | ✅ |
| [`release-monthly`][release-monthly] | - | - | ✅ | ✅ | | [`php-composer`][php-composer] | - | | ✅ | ✅ |
| [`stale`][stale] | - | - | ✅ | ✅ | | [`php-laravel-phpunit`][php-laravel-phpunit] | - | | ✅ | ✅ |
| [`sync-labels`][sync-labels] | - | - | ✅ | ✅ | | [`php-tests`][php-tests] | - | - | ✅ | ✅ |
| [`terraform-lint-fix`][terraform-lint-fix] | - | - | | ✅ | | [`php-version-detect`][php-version-detect] | - | | - | ✅ |
| [`validate-inputs`][validate-inputs] | - | - | ✅ | ✅ | | [`pr-lint`][pr-lint] | | | ✅ | ✅ |
| [`version-file-parser`][version-file-parser] | - | ✅ | - | ✅ | | [`pre-commit`][pre-commit] | - | ✅ | | ✅ |
| [`prettier-check`][prettier-check] | ✅ | - | - | ✅ |
| [`prettier-fix`][prettier-fix] | - | - | ✅ | ✅ |
| [`python-lint-fix`][python-lint-fix] | ✅ | ✅ | ✅ | ✅ |
| [`python-version-detect`][python-version-detect] | - | ✅ | - | ✅ |
| [`python-version-detect-v2`][python-version-detect-v2] | - | ✅ | - | ✅ |
| [`release-monthly`][release-monthly] | - | - | ✅ | ✅ |
| [`set-git-config`][set-git-config] | - | - | ✅ | ✅ |
| [`stale`][stale] | - | - | ✅ | ✅ |
| [`sync-labels`][sync-labels] | - | - | ✅ | ✅ |
| [`terraform-lint-fix`][terraform-lint-fix] | - | - | ✅ | ✅ |
| [`validate-inputs`][validate-inputs] | - | - | ✅ | ✅ |
| [`version-file-parser`][version-file-parser] | - | ✅ | - | ✅ |
| [`version-validator`][version-validator] | - | ✅ | - | ✅ |
### Language Support ### Language Support
| Language | Actions | | Language | Actions |
|:---------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------| |:-----------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .NET | [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish], [`language-version-detect`][language-version-detect] | | .NET | [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish], [`dotnet-version-detect`][dotnet-version-detect] |
| Ansible | [`ansible-lint-fix`][ansible-lint-fix] | | Ansible | [`ansible-lint-fix`][ansible-lint-fix] |
| C# | [`codeql-analysis`][codeql-analysis], [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish] | | C# | [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish], [`dotnet-version-detect`][dotnet-version-detect] |
| C++ | [`codeql-analysis`][codeql-analysis] | | Docker | [`docker-build`][docker-build], [`docker-publish`][docker-publish], [`docker-publish-gh`][docker-publish-gh], [`docker-publish-hub`][docker-publish-hub] |
| Caching | [`common-cache`][common-cache] | | Go | [`go-build`][go-build], [`go-lint`][go-lint], [`go-version-detect`][go-version-detect] |
| Conventional Commits | [`pr-lint`][pr-lint] | | HCL | [`terraform-lint-fix`][terraform-lint-fix] |
| Docker | [`docker-build`][docker-build], [`docker-publish`][docker-publish] | | JSON | [`biome-check`][biome-check], [`biome-fix`][biome-fix], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] |
| GitHub | [`sync-labels`][sync-labels] | | JavaScript | [`biome-check`][biome-check], [`biome-fix`][biome-fix], [`eslint-check`][eslint-check], [`eslint-fix`][eslint-fix], [`node-setup`][node-setup], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] |
| GitHub Actions | [`action-versioning`][action-versioning], [`release-monthly`][release-monthly], [`stale`][stale], [`validate-inputs`][validate-inputs] | | Laravel | [`php-laravel-phpunit`][php-laravel-phpunit] |
| Go | [`codeql-analysis`][codeql-analysis], [`go-build`][go-build], [`go-lint`][go-lint], [`language-version-detect`][language-version-detect] | | Markdown | [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] |
| HCL | [`terraform-lint-fix`][terraform-lint-fix] | | Node.js | [`node-setup`][node-setup], [`npm-publish`][npm-publish] |
| Images | [`compress-images`][compress-images] | | PHP | [`php-composer`][php-composer], [`php-laravel-phpunit`][php-laravel-phpunit], [`php-tests`][php-tests], [`php-version-detect`][php-version-detect] |
| JPEG | [`compress-images`][compress-images] | | Python | [`python-lint-fix`][python-lint-fix], [`python-version-detect`][python-version-detect], [`python-version-detect-v2`][python-version-detect-v2] |
| JSON | [`biome-lint`][biome-lint], [`prettier-lint`][prettier-lint] | | Terraform | [`terraform-lint-fix`][terraform-lint-fix] |
| Java | [`codeql-analysis`][codeql-analysis] | | TypeScript | [`biome-check`][biome-check], [`biome-fix`][biome-fix], [`eslint-check`][eslint-check], [`eslint-fix`][eslint-fix], [`node-setup`][node-setup], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] |
| JavaScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`node-setup`][node-setup], [`prettier-lint`][prettier-lint] | | YAML | [`ansible-lint-fix`][ansible-lint-fix], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] |
| Laravel | [`php-laravel-phpunit`][php-laravel-phpunit] | | npm | [`npm-publish`][npm-publish] |
| Markdown | [`prettier-lint`][prettier-lint] |
| Multiple Languages | [`pre-commit`][pre-commit], [`version-file-parser`][version-file-parser] |
| Node.js | [`language-version-detect`][language-version-detect], [`node-setup`][node-setup], [`npm-publish`][npm-publish] |
| PHP | [`language-version-detect`][language-version-detect], [`php-composer`][php-composer], [`php-laravel-phpunit`][php-laravel-phpunit], [`php-tests`][php-tests] |
| PNG | [`compress-images`][compress-images] |
| Python | [`codeql-analysis`][codeql-analysis], [`language-version-detect`][language-version-detect], [`pre-commit`][pre-commit], [`python-lint-fix`][python-lint-fix] |
| Ruby | [`codeql-analysis`][codeql-analysis] |
| Terraform | [`terraform-lint-fix`][terraform-lint-fix] |
| TypeScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`node-setup`][node-setup], [`prettier-lint`][prettier-lint] |
| YAML | [`ansible-lint-fix`][ansible-lint-fix], [`prettier-lint`][prettier-lint], [`sync-labels`][sync-labels], [`validate-inputs`][validate-inputs] |
| npm | [`npm-publish`][npm-publish] |
### Action Usage ### Action Usage
@@ -205,7 +226,7 @@ All actions can be used independently in your workflows:
```yaml ```yaml
# Recommended: Use pinned refs for supply-chain security # Recommended: Use pinned refs for supply-chain security
- uses: ivuorinen/actions/action-name@vYYYY-MM-DD # Date-based tag (example) - uses: ivuorinen/actions/action-name@2025-01-15 # Date-based tag
with: with:
# action-specific inputs # action-specific inputs
@@ -219,36 +240,49 @@ All actions can be used independently in your workflows:
<!-- Reference Links --> <!-- Reference Links -->
[action-versioning]: action-versioning/README.md
[ansible-lint-fix]: ansible-lint-fix/README.md [ansible-lint-fix]: ansible-lint-fix/README.md
[biome-lint]: biome-lint/README.md [biome-check]: biome-check/README.md
[biome-fix]: biome-fix/README.md
[codeql-analysis]: codeql-analysis/README.md [codeql-analysis]: codeql-analysis/README.md
[common-cache]: common-cache/README.md [common-cache]: common-cache/README.md
[common-file-check]: common-file-check/README.md
[common-retry]: common-retry/README.md
[compress-images]: compress-images/README.md [compress-images]: compress-images/README.md
[csharp-build]: csharp-build/README.md [csharp-build]: csharp-build/README.md
[csharp-lint-check]: csharp-lint-check/README.md [csharp-lint-check]: csharp-lint-check/README.md
[csharp-publish]: csharp-publish/README.md [csharp-publish]: csharp-publish/README.md
[docker-build]: docker-build/README.md [docker-build]: docker-build/README.md
[docker-publish]: docker-publish/README.md [docker-publish]: docker-publish/README.md
[eslint-lint]: eslint-lint/README.md [docker-publish-gh]: docker-publish-gh/README.md
[docker-publish-hub]: docker-publish-hub/README.md
[dotnet-version-detect]: dotnet-version-detect/README.md
[eslint-check]: eslint-check/README.md
[eslint-fix]: eslint-fix/README.md
[github-release]: github-release/README.md
[go-build]: go-build/README.md [go-build]: go-build/README.md
[go-lint]: go-lint/README.md [go-lint]: go-lint/README.md
[language-version-detect]: language-version-detect/README.md [go-version-detect]: go-version-detect/README.md
[node-setup]: node-setup/README.md [node-setup]: node-setup/README.md
[npm-publish]: npm-publish/README.md [npm-publish]: npm-publish/README.md
[php-composer]: php-composer/README.md [php-composer]: php-composer/README.md
[php-laravel-phpunit]: php-laravel-phpunit/README.md [php-laravel-phpunit]: php-laravel-phpunit/README.md
[php-tests]: php-tests/README.md [php-tests]: php-tests/README.md
[php-version-detect]: php-version-detect/README.md
[pr-lint]: pr-lint/README.md [pr-lint]: pr-lint/README.md
[pre-commit]: pre-commit/README.md [pre-commit]: pre-commit/README.md
[prettier-lint]: prettier-lint/README.md [prettier-check]: prettier-check/README.md
[prettier-fix]: prettier-fix/README.md
[python-lint-fix]: python-lint-fix/README.md [python-lint-fix]: python-lint-fix/README.md
[python-version-detect]: python-version-detect/README.md
[python-version-detect-v2]: python-version-detect-v2/README.md
[release-monthly]: release-monthly/README.md [release-monthly]: release-monthly/README.md
[set-git-config]: set-git-config/README.md
[stale]: stale/README.md [stale]: stale/README.md
[sync-labels]: sync-labels/README.md [sync-labels]: sync-labels/README.md
[terraform-lint-fix]: terraform-lint-fix/README.md [terraform-lint-fix]: terraform-lint-fix/README.md
[validate-inputs]: validate-inputs/README.md [validate-inputs]: validate-inputs/README.md
[version-file-parser]: version-file-parser/README.md [version-file-parser]: version-file-parser/README.md
[version-validator]: version-validator/README.md
--- ---

View File

@@ -57,21 +57,6 @@ get_action_name() {
uv run "$script_dir/../shared/validation_core.py" --name "$action_file" uv run "$script_dir/../shared/validation_core.py" --name "$action_file"
} }
# Check if an input is required in an action.yml file
is_input_required() {
local action_file="$1"
local input_name="$2"
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Get the 'required' property for the input
local required_status
required_status=$(uv run "$script_dir/../shared/validation_core.py" --property "$action_file" "$input_name" "required")
# Return 0 (success) if input is required, 1 (failure) if optional
[[ $required_status == "required" ]]
}
# Test input validation using Python validation module # Test input validation using Python validation module
test_input_validation() { test_input_validation() {
local action_dir="$1" local action_dir="$1"
@@ -363,5 +348,5 @@ run_action_tests() {
} }
# Export all functions # Export all functions
export -f validate_action_yml get_action_inputs get_action_outputs get_action_name is_input_required export -f validate_action_yml get_action_inputs get_action_outputs get_action_name
export -f test_input_validation test_action_outputs test_external_usage measure_action_time run_action_tests export -f test_input_validation test_action_outputs test_external_usage measure_action_time run_action_tests

View File

@@ -0,0 +1,440 @@
---
name: Integration Test - GitHub Release
on:
workflow_dispatch:
push:
paths:
- 'github-release/**'
- '_tests/integration/workflows/github-release-test.yml'
jobs:
test-github-release-validation:
name: Test Input Validation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Test invalid version format
run: |
VERSION='not.a.version'
if [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Invalid version format should have failed"
exit 1
fi
echo "✓ Invalid version format correctly rejected"
- name: Test version with alphabetic characters
run: |
VERSION='abc.def.ghi'
if [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Alphabetic version should have failed"
exit 1
fi
echo "✓ Alphabetic version correctly rejected"
- name: Test valid version formats
run: |
for version in "1.2.3" "v1.2.3" "1.0.0-alpha" "2.0.0+build"; do
if ! [[ "$version" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Valid version '$version' should have passed"
exit 1
fi
echo "✓ Valid version '$version' accepted"
done
test-github-release-version-formats:
name: Test Version Format Support
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Test basic SemVer (dry run)
run: |
echo "Testing basic SemVer format: 1.2.3"
VERSION="1.2.3"
if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Valid version rejected"
exit 1
fi
echo "✓ Basic SemVer format accepted"
- name: Test SemVer with v prefix
run: |
echo "Testing SemVer with v prefix: v1.2.3"
VERSION="v1.2.3"
if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Valid version rejected"
exit 1
fi
echo "✓ SemVer with v prefix accepted"
- name: Test prerelease version
run: |
echo "Testing prerelease version: 1.0.0-alpha.1"
VERSION="1.0.0-alpha.1"
if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Valid prerelease version rejected"
exit 1
fi
echo "✓ Prerelease version accepted"
- name: Test version with build metadata
run: |
echo "Testing version with build metadata: 1.0.0+build.123"
VERSION="1.0.0+build.123"
if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Valid build metadata version rejected"
exit 1
fi
echo "✓ Version with build metadata accepted"
- name: Test complex version
run: |
echo "Testing complex version: v2.1.0-rc.1+build.456"
VERSION="v2.1.0-rc.1+build.456"
if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Valid complex version rejected"
exit 1
fi
echo "✓ Complex version accepted"
test-github-release-tool-availability:
name: Test Tool Availability Checks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Test gh CLI detection logic
run: |
# Test the logic for checking gh availability
# In actual action, this would fail if gh is not available
if command -v gh >/dev/null 2>&1; then
echo "✓ gh CLI is available in this environment"
gh --version
else
echo "⚠️ gh CLI not available in test environment (would fail in actual action)"
echo "✓ Tool detection logic works correctly"
fi
- name: Test jq detection logic
run: |
# Test the logic for checking jq availability
# In actual action, this would fail if jq is not available
if command -v jq >/dev/null 2>&1; then
echo "✓ jq is available in this environment"
jq --version
else
echo "⚠️ jq not available in test environment (would fail in actual action)"
echo "✓ Tool detection logic works correctly"
fi
- name: Test tool requirement validation
run: |
# Verify the action correctly checks for required tools
REQUIRED_TOOLS=("gh" "jq")
echo "Testing tool requirement checks..."
for tool in "${REQUIRED_TOOLS[@]}"; do
if command -v "$tool" >/dev/null 2>&1; then
echo " ✓ $tool: available"
else
echo " ⚠️ $tool: not available (action would fail at this check)"
fi
done
echo "✓ Tool requirement validation logic verified"
- name: Test gh authentication logic
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Test authentication detection logic
if [[ -n "$GITHUB_TOKEN" ]]; then
echo "✓ GITHUB_TOKEN environment variable is set"
else
echo "⚠️ GITHUB_TOKEN not set in test environment"
fi
# Test gh auth status check (without requiring it to pass)
if command -v gh >/dev/null 2>&1; then
if gh auth status >/dev/null 2>&1; then
echo "✓ gh authentication successful"
else
echo "⚠️ gh auth check failed (expected without proper setup)"
fi
else
echo "⚠️ gh not available, skipping auth check"
fi
echo "✓ Authentication detection logic verified"
test-github-release-changelog-validation:
name: Test Changelog Validation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Test empty changelog (should trigger autogenerated notes)
run: |
echo "Testing empty changelog handling..."
CHANGELOG=""
if [[ -n "$CHANGELOG" ]]; then
echo "❌ ERROR: Empty string should be empty"
exit 1
fi
echo "✓ Empty changelog correctly detected"
- name: Test normal changelog
run: |
echo "Testing normal changelog..."
CHANGELOG="## Features
- Added new feature
- Improved performance"
if [[ -z "$CHANGELOG" ]]; then
echo "❌ ERROR: Changelog should not be empty"
exit 1
fi
if [[ ${#CHANGELOG} -gt 10000 ]]; then
echo "⚠️ Changelog is very long"
fi
echo "✓ Normal changelog processed correctly"
- name: Test very long changelog warning
run: |
echo "Testing very long changelog..."
# Create a changelog with >10000 characters
CHANGELOG=$(printf 'A%.0s' {1..10001})
if [[ ${#CHANGELOG} -le 10000 ]]; then
echo "❌ ERROR: Test changelog should be >10000 chars"
exit 1
fi
echo "✓ Long changelog warning would trigger (${#CHANGELOG} characters)"
test-github-release-changelog-types:
name: Test Changelog Content Types
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Test multiline changelog
run: |
echo "Testing multiline changelog..."
CHANGELOG="## Version 1.2.3
### Features
- Feature A
- Feature B
### Bug Fixes
- Fix #123
- Fix #456"
echo "✓ Multiline changelog supported"
- name: Test changelog with special characters
run: |
echo "Testing changelog with special characters..."
CHANGELOG='## Release Notes
Special chars: $, `, \, ", '\'', !, @, #, %, &, *, (, )'
echo "✓ Special characters in changelog supported"
- name: Test changelog with markdown
run: |
echo "Testing changelog with markdown..."
CHANGELOG="## Changes
**Bold** and *italic* text
- [x] Task completed
- [ ] Task pending
\`\`\`bash
echo 'code block'
\`\`\`
| Table | Header |
|-------|--------|
| Cell | Data |"
echo "✓ Markdown in changelog supported"
test-github-release-output-format:
name: Test Output Format
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Verify output structure (mock test)
run: |
echo "Testing output structure..."
# Check if jq is available for this test
if ! command -v jq >/dev/null 2>&1; then
echo "⚠️ jq not available, skipping JSON parsing test"
echo "✓ Output format validation logic verified (jq would be required in actual action)"
exit 0
fi
# Mock release info JSON (similar to gh release view output)
RELEASE_INFO='{
"url": "https://github.com/owner/repo/releases/tag/v1.0.0",
"id": "RE_12345",
"uploadUrl": "https://uploads.github.com/repos/owner/repo/releases/12345/assets{?name,label}"
}'
# Extract outputs
release_url=$(echo "$RELEASE_INFO" | jq -r '.url')
release_id=$(echo "$RELEASE_INFO" | jq -r '.id')
upload_url=$(echo "$RELEASE_INFO" | jq -r '.uploadUrl')
echo "Release URL: $release_url"
echo "Release ID: $release_id"
echo "Upload URL: $upload_url"
# Verify output format
if [[ ! "$release_url" =~ ^https://github\.com/.*/releases/tag/.* ]]; then
echo "❌ ERROR: Invalid release URL format"
exit 1
fi
if [[ ! "$release_id" =~ ^RE_.* ]]; then
echo "❌ ERROR: Invalid release ID format"
exit 1
fi
if [[ ! "$upload_url" =~ ^https://uploads\.github\.com/.* ]]; then
echo "❌ ERROR: Invalid upload URL format"
exit 1
fi
echo "✓ Output format validation passed"
test-github-release-integration-scenarios:
name: Test Integration Scenarios
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Test release workflow without actual creation
run: |
echo "Simulating release workflow..."
# Validate version
VERSION="v1.2.3-test"
if ! [[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
echo "❌ ERROR: Version validation failed"
exit 1
fi
echo "✓ Version validation passed"
# Check tool availability (non-fatal for test environment)
gh_available=false
jq_available=false
if command -v gh >/dev/null 2>&1; then
echo "✓ gh CLI is available"
gh_available=true
else
echo "⚠️ gh not available (would fail in actual action)"
fi
if command -v jq >/dev/null 2>&1; then
echo "✓ jq is available"
jq_available=true
else
echo "⚠️ jq not available (would fail in actual action)"
fi
# Create mock changelog
CHANGELOG="Test release notes"
NOTES_FILE="$(mktemp)"
printf '%s' "$CHANGELOG" > "$NOTES_FILE"
# Verify changelog file
if [[ ! -f "$NOTES_FILE" ]]; then
echo "❌ ERROR: Changelog file not created"
exit 1
fi
CONTENT=$(cat "$NOTES_FILE")
if [[ "$CONTENT" != "$CHANGELOG" ]]; then
echo "❌ ERROR: Changelog content mismatch"
exit 1
fi
rm -f "$NOTES_FILE"
echo "✓ Release workflow simulation passed"
- name: Test autogenerated changelog scenario
run: |
echo "Testing autogenerated changelog scenario..."
VERSION="v2.0.0"
CHANGELOG=""
if [[ -z "$CHANGELOG" ]]; then
echo "✓ Would use --generate-notes flag"
else
echo "✓ Would use custom changelog"
fi
- name: Test custom changelog scenario
run: |
echo "Testing custom changelog scenario..."
VERSION="v2.1.0"
CHANGELOG="## Custom Release Notes
This release includes:
- Feature X
- Bug fix Y"
if [[ -n "$CHANGELOG" ]]; then
echo "✓ Would use --notes-file with custom changelog"
else
echo "✓ Would use --generate-notes"
fi
integration-test-summary:
name: Integration Test Summary
runs-on: ubuntu-latest
needs:
- test-github-release-validation
- test-github-release-version-formats
- test-github-release-tool-availability
- test-github-release-changelog-validation
- test-github-release-changelog-types
- test-github-release-output-format
- test-github-release-integration-scenarios
steps:
- name: Summary
run: |
echo "=========================================="
echo "GitHub Release Integration Tests - PASSED"
echo "=========================================="
echo ""
echo "✓ Input validation tests"
echo "✓ Version format tests"
echo "✓ Tool availability tests"
echo "✓ Changelog validation tests"
echo "✓ Changelog content tests"
echo "✓ Output format tests"
echo "✓ Integration scenario tests"
echo ""
echo "All github-release integration tests completed successfully!"

View File

@@ -4,8 +4,10 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
paths: paths:
- 'eslint-lint/**' - 'eslint-check/**'
- 'prettier-lint/**' - 'eslint-fix/**'
- 'prettier-check/**'
- 'prettier-fix/**'
- 'node-setup/**' - 'node-setup/**'
- 'common-cache/**' - 'common-cache/**'
- '_tests/integration/workflows/lint-fix-chain-test.yml' - '_tests/integration/workflows/lint-fix-chain-test.yml'
@@ -62,15 +64,14 @@ jobs:
node-version: '18' node-version: '18'
working-directory: './test-project' working-directory: './test-project'
- name: Test eslint-lint check mode (should find errors) - name: Test eslint-check (should find errors)
id: eslint-check id: eslint-check
uses: ./eslint-lint uses: ./eslint-check
with: with:
mode: 'check'
working-directory: './test-project' working-directory: './test-project'
continue-on-error: true continue-on-error: true
- name: Validate eslint-lint check found issues - name: Validate eslint-check found issues
run: | run: |
echo "ESLint check outcome: ${{ steps.eslint-check.outcome }}" echo "ESLint check outcome: ${{ steps.eslint-check.outcome }}"
echo "Error count: ${{ steps.eslint-check.outputs.error-count }}" echo "Error count: ${{ steps.eslint-check.outputs.error-count }}"
@@ -85,24 +86,23 @@ jobs:
echo "✅ ESLint check validated" echo "✅ ESLint check validated"
- name: Test eslint-lint fix mode (should fix issues) - name: Test eslint-fix (should fix issues)
id: eslint-fix id: eslint-fix
uses: ./eslint-lint uses: ./eslint-fix
with: with:
mode: 'fix'
working-directory: './test-project' working-directory: './test-project'
token: ${{ github.token }} token: ${{ github.token }}
email: 'test@example.com' email: 'test@example.com'
username: 'test-user' username: 'test-user'
- name: Validate eslint-lint fix ran - name: Validate eslint-fix ran
run: | run: |
echo "Errors fixed: ${{ steps.eslint-fix.outputs.errors-fixed }}" echo "Fixed count: ${{ steps.eslint-fix.outputs.fixed-count }}"
echo "Files changed: ${{ steps.eslint-fix.outputs.files-changed }}" echo "Files fixed: ${{ steps.eslint-fix.outputs.files-fixed }}"
# Check that fixes were attempted # Check that fixes were attempted
if [[ -n "${{ steps.eslint-fix.outputs.errors-fixed }}" ]]; then if [[ -n "${{ steps.eslint-fix.outputs.fixed-count }}" ]]; then
echo "✅ ESLint fixed ${{ steps.eslint-fix.outputs.errors-fixed }} issues" echo "✅ ESLint fixed ${{ steps.eslint-fix.outputs.fixed-count }} issues"
else else
echo "⚠️ No fix count reported (may be expected if no fixable issues)" echo "⚠️ No fix count reported (may be expected if no fixable issues)"
fi fi
@@ -156,11 +156,10 @@ jobs:
node-version: '18' node-version: '18'
working-directory: './test-prettier' working-directory: './test-prettier'
- name: Test prettier-lint check mode (should find issues) - name: Test prettier-check (should find issues)
id: prettier-check id: prettier-check
uses: ./prettier-lint uses: ./prettier-check
with: with:
mode: 'check'
working-directory: './test-prettier' working-directory: './test-prettier'
continue-on-error: true continue-on-error: true
@@ -175,17 +174,16 @@ jobs:
echo "⚠️ WARNING: Expected Prettier to find formatting issues" echo "⚠️ WARNING: Expected Prettier to find formatting issues"
fi fi
- name: Test prettier-lint fix mode (should fix issues) - name: Test prettier-fix (should fix issues)
id: prettier-fix id: prettier-fix
uses: ./prettier-lint uses: ./prettier-fix
with: with:
mode: 'fix'
working-directory: './test-prettier' working-directory: './test-prettier'
token: ${{ github.token }} token: ${{ github.token }}
email: 'test@example.com' email: 'test@example.com'
username: 'test-user' username: 'test-user'
- name: Validate prettier-lint fix ran - name: Validate prettier-fix ran
run: | run: |
echo "Prettier fix completed" echo "Prettier fix completed"
@@ -263,25 +261,22 @@ jobs:
- name: Run ESLint check - name: Run ESLint check
id: lint-check id: lint-check
uses: ./eslint-lint uses: ./eslint-check
with: with:
mode: 'check'
working-directory: './test-chain' working-directory: './test-chain'
continue-on-error: true continue-on-error: true
- name: Run Prettier check - name: Run Prettier check
id: format-check id: format-check
uses: ./prettier-lint uses: ./prettier-check
with: with:
mode: 'check'
working-directory: './test-chain' working-directory: './test-chain'
continue-on-error: true continue-on-error: true
- name: Run ESLint fix - name: Run ESLint fix
id: lint-fix id: lint-fix
uses: ./eslint-lint uses: ./eslint-fix
with: with:
mode: 'fix'
working-directory: './test-chain' working-directory: './test-chain'
token: ${{ github.token }} token: ${{ github.token }}
email: 'test@example.com' email: 'test@example.com'
@@ -289,9 +284,8 @@ jobs:
- name: Run Prettier fix - name: Run Prettier fix
id: format-fix id: format-fix
uses: ./prettier-lint uses: ./prettier-fix
with: with:
mode: 'fix'
working-directory: './test-chain' working-directory: './test-chain'
token: ${{ github.token }} token: ${{ github.token }}
email: 'test@example.com' email: 'test@example.com'

View File

@@ -5,6 +5,7 @@ on:
push: push:
paths: paths:
- 'pre-commit/**' - 'pre-commit/**'
- 'set-git-config/**'
- 'validate-inputs/**' - 'validate-inputs/**'
- '_tests/integration/workflows/pre-commit-test.yml' - '_tests/integration/workflows/pre-commit-test.yml'

View File

@@ -151,14 +151,14 @@ discover_actions() {
if [[ $action_name == *"$ACTION_FILTER"* ]]; then if [[ $action_name == *"$ACTION_FILTER"* ]]; then
actions+=("$action_name") actions+=("$action_name")
fi fi
done < <(find "${TEST_ROOT}/.." -mindepth 2 -maxdepth 2 -type f -name "action.yml" -exec dirname {} \; | sort) done < <(find "${TEST_ROOT}/.." -mindepth 1 -maxdepth 1 -type d -name "*-*" | sort)
else else
# All actions # All actions
while IFS= read -r action_dir; do while IFS= read -r action_dir; do
local action_name local action_name
action_name=$(basename "$action_dir") action_name=$(basename "$action_dir")
actions+=("$action_name") actions+=("$action_name")
done < <(find "${TEST_ROOT}/.." -mindepth 2 -maxdepth 2 -type f -name "action.yml" -exec dirname {} \; | sort) done < <(find "${TEST_ROOT}/.." -mindepth 1 -maxdepth 1 -type d -name "*-*" | sort)
fi fi
log_info "Discovered ${#actions[@]} actions to test: ${actions[*]}" log_info "Discovered ${#actions[@]} actions to test: ${actions[*]}"
@@ -203,7 +203,7 @@ install_shellspec() {
# Pinned SHA256 checksum for ShellSpec 0.28.1 # Pinned SHA256 checksum for ShellSpec 0.28.1
# Source: https://github.com/shellspec/shellspec/archive/refs/tags/0.28.1.tar.gz # Source: https://github.com/shellspec/shellspec/archive/refs/tags/0.28.1.tar.gz
local checksum="400d835466429a5fe6c77a62775a9173729d61dd43e05dfa893e8cf6cb511783" local checksum="351e7a63b8df47c07b022c19d21a167b85693f5eb549fa96e64f64844b680024"
# Ensure cleanup of the downloaded file # Ensure cleanup of the downloaded file
# Use ${tarball:-} to handle unbound variable when trap fires after function returns # Use ${tarball:-} to handle unbound variable when trap fires after function returns

View File

@@ -1,142 +0,0 @@
#!/usr/bin/env shellspec
# Unit tests for action-versioning action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "action-versioning action"
ACTION_DIR="action-versioning"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating major-version input"
It "accepts valid year-based version (vYYYY)"
When call validate_input_python "action-versioning" "major-version" "v2025"
The status should be success
End
It "accepts valid semantic version (v1)"
When call validate_input_python "action-versioning" "major-version" "v1"
The status should be success
End
It "accepts valid semantic version (v2)"
When call validate_input_python "action-versioning" "major-version" "v2"
The status should be success
End
It "accepts year-based version from 2020"
When call validate_input_python "action-versioning" "major-version" "v2020"
The status should be success
End
It "accepts year-based version for 2030"
When call validate_input_python "action-versioning" "major-version" "v2030"
The status should be success
End
It "rejects version without v prefix"
When call validate_input_python "action-versioning" "major-version" "2025"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "action-versioning" "major-version" "invalid"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "action-versioning" "major-version" ""
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "action-versioning" "major-version" "v2025; rm -rf /"
The status should be failure
End
End
Context "when validating token input"
It "accepts valid GitHub token (classic)"
When call validate_input_python "action-versioning" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts valid GitHub fine-grained token"
When call validate_input_python "action-versioning" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a"
The status should be success
End
It "accepts empty token (optional input)"
When call validate_input_python "action-versioning" "token" ""
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "action-versioning" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "action-versioning" "token" "ghp_123456789012345678901234567890123456; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Action Versioning"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "major-version"
The output should include "token"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "updated"
The output should include "commit-sha"
The output should include "needs-annual-bump"
End
End
Context "when testing input requirements"
It "requires major-version input"
When call is_input_required "$ACTION_FILE" "major-version"
The status should be success
End
It "has token as optional input"
When call is_input_required "$ACTION_FILE" "token"
The status should be failure
End
End
Context "when testing security validations"
It "validates against path traversal in major-version"
When call validate_input_python "action-versioning" "major-version" "v../../etc"
The status should be failure
End
It "validates against shell metacharacters in major-version"
When call validate_input_python "action-versioning" "major-version" "v2025|echo"
The status should be failure
End
It "validates against command substitution in major-version"
When call validate_input_python "action-versioning" "major-version" "v\$(whoami)"
The status should be failure
End
It "validates against path traversal in token"
When call validate_input_python "action-versioning" "token" "../../../etc/passwd"
The status should be failure
End
End
End

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env shellspec
# Unit tests for biome-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "biome-check action"
ACTION_DIR="biome-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts personal access token"
When call validate_input_python "biome-check" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts organization token"
When call validate_input_python "biome-check" "token" "gho_123456789012345678901234567890123456"
The status should be success
End
It "accepts user token"
When call validate_input_python "biome-check" "token" "ghu_123456789012345678901234567890123456"
The status should be success
End
It "accepts server token"
When call validate_input_python "biome-check" "token" "ghs_123456789012345678901234567890123456"
The status should be success
End
It "accepts refresh token"
When call validate_input_python "biome-check" "token" "ghr_123456789012345678901234567890123456"
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "biome-check" "email" "test@example.com"
The status should be success
End
It "rejects invalid email without @"
When call validate_input_python "biome-check" "email" "testexample.com"
The status should be failure
End
It "rejects invalid email without domain"
When call validate_input_python "biome-check" "email" "test@"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "biome-check" "username" "github-actions"
The status should be success
End
It "rejects semicolon injection"
When call validate_input_python "biome-check" "username" "user;rm -rf /"
The status should be failure
End
It "rejects ampersand injection"
When call validate_input_python "biome-check" "username" "user&&malicious"
The status should be failure
End
It "rejects pipe injection"
When call validate_input_python "biome-check" "username" "user|dangerous"
The status should be failure
End
It "rejects overly long username"
When call validate_input_python "biome-check" "username" "this-username-is-definitely-too-long-for-github-maximum-length-limit"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "biome-check" "max-retries" "5"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "biome-check" "max-retries" "0"
The status should be failure
End
It "rejects negative retries"
When call validate_input_python "biome-check" "max-retries" "-1"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "biome-check" "max-retries" "15"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "biome-check" "max-retries" "invalid"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Biome Check"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "check_status"
The output should include "errors_count"
The output should include "warnings_count"
End
End
Context "when validating security"
It "rejects command injection in token"
When call validate_input_python "biome-check" "token" "ghp_123;rm -rf /"
The status should be failure
End
It "rejects command injection in email"
When call validate_input_python "biome-check" "email" "user@domain.com;rm -rf /"
The status should be failure
End
It "validates all inputs for injection patterns"
When call validate_input_python "biome-check" "max-retries" "3;malicious"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_123456789012345678901234567890123456" "username" "github-actions" "email" "test@example.com" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: biome-check"
The stderr should include "Output test passed for: biome-check"
End
End
End

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env shellspec
# Unit tests for biome-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "biome-fix action"
ACTION_DIR="biome-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts personal access token"
When call validate_input_python "biome-fix" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts organization token"
When call validate_input_python "biome-fix" "token" "gho_123456789012345678901234567890123456"
The status should be success
End
It "accepts user token"
When call validate_input_python "biome-fix" "token" "ghu_123456789012345678901234567890123456"
The status should be success
End
It "accepts server token"
When call validate_input_python "biome-fix" "token" "ghs_123456789012345678901234567890123456"
The status should be success
End
It "accepts refresh token"
When call validate_input_python "biome-fix" "token" "ghr_123456789012345678901234567890123456"
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "biome-fix" "email" "test@example.com"
The status should be success
End
It "rejects invalid email without @"
When call validate_input_python "biome-fix" "email" "testexample.com"
The status should be failure
End
It "rejects invalid email without domain"
When call validate_input_python "biome-fix" "email" "test@"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "biome-fix" "username" "github-actions"
The status should be success
End
It "rejects semicolon injection"
When call validate_input_python "biome-fix" "username" "user;rm -rf /"
The status should be failure
End
It "rejects ampersand injection"
When call validate_input_python "biome-fix" "username" "user&&malicious"
The status should be failure
End
It "rejects pipe injection"
When call validate_input_python "biome-fix" "username" "user|dangerous"
The status should be failure
End
It "rejects overly long username"
When call validate_input_python "biome-fix" "username" "this-username-is-definitely-too-long-for-github-maximum-length-limit"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "biome-fix" "max-retries" "5"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "biome-fix" "max-retries" "0"
The status should be failure
End
It "rejects negative retries"
When call validate_input_python "biome-fix" "max-retries" "-1"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "biome-fix" "max-retries" "15"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "biome-fix" "max-retries" "invalid"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Biome Fix"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "files_changed"
The output should include "fix_status"
End
End
Context "when validating security"
It "rejects command injection in token"
When call validate_input_python "biome-fix" "token" "ghp_123;rm -rf /"
The status should be failure
End
It "rejects command injection in email"
When call validate_input_python "biome-fix" "email" "user@domain.com;rm -rf /"
The status should be failure
End
It "validates all inputs for injection patterns"
When call validate_input_python "biome-fix" "max-retries" "3;malicious"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_123456789012345678901234567890123456" "username" "github-actions" "email" "test@example.com" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: biome-fix"
The stderr should include "Output test passed for: biome-fix"
End
End
End

View File

@@ -1,230 +0,0 @@
#!/usr/bin/env shellspec
# Unit tests for biome-lint action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "biome-lint action"
ACTION_DIR="biome-lint"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating mode input"
It "accepts check mode"
When call validate_input_python "biome-lint" "mode" "check"
The status should be success
End
It "accepts fix mode"
When call validate_input_python "biome-lint" "mode" "fix"
The status should be success
End
It "accepts empty mode (uses default)"
When call validate_input_python "biome-lint" "mode" ""
The status should be success
End
It "rejects invalid mode"
When call validate_input_python "biome-lint" "mode" "invalid"
The status should be failure
End
It "rejects mode with command injection"
When call validate_input_python "biome-lint" "mode" "check; rm -rf /"
The status should be failure
End
End
Context "when validating token input"
It "accepts valid GitHub token (classic)"
When call validate_input_python "biome-lint" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts valid GitHub fine-grained token"
When call validate_input_python "biome-lint" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a"
The status should be success
End
It "accepts empty token (optional)"
When call validate_input_python "biome-lint" "token" ""
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "biome-lint" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "biome-lint" "token" "ghp_123456789012345678901234567890123456; echo"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "biome-lint" "username" "github-actions"
The status should be success
End
It "accepts username with hyphens"
When call validate_input_python "biome-lint" "username" "my-bot-user"
The status should be success
End
It "accepts empty username (uses default)"
When call validate_input_python "biome-lint" "username" ""
The status should be success
End
It "rejects username with command injection"
When call validate_input_python "biome-lint" "username" "user; rm -rf /"
The status should be failure
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "biome-lint" "email" "github-actions@github.com"
The status should be success
End
It "accepts email with plus sign"
When call validate_input_python "biome-lint" "email" "user+bot@example.com"
The status should be success
End
It "accepts email with subdomain"
When call validate_input_python "biome-lint" "email" "bot@ci.example.com"
The status should be success
End
It "accepts empty email (uses default)"
When call validate_input_python "biome-lint" "email" ""
The status should be success
End
It "rejects invalid email format"
When call validate_input_python "biome-lint" "email" "not-an-email"
The status should be failure
End
It "rejects email with command injection"
When call validate_input_python "biome-lint" "email" "user@example.com; rm -rf /"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count (default)"
When call validate_input_python "biome-lint" "max-retries" "3"
The status should be success
End
It "accepts retry count of 1"
When call validate_input_python "biome-lint" "max-retries" "1"
The status should be success
End
It "accepts retry count of 10"
When call validate_input_python "biome-lint" "max-retries" "10"
The status should be success
End
It "accepts empty max-retries (uses default)"
When call validate_input_python "biome-lint" "max-retries" ""
The status should be success
End
It "rejects negative retry count"
When call validate_input_python "biome-lint" "max-retries" "-1"
The status should be failure
End
It "rejects non-numeric retry count"
When call validate_input_python "biome-lint" "max-retries" "abc"
The status should be failure
End
It "rejects retry count with command injection"
When call validate_input_python "biome-lint" "max-retries" "3; echo"
The status should be failure
End
End
Context "when validating fail-on-error input"
It "accepts true"
When call validate_input_python "biome-lint" "fail-on-error" "true"
The status should be success
End
It "accepts false"
When call validate_input_python "biome-lint" "fail-on-error" "false"
The status should be success
End
It "accepts empty (uses default)"
When call validate_input_python "biome-lint" "fail-on-error" ""
The status should be success
End
It "rejects invalid boolean value"
When call validate_input_python "biome-lint" "fail-on-error" "maybe"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Biome Lint"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "mode"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
The output should include "fail-on-error"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "status"
The output should include "errors_count"
The output should include "warnings_count"
The output should include "files_changed"
End
End
Context "when testing input requirements"
It "has all inputs as optional (with defaults)"
When call is_input_required "$ACTION_FILE" "mode"
The status should be failure
End
End
Context "when testing security validations"
It "validates against path traversal"
When call validate_input_python "biome-lint" "username" "../../../etc"
The status should be failure
End
It "validates against shell metacharacters"
When call validate_input_python "biome-lint" "email" "user@example.com|echo"
The status should be failure
End
It "validates against command substitution"
When call validate_input_python "biome-lint" "username" "\$(whoami)"
The status should be failure
End
End
End

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env shellspec
# Unit tests for common-file-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "common-file-check action"
ACTION_DIR="common-file-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating file-pattern input"
It "accepts simple file pattern"
When call validate_input_python "common-file-check" "file-pattern" "package.json"
The status should be success
End
It "accepts glob pattern with wildcard"
When call validate_input_python "common-file-check" "file-pattern" "*.json"
The status should be success
End
It "accepts glob pattern with question mark"
When call validate_input_python "common-file-check" "file-pattern" "test?.js"
The status should be success
End
It "accepts nested path pattern"
When call validate_input_python "common-file-check" "file-pattern" "src/**/*.ts"
The status should be success
End
It "accepts pattern with braces"
When call validate_input_python "common-file-check" "file-pattern" "*.{js,ts}"
The status should be success
End
It "accepts pattern with brackets"
When call validate_input_python "common-file-check" "file-pattern" "[A-Z]*.txt"
The status should be success
End
It "rejects empty file pattern"
When call validate_input_python "common-file-check" "file-pattern" ""
The status should be failure
End
It "rejects path traversal"
When call validate_input_python "common-file-check" "file-pattern" "../../../etc/passwd"
The status should be failure
End
It "rejects command injection"
When call validate_input_python "common-file-check" "file-pattern" "*.json;rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Common File Check"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "file-pattern"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "found"
End
End
Context "when validating security"
It "validates glob patterns safely"
When call validate_input_python "common-file-check" "file-pattern" "**/*.{js,ts,json}"
The status should be success
End
It "rejects injection in glob patterns"
When call validate_input_python "common-file-check" "file-pattern" "*.js&&malicious"
The status should be failure
End
It "rejects pipe injection in patterns"
When call validate_input_python "common-file-check" "file-pattern" "*.js|dangerous"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "file-pattern" "*.json"
The status should be success
The stderr should include "Testing action outputs for: common-file-check"
The stderr should include "Output test passed for: common-file-check"
End
End
End

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env shellspec
# Unit tests for common-retry action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "common-retry action"
ACTION_DIR="common-retry"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating max-retries input"
It "accepts minimum value (1)"
When call validate_input_python "common-retry" "max-retries" "1"
The status should be success
End
It "accepts maximum value (10)"
When call validate_input_python "common-retry" "max-retries" "10"
The status should be success
End
It "rejects below minimum"
When call validate_input_python "common-retry" "max-retries" "0"
The status should be failure
End
It "rejects above maximum"
When call validate_input_python "common-retry" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric"
When call validate_input_python "common-retry" "max-retries" "invalid"
The status should be failure
End
End
Context "when validating retry-delay input"
It "accepts minimum value (1)"
When call validate_input_python "common-retry" "retry-delay" "1"
The status should be success
End
It "accepts maximum value (300)"
When call validate_input_python "common-retry" "retry-delay" "300"
The status should be success
End
It "rejects below minimum"
When call validate_input_python "common-retry" "retry-delay" "0"
The status should be failure
End
It "rejects above maximum"
When call validate_input_python "common-retry" "retry-delay" "301"
The status should be failure
End
End
Context "when validating backoff-strategy input"
It "accepts linear strategy"
When call validate_input_python "common-retry" "backoff-strategy" "linear"
The status should be success
End
It "accepts exponential strategy"
When call validate_input_python "common-retry" "backoff-strategy" "exponential"
The status should be success
End
It "accepts fixed strategy"
When call validate_input_python "common-retry" "backoff-strategy" "fixed"
The status should be success
End
It "rejects invalid strategy"
When call validate_input_python "common-retry" "backoff-strategy" "invalid"
The status should be failure
End
End
Context "when validating timeout input"
It "accepts minimum value (1)"
When call validate_input_python "common-retry" "timeout" "1"
The status should be success
End
It "accepts maximum value (3600)"
When call validate_input_python "common-retry" "timeout" "3600"
The status should be success
End
It "rejects below minimum"
When call validate_input_python "common-retry" "timeout" "0"
The status should be failure
End
It "rejects above maximum"
When call validate_input_python "common-retry" "timeout" "3601"
The status should be failure
End
End
Context "when validating working-directory input"
It "accepts current directory"
When call validate_input_python "common-retry" "working-directory" "."
The status should be success
End
It "accepts relative path"
When call validate_input_python "common-retry" "working-directory" "src/app"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "common-retry" "working-directory" "../../../etc"
The status should be failure
End
End
Context "when validating shell input"
It "accepts bash shell"
When call validate_input_python "common-retry" "shell" "bash"
The status should be success
End
It "accepts sh shell"
When call validate_input_python "common-retry" "shell" "sh"
The status should be success
End
It "rejects zsh shell"
When call validate_input_python "common-retry" "shell" "zsh"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Common Retry"
End
End
Context "when validating security"
It "rejects command injection with semicolon"
When call validate_input_python "common-retry" "command" "value; rm -rf /"
The status should be failure
End
It "rejects command injection with ampersand"
When call validate_input_python "common-retry" "command" "value && malicious"
The status should be failure
End
It "accepts valid success codes"
When call validate_input_python "common-retry" "success-codes" "0,1,2"
The status should be success
End
It "rejects success codes with injection"
When call validate_input_python "common-retry" "success-codes" "0;rm -rf /"
The status should be failure
End
It "accepts valid retry codes"
When call validate_input_python "common-retry" "retry-codes" "1,126,127"
The status should be success
End
It "rejects retry codes with injection"
When call validate_input_python "common-retry" "retry-codes" "1;rm -rf /"
The status should be failure
End
End
End

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env shellspec
# Unit tests for docker-publish-gh action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "docker-publish-gh action"
ACTION_DIR="docker-publish-gh"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs"
It "accepts valid image name"
When call validate_input_python "docker-publish-gh" "image-name" "myapp"
The status should be success
End
It "accepts valid GitHub token"
When call validate_input_python "docker-publish-gh" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts valid tags"
When call validate_input_python "docker-publish-gh" "tags" "v1.0.0,latest"
The status should be success
End
It "rejects injection in token"
When call validate_input_python "docker-publish-gh" "token" "ghp_123;malicious"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*Docker*"
End
End
End

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env shellspec
# Unit tests for docker-publish-hub action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "docker-publish-hub action"
ACTION_DIR="docker-publish-hub"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs"
It "accepts valid image name"
When call validate_input_python "docker-publish-hub" "image-name" "myapp"
The status should be success
End
It "accepts valid username"
When call validate_input_python "docker-publish-hub" "username" "dockeruser"
The status should be success
End
It "accepts valid password"
When call validate_input_python "docker-publish-hub" "password" "secretpassword123"
The status should be success
End
It "accepts valid tags"
When call validate_input_python "docker-publish-hub" "tags" "v1.0.0,latest"
The status should be success
End
It "rejects injection in username"
When call validate_input_python "docker-publish-hub" "username" "user;malicious"
The status should be failure
End
It "rejects injection in password"
When call validate_input_python "docker-publish-hub" "password" "pass;rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*Docker*"
End
End
End

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env shellspec
# Unit tests for dotnet-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "dotnet-version-detect action"
ACTION_DIR="dotnet-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating default-version input"
It "accepts valid dotnet version"
When call validate_input_python "dotnet-version-detect" "default-version" "8.0"
The status should be success
End
It "accepts full semantic version"
When call validate_input_python "dotnet-version-detect" "default-version" "8.0.0"
The status should be success
End
It "accepts dotnet 6 version"
When call validate_input_python "dotnet-version-detect" "default-version" "6.0.0"
The status should be success
End
It "accepts dotnet 7 version"
When call validate_input_python "dotnet-version-detect" "default-version" "7.0.0"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "dotnet-version-detect" "default-version" "invalid"
The status should be failure
End
It "rejects version with leading zeros"
When call validate_input_python "dotnet-version-detect" "default-version" "08.0.0"
The status should be failure
End
It "rejects unsupported version"
When call validate_input_python "dotnet-version-detect" "default-version" "2.0.0"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Dotnet Version Detect"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "default-version"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "dotnet-version"
End
End
Context "when validating security"
It "rejects injection in version"
When call validate_input_python "dotnet-version-detect" "default-version" "8.0;malicious"
The status should be failure
End
It "validates version security"
When call validate_input_python "dotnet-version-detect" "default-version" "8.0&&malicious"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "default-version" "8.0"
The status should be success
The stderr should include "Testing action outputs for: dotnet-version-detect"
The stderr should include "Output test passed for: dotnet-version-detect"
End
End
End

View File

@@ -0,0 +1,355 @@
#!/usr/bin/env shellspec
# Unit tests for eslint-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "eslint-check action"
ACTION_DIR="eslint-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating working-directory input"
It "accepts current directory"
When call validate_input_python "eslint-check" "working-directory" "."
The status should be success
End
It "accepts relative path"
When call validate_input_python "eslint-check" "working-directory" "src/frontend"
The status should be success
End
It "accepts nested directory"
When call validate_input_python "eslint-check" "working-directory" "packages/ui"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "eslint-check" "working-directory" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute paths"
When call validate_input_python "eslint-check" "working-directory" "/etc/passwd"
The status should be failure
End
It "rejects injection attempts"
When call validate_input_python "eslint-check" "working-directory" "src; rm -rf /"
The status should be failure
End
End
Context "when validating eslint-version input"
It "accepts latest version"
When call validate_input_python "eslint-check" "eslint-version" "latest"
The status should be success
End
It "accepts semantic version"
When call validate_input_python "eslint-check" "eslint-version" "8.57.0"
The status should be success
End
It "accepts version with prerelease"
When call validate_input_python "eslint-check" "eslint-version" "9.0.0-alpha.0"
The status should be success
End
It "accepts older stable version"
When call validate_input_python "eslint-check" "eslint-version" "7.32.0"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "eslint-check" "eslint-version" "8.57"
The status should be failure
End
It "rejects version with letters"
When call validate_input_python "eslint-check" "eslint-version" "8.57.0a"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "eslint-check" "eslint-version" ""
The status should be failure
End
End
Context "when validating config-file input"
It "accepts default eslintrc"
When call validate_input_python "eslint-check" "config-file" ".eslintrc"
The status should be success
End
It "accepts eslintrc.json"
When call validate_input_python "eslint-check" "config-file" ".eslintrc.json"
The status should be success
End
It "accepts eslint.config.js"
When call validate_input_python "eslint-check" "config-file" "eslint.config.js"
The status should be success
End
It "accepts relative path config"
When call validate_input_python "eslint-check" "config-file" "config/eslint.json"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "eslint-check" "config-file" "../../../malicious.js"
The status should be failure
End
It "rejects injection in config path"
When call validate_input_python "eslint-check" "config-file" "config.js;rm -rf /"
The status should be failure
End
End
Context "when validating ignore-file input"
It "accepts default eslintignore"
When call validate_input_python "eslint-check" "ignore-file" ".eslintignore"
The status should be success
End
It "accepts custom ignore file"
When call validate_input_python "eslint-check" "ignore-file" "eslint-ignore.txt"
The status should be success
End
It "accepts relative path ignore file"
When call validate_input_python "eslint-check" "ignore-file" "config/.eslintignore"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "eslint-check" "ignore-file" "../../sensitive.txt"
The status should be failure
End
End
Context "when validating file-extensions input"
It "accepts default extensions"
When call validate_input_python "eslint-check" "file-extensions" ".js,.jsx,.ts,.tsx"
The status should be success
End
It "accepts single extension"
When call validate_input_python "eslint-check" "file-extensions" ".js"
The status should be success
End
It "accepts TypeScript extensions only"
When call validate_input_python "eslint-check" "file-extensions" ".ts,.tsx"
The status should be success
End
It "accepts Vue and JavaScript extensions"
When call validate_input_python "eslint-check" "file-extensions" ".js,.vue,.ts"
The status should be success
End
It "rejects extensions without dots"
When call validate_input_python "eslint-check" "file-extensions" "js,ts"
The status should be failure
End
It "rejects invalid extension format"
When call validate_input_python "eslint-check" "file-extensions" ".js;.ts"
The status should be failure
End
It "rejects extensions with special characters"
When call validate_input_python "eslint-check" "file-extensions" ".js,.t$"
The status should be failure
End
End
Context "when validating boolean inputs"
It "accepts cache as true"
When call validate_input_python "eslint-check" "cache" "true"
The status should be success
End
It "accepts cache as false"
When call validate_input_python "eslint-check" "cache" "false"
The status should be success
End
It "accepts fail-on-error as true"
When call validate_input_python "eslint-check" "fail-on-error" "true"
The status should be success
End
It "accepts fail-on-error as false"
When call validate_input_python "eslint-check" "fail-on-error" "false"
The status should be success
End
It "rejects invalid boolean value"
When call validate_input_python "eslint-check" "cache" "maybe"
The status should be failure
End
It "rejects numeric boolean"
When call validate_input_python "eslint-check" "fail-on-error" "1"
The status should be failure
End
End
Context "when validating numeric inputs"
It "accepts zero max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "0"
The status should be success
End
It "accepts reasonable max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "10"
The status should be success
End
It "accepts large max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "1000"
The status should be success
End
It "accepts valid max-retries"
When call validate_input_python "eslint-check" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "eslint-check" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "eslint-check" "max-retries" "10"
The status should be success
End
It "rejects negative max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "-1"
The status should be failure
End
It "rejects non-numeric max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "many"
The status should be failure
End
It "rejects zero retries"
When call validate_input_python "eslint-check" "max-retries" "0"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "eslint-check" "max-retries" "15"
The status should be failure
End
End
Context "when validating report-format input"
It "accepts stylish format"
When call validate_input_python "eslint-check" "report-format" "stylish"
The status should be success
End
It "accepts json format"
When call validate_input_python "eslint-check" "report-format" "json"
The status should be success
End
It "accepts sarif format"
When call validate_input_python "eslint-check" "report-format" "sarif"
The status should be success
End
It "accepts checkstyle format"
When call validate_input_python "eslint-check" "report-format" "checkstyle"
The status should be success
End
It "accepts compact format"
When call validate_input_python "eslint-check" "report-format" "compact"
The status should be success
End
It "accepts html format"
When call validate_input_python "eslint-check" "report-format" "html"
The status should be success
End
It "accepts junit format"
When call validate_input_python "eslint-check" "report-format" "junit"
The status should be success
End
It "accepts tap format"
When call validate_input_python "eslint-check" "report-format" "tap"
The status should be success
End
It "accepts unix format"
When call validate_input_python "eslint-check" "report-format" "unix"
The status should be success
End
It "rejects invalid format"
When call validate_input_python "eslint-check" "report-format" "invalid"
The status should be failure
End
It "rejects empty format"
When call validate_input_python "eslint-check" "report-format" ""
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "ESLint Check"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "working-directory"
The output should include "eslint-version"
The output should include "max-retries"
End
It "defines optional inputs with defaults"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "config-file"
The output should include "ignore-file"
The output should include "file-extensions"
The output should include "cache"
The output should include "max-warnings"
The output should include "fail-on-error"
The output should include "report-format"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "error-count"
The output should include "warning-count"
The output should include "sarif-file"
The output should include "files-checked"
End
It "has composite run type"
When call grep -q "using: composite" "$ACTION_FILE"
The status should be success
End
It "includes input validation step"
When call grep -q "Validate Inputs" "$ACTION_FILE"
The status should be success
End
It "uses node-setup action"
When call grep -q "./node-setup" "$ACTION_FILE"
The status should be success
End
It "uses common-cache action"
When call grep -q "./common-cache" "$ACTION_FILE"
The status should be success
End
End
Context "when validating security"
It "validates input paths to prevent injection"
When call validate_input_python "eslint-check" "working-directory" "../../../etc"
The status should be failure
End
It "validates config file paths"
When call validate_input_python "eslint-check" "config-file" "../../malicious.js"
The status should be failure
End
It "sanitizes file extensions input"
When call validate_input_python "eslint-check" "file-extensions" ".js;rm -rf /"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "working-directory" "." "eslint-version" "latest" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: eslint-check"
The stderr should include "Output test passed for: eslint-check"
End
It "outputs consistent error and warning counts"
When call test_action_outputs "$ACTION_DIR" "max-warnings" "0" "report-format" "sarif"
The status should be success
The stderr should include "Testing action outputs for: eslint-check"
The stderr should include "Output test passed for: eslint-check"
End
End
End

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env shellspec
# Unit tests for eslint-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "eslint-fix action"
ACTION_DIR="eslint-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts valid GitHub token"
When call validate_input_python "eslint-fix" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "rejects injection in token"
When call validate_input_python "eslint-fix" "token" "token; rm -rf /"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "eslint-fix" "username" "github-actions"
The status should be success
End
It "rejects injection in username"
When call validate_input_python "eslint-fix" "username" "user; rm -rf /"
The status should be failure
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "eslint-fix" "email" "test@example.com"
The status should be success
End
It "rejects invalid email format"
When call validate_input_python "eslint-fix" "email" "invalid-email"
The status should be failure
End
End
Context "when validating numeric inputs"
It "accepts valid max-retries"
When call validate_input_python "eslint-fix" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "eslint-fix" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "eslint-fix" "max-retries" "10"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "eslint-fix" "max-retries" "0"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "eslint-fix" "max-retries" "15"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "ESLint Fix"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "files_changed"
The output should include "lint_status"
The output should include "errors_fixed"
End
End
Context "when validating security"
It "validates token format"
When call validate_input_python "eslint-fix" "token" "invalid-token;rm -rf /"
The status should be failure
End
It "validates email format"
When call validate_input_python "eslint-fix" "email" "invalid@email"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_test" "username" "test" "email" "test@example.com" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: eslint-fix"
The stderr should include "Output test passed for: eslint-fix"
End
End
End

View File

@@ -1,527 +0,0 @@
#!/usr/bin/env shellspec
# Unit tests for eslint-lint action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "eslint-lint action"
ACTION_DIR="eslint-lint"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating mode input"
It "accepts check mode"
When call validate_input_python "eslint-lint" "mode" "check"
The status should be success
End
It "accepts fix mode"
When call validate_input_python "eslint-lint" "mode" "fix"
The status should be success
End
It "accepts empty mode (uses default)"
When call validate_input_python "eslint-lint" "mode" ""
The status should be success
End
It "rejects invalid mode"
When call validate_input_python "eslint-lint" "mode" "invalid"
The status should be failure
End
It "rejects mode with command injection"
When call validate_input_python "eslint-lint" "mode" "check; rm -rf /"
The status should be failure
End
End
Context "when validating working-directory input"
It "accepts default directory"
When call validate_input_python "eslint-lint" "working-directory" "."
The status should be success
End
It "accepts valid subdirectory"
When call validate_input_python "eslint-lint" "working-directory" "src"
The status should be success
End
It "accepts empty working-directory (uses default)"
When call validate_input_python "eslint-lint" "working-directory" ""
The status should be success
End
It "rejects path traversal"
When call validate_input_python "eslint-lint" "working-directory" "../../../etc"
The status should be failure
End
It "rejects directory with command injection"
When call validate_input_python "eslint-lint" "working-directory" "src; rm -rf /"
The status should be failure
End
End
Context "when validating eslint-version input"
It "accepts latest version"
When call validate_input_python "eslint-lint" "eslint-version" "latest"
The status should be success
End
It "accepts semantic version"
When call validate_input_python "eslint-lint" "eslint-version" "8.57.0"
The status should be success
End
It "accepts major.minor version"
When call validate_input_python "eslint-lint" "eslint-version" "8.57"
The status should be success
End
It "accepts major version"
When call validate_input_python "eslint-lint" "eslint-version" "8"
The status should be success
End
It "accepts version with pre-release"
When call validate_input_python "eslint-lint" "eslint-version" "9.0.0-beta.1"
The status should be success
End
It "accepts empty version (uses default)"
When call validate_input_python "eslint-lint" "eslint-version" ""
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "eslint-lint" "eslint-version" "invalid"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "eslint-lint" "eslint-version" "8.57.0; echo"
The status should be failure
End
End
Context "when validating config-file input"
It "accepts default config file"
When call validate_input_python "eslint-lint" "config-file" ".eslintrc"
The status should be success
End
It "accepts custom config file"
When call validate_input_python "eslint-lint" "config-file" ".eslintrc.js"
The status should be success
End
It "accepts config file in subdirectory"
When call validate_input_python "eslint-lint" "config-file" "config/eslint.config.js"
The status should be success
End
It "accepts empty config-file (uses default)"
When call validate_input_python "eslint-lint" "config-file" ""
The status should be success
End
It "rejects config file with path traversal"
When call validate_input_python "eslint-lint" "config-file" "../../../.eslintrc"
The status should be failure
End
It "rejects config file with command injection"
When call validate_input_python "eslint-lint" "config-file" ".eslintrc; rm -rf /"
The status should be failure
End
End
Context "when validating ignore-file input"
It "accepts default ignore file"
When call validate_input_python "eslint-lint" "ignore-file" ".eslintignore"
The status should be success
End
It "accepts custom ignore file"
When call validate_input_python "eslint-lint" "ignore-file" ".eslintignore.custom"
The status should be success
End
It "accepts empty ignore-file (uses default)"
When call validate_input_python "eslint-lint" "ignore-file" ""
The status should be success
End
It "rejects ignore file with path traversal"
When call validate_input_python "eslint-lint" "ignore-file" "../../../.eslintignore"
The status should be failure
End
It "rejects ignore file with command injection"
When call validate_input_python "eslint-lint" "ignore-file" ".eslintignore; echo"
The status should be failure
End
End
Context "when validating file-extensions input"
It "accepts default extensions"
When call validate_input_python "eslint-lint" "file-extensions" ".js,.jsx,.ts,.tsx"
The status should be success
End
It "accepts single extension"
When call validate_input_python "eslint-lint" "file-extensions" ".js"
The status should be success
End
It "accepts multiple extensions"
When call validate_input_python "eslint-lint" "file-extensions" ".js,.ts,.mjs"
The status should be success
End
It "accepts empty file-extensions (uses default)"
When call validate_input_python "eslint-lint" "file-extensions" ""
The status should be success
End
It "rejects extensions without leading dot"
When call validate_input_python "eslint-lint" "file-extensions" "js,jsx"
The status should be failure
End
It "rejects extensions with command injection"
When call validate_input_python "eslint-lint" "file-extensions" ".js; rm -rf /"
The status should be failure
End
End
Context "when validating cache input"
It "accepts true"
When call validate_input_python "eslint-lint" "cache" "true"
The status should be success
End
It "accepts false"
When call validate_input_python "eslint-lint" "cache" "false"
The status should be success
End
It "accepts empty cache (uses default)"
When call validate_input_python "eslint-lint" "cache" ""
The status should be success
End
It "rejects invalid boolean value"
When call validate_input_python "eslint-lint" "cache" "maybe"
The status should be failure
End
End
Context "when validating max-warnings input"
It "accepts default value 0"
When call validate_input_python "eslint-lint" "max-warnings" "0"
The status should be success
End
It "accepts positive integer"
When call validate_input_python "eslint-lint" "max-warnings" "10"
The status should be success
End
It "accepts large number"
When call validate_input_python "eslint-lint" "max-warnings" "1000"
The status should be success
End
It "accepts empty max-warnings (uses default)"
When call validate_input_python "eslint-lint" "max-warnings" ""
The status should be success
End
It "rejects negative number"
When call validate_input_python "eslint-lint" "max-warnings" "-1"
The status should be failure
End
It "rejects non-numeric value"
When call validate_input_python "eslint-lint" "max-warnings" "abc"
The status should be failure
End
It "rejects max-warnings with command injection"
When call validate_input_python "eslint-lint" "max-warnings" "0; echo"
The status should be failure
End
End
Context "when validating fail-on-error input"
It "accepts true"
When call validate_input_python "eslint-lint" "fail-on-error" "true"
The status should be success
End
It "accepts false"
When call validate_input_python "eslint-lint" "fail-on-error" "false"
The status should be success
End
It "accepts empty fail-on-error (uses default)"
When call validate_input_python "eslint-lint" "fail-on-error" ""
The status should be success
End
It "rejects invalid boolean value"
When call validate_input_python "eslint-lint" "fail-on-error" "yes"
The status should be failure
End
End
Context "when validating report-format input"
It "accepts stylish format"
When call validate_input_python "eslint-lint" "report-format" "stylish"
The status should be success
End
It "accepts json format"
When call validate_input_python "eslint-lint" "report-format" "json"
The status should be success
End
It "accepts sarif format"
When call validate_input_python "eslint-lint" "report-format" "sarif"
The status should be success
End
It "accepts empty report-format (uses default)"
When call validate_input_python "eslint-lint" "report-format" ""
The status should be success
End
It "rejects invalid format"
When call validate_input_python "eslint-lint" "report-format" "invalid"
The status should be failure
End
It "rejects format with command injection"
When call validate_input_python "eslint-lint" "report-format" "json; rm -rf /"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts default value 3"
When call validate_input_python "eslint-lint" "max-retries" "3"
The status should be success
End
It "accepts retry count of 1"
When call validate_input_python "eslint-lint" "max-retries" "1"
The status should be success
End
It "accepts retry count of 10"
When call validate_input_python "eslint-lint" "max-retries" "10"
The status should be success
End
It "accepts empty max-retries (uses default)"
When call validate_input_python "eslint-lint" "max-retries" ""
The status should be success
End
It "rejects zero retries"
When call validate_input_python "eslint-lint" "max-retries" "0"
The status should be failure
End
It "rejects negative retry count"
When call validate_input_python "eslint-lint" "max-retries" "-1"
The status should be failure
End
It "rejects retry count above 10"
When call validate_input_python "eslint-lint" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric retry count"
When call validate_input_python "eslint-lint" "max-retries" "abc"
The status should be failure
End
It "rejects retry count with command injection"
When call validate_input_python "eslint-lint" "max-retries" "3; echo"
The status should be failure
End
End
Context "when validating token input"
It "accepts valid GitHub token (classic)"
When call validate_input_python "eslint-lint" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts valid GitHub fine-grained token"
When call validate_input_python "eslint-lint" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a"
The status should be success
End
It "accepts empty token (optional)"
When call validate_input_python "eslint-lint" "token" ""
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "eslint-lint" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "eslint-lint" "token" "ghp_123456789012345678901234567890123456; rm -rf /"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "eslint-lint" "username" "github-actions"
The status should be success
End
It "accepts username with hyphens"
When call validate_input_python "eslint-lint" "username" "my-bot-user"
The status should be success
End
It "accepts alphanumeric username"
When call validate_input_python "eslint-lint" "username" "user123"
The status should be success
End
It "accepts empty username (uses default)"
When call validate_input_python "eslint-lint" "username" ""
The status should be success
End
It "rejects username with command injection"
When call validate_input_python "eslint-lint" "username" "user; rm -rf /"
The status should be failure
End
It "rejects username with special characters"
When call validate_input_python "eslint-lint" "username" "user@bot"
The status should be failure
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "eslint-lint" "email" "github-actions@github.com"
The status should be success
End
It "accepts email with plus sign"
When call validate_input_python "eslint-lint" "email" "user+bot@example.com"
The status should be success
End
It "accepts email with subdomain"
When call validate_input_python "eslint-lint" "email" "bot@ci.example.com"
The status should be success
End
It "accepts empty email (uses default)"
When call validate_input_python "eslint-lint" "email" ""
The status should be success
End
It "rejects invalid email format"
When call validate_input_python "eslint-lint" "email" "not-an-email"
The status should be failure
End
It "rejects email with command injection"
When call validate_input_python "eslint-lint" "email" "user@example.com; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "ESLint Lint"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "mode"
The output should include "working-directory"
The output should include "eslint-version"
The output should include "config-file"
The output should include "ignore-file"
The output should include "file-extensions"
The output should include "cache"
The output should include "max-warnings"
The output should include "fail-on-error"
The output should include "report-format"
The output should include "max-retries"
The output should include "token"
The output should include "username"
The output should include "email"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "status"
The output should include "error-count"
The output should include "warning-count"
The output should include "sarif-file"
The output should include "files-checked"
The output should include "files-changed"
The output should include "errors-fixed"
End
End
Context "when testing input requirements"
It "has all inputs as optional (with defaults)"
When call is_input_required "$ACTION_FILE" "mode"
The status should be failure
End
End
Context "when testing security validations"
It "validates against path traversal in working-directory"
When call validate_input_python "eslint-lint" "working-directory" "../../../etc"
The status should be failure
End
It "validates against shell metacharacters in mode"
When call validate_input_python "eslint-lint" "mode" "check|echo"
The status should be failure
End
It "validates against command substitution in config-file"
When call validate_input_python "eslint-lint" "config-file" "\$(whoami)"
The status should be failure
End
It "validates against path traversal in token"
When call validate_input_python "eslint-lint" "token" "../../../etc/passwd"
The status should be failure
End
It "validates against shell metacharacters in username"
When call validate_input_python "eslint-lint" "username" "user&whoami"
The status should be failure
End
It "validates against command injection in email"
When call validate_input_python "eslint-lint" "email" "user@example.com\`whoami\`"
The status should be failure
End
End
End

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env shellspec
# Unit tests for github-release action validation and logic
# Framework is automatically loaded via spec_helper.sh
# Using the centralized validate_input_python function from spec_helper.sh
Describe "github-release action"
ACTION_DIR="github-release"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating version input"
It "accepts valid semantic version"
When call validate_input_python "github-release" "version" "1.2.3"
The status should be success
End
It "accepts semantic version with v prefix"
When call validate_input_python "github-release" "version" "v1.2.3"
The status should be success
End
It "accepts prerelease version"
When call validate_input_python "github-release" "version" "1.2.3-alpha"
The status should be success
End
It "accepts build metadata version"
When call validate_input_python "github-release" "version" "1.2.3+build.1"
The status should be success
End
It "accepts prerelease with build metadata"
When call validate_input_python "github-release" "version" "1.2.3-alpha.1+build.1"
The status should be success
End
It "accepts CalVer format"
When call validate_input_python "github-release" "version" "2024.3.1"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "github-release" "version" "invalid-version"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "github-release" "version" "1.2.3; rm -rf /"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "github-release" "version" ""
The status should be failure
End
End
Context "when validating changelog input"
It "accepts empty changelog"
When call validate_input_python "github-release" "changelog" ""
The status should be success
End
It "accepts normal changelog content"
When call validate_input_python "github-release" "changelog" "## What's Changed\n- Fixed bug #123\n- Added feature X"
The status should be success
End
It "accepts changelog with special characters"
When call validate_input_python "github-release" "changelog" "Version 1.2.3\n\n- Bug fixes & improvements\n- Added @mention support"
The status should be success
End
It "rejects changelog with command injection"
When call validate_input_python "github-release" "changelog" "Release notes; rm -rf /"
The status should be failure
End
It "rejects changelog with shell expansion"
When call validate_input_python "github-release" "changelog" "Release \$(whoami) notes"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "GitHub Release"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "version"
The output should include "changelog"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "release_url"
The output should include "release_id"
The output should include "upload_url"
End
End
Context "when testing input requirements"
It "requires version input"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "version"
End
It "has changelog as optional input"
# Test that changelog has a default value in action.yml
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "changelog" "optional"
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "github-release" "version" "../1.2.3"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "github-release" "version" "1.2.3|echo"
The status should be failure
End
It "validates against shell metacharacters in changelog"
When call validate_input_python "github-release" "changelog" "Release notes|echo test"
The status should be failure
End
End
End

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env shellspec
# Unit tests for go-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "go-version-detect action"
ACTION_DIR="go-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
# Test version constants (update these when Go releases new versions)
CURRENT_STABLE_GO_VERSION="1.25"
CURRENT_STABLE_GO_PATCH="1.25.0"
PREVIOUS_GO_VERSION="1.24.0"
MIN_SUPPORTED_GO_VERSION="1.18"
MAX_SUPPORTED_GO_VERSION="1.30"
TOO_OLD_GO_VERSION="1.17"
TOO_NEW_GO_VERSION="1.31"
Context "when validating default-version input"
It "accepts valid semantic version"
When call validate_input_python "go-version-detect" "default-version" "$CURRENT_STABLE_GO_VERSION"
The status should be success
End
It "accepts semantic version with patch"
When call validate_input_python "go-version-detect" "default-version" "$PREVIOUS_GO_VERSION"
The status should be success
End
It "accepts minimum supported Go version"
When call validate_input_python "go-version-detect" "default-version" "$MIN_SUPPORTED_GO_VERSION"
The status should be success
End
It "accepts current stable Go version"
When call validate_input_python "go-version-detect" "default-version" "$CURRENT_STABLE_GO_PATCH"
The status should be success
End
It "rejects version without minor"
When call validate_input_python "go-version-detect" "default-version" "1"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "go-version-detect" "default-version" "invalid-version"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}; rm -rf /"
The status should be failure
End
It "rejects version with shell expansion"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\$(echo test)"
The status should be failure
End
It "rejects major version other than 1"
When call validate_input_python "go-version-detect" "default-version" "2.0"
The status should be failure
End
It "rejects too old minor version"
When call validate_input_python "go-version-detect" "default-version" "$TOO_OLD_GO_VERSION"
The status should be failure
End
It "rejects too new minor version"
When call validate_input_python "go-version-detect" "default-version" "$TOO_NEW_GO_VERSION"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "go-version-detect" "default-version" ""
The status should be failure
End
It "rejects version with leading v"
When call validate_input_python "go-version-detect" "default-version" "v${CURRENT_STABLE_GO_VERSION}"
The status should be failure
End
It "rejects version with prerelease"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}-beta"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Go Version Detect"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "go-version"
End
End
Context "when testing input requirements"
It "has default-version as optional input"
# Test that default-version has a default value in action.yml
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional"
The output should equal "optional"
End
It "has correct default version"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "default"
The output should equal "$CURRENT_STABLE_GO_VERSION"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "go-version-detect" "default-version" "../${CURRENT_STABLE_GO_VERSION}"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}|echo"
The status should be failure
End
It "validates against backtick injection"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\`whoami\`"
The status should be failure
End
It "validates against variable expansion"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\${HOME}"
The status should be failure
End
End
Context "when testing version range validation"
It "validates reasonable Go version range boundaries"
# Test boundary conditions for Go version validation
When call validate_input_python "go-version-detect" "default-version" "$TOO_OLD_GO_VERSION"
The status should be failure
End
It "validates upper boundary"
When call validate_input_python "go-version-detect" "default-version" "$TOO_NEW_GO_VERSION"
The status should be failure
End
It "validates exact boundary valid values"
When call validate_input_python "go-version-detect" "default-version" "$MIN_SUPPORTED_GO_VERSION"
The status should be success
End
It "validates exact boundary valid values upper"
When call validate_input_python "go-version-detect" "default-version" "$MAX_SUPPORTED_GO_VERSION"
The status should be success
End
End
End

View File

@@ -1,297 +0,0 @@
#!/usr/bin/env shellspec
# Unit tests for language-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "language-version-detect action"
ACTION_DIR="language-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating language input"
It "accepts php language"
When call validate_input_python "language-version-detect" "language" "php"
The status should be success
End
It "accepts python language"
When call validate_input_python "language-version-detect" "language" "python"
The status should be success
End
It "accepts go language"
When call validate_input_python "language-version-detect" "language" "go"
The status should be success
End
It "accepts dotnet language"
When call validate_input_python "language-version-detect" "language" "dotnet"
The status should be success
End
It "rejects invalid language"
When call validate_input_python "language-version-detect" "language" "javascript"
The status should be failure
End
It "rejects empty language (required)"
When call validate_input_python "language-version-detect" "language" ""
The status should be failure
End
It "rejects language with command injection"
When call validate_input_python "language-version-detect" "language" "php; rm -rf /"
The status should be failure
End
It "rejects language with shell metacharacters"
When call validate_input_python "language-version-detect" "language" "php|echo"
The status should be failure
End
End
Context "when validating default-version input for PHP"
It "accepts valid PHP version 8.4"
When call validate_input_python "language-version-detect" "default-version" "8.4"
The status should be success
End
It "accepts valid PHP version 8.3"
When call validate_input_python "language-version-detect" "default-version" "8.3"
The status should be success
End
It "accepts valid PHP version 7.4"
When call validate_input_python "language-version-detect" "default-version" "7.4"
The status should be success
End
It "accepts valid PHP version with patch 8.3.1"
When call validate_input_python "language-version-detect" "default-version" "8.3.1"
The status should be success
End
It "accepts empty default-version (uses language default)"
When call validate_input_python "language-version-detect" "default-version" ""
The status should be success
End
It "rejects invalid PHP version format"
When call validate_input_python "language-version-detect" "default-version" "invalid"
The status should be failure
End
End
Context "when validating default-version input for Python"
It "accepts valid Python version 3.12"
When call validate_input_python "language-version-detect" "default-version" "3.12"
The status should be success
End
It "accepts valid Python version 3.11"
When call validate_input_python "language-version-detect" "default-version" "3.11"
The status should be success
End
It "accepts valid Python version 3.10"
When call validate_input_python "language-version-detect" "default-version" "3.10"
The status should be success
End
It "accepts valid Python version with patch 3.12.1"
When call validate_input_python "language-version-detect" "default-version" "3.12.1"
The status should be success
End
It "accepts valid Python version 3.9"
When call validate_input_python "language-version-detect" "default-version" "3.9"
The status should be success
End
It "accepts valid Python version 3.8"
When call validate_input_python "language-version-detect" "default-version" "3.8"
The status should be success
End
End
Context "when validating default-version input for Go"
It "accepts valid Go version 1.21"
When call validate_input_python "language-version-detect" "default-version" "1.21"
The status should be success
End
It "accepts valid Go version 1.20"
When call validate_input_python "language-version-detect" "default-version" "1.20"
The status should be success
End
It "accepts valid Go version with patch 1.21.5"
When call validate_input_python "language-version-detect" "default-version" "1.21.5"
The status should be success
End
It "accepts valid Go version 1.22"
When call validate_input_python "language-version-detect" "default-version" "1.22"
The status should be success
End
End
Context "when validating default-version input for .NET"
It "accepts valid .NET version 7.0"
When call validate_input_python "language-version-detect" "default-version" "7.0"
The status should be success
End
It "accepts valid .NET version 8.0"
When call validate_input_python "language-version-detect" "default-version" "8.0"
The status should be success
End
It "accepts valid .NET version 6.0"
When call validate_input_python "language-version-detect" "default-version" "6.0"
The status should be success
End
It "accepts valid .NET version with patch 7.0.1"
When call validate_input_python "language-version-detect" "default-version" "7.0.1"
The status should be success
End
It "accepts valid .NET major version 7"
When call validate_input_python "language-version-detect" "default-version" "7"
The status should be success
End
End
Context "when validating default-version input edge cases"
It "rejects version with v prefix"
When call validate_input_python "language-version-detect" "default-version" "v3.12"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "language-version-detect" "default-version" "3.12; rm -rf /"
The status should be failure
End
It "rejects version with shell metacharacters"
When call validate_input_python "language-version-detect" "default-version" "3.12|echo"
The status should be failure
End
It "rejects version with command substitution"
When call validate_input_python "language-version-detect" "default-version" "\$(whoami)"
The status should be failure
End
It "rejects alphabetic version"
When call validate_input_python "language-version-detect" "default-version" "latest"
The status should be failure
End
End
Context "when validating token input"
It "accepts valid GitHub token (classic)"
When call validate_input_python "language-version-detect" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts valid GitHub fine-grained token"
When call validate_input_python "language-version-detect" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a"
The status should be success
End
It "accepts empty token (optional)"
When call validate_input_python "language-version-detect" "token" ""
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "language-version-detect" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "language-version-detect" "token" "ghp_123456789012345678901234567890123456; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Language Version Detect"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "language"
The output should include "default-version"
The output should include "token"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "detected-version"
The output should include "package-manager"
End
End
Context "when testing input requirements"
It "requires language input"
When call is_input_required "$ACTION_FILE" "language"
The status should be success
End
It "has default-version as optional input"
When call is_input_required "$ACTION_FILE" "default-version"
The status should be failure
End
It "has token as optional input"
When call is_input_required "$ACTION_FILE" "token"
The status should be failure
End
End
Context "when testing security validations"
It "validates against path traversal in language"
When call validate_input_python "language-version-detect" "language" "../../../etc"
The status should be failure
End
It "validates against shell metacharacters in language"
When call validate_input_python "language-version-detect" "language" "php&whoami"
The status should be failure
End
It "validates against command substitution in language"
When call validate_input_python "language-version-detect" "language" "php\`whoami\`"
The status should be failure
End
It "validates against path traversal in default-version"
When call validate_input_python "language-version-detect" "default-version" "../../../etc"
The status should be failure
End
It "validates against shell metacharacters in default-version"
When call validate_input_python "language-version-detect" "default-version" "3.12&echo"
The status should be failure
End
It "validates against command substitution in default-version"
When call validate_input_python "language-version-detect" "default-version" "3.12\$(whoami)"
The status should be failure
End
It "validates against path traversal in token"
When call validate_input_python "language-version-detect" "token" "../../../etc/passwd"
The status should be failure
End
End
End

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env shellspec
# Unit tests for php-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "php-version-detect action"
ACTION_DIR="php-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating default-version input"
It "accepts valid PHP version"
When call validate_input_python "php-version-detect" "default-version" "8.2"
The status should be success
End
It "accepts PHP version with patch"
When call validate_input_python "php-version-detect" "default-version" "8.3.1"
The status should be success
End
It "accepts PHP 7.4"
When call validate_input_python "php-version-detect" "default-version" "7.4"
The status should be success
End
It "accepts PHP 8.0"
When call validate_input_python "php-version-detect" "default-version" "8.0"
The status should be success
End
It "accepts PHP 8.1"
When call validate_input_python "php-version-detect" "default-version" "8.1"
The status should be success
End
It "accepts PHP 8.4"
When call validate_input_python "php-version-detect" "default-version" "8.4"
The status should be success
End
It "rejects PHP version too old"
When call validate_input_python "php-version-detect" "default-version" "5.6"
The status should be failure
End
It "rejects PHP version too new"
When call validate_input_python "php-version-detect" "default-version" "10.0"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "php-version-detect" "default-version" "php8.2"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "php-version-detect" "default-version" "8.2; rm -rf /"
The status should be failure
End
It "rejects version without minor"
When call validate_input_python "php-version-detect" "default-version" "8"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "php-version-detect" "default-version" ""
The status should be failure
End
It "rejects version with v prefix"
When call validate_input_python "php-version-detect" "default-version" "v8.2"
The status should be failure
End
It "accepts PHP 8.5 for future compatibility"
When call validate_input_python "php-version-detect" "default-version" "8.5"
The status should be success
End
It "rejects unreasonably high minor version"
When call validate_input_python "php-version-detect" "default-version" "8.100"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "PHP Version Detect"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "php-version"
End
End
Context "when testing input requirements"
It "has default-version as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional"
The output should equal "optional"
End
It "has correct default version"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "default"
The output should equal "8.2"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "php-version-detect" "default-version" "../8.2"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "php-version-detect" "default-version" "8.2|echo"
The status should be failure
End
It "validates against backtick injection"
When call validate_input_python "php-version-detect" "default-version" "8.2\`whoami\`"
The status should be failure
End
It "validates against variable expansion"
When call validate_input_python "php-version-detect" "default-version" "8.2\${HOME}"
The status should be failure
End
End
Context "when testing PHP version range validation"
It "validates PHP 7 minor version boundaries"
When call validate_input_python "php-version-detect" "default-version" "7.0"
The status should be success
End
It "validates PHP 7.4 specifically"
When call validate_input_python "php-version-detect" "default-version" "7.4"
The status should be success
End
It "validates PHP 8 minor version boundaries"
When call validate_input_python "php-version-detect" "default-version" "8.0"
The status should be success
End
It "validates PHP 8.4 boundary"
When call validate_input_python "php-version-detect" "default-version" "8.4"
The status should be success
End
It "validates PHP 9 future version"
When call validate_input_python "php-version-detect" "default-version" "9.0"
The status should be success
End
End
End

View File

@@ -0,0 +1,332 @@
#!/usr/bin/env shellspec
# Unit tests for prettier-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "prettier-check action"
ACTION_DIR="prettier-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating working-directory input"
It "accepts current directory"
When call validate_input_python "prettier-check" "working-directory" "."
The status should be success
End
It "accepts relative directory"
When call validate_input_python "prettier-check" "working-directory" "src"
The status should be success
End
It "accepts nested directory"
When call validate_input_python "prettier-check" "working-directory" "src/components"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "prettier-check" "working-directory" "../malicious"
The status should be failure
End
It "rejects absolute paths"
When call validate_input_python "prettier-check" "working-directory" "/etc/passwd"
The status should be failure
End
It "rejects directory with command injection"
When call validate_input_python "prettier-check" "working-directory" "src; rm -rf /"
The status should be failure
End
End
Context "when validating prettier-version input"
It "accepts latest version"
When call validate_input_python "prettier-check" "prettier-version" "latest"
The status should be success
End
It "accepts semantic version"
When call validate_input_python "prettier-check" "prettier-version" "3.0.0"
The status should be success
End
It "accepts prerelease version"
When call validate_input_python "prettier-check" "prettier-version" "3.0.0-alpha"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "prettier-check" "prettier-version" "v3.0.0"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "prettier-check" "prettier-version" "3.0.0; rm -rf /"
The status should be failure
End
End
Context "when validating config-file input"
It "accepts valid config file"
When call validate_input_python "prettier-check" "config-file" ".prettierrc"
The status should be success
End
It "accepts config file with extension"
When call validate_input_python "prettier-check" "config-file" ".prettierrc.json"
The status should be success
End
It "accepts config file in subdirectory"
When call validate_input_python "prettier-check" "config-file" "config/.prettierrc"
The status should be success
End
It "rejects path traversal in config file"
When call validate_input_python "prettier-check" "config-file" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute path in config file"
When call validate_input_python "prettier-check" "config-file" "/etc/passwd"
The status should be failure
End
End
Context "when validating ignore-file input"
It "accepts valid ignore file"
When call validate_input_python "prettier-check" "ignore-file" ".prettierignore"
The status should be success
End
It "accepts ignore file in subdirectory"
When call validate_input_python "prettier-check" "ignore-file" "config/.prettierignore"
The status should be success
End
It "rejects path traversal in ignore file"
When call validate_input_python "prettier-check" "ignore-file" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute path in ignore file"
When call validate_input_python "prettier-check" "ignore-file" "/etc/passwd"
The status should be failure
End
End
Context "when validating file-pattern input"
It "accepts valid glob pattern"
When call validate_input_python "prettier-check" "file-pattern" "**/*.{js,ts}"
The status should be success
End
It "accepts simple file pattern"
When call validate_input_python "prettier-check" "file-pattern" "*.js"
The status should be success
End
It "accepts multiple extensions"
When call validate_input_python "prettier-check" "file-pattern" "**/*.{js,jsx,ts,tsx,css}"
The status should be success
End
It "rejects pattern with path traversal"
When call validate_input_python "prettier-check" "file-pattern" "../**/*.js"
The status should be failure
End
It "rejects pattern with absolute path"
When call validate_input_python "prettier-check" "file-pattern" "/etc/**/*.conf"
The status should be failure
End
End
Context "when validating boolean inputs"
It "accepts true for cache"
When call validate_input_python "prettier-check" "cache" "true"
The status should be success
End
It "accepts false for cache"
When call validate_input_python "prettier-check" "cache" "false"
The status should be success
End
It "rejects invalid cache value"
When call validate_input_python "prettier-check" "cache" "yes"
The status should be failure
End
It "accepts true for fail-on-error"
When call validate_input_python "prettier-check" "fail-on-error" "true"
The status should be success
End
It "accepts false for fail-on-error"
When call validate_input_python "prettier-check" "fail-on-error" "false"
The status should be success
End
It "accepts true for check-only"
When call validate_input_python "prettier-check" "check-only" "true"
The status should be success
End
It "accepts false for check-only"
When call validate_input_python "prettier-check" "check-only" "false"
The status should be success
End
End
Context "when validating report-format input"
It "accepts json format"
When call validate_input_python "prettier-check" "report-format" "json"
The status should be success
End
It "accepts sarif format"
When call validate_input_python "prettier-check" "report-format" "sarif"
The status should be success
End
It "rejects invalid format"
When call validate_input_python "prettier-check" "report-format" "xml"
The status should be failure
End
It "rejects empty format"
When call validate_input_python "prettier-check" "report-format" ""
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "prettier-check" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "prettier-check" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "prettier-check" "max-retries" "10"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "prettier-check" "max-retries" "0"
The status should be failure
End
It "rejects too many retries"
When call validate_input_python "prettier-check" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "prettier-check" "max-retries" "many"
The status should be failure
End
End
Context "when validating plugins input"
It "accepts empty plugins"
When call validate_input_python "prettier-check" "plugins" ""
The status should be success
End
It "accepts valid plugin name"
When call validate_input_python "prettier-check" "plugins" "prettier-plugin-java"
The status should be success
End
It "accepts scoped plugin"
When call validate_input_python "prettier-check" "plugins" "@prettier/plugin-xml"
The status should be success
End
It "accepts multiple plugins"
When call validate_input_python "prettier-check" "plugins" "plugin1,@scope/plugin2"
The status should be success
End
It "rejects plugins with command injection"
When call validate_input_python "prettier-check" "plugins" "plugin1; rm -rf /"
The status should be failure
End
It "rejects plugins with shell operators"
When call validate_input_python "prettier-check" "plugins" "plugin1 && malicious"
The status should be failure
End
It "rejects plugins with pipe"
When call validate_input_python "prettier-check" "plugins" "plugin1 | cat /etc/passwd"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Prettier Check"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "working-directory"
The output should include "prettier-version"
The output should include "config-file"
The output should include "ignore-file"
The output should include "file-pattern"
The output should include "cache"
The output should include "fail-on-error"
The output should include "report-format"
The output should include "max-retries"
The output should include "plugins"
The output should include "check-only"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "files-checked"
The output should include "unformatted-files"
The output should include "sarif-file"
The output should include "cache-hit"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "any" "all_optional"
The output should equal "none"
End
End
Context "when testing security validations"
It "validates against path traversal in multiple inputs"
When call validate_input_python "prettier-check" "working-directory" "../../malicious"
The status should be failure
End
It "validates against command injection in plugins"
When call validate_input_python "prettier-check" "plugins" "plugin\`whoami\`"
The status should be failure
End
It "validates against shell expansion in file patterns"
When call validate_input_python "prettier-check" "file-pattern" "**/*.js\${HOME}"
The status should be failure
End
End
End

View File

@@ -0,0 +1,285 @@
#!/usr/bin/env shellspec
# Unit tests for prettier-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "prettier-fix action"
ACTION_DIR="prettier-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts GitHub token expression"
When call validate_input_python "prettier-fix" "token" "\${{ github.token }}"
The status should be success
End
It "accepts GitHub fine-grained token"
When call validate_input_python "prettier-fix" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub app token"
When call validate_input_python "prettier-fix" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub enterprise token"
When call validate_input_python "prettier-fix" "token" "ghe_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "prettier-fix" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "prettier-fix" "token" "ghp_token; rm -rf /"
The status should be failure
End
It "accepts empty token (uses default)"
When call validate_input_python "prettier-fix" "token" ""
The status should be success
End
End
Context "when validating username input"
It "accepts valid GitHub username"
When call validate_input_python "prettier-fix" "username" "github-actions"
The status should be success
End
It "accepts username with hyphens"
When call validate_input_python "prettier-fix" "username" "user-name"
The status should be success
End
It "accepts username with numbers"
When call validate_input_python "prettier-fix" "username" "user123"
The status should be success
End
It "accepts single character username"
When call validate_input_python "prettier-fix" "username" "a"
The status should be success
End
It "accepts maximum length username"
When call validate_input_python "prettier-fix" "username" "abcdefghijklmnopqrstuvwxyz0123456789abc"
The status should be success
End
It "rejects username too long"
When call validate_input_python "prettier-fix" "username" "abcdefghijklmnopqrstuvwxyz0123456789abcd"
The status should be failure
End
It "rejects username with command injection"
When call validate_input_python "prettier-fix" "username" "user; rm -rf /"
The status should be failure
End
It "rejects username with shell operators"
When call validate_input_python "prettier-fix" "username" "user && rm -rf /"
The status should be failure
End
It "rejects username with pipe"
When call validate_input_python "prettier-fix" "username" "user | rm -rf /"
The status should be failure
End
It "accepts empty username (uses default)"
When call validate_input_python "prettier-fix" "username" ""
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "prettier-fix" "email" "user@example.com"
The status should be success
End
It "accepts email with subdomain"
When call validate_input_python "prettier-fix" "email" "user@mail.example.com"
The status should be success
End
It "accepts email with plus sign"
When call validate_input_python "prettier-fix" "email" "user+tag@example.com"
The status should be success
End
It "accepts email with numbers"
When call validate_input_python "prettier-fix" "email" "user123@example123.com"
The status should be success
End
It "accepts email with hyphens"
When call validate_input_python "prettier-fix" "email" "user-name@example-domain.com"
The status should be success
End
It "rejects email without at symbol"
When call validate_input_python "prettier-fix" "email" "userexample.com"
The status should be failure
End
It "rejects email without domain"
When call validate_input_python "prettier-fix" "email" "user@"
The status should be failure
End
It "rejects email without username"
When call validate_input_python "prettier-fix" "email" "@example.com"
The status should be failure
End
It "rejects email without dot in domain"
When call validate_input_python "prettier-fix" "email" "user@example"
The status should be failure
End
It "rejects email with spaces"
When call validate_input_python "prettier-fix" "email" "user @example.com"
The status should be failure
End
It "rejects empty email"
When call validate_input_python "prettier-fix" "email" ""
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "prettier-fix" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "prettier-fix" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "prettier-fix" "max-retries" "10"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "prettier-fix" "max-retries" "0"
The status should be failure
End
It "rejects too many retries"
When call validate_input_python "prettier-fix" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "prettier-fix" "max-retries" "many"
The status should be failure
End
It "rejects negative retries"
When call validate_input_python "prettier-fix" "max-retries" "-1"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Prettier Fix"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "files_changed"
The output should include "format_status"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
It "has correct default token"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "token" "default"
The output should equal "\${{ github.token }}"
End
It "has correct default username"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "username" "default"
The output should equal "github-actions"
End
It "has correct default email"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "email" "default"
The output should equal "github-actions@github.com"
End
End
Context "when testing security validations"
It "validates against command injection in username"
When call validate_input_python "prettier-fix" "username" "user\`whoami\`"
The status should be failure
End
It "validates against shell metacharacters in email"
When call validate_input_python "prettier-fix" "email" "user@example.com; rm -rf /"
The status should be failure
End
It "validates against variable expansion in token"
When call validate_input_python "prettier-fix" "token" "\${MALICIOUS_VAR}"
The status should be failure
End
It "validates against backtick injection in email"
When call validate_input_python "prettier-fix" "email" "user@example.com\`echo test\`"
The status should be failure
End
End
Context "when testing Prettier-specific validations"
It "validates username length boundaries for Git"
When call validate_input_python "prettier-fix" "username" "$(awk 'BEGIN{for(i=1;i<=40;i++)printf "a"}')"
The status should be failure
End
It "validates email format for Git commits"
When call validate_input_python "prettier-fix" "email" "noreply@github.com"
The status should be success
End
It "validates retry count boundaries"
When call validate_input_python "prettier-fix" "max-retries" "0"
The status should be failure
End
It "validates default values are secure"
When call validate_input_python "prettier-fix" "username" "github-actions"
The status should be success
End
End
End

View File

@@ -1,515 +0,0 @@
#!/usr/bin/env shellspec
# Unit tests for prettier-lint action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "prettier-lint action"
ACTION_DIR="prettier-lint"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating mode input"
It "accepts check mode"
When call validate_input_python "prettier-lint" "mode" "check"
The status should be success
End
It "accepts fix mode"
When call validate_input_python "prettier-lint" "mode" "fix"
The status should be success
End
It "accepts empty mode (uses default)"
When call validate_input_python "prettier-lint" "mode" ""
The status should be success
End
It "rejects invalid mode"
When call validate_input_python "prettier-lint" "mode" "invalid"
The status should be failure
End
It "rejects mode with command injection"
When call validate_input_python "prettier-lint" "mode" "check; rm -rf /"
The status should be failure
End
End
Context "when validating working-directory input"
It "accepts default directory"
When call validate_input_python "prettier-lint" "working-directory" "."
The status should be success
End
It "accepts valid subdirectory"
When call validate_input_python "prettier-lint" "working-directory" "src"
The status should be success
End
It "accepts empty working-directory (uses default)"
When call validate_input_python "prettier-lint" "working-directory" ""
The status should be success
End
It "rejects path traversal"
When call validate_input_python "prettier-lint" "working-directory" "../../../etc"
The status should be failure
End
It "rejects directory with command injection"
When call validate_input_python "prettier-lint" "working-directory" "src; rm -rf /"
The status should be failure
End
End
Context "when validating prettier-version input"
It "accepts latest version"
When call validate_input_python "prettier-lint" "prettier-version" "latest"
The status should be success
End
It "accepts semantic version"
When call validate_input_python "prettier-lint" "prettier-version" "3.2.5"
The status should be success
End
It "accepts major.minor version"
When call validate_input_python "prettier-lint" "prettier-version" "3.2"
The status should be success
End
It "accepts major version"
When call validate_input_python "prettier-lint" "prettier-version" "3"
The status should be success
End
It "accepts version with pre-release"
When call validate_input_python "prettier-lint" "prettier-version" "3.0.0-alpha.1"
The status should be success
End
It "accepts empty version (uses default)"
When call validate_input_python "prettier-lint" "prettier-version" ""
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "prettier-lint" "prettier-version" "invalid"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "prettier-lint" "prettier-version" "3.2.5; echo"
The status should be failure
End
End
Context "when validating config-file input"
It "accepts default config file"
When call validate_input_python "prettier-lint" "config-file" ".prettierrc"
The status should be success
End
It "accepts custom config file"
When call validate_input_python "prettier-lint" "config-file" ".prettierrc.js"
The status should be success
End
It "accepts config file in subdirectory"
When call validate_input_python "prettier-lint" "config-file" "config/prettier.config.js"
The status should be success
End
It "accepts empty config-file (uses default)"
When call validate_input_python "prettier-lint" "config-file" ""
The status should be success
End
It "rejects config file with path traversal"
When call validate_input_python "prettier-lint" "config-file" "../../../.prettierrc"
The status should be failure
End
It "rejects config file with command injection"
When call validate_input_python "prettier-lint" "config-file" ".prettierrc; rm -rf /"
The status should be failure
End
End
Context "when validating ignore-file input"
It "accepts default ignore file"
When call validate_input_python "prettier-lint" "ignore-file" ".prettierignore"
The status should be success
End
It "accepts custom ignore file"
When call validate_input_python "prettier-lint" "ignore-file" ".prettierignore.custom"
The status should be success
End
It "accepts empty ignore-file (uses default)"
When call validate_input_python "prettier-lint" "ignore-file" ""
The status should be success
End
It "rejects ignore file with path traversal"
When call validate_input_python "prettier-lint" "ignore-file" "../../../.prettierignore"
The status should be failure
End
It "rejects ignore file with command injection"
When call validate_input_python "prettier-lint" "ignore-file" ".prettierignore; echo"
The status should be failure
End
End
Context "when validating file-pattern input"
It "accepts default pattern"
When call validate_input_python "prettier-lint" "file-pattern" "**/*.{js,jsx,ts,tsx,css,scss,json,md,yaml,yml}"
The status should be success
End
It "accepts simple pattern"
When call validate_input_python "prettier-lint" "file-pattern" "**/*.js"
The status should be success
End
It "accepts multiple patterns"
When call validate_input_python "prettier-lint" "file-pattern" "**/*.{js,ts}"
The status should be success
End
It "accepts specific directory pattern"
When call validate_input_python "prettier-lint" "file-pattern" "src/**/*.js"
The status should be success
End
It "accepts empty file-pattern (uses default)"
When call validate_input_python "prettier-lint" "file-pattern" ""
The status should be success
End
It "rejects pattern with command injection"
When call validate_input_python "prettier-lint" "file-pattern" "**/*.js; rm -rf /"
The status should be failure
End
End
Context "when validating cache input"
It "accepts true"
When call validate_input_python "prettier-lint" "cache" "true"
The status should be success
End
It "accepts false"
When call validate_input_python "prettier-lint" "cache" "false"
The status should be success
End
It "accepts empty cache (uses default)"
When call validate_input_python "prettier-lint" "cache" ""
The status should be success
End
It "rejects invalid boolean value"
When call validate_input_python "prettier-lint" "cache" "maybe"
The status should be failure
End
End
Context "when validating fail-on-error input"
It "accepts true"
When call validate_input_python "prettier-lint" "fail-on-error" "true"
The status should be success
End
It "accepts false"
When call validate_input_python "prettier-lint" "fail-on-error" "false"
The status should be success
End
It "accepts empty fail-on-error (uses default)"
When call validate_input_python "prettier-lint" "fail-on-error" ""
The status should be success
End
It "rejects invalid boolean value"
When call validate_input_python "prettier-lint" "fail-on-error" "yes"
The status should be failure
End
End
Context "when validating report-format input"
It "accepts json format"
When call validate_input_python "prettier-lint" "report-format" "json"
The status should be success
End
It "accepts sarif format"
When call validate_input_python "prettier-lint" "report-format" "sarif"
The status should be success
End
It "accepts empty report-format (uses default)"
When call validate_input_python "prettier-lint" "report-format" ""
The status should be success
End
It "rejects invalid format"
When call validate_input_python "prettier-lint" "report-format" "invalid"
The status should be failure
End
It "rejects format with command injection"
When call validate_input_python "prettier-lint" "report-format" "json; rm -rf /"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts default value 3"
When call validate_input_python "prettier-lint" "max-retries" "3"
The status should be success
End
It "accepts retry count of 1"
When call validate_input_python "prettier-lint" "max-retries" "1"
The status should be success
End
It "accepts retry count of 10"
When call validate_input_python "prettier-lint" "max-retries" "10"
The status should be success
End
It "accepts empty max-retries (uses default)"
When call validate_input_python "prettier-lint" "max-retries" ""
The status should be success
End
It "rejects zero retries"
When call validate_input_python "prettier-lint" "max-retries" "0"
The status should be failure
End
It "rejects negative retry count"
When call validate_input_python "prettier-lint" "max-retries" "-1"
The status should be failure
End
It "rejects retry count above 10"
When call validate_input_python "prettier-lint" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric retry count"
When call validate_input_python "prettier-lint" "max-retries" "abc"
The status should be failure
End
It "rejects retry count with command injection"
When call validate_input_python "prettier-lint" "max-retries" "3; echo"
The status should be failure
End
End
Context "when validating plugins input"
It "accepts empty plugins (optional)"
When call validate_input_python "prettier-lint" "plugins" ""
The status should be success
End
It "accepts single plugin"
When call validate_input_python "prettier-lint" "plugins" "prettier-plugin-tailwindcss"
The status should be success
End
It "accepts multiple plugins"
When call validate_input_python "prettier-lint" "plugins" "prettier-plugin-tailwindcss,prettier-plugin-organize-imports"
The status should be success
End
It "accepts scoped plugin"
When call validate_input_python "prettier-lint" "plugins" "@trivago/prettier-plugin-sort-imports"
The status should be success
End
It "rejects plugins with command injection"
When call validate_input_python "prettier-lint" "plugins" "prettier-plugin-tailwindcss; rm -rf /"
The status should be failure
End
End
Context "when validating token input"
It "accepts valid GitHub token (classic)"
When call validate_input_python "prettier-lint" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts valid GitHub fine-grained token"
When call validate_input_python "prettier-lint" "token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a"
The status should be success
End
It "accepts empty token (optional)"
When call validate_input_python "prettier-lint" "token" ""
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "prettier-lint" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "prettier-lint" "token" "ghp_123456789012345678901234567890123456; rm -rf /"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "prettier-lint" "username" "github-actions"
The status should be success
End
It "accepts username with hyphens"
When call validate_input_python "prettier-lint" "username" "my-bot-user"
The status should be success
End
It "accepts alphanumeric username"
When call validate_input_python "prettier-lint" "username" "user123"
The status should be success
End
It "accepts empty username (uses default)"
When call validate_input_python "prettier-lint" "username" ""
The status should be success
End
It "rejects username with command injection"
When call validate_input_python "prettier-lint" "username" "user; rm -rf /"
The status should be failure
End
It "rejects username with special characters"
When call validate_input_python "prettier-lint" "username" "user@bot"
The status should be failure
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "prettier-lint" "email" "github-actions@github.com"
The status should be success
End
It "accepts email with plus sign"
When call validate_input_python "prettier-lint" "email" "user+bot@example.com"
The status should be success
End
It "accepts email with subdomain"
When call validate_input_python "prettier-lint" "email" "bot@ci.example.com"
The status should be success
End
It "accepts empty email (uses default)"
When call validate_input_python "prettier-lint" "email" ""
The status should be success
End
It "rejects invalid email format"
When call validate_input_python "prettier-lint" "email" "not-an-email"
The status should be failure
End
It "rejects email with command injection"
When call validate_input_python "prettier-lint" "email" "user@example.com; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Prettier Lint"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "mode"
The output should include "working-directory"
The output should include "prettier-version"
The output should include "config-file"
The output should include "ignore-file"
The output should include "file-pattern"
The output should include "cache"
The output should include "fail-on-error"
The output should include "report-format"
The output should include "max-retries"
The output should include "plugins"
The output should include "token"
The output should include "username"
The output should include "email"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "status"
The output should include "files-checked"
The output should include "unformatted-files"
The output should include "sarif-file"
The output should include "files-changed"
End
End
Context "when testing input requirements"
It "has all inputs as optional (with defaults)"
When call is_input_required "$ACTION_FILE" "mode"
The status should be failure
End
End
Context "when testing security validations"
It "validates against path traversal in working-directory"
When call validate_input_python "prettier-lint" "working-directory" "../../../etc"
The status should be failure
End
It "validates against shell metacharacters in mode"
When call validate_input_python "prettier-lint" "mode" "check|echo"
The status should be failure
End
It "validates against command substitution in config-file"
When call validate_input_python "prettier-lint" "config-file" "\$(whoami)"
The status should be failure
End
It "validates against path traversal in token"
When call validate_input_python "prettier-lint" "token" "../../../etc/passwd"
The status should be failure
End
It "validates against shell metacharacters in username"
When call validate_input_python "prettier-lint" "username" "user&whoami"
The status should be failure
End
It "validates against command injection in email"
When call validate_input_python "prettier-lint" "email" "user@example.com\`whoami\`"
The status should be failure
End
It "validates against command injection in plugins"
When call validate_input_python "prettier-lint" "plugins" "plugin1,plugin2; rm -rf /"
The status should be failure
End
End
End

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env shellspec
# Unit tests for python-version-detect-v2 action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "python-version-detect-v2 action"
ACTION_DIR="python-version-detect-v2"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating default-version input"
It "accepts valid Python version"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11"
The status should be success
End
It "accepts Python version with patch"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11.5"
The status should be success
End
It "accepts Python 3.8"
When call validate_input_python "python-version-detect-v2" "default-version" "3.8"
The status should be success
End
It "accepts Python 3.12"
When call validate_input_python "python-version-detect-v2" "default-version" "3.12"
The status should be success
End
It "rejects Python version too old"
When call validate_input_python "python-version-detect-v2" "default-version" "2.7"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "python-version-detect-v2" "default-version" "python3.11"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11; rm -rf /"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "python-version-detect-v2" "default-version" ""
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Python Version Detect v2"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "python-version"
End
End
Context "when testing input requirements"
It "has default-version as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional"
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "python-version-detect-v2" "default-version" "../3.11"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11|echo"
The status should be failure
End
It "validates against backtick injection"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11\`whoami\`"
The status should be failure
End
End
End

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env shellspec
# Unit tests for python-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "python-version-detect action"
ACTION_DIR="python-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating default-version input"
It "accepts valid Python version"
When call validate_input_python "python-version-detect" "default-version" "3.11"
The status should be success
End
It "accepts Python version with patch"
When call validate_input_python "python-version-detect" "default-version" "3.11.5"
The status should be success
End
It "accepts Python 3.8"
When call validate_input_python "python-version-detect" "default-version" "3.8"
The status should be success
End
It "accepts Python 3.12"
When call validate_input_python "python-version-detect" "default-version" "3.12"
The status should be success
End
It "rejects Python version too old"
When call validate_input_python "python-version-detect" "default-version" "2.7"
The status should be failure
End
It "rejects Python version too new"
When call validate_input_python "python-version-detect" "default-version" "4.0"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "python-version-detect" "default-version" "python3.11"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "python-version-detect" "default-version" "3.11; rm -rf /"
The status should be failure
End
It "rejects version without minor"
When call validate_input_python "python-version-detect" "default-version" "3"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "python-version-detect" "default-version" ""
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Python Version Detect"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "python-version"
End
End
Context "when testing input requirements"
It "has default-version as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional"
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "python-version-detect" "default-version" "../3.11"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "python-version-detect" "default-version" "3.11|echo"
The status should be failure
End
It "validates against backtick injection"
When call validate_input_python "python-version-detect" "default-version" "3.11\`whoami\`"
The status should be failure
End
End
End

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env shellspec
# Unit tests for set-git-config action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "set-git-config action"
ACTION_DIR="set-git-config"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs (no validation logic in action)"
# NOTE: This action has no validation logic - all inputs are accepted
# The action simply passes through values and conditionally sets outputs
It "accepts valid token value"
When call validate_input_python "set-git-config" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts any username value"
When call validate_input_python "set-git-config" "username" "any-username"
The status should be success
End
It "accepts valid email value"
When call validate_input_python "set-git-config" "email" "test@example.com"
The status should be success
End
It "accepts any is_fiximus value"
When call validate_input_python "set-git-config" "is_fiximus" "any-value"
The status should be success
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Set Git Config"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "is_fiximus"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "is_fiximus"
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_test" "username" "test" "email" "test@example.com" "is_fiximus" "false"
The status should be success
The stderr should include "Testing action outputs for: set-git-config"
The stderr should include "Output test passed for: set-git-config"
End
End
End

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env shellspec
# Unit tests for version-validator action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "version-validator action"
ACTION_DIR="version-validator"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating version input"
It "accepts valid semantic version"
When call validate_input_python "version-validator" "version" "1.2.3"
The status should be success
End
It "accepts semantic version with v prefix"
When call validate_input_python "version-validator" "version" "v1.2.3"
The status should be success
End
It "accepts prerelease version"
When call validate_input_python "version-validator" "version" "1.2.3-alpha"
The status should be success
End
It "accepts prerelease with number"
When call validate_input_python "version-validator" "version" "1.2.3-alpha.1"
The status should be success
End
It "accepts build metadata"
When call validate_input_python "version-validator" "version" "1.2.3+build.1"
The status should be success
End
It "accepts prerelease with build metadata"
When call validate_input_python "version-validator" "version" "1.2.3-alpha.1+build.1"
The status should be success
End
It "accepts CalVer format"
When call validate_input_python "version-validator" "version" "2024.3.1"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "version-validator" "version" "invalid.version"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "version-validator" "version" "1.2.3; rm -rf /"
The status should be failure
End
It "rejects version with shell expansion"
When call validate_input_python "version-validator" "version" "1.2.3\$(whoami)"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "version-validator" "version" ""
The status should be failure
End
End
Context "when validating validation-regex input"
It "accepts valid regex pattern"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\.[0-9]+\.[0-9]+$"
The status should be success
End
It "accepts semantic version regex"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$"
The status should be success
End
It "accepts empty validation-regex (uses default)"
When call validate_input_python "version-validator" "validation-regex" ""
The status should be success
End
It "accepts valid regex patterns with quantifiers"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\\.[0-9]+$"
The status should be success
End
It "rejects regex with command injection"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$; rm -rf /"
The status should be failure
End
End
Context "when validating ReDoS patterns"
It "rejects nested quantifiers (a+)+"
When call validate_input_python "version-validator" "validation-regex" "(a+)+"
The status should be failure
End
It "rejects nested quantifiers (a*)+"
When call validate_input_python "version-validator" "validation-regex" "(a*)+"
The status should be failure
End
It "rejects nested quantifiers (a+)*"
When call validate_input_python "version-validator" "validation-regex" "(a+)*"
The status should be failure
End
It "rejects nested quantifiers (a*)*"
When call validate_input_python "version-validator" "validation-regex" "(a*)*"
The status should be failure
End
It "rejects quantified groups (a+){2,5}"
When call validate_input_python "version-validator" "validation-regex" "(a+){2,5}"
The status should be failure
End
It "rejects consecutive quantifiers .*.* (ReDoS)"
When call validate_input_python "version-validator" "validation-regex" ".*.*"
The status should be failure
End
It "rejects consecutive quantifiers .*+ (ReDoS)"
When call validate_input_python "version-validator" "validation-regex" ".*+"
The status should be failure
End
It "rejects duplicate alternatives (a|a)+"
When call validate_input_python "version-validator" "validation-regex" "(a|a)+"
The status should be failure
End
It "rejects overlapping alternatives (a|ab)+"
When call validate_input_python "version-validator" "validation-regex" "(a|ab)+"
The status should be failure
End
It "accepts safe pattern with single quantifier"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$"
The status should be success
End
It "accepts safe pattern with character class"
When call validate_input_python "version-validator" "validation-regex" "^[a-zA-Z0-9]+$"
The status should be success
End
It "accepts safe pattern with optional group"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+(\\.[0-9]+)?$"
The status should be success
End
It "accepts safe alternation without repetition"
When call validate_input_python "version-validator" "validation-regex" "^(alpha|beta|gamma)$"
The status should be success
End
End
Context "when validating language input"
It "accepts valid language name"
When call validate_input_python "version-validator" "language" "nodejs"
The status should be success
End
It "accepts version as language"
When call validate_input_python "version-validator" "language" "version"
The status should be success
End
It "accepts empty language (uses default)"
When call validate_input_python "version-validator" "language" ""
The status should be success
End
It "rejects language with command injection"
When call validate_input_python "version-validator" "language" "version; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*Version*"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "version"
The output should include "validation-regex"
The output should include "language"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "is-valid"
The output should include "validated-version"
The output should include "error-message"
End
End
Context "when testing input requirements"
It "requires version input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "version" "required"
The status should be success
The output should equal "required"
End
It "has validation-regex as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "validation-regex" "optional"
The status should be success
The output should equal "optional"
End
It "has language as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "language" "optional"
The status should be success
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "version-validator" "version" "../1.2.3"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "version-validator" "version" "1.2.3|echo"
The status should be failure
End
It "validates against backtick injection in language"
When call validate_input_python "version-validator" "language" "version\`whoami\`"
The status should be failure
End
It "validates against variable expansion in version"
When call validate_input_python "version-validator" "version" "1.2.3\${HOME}"
The status should be failure
End
End
Context "when testing version validation functionality"
It "validates semantic version format restrictions"
When call validate_input_python "version-validator" "version" "1.2"
The status should be success
End
It "validates regex pattern safety"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$"
The status should be success
End
It "validates language parameter format"
When call validate_input_python "version-validator" "language" "NODEJS"
The status should be success
End
It "validates complex version formats"
When call validate_input_python "version-validator" "version" "1.0.0-beta.1+exp.sha.5114f85"
The status should be success
End
End
End

View File

@@ -1,152 +0,0 @@
#!/bin/sh
# Undo the most recent release by deleting tags and optionally resetting HEAD
set -eu
# Source shared utilities
# shellcheck source=_tools/shared.sh
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# shellcheck disable=SC1091
. "$SCRIPT_DIR/shared.sh"
# Check git availability
require_git
msg_info "Finding most recent release tags..."
# Portable version sort function
# Sorts CalVer tags vYYYY.MM.DD numerically
version_sort_tags() {
# Try GNU sort first (Linux and some macOS with GNU coreutils)
if sort --version 2>/dev/null | grep -q GNU; then
sort -V
return
fi
# Try gsort (macOS with GNU coreutils via Homebrew)
if command -v gsort >/dev/null 2>&1; then
gsort -V
return
fi
# Fallback: awk-based numeric version sort with validation
awk -F. '{
# Validate CalVer format: vYYYY.MM.DD or YYYY.MM.DD
if ($0 !~ /^v?[0-9]+\.[0-9]+\.[0-9]+$/) {
printf "Warning: Skipping malformed tag: %s\n", $0 > "/dev/stderr"
next
}
# Check we have exactly 3 fields after splitting on dots
if (NF != 3) {
printf "Warning: Skipping invalid tag (wrong field count): %s\n", $0 > "/dev/stderr"
next
}
# Save original input before modification
original = $0
# Remove leading v and split into year, month, day
gsub(/^v/, "", $0)
# Verify each field is numeric after field recalculation
if ($1 !~ /^[0-9]+$/ || $2 !~ /^[0-9]+$/ || $3 !~ /^[0-9]+$/) {
printf "Warning: Skipping tag with non-numeric components: %s\n", original > "/dev/stderr"
next
}
printf "%04d.%02d.%02d %s\n", $1, $2, $3, original
}' | sort -n | cut -d' ' -f2
}
# Find all release tags matching vYYYY.MM.DD pattern
all_tags=$(git tag -l 'v[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]' | version_sort_tags)
if [ -z "$all_tags" ]; then
msg_warn "No release tags found"
exit 0
fi
# Get most recent tag
latest_tag=$(echo "$all_tags" | tail -n 1)
# Extract version components
version_no_v="${latest_tag#v}"
year=$(echo "$version_no_v" | cut -d'.' -f1)
month=$(echo "$version_no_v" | cut -d'.' -f2)
day=$(echo "$version_no_v" | cut -d'.' -f3)
major="v$year"
minor="v$year.$month"
patch="v$year.$month.$day"
printf '\n'
msg_info "Most recent release:"
printf ' Patch: %s\n' "$patch"
printf ' Minor: %s\n' "$minor"
printf ' Major: %s\n' "$major"
printf '\n'
# Show which tags exist
msg_info "Tags that will be deleted:"
for tag in "$patch" "$minor" "$major"; do
if check_tag_exists "$tag"; then
tag_sha=$(git rev-list -n 1 "$tag")
tag_sha_short=$(echo "$tag_sha" | cut -c1-7)
printf ' %s (points to %s)\n' "$tag" "$tag_sha_short"
fi
done
printf '\n'
# Check if HEAD commit is a release commit
head_message=$(git log -1 --pretty=%s)
if echo "$head_message" | grep -q "^chore: update action references for release"; then
msg_warn "Last commit appears to be a release preparation commit:"
printf ' %s\n' "$head_message"
printf '\n'
reset_head=true
else
reset_head=false
fi
# Confirm deletion
msg_warn "This will:"
printf ' 1. Delete tags: %s, %s, %s\n' "$patch" "$minor" "$major"
if [ "$reset_head" = "true" ]; then
printf ' 2. Reset HEAD to previous commit (undo release prep)\n'
fi
printf '\n'
if ! prompt_confirmation "Proceed with rollback?"; then
msg_warn "Rollback cancelled"
exit 0
fi
printf '\n'
# Delete tags
msg_info "Deleting tags..."
for tag in "$patch" "$minor" "$major"; do
if check_tag_exists "$tag"; then
git tag -d "$tag"
msg_item "Deleted tag: $tag"
else
msg_notice "Tag not found: $tag (skipping)"
fi
done
# Reset HEAD if needed
if [ "$reset_head" = "true" ]; then
printf '\n'
msg_info "Resetting HEAD to previous commit..."
git reset --hard HEAD~1
msg_item "Reset complete"
new_head=$(git rev-parse HEAD)
new_head_short=$(echo "$new_head" | cut -c1-7)
printf 'New HEAD: %s%s%s\n' "$GREEN" "$new_head_short" "$NC"
fi
printf '\n'
msg_done "Rollback complete"
printf '\n'
msg_warn "Note:"
printf ' Tags were deleted locally only\n'
printf ' If you had pushed the tags, delete them from remote:\n'
printf ' git push origin --delete %s %s %s\n' "$patch" "$minor" "$major"

View File

@@ -2,59 +2,7 @@
# Release script for creating versioned tags and updating action references # Release script for creating versioned tags and updating action references
set -eu set -eu
# Parse arguments VERSION="${1:-}"
VERSION=""
DRY_RUN=false
SKIP_CONFIRM=false
PREP_ONLY=false
TAG_ONLY=false
while [ $# -gt 0 ]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
--yes|--no-confirm)
SKIP_CONFIRM=true
shift
;;
--prep-only)
PREP_ONLY=true
shift
;;
--tag-only)
TAG_ONLY=true
shift
;;
--help|-h)
printf 'Usage: %s [OPTIONS] VERSION\n' "$0"
printf '\n'
printf 'Options:\n'
printf ' --dry-run Show what would happen without making changes\n'
printf ' --yes Skip confirmation prompt\n'
printf ' --no-confirm Alias for --yes\n'
printf ' --prep-only Only update refs and commit (no tags)\n'
printf ' --tag-only Only create tags (assumes prep done)\n'
printf ' --help, -h Show this help message\n'
printf '\n'
printf 'Examples:\n'
printf ' %s v2025.11.01\n' "$0"
printf ' %s --dry-run v2025.11.01\n' "$0"
printf ' %s --yes v2025.11.01\n' "$0"
exit 0
;;
-*)
printf 'Unknown option: %s\n' "$1" >&2
printf 'Use --help for usage information\n' >&2
exit 1
;;
*)
VERSION="$1"
shift
;;
esac
done
# Source shared utilities # Source shared utilities
# shellcheck source=_tools/shared.sh # shellcheck source=_tools/shared.sh
@@ -63,17 +11,15 @@ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
. "$SCRIPT_DIR/shared.sh" . "$SCRIPT_DIR/shared.sh"
if [ -z "$VERSION" ]; then if [ -z "$VERSION" ]; then
msg_error "VERSION argument required" printf '%b' "${RED}Error: VERSION argument required${NC}\n"
printf 'Usage: %s [OPTIONS] VERSION\n' "$0" printf 'Usage: %s v2025.10.18\n' "$0"
printf 'Use --help for more information\n'
exit 1 exit 1
fi fi
# Validate version format # Validate version format
if ! validate_version "$VERSION"; then if ! validate_version "$VERSION"; then
msg_error "Invalid version format: $VERSION" printf '%b' "${RED}Error: Invalid version format: $VERSION${NC}\n"
printf 'Expected: vYYYY.MM.DD with zero-padded month/day (e.g., v2025.10.18, v2025.01.05)\n' printf 'Expected: vYYYY.MM.DD (e.g., v2025.10.18)\n'
printf 'Invalid: v2025.1.5 (must be zero-padded)\n'
exit 1 exit 1
fi fi
@@ -89,201 +35,68 @@ major="v$year"
minor="v$year.$month" minor="v$year.$month"
patch="v$year.$month.$day" patch="v$year.$month.$day"
# Show dry-run banner if applicable printf '%b' "${BLUE}Creating release $VERSION${NC}\n"
if [ "$DRY_RUN" = "true" ]; then
msg_plain "$YELLOW" "=== DRY RUN MODE ==="
printf 'No changes will be made to git repository\n'
printf '\n'
fi
msg_info "Creating release $VERSION"
printf ' Major: %s\n' "$major" printf ' Major: %s\n' "$major"
printf ' Minor: %s\n' "$minor" printf ' Minor: %s\n' "$minor"
printf ' Patch: %s\n' "$patch" printf ' Patch: %s\n' "$patch"
printf '\n' printf '\n'
# Check if git is available (required for all modes)
if ! require_git 2>/dev/null; then
msg_error "git not available"
exit 1
fi
# Pre-flight checks (skip for --tag-only since prep should be done)
if [ "$TAG_ONLY" = "false" ]; then
msg_info "Running pre-flight checks..."
msg_item "git is available"
# Check if on main branch
if ! check_on_branch "main"; then
current_branch=$(git rev-parse --abbrev-ref HEAD)
msg_error "Not on main branch (currently on: $current_branch)"
if [ "$DRY_RUN" = "false" ]; then
exit 1
fi
else
msg_item "On main branch"
fi
# Check if working directory is clean
if ! check_git_clean; then
msg_error "Working directory has uncommitted changes"
if [ "$DRY_RUN" = "false" ]; then
printf 'Please commit or stash changes before creating a release\n'
exit 1
fi
else
msg_item "Working directory is clean"
fi
# Check if patch tag already exists
if check_tag_exists "$patch"; then
msg_error "Tag $patch already exists"
if [ "$DRY_RUN" = "false" ]; then
printf 'Use a different version or delete the existing tag first\n'
exit 1
fi
else
msg_item "Tag $patch does not exist"
fi
printf '\n'
fi
# Get current commit SHA # Get current commit SHA
current_sha=$(git rev-parse HEAD) current_sha=$(git rev-parse HEAD)
printf 'Current HEAD: %s%s%s\n' "$GREEN" "$current_sha" "$NC" printf '%b' "Current HEAD: ${GREEN}$current_sha${NC}\n"
printf '\n' printf '\n'
# Confirmation prompt (skip if --yes or --dry-run) # Update all action references to current SHA
if [ "$DRY_RUN" = "false" ] && [ "$SKIP_CONFIRM" = "false" ]; then printf '%b' "${BLUE}Updating action references to $current_sha...${NC}\n"
if ! prompt_confirmation "Proceed with release $VERSION?"; then "$SCRIPT_DIR/update-action-refs.sh" "$current_sha" "direct"
msg_warn "Release cancelled by user"
exit 0
fi
printf '\n'
fi
# Skip prep if --tag-only # Commit the changes
if [ "$TAG_ONLY" = "true" ]; then if ! git diff --quiet; then
msg_info "Skipping preparation (--tag-only mode)" git add -- */action.yml
printf '\n' git commit -m "chore: update action references for release $VERSION
else
# Update all action references to current SHA
msg_info "Updating action references to $current_sha..."
if [ "$DRY_RUN" = "true" ]; then
msg_warn "[DRY RUN] Would run: update-action-refs.sh $current_sha direct"
else
"$SCRIPT_DIR/update-action-refs.sh" "$current_sha" "direct"
fi
fi
# Commit the changes (skip if --tag-only)
if [ "$TAG_ONLY" = "false" ]; then
if ! git diff --quiet; then
if [ "$DRY_RUN" = "true" ]; then
msg_warn "[DRY RUN] Would add: */action.yml"
msg_warn "[DRY RUN] Would commit: update action references for release $VERSION"
else
git add -- */action.yml
git commit -m "chore: update action references for release $VERSION
This commit updates all internal action references to point to the current This commit updates all internal action references to point to the current
commit SHA in preparation for release $VERSION." commit SHA in preparation for release $VERSION."
# Update SHA since we just created a new commit # Update SHA since we just created a new commit
current_sha=$(git rev-parse HEAD) current_sha=$(git rev-parse HEAD)
msg_done "Committed updated action references" printf '%b' "${GREEN}Committed updated action references${NC}\n"
printf 'New HEAD: %s%s%s\n' "$GREEN" "$current_sha" "$NC" printf '%b' "New HEAD: ${GREEN}$current_sha${NC}\n"
fi else
else printf '%b' "${BLUE}No changes to commit${NC}\n"
msg_info "No changes to commit"
fi
fi
# Exit early if --prep-only
if [ "$PREP_ONLY" = "true" ]; then
printf '\n'
msg_done "Preparation complete (--prep-only mode)"
msg_warn "Run with --tag-only to create tags"
exit 0
fi fi
# Create/update tags # Create/update tags
printf '\n' printf '%b' "${BLUE}Creating tags...${NC}\n"
msg_info "Creating tags..."
# Create patch tag # Create patch tag
if [ "$DRY_RUN" = "true" ]; then git tag -a "$patch" -m "Release $patch"
msg_warn "[DRY RUN] Would create tag: $patch" printf '%b' " ${GREEN}${NC} Created tag: $patch\n"
else
git tag -a "$patch" -m "Release $patch"
msg_item "Created tag: $patch"
fi
# Move/create minor tag # Move/create minor tag
if git rev-parse "$minor" >/dev/null 2>&1; then if git rev-parse "$minor" >/dev/null 2>&1; then
if [ "$DRY_RUN" = "true" ]; then git tag -f -a "$minor" -m "Latest $minor release: $patch"
msg_warn "[DRY RUN] Would force-update tag: $minor" printf '%b' " ${GREEN}${NC} Updated tag: $minor (force)\n"
else
git tag -f -a "$minor" -m "Latest $minor release: $patch"
msg_item "Updated tag: $minor (force)"
fi
else else
if [ "$DRY_RUN" = "true" ]; then git tag -a "$minor" -m "Latest $minor release: $patch"
msg_warn "[DRY RUN] Would create tag: $minor" printf '%b' " ${GREEN}${NC} Created tag: $minor\n"
else
git tag -a "$minor" -m "Latest $minor release: $patch"
msg_item "Created tag: $minor"
fi
fi fi
# Move/create major tag # Move/create major tag
if git rev-parse "$major" >/dev/null 2>&1; then if git rev-parse "$major" >/dev/null 2>&1; then
if [ "$DRY_RUN" = "true" ]; then git tag -f -a "$major" -m "Latest $major release: $patch"
msg_warn "[DRY RUN] Would force-update tag: $major" printf '%b' " ${GREEN}${NC} Updated tag: $major (force)\n"
else
git tag -f -a "$major" -m "Latest $major release: $patch"
msg_item "Updated tag: $major (force)"
fi
else else
if [ "$DRY_RUN" = "true" ]; then git tag -a "$major" -m "Latest $major release: $patch"
msg_warn "[DRY RUN] Would create tag: $major" printf '%b' " ${GREEN}${NC} Created tag: $major\n"
else
git tag -a "$major" -m "Latest $major release: $patch"
msg_item "Created tag: $major"
fi
fi fi
printf '\n' printf '\n'
if [ "$DRY_RUN" = "true" ]; then printf '%b' "${GREEN}✅ Release $VERSION created successfully${NC}\n"
msg_done "Dry run complete - no changes made"
printf '\n'
msg_info "Would have created release $VERSION"
else
msg_done "Release $VERSION created successfully"
fi
printf '\n' printf '\n'
msg_plain "$YELLOW" "All tags point to: $current_sha" printf '%b' "${YELLOW}All tags point to: $current_sha${NC}\n"
printf '\n' printf '\n'
msg_info "Tags created:" printf '%b' "${BLUE}Tags created:${NC}\n"
printf ' %s\n' "$patch" printf ' %s\n' "$patch"
printf ' %s\n' "$minor" printf ' %s\n' "$minor"
printf ' %s\n' "$major" printf ' %s\n' "$major"
printf '\n'
# Enhanced next steps
if [ "$DRY_RUN" = "false" ]; then
msg_warn "Next steps:"
printf ' 1. Review changes: git show HEAD\n'
printf ' 2. Verify CI status: gh run list --limit 5\n'
printf ' 3. Push tags: git push origin main --tags --force-with-lease\n'
printf ' 4. Update workflow refs: make update-version-refs MAJOR=%s\n' "$major"
printf ' 5. Update README examples if needed\n'
printf ' 6. Create GitHub release: gh release create %s --generate-notes\n' "$VERSION"
printf '\n'
msg_info "If something went wrong:"
printf ' Rollback: make release-undo\n'
else
msg_warn "To execute this release:"
printf ' Run without --dry-run flag\n'
fi

View File

@@ -14,12 +14,12 @@ YELLOW='\033[1;33m'
# shellcheck disable=SC2034 # shellcheck disable=SC2034
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Validate CalVer version format: vYYYY.MM.DD (zero-padded) # Validate CalVer version format: vYYYY.MM.DD
validate_version() { validate_version() {
version="$1" version="$1"
# Check format: vYYYY.MM.DD (require zero-padding) using grep # Check format: vYYYY.MM.DD using grep
if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}$'; then if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}$'; then
return 1 return 1
fi fi
@@ -34,12 +34,12 @@ validate_version() {
return 1 return 1
fi fi
# Validate month (01-12) # Validate month (1-12)
if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then
return 1 return 1
fi fi
# Validate day (01-31) # Validate day (1-31)
if [ "$day" -lt 1 ] || [ "$day" -gt 31 ]; then if [ "$day" -lt 1 ] || [ "$day" -gt 31 ]; then
return 1 return 1
fi fi
@@ -67,12 +67,12 @@ validate_major_version() {
return 0 return 0
} }
# Validate minor version format: vYYYY.MM (zero-padded) # Validate minor version format: vYYYY.MM
validate_minor_version() { validate_minor_version() {
version="$1" version="$1"
# Check format: vYYYY.MM (require zero-padding) using grep # Check format: vYYYY.MM using grep
if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{2}$'; then if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{1,2}$'; then
return 1 return 1
fi fi
@@ -86,7 +86,7 @@ validate_minor_version() {
return 1 return 1
fi fi
# Validate month (01-12) # Validate month (1-12)
if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then
return 1 return 1
fi fi
@@ -94,139 +94,6 @@ validate_minor_version() {
return 0 return 0
} }
# Check if working directory is clean (no uncommitted changes)
check_git_clean() {
if ! has_git; then
return 1
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
return 1
fi
return 0
}
# Check if currently on specified branch (default: main)
check_on_branch() {
target_branch="${1:-main}"
if ! has_git; then
return 1
fi
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
if [ "$current_branch" != "$target_branch" ]; then
return 1
fi
return 0
}
# Check if a git tag exists
check_tag_exists() {
tag="$1"
if ! has_git; then
return 1
fi
if git rev-parse "$tag" >/dev/null 2>&1; then
return 0
fi
return 1
}
# Prompt user for yes/no confirmation
# Usage: if prompt_confirmation "Continue?"; then ...; fi
prompt_confirmation() {
prompt_text="${1:-Continue?}"
timeout_seconds="${2:-30}"
# Check if stdin is a TTY (interactive terminal)
if [ ! -t 0 ]; then
msg_error "Non-interactive session detected - cannot prompt for confirmation"
return 1
fi
# Check if timeout command is available for optional timeout support
if command -v timeout >/dev/null 2>&1; then
printf '%s [y/N] (timeout in %ss) ' "$prompt_text" "$timeout_seconds"
# Create a temporary file to store the response
_temp_response=$(mktemp) || return 1
# Use timeout with --foreground to allow reading from TTY
# Write response to temp file instead of trying to capture in command substitution
if timeout --foreground "$timeout_seconds" sh -c "read -r r && printf '%s' \"\$r\" > '$_temp_response'" 2>/dev/null; then
response=$(cat "$_temp_response")
rm -f "$_temp_response"
else
rm -f "$_temp_response"
printf '\n'
msg_warn "Confirmation timeout - defaulting to No"
return 1
fi
else
# No timeout available - plain read
printf '%s [y/N] ' "$prompt_text"
read -r response || return 1
fi
case "$response" in
[yY]|[yY][eE][sS])
return 0
;;
*)
return 1
;;
esac
}
# Message output functions for consistent, colored output
# These functions provide a clean API for printing status messages
# msg_error "message" - Print error message in red with ✗ symbol to stderr
msg_error() {
printf '%s✗ %s%s\n' "$RED" "$1" "$NC" >&2
}
# msg_success "message" - Print success message in green with ✓ symbol
msg_success() {
printf '%s✓ %s%s\n' "$GREEN" "$1" "$NC"
}
# msg_done "message" - Print completion message in green with ✅ symbol
msg_done() {
printf '%s✅ %s%s\n' "$GREEN" "$1" "$NC"
}
# msg_info "message" - Print info/status message in blue (no symbol)
msg_info() {
printf '%s%s%s\n' "$BLUE" "$1" "$NC"
}
# msg_warn "message" - Print warning message in yellow (no symbol)
msg_warn() {
printf '%s%s%s\n' "$YELLOW" "$1" "$NC"
}
# msg_item "message" - Print indented item with ✓ in green
msg_item() {
printf ' %s✓%s %s\n' "$GREEN" "$NC" "$1"
}
# msg_notice "message" - Print indented notice with in blue
msg_notice() {
printf ' %s%s %s\n' "$BLUE" "$NC" "$1"
}
# msg_plain "color" "message" - Print plain colored message (no symbol)
# Usage: msg_plain "$YELLOW" "=== BANNER ==="
msg_plain() {
color="$1"
message="$2"
printf '%s%s%s\n' "$color" "$message" "$NC"
}
# Get the directory where the calling script is located # Get the directory where the calling script is located
get_script_dir() { get_script_dir() {
cd "$(dirname -- "$1")" && pwd cd "$(dirname -- "$1")" && pwd
@@ -240,7 +107,7 @@ has_git() {
# Require git to be available, exit with error if not # Require git to be available, exit with error if not
require_git() { require_git() {
if ! has_git; then if ! has_git; then
msg_error "git is not installed or not in PATH" printf '%b' "${RED}Error: git is not installed or not in PATH${NC}\n" >&2
printf 'Please install git to use this script.\n' >&2 printf 'Please install git to use this script.\n' >&2
exit 1 exit 1
fi fi
@@ -250,7 +117,7 @@ require_git() {
safe_mktemp() { safe_mktemp() {
_temp_file="" _temp_file=""
if ! _temp_file=$(mktemp); then if ! _temp_file=$(mktemp); then
msg_error "Failed to create temp file" printf '%b' "${RED}Error: Failed to create temp file${NC}\n" >&2
exit 1 exit 1
fi fi
printf '%s' "$_temp_file" printf '%s' "$_temp_file"

View File

@@ -34,7 +34,7 @@ runs:
using: composite using: composite
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: ${{ inputs.token || github.token }} token: ${{ inputs.token || github.token }}
fetch-depth: 0 fetch-depth: 0

View File

@@ -45,19 +45,55 @@ runs:
steps: steps:
- name: Validate Inputs - name: Validate Inputs
id: validate id: validate
uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 shell: bash
with: env:
action-type: 'ansible-lint-fix' GITHUB_TOKEN: ${{ inputs.token }}
token: ${{ inputs.token }} EMAIL: ${{ inputs.email }}
email: ${{ inputs.email }} USERNAME: ${{ inputs.username }}
username: ${{ inputs.username }} MAX_RETRIES: ${{ inputs.max-retries }}
max-retries: ${{ inputs.max-retries }} run: |
set -euo pipefail
# Validate GitHub token format (basic validation)
if [[ -n "$GITHUB_TOKEN" ]]; then
# Skip validation for GitHub expressions (they'll be resolved at runtime)
if ! [[ "$GITHUB_TOKEN" =~ ^gh[efpousr]_[a-zA-Z0-9]{36}$ ]] && ! [[ "$GITHUB_TOKEN" =~ ^\$\{\{ ]]; then
echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters"
fi
fi
# Validate email format (basic check)
if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then
echo "::error::Invalid email format: '$EMAIL'. Expected valid email address"
exit 1
fi
# Validate username format (prevent command injection)
if [[ "$USERNAME" == *";"* ]] || [[ "$USERNAME" == *"&&"* ]] || [[ "$USERNAME" == *"|"* ]]; then
echo "::error::Invalid username: '$USERNAME'. Command injection patterns not allowed"
exit 1
fi
# Validate username length
username="$USERNAME"
if [ ${#username} -gt 39 ]; then
echo "::error::Username too long: ${#username} characters. GitHub usernames are max 39 characters"
exit 1
fi
# Validate max retries (positive integer with reasonable upper limit)
if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then
echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10"
exit 1
fi
echo "Input validation completed successfully"
- name: Check for Ansible Files - name: Check for Ansible Files
id: check-files id: check-files
shell: sh shell: bash
run: | run: |
set -eu set -euo pipefail
# Check for both .yml and .yaml files # Check for both .yml and .yaml files
if find . \( -name "*.yml" -o -name "*.yaml" \) -type f | grep -q .; then if find . \( -name "*.yml" -o -name "*.yaml" \) -type f | grep -q .; then
@@ -69,14 +105,14 @@ runs:
fi fi
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: ${{ inputs.token || github.token }} token: ${{ inputs.token || github.token }}
- name: Cache Python Dependencies - name: Cache Python Dependencies
if: steps.check-files.outputs.files_found == 'true' if: steps.check-files.outputs.files_found == 'true'
id: cache-pip id: cache-pip
uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/common-cache@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
type: 'pip' type: 'pip'
paths: '~/.cache/pip' paths: '~/.cache/pip'
@@ -86,18 +122,18 @@ runs:
- name: Install ansible-lint - name: Install ansible-lint
id: install-ansible-lint id: install-ansible-lint
if: steps.check-files.outputs.files_found == 'true' if: steps.check-files.outputs.files_found == 'true'
uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 uses: ivuorinen/actions/common-retry@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
timeout_minutes: 5
max_attempts: ${{ inputs.max-retries }}
command: 'pip install ansible-lint==6.22.1' command: 'pip install ansible-lint==6.22.1'
max-retries: ${{ inputs.max-retries }}
description: 'Installing Python dependencies (ansible-lint)'
- name: Run ansible-lint - name: Run ansible-lint
if: steps.check-files.outputs.files_found == 'true' if: steps.check-files.outputs.files_found == 'true'
id: lint id: lint
shell: sh shell: bash
run: | run: |
set -eu set -euo pipefail
# Run ansible-lint and capture exit code # Run ansible-lint and capture exit code
if ansible-lint --write --parseable-severity --format sarif > ansible-lint.sarif; then if ansible-lint --write --parseable-severity --format sarif > ansible-lint.sarif; then
@@ -123,16 +159,31 @@ runs:
# Exit with the original ansible-lint exit code # Exit with the original ansible-lint exit code
exit "$lint_exit_code" exit "$lint_exit_code"
- name: Set Git Config for Fixes
id: set-git-config
if: steps.check-files.outputs.files_found == 'true'
uses: ivuorinen/actions/set-git-config@e2222afff180ee77f330ef4325f60d6e85477c01
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
email: ${{ inputs.email }}
- name: Commit Fixes - name: Commit Fixes
if: steps.check-files.outputs.files_found == 'true' if: steps.check-files.outputs.files_found == 'true'
uses: stefanzweifel/git-auto-commit-action@be7095c202abcf573b09f20541e0ee2f6a3a9d9b # v5.0.1 shell: bash
with: run: |
commit_message: 'style: apply ansible lint fixes' set -euo pipefail
commit_user_name: ${{ inputs.username }}
commit_user_email: ${{ inputs.email }} if git diff --quiet; then
echo "No changes to commit."
else
git add .
git commit -m "fix: applied ansible lint fixes"
git push
fi
- name: Upload SARIF Report - name: Upload SARIF Report
if: steps.check-files.outputs.files_found == 'true' if: steps.check-files.outputs.files_found == 'true'
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
sarif_file: ansible-lint.sarif sarif_file: ansible-lint.sarif

58
biome-check/README.md Normal file
View File

@@ -0,0 +1,58 @@
# ivuorinen/actions/biome-check
## Biome Check
### Description
Run Biome check on the repository
### Inputs
| name | description | required | default |
|---------------|--------------------------------------------------------------------|----------|-----------------------------|
| `token` | <p>GitHub token for authentication</p> | `false` | `${{ github.token }}` |
| `username` | <p>GitHub username for commits</p> | `false` | `github-actions` |
| `email` | <p>GitHub email for commits</p> | `false` | `github-actions@github.com` |
| `max-retries` | <p>Maximum number of retry attempts for npm install operations</p> | `false` | `3` |
### Outputs
| name | description |
|------------------|---------------------------------------|
| `check_status` | <p>Check status (success/failure)</p> |
| `errors_count` | <p>Number of errors found</p> |
| `warnings_count` | <p>Number of warnings found</p> |
### 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
```

238
biome-check/action.yml Normal file
View File

@@ -0,0 +1,238 @@
# 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
---
name: Biome Check
description: Run Biome check 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:
check_status:
description: 'Check status (success/failure)'
value: ${{ steps.check.outputs.status }}
errors_count:
description: 'Number of errors found'
value: ${{ steps.check.outputs.errors }}
warnings_count:
description: 'Number of warnings found'
value: ${{ steps.check.outputs.warnings }}
runs:
using: composite
steps:
- name: Validate Inputs (Centralized)
uses: ivuorinen/actions/validate-inputs@e2222afff180ee77f330ef4325f60d6e85477c01
with:
action-type: biome-check
- name: Validate Inputs (Additional)
id: validate
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
EMAIL: ${{ inputs.email }}
USERNAME: ${{ inputs.username }}
MAX_RETRIES: ${{ inputs.max-retries }}
run: |
set -euo pipefail
# Validate GitHub token presence (no format validation to avoid false warnings)
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 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 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 no consecutive hyphens
if [[ "$username" == *--* ]]; then
echo "::error::Invalid username '$username'. Consecutive hyphens not allowed"
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token }}
- name: Set Git Config
uses: ivuorinen/actions/set-git-config@e2222afff180ee77f330ef4325f60d6e85477c01
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
email: ${{ inputs.email }}
- name: Node Setup
id: node-setup
uses: ivuorinen/actions/node-setup@e2222afff180ee77f330ef4325f60d6e85477c01
- name: Cache Node Dependencies
id: cache
uses: ivuorinen/actions/common-cache@e2222afff180ee77f330ef4325f60d6e85477c01
with:
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 }}'
- name: Install Biome
shell: bash
env:
PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }}
MAX_RETRIES: ${{ inputs.max-retries }}
run: |
set -euo pipefail
# 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 Check
id: check
shell: bash
run: |
set -euo pipefail
echo "Running Biome check..."
# Run Biome check with SARIF reporter
biome_exit_code=0
biome check . --reporter=sarif > biome-report.sarif || biome_exit_code=$?
# Handle failures gracefully
if [ $biome_exit_code -ne 0 ] && [ ! -s biome-report.sarif ]; then
echo "::warning::SARIF report generation failed with exit code $biome_exit_code"
# Create empty SARIF file to avoid upload errors
echo '{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[]}' > biome-report.sarif
fi
# Parse SARIF output for error counts
if [ -f biome-report.sarif ]; then
errors=$(jq '[.runs[]?.results[]? | select(.level == "error" or .level == "warning")] | length' biome-report.sarif 2>/dev/null || echo "0")
warnings="0" # Biome doesn't separate warnings in SARIF output
else
errors="0"
warnings="0"
fi
if [ $biome_exit_code -eq 0 ]; then
echo "status=success" >> "$GITHUB_OUTPUT"
echo "errors=0" >> "$GITHUB_OUTPUT"
echo "warnings=0" >> "$GITHUB_OUTPUT"
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 to fail the job on errors
exit $biome_exit_code
- name: Upload Biome Results
if: always()
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
sarif_file: biome-report.sarif

41
biome-check/rules.yml Normal file
View File

@@ -0,0 +1,41 @@
---
# 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

57
biome-fix/README.md Normal file
View File

@@ -0,0 +1,57 @@
# ivuorinen/actions/biome-fix
## Biome Fix
### Description
Run Biome fix on the repository
### Inputs
| name | description | required | default |
|---------------|--------------------------------------------------------------------|----------|-----------------------------|
| `token` | <p>GitHub token for authentication</p> | `false` | `${{ github.token }}` |
| `username` | <p>GitHub username for commits</p> | `false` | `github-actions` |
| `email` | <p>GitHub email for commits</p> | `false` | `github-actions@github.com` |
| `max-retries` | <p>Maximum number of retry attempts for npm install operations</p> | `false` | `3` |
### Outputs
| name | description |
|-----------------|----------------------------------------------|
| `files_changed` | <p>Number of files changed by formatting</p> |
| `fix_status` | <p>Fix status (success/failure)</p> |
### 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
```

204
biome-fix/action.yml Normal file
View File

@@ -0,0 +1,204 @@
# 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token }}
- name: Set Git Config
uses: ivuorinen/actions/set-git-config@e2222afff180ee77f330ef4325f60d6e85477c01
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
email: ${{ inputs.email }}
- name: Node Setup
id: node-setup
uses: ivuorinen/actions/node-setup@e2222afff180ee77f330ef4325f60d6e85477c01
- name: Cache Node Dependencies
id: cache
uses: ivuorinen/actions/common-cache@e2222afff180ee77f330ef4325f60d6e85477c01
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'

41
biome-fix/rules.yml Normal file
View File

@@ -0,0 +1,41 @@
---
# 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

View File

@@ -1,73 +0,0 @@
# ivuorinen/actions/biome-lint
## Biome Lint
### Description
Run Biome linter in check or fix mode
### Inputs
| name | description | required | default |
|-----------------|---------------------------------------------------------------------------------|----------|-----------------------------|
| `mode` | <p>Mode to run (check or fix)</p> | `false` | `check` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
| `username` | <p>GitHub username for commits (fix mode only)</p> | `false` | `github-actions` |
| `email` | <p>GitHub email for commits (fix mode only)</p> | `false` | `github-actions@github.com` |
| `max-retries` | <p>Maximum number of retry attempts for npm install operations</p> | `false` | `3` |
| `fail-on-error` | <p>Whether to fail the action if linting errors are found (check mode only)</p> | `false` | `true` |
### Outputs
| name | description |
|------------------|---------------------------------------------------|
| `status` | <p>Overall status (success/failure)</p> |
| `errors_count` | <p>Number of errors found (check mode only)</p> |
| `warnings_count` | <p>Number of warnings found (check mode only)</p> |
| `files_changed` | <p>Number of files changed (fix mode only)</p> |
### 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
```

View File

@@ -1,297 +0,0 @@
# 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: Biome Lint
description: Run Biome linter in check or fix mode
author: Ismo Vuorinen
branding:
icon: check-circle
color: green
inputs:
mode:
description: 'Mode to run (check or fix)'
required: false
default: 'check'
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'
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:
status:
description: 'Overall status (success/failure)'
value: ${{ steps.check.outputs.status || steps.fix.outputs.status }}
errors_count:
description: 'Number of errors found (check mode only)'
value: ${{ steps.check.outputs.errors }}
warnings_count:
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
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 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
echo "Using provided GitHub token"
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"
# 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 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
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 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 || 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: 'biome-lint-${{ inputs.mode }}-${{ steps.node-setup.outputs.package-manager }}'
- name: Install Biome
shell: bash
env:
PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }}
MAX_RETRIES: ${{ inputs.max-retries }}
run: |
set -euo pipefail
# 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 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 mode..."
# Run Biome check with SARIF reporter
biome_exit_code=0
biome check . --reporter=sarif > biome-report.sarif || biome_exit_code=$?
# Handle failures gracefully
if [ $biome_exit_code -ne 0 ] && [ ! -s biome-report.sarif ]; then
echo "::warning::SARIF report generation failed with exit code $biome_exit_code"
# Create empty SARIF file to avoid upload errors
echo '{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[]}' > biome-report.sarif
fi
# Parse SARIF output for error counts
if [ -f biome-report.sarif ]; then
errors=$(jq '[.runs[]?.results[]? | select(.level == "error" or .level == "warning")] | length' biome-report.sarif 2>/dev/null || echo "0")
warnings="0" # Biome doesn't separate warnings in SARIF output
else
errors="0"
warnings="0"
fi
if [ $biome_exit_code -eq 0 ]; then
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
# Exit with biome's exit code if fail-on-error is true
if [ "$FAIL_ON_ERROR" = "true" ]; then
exit $biome_exit_code
fi
- 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'

View File

@@ -26,6 +26,7 @@ Run CodeQL security analysis for a single language with configurable query suite
| `threads` | <p>Number of threads that can be used by CodeQL</p> | `false` | `""` | | `threads` | <p>Number of threads that can be used by CodeQL</p> | `false` | `""` |
| `output` | <p>Path to save SARIF results</p> | `false` | `../results` | | `output` | <p>Path to save SARIF results</p> | `false` | `../results` |
| `skip-queries` | <p>Build database but skip running queries</p> | `false` | `false` | | `skip-queries` | <p>Build database but skip running queries</p> | `false` | `false` |
| `add-snippets` | <p>Add code snippets to SARIF output</p> | `false` | `false` |
### Outputs ### Outputs
@@ -139,4 +140,10 @@ This action is a `composite` action.
# #
# Required: false # Required: false
# Default: false # Default: false
add-snippets:
# Add code snippets to SARIF output
#
# Required: false
# Default: false
``` ```

View File

@@ -90,6 +90,11 @@ inputs:
required: false required: false
default: 'false' default: 'false'
add-snippets:
description: 'Add code snippets to SARIF output'
required: false
default: 'false'
outputs: outputs:
language-analyzed: language-analyzed:
description: 'Language that was analyzed' description: 'Language that was analyzed'
@@ -107,7 +112,7 @@ runs:
using: composite using: composite
steps: steps:
- name: Validate inputs - name: Validate inputs
uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/validate-inputs@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
action-type: codeql-analysis action-type: codeql-analysis
language: ${{ inputs.language }} language: ${{ inputs.language }}
@@ -126,6 +131,7 @@ runs:
threads: ${{ inputs.threads }} threads: ${{ inputs.threads }}
output: ${{ inputs.output }} output: ${{ inputs.output }}
skip-queries: ${{ inputs.skip-queries }} skip-queries: ${{ inputs.skip-queries }}
add-snippets: ${{ inputs.add-snippets }}
- name: Validate checkout safety - name: Validate checkout safety
shell: bash shell: bash
@@ -140,7 +146,7 @@ runs:
fi fi
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: ${{ inputs.checkout-ref || github.sha }} ref: ${{ inputs.checkout-ref || github.sha }}
token: ${{ inputs.token }} token: ${{ inputs.token }}
@@ -183,7 +189,7 @@ runs:
echo "Using build mode: $build_mode" echo "Using build mode: $build_mode"
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
languages: ${{ inputs.language }} languages: ${{ inputs.language }}
queries: ${{ inputs.queries }} queries: ${{ inputs.queries }}
@@ -196,18 +202,19 @@ runs:
threads: ${{ inputs.threads }} threads: ${{ inputs.threads }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
if: ${{ steps.set-build-mode.outputs.build-mode == 'autobuild' }} if: ${{ steps.set-build-mode.outputs.build-mode == 'autobuild' }}
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
id: analysis id: analysis
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
category: ${{ steps.set-category.outputs.category }} category: ${{ steps.set-category.outputs.category }}
upload: ${{ inputs.upload-results }} upload: ${{ inputs.upload-results }}
output: ${{ inputs.output }} output: ${{ inputs.output }}
ram: ${{ inputs.ram }} ram: ${{ inputs.ram }}
threads: ${{ inputs.threads }} threads: ${{ inputs.threads }}
add-snippets: ${{ inputs.add-snippets }}
skip-queries: ${{ inputs.skip-queries }} skip-queries: ${{ inputs.skip-queries }}
- name: Summary - name: Summary

View File

@@ -2,7 +2,7 @@
# Validation rules for codeql-analysis action # Validation rules for codeql-analysis action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0 # Schema version: 1.0
# Coverage: 94% (15/16 inputs) # Coverage: 94% (16/17 inputs)
# #
# This file defines validation rules for the codeql-analysis GitHub Action. # This file defines validation rules for the codeql-analysis GitHub Action.
# Rules are automatically applied by validate-inputs action when this # Rules are automatically applied by validate-inputs action when this
@@ -16,6 +16,7 @@ generator_version: 1.0.0
required_inputs: required_inputs:
- language - language
optional_inputs: optional_inputs:
- add-snippets
- build-mode - build-mode
- category - category
- checkout-ref - checkout-ref
@@ -32,6 +33,7 @@ optional_inputs:
- upload-results - upload-results
- working-directory - working-directory
conventions: conventions:
add-snippets: boolean
build-mode: codeql_build_mode build-mode: codeql_build_mode
category: category_format category: category_format
checkout-ref: branch_name checkout-ref: branch_name
@@ -60,8 +62,8 @@ overrides:
threads: numeric_range_1_128 threads: numeric_range_1_128
token: github_token token: github_token
statistics: statistics:
total_inputs: 16 total_inputs: 17
validated_inputs: 15 validated_inputs: 16
skipped_inputs: 0 skipped_inputs: 0
coverage_percentage: 94 coverage_percentage: 94
validation_coverage: 94 validation_coverage: 94

View File

@@ -0,0 +1,115 @@
#!/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")

View File

@@ -0,0 +1,36 @@
# 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` | <p>Glob pattern for files to check.</p> | `true` | `""` |
### Outputs
| name | description |
|---------|----------------------------------------------------------------|
| `found` | <p>Indicates if the files matching the pattern were found.</p> |
### 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: ""
```

View File

@@ -0,0 +1,87 @@
# 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

View File

@@ -0,0 +1,39 @@
---
# 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

185
common-retry/CustomValidator.py Executable file
View File

@@ -0,0 +1,185 @@
#!/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",
},
}

101
common-retry/README.md Normal file
View File

@@ -0,0 +1,101 @@
# ivuorinen/actions/common-retry
## Common Retry
### Description
Standardized retry utility for network operations and flaky commands
### Inputs
| name | description | required | default |
|---------------------|---------------------------------------------------------------------|----------|---------------------|
| `command` | <p>Command to execute with retry logic</p> | `true` | `""` |
| `max-retries` | <p>Maximum number of retry attempts</p> | `false` | `3` |
| `retry-delay` | <p>Initial delay between retries in seconds</p> | `false` | `5` |
| `backoff-strategy` | <p>Backoff strategy (linear, exponential, fixed)</p> | `false` | `exponential` |
| `timeout` | <p>Timeout for each attempt in seconds</p> | `false` | `300` |
| `working-directory` | <p>Working directory to execute command in</p> | `false` | `.` |
| `shell` | <p>Shell to use for command execution</p> | `false` | `bash` |
| `success-codes` | <p>Comma-separated list of success exit codes</p> | `false` | `0` |
| `retry-codes` | <p>Comma-separated list of exit codes that should trigger retry</p> | `false` | `1,2,124,126,127` |
| `description` | <p>Human-readable description of the operation for logging</p> | `false` | `Command execution` |
### Outputs
| name | description |
|-------------|---------------------------------------------------|
| `success` | <p>Whether the command succeeded (true/false)</p> |
| `attempts` | <p>Number of attempts made</p> |
| `exit-code` | <p>Final exit code of the command</p> |
| `duration` | <p>Total execution duration in seconds</p> |
### 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
```

246
common-retry/action.yml Normal file
View File

@@ -0,0 +1,246 @@
# 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

50
common-retry/rules.yml Normal file
View File

@@ -0,0 +1,50 @@
---
# 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

View File

@@ -141,9 +141,16 @@ runs:
echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters" echo "::warning::GitHub token format may be invalid. Expected format: gh*_36characters"
fi fi
fi fi
- name: Set Git Config
id: set-git-config
uses: ivuorinen/actions/set-git-config@e2222afff180ee77f330ef4325f60d6e85477c01
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
email: ${{ inputs.email }}
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: ${{ inputs.token }} token: ${{ inputs.token }}
@@ -162,8 +169,7 @@ runs:
if: steps.calibre.outputs.markdown != '' if: steps.calibre.outputs.markdown != ''
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with: with:
token: ${{ inputs.token }} title: Compressed Images Nightly
title: 'chore: compress images'
branch-suffix: timestamp branch-suffix: timestamp
commit-message: 'chore: compress images' commit-message: Compressed Images
body: ${{ steps.calibre.outputs.markdown }} body: ${{ steps.calibre.outputs.markdown }}

View File

@@ -32,7 +32,7 @@ outputs:
value: ${{ steps.test.outputs.status }} value: ${{ steps.test.outputs.status }}
dotnet_version: dotnet_version:
description: 'Version of .NET SDK used' description: 'Version of .NET SDK used'
value: ${{ steps.detect-dotnet-version.outputs.detected-version }} value: ${{ steps.detect-dotnet-version.outputs.dotnet-version }}
artifacts_path: artifacts_path:
description: 'Path to build artifacts' description: 'Path to build artifacts'
value: '**/bin/Release/**/*' value: '**/bin/Release/**/*'
@@ -44,25 +44,24 @@ runs:
using: composite using: composite
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: ${{ inputs.token || github.token }} token: ${{ inputs.token || github.token }}
- name: Detect .NET SDK Version - name: Detect .NET SDK Version
id: detect-dotnet-version id: detect-dotnet-version
uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/dotnet-version-detect@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
language: 'dotnet'
default-version: "${{ inputs.dotnet-version || '7.0' }}" default-version: "${{ inputs.dotnet-version || '7.0' }}"
- name: Setup .NET SDK - name: Setup .NET SDK
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with: with:
dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }} dotnet-version: ${{ steps.detect-dotnet-version.outputs.dotnet-version }}
- name: Cache NuGet packages - name: Cache NuGet packages
id: cache-nuget id: cache-nuget
uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/common-cache@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
type: 'nuget' type: 'nuget'
paths: '~/.nuget/packages' paths: '~/.nuget/packages'
@@ -71,13 +70,13 @@ runs:
- name: Restore Dependencies - name: Restore Dependencies
if: steps.cache-nuget.outputs.cache-hit != 'true' if: steps.cache-nuget.outputs.cache-hit != 'true'
uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3 uses: ivuorinen/actions/common-retry@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
timeout_minutes: 10
max_attempts: ${{ inputs.max-retries }}
command: | command: |
echo "Restoring .NET dependencies..." echo "Restoring .NET dependencies..."
dotnet restore --verbosity normal dotnet restore --verbosity normal
max-retries: ${{ inputs.max-retries }}
description: 'Restoring .NET dependencies'
- name: Skip Restore (Cache Hit) - name: Skip Restore (Cache Hit)
if: steps.cache-nuget.outputs.cache-hit == 'true' if: steps.cache-nuget.outputs.cache-hit == 'true'
@@ -125,7 +124,7 @@ runs:
- name: Upload Test Results - name: Upload Test Results
if: always() if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: csharp-test-results name: csharp-test-results
path: | path: |

View File

@@ -60,21 +60,20 @@ runs:
echo "Input validation completed successfully" echo "Input validation completed successfully"
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: ${{ inputs.token || github.token }} token: ${{ inputs.token || github.token }}
- name: Detect .NET SDK Version - name: Detect .NET SDK Version
id: detect-dotnet-version id: detect-dotnet-version
uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/dotnet-version-detect@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
language: 'dotnet'
default-version: ${{ inputs.dotnet-version || '7.0' }} default-version: ${{ inputs.dotnet-version || '7.0' }}
- name: Setup .NET SDK - name: Setup .NET SDK
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with: with:
dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }} dotnet-version: ${{ steps.detect-dotnet-version.outputs.dotnet-version }}
- name: Install dotnet-format - name: Install dotnet-format
shell: bash shell: bash
@@ -112,6 +111,6 @@ runs:
fi fi
- name: Upload SARIF Report - name: Upload SARIF Report
uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
sarif_file: dotnet-format.sarif sarif_file: dotnet-format.sarif

View File

@@ -45,13 +45,13 @@ runs:
echo "::add-mask::$API_KEY" echo "::add-mask::$API_KEY"
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: ${{ inputs.token || github.token }} token: ${{ inputs.token || github.token }}
- name: Validate Inputs - name: Validate Inputs
id: validate id: validate
uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/validate-inputs@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
action-type: 'csharp-publish' action-type: 'csharp-publish'
token: ${{ inputs.token }} token: ${{ inputs.token }}
@@ -60,19 +60,18 @@ runs:
- name: Detect .NET SDK Version - name: Detect .NET SDK Version
id: detect-dotnet-version id: detect-dotnet-version
uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/dotnet-version-detect@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
language: 'dotnet'
default-version: '7.0' default-version: '7.0'
- name: Setup .NET SDK - name: Setup .NET SDK
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with: with:
dotnet-version: ${{ inputs.dotnet-version || steps.detect-dotnet-version.outputs.detected-version }} dotnet-version: ${{ inputs.dotnet-version || steps.detect-dotnet-version.outputs.dotnet-version }}
- name: Cache NuGet packages - name: Cache NuGet packages
id: cache-nuget id: cache-nuget
uses: ivuorinen/actions/common-cache@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/common-cache@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
type: 'nuget' type: 'nuget'
paths: '~/.nuget/packages' paths: '~/.nuget/packages'

View File

@@ -120,7 +120,7 @@ outputs:
value: ${{ steps.build.outputs.metadata }} value: ${{ steps.build.outputs.metadata }}
platforms: platforms:
description: 'Successfully built platforms' description: 'Successfully built platforms'
value: ${{ steps.detect-platforms.outputs.platforms }} value: ${{ steps.platforms.outputs.built }}
platform-matrix: platform-matrix:
description: 'Build status per platform in JSON format' description: 'Build status per platform in JSON format'
value: ${{ steps.build.outputs.platform-matrix }} value: ${{ steps.build.outputs.platform-matrix }}
@@ -141,13 +141,13 @@ runs:
using: composite using: composite
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
token: ${{ inputs.token || github.token }} token: ${{ inputs.token || github.token }}
- name: Validate Inputs - name: Validate Inputs
id: validate id: validate
uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42 uses: ivuorinen/actions/validate-inputs@e2222afff180ee77f330ef4325f60d6e85477c01
with: with:
action-type: 'docker-build' action-type: 'docker-build'
image-name: ${{ inputs.image-name }} image-name: ${{ inputs.image-name }}
@@ -169,7 +169,7 @@ runs:
fi fi
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
with: with:
platforms: ${{ inputs.architectures }} platforms: ${{ inputs.architectures }}
@@ -186,28 +186,22 @@ runs:
- name: Detect Available Platforms - name: Detect Available Platforms
id: detect-platforms id: detect-platforms
if: inputs.auto-detect-platforms == 'true'
shell: bash shell: bash
env: env:
ARCHITECTURES: ${{ inputs.architectures }} ARCHITECTURES: ${{ inputs.architectures }}
AUTO_DETECT: ${{ inputs.auto-detect-platforms }}
run: | run: |
set -euo pipefail set -euo pipefail
# When auto-detect is enabled, try to detect available platforms # Get available platforms from buildx
if [ "$AUTO_DETECT" = "true" ]; then available_platforms=$(docker buildx ls | grep -o 'linux/[^ ]*' | sort -u | tr '\n' ',' | sed 's/,$//')
available_platforms=$(docker buildx ls | grep -o 'linux/[^ ]*' | sort -u | tr '\n' ',' | sed 's/,$//' || true)
if [ -n "$available_platforms" ]; then if [ -n "$available_platforms" ]; then
echo "platforms=${available_platforms}" >> $GITHUB_OUTPUT echo "platforms=${available_platforms}" >> $GITHUB_OUTPUT
echo "Detected platforms: ${available_platforms}" echo "Detected platforms: ${available_platforms}"
else
echo "platforms=$ARCHITECTURES" >> $GITHUB_OUTPUT
echo "Using default platforms (detection failed): $ARCHITECTURES"
fi
else else
# Auto-detect disabled, use configured architectures
echo "platforms=$ARCHITECTURES" >> $GITHUB_OUTPUT echo "platforms=$ARCHITECTURES" >> $GITHUB_OUTPUT
echo "Using configured platforms: $ARCHITECTURES" echo "Using default platforms: $ARCHITECTURES"
fi fi
- name: Determine Image Name - name: Determine Image Name

View File

@@ -0,0 +1,83 @@
#!/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",
},
}

147
docker-publish-gh/README.md Normal file
View File

@@ -0,0 +1,147 @@
# 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` | <p>The name of the Docker image to publish. Defaults to the repository name.</p> | `false` | `""` |
| `tags` | <p>Comma-separated list of tags for the Docker image.</p> | `true` | `""` |
| `platforms` | <p>Platforms to publish (comma-separated). Defaults to amd64 and arm64.</p> | `false` | `linux/amd64,linux/arm64` |
| `registry` | <p>GitHub Container Registry URL</p> | `false` | `ghcr.io` |
| `token` | <p>GitHub token with package write permissions</p> | `false` | `""` |
| `provenance` | <p>Enable SLSA provenance generation</p> | `false` | `true` |
| `sbom` | <p>Generate Software Bill of Materials</p> | `false` | `true` |
| `max-retries` | <p>Maximum number of retry attempts for publishing</p> | `false` | `3` |
| `retry-delay` | <p>Delay in seconds between retries</p> | `false` | `10` |
| `buildx-version` | <p>Specific Docker Buildx version to use</p> | `false` | `latest` |
| `cache-mode` | <p>Cache mode for build layers (min, max, or inline)</p> | `false` | `max` |
| `auto-detect-platforms` | <p>Automatically detect and build for all available platforms</p> | `false` | `false` |
| `scan-image` | <p>Scan published image for vulnerabilities</p> | `false` | `true` |
| `sign-image` | <p>Sign the published image with cosign</p> | `false` | `true` |
| `parallel-builds` | <p>Number of parallel platform builds (0 for auto)</p> | `false` | `0` |
| `verbose` | <p>Enable verbose logging</p> | `false` | `false` |
### Outputs
| name | description |
|-------------------|-------------------------------------------|
| `image-name` | <p>Full image name including registry</p> |
| `digest` | <p>The digest of the published image</p> |
| `tags` | <p>List of published tags</p> |
| `provenance` | <p>SLSA provenance attestation</p> |
| `sbom` | <p>SBOM document location</p> |
| `scan-results` | <p>Vulnerability scan results</p> |
| `platform-matrix` | <p>Build status per platform</p> |
| `build-time` | <p>Total build time in seconds</p> |
### 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
```

View File

@@ -0,0 +1,495 @@
# 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@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.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"

View File

@@ -0,0 +1,65 @@
---
# 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

View File

@@ -0,0 +1,90 @@
#!/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",
},
}

View File

@@ -0,0 +1,154 @@
# 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` | <p>The name of the Docker image to publish. Defaults to the repository name.</p> | `false` | `""` |
| `tags` | <p>Comma-separated list of tags for the Docker image.</p> | `true` | `""` |
| `platforms` | <p>Platforms to publish (comma-separated). Defaults to amd64 and arm64.</p> | `false` | `linux/amd64,linux/arm64` |
| `username` | <p>Docker Hub username</p> | `true` | `""` |
| `password` | <p>Docker Hub password or access token</p> | `true` | `""` |
| `repository-description` | <p>Update Docker Hub repository description</p> | `false` | `""` |
| `readme-file` | <p>Path to README file to update on Docker Hub</p> | `false` | `README.md` |
| `provenance` | <p>Enable SLSA provenance generation</p> | `false` | `true` |
| `sbom` | <p>Generate Software Bill of Materials</p> | `false` | `true` |
| `max-retries` | <p>Maximum number of retry attempts for publishing</p> | `false` | `3` |
| `retry-delay` | <p>Delay in seconds between retries</p> | `false` | `10` |
| `buildx-version` | <p>Specific Docker Buildx version to use</p> | `false` | `latest` |
| `cache-mode` | <p>Cache mode for build layers (min, max, or inline)</p> | `false` | `max` |
| `auto-detect-platforms` | <p>Automatically detect and build for all available platforms</p> | `false` | `false` |
| `scan-image` | <p>Scan published image for vulnerabilities</p> | `false` | `true` |
| `sign-image` | <p>Sign the published image with cosign</p> | `false` | `false` |
| `verbose` | <p>Enable verbose logging</p> | `false` | `false` |
### Outputs
| name | description |
|-------------------|-------------------------------------------|
| `image-name` | <p>Full image name including registry</p> |
| `digest` | <p>The digest of the published image</p> |
| `tags` | <p>List of published tags</p> |
| `repo-url` | <p>Docker Hub repository URL</p> |
| `scan-results` | <p>Vulnerability scan results</p> |
| `platform-matrix` | <p>Build status per platform</p> |
| `build-time` | <p>Total build time in seconds</p> |
| `signature` | <p>Image signature if signing enabled</p> |
### 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
```

View File

@@ -0,0 +1,500 @@
# 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@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.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

View File

@@ -0,0 +1,68 @@
---
# 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

View File

@@ -4,32 +4,37 @@
### Description ### Description
Simple wrapper to publish Docker images to GitHub Packages and/or Docker Hub Publish a Docker image to GitHub Packages and Docker Hub.
### Inputs ### Inputs
| name | description | required | default | | name | description | required | default |
|----------------------|-------------------------------------------------------------------|----------|---------------------------| |-------------------------|-------------------------------------------------------------------|----------|----------------------------------------|
| `registry` | <p>Registry to publish to (dockerhub, github, or both)</p> | `false` | `both` | | `registry` | <p>Registry to publish to (dockerhub, github, or both).</p> | `true` | `both` |
| `image-name` | <p>Docker image name (defaults to repository name)</p> | `false` | `""` | | `nightly` | <p>Is this a nightly build? (true or false)</p> | `false` | `false` |
| `tags` | <p>Comma-separated list of tags (e.g., latest,v1.0.0)</p> | `false` | `latest` | | `platforms` | <p>Platforms to build for (comma-separated)</p> | `false` | `linux/amd64,linux/arm64,linux/arm/v7` |
| `platforms` | <p>Platforms to build for (comma-separated)</p> | `false` | `linux/amd64,linux/arm64` | | `auto-detect-platforms` | <p>Automatically detect and build for all available platforms</p> | `false` | `false` |
| `context` | <p>Build context path</p> | `false` | `.` | | `scan-image` | <p>Scan images for vulnerabilities</p> | `false` | `true` |
| `dockerfile` | <p>Path to Dockerfile</p> | `false` | `Dockerfile` | | `sign-image` | <p>Sign images with cosign</p> | `false` | `false` |
| `build-args` | <p>Build arguments (newline-separated KEY=VALUE pairs)</p> | `false` | `""` | | `cache-mode` | <p>Cache mode for build layers (min, max, or inline)</p> | `false` | `max` |
| `push` | <p>Whether to push the image</p> | `false` | `true` | | `buildx-version` | <p>Specific Docker Buildx version to use</p> | `false` | `latest` |
| `token` | <p>GitHub token for authentication (for GitHub registry)</p> | `false` | `""` | | `verbose` | <p>Enable verbose logging</p> | `false` | `false` |
| `dockerhub-username` | <p>Docker Hub username (required if publishing to Docker Hub)</p> | `false` | `""` | | `dockerhub-username` | <p>Docker Hub username for authentication</p> | `false` | `""` |
| `dockerhub-token` | <p>Docker Hub token (required if publishing to Docker Hub)</p> | `false` | `""` | | `dockerhub-password` | <p>Docker Hub password or access token for authentication</p> | `false` | `""` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
### Outputs ### Outputs
| name | description | | name | description |
|--------------|--------------------------------------| |-------------------|-------------------------------------------------------|
| `image-name` | <p>Full image name with registry</p> | | `registry` | <p>Registry where image was published</p> |
| `tags` | <p>Tags that were published</p> | | `tags` | <p>Tags that were published</p> |
| `digest` | <p>Image digest</p> | | `build-time` | <p>Total build time in seconds</p> |
| `metadata` | <p>Build metadata</p> | | `platform-matrix` | <p>Build status per platform</p> |
| `scan-results` | <p>Vulnerability scan results if scanning enabled</p> |
| `image-id` | <p>Published image ID</p> |
| `image-digest` | <p>Published image digest</p> |
| `repository` | <p>Repository where image was published</p> |
### Runs ### Runs
@@ -41,67 +46,73 @@ This action is a `composite` action.
- uses: ivuorinen/actions/docker-publish@main - uses: ivuorinen/actions/docker-publish@main
with: with:
registry: registry:
# Registry to publish to (dockerhub, github, or both) # Registry to publish to (dockerhub, github, or both).
# #
# Required: false # Required: true
# Default: both # Default: both
image-name: nightly:
# Docker image name (defaults to repository name) # Is this a nightly build? (true or false)
# #
# Required: false # Required: false
# Default: "" # Default: false
tags:
# Comma-separated list of tags (e.g., latest,v1.0.0)
#
# Required: false
# Default: latest
platforms: platforms:
# Platforms to build for (comma-separated) # Platforms to build for (comma-separated)
# #
# Required: false # Required: false
# Default: linux/amd64,linux/arm64 # Default: linux/amd64,linux/arm64,linux/arm/v7
context: auto-detect-platforms:
# Build context path # Automatically detect and build for all available platforms
# #
# Required: false # Required: false
# Default: . # Default: false
dockerfile: scan-image:
# Path to Dockerfile # Scan images for vulnerabilities
#
# Required: false
# Default: Dockerfile
build-args:
# Build arguments (newline-separated KEY=VALUE pairs)
#
# Required: false
# Default: ""
push:
# Whether to push the image
# #
# Required: false # Required: false
# Default: true # Default: true
token: sign-image:
# GitHub token for authentication (for GitHub registry) # Sign images with cosign
# #
# Required: false # Required: false
# Default: "" # 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
dockerhub-username: dockerhub-username:
# Docker Hub username (required if publishing to Docker Hub) # Docker Hub username for authentication
# #
# Required: false # Required: false
# Default: "" # Default: ""
dockerhub-token: dockerhub-password:
# Docker Hub token (required if publishing to Docker Hub) # Docker Hub password or access token for authentication
#
# Required: false
# Default: ""
token:
# GitHub token for authentication
# #
# Required: false # Required: false
# Default: "" # Default: ""

View File

@@ -2,33 +2,9 @@
# permissions: # permissions:
# - packages: write # Required for publishing to Docker registries # - packages: write # Required for publishing to Docker registries
# - contents: read # Required for checking out repository # - 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@<commit-sha>`
--- ---
name: Docker Publish name: Docker Publish
description: Simple wrapper to publish Docker images to GitHub Packages and/or Docker Hub description: Publish a Docker image to GitHub Packages and Docker Hub.
author: Ismo Vuorinen author: Ismo Vuorinen
branding: branding:
@@ -37,225 +13,309 @@ branding:
inputs: inputs:
registry: registry:
description: 'Registry to publish to (dockerhub, github, or both)' description: 'Registry to publish to (dockerhub, github, or both).'
required: false required: true
default: 'both' default: 'both'
image-name: nightly:
description: 'Docker image name (defaults to repository name)' description: 'Is this a nightly build? (true or false)'
required: false required: false
tags: default: 'false'
description: 'Comma-separated list of tags (e.g., latest,v1.0.0)'
required: false
default: 'latest'
platforms: platforms:
description: 'Platforms to build for (comma-separated)' description: 'Platforms to build for (comma-separated)'
required: false required: false
default: 'linux/amd64,linux/arm64' default: 'linux/amd64,linux/arm64,linux/arm/v7'
context: auto-detect-platforms:
description: 'Build context path' description: 'Automatically detect and build for all available platforms'
required: false required: false
default: '.' default: 'false'
dockerfile: scan-image:
description: 'Path to Dockerfile' description: 'Scan images for vulnerabilities'
required: false
default: 'Dockerfile'
build-args:
description: 'Build arguments (newline-separated KEY=VALUE pairs)'
required: false
push:
description: 'Whether to push the image'
required: false required: false
default: 'true' default: 'true'
sign-image:
description: 'Sign images with cosign'
required: false
default: 'false'
cache-mode:
description: 'Cache mode for build layers (min, max, or inline)'
required: false
default: 'max'
buildx-version:
description: 'Specific Docker Buildx version to use'
required: false
default: 'latest'
verbose:
description: 'Enable verbose logging'
required: false
default: 'false'
dockerhub-username:
description: 'Docker Hub username for authentication'
required: false
dockerhub-password:
description: 'Docker Hub password or access token for authentication'
required: false
token: token:
description: 'GitHub token for authentication (for GitHub registry)' description: 'GitHub token for authentication'
required: false required: false
default: '' default: ''
dockerhub-username:
description: 'Docker Hub username (required if publishing to Docker Hub)'
required: false
dockerhub-token:
description: 'Docker Hub token (required if publishing to Docker Hub)'
required: false
outputs: outputs:
image-name: registry:
description: 'Full image name with registry' description: 'Registry where image was published'
value: ${{ steps.meta.outputs.image-name }} value: ${{ steps.dest.outputs.reg }}
tags: tags:
description: 'Tags that were published' description: 'Tags that were published'
value: ${{ steps.meta.outputs.tags }} value: ${{ steps.tags.outputs.all-tags }}
digest: build-time:
description: 'Image digest' description: 'Total build time in seconds'
value: ${{ steps.build.outputs.digest }} value: ${{ steps.build.outputs.build-time }}
metadata: platform-matrix:
description: 'Build metadata' description: 'Build status per platform'
value: ${{ steps.build.outputs.metadata }} value: ${{ steps.build.outputs.platform-matrix }}
scan-results:
description: 'Vulnerability scan results if scanning enabled'
value: ${{ steps.build.outputs.scan-results }}
image-id:
description: 'Published image ID'
value: ${{ steps.publish-dockerhub.outputs.image-id || steps.publish-github.outputs.image-id }}
image-digest:
description: 'Published image digest'
value: ${{ steps.publish-dockerhub.outputs.digest || steps.publish-github.outputs.digest }}
repository:
description: 'Repository where image was published'
value: ${{ steps.publish-dockerhub.outputs.repository || steps.publish-github.outputs.repository }}
runs: runs:
using: composite using: composite
steps: steps:
- name: Mask Sensitive Inputs
shell: bash
env:
DOCKERHUB_PASSWORD: ${{ inputs.dockerhub-password }}
run: |
set -euo pipefail
# Mask Docker Hub credentials to prevent exposure in logs
if [[ -n "${DOCKERHUB_PASSWORD}" ]]; then
echo "::add-mask::${DOCKERHUB_PASSWORD}"
fi
- name: Validate Inputs - name: Validate Inputs
id: validate id: validate
shell: sh shell: bash
env: env:
INPUT_REGISTRY: ${{ inputs.registry }} REGISTRY: ${{ inputs.registry }}
INPUT_DOCKERHUB_USERNAME: ${{ inputs.dockerhub-username }}
INPUT_DOCKERHUB_TOKEN: ${{ inputs.dockerhub-token }}
INPUT_TOKEN: ${{ inputs.token }}
INPUT_CONTEXT: ${{ inputs.context }}
INPUT_DOCKERFILE: ${{ inputs.dockerfile }}
run: | run: |
set -eu set -euo pipefail
# Validate registry input # Validate registry input
case "$INPUT_REGISTRY" in if ! [[ "$REGISTRY" =~ ^(dockerhub|github|both)$ ]]; then
dockerhub|github|both) echo "::error::Invalid registry value. Must be 'dockerhub', 'github', or 'both'"
;; exit 1
*)
echo "::error::Invalid registry value. Must be 'dockerhub', 'github', or 'both'"
exit 1
;;
esac
# Validate Docker Hub credentials if needed
if [ "$INPUT_REGISTRY" = "dockerhub" ] || [ "$INPUT_REGISTRY" = "both" ]; then
if [ -z "$INPUT_DOCKERHUB_USERNAME" ] || [ -z "$INPUT_DOCKERHUB_TOKEN" ]; then
echo "::error::Docker Hub username and token are required when publishing to Docker Hub"
exit 1
fi
fi fi
# Validate GitHub token if needed - name: Determine Tags
if [ "$INPUT_REGISTRY" = "github" ] || [ "$INPUT_REGISTRY" = "both" ]; then id: tags
token="${INPUT_TOKEN:-${GITHUB_TOKEN:-}}" shell: bash
if [ -z "$token" ]; then
echo "::error::GitHub token is required when publishing to GitHub Packages"
exit 1
fi
fi
# Validate context input for security
INPUT_CONTEXT="${INPUT_CONTEXT:-.}"
case "$INPUT_CONTEXT" in
.|./*|*/*)
# Relative paths are allowed
;;
/*)
echo "::error::Context cannot be an absolute path: '$INPUT_CONTEXT'"
echo "::error::Use relative paths (e.g., '.', './app') to prevent code injection"
exit 1
;;
*://*)
echo "::warning::Context is a remote URL: '$INPUT_CONTEXT'"
echo "::warning::Ensure this URL is from a trusted source to prevent code injection"
;;
esac
# Validate dockerfile input for security
INPUT_DOCKERFILE="${INPUT_DOCKERFILE:-Dockerfile}"
case "$INPUT_DOCKERFILE" in
Dockerfile|*/Dockerfile|*.dockerfile|*/*.dockerfile)
# Common dockerfile patterns are allowed
;;
/*)
echo "::error::Dockerfile path cannot be absolute: '$INPUT_DOCKERFILE'"
echo "::error::Use relative paths (e.g., 'Dockerfile', './docker/Dockerfile')"
exit 1
;;
*://*)
echo "::error::Dockerfile path cannot be a URL: '$INPUT_DOCKERFILE'"
exit 1
;;
esac
echo "Input validation completed successfully"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
- name: Determine Image Names and Tags
id: meta
shell: sh
env: env:
INPUT_REGISTRY: ${{ inputs.registry }} NIGHTLY: ${{ inputs.nightly }}
INPUT_IMAGE_NAME: ${{ inputs.image-name }} RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_TAGS: ${{ inputs.tags }} run: |
set -euo pipefail
# Initialize variables
declare -a tag_array
if [[ "$NIGHTLY" == "true" ]]; then
# Nightly build tags
current_date=$(date +'%Y%m%d-%H%M')
tag_array+=("nightly")
tag_array+=("nightly-${current_date}")
else
# Release tags
if [[ -n "$RELEASE_TAG" ]]; then
tag_array+=("$RELEASE_TAG")
tag_array+=("latest")
else
echo "::error::No release tag found and not a nightly build"
exit 1
fi
fi
# Join tags with comma
tags=$(IFS=,; echo "${tag_array[*]}")
echo "all-tags=${tags}" >> "$GITHUB_OUTPUT"
echo "Generated tags: ${tags}"
- name: Determine Publish Destination
id: dest
shell: bash
env:
REGISTRY: ${{ inputs.registry }}
run: |
set -euo pipefail
if [[ "$REGISTRY" == "both" ]]; then
echo "reg=github,dockerhub" >> "$GITHUB_OUTPUT"
else
echo "reg=$REGISTRY" >> "$GITHUB_OUTPUT"
fi
echo "Publishing to: $REGISTRY"
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token || github.token }}
- name: Build Multi-Arch Docker Image
id: build
uses: ivuorinen/actions/docker-build@e2222afff180ee77f330ef4325f60d6e85477c01
with:
tag: ${{ steps.tags.outputs.all-tags }}
architectures: ${{ inputs.platforms }}
auto-detect-platforms: ${{ inputs.auto-detect-platforms }}
scan-image: ${{ inputs.scan-image }}
sign-image: ${{ inputs.sign-image }}
cache-mode: ${{ inputs.cache-mode }}
buildx-version: ${{ inputs.buildx-version }}
verbose: ${{ inputs.verbose }}
push: 'false' # Don't push during build, let publish actions handle it
- name: Publish to Docker Hub
id: publish-dockerhub
if: contains(steps.dest.outputs.reg, 'dockerhub')
uses: ivuorinen/actions/docker-publish-hub@e2222afff180ee77f330ef4325f60d6e85477c01
with:
tags: ${{ steps.tags.outputs.all-tags }}
platforms: ${{ inputs.platforms }}
auto-detect-platforms: ${{ inputs.auto-detect-platforms }}
scan-image: ${{ inputs.scan-image }}
sign-image: ${{ inputs.sign-image }}
cache-mode: ${{ inputs.cache-mode }}
buildx-version: ${{ inputs.buildx-version }}
verbose: ${{ inputs.verbose }}
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-password }}
- name: Publish to GitHub Packages
id: publish-github
if: contains(steps.dest.outputs.reg, 'github')
uses: ivuorinen/actions/docker-publish-gh@e2222afff180ee77f330ef4325f60d6e85477c01
with:
tags: ${{ steps.tags.outputs.all-tags }}
platforms: ${{ inputs.platforms }}
auto-detect-platforms: ${{ inputs.auto-detect-platforms }}
scan-image: ${{ inputs.scan-image }}
sign-image: ${{ inputs.sign-image }}
cache-mode: ${{ inputs.cache-mode }}
buildx-version: ${{ inputs.buildx-version }}
verbose: ${{ inputs.verbose }}
- name: Verify Publications
id: verify
shell: bash
env:
DEST_REG: ${{ steps.dest.outputs.reg }}
DOCKERHUB_IMAGE_NAME: ${{ steps.publish-dockerhub.outputs.image-name }}
DOCKERHUB_TAGS: ${{ steps.publish-dockerhub.outputs.tags }}
GITHUB_IMAGE_NAME: ${{ steps.publish-github.outputs.image-name }}
GITHUB_TAGS: ${{ steps.publish-github.outputs.tags }}
ALL_TAGS: ${{ steps.tags.outputs.all-tags }}
GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_REPOSITORY: ${{ github.repository }}
run: | run: |
set -eu set -euo pipefail
# Determine base image name echo "Verifying publications..."
if [ -n "$INPUT_IMAGE_NAME" ]; then success=true
base_name="$INPUT_IMAGE_NAME"
else
# Use repository name (lowercase)
base_name=$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]')
fi
# Build full image names based on registry # Split registry string into array
image_names="" IFS=',' read -ra REGISTRIES <<< "$DEST_REG"
case "$INPUT_REGISTRY" in
dockerhub)
image_names="docker.io/${base_name}"
;;
github)
image_names="ghcr.io/${base_name}"
;;
both)
image_names="docker.io/${base_name},ghcr.io/${base_name}"
;;
esac
# Build full tags (image:tag format) for registry in "${REGISTRIES[@]}"; do
tags="" echo "Checking ${registry} publication..."
IFS=',' case "${registry}" in
for image in $image_names; do "dockerhub")
for tag in $INPUT_TAGS; do # Get actual image name from publish step output or fallback to repo-based name
tag=$(echo "$tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') image_name="$DOCKERHUB_IMAGE_NAME"
if [ -n "$tags" ]; then if [[ -z "$image_name" ]]; then
tags="$(printf '%s\n%s' "$tags" "${image}:${tag}")" image_name="docker.io/$GITHUB_REPOSITORY"
else fi
tags="${image}:${tag}"
fi # Get tags from publish step or fallback to metadata
done tags="$DOCKERHUB_TAGS"
if [[ -z "$tags" ]]; then
tags="$ALL_TAGS"
fi
IFS=',' read -ra TAGS <<< "$tags"
for tag in "${TAGS[@]}"; do
tag=$(echo "$tag" | xargs) # trim whitespace
if ! docker manifest inspect "${image_name}:${tag}" > /dev/null 2>&1; then
echo "::error::Failed to verify Docker Hub publication for ${tag}"
success=false
break
fi
done
if [[ "${success}" != "true" ]]; then
break
fi
;;
"github")
# Get actual image name from publish step output or fallback to repo-based name
image_name="$GITHUB_IMAGE_NAME"
if [[ -z "$image_name" ]]; then
image_name="ghcr.io/$GITHUB_REPOSITORY"
fi
# Get tags from publish step or fallback to metadata
tags="$GITHUB_TAGS"
if [[ -z "$tags" ]]; then
tags="$ALL_TAGS"
fi
IFS=',' read -ra TAGS <<< "$tags"
for tag in "${TAGS[@]}"; do
tag=$(echo "$tag" | xargs) # trim whitespace
if ! docker manifest inspect "${image_name}:${tag}" > /dev/null 2>&1; then
echo "::error::Failed to verify GitHub Packages publication for ${tag}"
success=false
break
fi
done
if [[ "${success}" != "true" ]]; then
break
fi
;;
esac
done done
# Output results if [[ "${success}" != "true" ]]; then
printf 'image-name=%s\n' "$base_name" >> "$GITHUB_OUTPUT" echo "::error::Publication verification failed"
{ exit 1
echo 'tags<<EOF' fi
echo "$tags"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
echo "Image name: $base_name" echo "All publications verified successfully"
echo "Tags:"
echo "$tags"
- name: Login to Docker Hub - name: Cleanup
if: inputs.registry == 'dockerhub' || inputs.registry == 'both' if: always()
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 shell: bash
with: env:
username: ${{ inputs.dockerhub-username }} DEST_REG: ${{ steps.dest.outputs.reg }}
password: ${{ inputs.dockerhub-token }} run: |-
set -euo pipefail
- name: Login to GitHub Container Registry echo "Cleaning up..."
if: inputs.registry == 'github' || inputs.registry == 'both'
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ inputs.token || github.token }}
- name: Build and Push Docker Image # Remove any temporary files or caches
id: build docker buildx prune -f --keep-storage=10GB
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
with: # Remove any temporary authentication
context: ${{ inputs.context }} if [[ "$DEST_REG" =~ "dockerhub" ]]; then
file: ${{ inputs.dockerfile }} docker logout docker.io || true
platforms: ${{ inputs.platforms }} fi
push: ${{ inputs.push }} if [[ "$DEST_REG" =~ "github" ]]; then
tags: ${{ steps.meta.outputs.tags }} docker logout ghcr.io || true
build-args: ${{ inputs.build-args }} fi
cache-from: type=gha
cache-to: type=gha,mode=max echo "Cleanup completed"

View File

@@ -2,7 +2,7 @@
# Validation rules for docker-publish action # Validation rules for docker-publish action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY # Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0 # Schema version: 1.0
# Coverage: 73% (8/11 inputs) # Coverage: 100% (12/12 inputs)
# #
# This file defines validation rules for the docker-publish GitHub Action. # This file defines validation rules for the docker-publish GitHub Action.
# Rules are automatically applied by validate-inputs action when this # Rules are automatically applied by validate-inputs action when this
@@ -11,44 +11,50 @@
schema_version: '1.0' schema_version: '1.0'
action: docker-publish action: docker-publish
description: Simple wrapper to publish Docker images to GitHub Packages and/or Docker Hub description: Publish a Docker image to GitHub Packages and Docker Hub.
generator_version: 1.0.0 generator_version: 1.0.0
required_inputs: [] required_inputs:
optional_inputs:
- build-args
- context
- dockerfile
- dockerhub-token
- dockerhub-username
- image-name
- platforms
- push
- registry - registry
- tags optional_inputs:
- auto-detect-platforms
- buildx-version
- cache-mode
- dockerhub-password
- dockerhub-username
- nightly
- platforms
- scan-image
- sign-image
- token - token
- verbose
conventions: conventions:
dockerfile: file_path auto-detect-platforms: docker_architectures
dockerhub-token: github_token buildx-version: semantic_version
cache-mode: boolean
dockerhub-password: github_token
dockerhub-username: username dockerhub-username: username
image-name: docker_image_name nightly: boolean
platforms: docker_architectures platforms: docker_architectures
registry: registry registry: registry
tags: docker_tag scan-image: boolean
sign-image: boolean
token: github_token token: github_token
verbose: boolean
overrides: overrides:
cache-mode: cache_mode
platforms: null platforms: null
registry: registry_enum registry: registry_enum
statistics: statistics:
total_inputs: 11 total_inputs: 12
validated_inputs: 8 validated_inputs: 12
skipped_inputs: 1 skipped_inputs: 1
coverage_percentage: 73 coverage_percentage: 100
validation_coverage: 73 validation_coverage: 100
auto_detected: true auto_detected: true
manual_review_required: true manual_review_required: false
quality_indicators: quality_indicators:
has_required_inputs: false has_required_inputs: true
has_token_validation: true has_token_validation: true
has_version_validation: false has_version_validation: true
has_file_validation: true has_file_validation: false
has_security_validation: true has_security_validation: true

View File

@@ -0,0 +1,42 @@
# ivuorinen/actions/dotnet-version-detect
## Dotnet Version Detect
### Description
Detects .NET SDK version from global.json or defaults to a specified version.
### Inputs
| name | description | required | default |
|-------------------|---------------------------------------------------------------------|----------|---------|
| `default-version` | <p>Default .NET SDK version to use if global.json is not found.</p> | `true` | `7.0` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
### Outputs
| name | description |
|------------------|----------------------------------------------|
| `dotnet-version` | <p>Detected or default .NET SDK version.</p> |
### 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: ""
```

View File

@@ -0,0 +1,67 @@
# 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token || github.token }}
- name: Parse .NET Version
id: parse-version
uses: ivuorinen/actions/version-file-parser@e2222afff180ee77f330ef4325f60d6e85477c01
with:
language: 'dotnet'
tool-versions-key: 'dotnet'
dockerfile-image: 'dotnet'
validation-regex: '^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$'
default-version: ${{ inputs.default-version }}

View File

@@ -0,0 +1,38 @@
---
# 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

256
eslint-check/CustomValidator.py Executable file
View File

@@ -0,0 +1,256 @@
#!/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",
},
}

108
eslint-check/README.md Normal file
View File

@@ -0,0 +1,108 @@
# 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` | <p>Directory containing files to lint</p> | `false` | `.` |
| `eslint-version` | <p>ESLint version to use</p> | `false` | `latest` |
| `config-file` | <p>Path to ESLint config file</p> | `false` | `.eslintrc` |
| `ignore-file` | <p>Path to ESLint ignore file</p> | `false` | `.eslintignore` |
| `file-extensions` | <p>File extensions to lint (comma-separated)</p> | `false` | `.js,.jsx,.ts,.tsx` |
| `cache` | <p>Enable ESLint caching</p> | `false` | `true` |
| `max-warnings` | <p>Maximum number of warnings allowed</p> | `false` | `0` |
| `fail-on-error` | <p>Fail workflow if issues are found</p> | `false` | `true` |
| `report-format` | <p>Output format (stylish, json, sarif)</p> | `false` | `sarif` |
| `max-retries` | <p>Maximum number of retry attempts</p> | `false` | `3` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
### Outputs
| name | description |
|-----------------|----------------------------------|
| `error-count` | <p>Number of errors found</p> |
| `warning-count` | <p>Number of warnings found</p> |
| `sarif-file` | <p>Path to SARIF report file</p> |
| `files-checked` | <p>Number of files checked</p> |
### 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: ""
```

Some files were not shown because too many files have changed in this diff Show More