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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- 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
on:
workflow_dispatch:
@@ -17,7 +16,7 @@ jobs:
pull-requests: read
steps:
- name: Get dates for last month
shell: sh
shell: bash
run: |
# Calculate the first day of the previous month
first_day=$(date -d "last month" +%Y-%m-01)
@@ -30,7 +29,7 @@ jobs:
echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV"
- name: Run issue-metrics tool
uses: github/issue-metrics@637a24e71b78bc10881e61972b19ea9ff736e14a # v3.25.2
uses: github/issue-metrics@c640329f02bd24b12b91d51cd385f0b1c25cefb9 # v3.25.1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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 }}
steps:
- uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Create tag if necessary
uses: fregante/daily-version-action@fb1a60b7c4daf1410cd755e360ebec3901e58588 # v2.1.3

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -49,7 +49,7 @@ jobs:
- name: Create Pull Request
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:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: update action references to ${{ steps.version.outputs.major }}'
@@ -78,7 +78,7 @@ jobs:
- name: Check for Annual Bump
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:
script: |
const currentYear = new Date().getFullYear();

2
.gitignore vendored
View File

@@ -13,7 +13,6 @@
.cache
.cache/
.coverage
.worktrees/
.coverage.*
.docusaurus
.dynamodb/
@@ -84,4 +83,3 @@ tests/reports/**/*.json
!uv.lock
code-scanning-report-*
*.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
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]
files: ^(docs/.*|README\.md|CONTRIBUTING\.md|CHANGELOG\.md|.*\.py|.*\.ya?ml)$
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.9.8
rev: 0.9.5
hooks:
- id: uv-lock
- id: uv-sync
@@ -44,7 +44,7 @@ repos:
args: [--autofix, --no-sort-keys]
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.19.0
rev: v0.18.1
hooks:
- id: markdownlint-cli2
args: [--fix]
@@ -55,7 +55,7 @@ repos:
- id: yamllint
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.5
rev: v0.14.2
hooks:
# Run the linter with auto-fix
- id: ruff-check
@@ -84,18 +84,18 @@ repos:
args: ['-shellcheck=']
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 42.6.2
rev: 41.149.2
hooks:
- id: renovate-config-validator
- repo: https://github.com/bridgecrewio/checkov.git
rev: '3.2.489'
rev: '3.2.487'
hooks:
- id: checkov
args:
- '--quiet'
- repo: https://github.com/gitleaks/gitleaks
rev: v8.29.0
rev: v8.28.0
hooks:
- id: gitleaks

View File

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

View File

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

View File

@@ -4,6 +4,10 @@
# * For JavaScript, use typescript
# Special requirements:
# * 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
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
@@ -62,8 +66,3 @@ excluded_tools: []
initial_prompt: ''
project_name: 'actions'
languages:
- bash
- python
included_optional_tools: []
encoding: utf-8

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# Makefile for GitHub Actions repository
# 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
# Colors for output
@@ -43,7 +43,7 @@ help: ## Show this help message
@echo " make check # Quick syntax checks"
# 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)"
docs: ## Generate documentation for all actions
@@ -66,16 +66,6 @@ docs: ## Generate documentation for all actions
done; \
[ $$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
@echo "$(BLUE)🔧 Updating validation rules...$(RESET)"
@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])
@VERSION_TO_USE=$$(if [ -n "$(VERSION)" ]; then echo "$(VERSION)"; else date +v%Y.%m.%d; fi); \
echo "$(BLUE)🚀 Creating release $$VERSION_TO_USE...$(RESET)"; \
sh _tools/release.sh "$$VERSION_TO_USE"
release-dry: ## Preview release without making changes (usage: make release-dry VERSION=v2025.11.01)
@if [ -z "$(VERSION)" ]; then \
VERSION_TO_USE=$$(date +v%Y.%m.%d); \
else \
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
sh _tools/release.sh "$$VERSION_TO_USE"; \
echo "$(GREEN)✅ Release created$(RESET)"; \
echo ""; \
echo "$(YELLOW)Next steps:$(RESET)"; \
echo " 1. Review changes: git show HEAD"; \
echo " 2. Push tags: git push origin main --tags --force-with-lease"
update-version-refs: ## Update all action references to a specific version tag (usage: make update-version-refs MAJOR=v2025)
@if [ -z "$(MAJOR)" ]; then \
@@ -217,7 +183,7 @@ check-version-refs: ## List all current SHA-pinned action references
# Formatting targets
format-markdown: ## Format markdown files
@echo "$(BLUE)📝 Formatting markdown...$(RESET)"
@if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" "#.worktrees" 2>/dev/null; then \
@if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" 2>/dev/null; then \
echo "$(GREEN)✅ Markdown formatted$(RESET)"; \
else \
echo "$(YELLOW)⚠️ Markdown formatting issues found$(RESET)" | tee -a $(LOG_FILE); \
@@ -269,7 +235,7 @@ format-python: ## Format Python files with ruff
# Linting targets
lint-markdown: ## Lint markdown files
@echo "$(BLUE)🔍 Linting markdown...$(RESET)"
@if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules" "#.worktrees"; then \
@if npx --yes markdownlint-cli2 --fix "**/*.md" "#node_modules"; then \
echo "$(GREEN)✅ Markdown linting passed$(RESET)"; \
else \
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"; \
exit 1; \
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)"; \
else \
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
@echo "$(BLUE)🔍 Checking syntax...$(RESET)"
@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 \
echo "$(RED)❌ Syntax error in $$file$(RESET)" >&2; \
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)
@if command -v entr >/dev/null 2>&1; then \
echo "$(BLUE)👀 Watching for changes... (press Ctrl+C to stop)$(RESET)"; \
find . \( -name "*.yml" -o -name "*.yaml" -o -name "*.md" -o -name "*.sh" \) \
-not -path "./_tests/*" -not -path "./.worktrees/*" -not -path "./node_modules/*" | \
find . -name "*.yml" -o -name "*.yaml" -o -name "*.md" -o -name "*.sh" | \
entr -c $(MAKE) format; \
else \
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
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 |
|:----:|:-----------------------------------------------------|:-----------|:----------------------------------------------------------------|:---------------------------------------------|
| 🔀 | [`action-versioning`][action-versioning] | Utilities | Automatically update SHA-pinned action references to match l... | Token auth, Outputs |
| 📦 | [`ansible-lint-fix`][ansible-lint-fix] | Linting | Lints and fixes Ansible playbooks, commits changes, and uplo... | Token auth, Outputs |
| ✅ | [`biome-lint`][biome-lint] | Linting | Run Biome linter in check or fix mode | Token auth, Outputs |
| 🛡️ | [`codeql-analysis`][codeql-analysis] | Repository | Run CodeQL security analysis for a single language with conf... | Auto-detection, Token auth, Outputs |
| 💾 | [`common-cache`][common-cache] | Repository | Standardized caching strategy for all actions | Caching, Outputs |
| 🖼️ | [`compress-images`][compress-images] | Repository | Compress images on demand (workflow_dispatch), and at 11pm e... | Token auth, Outputs |
| 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Auto-detection, Token auth, Outputs |
| 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Auto-detection, Token auth, Outputs |
| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Auto-detection, Token auth, Outputs |
| 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs |
| ☁️ | [`docker-publish`][docker-publish] | Publishing | Simple wrapper to publish Docker images to GitHub Packages a... | Token auth, Outputs |
| | [`eslint-lint`][eslint-lint] | Linting | Run ESLint in check or fix mode with advanced configuration ... | Caching, Token auth, Outputs |
| 📦 | [`go-build`][go-build] | Build | Builds the Go project. | Caching, Auto-detection, Token auth, Outputs |
| 📝 | [`go-lint`][go-lint] | Linting | Run golangci-lint with advanced configuration, caching, and ... | Caching, Token auth, Outputs |
| 📝 | [`language-version-detect`][language-version-detect] | Setup | Detects language version from project configuration files wi... | Auto-detection, Token auth, Outputs |
| 🖥️ | [`node-setup`][node-setup] | Setup | Sets up Node.js environment with version detection and packa... | Auto-detection, Token auth, Outputs |
| 📦 | [`npm-publish`][npm-publish] | Publishing | Publishes the package to the NPM registry with configurable ... | Token auth, Outputs |
| 🖥️ | [`php-composer`][php-composer] | Testing | Runs Composer install on a repository with advanced caching ... | Auto-detection, Token auth, Outputs |
| 💻 | [`php-laravel-phpunit`][php-laravel-phpunit] | Testing | Setup PHP, install dependencies, generate key, create databa... | Auto-detection, Token auth, Outputs |
| | [`php-tests`][php-tests] | Testing | Run PHPUnit tests on the repository | Token auth, Outputs |
| | [`pr-lint`][pr-lint] | Linting | Runs MegaLinter against pull requests | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`pre-commit`][pre-commit] | Linting | Runs pre-commit on the repository and pushes the fixes back ... | Auto-detection, Token auth, Outputs |
| | [`prettier-lint`][prettier-lint] | Linting | Run Prettier in check or fix mode with advanced configuratio... | Caching, Token auth, Outputs |
| 📝 | [`python-lint-fix`][python-lint-fix] | Linting | Lints and fixes Python files, commits changes, and uploads S... | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs |
| 📦 | [`stale`][stale] | Repository | A GitHub Action to close stale issues and pull requests. | Token auth, Outputs |
| 🏷️ | [`sync-labels`][sync-labels] | Repository | Sync labels from a YAML file to a GitHub repository | Token auth, Outputs |
| 🖥️ | [`terraform-lint-fix`][terraform-lint-fix] | Linting | Lints and fixes Terraform files with advanced validation and... | Token auth, Outputs |
| 🛡️ | [`validate-inputs`][validate-inputs] | Validation | Centralized Python-based input validation for GitHub Actions... | Token auth, Outputs |
| 📦 | [`version-file-parser`][version-file-parser] | Utilities | Universal parser for common version detection files (.tool-v... | Auto-detection, Outputs |
| Icon | Action | Category | Description | Key Features |
|:----:|:-------------------------------------------------------|:-----------|:----------------------------------------------------------------|:---------------------------------------------|
| 📦 | [`ansible-lint-fix`][ansible-lint-fix] | Linting | Lints and fixes Ansible playbooks, commits changes, and uplo... | Token auth, Outputs |
| | [`biome-check`][biome-check] | Linting | Run Biome check on the repository | Token auth, Outputs |
| ✅ | [`biome-fix`][biome-fix] | Linting | Run Biome fix on the repository | Token auth, Outputs |
| 🛡️ | [`codeql-analysis`][codeql-analysis] | 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-file-check`][common-file-check] | Repository | A reusable action to check if a specific file or type of fil... | Outputs |
| 🔄 | [`common-retry`][common-retry] | Repository | Standardized retry utility for network operations and flaky ... | Outputs |
| 🖼️ | [`compress-images`][compress-images] | Repository | Compress images on demand (workflow_dispatch), and at 11pm e... | Token auth, Outputs |
| 📝 | [`csharp-build`][csharp-build] | Build | Builds and tests C# projects. | Auto-detection, Outputs |
| 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Auto-detection, Outputs |
| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Auto-detection, Token auth, Outputs |
| 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs |
| ☁️ | [`docker-publish`][docker-publish] | Publishing | Publish a Docker image to GitHub Packages and Docker Hub. | Auto-detection, Outputs |
| 📦 | [`docker-publish-gh`][docker-publish-gh] | Publishing | Publishes a Docker image to GitHub Packages with advanced se... | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`docker-publish-hub`][docker-publish-hub] | Publishing | Publishes a Docker image to Docker Hub with enhanced securit... | Caching, Auto-detection, Outputs |
| 📝 | [`dotnet-version-detect`][dotnet-version-detect] | Setup | Detects .NET SDK version from global.json or defaults to a s... | Auto-detection, Outputs |
| | [`eslint-check`][eslint-check] | Linting | Run ESLint check on the repository with advanced configurati... | Caching, Outputs |
| 📝 | [`eslint-fix`][eslint-fix] | Linting | Fixes ESLint violations in a project. | Token auth, Outputs |
| 🏷️ | [`github-release`][github-release] | Repository | Creates a GitHub release with a version and changelog. | Outputs |
| 📦 | [`go-build`][go-build] | Build | Builds the Go project. | Caching, Auto-detection, Outputs |
| 📝 | [`go-lint`][go-lint] | Linting | Run golangci-lint with advanced configuration, caching, and ... | Caching, Outputs |
| 📝 | [`go-version-detect`][go-version-detect] | Setup | Detects the Go version from the project's go.mod file or def... | Auto-detection, Outputs |
| 🖥️ | [`node-setup`][node-setup] | Setup | Sets up Node.js env with advanced version management, cachin... | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`npm-publish`][npm-publish] | Publishing | Publishes the package to the NPM registry with configurable ... | Outputs |
| 🖥️ | [`php-composer`][php-composer] | Testing | Runs Composer install on a repository with advanced caching ... | Auto-detection, Token auth, Outputs |
| 💻 | [`php-laravel-phpunit`][php-laravel-phpunit] | Testing | Setup PHP, install dependencies, generate key, create databa... | Auto-detection, Token auth, Outputs |
| | [`php-tests`][php-tests] | Testing | Run PHPUnit tests on the repository | Token auth, Outputs |
| 📝 | [`php-version-detect`][php-version-detect] | Setup | Detects the PHP version from the project's composer.json, ph... | Auto-detection, Outputs |
| | [`pr-lint`][pr-lint] | Linting | Runs MegaLinter against pull requests | Caching, Auto-detection, Token auth, Outputs |
| 📦 | [`pre-commit`][pre-commit] | Linting | Runs pre-commit on the repository and pushes the fixes back ... | Auto-detection, Token auth, Outputs |
| ✅ | [`prettier-check`][prettier-check] | Linting | Run Prettier check on the repository with advanced configura... | Caching, 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
#### 🔧 Setup (2 actions)
#### 🔧 Setup (7 actions)
| Action | Description | Languages | Features |
|:--------------------------------------------------------|:------------------------------------------------------|:--------------------------------|:------------------------------------|
| 📝 [`language-version-detect`][language-version-detect] | Detects language version from project configuratio... | PHP, Python, Go, .NET, Node.js | Auto-detection, Token auth, Outputs |
| 🖥️ [`node-setup`][node-setup] | Sets up Node.js environment with version detection... | Node.js, JavaScript, TypeScript | Auto-detection, Token auth, Outputs |
| Action | Description | Languages | Features |
|:----------------------------------------------------------|:------------------------------------------------------|:--------------------------------|:---------------------------------------------|
| 📝 [`dotnet-version-detect`][dotnet-version-detect] | Detects .NET SDK version from global.json or defau... | C#, .NET | Auto-detection, 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)
| Action | Description | Languages | Features |
|:------------------------------------------------|:------------------------------------------------------|:-------------------|:------------------------|
| 🔀 [`action-versioning`][action-versioning] | Automatically update SHA-pinned action references ... | GitHub Actions | Token auth, Outputs |
| 📦 [`version-file-parser`][version-file-parser] | Universal parser for common version detection file... | Multiple Languages | Auto-detection, Outputs |
| Action | Description | Languages | Features |
|:------------------------------------------------|:------------------------------------------------------|:----------|:------------------------|
| 📦 [`version-file-parser`][version-file-parser] | Universal parser for common version detection file... | - | 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 |
|:-----------------------------------------------|:------------------------------------------------------|:---------------------------------------------|:---------------------------------------------|
| 📦 [`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 |
| 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Auto-detection, Token auth, Outputs |
| [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, Token auth, Outputs |
| 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, Token auth, Outputs |
| [`pr-lint`][pr-lint] | Runs MegaLinter against pull requests | Conventional Commits | Caching, Auto-detection, Token auth, Outputs |
| 📦 [`pre-commit`][pre-commit] | Runs pre-commit on the repository and pushes the f... | Python, Multiple Languages | Auto-detection, Token auth, Outputs |
| ✅ [`prettier-lint`][prettier-lint] | Run Prettier in check or fix mode with advanced co... | JavaScript, TypeScript, Markdown, YAML, JSON | Caching, Token auth, Outputs |
| ✅ [`biome-check`][biome-check] | Run Biome check on the repository | JavaScript, TypeScript, JSON | Token auth, Outputs |
| [`biome-fix`][biome-fix] | Run Biome fix on the repository | JavaScript, TypeScript, JSON | Token auth, Outputs |
| 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Auto-detection, Outputs |
| [`eslint-check`][eslint-check] | Run ESLint check on the repository with advanced c... | JavaScript, TypeScript | Caching, Outputs |
| 📝 [`eslint-fix`][eslint-fix] | Fixes ESLint violations in a project. | JavaScript, TypeScript | Token auth, Outputs |
| 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, 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 |
| 🖥️ [`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 |
|:----------------------------------|:------------------------------------------------------|:----------|:---------------------------------------------|
| 📝 [`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 |
| 📦 [`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 |
|:--------------------------------------|:------------------------------------------------------|:-------------|:------------------------------------|
| 📦 [`csharp-publish`][csharp-publish] | Publishes a C# project to GitHub Packages. | C#, .NET | Auto-detection, Token auth, Outputs |
| ☁️ [`docker-publish`][docker-publish] | Simple wrapper to publish Docker images to GitHub ... | Docker | Token auth, Outputs |
| 📦 [`npm-publish`][npm-publish] | Publishes the package to the NPM registry with con... | Node.js, npm | Token auth, Outputs |
| Action | Description | Languages | Features |
|:----------------------------------------------|:------------------------------------------------------|:-------------|:---------------------------------------------|
| 📦 [`csharp-publish`][csharp-publish] | Publishes a C# project to GitHub Packages. | C#, .NET | Auto-detection, Token auth, Outputs |
| ☁️ [`docker-publish`][docker-publish] | Publish a Docker image to GitHub Packages and Dock... | Docker | Auto-detection, 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 |
|:-----------------------------------------|:------------------------------------------------------|:--------------------------------------------------------|:------------------------------------|
| 🛡️ [`codeql-analysis`][codeql-analysis] | Run CodeQL security analysis for a single language... | JavaScript, TypeScript, Python, Java, C#, C++, Go, Ruby | Auto-detection, Token auth, Outputs |
| 💾 [`common-cache`][common-cache] | Standardized caching strategy for all actions | Caching | Caching, Outputs |
| 🖼️ [`compress-images`][compress-images] | Compress images on demand (workflow_dispatch), and... | Images, PNG, JPEG | Token auth, Outputs |
| 📦 [`release-monthly`][release-monthly] | Creates a release for the current month, increment... | GitHub Actions | Token auth, Outputs |
| 📦 [`stale`][stale] | A GitHub Action to close stale issues and pull req... | GitHub Actions | Token auth, Outputs |
| 🏷️ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | YAML, GitHub | Token auth, Outputs |
#### ✅ Validation (1 action)
| Action | Description | Languages | Features |
|:-----------------------------------------|:------------------------------------------------------|:---------------------|:--------------------|
| 🛡️ [`validate-inputs`][validate-inputs] | Centralized Python-based input validation for GitH... | YAML, GitHub Actions | Token auth, Outputs |
| Action | Description | Languages | Features |
|:--------------------------------------------|:------------------------------------------------------|:----------|:--------------------|
| 💾 [`common-cache`][common-cache] | Standardized caching strategy for all actions | - | Caching, Outputs |
| 📦 [`common-file-check`][common-file-check] | A reusable action to check if a specific file or t... | - | Outputs |
| 🔄 [`common-retry`][common-retry] | Standardized retry utility for network operations ... | - | Outputs |
| 🖼️ [`compress-images`][compress-images] | Compress images on demand (workflow_dispatch), and... | - | Token auth, Outputs |
| 🏷️ [`github-release`][github-release] | Creates a GitHub release with a version and change... | - | Outputs |
| 📦 [`release-monthly`][release-monthly] | Creates a release for the current month, increment... | - | Token auth, Outputs |
| 📦 [`stale`][stale] | A GitHub Action to close stale issues and pull req... | - | Token auth, Outputs |
| 🏷️ [`sync-labels`][sync-labels] | Sync labels from a YAML file to a GitHub repositor... | - | Token auth, Outputs |
### Feature Matrix
| Action | Caching | Auto-detection | Token auth | Outputs |
|:-----------------------------------------------------|:-------:|:--------------:|:----------:|:-------:|
| [`action-versioning`][action-versioning] | - | - | ✅ | ✅ |
| [`ansible-lint-fix`][ansible-lint-fix] | - | - | ✅ | ✅ |
| [`biome-lint`][biome-lint] | - | - | ✅ | ✅ |
| [`codeql-analysis`][codeql-analysis] | - | ✅ | ✅ | ✅ |
| [`common-cache`][common-cache] | ✅ | - | - | ✅ |
| [`compress-images`][compress-images] | - | - | | ✅ |
| [`csharp-build`][csharp-build] | - | | | ✅ |
| [`csharp-lint-check`][csharp-lint-check] | - | | ✅ | ✅ |
| [`csharp-publish`][csharp-publish] | - | ✅ | | ✅ |
| [`docker-build`][docker-build] | | ✅ | | ✅ |
| [`docker-publish`][docker-publish] | - | - | ✅ | ✅ |
| [`eslint-lint`][eslint-lint] | ✅ | - | ✅ | ✅ |
| [`go-build`][go-build] | | ✅ | | ✅ |
| [`go-lint`][go-lint] | ✅ | - | ✅ | ✅ |
| [`language-version-detect`][language-version-detect] | - | ✅ | | ✅ |
| [`node-setup`][node-setup] | - | ✅ | | ✅ |
| [`npm-publish`][npm-publish] | - | - | | ✅ |
| [`php-composer`][php-composer] | - | | ✅ | ✅ |
| [`php-laravel-phpunit`][php-laravel-phpunit] | - | | | ✅ |
| [`php-tests`][php-tests] | - | - | | ✅ |
| [`pr-lint`][pr-lint] | ✅ | | | ✅ |
| [`pre-commit`][pre-commit] | - | ✅ | | ✅ |
| [`prettier-lint`][prettier-lint] | ✅ | - | ✅ | ✅ |
| [`python-lint-fix`][python-lint-fix] | | | | ✅ |
| [`release-monthly`][release-monthly] | - | - | ✅ | ✅ |
| [`stale`][stale] | - | - | ✅ | ✅ |
| [`sync-labels`][sync-labels] | - | - | ✅ | ✅ |
| [`terraform-lint-fix`][terraform-lint-fix] | - | - | | ✅ |
| [`validate-inputs`][validate-inputs] | - | - | ✅ | ✅ |
| [`version-file-parser`][version-file-parser] | - | ✅ | - | ✅ |
| Action | Caching | Auto-detection | Token auth | Outputs |
|:-------------------------------------------------------|:-------:|:--------------:|:----------:|:-------:|
| [`ansible-lint-fix`][ansible-lint-fix] | - | - | ✅ | ✅ |
| [`biome-check`][biome-check] | - | - | ✅ | ✅ |
| [`biome-fix`][biome-fix] | - | - | ✅ | ✅ |
| [`codeql-analysis`][codeql-analysis] | - | ✅ | ✅ | ✅ |
| [`common-cache`][common-cache] | ✅ | - | - | ✅ |
| [`common-file-check`][common-file-check] | - | - | - | ✅ |
| [`common-retry`][common-retry] | - | - | - | ✅ |
| [`compress-images`][compress-images] | - | - | ✅ | ✅ |
| [`csharp-build`][csharp-build] | - | ✅ | - | ✅ |
| [`csharp-lint-check`][csharp-lint-check] | - | ✅ | - | ✅ |
| [`csharp-publish`][csharp-publish] | - | | ✅ | ✅ |
| [`docker-build`][docker-build] | ✅ | | ✅ | ✅ |
| [`docker-publish`][docker-publish] | - | ✅ | - | ✅ |
| [`docker-publish-gh`][docker-publish-gh] | ✅ | | ✅ | ✅ |
| [`docker-publish-hub`][docker-publish-hub] | | ✅ | - | ✅ |
| [`dotnet-version-detect`][dotnet-version-detect] | - | ✅ | - | ✅ |
| [`eslint-check`][eslint-check] | | - | - | ✅ |
| [`eslint-fix`][eslint-fix] | - | - | ✅ | ✅ |
| [`github-release`][github-release] | - | - | - | ✅ |
| [`go-build`][go-build] | | | - | ✅ |
| [`go-lint`][go-lint] | ✅ | - | - | ✅ |
| [`go-version-detect`][go-version-detect] | - | ✅ | - | ✅ |
| [`node-setup`][node-setup] | ✅ | | ✅ | ✅ |
| [`npm-publish`][npm-publish] | - | - | - | ✅ |
| [`php-composer`][php-composer] | - | | ✅ | ✅ |
| [`php-laravel-phpunit`][php-laravel-phpunit] | - | | ✅ | ✅ |
| [`php-tests`][php-tests] | - | - | ✅ | ✅ |
| [`php-version-detect`][php-version-detect] | - | | - | ✅ |
| [`pr-lint`][pr-lint] | | | ✅ | ✅ |
| [`pre-commit`][pre-commit] | - | ✅ | | ✅ |
| [`prettier-check`][prettier-check] | ✅ | - | - | ✅ |
| [`prettier-fix`][prettier-fix] | - | - | ✅ | ✅ |
| [`python-lint-fix`][python-lint-fix] | ✅ | ✅ | ✅ | ✅ |
| [`python-version-detect`][python-version-detect] | - | ✅ | - | ✅ |
| [`python-version-detect-v2`][python-version-detect-v2] | - | ✅ | - | ✅ |
| [`release-monthly`][release-monthly] | - | - | ✅ | ✅ |
| [`set-git-config`][set-git-config] | - | - | ✅ | ✅ |
| [`stale`][stale] | - | - | ✅ | ✅ |
| [`sync-labels`][sync-labels] | - | - | ✅ | ✅ |
| [`terraform-lint-fix`][terraform-lint-fix] | - | - | ✅ | ✅ |
| [`validate-inputs`][validate-inputs] | - | - | ✅ | ✅ |
| [`version-file-parser`][version-file-parser] | - | ✅ | - | ✅ |
| [`version-validator`][version-validator] | - | ✅ | - | ✅ |
### Language Support
| Language | Actions |
|:---------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .NET | [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish], [`language-version-detect`][language-version-detect] |
| Ansible | [`ansible-lint-fix`][ansible-lint-fix] |
| C# | [`codeql-analysis`][codeql-analysis], [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish] |
| C++ | [`codeql-analysis`][codeql-analysis] |
| Caching | [`common-cache`][common-cache] |
| Conventional Commits | [`pr-lint`][pr-lint] |
| Docker | [`docker-build`][docker-build], [`docker-publish`][docker-publish] |
| GitHub | [`sync-labels`][sync-labels] |
| GitHub Actions | [`action-versioning`][action-versioning], [`release-monthly`][release-monthly], [`stale`][stale], [`validate-inputs`][validate-inputs] |
| Go | [`codeql-analysis`][codeql-analysis], [`go-build`][go-build], [`go-lint`][go-lint], [`language-version-detect`][language-version-detect] |
| HCL | [`terraform-lint-fix`][terraform-lint-fix] |
| Images | [`compress-images`][compress-images] |
| JPEG | [`compress-images`][compress-images] |
| JSON | [`biome-lint`][biome-lint], [`prettier-lint`][prettier-lint] |
| Java | [`codeql-analysis`][codeql-analysis] |
| JavaScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`node-setup`][node-setup], [`prettier-lint`][prettier-lint] |
| Laravel | [`php-laravel-phpunit`][php-laravel-phpunit] |
| Markdown | [`prettier-lint`][prettier-lint] |
| Multiple Languages | [`pre-commit`][pre-commit], [`version-file-parser`][version-file-parser] |
| Node.js | [`language-version-detect`][language-version-detect], [`node-setup`][node-setup], [`npm-publish`][npm-publish] |
| PHP | [`language-version-detect`][language-version-detect], [`php-composer`][php-composer], [`php-laravel-phpunit`][php-laravel-phpunit], [`php-tests`][php-tests] |
| PNG | [`compress-images`][compress-images] |
| Python | [`codeql-analysis`][codeql-analysis], [`language-version-detect`][language-version-detect], [`pre-commit`][pre-commit], [`python-lint-fix`][python-lint-fix] |
| Ruby | [`codeql-analysis`][codeql-analysis] |
| Terraform | [`terraform-lint-fix`][terraform-lint-fix] |
| TypeScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`node-setup`][node-setup], [`prettier-lint`][prettier-lint] |
| YAML | [`ansible-lint-fix`][ansible-lint-fix], [`prettier-lint`][prettier-lint], [`sync-labels`][sync-labels], [`validate-inputs`][validate-inputs] |
| npm | [`npm-publish`][npm-publish] |
| Language | Actions |
|:-----------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .NET | [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish], [`dotnet-version-detect`][dotnet-version-detect] |
| Ansible | [`ansible-lint-fix`][ansible-lint-fix] |
| C# | [`csharp-build`][csharp-build], [`csharp-lint-check`][csharp-lint-check], [`csharp-publish`][csharp-publish], [`dotnet-version-detect`][dotnet-version-detect] |
| Docker | [`docker-build`][docker-build], [`docker-publish`][docker-publish], [`docker-publish-gh`][docker-publish-gh], [`docker-publish-hub`][docker-publish-hub] |
| Go | [`go-build`][go-build], [`go-lint`][go-lint], [`go-version-detect`][go-version-detect] |
| HCL | [`terraform-lint-fix`][terraform-lint-fix] |
| JSON | [`biome-check`][biome-check], [`biome-fix`][biome-fix], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] |
| 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] |
| Laravel | [`php-laravel-phpunit`][php-laravel-phpunit] |
| Markdown | [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] |
| Node.js | [`node-setup`][node-setup], [`npm-publish`][npm-publish] |
| PHP | [`php-composer`][php-composer], [`php-laravel-phpunit`][php-laravel-phpunit], [`php-tests`][php-tests], [`php-version-detect`][php-version-detect] |
| Python | [`python-lint-fix`][python-lint-fix], [`python-version-detect`][python-version-detect], [`python-version-detect-v2`][python-version-detect-v2] |
| Terraform | [`terraform-lint-fix`][terraform-lint-fix] |
| 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] |
| YAML | [`ansible-lint-fix`][ansible-lint-fix], [`prettier-check`][prettier-check], [`prettier-fix`][prettier-fix] |
| npm | [`npm-publish`][npm-publish] |
### Action Usage
@@ -205,7 +226,7 @@ All actions can be used independently in your workflows:
```yaml
# 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:
# action-specific inputs
@@ -219,36 +240,49 @@ All actions can be used independently in your workflows:
<!-- Reference Links -->
[action-versioning]: action-versioning/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
[common-cache]: common-cache/README.md
[common-file-check]: common-file-check/README.md
[common-retry]: common-retry/README.md
[compress-images]: compress-images/README.md
[csharp-build]: csharp-build/README.md
[csharp-lint-check]: csharp-lint-check/README.md
[csharp-publish]: csharp-publish/README.md
[docker-build]: docker-build/README.md
[docker-publish]: docker-publish/README.md
[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-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
[npm-publish]: npm-publish/README.md
[php-composer]: php-composer/README.md
[php-laravel-phpunit]: php-laravel-phpunit/README.md
[php-tests]: php-tests/README.md
[php-version-detect]: php-version-detect/README.md
[pr-lint]: pr-lint/README.md
[pre-commit]: pre-commit/README.md
[prettier-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-version-detect]: python-version-detect/README.md
[python-version-detect-v2]: python-version-detect-v2/README.md
[release-monthly]: release-monthly/README.md
[set-git-config]: set-git-config/README.md
[stale]: stale/README.md
[sync-labels]: sync-labels/README.md
[terraform-lint-fix]: terraform-lint-fix/README.md
[validate-inputs]: validate-inputs/README.md
[version-file-parser]: version-file-parser/README.md
[version-validator]: version-validator/README.md
---

View File

@@ -57,21 +57,6 @@ get_action_name() {
uv run "$script_dir/../shared/validation_core.py" --name "$action_file"
}
# Check if an input is required in an action.yml file
is_input_required() {
local action_file="$1"
local input_name="$2"
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Get the 'required' property for the input
local required_status
required_status=$(uv run "$script_dir/../shared/validation_core.py" --property "$action_file" "$input_name" "required")
# Return 0 (success) if input is required, 1 (failure) if optional
[[ $required_status == "required" ]]
}
# Test input validation using Python validation module
test_input_validation() {
local action_dir="$1"
@@ -363,5 +348,5 @@ run_action_tests() {
}
# 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

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

View File

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

View File

@@ -151,14 +151,14 @@ discover_actions() {
if [[ $action_name == *"$ACTION_FILTER"* ]]; then
actions+=("$action_name")
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
# All actions
while IFS= read -r action_dir; do
local action_name
action_name=$(basename "$action_dir")
actions+=("$action_name")
done < <(find "${TEST_ROOT}/.." -mindepth 2 -maxdepth 2 -type f -name "action.yml" -exec dirname {} \; | sort)
done < <(find "${TEST_ROOT}/.." -mindepth 1 -maxdepth 1 -type d -name "*-*" | sort)
fi
log_info "Discovered ${#actions[@]} actions to test: ${actions[*]}"
@@ -203,7 +203,7 @@ install_shellspec() {
# Pinned SHA256 checksum for ShellSpec 0.28.1
# 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
# 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
set -eu
# Parse arguments
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
VERSION="${1:-}"
# Source shared utilities
# shellcheck source=_tools/shared.sh
@@ -63,17 +11,15 @@ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
. "$SCRIPT_DIR/shared.sh"
if [ -z "$VERSION" ]; then
msg_error "VERSION argument required"
printf 'Usage: %s [OPTIONS] VERSION\n' "$0"
printf 'Use --help for more information\n'
printf '%b' "${RED}Error: VERSION argument required${NC}\n"
printf 'Usage: %s v2025.10.18\n' "$0"
exit 1
fi
# Validate version format
if ! validate_version "$VERSION"; then
msg_error "Invalid version format: $VERSION"
printf 'Expected: vYYYY.MM.DD with zero-padded month/day (e.g., v2025.10.18, v2025.01.05)\n'
printf 'Invalid: v2025.1.5 (must be zero-padded)\n'
printf '%b' "${RED}Error: Invalid version format: $VERSION${NC}\n"
printf 'Expected: vYYYY.MM.DD (e.g., v2025.10.18)\n'
exit 1
fi
@@ -89,201 +35,68 @@ major="v$year"
minor="v$year.$month"
patch="v$year.$month.$day"
# Show dry-run banner if applicable
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 '%b' "${BLUE}Creating release $VERSION${NC}\n"
printf ' Major: %s\n' "$major"
printf ' Minor: %s\n' "$minor"
printf ' Patch: %s\n' "$patch"
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
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'
# Confirmation prompt (skip if --yes or --dry-run)
if [ "$DRY_RUN" = "false" ] && [ "$SKIP_CONFIRM" = "false" ]; then
if ! prompt_confirmation "Proceed with release $VERSION?"; then
msg_warn "Release cancelled by user"
exit 0
fi
printf '\n'
fi
# Update all action references to current SHA
printf '%b' "${BLUE}Updating action references to $current_sha...${NC}\n"
"$SCRIPT_DIR/update-action-refs.sh" "$current_sha" "direct"
# Skip prep if --tag-only
if [ "$TAG_ONLY" = "true" ]; then
msg_info "Skipping preparation (--tag-only mode)"
printf '\n'
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
# Commit the changes
if ! git diff --quiet; then
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
commit SHA in preparation for release $VERSION."
# Update SHA since we just created a new commit
current_sha=$(git rev-parse HEAD)
msg_done "Committed updated action references"
printf 'New HEAD: %s%s%s\n' "$GREEN" "$current_sha" "$NC"
fi
else
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
# Update SHA since we just created a new commit
current_sha=$(git rev-parse HEAD)
printf '%b' "${GREEN}Committed updated action references${NC}\n"
printf '%b' "New HEAD: ${GREEN}$current_sha${NC}\n"
else
printf '%b' "${BLUE}No changes to commit${NC}\n"
fi
# Create/update tags
printf '\n'
msg_info "Creating tags..."
printf '%b' "${BLUE}Creating tags...${NC}\n"
# Create patch tag
if [ "$DRY_RUN" = "true" ]; then
msg_warn "[DRY RUN] Would create tag: $patch"
else
git tag -a "$patch" -m "Release $patch"
msg_item "Created tag: $patch"
fi
git tag -a "$patch" -m "Release $patch"
printf '%b' " ${GREEN}${NC} Created tag: $patch\n"
# Move/create minor tag
if git rev-parse "$minor" >/dev/null 2>&1; then
if [ "$DRY_RUN" = "true" ]; then
msg_warn "[DRY RUN] Would force-update tag: $minor"
else
git tag -f -a "$minor" -m "Latest $minor release: $patch"
msg_item "Updated tag: $minor (force)"
fi
git tag -f -a "$minor" -m "Latest $minor release: $patch"
printf '%b' " ${GREEN}${NC} Updated tag: $minor (force)\n"
else
if [ "$DRY_RUN" = "true" ]; then
msg_warn "[DRY RUN] Would create tag: $minor"
else
git tag -a "$minor" -m "Latest $minor release: $patch"
msg_item "Created tag: $minor"
fi
git tag -a "$minor" -m "Latest $minor release: $patch"
printf '%b' " ${GREEN}${NC} Created tag: $minor\n"
fi
# Move/create major tag
if git rev-parse "$major" >/dev/null 2>&1; then
if [ "$DRY_RUN" = "true" ]; then
msg_warn "[DRY RUN] Would force-update tag: $major"
else
git tag -f -a "$major" -m "Latest $major release: $patch"
msg_item "Updated tag: $major (force)"
fi
git tag -f -a "$major" -m "Latest $major release: $patch"
printf '%b' " ${GREEN}${NC} Updated tag: $major (force)\n"
else
if [ "$DRY_RUN" = "true" ]; then
msg_warn "[DRY RUN] Would create tag: $major"
else
git tag -a "$major" -m "Latest $major release: $patch"
msg_item "Created tag: $major"
fi
git tag -a "$major" -m "Latest $major release: $patch"
printf '%b' " ${GREEN}${NC} Created tag: $major\n"
fi
printf '\n'
if [ "$DRY_RUN" = "true" ]; then
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 '%b' "${GREEN}✅ Release $VERSION created successfully${NC}\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'
msg_info "Tags created:"
printf '%b' "${BLUE}Tags created:${NC}\n"
printf ' %s\n' "$patch"
printf ' %s\n' "$minor"
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
NC='\033[0m' # No Color
# Validate CalVer version format: vYYYY.MM.DD (zero-padded)
# Validate CalVer version format: vYYYY.MM.DD
validate_version() {
version="$1"
# Check format: vYYYY.MM.DD (require zero-padding) using grep
if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}$'; then
# Check format: vYYYY.MM.DD using grep
if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}$'; then
return 1
fi
@@ -34,12 +34,12 @@ validate_version() {
return 1
fi
# Validate month (01-12)
# Validate month (1-12)
if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then
return 1
fi
# Validate day (01-31)
# Validate day (1-31)
if [ "$day" -lt 1 ] || [ "$day" -gt 31 ]; then
return 1
fi
@@ -67,12 +67,12 @@ validate_major_version() {
return 0
}
# Validate minor version format: vYYYY.MM (zero-padded)
# Validate minor version format: vYYYY.MM
validate_minor_version() {
version="$1"
# Check format: vYYYY.MM (require zero-padding) using grep
if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{2}$'; then
# Check format: vYYYY.MM using grep
if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{1,2}$'; then
return 1
fi
@@ -86,7 +86,7 @@ validate_minor_version() {
return 1
fi
# Validate month (01-12)
# Validate month (1-12)
if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then
return 1
fi
@@ -94,139 +94,6 @@ validate_minor_version() {
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_script_dir() {
cd "$(dirname -- "$1")" && pwd
@@ -240,7 +107,7 @@ has_git() {
# Require git to be available, exit with error if not
require_git() {
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
exit 1
fi
@@ -250,7 +117,7 @@ require_git() {
safe_mktemp() {
_temp_file=""
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
fi
printf '%s' "$_temp_file"

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
# Validation rules for codeql-analysis action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 94% (15/16 inputs)
# Coverage: 94% (16/17 inputs)
#
# This file defines validation rules for the codeql-analysis GitHub Action.
# Rules are automatically applied by validate-inputs action when this
@@ -16,6 +16,7 @@ generator_version: 1.0.0
required_inputs:
- language
optional_inputs:
- add-snippets
- build-mode
- category
- checkout-ref
@@ -32,6 +33,7 @@ optional_inputs:
- upload-results
- working-directory
conventions:
add-snippets: boolean
build-mode: codeql_build_mode
category: category_format
checkout-ref: branch_name
@@ -60,8 +62,8 @@ overrides:
threads: numeric_range_1_128
token: github_token
statistics:
total_inputs: 16
validated_inputs: 15
total_inputs: 17
validated_inputs: 16
skipped_inputs: 0
coverage_percentage: 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"
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
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token }}
@@ -162,8 +169,7 @@ runs:
if: steps.calibre.outputs.markdown != ''
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ inputs.token }}
title: 'chore: compress images'
title: Compressed Images Nightly
branch-suffix: timestamp
commit-message: 'chore: compress images'
commit-message: Compressed Images
body: ${{ steps.calibre.outputs.markdown }}

View File

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

View File

@@ -60,21 +60,20 @@ runs:
echo "Input validation completed successfully"
- name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token || github.token }}
- name: Detect .NET SDK Version
id: detect-dotnet-version
uses: ivuorinen/actions/language-version-detect@0fa9a68f07a1260b321f814202658a6089a43d42
uses: ivuorinen/actions/dotnet-version-detect@e2222afff180ee77f330ef4325f60d6e85477c01
with:
language: 'dotnet'
default-version: ${{ inputs.dotnet-version || '7.0' }}
- name: Setup .NET SDK
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: ${{ steps.detect-dotnet-version.outputs.detected-version }}
dotnet-version: ${{ steps.detect-dotnet-version.outputs.dotnet-version }}
- name: Install dotnet-format
shell: bash
@@ -112,6 +111,6 @@ runs:
fi
- 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:
sarif_file: dotnet-format.sarif

View File

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

View File

@@ -120,7 +120,7 @@ outputs:
value: ${{ steps.build.outputs.metadata }}
platforms:
description: 'Successfully built platforms'
value: ${{ steps.detect-platforms.outputs.platforms }}
value: ${{ steps.platforms.outputs.built }}
platform-matrix:
description: 'Build status per platform in JSON format'
value: ${{ steps.build.outputs.platform-matrix }}
@@ -141,13 +141,13 @@ runs:
using: composite
steps:
- name: Checkout Repository
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token || github.token }}
- name: Validate Inputs
id: validate
uses: ivuorinen/actions/validate-inputs@0fa9a68f07a1260b321f814202658a6089a43d42
uses: ivuorinen/actions/validate-inputs@e2222afff180ee77f330ef4325f60d6e85477c01
with:
action-type: 'docker-build'
image-name: ${{ inputs.image-name }}
@@ -169,7 +169,7 @@ runs:
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
with:
platforms: ${{ inputs.architectures }}
@@ -186,28 +186,22 @@ runs:
- name: Detect Available Platforms
id: detect-platforms
if: inputs.auto-detect-platforms == 'true'
shell: bash
env:
ARCHITECTURES: ${{ inputs.architectures }}
AUTO_DETECT: ${{ inputs.auto-detect-platforms }}
run: |
set -euo pipefail
# When auto-detect is enabled, try to detect available platforms
if [ "$AUTO_DETECT" = "true" ]; then
available_platforms=$(docker buildx ls | grep -o 'linux/[^ ]*' | sort -u | tr '\n' ',' | sed 's/,$//' || true)
# 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=$ARCHITECTURES" >> $GITHUB_OUTPUT
echo "Using default platforms (detection failed): $ARCHITECTURES"
fi
if [ -n "$available_platforms" ]; then
echo "platforms=${available_platforms}" >> $GITHUB_OUTPUT
echo "Detected platforms: ${available_platforms}"
else
# Auto-detect disabled, use configured architectures
echo "platforms=$ARCHITECTURES" >> $GITHUB_OUTPUT
echo "Using configured platforms: $ARCHITECTURES"
echo "Using default platforms: $ARCHITECTURES"
fi
- 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
Simple wrapper to publish Docker images to GitHub Packages and/or Docker Hub
Publish a Docker image to GitHub Packages and Docker Hub.
### Inputs
| name | description | required | default |
|----------------------|-------------------------------------------------------------------|----------|---------------------------|
| `registry` | <p>Registry to publish to (dockerhub, github, or both)</p> | `false` | `both` |
| `image-name` | <p>Docker image name (defaults to repository name)</p> | `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` |
| `context` | <p>Build context path</p> | `false` | `.` |
| `dockerfile` | <p>Path to Dockerfile</p> | `false` | `Dockerfile` |
| `build-args` | <p>Build arguments (newline-separated KEY=VALUE pairs)</p> | `false` | `""` |
| `push` | <p>Whether to push the image</p> | `false` | `true` |
| `token` | <p>GitHub token for authentication (for GitHub registry)</p> | `false` | `""` |
| `dockerhub-username` | <p>Docker Hub username (required if publishing to Docker Hub)</p> | `false` | `""` |
| `dockerhub-token` | <p>Docker Hub token (required if publishing to Docker Hub)</p> | `false` | `""` |
| name | description | required | default |
|-------------------------|-------------------------------------------------------------------|----------|----------------------------------------|
| `registry` | <p>Registry to publish to (dockerhub, github, or both).</p> | `true` | `both` |
| `nightly` | <p>Is this a nightly build? (true or false)</p> | `false` | `false` |
| `platforms` | <p>Platforms to build for (comma-separated)</p> | `false` | `linux/amd64,linux/arm64,linux/arm/v7` |
| `auto-detect-platforms` | <p>Automatically detect and build for all available platforms</p> | `false` | `false` |
| `scan-image` | <p>Scan images for vulnerabilities</p> | `false` | `true` |
| `sign-image` | <p>Sign images with cosign</p> | `false` | `false` |
| `cache-mode` | <p>Cache mode for build layers (min, max, or inline)</p> | `false` | `max` |
| `buildx-version` | <p>Specific Docker Buildx version to use</p> | `false` | `latest` |
| `verbose` | <p>Enable verbose logging</p> | `false` | `false` |
| `dockerhub-username` | <p>Docker Hub username for authentication</p> | `false` | `""` |
| `dockerhub-password` | <p>Docker Hub password or access token for authentication</p> | `false` | `""` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
### Outputs
| name | description |
|--------------|--------------------------------------|
| `image-name` | <p>Full image name with registry</p> |
| `tags` | <p>Tags that were published</p> |
| `digest` | <p>Image digest</p> |
| `metadata` | <p>Build metadata</p> |
| name | description |
|-------------------|-------------------------------------------------------|
| `registry` | <p>Registry where image was published</p> |
| `tags` | <p>Tags that were published</p> |
| `build-time` | <p>Total build time in seconds</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
@@ -41,67 +46,73 @@ This action is a `composite` action.
- uses: ivuorinen/actions/docker-publish@main
with:
registry:
# Registry to publish to (dockerhub, github, or both)
# Registry to publish to (dockerhub, github, or both).
#
# Required: false
# Required: true
# Default: both
image-name:
# Docker image name (defaults to repository name)
nightly:
# Is this a nightly build? (true or false)
#
# Required: false
# Default: ""
tags:
# Comma-separated list of tags (e.g., latest,v1.0.0)
#
# Required: false
# Default: latest
# Default: false
platforms:
# Platforms to build for (comma-separated)
#
# Required: false
# Default: linux/amd64,linux/arm64
# Default: linux/amd64,linux/arm64,linux/arm/v7
context:
# Build context path
auto-detect-platforms:
# Automatically detect and build for all available platforms
#
# Required: false
# Default: .
# Default: false
dockerfile:
# Path to Dockerfile
#
# Required: false
# Default: Dockerfile
build-args:
# Build arguments (newline-separated KEY=VALUE pairs)
#
# Required: false
# Default: ""
push:
# Whether to push the image
scan-image:
# Scan images for vulnerabilities
#
# Required: false
# Default: true
token:
# GitHub token for authentication (for GitHub registry)
sign-image:
# Sign images with cosign
#
# 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:
# Docker Hub username (required if publishing to Docker Hub)
# Docker Hub username for authentication
#
# Required: false
# Default: ""
dockerhub-token:
# Docker Hub token (required if publishing to Docker Hub)
dockerhub-password:
# Docker Hub password or access token for authentication
#
# Required: false
# Default: ""
token:
# GitHub token for authentication
#
# Required: false
# Default: ""

View File

@@ -2,33 +2,9 @@
# permissions:
# - packages: write # Required for publishing to Docker registries
# - contents: read # Required for checking out repository
#
# Security Considerations:
#
# Trust Model: This action should only be used in trusted workflows controlled by repository owners.
# Do not pass untrusted user input (e.g., PR labels, comments, external webhooks) to the `context`
# or `dockerfile` parameters.
#
# Input Validation: The action validates `context` and `dockerfile` inputs to prevent code injection attacks:
#
# - `context`: Must be a relative path (e.g., `.`, `./app`, `subdir/`). Absolute paths are rejected.
# Remote URLs trigger a warning and should only be used from trusted sources.
# - `dockerfile`: Must be a relative path (e.g., `Dockerfile`, `./docker/Dockerfile`). Absolute paths
# and URLs are rejected.
#
# These validations help prevent malicious actors from:
# - Building Docker images from arbitrary file system locations
# - Fetching malicious Dockerfiles from untrusted remote sources
# - Executing code injection attacks through build context manipulation
#
# Best Practices:
# 1. Only use hard-coded values or trusted workflow variables for `context` and `dockerfile`
# 2. Never accept these values from PR comments, labels, or external webhooks
# 3. Review workflow permissions before granting write access to this action
# 4. Use SHA-pinned action references: `ivuorinen/actions/docker-publish@<commit-sha>`
---
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
branding:
@@ -37,225 +13,309 @@ branding:
inputs:
registry:
description: 'Registry to publish to (dockerhub, github, or both)'
required: false
description: 'Registry to publish to (dockerhub, github, or both).'
required: true
default: 'both'
image-name:
description: 'Docker image name (defaults to repository name)'
nightly:
description: 'Is this a nightly build? (true or false)'
required: false
tags:
description: 'Comma-separated list of tags (e.g., latest,v1.0.0)'
required: false
default: 'latest'
default: 'false'
platforms:
description: 'Platforms to build for (comma-separated)'
required: false
default: 'linux/amd64,linux/arm64'
context:
description: 'Build context path'
default: 'linux/amd64,linux/arm64,linux/arm/v7'
auto-detect-platforms:
description: 'Automatically detect and build for all available platforms'
required: false
default: '.'
dockerfile:
description: 'Path to Dockerfile'
required: false
default: 'Dockerfile'
build-args:
description: 'Build arguments (newline-separated KEY=VALUE pairs)'
required: false
push:
description: 'Whether to push the image'
default: 'false'
scan-image:
description: 'Scan images for vulnerabilities'
required: false
default: 'true'
sign-image:
description: 'Sign images with cosign'
required: false
default: 'false'
cache-mode:
description: 'Cache mode for build layers (min, max, or inline)'
required: false
default: 'max'
buildx-version:
description: 'Specific Docker Buildx version to use'
required: false
default: 'latest'
verbose:
description: 'Enable verbose logging'
required: false
default: 'false'
dockerhub-username:
description: 'Docker Hub username for authentication'
required: false
dockerhub-password:
description: 'Docker Hub password or access token for authentication'
required: false
token:
description: 'GitHub token for authentication (for GitHub registry)'
description: 'GitHub token for authentication'
required: false
default: ''
dockerhub-username:
description: 'Docker Hub username (required if publishing to Docker Hub)'
required: false
dockerhub-token:
description: 'Docker Hub token (required if publishing to Docker Hub)'
required: false
outputs:
image-name:
description: 'Full image name with registry'
value: ${{ steps.meta.outputs.image-name }}
registry:
description: 'Registry where image was published'
value: ${{ steps.dest.outputs.reg }}
tags:
description: 'Tags that were published'
value: ${{ steps.meta.outputs.tags }}
digest:
description: 'Image digest'
value: ${{ steps.build.outputs.digest }}
metadata:
description: 'Build metadata'
value: ${{ steps.build.outputs.metadata }}
value: ${{ steps.tags.outputs.all-tags }}
build-time:
description: 'Total build time in seconds'
value: ${{ steps.build.outputs.build-time }}
platform-matrix:
description: 'Build status per platform'
value: ${{ steps.build.outputs.platform-matrix }}
scan-results:
description: 'Vulnerability scan results if scanning enabled'
value: ${{ steps.build.outputs.scan-results }}
image-id:
description: 'Published image ID'
value: ${{ steps.publish-dockerhub.outputs.image-id || steps.publish-github.outputs.image-id }}
image-digest:
description: 'Published image digest'
value: ${{ steps.publish-dockerhub.outputs.digest || steps.publish-github.outputs.digest }}
repository:
description: 'Repository where image was published'
value: ${{ steps.publish-dockerhub.outputs.repository || steps.publish-github.outputs.repository }}
runs:
using: composite
steps:
- name: Mask Sensitive Inputs
shell: bash
env:
DOCKERHUB_PASSWORD: ${{ inputs.dockerhub-password }}
run: |
set -euo pipefail
# Mask Docker Hub credentials to prevent exposure in logs
if [[ -n "${DOCKERHUB_PASSWORD}" ]]; then
echo "::add-mask::${DOCKERHUB_PASSWORD}"
fi
- name: Validate Inputs
id: validate
shell: sh
shell: bash
env:
INPUT_REGISTRY: ${{ inputs.registry }}
INPUT_DOCKERHUB_USERNAME: ${{ inputs.dockerhub-username }}
INPUT_DOCKERHUB_TOKEN: ${{ inputs.dockerhub-token }}
INPUT_TOKEN: ${{ inputs.token }}
INPUT_CONTEXT: ${{ inputs.context }}
INPUT_DOCKERFILE: ${{ inputs.dockerfile }}
REGISTRY: ${{ inputs.registry }}
run: |
set -eu
set -euo pipefail
# Validate registry input
case "$INPUT_REGISTRY" in
dockerhub|github|both)
;;
*)
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
if ! [[ "$REGISTRY" =~ ^(dockerhub|github|both)$ ]]; then
echo "::error::Invalid registry value. Must be 'dockerhub', 'github', or 'both'"
exit 1
fi
# Validate GitHub token if needed
if [ "$INPUT_REGISTRY" = "github" ] || [ "$INPUT_REGISTRY" = "both" ]; then
token="${INPUT_TOKEN:-${GITHUB_TOKEN:-}}"
if [ -z "$token" ]; then
echo "::error::GitHub token is required when publishing to GitHub Packages"
exit 1
fi
fi
# 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
- name: Determine Tags
id: tags
shell: bash
env:
INPUT_REGISTRY: ${{ inputs.registry }}
INPUT_IMAGE_NAME: ${{ inputs.image-name }}
INPUT_TAGS: ${{ inputs.tags }}
NIGHTLY: ${{ inputs.nightly }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
# Initialize variables
declare -a tag_array
if [[ "$NIGHTLY" == "true" ]]; then
# Nightly build tags
current_date=$(date +'%Y%m%d-%H%M')
tag_array+=("nightly")
tag_array+=("nightly-${current_date}")
else
# Release tags
if [[ -n "$RELEASE_TAG" ]]; then
tag_array+=("$RELEASE_TAG")
tag_array+=("latest")
else
echo "::error::No release tag found and not a nightly build"
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 }}
run: |
set -eu
set -euo pipefail
# Determine base image name
if [ -n "$INPUT_IMAGE_NAME" ]; then
base_name="$INPUT_IMAGE_NAME"
else
# Use repository name (lowercase)
base_name=$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]')
fi
echo "Verifying publications..."
success=true
# Build full image names based on registry
image_names=""
case "$INPUT_REGISTRY" in
dockerhub)
image_names="docker.io/${base_name}"
;;
github)
image_names="ghcr.io/${base_name}"
;;
both)
image_names="docker.io/${base_name},ghcr.io/${base_name}"
;;
esac
# Split registry string into array
IFS=',' read -ra REGISTRIES <<< "$DEST_REG"
# Build full tags (image:tag format)
tags=""
IFS=','
for image in $image_names; do
for tag in $INPUT_TAGS; do
tag=$(echo "$tag" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -n "$tags" ]; then
tags="$(printf '%s\n%s' "$tags" "${image}:${tag}")"
else
tags="${image}:${tag}"
fi
done
for registry in "${REGISTRIES[@]}"; do
echo "Checking ${registry} publication..."
case "${registry}" in
"dockerhub")
# Get actual image name from publish step output or fallback to repo-based name
image_name="$DOCKERHUB_IMAGE_NAME"
if [[ -z "$image_name" ]]; then
image_name="docker.io/$GITHUB_REPOSITORY"
fi
# Get tags from publish step or fallback to metadata
tags="$DOCKERHUB_TAGS"
if [[ -z "$tags" ]]; then
tags="$ALL_TAGS"
fi
IFS=',' read -ra TAGS <<< "$tags"
for tag in "${TAGS[@]}"; do
tag=$(echo "$tag" | xargs) # trim whitespace
if ! docker manifest inspect "${image_name}:${tag}" > /dev/null 2>&1; then
echo "::error::Failed to verify Docker Hub publication for ${tag}"
success=false
break
fi
done
if [[ "${success}" != "true" ]]; then
break
fi
;;
"github")
# Get actual image name from publish step output or fallback to repo-based name
image_name="$GITHUB_IMAGE_NAME"
if [[ -z "$image_name" ]]; then
image_name="ghcr.io/$GITHUB_REPOSITORY"
fi
# Get tags from publish step or fallback to metadata
tags="$GITHUB_TAGS"
if [[ -z "$tags" ]]; then
tags="$ALL_TAGS"
fi
IFS=',' read -ra TAGS <<< "$tags"
for tag in "${TAGS[@]}"; do
tag=$(echo "$tag" | xargs) # trim whitespace
if ! docker manifest inspect "${image_name}:${tag}" > /dev/null 2>&1; then
echo "::error::Failed to verify GitHub Packages publication for ${tag}"
success=false
break
fi
done
if [[ "${success}" != "true" ]]; then
break
fi
;;
esac
done
# Output results
printf 'image-name=%s\n' "$base_name" >> "$GITHUB_OUTPUT"
{
echo 'tags<<EOF'
echo "$tags"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
if [[ "${success}" != "true" ]]; then
echo "::error::Publication verification failed"
exit 1
fi
echo "Image name: $base_name"
echo "Tags:"
echo "$tags"
echo "All publications verified successfully"
- name: Login to Docker Hub
if: inputs.registry == 'dockerhub' || inputs.registry == 'both'
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-token }}
- name: Cleanup
if: always()
shell: bash
env:
DEST_REG: ${{ steps.dest.outputs.reg }}
run: |-
set -euo pipefail
- name: Login to GitHub Container Registry
if: inputs.registry == 'github' || inputs.registry == 'both'
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ inputs.token || github.token }}
echo "Cleaning up..."
- name: Build and Push Docker Image
id: build
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: ${{ inputs.platforms }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
build-args: ${{ inputs.build-args }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Remove any temporary files or caches
docker buildx prune -f --keep-storage=10GB
# Remove any temporary authentication
if [[ "$DEST_REG" =~ "dockerhub" ]]; then
docker logout docker.io || true
fi
if [[ "$DEST_REG" =~ "github" ]]; then
docker logout ghcr.io || true
fi
echo "Cleanup completed"

View File

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