mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 11:34:00 +00:00
Compare commits
1 Commits
v2025.12.1
...
v2025.10.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fa9a68f07 |
@@ -17,12 +17,12 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version-file: pyproject.toml
|
||||
|
||||
@@ -31,9 +31,9 @@ runs:
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
|
||||
- name: Install Node dependencies
|
||||
|
||||
16
.github/codeql/codeql-config.yml
vendored
16
.github/codeql/codeql-config.yml
vendored
@@ -15,19 +15,3 @@ paths-ignore:
|
||||
# Use security and quality query suite
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
|
||||
# Suppress specific false positives
|
||||
# These findings have been manually reviewed and determined to be false positives
|
||||
# with appropriate security controls in place
|
||||
query-filters:
|
||||
# docker-publish: Code injection in validated context
|
||||
# False positive: User input is validated and sanitized before use
|
||||
# - Only relative paths and trusted git URLs are allowed
|
||||
# - Absolute paths and arbitrary URLs are rejected
|
||||
# - Path traversal attempts are blocked
|
||||
# - Custom contexts require explicit opt-in via use-custom-context: true
|
||||
# - Wraps docker/build-push-action (trusted Docker-maintained action)
|
||||
# - Action is designed for trusted workflows only (documented in action.yml)
|
||||
- exclude:
|
||||
id: js/actions/code-injection
|
||||
kind: problem
|
||||
|
||||
1
.github/tag-changelog-config.js
vendored
1
.github/tag-changelog-config.js
vendored
@@ -1,7 +1,6 @@
|
||||
module.exports = {
|
||||
types: [
|
||||
{ types: ['feat', 'feature', 'Feat'], label: '🎉 New Features' },
|
||||
{ types: ['security'], label: '🔐 Security' },
|
||||
{ types: ['fix', 'bugfix', 'Fix'], label: '🐛 Bugfixes' },
|
||||
{ types: ['improvements', 'enhancement'], label: '🔨 Improvements' },
|
||||
{ types: ['perf'], label: '🏎️ Performance Improvements' },
|
||||
|
||||
210
.github/workflows/action-security.yml
vendored
210
.github/workflows/action-security.yml
vendored
@@ -35,34 +35,216 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Security Scan
|
||||
id: security-scan
|
||||
uses: ./security-scan
|
||||
- name: Check Required Configurations
|
||||
id: check-configs
|
||||
shell: bash
|
||||
run: |
|
||||
# Initialize all flags as false
|
||||
{
|
||||
echo "run_gitleaks=false"
|
||||
echo "run_trivy=true"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Check Gitleaks configuration and license
|
||||
if [ -f ".gitleaks.toml" ] && [ -n "${{ secrets.GITLEAKS_LICENSE }}" ]; then
|
||||
echo "Gitleaks config and license found"
|
||||
printf '%s\n' "run_gitleaks=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::Gitleaks config or license missing - skipping Gitleaks scan"
|
||||
fi
|
||||
|
||||
- name: Run actionlint
|
||||
uses: raven-actions/actionlint@3a24062651993d40fed1019b58ac6fbdfbf276cc # v2.0.1
|
||||
with:
|
||||
gitleaks-license: ${{ secrets.GITLEAKS_LICENSE }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cache: true
|
||||
fail-on-error: true
|
||||
shellcheck: false
|
||||
|
||||
- name: Run Gitleaks
|
||||
if: steps.check-configs.outputs.run_gitleaks == 'true'
|
||||
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
||||
with:
|
||||
config-path: .gitleaks.toml
|
||||
report-format: sarif
|
||||
report-path: gitleaks-report.sarif
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@a11da62073708815958ea6d84f5650c78a3ef85b # master
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
scanners: 'vuln,config,secret'
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
timeout: '10m'
|
||||
|
||||
- name: Verify SARIF files
|
||||
id: verify-sarif
|
||||
shell: bash
|
||||
run: |
|
||||
# Initialize outputs
|
||||
{
|
||||
echo "has_trivy=false"
|
||||
echo "has_gitleaks=false"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Check Trivy results
|
||||
if [ -f "trivy-results.sarif" ]; then
|
||||
if jq -e . </dev/null 2>&1 <"trivy-results.sarif"; then
|
||||
printf '%s\n' "has_trivy=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::Trivy SARIF file exists but is not valid JSON"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check Gitleaks results if it ran
|
||||
if [ "${{ steps.check-configs.outputs.run_gitleaks }}" = "true" ]; then
|
||||
if [ -f "gitleaks-report.sarif" ]; then
|
||||
if jq -e . </dev/null 2>&1 <"gitleaks-report.sarif"; then
|
||||
printf '%s\n' "has_gitleaks=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::Gitleaks SARIF file exists but is not valid JSON"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Upload Trivy results
|
||||
if: steps.verify-sarif.outputs.has_trivy == 'true'
|
||||
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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
sarif_file: 'gitleaks-report.sarif'
|
||||
category: 'gitleaks'
|
||||
|
||||
- name: Archive security reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: security-reports-${{ github.run_id }}
|
||||
path: |
|
||||
${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'trivy-results.sarif' || '' }}
|
||||
${{ steps.verify-sarif.outputs.has_gitleaks == 'true' && 'gitleaks-report.sarif' || '' }}
|
||||
retention-days: 30
|
||||
|
||||
- name: Analyze Results
|
||||
if: always()
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
try {
|
||||
let totalIssues = 0;
|
||||
let criticalIssues = 0;
|
||||
|
||||
const analyzeSarif = (file, tool) => {
|
||||
if (!fs.existsSync(file)) {
|
||||
console.log(`No results file found for ${tool}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
return sarif.runs.reduce((acc, run) => {
|
||||
if (!run.results) return acc;
|
||||
|
||||
const critical = run.results.filter(r =>
|
||||
r.level === 'error' ||
|
||||
r.level === 'critical' ||
|
||||
(r.ruleId || '').toLowerCase().includes('critical')
|
||||
).length;
|
||||
|
||||
return {
|
||||
total: acc.total + run.results.length,
|
||||
critical: acc.critical + critical
|
||||
};
|
||||
}, { total: 0, critical: 0 });
|
||||
} catch (error) {
|
||||
console.log(`Error analyzing ${tool} results: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Only analyze results from tools that ran successfully
|
||||
const results = {
|
||||
trivy: ${{ steps.verify-sarif.outputs.has_trivy }} ?
|
||||
analyzeSarif('trivy-results.sarif', 'trivy') : null,
|
||||
gitleaks: ${{ steps.verify-sarif.outputs.has_gitleaks }} ?
|
||||
analyzeSarif('gitleaks-report.sarif', 'gitleaks') : null
|
||||
};
|
||||
|
||||
// Aggregate results
|
||||
Object.entries(results).forEach(([tool, result]) => {
|
||||
if (result) {
|
||||
totalIssues += result.total;
|
||||
criticalIssues += result.critical;
|
||||
console.log(`${tool}: ${result.total} total, ${result.critical} critical issues`);
|
||||
}
|
||||
});
|
||||
|
||||
// Create summary
|
||||
const summary = `## Security Scan Summary
|
||||
|
||||
- Total Issues Found: ${totalIssues}
|
||||
- Critical Issues: ${criticalIssues}
|
||||
|
||||
### Tool Breakdown
|
||||
${Object.entries(results)
|
||||
.filter(([_, r]) => r)
|
||||
.map(([tool, r]) =>
|
||||
`- ${tool}: ${r.total} total, ${r.critical} critical`
|
||||
).join('\n')}
|
||||
|
||||
### Tools Run Status
|
||||
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy }}
|
||||
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks }}
|
||||
`;
|
||||
|
||||
// Set output
|
||||
core.setOutput('total_issues', totalIssues);
|
||||
core.setOutput('critical_issues', criticalIssues);
|
||||
|
||||
// Add job summary
|
||||
await core.summary
|
||||
.addRaw(summary)
|
||||
.write();
|
||||
|
||||
// Fail if critical issues found
|
||||
if (criticalIssues > 0) {
|
||||
core.setFailed(`Found ${criticalIssues} critical security issues`);
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(`Analysis failed: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Notify on Critical Issues
|
||||
if: failure() && steps.security-scan.outputs.critical_issues != '0'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
if: failure()
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |-
|
||||
const { repo, owner } = context.repo;
|
||||
const critical = '${{ steps.security-scan.outputs.critical_issues }}';
|
||||
const total = '${{ steps.security-scan.outputs.total_issues }}';
|
||||
const critical = core.getInput('critical_issues');
|
||||
|
||||
const body = `🚨 Critical security issues found in GitHub Actions
|
||||
|
||||
${critical} critical security issues (out of ${total} total) were found during the security scan.
|
||||
${critical} critical security issues were found during the security scan.
|
||||
|
||||
### Scan Results
|
||||
- Actionlint: Completed
|
||||
- Trivy: ${{ steps.security-scan.outputs.has_trivy_results == 'true' && 'Completed' || 'Skipped/Failed' }}
|
||||
- Gitleaks: ${{ steps.security-scan.outputs.has_gitleaks_results == 'true' && 'Completed' || 'Skipped' }}
|
||||
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'Completed' || 'Skipped/Failed' }}
|
||||
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks == 'true' && 'Completed' || 'Skipped' }}
|
||||
|
||||
[View detailed scan results](https://github.com/${owner}/${repo}/actions/runs/${context.runId})
|
||||
|
||||
|
||||
4
.github/workflows/build-testing-image.yml
vendored
4
.github/workflows/build-testing-image.yml
vendored
@@ -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@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/actions
|
||||
tags: |
|
||||
|
||||
3
.github/workflows/codeql-new.yml
vendored
3
.github/workflows/codeql-new.yml
vendored
@@ -35,12 +35,11 @@ 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
|
||||
with:
|
||||
language: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
config-file: .github/codeql/codeql-config.yml
|
||||
token: ${{ github.token }}
|
||||
|
||||
51
.github/workflows/codeql.yml
vendored
Normal file
51
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
schedule:
|
||||
- cron: '30 1 * * 0' # Run at 1:30 AM UTC every Sunday
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language:
|
||||
- 'actions'
|
||||
- 'javascript'
|
||||
- 'python'
|
||||
|
||||
steps: # Add languages used in your actions
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
4
.github/workflows/dependency-review.yml
vendored
4
.github/workflows/dependency-review.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/issue-stats.yml
vendored
5
.github/workflows/issue-stats.yml
vendored
@@ -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@55bb0b704982057a101ab7515fb72b2293927c8a # v3.25.4
|
||||
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"'
|
||||
|
||||
41
.github/workflows/new-release.yml
vendored
41
.github/workflows/new-release.yml
vendored
@@ -20,30 +20,27 @@ jobs:
|
||||
version: ${{ steps.daily-version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Create daily release
|
||||
- name: Create tag if necessary
|
||||
uses: fregante/daily-version-action@fb1a60b7c4daf1410cd755e360ebec3901e58588 # v2.1.3
|
||||
id: daily-version
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
VERSION="v$(date '+%Y.%m.%d')"
|
||||
printf '%s\n' "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
- name: Create changelog text
|
||||
if: steps.daily-version.outputs.created
|
||||
id: changelog
|
||||
uses: loopwerk/tag-changelog@941366edb8920e2071eae0449031830984b9f26e # v1.3.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config_file: .github/tag-changelog-config.js
|
||||
|
||||
# Check if release already exists
|
||||
if gh release view "$VERSION" >/dev/null 2>&1; then
|
||||
printf '%s\n' "created=false" >> "$GITHUB_OUTPUT"
|
||||
printf '%s\n' "Release $VERSION already exists - skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create release with auto-generated changelog (also creates tag)
|
||||
gh release create "$VERSION" \
|
||||
--title "Release $VERSION" \
|
||||
--generate-notes \
|
||||
--target main
|
||||
|
||||
printf '%s\n' "created=true" >> "$GITHUB_OUTPUT"
|
||||
printf '%s\n' "Created release $VERSION"
|
||||
- name: Create release
|
||||
if: steps.daily-version.outputs.created
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: ${{ steps.daily-version.outputs.version }}
|
||||
name: Release ${{ steps.daily-version.outputs.version }}
|
||||
body: ${{ steps.changelog.outputs.changes }}
|
||||
allowUpdates: true
|
||||
|
||||
121
.github/workflows/pr-lint.yml
vendored
121
.github/workflows/pr-lint.yml
vendored
@@ -24,9 +24,17 @@ on:
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
# MegaLinter configuration - these override the action's defaults
|
||||
# Apply linter fixes configuration
|
||||
APPLY_FIXES: all
|
||||
APPLY_FIXES_EVENT: pull_request
|
||||
APPLY_FIXES_MODE: commit
|
||||
|
||||
# Disable linters that do not work or conflict
|
||||
DISABLE_LINTERS: REPOSITORY_DEVSKIM
|
||||
|
||||
# Additional settings
|
||||
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
GITHUB_TOKEN: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Report configuration
|
||||
REPORT_OUTPUT_FOLDER: megalinter-reports
|
||||
@@ -39,7 +47,6 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read # Required for private dependencies
|
||||
|
||||
jobs:
|
||||
megalinter:
|
||||
@@ -49,42 +56,124 @@ 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
|
||||
|
||||
- name: Run MegaLinter
|
||||
id: pr-lint
|
||||
uses: ./pr-lint
|
||||
- name: MegaLinter
|
||||
id: ml
|
||||
uses: oxsecurity/megalinter/flavors/cupcake@62c799d895af9bcbca5eacfebca29d527f125a57 # v9.1.0
|
||||
|
||||
- name: Check MegaLinter Results
|
||||
id: check-results
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
printf '%s\n' "status=success" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ -f "${{ env.REPORT_OUTPUT_FOLDER }}/megalinter.log" ]; then
|
||||
if grep -q "ERROR\|CRITICAL" "${{ env.REPORT_OUTPUT_FOLDER }}/megalinter.log"; then
|
||||
echo "Linting errors found"
|
||||
printf '%s\n' "status=failure" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "::warning::MegaLinter log file not found"
|
||||
fi
|
||||
|
||||
- name: Upload Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
username: fiximus
|
||||
email: github-bot@ivuorinen.net
|
||||
name: MegaLinter reports
|
||||
path: |
|
||||
megalinter-reports
|
||||
mega-linter.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload SARIF Report
|
||||
if: always() && hashFiles('megalinter-reports/sarif/*.sarif')
|
||||
uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
sarif_file: megalinter-reports/sarif
|
||||
category: megalinter
|
||||
|
||||
- name: Check Results
|
||||
- name: Prepare Git for Fixes
|
||||
if: steps.ml.outputs.has_updated_sources == 1
|
||||
shell: bash
|
||||
run: |
|
||||
sudo chown -Rc $UID .git/
|
||||
git config --global user.name "fiximus"
|
||||
git config --global user.email "github-bot@ivuorinen.net"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: |
|
||||
steps.ml.outputs.has_updated_sources == 1 &&
|
||||
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
|
||||
env.APPLY_FIXES_MODE == 'pull_request' &&
|
||||
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
||||
!contains(github.event.head_commit.message, 'skip fix')
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
commit-message: '[MegaLinter] Apply linters automatic fixes'
|
||||
title: '[MegaLinter] Apply linters automatic fixes'
|
||||
labels: bot
|
||||
branch: megalinter/fixes-${{ github.ref_name }}
|
||||
branch-suffix: timestamp
|
||||
delete-branch: true
|
||||
body: |
|
||||
## MegaLinter Fixes
|
||||
|
||||
MegaLinter has identified and fixed code style issues.
|
||||
|
||||
### 🔍 Changes Made
|
||||
- Automated code style fixes
|
||||
- Formatting improvements
|
||||
- Lint error corrections
|
||||
|
||||
### 📝 Notes
|
||||
- Please review the changes carefully
|
||||
- Run tests before merging
|
||||
- Verify formatting matches project standards
|
||||
|
||||
> Generated automatically by MegaLinter
|
||||
|
||||
- name: Commit Fixes
|
||||
if: |
|
||||
steps.ml.outputs.has_updated_sources == 1 &&
|
||||
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
|
||||
env.APPLY_FIXES_MODE == 'commit' &&
|
||||
github.ref != 'refs/heads/main' &&
|
||||
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
||||
!contains(github.event.head_commit.message, 'skip fix')
|
||||
uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
||||
commit_message: |
|
||||
style: apply MegaLinter fixes
|
||||
|
||||
[skip ci]
|
||||
commit_user_name: fiximus
|
||||
commit_user_email: github-bot@ivuorinen.net
|
||||
push_options: --force
|
||||
|
||||
- name: Create Status Check
|
||||
if: always()
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const status = '${{ steps.pr-lint.outputs.validation_status }}';
|
||||
const status = '${{ steps.check-results.outputs.status }}';
|
||||
const conclusion = status === 'success' ? 'success' : 'failure';
|
||||
|
||||
const summary = `## MegaLinter Results
|
||||
@@ -104,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
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e # v6-beta
|
||||
- uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
|
||||
57
.github/workflows/security-suite.yml
vendored
57
.github/workflows/security-suite.yml
vendored
@@ -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
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
# Record the base commit for diffing without checking it out
|
||||
# Keep PR head checked out so scanners analyze the new changes
|
||||
BASE_REF="refs/remotes/origin-base/${{ github.event.pull_request.base.ref }}"
|
||||
echo "BASE_REF=${BASE_REF}" >> "$GITHUB_ENV"
|
||||
echo "BASE_REF=${BASE_REF}" >> $GITHUB_ENV
|
||||
echo "Base ref: ${BASE_REF}"
|
||||
git log -1 --oneline "${BASE_REF}"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 🚀 Run stale
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
|
||||
2
.github/workflows/sync-labels.yml
vendored
2
.github/workflows/sync-labels.yml
vendored
@@ -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
|
||||
|
||||
54
.github/workflows/test-actions.yml
vendored
54
.github/workflows/test-actions.yml
vendored
@@ -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@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
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,18 +122,18 @@ 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"
|
||||
printf '%s\n' "reports-found=true" >> $GITHUB_OUTPUT
|
||||
echo "Integration test reports found"
|
||||
else
|
||||
printf '%s\n' "reports-found=false" >> "$GITHUB_OUTPUT"
|
||||
printf '%s\n' "reports-found=false" >> $GITHUB_OUTPUT
|
||||
echo "No integration test reports found"
|
||||
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
|
||||
|
||||
10
.github/workflows/version-maintenance.yml
vendored
10
.github/workflows/version-maintenance.yml
vendored
@@ -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@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
|
||||
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 }}'
|
||||
@@ -68,6 +68,8 @@ jobs:
|
||||
```bash
|
||||
make check-version-refs
|
||||
```
|
||||
|
||||
🤖 Auto-generated by version-maintenance workflow
|
||||
branch: automated/version-update-${{ steps.version.outputs.major }}
|
||||
delete-branch: true
|
||||
labels: |
|
||||
@@ -76,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Check for Annual Bump
|
||||
if: steps.action-versioning.outputs.needs-annual-bump == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const currentYear = new Date().getFullYear();
|
||||
@@ -118,6 +120,8 @@ jobs:
|
||||
\`\`\`bash
|
||||
make check-version-refs
|
||||
\`\`\`
|
||||
|
||||
🤖 Auto-generated by version-maintenance workflow
|
||||
`,
|
||||
labels: ['maintenance', 'high-priority']
|
||||
});
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
.worktrees/
|
||||
@@ -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)
|
||||
|
||||
@@ -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.17
|
||||
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.20.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.9
|
||||
rev: v0.14.2
|
||||
hooks:
|
||||
# Run the linter with auto-fix
|
||||
- id: ruff-check
|
||||
@@ -78,19 +78,24 @@ repos:
|
||||
exclude: '^_tests/.*\.sh$'
|
||||
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.9
|
||||
rev: v1.7.8
|
||||
hooks:
|
||||
- id: actionlint
|
||||
args: ['-shellcheck=']
|
||||
|
||||
- repo: https://github.com/renovatebot/pre-commit-hooks
|
||||
rev: 41.149.2
|
||||
hooks:
|
||||
- id: renovate-config-validator
|
||||
|
||||
- repo: https://github.com/bridgecrewio/checkov.git
|
||||
rev: '3.2.495'
|
||||
rev: '3.2.487'
|
||||
hooks:
|
||||
- id: checkov
|
||||
args:
|
||||
- '--quiet'
|
||||
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.30.0
|
||||
rev: v8.28.0
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
.github/renovate.json
|
||||
.venv
|
||||
.worktrees/
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.14.2
|
||||
3.14.0
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
- **Path**: /Users/ivuorinen/Code/ivuorinen/actions
|
||||
- **Branch**: main
|
||||
- **External Usage**: `ivuorinen/actions/<action-name>@main`
|
||||
- **Total Actions**: 44 self-contained actions
|
||||
- **Dogfooding**: Workflows use local actions (pr-lint, codeql-analysis, security-scan)
|
||||
- **Total Actions**: 43 self-contained actions
|
||||
|
||||
## Structure
|
||||
|
||||
```text
|
||||
/
|
||||
├── <action-dirs>/ # 44 self-contained actions
|
||||
├── <action-dirs>/ # 43 self-contained actions
|
||||
│ ├── action.yml # Action definition
|
||||
│ ├── README.md # Auto-generated
|
||||
│ └── CustomValidator.py # Optional validator
|
||||
@@ -26,14 +25,12 @@
|
||||
└── Makefile # Build automation
|
||||
```
|
||||
|
||||
## Action Categories (44 total)
|
||||
## Action Categories (43 total)
|
||||
|
||||
**Setup (7)**: node-setup, set-git-config, php-version-detect, python-version-detect, python-version-detect-v2, go-version-detect, dotnet-version-detect
|
||||
|
||||
**Linting (13)**: ansible-lint-fix, biome-check/fix, csharp-lint-check, eslint-check/fix, go-lint, pr-lint, pre-commit, prettier-check/fix, python-lint-fix, terraform-lint-fix
|
||||
|
||||
**Security (1)**: security-scan (actionlint, Gitleaks, Trivy scanning)
|
||||
|
||||
**Build (3)**: csharp-build, go-build, docker-build
|
||||
|
||||
**Publishing (5)**: npm-publish, docker-publish, docker-publish-gh, docker-publish-hub, csharp-publish
|
||||
@@ -88,28 +85,3 @@ make test # All tests (pytest + ShellSpec)
|
||||
- ✅ Convention-based validation
|
||||
- ✅ Test generation system
|
||||
- ✅ Full backward compatibility
|
||||
|
||||
## Dogfooding Strategy
|
||||
|
||||
The repository actively dogfoods its own actions in workflows:
|
||||
|
||||
**Fully Dogfooded Workflows**:
|
||||
|
||||
- **pr-lint.yml**: Uses `./pr-lint` (was 204 lines, now 112 lines - 45% reduction)
|
||||
- **action-security.yml**: Uses `./security-scan` (was 264 lines, now 82 lines - 69% reduction)
|
||||
- **codeql-new.yml**: Uses `./codeql-analysis`
|
||||
- **sync-labels.yml**: Uses `./sync-labels`
|
||||
- **version-maintenance.yml**: Uses `./action-versioning`
|
||||
|
||||
**Intentionally External**:
|
||||
|
||||
- **build-testing-image.yml**: Uses docker/\* actions directly (needs metadata extraction)
|
||||
- Core GitHub actions (checkout, upload-artifact, setup-\*) kept for standardization
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- Early detection of action issues
|
||||
- Real-world testing of actions
|
||||
- Reduced workflow duplication
|
||||
- Improved maintainability
|
||||
- Better documentation through usage examples
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
.venv
|
||||
.worktrees/
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
---
|
||||
extends: default
|
||||
|
||||
ignore: |
|
||||
node_modules/
|
||||
.worktrees/
|
||||
|
||||
rules:
|
||||
line-length:
|
||||
max: 200
|
||||
|
||||
12
CLAUDE.md
12
CLAUDE.md
@@ -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`.
|
||||
|
||||
**24 Actions**: Setup (language-version-detect), Utilities (action-versioning, version-file-parser),
|
||||
Linting (ansible-lint-fix, biome-lint, csharp-lint-check, eslint-lint, go-lint, pr-lint, pre-commit, prettier-lint, python-lint-fix, terraform-lint-fix),
|
||||
Testing (php-tests), Build (csharp-build, go-build, docker-build),
|
||||
Publishing (npm-publish, docker-publish, csharp-publish),
|
||||
Repository (release-monthly, sync-labels, stale, compress-images, codeql-analysis),
|
||||
**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, 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
|
||||
|
||||
61
Makefile
61
Makefile
@@ -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)"; \
|
||||
|
||||
332
README.md
332
README.md
@@ -22,174 +22,203 @@ Each action is fully self-contained and can be used independently in any GitHub
|
||||
|
||||
## 📚 Action Catalog
|
||||
|
||||
This repository contains **26 reusable GitHub Actions** for CI/CD automation.
|
||||
This repository contains **43 reusable GitHub Actions** for CI/CD automation.
|
||||
|
||||
### Quick Reference (26 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... | Caching, Token auth, Outputs |
|
||||
| ✅ | [`biome-lint`][biome-lint] | Linting | Run Biome linter in check or fix mode | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 🛡️ | [`codeql-analysis`][codeql-analysis] | Repository | Run CodeQL security analysis for a single language with conf... | Auto-detection, Token auth, Outputs |
|
||||
| 🖼️ | [`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. | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📝 | [`csharp-lint-check`][csharp-lint-check] | Linting | Runs linters like StyleCop or dotnet-format for C# code styl... | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📦 | [`csharp-publish`][csharp-publish] | Publishing | Publishes a C# project to GitHub Packages. | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📦 | [`docker-build`][docker-build] | Build | Builds a Docker image for multiple architectures with enhanc... | Caching, Auto-detection, Token auth, Outputs |
|
||||
| ☁️ | [`docker-publish`][docker-publish] | Publishing | Simple wrapper to publish Docker images to GitHub Packages a... | Token auth, Outputs |
|
||||
| ✅ | [`eslint-lint`][eslint-lint] | Linting | Run ESLint in check or fix mode with advanced configuration ... | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📦 | [`go-build`][go-build] | Build | Builds the Go project. | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📝 | [`go-lint`][go-lint] | Linting | Run golangci-lint with advanced configuration, caching, and ... | Caching, Token auth, Outputs |
|
||||
| 📝 | [`language-version-detect`][language-version-detect] | Setup | DEPRECATED: This action is deprecated. Inline version detect... | Auto-detection, Token auth, Outputs |
|
||||
| 📦 | [`npm-publish`][npm-publish] | Publishing | Publishes the package to the NPM registry with configurable ... | Caching, Auto-detection, Token auth, Outputs |
|
||||
| ✅ | [`php-tests`][php-tests] | Testing | Run PHPUnit tests with optional Laravel setup and Composer d... | Caching, Auto-detection, Token auth, Outputs |
|
||||
| ✅ | [`pr-lint`][pr-lint] | Linting | Runs MegaLinter against pull requests | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📦 | [`pre-commit`][pre-commit] | Linting | Runs pre-commit on the repository and pushes the fixes back ... | Auto-detection, Token auth, Outputs |
|
||||
| ✅ | [`prettier-lint`][prettier-lint] | Linting | Run Prettier in check or fix mode with advanced configuratio... | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📝 | [`python-lint-fix`][python-lint-fix] | Linting | Lints and fixes Python files, commits changes, and uploads S... | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📦 | [`release-monthly`][release-monthly] | Repository | Creates a release for the current month, incrementing patch ... | Token auth, Outputs |
|
||||
| 🛡️ | [`security-scan`][security-scan] | Security | Comprehensive security scanning for GitHub Actions including... | Caching, 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 |
|
||||
| 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 (1 action)
|
||||
#### 🔧 Setup (7 actions)
|
||||
|
||||
| Action | Description | Languages | Features |
|
||||
|:--------------------------------------------------------|:------------------------------------------------------|:-------------------------------|:------------------------------------|
|
||||
| 📝 [`language-version-detect`][language-version-detect] | DEPRECATED: This action is deprecated. Inline vers... | PHP, Python, Go, .NET, Node.js | Auto-detection, Token auth, Outputs |
|
||||
| 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 (1 action)
|
||||
#### 🛠️ Utilities (2 actions)
|
||||
|
||||
| Action | Description | Languages | Features |
|
||||
|:--------------------------------------------|:------------------------------------------------------|:---------------|:--------------------|
|
||||
| 🔀 [`action-versioning`][action-versioning] | Automatically update SHA-pinned action references ... | GitHub Actions | Token auth, 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 | Caching, Token auth, Outputs |
|
||||
| ✅ [`biome-lint`][biome-lint] | Run Biome linter in check or fix mode | JavaScript, TypeScript, JSON | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📝 [`csharp-lint-check`][csharp-lint-check] | Runs linters like StyleCop or dotnet-format for C#... | C#, .NET | Caching, Auto-detection, Token auth, Outputs |
|
||||
| ✅ [`eslint-lint`][eslint-lint] | Run ESLint in check or fix mode with advanced conf... | JavaScript, TypeScript | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📝 [`go-lint`][go-lint] | Run golangci-lint with advanced configuration, cac... | Go | Caching, Token auth, Outputs |
|
||||
| ✅ [`pr-lint`][pr-lint] | Runs MegaLinter against pull requests | Conventional Commits | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📦 [`pre-commit`][pre-commit] | Runs pre-commit on the repository and pushes the f... | Python, Multiple Languages | Auto-detection, Token auth, Outputs |
|
||||
| ✅ [`prettier-lint`][prettier-lint] | Run Prettier in check or fix mode with advanced co... | JavaScript, TypeScript, Markdown, YAML, JSON | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 📦 [`ansible-lint-fix`][ansible-lint-fix] | Lints and fixes Ansible playbooks, commits changes... | Ansible, YAML | Token auth, Outputs |
|
||||
| ✅ [`biome-check`][biome-check] | Run Biome check on the repository | JavaScript, TypeScript, JSON | Token auth, Outputs |
|
||||
| ✅ [`biome-fix`][biome-fix] | Run Biome fix on the repository | JavaScript, TypeScript, JSON | Token auth, Outputs |
|
||||
| 📝 [`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 |
|
||||
|
||||
#### 🧪 Testing (1 action)
|
||||
#### 🧪 Testing (3 actions)
|
||||
|
||||
| Action | Description | Languages | Features |
|
||||
|:---------------------------|:------------------------------------------------------|:-------------|:---------------------------------------------|
|
||||
| ✅ [`php-tests`][php-tests] | Run PHPUnit tests with optional Laravel setup and ... | PHP, Laravel | Caching, Auto-detection, Token auth, Outputs |
|
||||
| Action | Description | Languages | Features |
|
||||
|:------------------------------------------------|:------------------------------------------------------|:-------------|:------------------------------------|
|
||||
| 🖥️ [`php-composer`][php-composer] | Runs Composer install on a repository with advance... | PHP | Auto-detection, Token auth, Outputs |
|
||||
| 💻 [`php-laravel-phpunit`][php-laravel-phpunit] | Setup PHP, install dependencies, generate key, cre... | PHP, Laravel | Auto-detection, Token auth, Outputs |
|
||||
| ✅ [`php-tests`][php-tests] | Run PHPUnit tests on the repository | PHP | Token auth, Outputs |
|
||||
|
||||
#### 🏗️ Build (3 actions)
|
||||
|
||||
| Action | Description | Languages | Features |
|
||||
|:----------------------------------|:------------------------------------------------------|:----------|:---------------------------------------------|
|
||||
| 📝 [`csharp-build`][csharp-build] | Builds and tests C# projects. | C#, .NET | Caching, 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 | Caching, Auto-detection, Token auth, Outputs |
|
||||
| ☁️ [`docker-publish`][docker-publish] | Simple wrapper to publish Docker images to GitHub ... | Docker | Token auth, Outputs |
|
||||
| 📦 [`npm-publish`][npm-publish] | Publishes the package to the NPM registry with con... | Node.js, npm | Caching, Auto-detection, Token auth, Outputs |
|
||||
| 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 (5 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 |
|
||||
| 🖼️ [`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 |
|
||||
|
||||
#### 🛡️ Security (1 action)
|
||||
|
||||
| Action | Description | Languages | Features |
|
||||
|:-------------------------------------|:------------------------------------------------------|:----------|:-----------------------------|
|
||||
| 🛡️ [`security-scan`][security-scan] | Comprehensive security scanning for GitHub Actions... | - | Caching, 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] | - | ✅ | ✅ | ✅ |
|
||||
| [`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] | - | ✅ | ✅ | ✅ |
|
||||
| [`npm-publish`][npm-publish] | ✅ | ✅ | ✅ | ✅ |
|
||||
| [`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] | - | - | ✅ | ✅ |
|
||||
| [`security-scan`][security-scan] | ✅ | - | ✅ | ✅ |
|
||||
| [`stale`][stale] | - | - | ✅ | ✅ |
|
||||
| [`sync-labels`][sync-labels] | - | - | ✅ | ✅ |
|
||||
| [`terraform-lint-fix`][terraform-lint-fix] | - | - | ✅ | ✅ |
|
||||
| [`validate-inputs`][validate-inputs] | - | - | ✅ | ✅ |
|
||||
| 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] |
|
||||
| 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], [`prettier-lint`][prettier-lint] |
|
||||
| Laravel | [`php-tests`][php-tests] |
|
||||
| Markdown | [`prettier-lint`][prettier-lint] |
|
||||
| Multiple Languages | [`pre-commit`][pre-commit] |
|
||||
| Node.js | [`language-version-detect`][language-version-detect], [`npm-publish`][npm-publish] |
|
||||
| PHP | [`language-version-detect`][language-version-detect], [`php-tests`][php-tests] |
|
||||
| PNG | [`compress-images`][compress-images] |
|
||||
| Python | [`codeql-analysis`][codeql-analysis], [`language-version-detect`][language-version-detect], [`pre-commit`][pre-commit], [`python-lint-fix`][python-lint-fix] |
|
||||
| Ruby | [`codeql-analysis`][codeql-analysis] |
|
||||
| Terraform | [`terraform-lint-fix`][terraform-lint-fix] |
|
||||
| TypeScript | [`biome-lint`][biome-lint], [`codeql-analysis`][codeql-analysis], [`eslint-lint`][eslint-lint], [`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
|
||||
|
||||
@@ -197,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
|
||||
|
||||
@@ -211,32 +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
|
||||
[security-scan]: security-scan/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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ When security issues are fixed:
|
||||
- Replaced custom Bun installation with official action
|
||||
- Replaced custom Trivy installation with official action
|
||||
- Added secret masking to 7 critical actions (including docker-publish)
|
||||
- Migrated from custom common-cache to official actions/cache
|
||||
- Optimized file hashing in common-cache
|
||||
- Status: ✅ Complete
|
||||
|
||||
### Phase 3: Documentation & Policy (2024)
|
||||
|
||||
@@ -6,8 +6,8 @@ set -euo pipefail
|
||||
|
||||
# Source setup utilities
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
# shellcheck source=_tests/framework/setup.sh
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/setup.sh"
|
||||
|
||||
# Action testing utilities
|
||||
@@ -57,28 +57,6 @@ get_action_name() {
|
||||
uv run "$script_dir/../shared/validation_core.py" --name "$action_file"
|
||||
}
|
||||
|
||||
get_action_runs_using() {
|
||||
local action_file="$1"
|
||||
local script_dir
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
uv run "$script_dir/../shared/validation_core.py" --runs-using "$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"
|
||||
@@ -370,5 +348,5 @@ run_action_tests() {
|
||||
}
|
||||
|
||||
# Export all functions
|
||||
export -f validate_action_yml get_action_inputs get_action_outputs get_action_name get_action_runs_using 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
|
||||
|
||||
471
_tests/integration/workflows/common-cache-test.yml
Normal file
471
_tests/integration/workflows/common-cache-test.yml
Normal file
@@ -0,0 +1,471 @@
|
||||
---
|
||||
name: Integration Test - Common Cache
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'common-cache/**'
|
||||
- '_tests/integration/workflows/common-cache-test.yml'
|
||||
|
||||
jobs:
|
||||
test-common-cache-key-generation:
|
||||
name: Test Cache Key Generation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test basic key generation
|
||||
run: |
|
||||
RUNNER_OS="Linux"
|
||||
CACHE_TYPE="npm"
|
||||
KEY_PREFIX=""
|
||||
|
||||
cache_key="$RUNNER_OS"
|
||||
[ -n "$CACHE_TYPE" ] && cache_key="${cache_key}-${CACHE_TYPE}"
|
||||
|
||||
expected="Linux-npm"
|
||||
if [[ "$cache_key" != "$expected" ]]; then
|
||||
echo "❌ ERROR: Expected '$expected', got '$cache_key'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Basic cache key generation works"
|
||||
|
||||
- name: Test key with prefix
|
||||
run: |
|
||||
RUNNER_OS="Linux"
|
||||
CACHE_TYPE="npm"
|
||||
KEY_PREFIX="node-20"
|
||||
|
||||
cache_key="$RUNNER_OS"
|
||||
[ -n "$KEY_PREFIX" ] && cache_key="${cache_key}-${KEY_PREFIX}"
|
||||
[ -n "$CACHE_TYPE" ] && cache_key="${cache_key}-${CACHE_TYPE}"
|
||||
|
||||
expected="Linux-node-20-npm"
|
||||
if [[ "$cache_key" != "$expected" ]]; then
|
||||
echo "❌ ERROR: Expected '$expected', got '$cache_key'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Cache key with prefix works"
|
||||
|
||||
- name: Test OS-specific keys
|
||||
run: |
|
||||
for os in "Linux" "macOS" "Windows"; do
|
||||
CACHE_TYPE="test"
|
||||
cache_key="$os-$CACHE_TYPE"
|
||||
if [[ ! "$cache_key" =~ ^(Linux|macOS|Windows)-test$ ]]; then
|
||||
echo "❌ ERROR: Invalid key for OS $os: $cache_key"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ OS-specific key for $os: $cache_key"
|
||||
done
|
||||
|
||||
test-common-cache-file-hashing:
|
||||
name: Test File Hashing
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create test files
|
||||
run: |
|
||||
mkdir -p test-cache
|
||||
cd test-cache
|
||||
echo "content1" > file1.txt
|
||||
echo "content2" > file2.txt
|
||||
echo "content3" > file3.txt
|
||||
|
||||
- name: Test single file hash
|
||||
run: |
|
||||
cd test-cache
|
||||
file_hash=$(cat file1.txt | sha256sum | cut -d' ' -f1)
|
||||
|
||||
if [[ ! "$file_hash" =~ ^[a-f0-9]{64}$ ]]; then
|
||||
echo "❌ ERROR: Invalid hash format: $file_hash"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Single file hash: $file_hash"
|
||||
|
||||
- name: Test multiple file hash
|
||||
run: |
|
||||
cd test-cache
|
||||
multi_hash=$(cat file1.txt file2.txt file3.txt | sha256sum | cut -d' ' -f1)
|
||||
|
||||
if [[ ! "$multi_hash" =~ ^[a-f0-9]{64}$ ]]; then
|
||||
echo "❌ ERROR: Invalid hash format: $multi_hash"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Multiple file hash: $multi_hash"
|
||||
|
||||
- name: Test hash changes with content
|
||||
run: |
|
||||
cd test-cache
|
||||
|
||||
# Get initial hash
|
||||
hash1=$(cat file1.txt | sha256sum | cut -d' ' -f1)
|
||||
|
||||
# Modify file
|
||||
echo "modified" > file1.txt
|
||||
|
||||
# Get new hash
|
||||
hash2=$(cat file1.txt | sha256sum | cut -d' ' -f1)
|
||||
|
||||
if [[ "$hash1" == "$hash2" ]]; then
|
||||
echo "❌ ERROR: Hash should change when content changes"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Hash changes with content modification"
|
||||
|
||||
- name: Test comma-separated file list processing
|
||||
run: |
|
||||
cd test-cache
|
||||
|
||||
KEY_FILES="file1.txt,file2.txt,file3.txt"
|
||||
IFS=',' read -ra FILES <<< "$KEY_FILES"
|
||||
|
||||
existing_files=()
|
||||
for file in "${FILES[@]}"; do
|
||||
file=$(echo "$file" | xargs)
|
||||
if [ -f "$file" ]; then
|
||||
existing_files+=("$file")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#existing_files[@]} -ne 3 ]; then
|
||||
echo "❌ ERROR: Should find 3 files, found ${#existing_files[@]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Comma-separated file list processing works"
|
||||
|
||||
- name: Test missing file handling
|
||||
run: |
|
||||
cd test-cache
|
||||
|
||||
KEY_FILES="file1.txt,missing.txt,file2.txt"
|
||||
IFS=',' read -ra FILES <<< "$KEY_FILES"
|
||||
|
||||
existing_files=()
|
||||
for file in "${FILES[@]}"; do
|
||||
file=$(echo "$file" | xargs)
|
||||
if [ -f "$file" ]; then
|
||||
existing_files+=("$file")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#existing_files[@]} -ne 2 ]; then
|
||||
echo "❌ ERROR: Should find 2 files, found ${#existing_files[@]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Missing files correctly skipped"
|
||||
|
||||
test-common-cache-env-vars:
|
||||
name: Test Environment Variables
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test single env var inclusion
|
||||
run: |
|
||||
export NODE_VERSION="20.9.0"
|
||||
ENV_VARS="NODE_VERSION"
|
||||
|
||||
IFS=',' read -ra VARS <<< "$ENV_VARS"
|
||||
env_hash=""
|
||||
for var in "${VARS[@]}"; do
|
||||
if [ -n "${!var}" ]; then
|
||||
env_hash="${env_hash}-${var}-${!var}"
|
||||
fi
|
||||
done
|
||||
|
||||
expected="-NODE_VERSION-20.9.0"
|
||||
if [[ "$env_hash" != "$expected" ]]; then
|
||||
echo "❌ ERROR: Expected '$expected', got '$env_hash'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Single env var inclusion works"
|
||||
|
||||
- name: Test multiple env vars
|
||||
run: |
|
||||
export NODE_VERSION="20.9.0"
|
||||
export PACKAGE_MANAGER="npm"
|
||||
ENV_VARS="NODE_VERSION,PACKAGE_MANAGER"
|
||||
|
||||
IFS=',' read -ra VARS <<< "$ENV_VARS"
|
||||
env_hash=""
|
||||
for var in "${VARS[@]}"; do
|
||||
if [ -n "${!var}" ]; then
|
||||
env_hash="${env_hash}-${var}-${!var}"
|
||||
fi
|
||||
done
|
||||
|
||||
expected="-NODE_VERSION-20.9.0-PACKAGE_MANAGER-npm"
|
||||
if [[ "$env_hash" != "$expected" ]]; then
|
||||
echo "❌ ERROR: Expected '$expected', got '$env_hash'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Multiple env vars inclusion works"
|
||||
|
||||
- name: Test undefined env var skipping
|
||||
run: |
|
||||
export NODE_VERSION="20.9.0"
|
||||
ENV_VARS="NODE_VERSION,UNDEFINED_VAR"
|
||||
|
||||
IFS=',' read -ra VARS <<< "$ENV_VARS"
|
||||
env_hash=""
|
||||
for var in "${VARS[@]}"; do
|
||||
if [ -n "${!var}" ]; then
|
||||
env_hash="${env_hash}-${var}-${!var}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Should only include NODE_VERSION
|
||||
expected="-NODE_VERSION-20.9.0"
|
||||
if [[ "$env_hash" != "$expected" ]]; then
|
||||
echo "❌ ERROR: Expected '$expected', got '$env_hash'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Undefined env vars correctly skipped"
|
||||
|
||||
test-common-cache-path-processing:
|
||||
name: Test Path Processing
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test single path
|
||||
run: |
|
||||
CACHE_PATHS="~/.npm"
|
||||
IFS=',' read -ra PATHS <<< "$CACHE_PATHS"
|
||||
|
||||
if [ ${#PATHS[@]} -ne 1 ]; then
|
||||
echo "❌ ERROR: Should have 1 path, got ${#PATHS[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Single path processing works"
|
||||
|
||||
- name: Test multiple paths
|
||||
run: |
|
||||
CACHE_PATHS="~/.npm,~/.yarn/cache,node_modules"
|
||||
IFS=',' read -ra PATHS <<< "$CACHE_PATHS"
|
||||
|
||||
if [ ${#PATHS[@]} -ne 3 ]; then
|
||||
echo "❌ ERROR: Should have 3 paths, got ${#PATHS[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Multiple paths processing works"
|
||||
|
||||
- name: Test path with spaces (trimming)
|
||||
run: |
|
||||
CACHE_PATHS=" ~/.npm , ~/.yarn/cache , node_modules "
|
||||
IFS=',' read -ra PATHS <<< "$CACHE_PATHS"
|
||||
|
||||
trimmed_paths=()
|
||||
for path in "${PATHS[@]}"; do
|
||||
trimmed=$(echo "$path" | xargs)
|
||||
trimmed_paths+=("$trimmed")
|
||||
done
|
||||
|
||||
# Check first path is trimmed
|
||||
if [[ "${trimmed_paths[0]}" != "~/.npm" ]]; then
|
||||
echo "❌ ERROR: Path not trimmed: '${trimmed_paths[0]}'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Path trimming works"
|
||||
|
||||
test-common-cache-complete-key-generation:
|
||||
name: Test Complete Key Generation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create test files
|
||||
run: |
|
||||
mkdir -p test-complete
|
||||
cd test-complete
|
||||
echo "package-lock content" > package-lock.json
|
||||
|
||||
- name: Test complete cache key with all components
|
||||
run: |
|
||||
cd test-complete
|
||||
|
||||
RUNNER_OS="Linux"
|
||||
CACHE_TYPE="npm"
|
||||
KEY_PREFIX="node-20"
|
||||
|
||||
# Generate file hash
|
||||
files_hash=$(cat package-lock.json | sha256sum | cut -d' ' -f1)
|
||||
|
||||
# Generate env hash
|
||||
export NODE_VERSION="20.9.0"
|
||||
env_hash="-NODE_VERSION-20.9.0"
|
||||
|
||||
# Generate final key
|
||||
cache_key="$RUNNER_OS"
|
||||
[ -n "$KEY_PREFIX" ] && cache_key="${cache_key}-${KEY_PREFIX}"
|
||||
[ -n "$CACHE_TYPE" ] && cache_key="${cache_key}-${CACHE_TYPE}"
|
||||
[ -n "$files_hash" ] && cache_key="${cache_key}-${files_hash}"
|
||||
[ -n "$env_hash" ] && cache_key="${cache_key}${env_hash}"
|
||||
|
||||
echo "Generated cache key: $cache_key"
|
||||
|
||||
# Verify structure
|
||||
if [[ ! "$cache_key" =~ ^Linux-node-20-npm-[a-f0-9]{64}-NODE_VERSION-20\.9\.0$ ]]; then
|
||||
echo "❌ ERROR: Invalid cache key structure: $cache_key"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Complete cache key generation works"
|
||||
|
||||
test-common-cache-restore-keys:
|
||||
name: Test Restore Keys
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test single restore key
|
||||
run: |
|
||||
RESTORE_KEYS="Linux-npm-"
|
||||
|
||||
if [[ -z "$RESTORE_KEYS" ]]; then
|
||||
echo "❌ ERROR: Restore keys should not be empty"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Single restore key: $RESTORE_KEYS"
|
||||
|
||||
- name: Test multiple restore keys
|
||||
run: |
|
||||
RESTORE_KEYS="Linux-node-20-npm-,Linux-node-npm-,Linux-npm-"
|
||||
|
||||
IFS=',' read -ra KEYS <<< "$RESTORE_KEYS"
|
||||
if [ ${#KEYS[@]} -ne 3 ]; then
|
||||
echo "❌ ERROR: Should have 3 restore keys, got ${#KEYS[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Multiple restore keys work"
|
||||
|
||||
test-common-cache-type-specific-scenarios:
|
||||
name: Test Type-Specific Scenarios
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test NPM cache key
|
||||
run: |
|
||||
TYPE="npm"
|
||||
FILES="package-lock.json"
|
||||
PATHS="~/.npm,node_modules"
|
||||
|
||||
echo "✓ NPM cache configuration valid"
|
||||
echo " Type: $TYPE"
|
||||
echo " Key files: $FILES"
|
||||
echo " Paths: $PATHS"
|
||||
|
||||
- name: Test Composer cache key
|
||||
run: |
|
||||
TYPE="composer"
|
||||
FILES="composer.lock"
|
||||
PATHS="~/.composer/cache,vendor"
|
||||
|
||||
echo "✓ Composer cache configuration valid"
|
||||
echo " Type: $TYPE"
|
||||
echo " Key files: $FILES"
|
||||
echo " Paths: $PATHS"
|
||||
|
||||
- name: Test Go cache key
|
||||
run: |
|
||||
TYPE="go"
|
||||
FILES="go.sum"
|
||||
PATHS="~/go/pkg/mod,~/.cache/go-build"
|
||||
|
||||
echo "✓ Go cache configuration valid"
|
||||
echo " Type: $TYPE"
|
||||
echo " Key files: $FILES"
|
||||
echo " Paths: $PATHS"
|
||||
|
||||
- name: Test Pip cache key
|
||||
run: |
|
||||
TYPE="pip"
|
||||
FILES="requirements.txt"
|
||||
PATHS="~/.cache/pip"
|
||||
|
||||
echo "✓ Pip cache configuration valid"
|
||||
echo " Type: $TYPE"
|
||||
echo " Key files: $FILES"
|
||||
echo " Paths: $PATHS"
|
||||
|
||||
test-common-cache-edge-cases:
|
||||
name: Test Edge Cases
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test empty prefix
|
||||
run: |
|
||||
KEY_PREFIX=""
|
||||
cache_key="Linux"
|
||||
[ -n "$KEY_PREFIX" ] && cache_key="${cache_key}-${KEY_PREFIX}"
|
||||
|
||||
if [[ "$cache_key" != "Linux" ]]; then
|
||||
echo "❌ ERROR: Empty prefix should not modify key"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Empty prefix handling works"
|
||||
|
||||
- name: Test no key files
|
||||
run: |
|
||||
KEY_FILES=""
|
||||
files_hash=""
|
||||
|
||||
if [ -n "$KEY_FILES" ]; then
|
||||
echo "❌ ERROR: Should detect empty key files"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ No key files handling works"
|
||||
|
||||
- name: Test no env vars
|
||||
run: |
|
||||
ENV_VARS=""
|
||||
env_hash=""
|
||||
|
||||
if [ -n "$ENV_VARS" ]; then
|
||||
echo "❌ ERROR: Should detect empty env vars"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ No env vars handling works"
|
||||
|
||||
integration-test-summary:
|
||||
name: Integration Test Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test-common-cache-key-generation
|
||||
- test-common-cache-file-hashing
|
||||
- test-common-cache-env-vars
|
||||
- test-common-cache-path-processing
|
||||
- test-common-cache-complete-key-generation
|
||||
- test-common-cache-restore-keys
|
||||
- test-common-cache-type-specific-scenarios
|
||||
- test-common-cache-edge-cases
|
||||
steps:
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "Common Cache Integration Tests - PASSED"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "✓ Cache key generation tests"
|
||||
echo "✓ File hashing tests"
|
||||
echo "✓ Environment variable tests"
|
||||
echo "✓ Path processing tests"
|
||||
echo "✓ Complete key generation tests"
|
||||
echo "✓ Restore keys tests"
|
||||
echo "✓ Type-specific scenario tests"
|
||||
echo "✓ Edge case tests"
|
||||
echo ""
|
||||
echo "All common-cache integration tests completed successfully!"
|
||||
440
_tests/integration/workflows/github-release-test.yml
Normal file
440
_tests/integration/workflows/github-release-test.yml
Normal 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!"
|
||||
@@ -4,9 +4,12 @@ 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'
|
||||
|
||||
jobs:
|
||||
@@ -61,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 }}"
|
||||
@@ -84,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
|
||||
@@ -155,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
|
||||
|
||||
@@ -174,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"
|
||||
|
||||
@@ -262,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'
|
||||
@@ -288,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'
|
||||
|
||||
513
_tests/integration/workflows/node-setup-test.yml
Normal file
513
_tests/integration/workflows/node-setup-test.yml
Normal file
@@ -0,0 +1,513 @@
|
||||
---
|
||||
name: Integration Test - Node Setup
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'node-setup/**'
|
||||
- 'version-file-parser/**'
|
||||
- 'common-cache/**'
|
||||
- 'common-retry/**'
|
||||
- '_tests/integration/workflows/node-setup-test.yml'
|
||||
|
||||
jobs:
|
||||
test-node-setup-version-validation:
|
||||
name: Test Version Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test invalid default version format (alphabetic)
|
||||
run: |
|
||||
VERSION="abc"
|
||||
if [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then
|
||||
echo "❌ ERROR: Should reject alphabetic version"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Alphabetic version correctly rejected"
|
||||
|
||||
- name: Test invalid default version (too low)
|
||||
run: |
|
||||
VERSION="10"
|
||||
major=$(echo "$VERSION" | cut -d'.' -f1)
|
||||
if [ "$major" -lt 14 ] || [ "$major" -gt 30 ]; then
|
||||
echo "✓ Version $VERSION correctly rejected (major < 14)"
|
||||
else
|
||||
echo "❌ ERROR: Should reject Node.js $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test invalid default version (too high)
|
||||
run: |
|
||||
VERSION="50"
|
||||
major=$(echo "$VERSION" | cut -d'.' -f1)
|
||||
if [ "$major" -lt 14 ] || [ "$major" -gt 30 ]; then
|
||||
echo "✓ Version $VERSION correctly rejected (major > 30)"
|
||||
else
|
||||
echo "❌ ERROR: Should reject Node.js $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test valid version formats
|
||||
run: |
|
||||
for version in "20" "20.9" "20.9.0" "18" "22.1.0"; do
|
||||
if [[ "$version" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then
|
||||
major=$(echo "$version" | cut -d'.' -f1)
|
||||
if [ "$major" -ge 14 ] && [ "$major" -le 30 ]; then
|
||||
echo "✓ Version $version accepted"
|
||||
else
|
||||
echo "❌ ERROR: Version $version should be accepted"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ ERROR: Version $version format validation failed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
test-node-setup-package-manager-validation:
|
||||
name: Test Package Manager Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test valid package managers
|
||||
run: |
|
||||
for pm in "npm" "yarn" "pnpm" "bun" "auto"; do
|
||||
case "$pm" in
|
||||
"npm"|"yarn"|"pnpm"|"bun"|"auto")
|
||||
echo "✓ Package manager $pm accepted"
|
||||
;;
|
||||
*)
|
||||
echo "❌ ERROR: Valid package manager $pm rejected"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
- name: Test invalid package manager
|
||||
run: |
|
||||
PM="invalid-pm"
|
||||
case "$PM" in
|
||||
"npm"|"yarn"|"pnpm"|"bun"|"auto")
|
||||
echo "❌ ERROR: Invalid package manager should be rejected"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "✓ Invalid package manager correctly rejected"
|
||||
;;
|
||||
esac
|
||||
|
||||
test-node-setup-url-validation:
|
||||
name: Test URL Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test valid registry URLs
|
||||
run: |
|
||||
for url in "https://registry.npmjs.org" "http://localhost:4873" "https://npm.custom.com/"; do
|
||||
if [[ "$url" == "https://"* ]] || [[ "$url" == "http://"* ]]; then
|
||||
echo "✓ Registry URL $url accepted"
|
||||
else
|
||||
echo "❌ ERROR: Valid URL $url rejected"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Test invalid registry URLs
|
||||
run: |
|
||||
for url in "ftp://registry.com" "not-a-url" "registry.com"; do
|
||||
if [[ "$url" == "https://"* ]] || [[ "$url" == "http://"* ]]; then
|
||||
echo "❌ ERROR: Invalid URL $url should be rejected"
|
||||
exit 1
|
||||
else
|
||||
echo "✓ Invalid URL $url correctly rejected"
|
||||
fi
|
||||
done
|
||||
|
||||
test-node-setup-retries-validation:
|
||||
name: Test Retries Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test valid retry counts
|
||||
run: |
|
||||
for retries in "1" "3" "5" "10"; do
|
||||
if [[ "$retries" =~ ^[0-9]+$ ]] && [ "$retries" -gt 0 ] && [ "$retries" -le 10 ]; then
|
||||
echo "✓ Max retries $retries accepted"
|
||||
else
|
||||
echo "❌ ERROR: Valid retry count $retries rejected"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Test invalid retry counts
|
||||
run: |
|
||||
for retries in "0" "11" "abc" "-1"; do
|
||||
if [[ "$retries" =~ ^[0-9]+$ ]] && [ "$retries" -gt 0 ] && [ "$retries" -le 10 ]; then
|
||||
echo "❌ ERROR: Invalid retry count $retries should be rejected"
|
||||
exit 1
|
||||
else
|
||||
echo "✓ Invalid retry count $retries correctly rejected"
|
||||
fi
|
||||
done
|
||||
|
||||
test-node-setup-boolean-validation:
|
||||
name: Test Boolean Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test valid boolean values
|
||||
run: |
|
||||
for value in "true" "false"; do
|
||||
if [[ "$value" == "true" ]] || [[ "$value" == "false" ]]; then
|
||||
echo "✓ Boolean value $value accepted"
|
||||
else
|
||||
echo "❌ ERROR: Valid boolean $value rejected"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Test invalid boolean values
|
||||
run: |
|
||||
for value in "yes" "no" "1" "0" "True" "FALSE" ""; do
|
||||
if [[ "$value" != "true" ]] && [[ "$value" != "false" ]]; then
|
||||
echo "✓ Invalid boolean value '$value' correctly rejected"
|
||||
else
|
||||
echo "❌ ERROR: Invalid boolean $value should be rejected"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
test-node-setup-token-validation:
|
||||
name: Test Auth Token Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test injection pattern detection
|
||||
run: |
|
||||
for token in "token;malicious" "token&&command" "token|pipe"; do
|
||||
if [[ "$token" == *";"* ]] || [[ "$token" == *"&&"* ]] || [[ "$token" == *"|"* ]]; then
|
||||
echo "✓ Injection pattern in token correctly detected"
|
||||
else
|
||||
echo "❌ ERROR: Should detect injection pattern in: $token"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Test valid tokens
|
||||
run: |
|
||||
for token in "npm_AbCdEf1234567890" "github_pat_12345abcdef" "simple-token"; do
|
||||
if [[ "$token" == *";"* ]] || [[ "$token" == *"&&"* ]] || [[ "$token" == *"|"* ]]; then
|
||||
echo "❌ ERROR: Valid token should not be rejected: $token"
|
||||
exit 1
|
||||
else
|
||||
echo "✓ Valid token accepted"
|
||||
fi
|
||||
done
|
||||
|
||||
test-node-setup-package-manager-resolution:
|
||||
name: Test Package Manager Resolution
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test auto detection with detected PM
|
||||
run: |
|
||||
INPUT_PM="auto"
|
||||
DETECTED_PM="pnpm"
|
||||
|
||||
if [ "$INPUT_PM" = "auto" ]; then
|
||||
if [ -n "$DETECTED_PM" ]; then
|
||||
FINAL_PM="$DETECTED_PM"
|
||||
else
|
||||
FINAL_PM="npm"
|
||||
fi
|
||||
else
|
||||
FINAL_PM="$INPUT_PM"
|
||||
fi
|
||||
|
||||
if [[ "$FINAL_PM" != "pnpm" ]]; then
|
||||
echo "❌ ERROR: Should use detected PM (pnpm)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Auto-detected package manager correctly resolved"
|
||||
|
||||
- name: Test auto detection without detected PM
|
||||
run: |
|
||||
INPUT_PM="auto"
|
||||
DETECTED_PM=""
|
||||
|
||||
if [ "$INPUT_PM" = "auto" ]; then
|
||||
if [ -n "$DETECTED_PM" ]; then
|
||||
FINAL_PM="$DETECTED_PM"
|
||||
else
|
||||
FINAL_PM="npm"
|
||||
fi
|
||||
else
|
||||
FINAL_PM="$INPUT_PM"
|
||||
fi
|
||||
|
||||
if [[ "$FINAL_PM" != "npm" ]]; then
|
||||
echo "❌ ERROR: Should default to npm"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Defaults to npm when no detection"
|
||||
|
||||
- name: Test explicit package manager
|
||||
run: |
|
||||
INPUT_PM="yarn"
|
||||
DETECTED_PM="pnpm"
|
||||
|
||||
if [ "$INPUT_PM" = "auto" ]; then
|
||||
if [ -n "$DETECTED_PM" ]; then
|
||||
FINAL_PM="$DETECTED_PM"
|
||||
else
|
||||
FINAL_PM="npm"
|
||||
fi
|
||||
else
|
||||
FINAL_PM="$INPUT_PM"
|
||||
fi
|
||||
|
||||
if [[ "$FINAL_PM" != "yarn" ]]; then
|
||||
echo "❌ ERROR: Should use explicit PM (yarn)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Explicit package manager correctly used"
|
||||
|
||||
test-node-setup-feature-detection:
|
||||
name: Test Feature Detection
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create test package.json with ESM
|
||||
run: |
|
||||
mkdir -p test-esm
|
||||
cd test-esm
|
||||
cat > package.json <<'EOF'
|
||||
{
|
||||
"name": "test-esm",
|
||||
"version": "1.0.0",
|
||||
"type": "module"
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Test ESM detection
|
||||
run: |
|
||||
cd test-esm
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
pkg_type=$(jq -r '.type // "commonjs"' package.json 2>/dev/null)
|
||||
if [[ "$pkg_type" == "module" ]]; then
|
||||
echo "✓ ESM support correctly detected"
|
||||
else
|
||||
echo "❌ ERROR: Should detect ESM support"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "⚠️ jq not available, skipping ESM detection test"
|
||||
echo "✓ ESM detection logic verified (jq would be required in actual action)"
|
||||
fi
|
||||
|
||||
- name: Create test with TypeScript
|
||||
run: |
|
||||
mkdir -p test-ts
|
||||
cd test-ts
|
||||
touch tsconfig.json
|
||||
cat > package.json <<'EOF'
|
||||
{
|
||||
"name": "test-ts",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Test TypeScript detection
|
||||
run: |
|
||||
cd test-ts
|
||||
typescript_support="false"
|
||||
if [ -f tsconfig.json ]; then
|
||||
typescript_support="true"
|
||||
fi
|
||||
if [[ "$typescript_support" != "true" ]]; then
|
||||
echo "❌ ERROR: Should detect TypeScript"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ TypeScript support correctly detected"
|
||||
|
||||
- name: Create test with frameworks
|
||||
run: |
|
||||
mkdir -p test-frameworks
|
||||
cd test-frameworks
|
||||
cat > package.json <<'EOF'
|
||||
{
|
||||
"name": "test-frameworks",
|
||||
"dependencies": {
|
||||
"react": "^18.0.0",
|
||||
"next": "^14.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Test framework detection
|
||||
run: |
|
||||
cd test-frameworks
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
has_next=$(jq -e '.dependencies.next or .devDependencies.next' package.json >/dev/null 2>&1 && echo "yes" || echo "no")
|
||||
has_react=$(jq -e '.dependencies.react or .devDependencies.react' package.json >/dev/null 2>&1 && echo "yes" || echo "no")
|
||||
|
||||
if [[ "$has_next" == "yes" ]] && [[ "$has_react" == "yes" ]]; then
|
||||
echo "✓ Frameworks (Next.js, React) correctly detected"
|
||||
else
|
||||
echo "❌ ERROR: Should detect Next.js and React"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "⚠️ jq not available, skipping framework detection test"
|
||||
echo "✓ Framework detection logic verified (jq would be required in actual action)"
|
||||
fi
|
||||
|
||||
test-node-setup-security:
|
||||
name: Test Security Measures
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test token sanitization
|
||||
run: |
|
||||
TOKEN="test-token
|
||||
with-newline"
|
||||
|
||||
# Should remove newlines
|
||||
sanitized=$(echo "$TOKEN" | tr -d '\n\r')
|
||||
|
||||
if [[ "$sanitized" == *$'\n'* ]] || [[ "$sanitized" == *$'\r'* ]]; then
|
||||
echo "❌ ERROR: Newlines not removed"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Token sanitization works correctly"
|
||||
|
||||
- name: Test package manager sanitization
|
||||
run: |
|
||||
PM="npm
|
||||
with-newline"
|
||||
|
||||
# Should remove newlines
|
||||
sanitized=$(echo "$PM" | tr -d '\n\r')
|
||||
|
||||
if [[ "$sanitized" == *$'\n'* ]] || [[ "$sanitized" == *$'\r'* ]]; then
|
||||
echo "❌ ERROR: Newlines not removed from PM"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Package manager sanitization works correctly"
|
||||
|
||||
test-node-setup-integration-workflow:
|
||||
name: Test Integration Workflow
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Simulate complete workflow
|
||||
run: |
|
||||
echo "=== Simulating Node Setup Workflow ==="
|
||||
|
||||
# 1. Validation
|
||||
echo "Step 1: Validate inputs"
|
||||
DEFAULT_VERSION="20"
|
||||
PACKAGE_MANAGER="npm"
|
||||
REGISTRY_URL="https://registry.npmjs.org"
|
||||
CACHE="true"
|
||||
INSTALL="true"
|
||||
MAX_RETRIES="3"
|
||||
echo "✓ Inputs validated"
|
||||
|
||||
# 2. Version parsing
|
||||
echo "Step 2: Parse Node.js version"
|
||||
NODE_VERSION="20.9.0"
|
||||
echo "✓ Version parsed: $NODE_VERSION"
|
||||
|
||||
# 3. Package manager resolution
|
||||
echo "Step 3: Resolve package manager"
|
||||
if [ "$PACKAGE_MANAGER" = "auto" ]; then
|
||||
FINAL_PM="npm"
|
||||
else
|
||||
FINAL_PM="$PACKAGE_MANAGER"
|
||||
fi
|
||||
echo "✓ Package manager resolved: $FINAL_PM"
|
||||
|
||||
# 4. Setup Node.js
|
||||
echo "Step 4: Setup Node.js $NODE_VERSION"
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
echo "✓ Node.js available: $(node --version)"
|
||||
fi
|
||||
|
||||
# 5. Enable Corepack
|
||||
echo "Step 5: Enable Corepack"
|
||||
if command -v corepack >/dev/null 2>&1; then
|
||||
echo "✓ Corepack available"
|
||||
else
|
||||
echo "⚠️ Corepack not available in test environment"
|
||||
fi
|
||||
|
||||
# 6. Cache dependencies
|
||||
if [[ "$CACHE" == "true" ]]; then
|
||||
echo "Step 6: Cache dependencies"
|
||||
echo "✓ Would use common-cache action"
|
||||
fi
|
||||
|
||||
# 7. Install dependencies
|
||||
if [[ "$INSTALL" == "true" ]]; then
|
||||
echo "Step 7: Install dependencies"
|
||||
echo "✓ Would run: $FINAL_PM install"
|
||||
fi
|
||||
|
||||
echo "=== Workflow simulation completed ==="
|
||||
|
||||
integration-test-summary:
|
||||
name: Integration Test Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test-node-setup-version-validation
|
||||
- test-node-setup-package-manager-validation
|
||||
- test-node-setup-url-validation
|
||||
- test-node-setup-retries-validation
|
||||
- test-node-setup-boolean-validation
|
||||
- test-node-setup-token-validation
|
||||
- test-node-setup-package-manager-resolution
|
||||
- test-node-setup-feature-detection
|
||||
- test-node-setup-security
|
||||
- test-node-setup-integration-workflow
|
||||
steps:
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "Node Setup Integration Tests - PASSED"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "✓ Version validation tests"
|
||||
echo "✓ Package manager validation tests"
|
||||
echo "✓ URL validation tests"
|
||||
echo "✓ Retries validation tests"
|
||||
echo "✓ Boolean validation tests"
|
||||
echo "✓ Token validation tests"
|
||||
echo "✓ Package manager resolution tests"
|
||||
echo "✓ Feature detection tests"
|
||||
echo "✓ Security measure tests"
|
||||
echo "✓ Integration workflow tests"
|
||||
echo ""
|
||||
echo "All node-setup integration tests completed successfully!"
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
push:
|
||||
paths:
|
||||
- 'pre-commit/**'
|
||||
- 'set-git-config/**'
|
||||
- 'validate-inputs/**'
|
||||
- '_tests/integration/workflows/pre-commit-test.yml'
|
||||
|
||||
|
||||
241
_tests/integration/workflows/version-file-parser-test.yml
Normal file
241
_tests/integration/workflows/version-file-parser-test.yml
Normal file
@@ -0,0 +1,241 @@
|
||||
---
|
||||
name: Test version-file-parser Integration
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'version-file-parser/**'
|
||||
- '_tests/integration/workflows/version-file-parser-test.yml'
|
||||
|
||||
jobs:
|
||||
test-version-file-parser:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
test-case:
|
||||
- name: 'Node.js project'
|
||||
language: 'node'
|
||||
tool-versions-key: 'nodejs'
|
||||
dockerfile-image: 'node'
|
||||
expected-version: '18.0.0'
|
||||
setup-files: |
|
||||
echo "18.17.0" > .nvmrc
|
||||
cat > package.json <<EOF
|
||||
{
|
||||
"name": "test-project",
|
||||
"engines": { "node": ">=18.0.0" }
|
||||
}
|
||||
EOF
|
||||
touch package-lock.json
|
||||
|
||||
- name: 'PHP project'
|
||||
language: 'php'
|
||||
tool-versions-key: 'php'
|
||||
dockerfile-image: 'php'
|
||||
expected-version: '8.1'
|
||||
setup-files: |
|
||||
cat > composer.json <<EOF
|
||||
{
|
||||
"require": { "php": "^8.1" }
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: 'Python project'
|
||||
language: 'python'
|
||||
tool-versions-key: 'python'
|
||||
dockerfile-image: 'python'
|
||||
expected-version: '3.9'
|
||||
setup-files: |
|
||||
echo "3.9.0" > .python-version
|
||||
cat > pyproject.toml <<EOF
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
EOF
|
||||
|
||||
- name: 'Go project'
|
||||
language: 'go'
|
||||
tool-versions-key: 'golang'
|
||||
dockerfile-image: 'golang'
|
||||
expected-version: '1.21'
|
||||
setup-files: |
|
||||
cat > go.mod <<EOF
|
||||
module test-project
|
||||
go 1.21
|
||||
EOF
|
||||
|
||||
- name: '.tool-versions file'
|
||||
language: 'node'
|
||||
tool-versions-key: 'nodejs'
|
||||
dockerfile-image: 'node'
|
||||
expected-version: '18.16.0'
|
||||
setup-files: |
|
||||
echo "nodejs 18.16.0" > .tool-versions
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clean up test files from previous runs
|
||||
run: |
|
||||
rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions
|
||||
|
||||
- name: Setup test files
|
||||
run: ${{ matrix.test-case.setup-files }}
|
||||
|
||||
- name: Test version-file-parser
|
||||
id: test-action
|
||||
uses: ./version-file-parser
|
||||
with:
|
||||
language: ${{ matrix.test-case.language }}
|
||||
tool-versions-key: ${{ matrix.test-case.tool-versions-key }}
|
||||
dockerfile-image: ${{ matrix.test-case.dockerfile-image }}
|
||||
default-version: '1.0.0'
|
||||
|
||||
- name: Validate outputs
|
||||
run: |
|
||||
echo "Test case: ${{ matrix.test-case.name }}"
|
||||
echo "Expected version: ${{ matrix.test-case.expected-version }}"
|
||||
echo "Detected version: ${{ steps.test-action.outputs.detected-version }}"
|
||||
echo "Package manager: ${{ steps.test-action.outputs.package-manager }}"
|
||||
|
||||
# Validate that we got some version
|
||||
if [[ -z "${{ steps.test-action.outputs.detected-version }}" ]]; then
|
||||
echo "❌ ERROR: No version detected"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate version format (basic semver check)
|
||||
if ! echo "${{ steps.test-action.outputs.detected-version }}" | grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?'; then
|
||||
echo "❌ ERROR: Invalid version format: ${{ steps.test-action.outputs.detected-version }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate detected version matches expected version (not the fallback)
|
||||
if [[ "${{ steps.test-action.outputs.detected-version }}" != "${{ matrix.test-case.expected-version }}" ]]; then
|
||||
echo "❌ ERROR: Version mismatch"
|
||||
echo "Expected: ${{ matrix.test-case.expected-version }}"
|
||||
echo "Got: ${{ steps.test-action.outputs.detected-version }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Version validation passed"
|
||||
|
||||
# Skip external reference test in local/CI environment to avoid auth issues
|
||||
- name: Test external reference (info only)
|
||||
run: |
|
||||
echo "External reference test would use: ivuorinen/actions/version-file-parser@main"
|
||||
echo "Skipping to avoid authentication issues in local testing"
|
||||
|
||||
test-edge-cases:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clean up test files from previous runs
|
||||
run: |
|
||||
rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions
|
||||
|
||||
- name: Setup test files (package.json engines)
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
cat > package.json <<'EOF'
|
||||
{
|
||||
"name": "edge-case",
|
||||
"engines": { "node": ">=18.0.0" }
|
||||
}
|
||||
EOF
|
||||
echo "18.17.0" > .nvmrc
|
||||
|
||||
- name: Test version detection from existing files
|
||||
id: existing-version
|
||||
uses: ./version-file-parser
|
||||
with:
|
||||
language: 'node'
|
||||
tool-versions-key: 'nodejs'
|
||||
dockerfile-image: 'node'
|
||||
default-version: '20.0.0'
|
||||
|
||||
- name: Validate existing version detection
|
||||
run: |
|
||||
# The action detects Node.js version from package.json engines field
|
||||
# package.json >=18.0.0 is parsed as 18.0.0
|
||||
# Note: .nvmrc exists but package.json takes precedence in this implementation
|
||||
expected_version="18.0.0"
|
||||
detected_version="${{ steps.existing-version.outputs.detected-version }}"
|
||||
|
||||
if [[ "$detected_version" != "$expected_version" ]]; then
|
||||
echo "❌ ERROR: Version mismatch"
|
||||
echo "Expected: $expected_version"
|
||||
echo "Got: $detected_version"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Existing version detection works correctly"
|
||||
|
||||
- name: Clean up before invalid regex test
|
||||
run: |
|
||||
rm -f .nvmrc package.json package-lock.json
|
||||
|
||||
- name: Test with invalid regex
|
||||
id: invalid-regex
|
||||
uses: ./version-file-parser
|
||||
with:
|
||||
language: 'node'
|
||||
tool-versions-key: 'nodejs'
|
||||
dockerfile-image: 'node'
|
||||
validation-regex: 'invalid[regex'
|
||||
default-version: '18.0.0'
|
||||
continue-on-error: true
|
||||
|
||||
- name: Validate regex error handling
|
||||
run: |
|
||||
echo "Testing regex error handling completed"
|
||||
# Action should handle invalid regex gracefully
|
||||
if [ "${{ steps.invalid-regex.outcome }}" != "failure" ]; then
|
||||
echo "::error::Expected invalid-regex step to fail, but it was: ${{ steps.invalid-regex.outcome }}"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Invalid regex properly failed as expected"
|
||||
|
||||
test-dockerfile-parsing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clean up test files from previous runs
|
||||
run: |
|
||||
rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions Dockerfile
|
||||
|
||||
- name: Create Dockerfile with Node.js
|
||||
run: |
|
||||
cat > Dockerfile <<EOF
|
||||
FROM node:18.17.0-alpine
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
EOF
|
||||
|
||||
- name: Test Dockerfile parsing
|
||||
id: dockerfile-test
|
||||
uses: ./version-file-parser
|
||||
with:
|
||||
language: 'node'
|
||||
tool-versions-key: 'nodejs'
|
||||
dockerfile-image: 'node'
|
||||
|
||||
- name: Validate Dockerfile parsing
|
||||
run: |
|
||||
expected_version="18.17.0"
|
||||
detected_version="${{ steps.dockerfile-test.outputs.dockerfile-version }}"
|
||||
|
||||
echo "Expected version: $expected_version"
|
||||
echo "Detected version: $detected_version"
|
||||
|
||||
if [[ "$detected_version" != "$expected_version" ]]; then
|
||||
echo "❌ ERROR: Version mismatch"
|
||||
echo "Expected: $expected_version"
|
||||
echo "Got: $detected_version"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Dockerfile parsing successful"
|
||||
@@ -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
|
||||
|
||||
@@ -521,16 +521,6 @@ class ActionFileParser:
|
||||
except (OSError, ValueError, yaml.YAMLError, AttributeError):
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_action_runs_using(action_file: str) -> str:
|
||||
"""Get the runs.using value from an action.yml file."""
|
||||
try:
|
||||
data = ActionFileParser.load_action_file(action_file)
|
||||
runs = data.get("runs", {})
|
||||
return runs.get("using", "unknown")
|
||||
except (OSError, ValueError, yaml.YAMLError, AttributeError):
|
||||
return "unknown"
|
||||
|
||||
@staticmethod
|
||||
def _get_required_property(input_data: dict, property_name: str) -> str:
|
||||
"""Get the required/optional property."""
|
||||
@@ -797,11 +787,6 @@ Examples:
|
||||
mode_group.add_argument("--inputs", metavar="ACTION_FILE", help="List action inputs")
|
||||
mode_group.add_argument("--outputs", metavar="ACTION_FILE", help="List action outputs")
|
||||
mode_group.add_argument("--name", metavar="ACTION_FILE", help="Get action name")
|
||||
mode_group.add_argument(
|
||||
"--runs-using",
|
||||
metavar="ACTION_FILE",
|
||||
help="Get action runs.using value",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--validate-yaml",
|
||||
metavar="YAML_FILE",
|
||||
@@ -849,12 +834,6 @@ def _handle_name_command(args):
|
||||
print(name)
|
||||
|
||||
|
||||
def _handle_runs_using_command(args):
|
||||
"""Handle the runs-using command."""
|
||||
runs_using = ActionFileParser.get_action_runs_using(args.runs_using)
|
||||
print(runs_using)
|
||||
|
||||
|
||||
def _handle_validate_yaml_command(args):
|
||||
"""Handle the validate-yaml command."""
|
||||
try:
|
||||
@@ -874,7 +853,6 @@ def _execute_command(args):
|
||||
"inputs": _handle_inputs_command,
|
||||
"outputs": _handle_outputs_command,
|
||||
"name": _handle_name_command,
|
||||
"runs_using": _handle_runs_using_command,
|
||||
"validate_yaml": _handle_validate_yaml_command,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
149
_tests/unit/biome-check/validation.spec.sh
Executable file
149
_tests/unit/biome-check/validation.spec.sh
Executable 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
|
||||
148
_tests/unit/biome-fix/validation.spec.sh
Executable file
148
_tests/unit/biome-fix/validation.spec.sh
Executable 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
|
||||
@@ -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
|
||||
168
_tests/unit/common-cache/validation.spec.sh
Executable file
168
_tests/unit/common-cache/validation.spec.sh
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env shellspec
|
||||
# Unit tests for common-cache action validation and logic
|
||||
|
||||
# Framework is automatically loaded via spec_helper.sh
|
||||
|
||||
Describe "common-cache action"
|
||||
ACTION_DIR="common-cache"
|
||||
ACTION_FILE="$ACTION_DIR/action.yml"
|
||||
|
||||
Context "when validating cache type input"
|
||||
It "accepts npm cache type"
|
||||
When call validate_input_python "common-cache" "type" "npm"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts composer cache type"
|
||||
When call validate_input_python "common-cache" "type" "composer"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts go cache type"
|
||||
When call validate_input_python "common-cache" "type" "go"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts pip cache type"
|
||||
When call validate_input_python "common-cache" "type" "pip"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts maven cache type"
|
||||
When call validate_input_python "common-cache" "type" "maven"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts gradle cache type"
|
||||
When call validate_input_python "common-cache" "type" "gradle"
|
||||
The status should be success
|
||||
End
|
||||
It "rejects empty cache type"
|
||||
When call validate_input_python "common-cache" "type" ""
|
||||
The status should be failure
|
||||
End
|
||||
It "rejects invalid cache type"
|
||||
Pending "TODO: Implement enum validation for cache type"
|
||||
When call validate_input_python "common-cache" "type" "invalid-type"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating paths input"
|
||||
It "accepts single path"
|
||||
When call validate_input_python "common-cache" "paths" "node_modules"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts multiple paths"
|
||||
When call validate_input_python "common-cache" "paths" "node_modules,dist,build"
|
||||
The status should be success
|
||||
End
|
||||
It "rejects empty paths"
|
||||
When call validate_input_python "common-cache" "paths" ""
|
||||
The status should be failure
|
||||
End
|
||||
It "rejects path traversal"
|
||||
When call validate_input_python "common-cache" "paths" "../../../etc/passwd"
|
||||
The status should be failure
|
||||
End
|
||||
It "rejects command injection in paths"
|
||||
When call validate_input_python "common-cache" "paths" "node_modules;rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating key-prefix input"
|
||||
It "accepts valid key prefix"
|
||||
When call validate_input_python "common-cache" "key-prefix" "v2-build"
|
||||
The status should be success
|
||||
End
|
||||
It "rejects command injection in key-prefix"
|
||||
When call validate_input_python "common-cache" "key-prefix" "v2&&malicious"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating key-files input"
|
||||
It "accepts single key file"
|
||||
When call validate_input_python "common-cache" "key-files" "package.json"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts multiple key files"
|
||||
When call validate_input_python "common-cache" "key-files" "package.json,package-lock.json,yarn.lock"
|
||||
The status should be success
|
||||
End
|
||||
It "rejects path traversal in key-files"
|
||||
When call validate_input_python "common-cache" "key-files" "../../../sensitive.json"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating restore-keys input"
|
||||
It "accepts valid restore keys format"
|
||||
When call validate_input_python "common-cache" "restore-keys" "Linux-npm-,Linux-"
|
||||
The status should be success
|
||||
End
|
||||
It "rejects malicious restore keys"
|
||||
When call validate_input_python "common-cache" "restore-keys" "Linux-npm-;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 Cache"
|
||||
End
|
||||
|
||||
It "defines required inputs"
|
||||
inputs=$(get_action_inputs "$ACTION_FILE")
|
||||
When call echo "$inputs"
|
||||
The output should include "type"
|
||||
The output should include "paths"
|
||||
End
|
||||
|
||||
It "defines optional inputs"
|
||||
inputs=$(get_action_inputs "$ACTION_FILE")
|
||||
When call echo "$inputs"
|
||||
The output should include "key-prefix"
|
||||
The output should include "key-files"
|
||||
The output should include "restore-keys"
|
||||
The output should include "env-vars"
|
||||
End
|
||||
|
||||
It "defines expected outputs"
|
||||
outputs=$(get_action_outputs "$ACTION_FILE")
|
||||
When call echo "$outputs"
|
||||
The output should include "cache-hit"
|
||||
The output should include "cache-key"
|
||||
The output should include "cache-paths"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating security"
|
||||
It "rejects injection in all input types"
|
||||
When call validate_input_python "common-cache" "type" "npm;malicious"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates environment variable names safely"
|
||||
When call validate_input_python "common-cache" "env-vars" "NODE_ENV,CI"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects injection in environment variables"
|
||||
When call validate_input_python "common-cache" "env-vars" "NODE_ENV;rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing outputs"
|
||||
It "produces all expected outputs consistently"
|
||||
When call test_action_outputs "$ACTION_DIR" "type" "npm" "paths" "node_modules"
|
||||
The status should be success
|
||||
The stderr should include "Testing action outputs for: common-cache"
|
||||
The stderr should include "Output test passed for: common-cache"
|
||||
End
|
||||
End
|
||||
End
|
||||
99
_tests/unit/common-file-check/validation.spec.sh
Executable file
99
_tests/unit/common-file-check/validation.spec.sh
Executable 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
|
||||
165
_tests/unit/common-retry/validation.spec.sh
Executable file
165
_tests/unit/common-retry/validation.spec.sh
Executable 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
|
||||
40
_tests/unit/docker-publish-gh/validation.spec.sh
Executable file
40
_tests/unit/docker-publish-gh/validation.spec.sh
Executable 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
|
||||
48
_tests/unit/docker-publish-hub/validation.spec.sh
Executable file
48
_tests/unit/docker-publish-hub/validation.spec.sh
Executable 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
|
||||
85
_tests/unit/dotnet-version-detect/validation.spec.sh
Executable file
85
_tests/unit/dotnet-version-detect/validation.spec.sh
Executable 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
|
||||
355
_tests/unit/eslint-check/validation.spec.sh
Executable file
355
_tests/unit/eslint-check/validation.spec.sh
Executable 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
|
||||
115
_tests/unit/eslint-fix/validation.spec.sh
Executable file
115
_tests/unit/eslint-fix/validation.spec.sh
Executable 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
|
||||
@@ -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
|
||||
141
_tests/unit/github-release/validation.spec.sh
Executable file
141
_tests/unit/github-release/validation.spec.sh
Executable 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
|
||||
171
_tests/unit/go-version-detect/validation.spec.sh
Executable file
171
_tests/unit/go-version-detect/validation.spec.sh
Executable 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
|
||||
@@ -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
|
||||
242
_tests/unit/node-setup/validation.spec.sh
Executable file
242
_tests/unit/node-setup/validation.spec.sh
Executable file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env shellspec
|
||||
# Unit tests for node-setup action
|
||||
|
||||
# Framework is automatically loaded via spec_helper.sh
|
||||
|
||||
Describe "node-setup action"
|
||||
ACTION_DIR="node-setup"
|
||||
ACTION_FILE="$ACTION_DIR/action.yml"
|
||||
|
||||
# Framework is automatically initialized via spec_helper.sh
|
||||
|
||||
Context "when validating inputs"
|
||||
It "accepts valid Node.js version"
|
||||
When call validate_input_python "node-setup" "default-version" "18.17.0"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts valid package manager"
|
||||
When call validate_input_python "node-setup" "package-manager" "npm"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts yarn as package manager"
|
||||
When call validate_input_python "node-setup" "package-manager" "yarn"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts pnpm as package manager"
|
||||
When call validate_input_python "node-setup" "package-manager" "pnpm"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts bun as package manager"
|
||||
When call validate_input_python "node-setup" "package-manager" "bun"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects invalid package manager"
|
||||
When call validate_input_python "node-setup" "package-manager" "invalid-manager"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects malformed Node.js version"
|
||||
When call validate_input_python "node-setup" "default-version" "not-a-version"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects command injection in inputs"
|
||||
When call validate_input_python "node-setup" "default-version" "18.0.0; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when checking action 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"
|
||||
When call get_action_name "$ACTION_FILE"
|
||||
The output should equal "Node Setup"
|
||||
End
|
||||
|
||||
It "defines expected inputs"
|
||||
When call get_action_inputs "$ACTION_FILE"
|
||||
The output should include "default-version"
|
||||
The output should include "package-manager"
|
||||
End
|
||||
|
||||
It "defines expected outputs"
|
||||
When call get_action_outputs "$ACTION_FILE"
|
||||
The output should include "node-version"
|
||||
The output should include "package-manager"
|
||||
The output should include "cache-hit"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing Node.js version detection"
|
||||
BeforeEach "shellspec_setup_test_env 'node-version-detection'"
|
||||
AfterEach "shellspec_cleanup_test_env 'node-version-detection'"
|
||||
|
||||
It "detects version from package.json engines field"
|
||||
create_mock_node_repo
|
||||
|
||||
# Mock action output based on package.json
|
||||
echo "node-version=18.0.0" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "node-version" "18.0.0"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "detects version from .nvmrc file"
|
||||
create_mock_node_repo
|
||||
echo "18.17.1" >.nvmrc
|
||||
|
||||
# Mock action output
|
||||
echo "node-version=18.17.1" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "node-version" "18.17.1"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "uses default version when none specified"
|
||||
create_mock_node_repo
|
||||
# Remove engines field simulation
|
||||
|
||||
# Mock default version output
|
||||
echo "node-version=20.0.0" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "node-version" "20.0.0"
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing package manager detection"
|
||||
BeforeEach "shellspec_setup_test_env 'package-manager-detection'"
|
||||
AfterEach "shellspec_cleanup_test_env 'package-manager-detection'"
|
||||
|
||||
It "detects bun from bun.lockb"
|
||||
create_mock_node_repo
|
||||
touch bun.lockb
|
||||
|
||||
echo "package-manager=bun" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "package-manager" "bun"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "detects pnpm from pnpm-lock.yaml"
|
||||
create_mock_node_repo
|
||||
touch pnpm-lock.yaml
|
||||
|
||||
echo "package-manager=pnpm" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "package-manager" "pnpm"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "detects yarn from yarn.lock"
|
||||
create_mock_node_repo
|
||||
touch yarn.lock
|
||||
|
||||
echo "package-manager=yarn" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "package-manager" "yarn"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "detects npm from package-lock.json"
|
||||
create_mock_node_repo
|
||||
touch package-lock.json
|
||||
|
||||
echo "package-manager=npm" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "package-manager" "npm"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "detects packageManager field from package.json"
|
||||
create_mock_node_repo
|
||||
|
||||
# Add packageManager field to package.json
|
||||
cat >package.json <<EOF
|
||||
{
|
||||
"name": "test-project",
|
||||
"version": "1.0.0",
|
||||
"packageManager": "pnpm@8.0.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "package-manager=pnpm" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "package-manager" "pnpm"
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing Corepack integration"
|
||||
BeforeEach "shellspec_setup_test_env 'corepack-test'"
|
||||
AfterEach "shellspec_cleanup_test_env 'corepack-test'"
|
||||
|
||||
It "enables Corepack when packageManager is specified"
|
||||
create_mock_node_repo
|
||||
|
||||
# Simulate packageManager field
|
||||
cat >package.json <<EOF
|
||||
{
|
||||
"name": "test-project",
|
||||
"version": "1.0.0",
|
||||
"packageManager": "yarn@3.6.0"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Mock Corepack enabled output
|
||||
echo "corepack-enabled=true" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "corepack-enabled" "true"
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing cache functionality"
|
||||
BeforeEach "shellspec_setup_test_env 'cache-test'"
|
||||
AfterEach "shellspec_cleanup_test_env 'cache-test'"
|
||||
|
||||
It "reports cache hit when dependencies are cached"
|
||||
create_mock_node_repo
|
||||
touch package-lock.json
|
||||
mkdir -p node_modules
|
||||
|
||||
# Mock cache hit
|
||||
echo "cache-hit=true" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "cache-hit" "true"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "reports cache miss when no cache exists"
|
||||
create_mock_node_repo
|
||||
touch package-lock.json
|
||||
|
||||
# Mock cache miss
|
||||
echo "cache-hit=false" >>"$GITHUB_OUTPUT"
|
||||
|
||||
When call shellspec_validate_action_output "cache-hit" "false"
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing output consistency"
|
||||
It "produces all expected outputs"
|
||||
When call test_action_outputs "$ACTION_DIR" "node-version" "18.0.0" "package-manager" "npm"
|
||||
The status should be success
|
||||
The stderr should include "Testing action outputs for: node-setup"
|
||||
The stderr should include "Output test passed for: node-setup"
|
||||
End
|
||||
End
|
||||
End
|
||||
407
_tests/unit/php-composer/validation.spec.sh
Executable file
407
_tests/unit/php-composer/validation.spec.sh
Executable file
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env shellspec
|
||||
# Unit tests for php-composer action validation and logic
|
||||
|
||||
# Framework is automatically loaded via spec_helper.sh
|
||||
|
||||
Describe "php-composer action"
|
||||
ACTION_DIR="php-composer"
|
||||
ACTION_FILE="$ACTION_DIR/action.yml"
|
||||
|
||||
Context "when validating php input"
|
||||
It "accepts valid PHP version"
|
||||
When call validate_input_python "php-composer" "php" "8.4"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts PHP version with patch"
|
||||
When call validate_input_python "php-composer" "php" "8.4.1"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts PHP 7.4"
|
||||
When call validate_input_python "php-composer" "php" "7.4"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts PHP 8.0"
|
||||
When call validate_input_python "php-composer" "php" "8.0"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts PHP 8.1"
|
||||
When call validate_input_python "php-composer" "php" "8.1"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects PHP version too old"
|
||||
When call validate_input_python "php-composer" "php" "5.5"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects invalid version format"
|
||||
When call validate_input_python "php-composer" "php" "php8.4"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects version with command injection"
|
||||
When call validate_input_python "php-composer" "php" "8.4; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects empty version"
|
||||
When call validate_input_python "php-composer" "php" ""
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating extensions input"
|
||||
It "accepts valid PHP extensions"
|
||||
When call validate_input_python "php-composer" "extensions" "mbstring, xml, zip"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts single extension"
|
||||
When call validate_input_python "php-composer" "extensions" "mbstring"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts extensions without spaces"
|
||||
When call validate_input_python "php-composer" "extensions" "mbstring,xml,zip"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts extensions with underscores"
|
||||
When call validate_input_python "php-composer" "extensions" "pdo_mysql, gd_jpeg"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects extensions with special characters"
|
||||
When call validate_input_python "php-composer" "extensions" "mbstring@xml"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects extensions with command injection"
|
||||
When call validate_input_python "php-composer" "extensions" "mbstring; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects empty extensions"
|
||||
When call validate_input_python "php-composer" "extensions" ""
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating tools input"
|
||||
It "accepts valid Composer tools"
|
||||
When call validate_input_python "php-composer" "tools" "composer:v2"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts multiple tools"
|
||||
When call validate_input_python "php-composer" "tools" "composer:v2, phpunit:^9.0"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts tools with version constraints"
|
||||
When call validate_input_python "php-composer" "tools" "phpcs, phpstan:1.10"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts tools with stability flags (@ allowed)"
|
||||
When call validate_input_python "php-composer" "tools" "dev-master@dev"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts tools with version and stability flag"
|
||||
When call validate_input_python "php-composer" "tools" "monolog/monolog@dev"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects tools with backticks"
|
||||
When call validate_input_python "php-composer" "tools" "composer\`whoami\`"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects tools with command injection"
|
||||
When call validate_input_python "php-composer" "tools" "composer; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects empty tools"
|
||||
When call validate_input_python "php-composer" "tools" ""
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating composer-version input"
|
||||
It "accepts composer version 1"
|
||||
When call validate_input_python "php-composer" "composer-version" "1"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts composer version 2"
|
||||
When call validate_input_python "php-composer" "composer-version" "2"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects invalid composer version"
|
||||
When call validate_input_python "php-composer" "composer-version" "3"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects non-numeric composer version"
|
||||
When call validate_input_python "php-composer" "composer-version" "latest"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects empty composer version"
|
||||
When call validate_input_python "php-composer" "composer-version" ""
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating stability input"
|
||||
It "accepts stable"
|
||||
When call validate_input_python "php-composer" "stability" "stable"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts RC"
|
||||
When call validate_input_python "php-composer" "stability" "RC"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts beta"
|
||||
When call validate_input_python "php-composer" "stability" "beta"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts alpha"
|
||||
When call validate_input_python "php-composer" "stability" "alpha"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts dev"
|
||||
When call validate_input_python "php-composer" "stability" "dev"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects invalid stability"
|
||||
When call validate_input_python "php-composer" "stability" "unstable"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects stability with injection"
|
||||
When call validate_input_python "php-composer" "stability" "stable; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating cache-directories input"
|
||||
It "accepts valid cache directory"
|
||||
When call validate_input_python "php-composer" "cache-directories" "vendor/cache"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts multiple cache directories"
|
||||
When call validate_input_python "php-composer" "cache-directories" "vendor/cache, .cache"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts directories with underscores and hyphens"
|
||||
When call validate_input_python "php-composer" "cache-directories" "cache_dir, cache-dir"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects path traversal"
|
||||
When call validate_input_python "php-composer" "cache-directories" "../malicious"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects absolute paths"
|
||||
When call validate_input_python "php-composer" "cache-directories" "/etc/passwd"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects directories with command injection"
|
||||
When call validate_input_python "php-composer" "cache-directories" "cache; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects empty cache directories"
|
||||
When call validate_input_python "php-composer" "cache-directories" ""
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating token input"
|
||||
It "accepts GitHub token expression"
|
||||
When call validate_input_python "php-composer" "token" "\${{ github.token }}"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts GitHub fine-grained token"
|
||||
When call validate_input_python "php-composer" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts GitHub app token"
|
||||
When call validate_input_python "php-composer" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects invalid token format"
|
||||
When call validate_input_python "php-composer" "token" "invalid-token"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects empty token"
|
||||
When call validate_input_python "php-composer" "token" ""
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating max-retries input"
|
||||
It "accepts valid retry count"
|
||||
When call validate_input_python "php-composer" "max-retries" "3"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts minimum retries"
|
||||
When call validate_input_python "php-composer" "max-retries" "1"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts maximum retries"
|
||||
When call validate_input_python "php-composer" "max-retries" "10"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects zero retries"
|
||||
When call validate_input_python "php-composer" "max-retries" "0"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects too many retries"
|
||||
When call validate_input_python "php-composer" "max-retries" "11"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects non-numeric retries"
|
||||
When call validate_input_python "php-composer" "max-retries" "many"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects negative retries"
|
||||
When call validate_input_python "php-composer" "max-retries" "-1"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating args input"
|
||||
It "accepts valid Composer arguments"
|
||||
When call validate_input_python "php-composer" "args" "--no-progress --prefer-dist"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects empty args"
|
||||
When call validate_input_python "php-composer" "args" ""
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects args with command injection"
|
||||
When call validate_input_python "php-composer" "args" "--no-progress; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects args with pipe"
|
||||
When call validate_input_python "php-composer" "args" "--no-progress | cat /etc/passwd"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when checking action.yml structure"
|
||||
It "has valid YAML syntax"
|
||||
When call validate_action_yml_quiet "$ACTION_FILE"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "has correct action name"
|
||||
name=$(get_action_name "$ACTION_FILE")
|
||||
When call echo "$name"
|
||||
The output should equal "Run Composer Install"
|
||||
End
|
||||
|
||||
It "defines expected inputs"
|
||||
When call get_action_inputs "$ACTION_FILE"
|
||||
The output should include "php"
|
||||
The output should include "extensions"
|
||||
The output should include "tools"
|
||||
The output should include "args"
|
||||
The output should include "composer-version"
|
||||
The output should include "stability"
|
||||
The output should include "cache-directories"
|
||||
The output should include "token"
|
||||
The output should include "max-retries"
|
||||
End
|
||||
|
||||
It "defines expected outputs"
|
||||
When call get_action_outputs "$ACTION_FILE"
|
||||
The output should include "lock"
|
||||
The output should include "php-version"
|
||||
The output should include "composer-version"
|
||||
The output should include "cache-hit"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing input requirements"
|
||||
It "requires php input"
|
||||
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php" "required"
|
||||
The output should equal "required"
|
||||
End
|
||||
|
||||
It "has extensions as optional input"
|
||||
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "extensions" "optional"
|
||||
The output should equal "optional"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing security validations"
|
||||
It "validates against path traversal in cache directories"
|
||||
When call validate_input_python "php-composer" "cache-directories" "../../etc/passwd"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates against shell metacharacters in tools"
|
||||
When call validate_input_python "php-composer" "tools" "composer && rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates against backtick injection in args"
|
||||
When call validate_input_python "php-composer" "args" "--no-progress \`whoami\`"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates against variable expansion in extensions"
|
||||
When call validate_input_python "php-composer" "extensions" "mbstring,\${HOME}"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing PHP-specific validations"
|
||||
It "validates PHP version boundaries"
|
||||
When call validate_input_python "php-composer" "php" "10.0"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates Composer version enum restriction"
|
||||
When call validate_input_python "php-composer" "composer-version" "0"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates stability enum values"
|
||||
When call validate_input_python "php-composer" "stability" "experimental"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
End
|
||||
280
_tests/unit/php-laravel-phpunit/validation.spec.sh
Executable file
280
_tests/unit/php-laravel-phpunit/validation.spec.sh
Executable file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env shellspec
|
||||
# Unit tests for php-laravel-phpunit action validation and logic
|
||||
|
||||
# Framework is automatically loaded via spec_helper.sh
|
||||
|
||||
Describe "php-laravel-phpunit action"
|
||||
ACTION_DIR="php-laravel-phpunit"
|
||||
ACTION_FILE="$ACTION_DIR/action.yml"
|
||||
|
||||
Context "when validating php-version input"
|
||||
It "accepts latest"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" "latest"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts valid PHP version"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" "8.4"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts PHP version with patch"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" "8.4.1"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts PHP 7.4"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" "7.4"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts PHP 8.0"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" "8.0"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects invalid version format"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" "php8.4"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects version with command injection"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" "8.4; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "accepts empty version (uses default)"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" ""
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating php-version-file input"
|
||||
It "accepts valid PHP version file"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" ".php-version"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts custom version file"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" "custom-php-version"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts version file with path"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" "config/.php-version"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects path traversal in version file"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" "../../../etc/passwd"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects absolute path in version file"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" "/etc/passwd"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects version file with command injection"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" ".php-version; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "accepts empty version file"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" ""
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating extensions input"
|
||||
It "accepts valid PHP extensions"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring, intl, json"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts single extension"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts extensions without spaces"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring,intl,json"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts extensions with underscores"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "pdo_sqlite, pdo_mysql"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts extensions with numbers"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "sqlite3, gd2"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects extensions with special characters"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring@intl"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects extensions with command injection"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "accepts empty extensions"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" ""
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating coverage input"
|
||||
It "accepts none coverage"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" "none"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts xdebug coverage"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" "xdebug"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts pcov coverage"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" "pcov"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts xdebug3 coverage"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" "xdebug3"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects invalid coverage driver"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" "invalid"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects coverage with command injection"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" "none; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "accepts empty coverage"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" ""
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating token input"
|
||||
It "accepts GitHub token expression"
|
||||
When call validate_input_python "php-laravel-phpunit" "token" "\${{ github.token }}"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts GitHub fine-grained token"
|
||||
When call validate_input_python "php-laravel-phpunit" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts GitHub app token"
|
||||
When call validate_input_python "php-laravel-phpunit" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects invalid token format"
|
||||
When call validate_input_python "php-laravel-phpunit" "token" "invalid-token"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "accepts empty token"
|
||||
When call validate_input_python "php-laravel-phpunit" "token" ""
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when checking action.yml structure"
|
||||
It "has valid YAML syntax"
|
||||
When call validate_action_yml_quiet "$ACTION_FILE"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "has correct action name"
|
||||
name=$(get_action_name "$ACTION_FILE")
|
||||
When call echo "$name"
|
||||
The output should equal "Laravel Setup and Composer test"
|
||||
End
|
||||
|
||||
It "defines expected inputs"
|
||||
When call get_action_inputs "$ACTION_FILE"
|
||||
The output should include "php-version"
|
||||
The output should include "php-version-file"
|
||||
The output should include "extensions"
|
||||
The output should include "coverage"
|
||||
The output should include "token"
|
||||
End
|
||||
|
||||
It "defines expected outputs"
|
||||
When call get_action_outputs "$ACTION_FILE"
|
||||
The output should include "php-version"
|
||||
The output should include "php-version-file"
|
||||
The output should include "extensions"
|
||||
The output should include "coverage"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing input requirements"
|
||||
It "has all inputs as optional"
|
||||
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
|
||||
The output should equal "none"
|
||||
End
|
||||
|
||||
It "has correct default php-version"
|
||||
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php-version" "default"
|
||||
The output should equal "latest"
|
||||
End
|
||||
|
||||
It "has correct default php-version-file"
|
||||
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php-version-file" "default"
|
||||
The output should equal ".php-version"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing security validations"
|
||||
It "validates against path traversal in php-version-file"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" "../../etc/passwd"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates against shell metacharacters in extensions"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring && rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates against backtick injection in coverage"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" "none\`whoami\`"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates against variable expansion in php-version"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version" "8.4\${HOME}"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing Laravel-specific validations"
|
||||
It "validates coverage driver enum values"
|
||||
When call validate_input_python "php-laravel-phpunit" "coverage" "invalid-driver"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates php-version-file path safety"
|
||||
When call validate_input_python "php-laravel-phpunit" "php-version-file" "/etc/shadow"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates extensions format for Laravel requirements"
|
||||
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring, intl, json, pdo_sqlite, sqlite3"
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
End
|
||||
@@ -174,10 +174,10 @@ End
|
||||
|
||||
It "defines expected outputs"
|
||||
When call get_action_outputs "$ACTION_FILE"
|
||||
The output should include "test-status"
|
||||
The output should include "tests-run"
|
||||
The output should include "tests-passed"
|
||||
The output should include "framework"
|
||||
The output should include "test_status"
|
||||
The output should include "tests_run"
|
||||
The output should include "tests_passed"
|
||||
The output should include "coverage_path"
|
||||
End
|
||||
End
|
||||
|
||||
@@ -245,214 +245,5 @@ It "validates default email is secure"
|
||||
When call validate_input_python "php-tests" "email" "github-actions@github.com"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
# Helper function that replicates the PHPUnit output parsing logic from action.yml
|
||||
parse_phpunit_output() {
|
||||
local phpunit_output="$1"
|
||||
local phpunit_exit_code="$2"
|
||||
|
||||
local tests_run="0"
|
||||
local tests_passed="0"
|
||||
|
||||
# Pattern 1: "OK (N test(s), M assertions)" - success case (handles both singular and plural)
|
||||
if echo "$phpunit_output" | grep -qE 'OK \([0-9]+ tests?,'; then
|
||||
tests_run=$(echo "$phpunit_output" | grep -oE 'OK \([0-9]+ tests?,' | grep -oE '[0-9]+' | head -1)
|
||||
tests_passed="$tests_run"
|
||||
# Pattern 2: "Tests: N" line - failure/error/skipped case
|
||||
elif echo "$phpunit_output" | grep -qE '^Tests:'; then
|
||||
tests_run=$(echo "$phpunit_output" | grep -E '^Tests:' | grep -oE '[0-9]+' | head -1)
|
||||
|
||||
# Calculate passed from failures and errors
|
||||
failures=$(echo "$phpunit_output" | grep -oE 'Failures: [0-9]+' | grep -oE '[0-9]+' | head -1 || echo "0")
|
||||
errors=$(echo "$phpunit_output" | grep -oE 'Errors: [0-9]+' | grep -oE '[0-9]+' | head -1 || echo "0")
|
||||
tests_passed=$((tests_run - failures - errors))
|
||||
|
||||
# Ensure non-negative
|
||||
if [ "$tests_passed" -lt 0 ]; then
|
||||
tests_passed="0"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine status
|
||||
local status
|
||||
if [ "$phpunit_exit_code" -eq 0 ]; then
|
||||
status="success"
|
||||
else
|
||||
status="failure"
|
||||
fi
|
||||
|
||||
# Output as KEY=VALUE format
|
||||
echo "tests_run=$tests_run"
|
||||
echo "tests_passed=$tests_passed"
|
||||
echo "status=$status"
|
||||
}
|
||||
|
||||
Context "when parsing PHPUnit output"
|
||||
# Success cases
|
||||
It "parses single successful test"
|
||||
output="OK (1 test, 2 assertions)"
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 1 of output should equal "tests_run=1"
|
||||
The line 2 of output should equal "tests_passed=1"
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
It "parses multiple successful tests"
|
||||
output="OK (5 tests, 10 assertions)"
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 1 of output should equal "tests_run=5"
|
||||
The line 2 of output should equal "tests_passed=5"
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
It "parses successful tests with plural form"
|
||||
output="OK (25 tests, 50 assertions)"
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 1 of output should equal "tests_run=25"
|
||||
The line 2 of output should equal "tests_passed=25"
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
# Failure cases
|
||||
It "parses test failures"
|
||||
output="FAILURES!
|
||||
Tests: 5, Assertions: 10, Failures: 2."
|
||||
When call parse_phpunit_output "$output" 1
|
||||
The line 1 of output should equal "tests_run=5"
|
||||
The line 2 of output should equal "tests_passed=3"
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
|
||||
It "parses test errors"
|
||||
output="ERRORS!
|
||||
Tests: 5, Assertions: 10, Errors: 1."
|
||||
When call parse_phpunit_output "$output" 2
|
||||
The line 1 of output should equal "tests_run=5"
|
||||
The line 2 of output should equal "tests_passed=4"
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
|
||||
It "parses mixed failures and errors"
|
||||
output="FAILURES!
|
||||
Tests: 10, Assertions: 20, Failures: 2, Errors: 1."
|
||||
When call parse_phpunit_output "$output" 1
|
||||
The line 1 of output should equal "tests_run=10"
|
||||
The line 2 of output should equal "tests_passed=7"
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
|
||||
It "handles all tests failing"
|
||||
output="FAILURES!
|
||||
Tests: 5, Assertions: 10, Failures: 5."
|
||||
When call parse_phpunit_output "$output" 1
|
||||
The line 1 of output should equal "tests_run=5"
|
||||
The line 2 of output should equal "tests_passed=0"
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
|
||||
It "prevents negative passed count"
|
||||
output="ERRORS!
|
||||
Tests: 2, Assertions: 4, Failures: 1, Errors: 2."
|
||||
When call parse_phpunit_output "$output" 2
|
||||
The line 1 of output should equal "tests_run=2"
|
||||
The line 2 of output should equal "tests_passed=0"
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
|
||||
# Skipped tests
|
||||
It "parses skipped tests with success"
|
||||
output="OK, but some tests were skipped!
|
||||
Tests: 5, Assertions: 8, Skipped: 2."
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 1 of output should equal "tests_run=5"
|
||||
The line 2 of output should equal "tests_passed=5"
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
# Edge cases
|
||||
It "handles no parseable output (fallback)"
|
||||
output="Some random output without test info"
|
||||
When call parse_phpunit_output "$output" 1
|
||||
The line 1 of output should equal "tests_run=0"
|
||||
The line 2 of output should equal "tests_passed=0"
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
|
||||
It "handles empty output"
|
||||
output=""
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 1 of output should equal "tests_run=0"
|
||||
The line 2 of output should equal "tests_passed=0"
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
It "handles PHPUnit 10+ format with singular test"
|
||||
output="OK (1 test, 3 assertions)"
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 1 of output should equal "tests_run=1"
|
||||
The line 2 of output should equal "tests_passed=1"
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
It "handles verbose output with noise"
|
||||
output="PHPUnit 10.5.0 by Sebastian Bergmann and contributors.
|
||||
Runtime: PHP 8.3.0
|
||||
|
||||
..... 5 / 5 (100%)
|
||||
|
||||
Time: 00:00.123, Memory: 10.00 MB
|
||||
|
||||
OK (5 tests, 10 assertions)"
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 1 of output should equal "tests_run=5"
|
||||
The line 2 of output should equal "tests_passed=5"
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
It "handles failure output with full details"
|
||||
output="PHPUnit 10.5.0 by Sebastian Bergmann and contributors.
|
||||
|
||||
..F.. 5 / 5 (100%)
|
||||
|
||||
Time: 00:00.234, Memory: 12.00 MB
|
||||
|
||||
FAILURES!
|
||||
Tests: 5, Assertions: 10, Failures: 1."
|
||||
When call parse_phpunit_output "$output" 1
|
||||
The line 1 of output should equal "tests_run=5"
|
||||
The line 2 of output should equal "tests_passed=4"
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
|
||||
# Status determination tests
|
||||
It "marks as success when exit code is 0"
|
||||
output="OK (3 tests, 6 assertions)"
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
It "marks as failure when exit code is non-zero"
|
||||
output="OK (3 tests, 6 assertions)"
|
||||
When call parse_phpunit_output "$output" 1
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
|
||||
It "handles skipped tests without OK prefix"
|
||||
output="Tests: 5, Assertions: 8, Skipped: 2."
|
||||
When call parse_phpunit_output "$output" 0
|
||||
The line 1 of output should equal "tests_run=5"
|
||||
The line 2 of output should equal "tests_passed=5"
|
||||
The line 3 of output should equal "status=success"
|
||||
End
|
||||
|
||||
It "handles risky tests output"
|
||||
output="FAILURES!
|
||||
Tests: 8, Assertions: 15, Failures: 1, Risky: 2."
|
||||
When call parse_phpunit_output "$output" 1
|
||||
The line 1 of output should equal "tests_run=8"
|
||||
The line 2 of output should equal "tests_passed=7"
|
||||
The line 3 of output should equal "status=failure"
|
||||
End
|
||||
End
|
||||
End
|
||||
End
|
||||
|
||||
170
_tests/unit/php-version-detect/validation.spec.sh
Executable file
170
_tests/unit/php-version-detect/validation.spec.sh
Executable 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
|
||||
332
_tests/unit/prettier-check/validation.spec.sh
Executable file
332
_tests/unit/prettier-check/validation.spec.sh
Executable 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
|
||||
285
_tests/unit/prettier-fix/validation.spec.sh
Executable file
285
_tests/unit/prettier-fix/validation.spec.sh
Executable 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
|
||||
@@ -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
|
||||
98
_tests/unit/python-version-detect-v2/validation.spec.sh
Executable file
98
_tests/unit/python-version-detect-v2/validation.spec.sh
Executable 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
|
||||
108
_tests/unit/python-version-detect/validation.spec.sh
Executable file
108
_tests/unit/python-version-detect/validation.spec.sh
Executable 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
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env shellspec
|
||||
# Unit tests for security-scan action validation and logic
|
||||
# Framework is automatically loaded via spec_helper.sh
|
||||
|
||||
Describe "security-scan action"
|
||||
ACTION_DIR="security-scan"
|
||||
ACTION_FILE="$ACTION_DIR/action.yml"
|
||||
|
||||
Context "when validating token input"
|
||||
It "accepts valid GitHub token"
|
||||
When call validate_input_python "security-scan" "token" "ghp_123456789012345678901234567890123456"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects injection in token"
|
||||
When call validate_input_python "security-scan" "token" "token; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "accepts empty token (optional)"
|
||||
When call validate_input_python "security-scan" "token" ""
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating actionlint-enabled input"
|
||||
It "accepts true value"
|
||||
When call validate_input_python "security-scan" "actionlint-enabled" "true"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts false value"
|
||||
When call validate_input_python "security-scan" "actionlint-enabled" "false"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects non-boolean value"
|
||||
When call validate_input_python "security-scan" "actionlint-enabled" "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 "Security Scan"
|
||||
End
|
||||
|
||||
It "defines all expected inputs"
|
||||
inputs=$(get_action_inputs "$ACTION_FILE")
|
||||
When call echo "$inputs"
|
||||
The output should include "gitleaks-license"
|
||||
The output should include "gitleaks-config"
|
||||
The output should include "trivy-severity"
|
||||
The output should include "trivy-scanners"
|
||||
The output should include "trivy-timeout"
|
||||
The output should include "actionlint-enabled"
|
||||
The output should include "token"
|
||||
End
|
||||
|
||||
It "defines all expected outputs"
|
||||
outputs=$(get_action_outputs "$ACTION_FILE")
|
||||
When call echo "$outputs"
|
||||
The output should include "has_trivy_results"
|
||||
The output should include "has_gitleaks_results"
|
||||
The output should include "total_issues"
|
||||
The output should include "critical_issues"
|
||||
End
|
||||
|
||||
It "uses composite run type"
|
||||
run_type=$(get_action_runs_using "$ACTION_FILE")
|
||||
When call echo "$run_type"
|
||||
The output should equal "composite"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating inputs per conventions"
|
||||
It "validates token against github_token convention"
|
||||
When call validate_input_python "security-scan" "token" "ghp_123456789012345678901234567890123456"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "validates actionlint-enabled as boolean"
|
||||
When call validate_input_python "security-scan" "actionlint-enabled" "true"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects invalid boolean for actionlint-enabled"
|
||||
When call validate_input_python "security-scan" "actionlint-enabled" "1"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing optional inputs"
|
||||
It "accepts empty gitleaks-license"
|
||||
When call validate_input_python "security-scan" "gitleaks-license" ""
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts empty token"
|
||||
When call validate_input_python "security-scan" "token" ""
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "accepts valid gitleaks-license value"
|
||||
When call validate_input_python "security-scan" "gitleaks-license" "license-key-123"
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
End
|
||||
69
_tests/unit/set-git-config/validation.spec.sh
Executable file
69
_tests/unit/set-git-config/validation.spec.sh
Executable 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
|
||||
@@ -92,6 +92,10 @@ setup_default_inputs() {
|
||||
"go-build" | "go-lint")
|
||||
[[ "$input_name" != "go-version" ]] && export INPUT_GO_VERSION="1.21"
|
||||
;;
|
||||
"common-cache")
|
||||
[[ "$input_name" != "type" ]] && export INPUT_TYPE="npm"
|
||||
[[ "$input_name" != "paths" ]] && export INPUT_PATHS="node_modules"
|
||||
;;
|
||||
"common-retry")
|
||||
[[ "$input_name" != "command" ]] && export INPUT_COMMAND="echo test"
|
||||
;;
|
||||
@@ -110,6 +114,11 @@ setup_default_inputs() {
|
||||
"validate-inputs")
|
||||
[[ "$input_name" != "action-type" && "$input_name" != "action" && "$input_name" != "rules-file" && "$input_name" != "fail-on-error" ]] && export INPUT_ACTION_TYPE="test-action"
|
||||
;;
|
||||
"version-file-parser")
|
||||
[[ "$input_name" != "language" ]] && export INPUT_LANGUAGE="node"
|
||||
[[ "$input_name" != "tool-versions-key" ]] && export INPUT_TOOL_VERSIONS_KEY="nodejs"
|
||||
[[ "$input_name" != "dockerfile-image" ]] && export INPUT_DOCKERFILE_IMAGE="node"
|
||||
;;
|
||||
"codeql-analysis")
|
||||
[[ "$input_name" != "language" ]] && export INPUT_LANGUAGE="javascript"
|
||||
[[ "$input_name" != "token" ]] && export INPUT_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
@@ -154,6 +163,10 @@ cleanup_default_inputs() {
|
||||
"go-build" | "go-lint")
|
||||
[[ "$input_name" != "go-version" ]] && unset INPUT_GO_VERSION
|
||||
;;
|
||||
"common-cache")
|
||||
[[ "$input_name" != "type" ]] && unset INPUT_TYPE
|
||||
[[ "$input_name" != "paths" ]] && unset INPUT_PATHS
|
||||
;;
|
||||
"common-retry")
|
||||
[[ "$input_name" != "command" ]] && unset INPUT_COMMAND
|
||||
;;
|
||||
@@ -172,6 +185,11 @@ cleanup_default_inputs() {
|
||||
"validate-inputs")
|
||||
[[ "$input_name" != "action-type" && "$input_name" != "action" && "$input_name" != "rules-file" && "$input_name" != "fail-on-error" ]] && unset INPUT_ACTION_TYPE
|
||||
;;
|
||||
"version-file-parser")
|
||||
[[ "$input_name" != "language" ]] && unset INPUT_LANGUAGE
|
||||
[[ "$input_name" != "tool-versions-key" ]] && unset INPUT_TOOL_VERSIONS_KEY
|
||||
[[ "$input_name" != "dockerfile-image" ]] && unset INPUT_DOCKERFILE_IMAGE
|
||||
;;
|
||||
"codeql-analysis")
|
||||
[[ "$input_name" != "language" ]] && unset INPUT_LANGUAGE
|
||||
[[ "$input_name" != "token" ]] && unset INPUT_TOKEN
|
||||
@@ -226,6 +244,10 @@ shellspec_mock_action_run() {
|
||||
action_name=$(basename "$action_dir")
|
||||
|
||||
case "$action_name" in
|
||||
"version-file-parser")
|
||||
echo "detected-version=1.0.0" >>"$GITHUB_OUTPUT"
|
||||
echo "package-manager=npm" >>"$GITHUB_OUTPUT"
|
||||
;;
|
||||
"node-setup")
|
||||
echo "node-version=18.0.0" >>"$GITHUB_OUTPUT"
|
||||
echo "package-manager=npm" >>"$GITHUB_OUTPUT"
|
||||
@@ -236,6 +258,11 @@ shellspec_mock_action_run() {
|
||||
echo "build-time=45" >>"$GITHUB_OUTPUT"
|
||||
echo "platforms=linux/amd64" >>"$GITHUB_OUTPUT"
|
||||
;;
|
||||
"common-cache")
|
||||
echo "cache-hit=true" >>"$GITHUB_OUTPUT"
|
||||
echo "cache-key=Linux-npm-abc123" >>"$GITHUB_OUTPUT"
|
||||
echo "cache-paths=node_modules" >>"$GITHUB_OUTPUT"
|
||||
;;
|
||||
"common-file-check")
|
||||
echo "found=true" >>"$GITHUB_OUTPUT"
|
||||
;;
|
||||
|
||||
125
_tests/unit/version-file-parser/validation.spec.sh
Executable file
125
_tests/unit/version-file-parser/validation.spec.sh
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env shellspec
|
||||
# Unit tests for version-file-parser action validation and logic
|
||||
# Framework is automatically loaded via spec_helper.sh
|
||||
|
||||
Describe "version-file-parser action"
|
||||
ACTION_DIR="version-file-parser"
|
||||
ACTION_FILE="$ACTION_DIR/action.yml"
|
||||
|
||||
Context "when validating language input"
|
||||
It "accepts valid language input"
|
||||
When call validate_input_python "version-file-parser" "language" "node"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts php language"
|
||||
When call validate_input_python "version-file-parser" "language" "php"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts python language"
|
||||
When call validate_input_python "version-file-parser" "language" "python"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts go language"
|
||||
When call validate_input_python "version-file-parser" "language" "go"
|
||||
The status should be success
|
||||
End
|
||||
It "rejects invalid language with special characters"
|
||||
When call validate_input_python "version-file-parser" "language" "node; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
It "rejects empty required inputs"
|
||||
When call validate_input_python "version-file-parser" "language" ""
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating dockerfile-image input"
|
||||
It "accepts valid dockerfile image"
|
||||
When call validate_input_python "version-file-parser" "dockerfile-image" "node"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts php dockerfile image"
|
||||
When call validate_input_python "version-file-parser" "dockerfile-image" "php"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts python dockerfile image"
|
||||
When call validate_input_python "version-file-parser" "dockerfile-image" "python"
|
||||
The status should be success
|
||||
End
|
||||
It "rejects injection in dockerfile image"
|
||||
When call validate_input_python "version-file-parser" "dockerfile-image" "node;malicious"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating optional inputs"
|
||||
It "accepts valid validation regex"
|
||||
When call validate_input_python "version-file-parser" "validation-regex" "^[0-9]+\.[0-9]+(\.[0-9]+)?$"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts valid default version"
|
||||
When call validate_input_python "version-file-parser" "default-version" "18.0.0"
|
||||
The status should be success
|
||||
End
|
||||
It "accepts tool versions key"
|
||||
When call validate_input_python "version-file-parser" "tool-versions-key" "nodejs"
|
||||
The status should be success
|
||||
End
|
||||
End
|
||||
|
||||
Context "when checking action.yml structure"
|
||||
It "has valid YAML syntax"
|
||||
When call validate_action_yml_quiet "$ACTION_FILE"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "contains required metadata"
|
||||
When call get_action_name "$ACTION_FILE"
|
||||
The output should equal "Version File Parser"
|
||||
End
|
||||
|
||||
It "defines expected inputs"
|
||||
When call get_action_inputs "$ACTION_FILE"
|
||||
The output should include "language"
|
||||
The output should include "tool-versions-key"
|
||||
The output should include "dockerfile-image"
|
||||
End
|
||||
|
||||
It "defines expected outputs"
|
||||
When call get_action_outputs "$ACTION_FILE"
|
||||
The output should include "detected-version"
|
||||
The output should include "package-manager"
|
||||
End
|
||||
End
|
||||
|
||||
Context "when validating security"
|
||||
It "rejects injection in language parameter"
|
||||
When call validate_input_python "version-file-parser" "language" "node&&malicious"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "rejects pipe injection in tool versions key"
|
||||
When call validate_input_python "version-file-parser" "tool-versions-key" "nodejs|dangerous"
|
||||
The status should be failure
|
||||
End
|
||||
|
||||
It "validates regex patterns safely"
|
||||
When call validate_input_python "version-file-parser" "validation-regex" "^[0-9]+\.[0-9]+$"
|
||||
The status should be success
|
||||
End
|
||||
|
||||
It "rejects malicious regex patterns"
|
||||
When call validate_input_python "version-file-parser" "validation-regex" ".*; rm -rf /"
|
||||
The status should be failure
|
||||
End
|
||||
End
|
||||
|
||||
Context "when testing outputs"
|
||||
It "produces all expected outputs consistently"
|
||||
When call test_action_outputs "$ACTION_DIR" "language" "node" "dockerfile-image" "node"
|
||||
The status should be success
|
||||
The stderr should include "Testing action outputs for: version-file-parser"
|
||||
The stderr should include "Output test passed for: version-file-parser"
|
||||
End
|
||||
End
|
||||
End
|
||||
233
_tests/unit/version-validator/validation.spec.sh
Executable file
233
_tests/unit/version-validator/validation.spec.sh
Executable 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
|
||||
@@ -76,7 +76,11 @@ if ! git diff --quiet; then
|
||||
git commit -m "chore: bump major version from $OLD_VERSION to $NEW_VERSION
|
||||
|
||||
This commit updates all internal action references from $OLD_VERSION
|
||||
to $NEW_VERSION."
|
||||
to $NEW_VERSION.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
|
||||
printf '%b' "${GREEN}✅ Committed version bump${NC}\n"
|
||||
else
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
155
_tools/shared.sh
155
_tools/shared.sh
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -95,7 +95,7 @@ runs:
|
||||
find . -maxdepth 2 -name "action.yml" -path "*/action.yml" ! -path "./_*" ! -path "./.github/*" -exec grep -h "uses: ivuorinen/actions/" {} \; > "$temp_file"
|
||||
|
||||
while IFS= read -r line; do
|
||||
current_sha=$(printf '%s' "$line" | grep -oE '@[a-f0-9]{40}' | sed 's/@//')
|
||||
current_sha=$(echo "$line" | grep -oE '@[a-f0-9]{40}' | sed 's/@//')
|
||||
|
||||
if [ "$current_sha" != "$TAG_SHA" ]; then
|
||||
echo "Found outdated reference: $current_sha (should be $TAG_SHA)"
|
||||
@@ -153,7 +153,11 @@ runs:
|
||||
git commit -m "chore: update action references to $MAJOR_VERSION ($TAG_SHA)" \
|
||||
-m "" \
|
||||
-m "This commit updates all internal action references to point to the latest" \
|
||||
-m "$MAJOR_VERSION tag SHA."
|
||||
-m "$MAJOR_VERSION tag SHA." \
|
||||
-m "" \
|
||||
-m "🤖 Generated with [Claude Code](https://claude.com/claude-code)" \
|
||||
-m "" \
|
||||
-m "Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
|
||||
commit_sha=$(git rev-parse HEAD)
|
||||
printf '%s\n' "sha=$commit_sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -45,19 +45,55 @@ runs:
|
||||
steps:
|
||||
- name: Validate Inputs
|
||||
id: validate
|
||||
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
|
||||
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,32 +105,35 @@ 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: Setup Python
|
||||
- name: Cache Python Dependencies
|
||||
if: steps.check-files.outputs.files_found == 'true'
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
id: cache-pip
|
||||
uses: ivuorinen/actions/common-cache@e2222afff180ee77f330ef4325f60d6e85477c01
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
type: 'pip'
|
||||
paths: '~/.cache/pip'
|
||||
key-files: 'requirements*.txt,pyproject.toml,setup.py,setup.cfg'
|
||||
key-prefix: 'ansible-lint-fix'
|
||||
|
||||
- name: Install ansible-lint
|
||||
id: install-ansible-lint
|
||||
if: steps.check-files.outputs.files_found == 'true'
|
||||
uses: step-security/retry@e1d59ce1f574b32f0915e3a8df055cfe9f99be5d # v3.0.4
|
||||
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
|
||||
@@ -120,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@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
|
||||
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@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
sarif_file: ansible-lint.sarif
|
||||
|
||||
58
biome-check/README.md
Normal file
58
biome-check/README.md
Normal 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
238
biome-check/action.yml
Normal 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
41
biome-check/rules.yml
Normal 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
57
biome-fix/README.md
Normal 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
204
biome-fix/action.yml
Normal 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
41
biome-fix/rules.yml
Normal 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
|
||||
@@ -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
|
||||
```
|
||||
@@ -1,373 +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: sh
|
||||
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 -eu
|
||||
|
||||
# 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" ]; then
|
||||
case "$GITHUB_TOKEN" in
|
||||
\$\{\{*)
|
||||
# Token is a GitHub Actions expression, skip validation
|
||||
;;
|
||||
*)
|
||||
echo "Using provided GitHub token"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Validate email format (basic check) - required for fix mode
|
||||
if [ "$MODE" = "fix" ]; then
|
||||
case "$EMAIL" in
|
||||
*@*.*) ;;
|
||||
*)
|
||||
echo "::error::Invalid email format: '$EMAIL'. Expected valid email address"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 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)
|
||||
case "$username" in
|
||||
*[!a-zA-Z0-9-]*)
|
||||
echo "::error::Invalid username characters in '$username'. Only letters, digits, and hyphens allowed"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check doesn't start or end with hyphen
|
||||
case "$username" in
|
||||
-*|*-)
|
||||
echo "::error::Invalid username '$username'. Cannot start or end with hyphen"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check no consecutive hyphens
|
||||
case "$username" in
|
||||
*--*)
|
||||
echo "::error::Invalid username '$username'. Consecutive hyphens not allowed"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Validate max retries (positive integer with reasonable upper limit)
|
||||
case "$MAX_RETRIES" in
|
||||
''|*[!0-9]*)
|
||||
echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then
|
||||
echo "::error::Invalid max-retries: '$MAX_RETRIES'. Must be a positive integer between 1 and 10"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate fail-on-error (boolean)
|
||||
if [ "$FAIL_ON_ERROR" != "true" ] && [ "$FAIL_ON_ERROR" != "false" ]; then
|
||||
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: Detect Package Manager
|
||||
id: detect-pm
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
# Detect package manager from lockfiles
|
||||
if [ -f bun.lockb ]; then
|
||||
package_manager="bun"
|
||||
elif [ -f pnpm-lock.yaml ]; then
|
||||
package_manager="pnpm"
|
||||
elif [ -f yarn.lock ]; then
|
||||
package_manager="yarn"
|
||||
else
|
||||
package_manager="npm"
|
||||
fi
|
||||
|
||||
printf 'package-manager=%s\n' "$package_manager" >> "$GITHUB_OUTPUT"
|
||||
echo "Detected package manager: $package_manager"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Enable Corepack
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
corepack enable
|
||||
|
||||
- name: Install Package Manager
|
||||
shell: sh
|
||||
env:
|
||||
PACKAGE_MANAGER: ${{ steps.detect-pm.outputs.package-manager }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
case "$PACKAGE_MANAGER" in
|
||||
pnpm)
|
||||
corepack prepare pnpm@latest --activate
|
||||
;;
|
||||
yarn)
|
||||
corepack prepare yarn@stable --activate
|
||||
;;
|
||||
bun|npm)
|
||||
# Bun installed separately, npm built-in
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.detect-pm.outputs.package-manager == 'bun'
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Cache Node Dependencies
|
||||
id: cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-biome-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-${{ hashFiles('package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-biome-lint-${{ inputs.mode }}-${{ steps.detect-pm.outputs.package-manager }}-
|
||||
${{ runner.os }}-biome-lint-${{ inputs.mode }}-
|
||||
|
||||
- name: Install Biome
|
||||
shell: sh
|
||||
env:
|
||||
PACKAGE_MANAGER: ${{ steps.detect-pm.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 Check
|
||||
if: inputs.mode == 'check'
|
||||
id: check
|
||||
shell: sh
|
||||
env:
|
||||
FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
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@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
with:
|
||||
sarif_file: biome-report.sarif
|
||||
|
||||
- name: Run Biome Fix
|
||||
if: inputs.mode == 'fix'
|
||||
id: fix
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
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'
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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@5cc7373a22402ee8985376bc713f00e09b5b2edb
|
||||
uses: ivuorinen/actions/validate-inputs@e2222afff180ee77f330ef4325f60d6e85477c01
|
||||
with:
|
||||
action-type: codeql-analysis
|
||||
language: ${{ inputs.language }}
|
||||
@@ -126,52 +131,50 @@ runs:
|
||||
threads: ${{ inputs.threads }}
|
||||
output: ${{ inputs.output }}
|
||||
skip-queries: ${{ inputs.skip-queries }}
|
||||
add-snippets: ${{ inputs.add-snippets }}
|
||||
|
||||
- name: Validate checkout safety
|
||||
shell: sh
|
||||
shell: bash
|
||||
env:
|
||||
CHECKOUT_REF: ${{ inputs.checkout-ref }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set -eu
|
||||
# Security check: Warn if checking out custom ref on pull_request_target
|
||||
if [ "$EVENT_NAME" = "pull_request_target" ] && [ -n "$CHECKOUT_REF" ]; then
|
||||
if [[ "$EVENT_NAME" == "pull_request_target" ]] && [[ -n "$CHECKOUT_REF" ]]; then
|
||||
echo "::warning::Using custom checkout-ref on pull_request_target is potentially unsafe"
|
||||
echo "::warning::Ensure the ref is validated before running untrusted code"
|
||||
fi
|
||||
|
||||
- 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 }}
|
||||
|
||||
- name: Set analysis category
|
||||
id: set-category
|
||||
shell: sh
|
||||
shell: bash
|
||||
env:
|
||||
CATEGORY: ${{ inputs.category }}
|
||||
LANGUAGE: ${{ inputs.language }}
|
||||
run: |
|
||||
set -eu
|
||||
if [ -n "$CATEGORY" ]; then
|
||||
if [[ -n "$CATEGORY" ]]; then
|
||||
category="$CATEGORY"
|
||||
else
|
||||
category="/language:$LANGUAGE"
|
||||
fi
|
||||
echo "category=$category" >> "$GITHUB_OUTPUT"
|
||||
echo "category=$category" >> $GITHUB_OUTPUT
|
||||
echo "Using analysis category: $category"
|
||||
|
||||
- name: Set build mode
|
||||
id: set-build-mode
|
||||
shell: sh
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_MODE: ${{ inputs.build-mode }}
|
||||
LANGUAGE: ${{ inputs.language }}
|
||||
run: |
|
||||
set -eu
|
||||
build_mode="$BUILD_MODE"
|
||||
if [ -z "$build_mode" ]; then
|
||||
if [[ -z "$build_mode" ]]; then
|
||||
# Auto-detect build mode based on language
|
||||
case "$LANGUAGE" in
|
||||
javascript|python|ruby|actions)
|
||||
@@ -182,11 +185,11 @@ runs:
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
echo "build-mode=$build_mode" >> "$GITHUB_OUTPUT"
|
||||
echo "build-mode=$build_mode" >> $GITHUB_OUTPUT
|
||||
echo "Using build mode: $build_mode"
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
languages: ${{ inputs.language }}
|
||||
queries: ${{ inputs.queries }}
|
||||
@@ -199,22 +202,23 @@ runs:
|
||||
threads: ${{ inputs.threads }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
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@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
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
|
||||
shell: sh
|
||||
shell: bash
|
||||
env:
|
||||
LANGUAGE: ${{ inputs.language }}
|
||||
CATEGORY: ${{ steps.set-category.outputs.category }}
|
||||
@@ -224,15 +228,14 @@ runs:
|
||||
UPLOAD_RESULTS: ${{ inputs.upload-results }}
|
||||
OUTPUT: ${{ inputs.output }}
|
||||
run: |
|
||||
set -eu
|
||||
echo "✅ CodeQL analysis completed for language: $LANGUAGE"
|
||||
echo "📊 Category: $CATEGORY"
|
||||
echo "🏗️ Build mode: $BUILD_MODE"
|
||||
echo "🔍 Queries: ${QUERIES:-default}"
|
||||
echo "📦 Packs: ${PACKS:-none}"
|
||||
if [ "$UPLOAD_RESULTS" = "true" ]; then
|
||||
if [[ "$UPLOAD_RESULTS" == "true" ]]; then
|
||||
echo "📤 Results uploaded to GitHub Security tab"
|
||||
fi
|
||||
if [ -n "$OUTPUT" ]; then
|
||||
if [[ -n "$OUTPUT" ]]; then
|
||||
echo "💾 SARIF saved to: $OUTPUT"
|
||||
fi
|
||||
|
||||
@@ -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
|
||||
@@ -42,7 +44,7 @@ conventions:
|
||||
packs: codeql_packs
|
||||
queries: codeql_queries
|
||||
ram: numeric_range_256_32768
|
||||
skip-queries: boolean
|
||||
skip-queries: codeql_queries
|
||||
source-root: file_path
|
||||
threads: numeric_range_1_128
|
||||
token: github_token
|
||||
@@ -51,7 +53,6 @@ overrides:
|
||||
build-mode: codeql_build_mode
|
||||
category: category_format
|
||||
config: codeql_config
|
||||
language: codeql_language
|
||||
output: file_path
|
||||
packs: codeql_packs
|
||||
queries: codeql_queries
|
||||
@@ -61,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
|
||||
|
||||
244
common-cache/CustomValidator.py
Executable file
244
common-cache/CustomValidator.py
Executable file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Custom validator for common-cache action.
|
||||
|
||||
This validator handles caching-specific validation including:
|
||||
- Cache types (npm, composer, go, pip, maven, gradle)
|
||||
- Cache paths (comma-separated list)
|
||||
- Cache keys and restore keys
|
||||
- Path validation with special handling for multiple paths
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Add validate-inputs directory to path to import validators
|
||||
validate_inputs_path = Path(__file__).parent.parent / "validate-inputs"
|
||||
sys.path.insert(0, str(validate_inputs_path))
|
||||
|
||||
from validators.base import BaseValidator
|
||||
from validators.file import FileValidator
|
||||
|
||||
|
||||
class CustomValidator(BaseValidator):
|
||||
"""Custom validator for common-cache action.
|
||||
|
||||
Provides validation for cache configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, action_type: str = "common-cache") -> None:
|
||||
"""Initialize the common-cache validator."""
|
||||
super().__init__(action_type)
|
||||
self.file_validator = FileValidator(action_type)
|
||||
|
||||
def validate_inputs(self, inputs: dict[str, str]) -> bool:
|
||||
"""Validate common-cache specific inputs.
|
||||
|
||||
Args:
|
||||
inputs: Dictionary of input names to values
|
||||
|
||||
Returns:
|
||||
True if all validations pass, False otherwise
|
||||
"""
|
||||
valid = True
|
||||
|
||||
# Validate type (required)
|
||||
if "type" in inputs:
|
||||
valid &= self.validate_cache_type(inputs["type"])
|
||||
else:
|
||||
# Type is required
|
||||
self.add_error("Cache type is required")
|
||||
valid = False
|
||||
|
||||
# Validate paths (required)
|
||||
if "paths" in inputs:
|
||||
valid &= self.validate_cache_paths(inputs["paths"])
|
||||
else:
|
||||
# Paths is required
|
||||
self.add_error("Cache paths are required")
|
||||
valid = False
|
||||
|
||||
# Validate key-prefix (optional)
|
||||
if inputs.get("key-prefix"):
|
||||
valid &= self.validate_key_prefix(inputs["key-prefix"])
|
||||
|
||||
# Validate key-files (optional)
|
||||
if inputs.get("key-files"):
|
||||
valid &= self.validate_key_files(inputs["key-files"])
|
||||
|
||||
# Validate restore-keys (optional)
|
||||
if inputs.get("restore-keys"):
|
||||
valid &= self.validate_restore_keys(inputs["restore-keys"])
|
||||
|
||||
# Validate env-vars (optional)
|
||||
if inputs.get("env-vars"):
|
||||
valid &= self.validate_env_vars(inputs["env-vars"])
|
||||
|
||||
return valid
|
||||
|
||||
def get_required_inputs(self) -> list[str]:
|
||||
"""Get list of required inputs for common-cache.
|
||||
|
||||
Returns:
|
||||
List of required input names
|
||||
"""
|
||||
return ["type", "paths"]
|
||||
|
||||
def get_validation_rules(self) -> dict:
|
||||
"""Get validation rules for common-cache.
|
||||
|
||||
Returns:
|
||||
Dictionary of validation rules
|
||||
"""
|
||||
return {
|
||||
"type": "Cache type (npm, composer, go, pip, maven, gradle)",
|
||||
"paths": "Comma-separated list of paths to cache",
|
||||
"key-prefix": "Optional prefix for cache key",
|
||||
"key-files": "Files to include in cache key hash",
|
||||
"restore-keys": "Fallback cache keys to try",
|
||||
}
|
||||
|
||||
def validate_cache_type(self, cache_type: str) -> bool:
|
||||
"""Validate cache type.
|
||||
|
||||
Args:
|
||||
cache_type: Type of cache
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
# Check for empty
|
||||
if not cache_type or not cache_type.strip():
|
||||
self.add_error("Cache type cannot be empty")
|
||||
return False
|
||||
|
||||
# Allow GitHub Actions expressions
|
||||
if self.is_github_expression(cache_type):
|
||||
return True
|
||||
|
||||
# Note: The test says "accepts invalid cache type (no validation in action)"
|
||||
# This suggests we should accept any value, not just the supported ones
|
||||
# So we'll just validate for security issues, not restrict to specific types
|
||||
|
||||
# Check for command injection using base validator
|
||||
return self.validate_security_patterns(cache_type, "cache type")
|
||||
|
||||
def validate_cache_paths(self, paths: str) -> bool:
|
||||
"""Validate cache paths (comma-separated).
|
||||
|
||||
Args:
|
||||
paths: Comma-separated paths
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
# Check for empty
|
||||
if not paths or not paths.strip():
|
||||
self.add_error("Cache paths cannot be empty")
|
||||
return False
|
||||
|
||||
# Allow GitHub Actions expressions
|
||||
if self.is_github_expression(paths):
|
||||
return True
|
||||
|
||||
# Split paths and validate each
|
||||
path_list = [p.strip() for p in paths.split(",")]
|
||||
|
||||
for path in path_list:
|
||||
if not path:
|
||||
continue
|
||||
|
||||
# Use FileValidator for path validation
|
||||
result = self.file_validator.validate_file_path(path, "paths")
|
||||
# Propagate errors from file validator
|
||||
for error in self.file_validator.errors:
|
||||
if error not in self.errors:
|
||||
self.add_error(error)
|
||||
self.file_validator.clear_errors()
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def validate_key_prefix(self, key_prefix: str) -> bool:
|
||||
"""Validate cache key prefix.
|
||||
|
||||
Args:
|
||||
key_prefix: Key prefix
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
# Allow GitHub Actions expressions
|
||||
if self.is_github_expression(key_prefix):
|
||||
return True
|
||||
|
||||
# Check for command injection using base validator
|
||||
return self.validate_security_patterns(key_prefix, "key-prefix")
|
||||
|
||||
def validate_key_files(self, key_files: str) -> bool:
|
||||
"""Validate key files (comma-separated).
|
||||
|
||||
Args:
|
||||
key_files: Comma-separated file paths
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
# Allow GitHub Actions expressions
|
||||
if self.is_github_expression(key_files):
|
||||
return True
|
||||
|
||||
# Split files and validate each
|
||||
file_list = [f.strip() for f in key_files.split(",")]
|
||||
|
||||
for file_path in file_list:
|
||||
if not file_path:
|
||||
continue
|
||||
|
||||
# Use FileValidator for path validation
|
||||
result = self.file_validator.validate_file_path(file_path, "key-files")
|
||||
# Propagate errors from file validator
|
||||
for error in self.file_validator.errors:
|
||||
if error not in self.errors:
|
||||
self.add_error(error)
|
||||
self.file_validator.clear_errors()
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def validate_restore_keys(self, restore_keys: str) -> bool:
|
||||
"""Validate restore keys.
|
||||
|
||||
Args:
|
||||
restore_keys: Restore keys specification
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
# Allow GitHub Actions expressions
|
||||
if self.is_github_expression(restore_keys):
|
||||
return True
|
||||
|
||||
# Check for command injection using base validator
|
||||
return self.validate_security_patterns(restore_keys, "restore-keys")
|
||||
|
||||
def validate_env_vars(self, env_vars: str) -> bool:
|
||||
"""Validate environment variables.
|
||||
|
||||
Args:
|
||||
env_vars: Environment variables specification
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
# Allow GitHub Actions expressions
|
||||
if self.is_github_expression(env_vars):
|
||||
return True
|
||||
|
||||
# Check for command injection using base validator
|
||||
return self.validate_security_patterns(env_vars, "env-vars")
|
||||
72
common-cache/README.md
Normal file
72
common-cache/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# ivuorinen/actions/common-cache
|
||||
|
||||
## Common Cache
|
||||
|
||||
### Description
|
||||
|
||||
Standardized caching strategy for all actions
|
||||
|
||||
### Inputs
|
||||
|
||||
| name | description | required | default |
|
||||
|----------------|------------------------------------------------------|----------|---------|
|
||||
| `type` | <p>Type of cache (npm, composer, go, pip, etc.)</p> | `true` | `""` |
|
||||
| `paths` | <p>Paths to cache (comma-separated)</p> | `true` | `""` |
|
||||
| `key-prefix` | <p>Custom prefix for cache key</p> | `false` | `""` |
|
||||
| `key-files` | <p>Files to hash for cache key (comma-separated)</p> | `false` | `""` |
|
||||
| `restore-keys` | <p>Fallback keys for cache restoration</p> | `false` | `""` |
|
||||
| `env-vars` | <p>Environment variables to include in cache key</p> | `false` | `""` |
|
||||
|
||||
### Outputs
|
||||
|
||||
| name | description |
|
||||
|---------------|-----------------------------|
|
||||
| `cache-hit` | <p>Cache hit indicator</p> |
|
||||
| `cache-key` | <p>Generated cache key</p> |
|
||||
| `cache-paths` | <p>Resolved cache paths</p> |
|
||||
|
||||
### Runs
|
||||
|
||||
This action is a `composite` action.
|
||||
|
||||
### Usage
|
||||
|
||||
```yaml
|
||||
- uses: ivuorinen/actions/common-cache@main
|
||||
with:
|
||||
type:
|
||||
# Type of cache (npm, composer, go, pip, etc.)
|
||||
#
|
||||
# Required: true
|
||||
# Default: ""
|
||||
|
||||
paths:
|
||||
# Paths to cache (comma-separated)
|
||||
#
|
||||
# Required: true
|
||||
# Default: ""
|
||||
|
||||
key-prefix:
|
||||
# Custom prefix for cache key
|
||||
#
|
||||
# Required: false
|
||||
# Default: ""
|
||||
|
||||
key-files:
|
||||
# Files to hash for cache key (comma-separated)
|
||||
#
|
||||
# Required: false
|
||||
# Default: ""
|
||||
|
||||
restore-keys:
|
||||
# Fallback keys for cache restoration
|
||||
#
|
||||
# Required: false
|
||||
# Default: ""
|
||||
|
||||
env-vars:
|
||||
# Environment variables to include in cache key
|
||||
#
|
||||
# Required: false
|
||||
# Default: ""
|
||||
```
|
||||
122
common-cache/action.yml
Normal file
122
common-cache/action.yml
Normal file
@@ -0,0 +1,122 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
|
||||
# permissions:
|
||||
# - contents: read # Required for reading cache contents
|
||||
---
|
||||
name: Common Cache
|
||||
description: 'Standardized caching strategy for all actions'
|
||||
author: 'Ismo Vuorinen'
|
||||
|
||||
branding:
|
||||
icon: database
|
||||
color: gray-dark
|
||||
|
||||
inputs:
|
||||
type:
|
||||
description: 'Type of cache (npm, composer, go, pip, etc.)'
|
||||
required: true
|
||||
paths:
|
||||
description: 'Paths to cache (comma-separated)'
|
||||
required: true
|
||||
key-prefix:
|
||||
description: 'Custom prefix for cache key'
|
||||
required: false
|
||||
default: ''
|
||||
key-files:
|
||||
description: 'Files to hash for cache key (comma-separated)'
|
||||
required: false
|
||||
default: ''
|
||||
restore-keys:
|
||||
description: 'Fallback keys for cache restoration'
|
||||
required: false
|
||||
default: ''
|
||||
env-vars:
|
||||
description: 'Environment variables to include in cache key'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
description: 'Cache hit indicator'
|
||||
value: ${{ steps.cache.outputs.cache-hit }}
|
||||
cache-key:
|
||||
description: 'Generated cache key'
|
||||
value: ${{ steps.prepare.outputs.cache-key }}
|
||||
cache-paths:
|
||||
description: 'Resolved cache paths'
|
||||
value: ${{ steps.prepare.outputs.cache-paths }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- id: prepare
|
||||
shell: bash
|
||||
env:
|
||||
RUNNER_OS: ${{ runner.os }}
|
||||
CACHE_TYPE: ${{ inputs.type }}
|
||||
KEY_PREFIX: ${{ inputs.key-prefix }}
|
||||
KEY_FILES: ${{ inputs.key-files }}
|
||||
ENV_VARS: ${{ inputs.env-vars }}
|
||||
CACHE_PATHS: ${{ inputs.paths }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Generate standardized cache key components
|
||||
os_key="$RUNNER_OS"
|
||||
type_key="$CACHE_TYPE"
|
||||
prefix_key="$KEY_PREFIX"
|
||||
|
||||
# Process file hashes
|
||||
# Note: For simple glob patterns, hashFiles() function could be used directly
|
||||
# in the cache key. This manual approach is used to support comma-separated
|
||||
# file lists with complex cache key construction.
|
||||
files_hash=""
|
||||
if [ -n "$KEY_FILES" ]; then
|
||||
IFS=',' read -ra FILES <<< "$KEY_FILES"
|
||||
existing_files=()
|
||||
for file in "${FILES[@]}"; do
|
||||
# Trim whitespace
|
||||
file=$(echo "$file" | xargs)
|
||||
if [ -f "$file" ]; then
|
||||
existing_files+=("$file")
|
||||
fi
|
||||
done
|
||||
# Hash all files together for better performance
|
||||
if [ ${#existing_files[@]} -gt 0 ]; then
|
||||
files_hash=$(cat "${existing_files[@]}" | sha256sum | cut -d' ' -f1)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Process environment variables
|
||||
env_hash=""
|
||||
if [ -n "$ENV_VARS" ]; then
|
||||
IFS=',' read -ra VARS <<< "$ENV_VARS"
|
||||
for var in "${VARS[@]}"; do
|
||||
if [ -n "${!var}" ]; then
|
||||
env_hash="${env_hash}-${var}-${!var}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Generate final cache key
|
||||
cache_key="${os_key}"
|
||||
[ -n "$prefix_key" ] && cache_key="${cache_key}-${prefix_key}"
|
||||
[ -n "$type_key" ] && cache_key="${cache_key}-${type_key}"
|
||||
[ -n "$files_hash" ] && cache_key="${cache_key}-${files_hash}"
|
||||
[ -n "$env_hash" ] && cache_key="${cache_key}-${env_hash}"
|
||||
|
||||
echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Process cache paths
|
||||
IFS=',' read -ra PATHS <<< "$CACHE_PATHS"
|
||||
cache_paths=""
|
||||
for path in "${PATHS[@]}"; do
|
||||
cache_paths="${cache_paths}${path}\n"
|
||||
done
|
||||
echo "cache-paths=${cache_paths}" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ steps.prepare.outputs.cache-paths }}
|
||||
key: ${{ steps.prepare.outputs.cache-key }}
|
||||
restore-keys: ${{ inputs.restore-keys }}
|
||||
42
common-cache/rules.yml
Normal file
42
common-cache/rules.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
# Validation rules for common-cache action
|
||||
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
|
||||
# Schema version: 1.0
|
||||
# Coverage: 50% (3/6 inputs)
|
||||
#
|
||||
# This file defines validation rules for the common-cache GitHub Action.
|
||||
# Rules are automatically applied by validate-inputs action when this
|
||||
# action is used.
|
||||
#
|
||||
|
||||
schema_version: '1.0'
|
||||
action: common-cache
|
||||
description: Standardized caching strategy for all actions
|
||||
generator_version: 1.0.0
|
||||
required_inputs:
|
||||
- paths
|
||||
- type
|
||||
optional_inputs:
|
||||
- env-vars
|
||||
- key-files
|
||||
- key-prefix
|
||||
- restore-keys
|
||||
conventions:
|
||||
key-files: file_path
|
||||
key-prefix: prefix
|
||||
paths: file_path
|
||||
overrides: {}
|
||||
statistics:
|
||||
total_inputs: 6
|
||||
validated_inputs: 3
|
||||
skipped_inputs: 0
|
||||
coverage_percentage: 50
|
||||
validation_coverage: 50
|
||||
auto_detected: true
|
||||
manual_review_required: true
|
||||
quality_indicators:
|
||||
has_required_inputs: true
|
||||
has_token_validation: false
|
||||
has_version_validation: false
|
||||
has_file_validation: true
|
||||
has_security_validation: false
|
||||
115
common-file-check/CustomValidator.py
Executable file
115
common-file-check/CustomValidator.py
Executable 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")
|
||||
36
common-file-check/README.md
Normal file
36
common-file-check/README.md
Normal 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: ""
|
||||
```
|
||||
87
common-file-check/action.yml
Normal file
87
common-file-check/action.yml
Normal 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
|
||||
39
common-file-check/rules.yml
Normal file
39
common-file-check/rules.yml
Normal 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
185
common-retry/CustomValidator.py
Executable 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
101
common-retry/README.md
Normal 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
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user