Compare commits

...

30 Commits

Author SHA1 Message Date
a1c0435c22 chore: update action references for release v2025.11.02 (#332)
This commit updates all internal action references to point to the current
commit SHA in preparation for release v2025.11.02.
2025-11-02 20:53:11 +02:00
2f1c73dd8b fix: release timeout wasn't accepting command (#331) 2025-11-02 19:39:44 +02:00
fd49ff6968 fix: ask_confirmation tty redirection (#330) 2025-11-02 17:10:27 +02:00
renovate[bot]
82edd1dc12 chore(deps): update github/codeql-action action (v4.31.0 → v4.31.2) (#327)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 12:19:40 +00:00
63a18808a0 feat: extended release make target, fixes (#329)
* feat: extended release make target, fixes

* fix: cr comments
2025-11-02 14:16:32 +02:00
renovate[bot]
8527166fbb chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.487 → 3.2.489) (#325)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 15:15:27 +02:00
fb5a978260 fix(pr-lint): add token fallback, fix shellspec checksum (#326) 2025-10-31 15:09:46 +02:00
renovate[bot]
ca7fc1a5ff chore(deps)!: update node (v22.21.0 → v24.11.0) (#324)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 20:07:29 +02:00
42a40cfaf1 chore: update root readme, generation listing (#322)
* chore: update root readme, generation listing

* fix: grammar fix, example version from real date to example

* chore: add docstrings to `chore/update` (#323)

Docstrings generation was requested by @ivuorinen.

* https://github.com/ivuorinen/actions/pull/322#issuecomment-3457571306

The following files were modified:

* `generate_listing.cjs`

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-28 19:18:26 +02:00
b06748cbef fix(set-git-config): remove credentials cleaning, it's automatic (#321) 2025-10-28 18:35:58 +02:00
cbbb0c8b8c fix: node-setup caching, validate-inputs optional_inputs type (#320)
* fix: node-setup caching, validate-inputs optional_inputs type

* test(validate-inputs): dict optional_inputs backward compatibility

Verify that legacy dict format for optional_inputs correctly generates
conventions from dict keys. Updates existing test to expect list type
for optional_inputs default.
2025-10-27 23:56:17 +02:00
renovate[bot]
1a8997715c chore(deps)!: update actions/upload-artifact (v4.6.2 → v5.0.0) (#316)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 14:15:31 +02:00
renovate[bot]
f50ab425b8 chore(deps)!: update actions/github-script (v7.1.0 → v8.0.0) (#315)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 12:07:05 +02:00
github-actions[bot]
41b1778849 chore: update action references to v2025 (0fa9a68f07) (#319)
This commit updates all internal action references to point to the latest v2025 tag SHA.
2025-10-27 12:03:38 +02:00
renovate[bot]
bbb05559e6 chore(deps): update actions/github-script action (v7.0.1 → v7.1.0) (#313)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 11:58:55 +02:00
renovate[bot]
7c18e12b06 chore(deps): update github/codeql-action action (v4.30.9 → v4.31.0) (#318)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 09:11:49 +02:00
renovate[bot]
88053f4197 chore(deps): update pre-commit hook renovatebot/pre-commit-hooks (41.149.2 → 41.159.4) (#306)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 22:49:50 +00:00
renovate[bot]
ee9a4877e8 chore(deps)!: update actions/download-artifact (v5.0.0 → v6.0.0) (#314) 2025-10-27 00:46:39 +02:00
renovate[bot]
c32f2813f0 chore(deps): update peter-evans/create-pull-request action (v7.0.5 → v7.0.8) (#310)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-26 22:12:35 +00:00
renovate[bot]
e416c272b5 chore(deps): update astral-sh/setup-uv action (v7.1.1 → v7.1.2) (#317)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 00:06:21 +02:00
74968d942f chore: update action references for release v2025.10.26 (#312)
This commit updates all internal action references to point to the current
commit SHA in preparation for release v2025.10.26.
2025-10-27 00:00:02 +02:00
e2222afff1 fix(validate-inputs): add logic to skip undefined empty (#311)
* fix(validate-inputs): add logic to skip undefined empty

* chore: code review comments
2025-10-26 23:52:47 +02:00
Copilot
81f54fda92 feat: standardize validate-inputs parameter to action-type (#309) 2025-10-25 18:14:42 +03:00
a09e59aa7c fix: test-actions security scan (#307) 2025-10-24 18:21:44 +03:00
2d8ff47548 fix: support INPUT_ACTION_TYPE and INPUT_ACTION (#305) 2025-10-24 15:55:09 +03:00
renovate[bot]
a3fb0bd8db chore(deps): update pre-commit hook bridgecrewio/checkov (3.2.483 → 3.2.487) (#304)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 11:46:52 +00:00
renovate[bot]
42312cdbe4 chore(deps): update pre-commit hook astral-sh/uv-pre-commit (0.9.2 → 0.9.5) (#303)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 04:27:51 +00:00
renovate[bot]
222a2fa571 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit (v0.14.0 → v0.14.2) (#302)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 23:28:02 +03:00
6ebc5a21d5 fix: local references, release workflow (#301)
* fix: local references, release workflow

* chore: apply cr comments
2025-10-23 23:24:20 +03:00
renovate[bot]
020a8fd26c chore(deps): update astral-sh/setup-uv action (v7.1.0 → v7.1.1) (#300)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 12:50:38 +03:00
68 changed files with 2390 additions and 429 deletions

View File

@@ -17,7 +17,7 @@ runs:
using: composite
steps:
- name: Install uv
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
with:
enable-cache: true
@@ -33,7 +33,7 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: '22'
node-version: '24'
cache: npm
- name: Install Node dependencies

View File

@@ -52,7 +52,7 @@ jobs:
# Check Gitleaks configuration and license
if [ -f ".gitleaks.toml" ] && [ -n "${{ secrets.GITLEAKS_LICENSE }}" ]; then
echo "Gitleaks config and license found"
echo "run_gitleaks=true" >> "$GITHUB_OUTPUT"
printf '%s\n' "run_gitleaks=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::Gitleaks config or license missing - skipping Gitleaks scan"
fi
@@ -98,7 +98,7 @@ jobs:
# Check Trivy results
if [ -f "trivy-results.sarif" ]; then
if jq -e . </dev/null 2>&1 <"trivy-results.sarif"; then
echo "has_trivy=true" >> "$GITHUB_OUTPUT"
printf '%s\n' "has_trivy=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::Trivy SARIF file exists but is not valid JSON"
fi
@@ -108,7 +108,7 @@ jobs:
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
echo "has_gitleaks=true" >> "$GITHUB_OUTPUT"
printf '%s\n' "has_gitleaks=true" >> "$GITHUB_OUTPUT"
else
echo "::warning::Gitleaks SARIF file exists but is not valid JSON"
fi
@@ -117,21 +117,21 @@ jobs:
- name: Upload Trivy results
if: steps.verify-sarif.outputs.has_trivy == 'true'
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
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
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: 'gitleaks-report.sarif'
category: 'gitleaks'
- name: Archive security reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: security-reports-${{ github.run_id }}
path: |

View File

@@ -37,15 +37,15 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: '/language:${{matrix.language}}'

View File

@@ -1,3 +1,4 @@
---
name: Monthly issue metrics
on:
workflow_dispatch:
@@ -29,7 +30,7 @@ jobs:
echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV"
- name: Run issue-metrics tool
uses: github/issue-metrics@c640329f02bd24b12b91d51cd385f0b1c25cefb9 # v3.25.1
uses: github/issue-metrics@637a24e71b78bc10881e61972b19ea9ff736e14a # v3.25.2
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SEARCH_QUERY: 'repo:ivuorinen/actions is:issue created:${{ env.last_month }} -reason:"not planned"'

View File

@@ -78,12 +78,12 @@ jobs:
if: always()
shell: bash
run: |
echo "status=success" >> "$GITHUB_OUTPUT"
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"
echo "status=failure" >> "$GITHUB_OUTPUT"
printf '%s\n' "status=failure" >> "$GITHUB_OUTPUT"
fi
else
echo "::warning::MegaLinter log file not found"
@@ -91,7 +91,7 @@ jobs:
- name: Upload Reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: MegaLinter reports
path: |
@@ -101,7 +101,7 @@ jobs:
- name: Upload SARIF Report
if: always() && hashFiles('megalinter-reports/sarif/*.sarif')
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: megalinter-reports/sarif
category: megalinter

View File

@@ -73,14 +73,14 @@ jobs:
if: always()
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: always()
with:
name: unit-test-results
@@ -125,15 +125,15 @@ jobs:
shell: bash
run: |
if [ -d "_tests/reports/integration" ] && [ -n "$(find _tests/reports/integration -type f 2>/dev/null)" ]; then
echo "reports-found=true" >> $GITHUB_OUTPUT
printf '%s\n' "reports-found=true" >> $GITHUB_OUTPUT
echo "Integration test reports found"
else
echo "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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
if: always() && steps.check-integration-reports.outputs.reports-found == 'true'
with:
name: integration-test-results
@@ -167,7 +167,7 @@ jobs:
run: make test-coverage
- name: Upload coverage report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: coverage-report
path: _tests/coverage/
@@ -235,8 +235,8 @@ jobs:
uses: trufflesecurity/trufflehog@0f58ae7c5036094a1e3e750d18772af92821b503
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
base: ${{ github.event_name == 'pull_request' && github.event.repository.default_branch || '' }}
head: ${{ github.event_name == 'pull_request' && 'HEAD' || '' }}
extra_args: --debug --only-verified
- name: Scan shell scripts
@@ -263,7 +263,7 @@ jobs:
steps:
- name: Download test results
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
pattern: '*-test-results'
merge-multiple: true

View File

@@ -0,0 +1,127 @@
---
name: Version Maintenance
on:
schedule:
# Run weekly on Monday at 9 AM UTC
- cron: '0 9 * * 1'
workflow_dispatch:
inputs:
major-version:
description: 'Major version to check (e.g., v2025)'
required: false
type: string
permissions:
contents: write
pull-requests: write
issues: write
jobs:
check-and-update:
name: Check Version References
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Determine Major Version
id: version
shell: sh
run: |
if [ -n "${{ inputs.major-version }}" ]; then
printf '%s\n' "major=${{ inputs.major-version }}" >> "$GITHUB_OUTPUT"
else
current_year=$(date +%Y)
printf '%s\n' "major=v$current_year" >> "$GITHUB_OUTPUT"
fi
- name: Run Action Versioning
id: action-versioning
uses: ./action-versioning
with:
major-version: ${{ steps.version.outputs.major }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Create Pull Request
if: steps.action-versioning.outputs.updated == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: update action references to ${{ steps.version.outputs.major }}'
title: 'chore: Update action references to ${{ steps.version.outputs.major }}'
body: |
## Version Maintenance
This PR updates all internal action references to match the latest ${{ steps.version.outputs.major }} tag.
**Updated SHA**: `${{ steps.action-versioning.outputs.commit-sha }}`
### Changes
- Updated all `*/action.yml` files to reference the current ${{ steps.version.outputs.major }} SHA
### Verification
```bash
make check-version-refs
```
🤖 Auto-generated by version-maintenance workflow
branch: automated/version-update-${{ steps.version.outputs.major }}
delete-branch: true
labels: |
automated
dependencies
- name: Check for Annual Bump
if: steps.action-versioning.outputs.needs-annual-bump == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const currentYear = new Date().getFullYear();
const majorVersion = '${{ steps.version.outputs.major }}';
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `🔄 Annual Version Bump Needed: ${majorVersion} → v${currentYear}`,
body: `## Annual Version Bump Required
It's time to bump the major version from ${majorVersion} to v${currentYear}.
### Steps
1. **Create the new major version tag:**
\`\`\`bash
git tag -a v${currentYear} -m "Major version v${currentYear}"
git push origin v${currentYear}
\`\`\`
2. **Bump all action references:**
\`\`\`bash
make bump-major-version OLD=${majorVersion} NEW=v${currentYear}
\`\`\`
3. **Update documentation:**
\`\`\`bash
make docs
\`\`\`
4. **Commit and push:**
\`\`\`bash
git push origin main
\`\`\`
### Verification
\`\`\`bash
make check-version-refs
\`\`\`
🤖 Auto-generated by version-maintenance workflow
`,
labels: ['maintenance', 'high-priority']
});

2
.nvmrc
View File

@@ -1 +1 @@
v22
24

View File

@@ -14,7 +14,7 @@ repos:
types: [markdown, python, yaml]
files: ^(docs/.*|README\.md|CONTRIBUTING\.md|CHANGELOG\.md|.*\.py|.*\.ya?ml)$
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.9.2
rev: 0.9.5
hooks:
- id: uv-lock
- id: uv-sync
@@ -55,7 +55,7 @@ repos:
- id: yamllint
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.0
rev: v0.14.2
hooks:
# Run the linter with auto-fix
- id: ruff-check
@@ -74,7 +74,7 @@ repos:
rev: v0.11.0.1
hooks:
- id: shellcheck
args: ['--severity=warning', '-x']
args: ['-x']
exclude: '^_tests/.*\.sh$'
- repo: https://github.com/rhysd/actionlint
@@ -84,12 +84,12 @@ repos:
args: ['-shellcheck=']
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 41.149.2
rev: 41.159.4
hooks:
- id: renovate-config-validator
- repo: https://github.com/bridgecrewio/checkov.git
rev: '3.2.483'
rev: '3.2.489'
hooks:
- id: checkov
args:

View File

@@ -22,17 +22,19 @@
- Unquoted variables cause word splitting and globbing
- Example: `"$variable"` not `$variable`, `basename -- "$path"` not `basename $path`
6. **ALWAYS** use local paths (`./action-name`) for intra-repo actions
- Avoids external dependencies and version drift
- Pattern: `uses: ./common-cache` not `uses: ivuorinen/actions/common-cache@main`
6. **ALWAYS** use SHA-pinned references for internal actions in action.yml
- Security: immutable, auditable, portable when used externally
- Pattern: `uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9`
- Test workflows use local: `uses: ./common-cache` (within repo only)
7. **ALWAYS** test regex patterns against edge cases
- Include prerelease tags (`1.0.0-rc.1`), build metadata (`1.0.0+build.123`)
- Version validation should support full semver/calver formats
8. **ALWAYS** use `set -euo pipefail` at script start
- `-e`: Exit on error, `-u`: Exit on undefined variable, `-o pipefail`: Exit on pipe failures
- Critical for fail-fast behavior in composite actions
8. **ALWAYS** use POSIX shell (`set -eu`) for all scripts
- Maximum portability: works on Alpine, busybox, all shells
- Use `#!/bin/sh` not `#!/usr/bin/env bash`
- Use `set -eu` not `set -euo pipefail` (pipefail not POSIX)
9. **Avoid** nesting `${{ }}` expressions inside quoted strings in specific contexts
- In `hashFiles()`: `"${{ inputs.value }}"` breaks cache key generation - use unquoted or extract to variable
@@ -92,42 +94,71 @@ Comprehensive linting with 30+ rule categories including:
**Example**: `# ruff: noqa: T201, S603` for action step scripts only
## Shell Script Standards
## Shell Script Standards (POSIX)
### Required Hardening Checklist
**ALL scripts use POSIX shell** (`#!/bin/sh`) for maximum portability.
-**Shebang**: `#!/usr/bin/env bash` (POSIX-compliant)
-**Error Handling**: `set -euo pipefail` at script start
-**Safe IFS**: `IFS=$' \t\n'` (space, tab, newline only)
-**Exit Trap**: `trap cleanup EXIT` for cleanup operations
-**Error Trap**: `trap 'echo "Error at line $LINENO" >&2' ERR` for debugging
### Required POSIX Compliance Checklist
-**Shebang**: `#!/bin/sh` (POSIX-compliant, not bash)
-**Error Handling**: `set -eu` at script start (no pipefail - not POSIX)
-**Defensive Expansion**: Use `${var:-default}` or `${var:?message}` patterns
-**Quote Everything**: Always quote expansions: `"$var"`, `basename -- "$path"`
-**Tool Availability**: `command -v tool >/dev/null 2>&1 || { echo "Missing tool"; exit 1; }`
-**Portable Output**: Use `printf` instead of `echo -e`
-**Portable Sourcing**: Use `. file` instead of `source file`
-**POSIX Tests**: Use `[ ]` instead of `[[ ]]`
-**Parsing**: Use `cut`, `grep`, pipes instead of here-strings `<<<`
-**No Associative Arrays**: Use temp files or line-based processing
### Key POSIX Differences from Bash
| Bash Feature | POSIX Replacement |
| --------------------- | --------------------------------- |
| `#!/usr/bin/env bash` | `#!/bin/sh` |
| `set -euo pipefail` | `set -eu` |
| `[[ condition ]]` | `[ condition ]` |
| `[[ $var =~ regex ]]` | `echo "$var" \| grep -qE 'regex'` |
| `<<<` here-strings | `echo \| cut` or pipes |
| `source file` | `. file` |
| `$BASH_SOURCE` | `$0` |
| `((var++))` | `var=$((var + 1))` |
| `((var < 10))` | `[ "$var" -lt 10 ]` |
| `echo -e` | `printf '%b'` |
| `declare -A map` | temp files + sort/uniq |
| Process substitution | pipes or temp files |
### Examples
```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$' \t\n'
# Cleanup trap
cleanup() { rm -f /tmp/tempfile; }
trap cleanup EXIT
# Error trap with line number
trap 'echo "Error at line $LINENO" >&2' ERR
```sh
#!/bin/sh
set -eu
# Defensive parameter expansion
config_file="${CONFIG_FILE:-config.yml}" # Use default if unset
required_param="${REQUIRED_PARAM:?Missing value}" # Error if unset
required_param="${REQUIRED_PARAM:?Missing value}" # Error if unset
# Always quote expansions
echo "Processing: $config_file"
printf 'Processing: %s\n' "$config_file"
result=$(basename -- "$file_path")
# POSIX test conditions
if [ -f "$config_file" ]; then
printf 'Found config\n'
fi
# Portable output
printf '%b' "Color: ${GREEN}text${NC}\n"
```
### Why POSIX Shell
- **Portability**: Works on Alpine Linux, busybox, minimal containers, all POSIX shells
- **Performance**: POSIX shells are lighter and faster than bash
- **CI-Friendly**: Minimal dependencies, works everywhere
- **Standards**: Follows POSIX best practices
- **Compatibility**: Works with sh, dash, ash, bash, zsh
### Additional Requirements
- **Security**: All external actions SHA-pinned
@@ -189,48 +220,49 @@ if: github.event_name == 'push'
- Don't quote in `with:`, `env:`, `if:` - GitHub evaluates these
- Never nest expressions: `"${{ inputs.value }}"` inside hashFiles breaks caching
### **Local Action References**
### Internal Action References (SHA-Pinned)
**CRITICAL**: When referencing actions within the same repository:
**CRITICAL**: Action files (`*/action.yml`) use SHA-pinned references for security:
-**CORRECT**: `uses: ./action-name` (relative to workspace root)
-**INCORRECT**: `uses: ../action-name` (relative paths that assume directory structure)
-**INCORRECT**: `uses: owner/repo/action-name@main` (floating branch reference)
-**CORRECT**: `uses: ivuorinen/actions/action-name@7061aafd35a2f21b57653e34f2b634b2a19334a9`
-**INCORRECT**: `uses: ./action-name` (security risk, not portable when used externally)
-**INCORRECT**: `uses: ivuorinen/actions/action-name@main` (floating reference)
**Rationale**:
- Uses GitHub workspace root (`$GITHUB_WORKSPACE`) as reference point
- Clear and unambiguous regardless of where action is called from
- Follows GitHub's recommended pattern for same-repository references
- Avoids issues if action checks out repository to different location
- Eliminates external dependencies and supply chain risks
- **Security**: Immutable, auditable references
- **Reproducibility**: Exact version control
- **Portability**: Works when actions used externally (e.g., `ivuorinen/f2b` using `ivuorinen/actions/pr-lint`)
- **Prevention**: No accidental version drift
**Examples**:
**Test Workflows Exception**:
Test workflows in `_tests/` use local references since they run within the repo:
```yaml
# ✅ Correct - relative to workspace root
- uses: ./validate-inputs
- uses: ./common-cache
- uses: ./node-setup
# ❌ Incorrect - relative directory navigation
- uses: ../validate-inputs
- uses: ../common-cache
- uses: ../node-setup
# ❌ Incorrect - external reference to same repo
- uses: ivuorinen/actions/validate-inputs@main
- uses: ivuorinen/actions/common-cache@v1
# ✅ Test workflows only
uses: ./validate-inputs
```
### **Step Output References**
### External Action References (SHA-Pinned)
```yaml
# ✅ Correct - SHA-pinned
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# ❌ Incorrect - floating reference
uses: actions/checkout@main
uses: actions/checkout@v4
```
### Step Output References
**CRITICAL**: Steps must have `id:` to reference their outputs:
```yaml
# ❌ INCORRECT - missing id
- name: Detect Version
uses: ./version-detect
uses: ivuorinen/actions/version-detect@<SHA>
- name: Setup
with:
@@ -239,7 +271,7 @@ if: github.event_name == 'push'
# ✅ CORRECT - id present
- name: Detect Version
id: detect-version # Required for output reference
uses: ./version-detect
uses: ivuorinen/actions/version-detect@<SHA>
- name: Setup
with:
@@ -250,7 +282,7 @@ if: github.event_name == 'push'
- **No Secrets**: Never commit secrets or keys to repository
- **No Logging**: Never expose or log secrets/keys in code
- **SHA Pinning**: All external actions use SHA commits, not tags
- **SHA Pinning**: All action references (internal + external) use SHA commits, not tags
- **Input Validation**: All actions import from shared validation library (`validate-inputs/`) - stateless validation functions, no inter-action dependencies
- **Output Sanitization**: Use `printf` or heredoc for `$GITHUB_OUTPUT` writes
- **Injection Prevention**: Validate inputs for command injection patterns (`;`, `&&`, `|`, backticks)
@@ -276,6 +308,7 @@ if: github.event_name == 'push'
- **Convention-Based**: Automatic rule generation based on input naming patterns
- **Error Handling**: Comprehensive error messages and proper exit codes
- **Defensive Programming**: Check tool availability, validate inputs, handle edge cases
- **POSIX Compliance**: All scripts portable across POSIX shells
## Pre-commit and Security Configuration

View File

@@ -44,6 +44,17 @@ make generate-tests # Create missing tests
make generate-tests-dry # Preview test generation
```
### Version Management
```bash
make release [VERSION=vYYYY.MM.DD] # Create new release (auto-generates version from date if omitted)
make update-version-refs MAJOR=vYYYY # Update refs to version
make bump-major-version OLD=vYYYY NEW=vYYYY # Annual bump
make check-version-refs # Verify current refs
```
See `versioning_system` memory for complete details.
## Code Style
### EditorConfig (BLOCKING ERRORS)
@@ -55,18 +66,36 @@ make generate-tests-dry # Preview test generation
- **Final Newline**: Required
- **Trailing Whitespace**: Trimmed
### Shell Scripts (REQUIRED)
### Shell Scripts (POSIX REQUIRED)
**ALL scripts use POSIX shell** (`#!/bin/sh`) for maximum portability:
```bash
#!/usr/bin/env bash
set -euo pipefail # MANDATORY
IFS=$' \t\n'
trap cleanup EXIT
trap 'echo "Error at line $LINENO" >&2' ERR
# Always quote: "$variable", basename -- "$path"
#!/bin/sh
set -eu # MANDATORY (no pipefail - not POSIX)
# Quote everything: "$variable", basename -- "$path"
# Check tools: command -v jq >/dev/null 2>&1
# Use printf instead of echo -e for portability
```
**Why POSIX:**
- Works on Alpine Linux, busybox, minimal containers
- Faster than bash
- Maximum compatibility (sh, dash, ash, bash, zsh)
- CI-friendly, minimal dependencies
**Key Differences from Bash:**
- Use `#!/bin/sh` not `#!/usr/bin/env bash`
- Use `set -eu` not `set -euo pipefail` (pipefail not POSIX)
- Use `[ ]` not `[[ ]]`
- Use `printf` not `echo -e`
- Use `. file` not `source file`
- Use `cut`/`grep` for parsing, not here-strings `<<<`
- Use temp files instead of associative arrays
- Use `$0` not `$BASH_SOURCE`
### Python (Ruff)
- **Line Length**: 100 chars
@@ -78,15 +107,68 @@ trap 'echo "Error at line $LINENO" >&2' ERR
### YAML/Actions
- **Indent**: 2 spaces
- **Local Actions**: `uses: ./action-name` (never `../` or `@main`)
- **Internal Actions (action.yml)**: `ivuorinen/actions/action-name@<SHA>` (SHA-pinned, security)
- **Test Workflows**: `./action-name` (local reference, runs within repo)
- **Internal Workflows**: `./action-name` (local reference for sync-labels.yml etc)
- **External Actions**: SHA-pinned (not `@main`/`@v1`)
- **Step IDs**: Required when outputs referenced
- **Permissions**: Minimal scope (contents: read default)
- **Output Sanitization**: Use `printf`, never `echo` for `$GITHUB_OUTPUT`
## Versioning System
### Internal References (SHA-Pinned)
All `*/action.yml` files use SHA-pinned references for security and reproducibility:
```yaml
uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9
```
**Why SHA-pinned internally:**
- Security: immutable, auditable references
- Reproducibility: exact version control
- Portability: works when actions used externally
- Prevention: no accidental version drift
### Test Workflows (Local References)
Test workflows in `_tests/` use local references:
```yaml
uses: ./validate-inputs
```
**Why local in tests:** Tests run within the repo, faster, simpler
### External User References
Users reference with version tags:
```yaml
uses: ivuorinen/actions/validate-inputs@v2025
```
### Version Format (CalVer)
- Major: `v2025` (year)
- Minor: `v2025.10` (year.month)
- Patch: `v2025.10.18` (year.month.day)
All three tags point to the same commit SHA.
### Creating Releases
```bash
make release # Auto-generates vYYYY.MM.DD from today's date
make release VERSION=v2025.10.18 # Specific version
git push origin main --tags --force-with-lease
```
## Security Requirements
1. **SHA Pinning**: All external actions use commit SHAs
1. **SHA Pinning**: All action references use commit SHAs (not moving tags)
2. **Token Safety**: `${{ github.token }}`, never hardcoded
3. **Input Validation**: All inputs validated via centralized system
4. **Output Sanitization**: `printf '%s\n' "$value" >> $GITHUB_OUTPUT`
@@ -104,9 +186,13 @@ trap 'echo "Error at line $LINENO" >&2' ERR
- Never skip testing after changes
- Never create files unless absolutely necessary
- Never nest `${{ }}` in quoted YAML strings (breaks hashFiles)
- Never use `@main` for internal action references (use SHA-pinned)
- Never use bash-specific features (scripts must be POSIX sh)
## Preferred Patterns
- POSIX shell for all scripts (not bash)
- SHA-pinned internal action references (security)
- Edit existing files over creating new ones
- Use centralized validation for all input handling
- Follow existing conventions in codebase

View File

@@ -0,0 +1,219 @@
# Version System Architecture
## Overview
This repository uses a CalVer-based SHA-pinned versioning system for all internal action references.
## Version Format
### CalVer: vYYYY.MM.DD
- **Major**: `v2025` (year, updated annually)
- **Minor**: `v2025.10` (year.month)
- **Patch**: `v2025.10.18` (year.month.day)
Example: Release `v2025.10.18` creates three tags pointing to the same commit:
- `v2025.10.18` (patch - specific release)
- `v2025.10` (minor - latest October 2025 release)
- `v2025` (major - latest 2025 release)
## Internal vs External References
### Internal (action.yml files)
- **Format**: `ivuorinen/actions/validate-inputs@<40-char-SHA>`
- **Purpose**: Security, reproducibility, precise control
- **Example**: `ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9`
### External (user consumption)
- **Format**: `ivuorinen/actions/validate-inputs@v2025`
- **Purpose**: Convenience, always gets latest release
- **Options**: `@v2025`, `@v2025.10`, or `@v2025.10.18`
### Test Workflows
- **Format**: `uses: ./action-name` (local reference)
- **Location**: `_tests/integration/workflows/*.yml`
- **Reason**: Tests run within the actions repo context
### Internal Workflows
- **Format**: `uses: ./sync-labels` (local reference)
- **Location**: `.github/workflows/sync-labels.yml`
- **Reason**: Runs within the actions repo, local is sufficient
## Release Process
### Creating a Release
```bash
# 1. Create release with version tags
make release VERSION=v2025.10.18
# This automatically:
# - Updates all action.yml SHA refs to current HEAD
# - Commits the changes
# - Creates tags: v2025.10.18, v2025.10, v2025
# - All tags point to the same commit SHA
# 2. Push to remote
git push origin main --tags --force-with-lease
```
### After Each Release
Tags are force-pushed to ensure `v2025` and `v2025.10` always point to latest:
```bash
git push origin v2025 --force
git push origin v2025.10 --force
git push origin v2025.10.18
```
Or use `--tags --force-with-lease` to push all at once.
## Makefile Targets
### `make release VERSION=v2025.10.18`
Creates new release with version tags and updates all action references.
### `make update-version-refs MAJOR=v2025`
Updates all action.yml files to reference the SHA of the specified major version tag.
### `make bump-major-version OLD=v2025 NEW=v2026`
Annual version bump - replaces all references from one major version to another.
### `make check-version-refs`
Lists all current SHA-pinned references grouped by SHA. Useful for verification.
## Helper Scripts (\_tools/)
### release.sh
Main release script - validates version, updates refs, creates tags.
### validate-version.sh
Validates CalVer format (vYYYY.MM.DD, vYYYY.MM, vYYYY).
### update-action-refs.sh
Updates all action references to a specific SHA or version tag.
### bump-major-version.sh
Handles annual version bumps with commit creation.
### check-version-refs.sh
Displays current SHA-pinned references with tag information.
### get-action-sha.sh
Retrieves SHA for a specific version tag.
## Action Versioning Action
**Location**: `action-versioning/action.yml`
Automatically checks if major version tag has moved and updates all action references.
**Usage in CI**:
```yaml
- uses: ./action-versioning
with:
major-version: v2025
```
**Outputs**:
- `updated`: true/false
- `commit-sha`: SHA of created commit (if any)
- `needs-annual-bump`: true/false (year mismatch)
## CI Workflow
**File**: `.github/workflows/version-maintenance.yml`
**Triggers**:
- Weekly (Monday 9 AM UTC)
- Manual (workflow_dispatch)
**Actions**:
1. Checks if `v2025` tag has moved
2. Updates action references if needed
3. Creates PR with changes
4. Creates issue if annual bump needed
## Annual Version Bump
**When**: Start of each new year
**Process**:
```bash
# 1. Create new major version tag
git tag -a v2026 -m "Major version v2026"
git push origin v2026
# 2. Bump all references
make bump-major-version OLD=v2025 NEW=v2026
# 3. Update documentation
make docs
# 4. Push changes
git push origin main
```
## Verification
### Check Current Refs
```bash
make check-version-refs
```
### Verify All Refs Match
All action references should point to the same SHA after a release.
### Test External Usage
Create a test repo and use:
```yaml
uses: ivuorinen/actions/pr-lint@v2025
```
## Migration from @main
All action.yml files have been migrated from:
- `uses: ./action-name`
- `uses: ivuorinen/actions/action-name@main`
To:
- `uses: ivuorinen/actions/action-name@<SHA>`
Test workflows still use `./action-name` for local testing.
## Security Considerations
**SHA Pinning**: Prevents supply chain attacks by ensuring exact commit is used.
**Version Tags**: Provide user-friendly references while maintaining security internally.
**Tag Verification**: Always verify tags point to expected commits before force-pushing.
**Annual Review**: Each year requires conscious version bump, preventing accidental drift.

View File

@@ -4,10 +4,6 @@
# * 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 **
@@ -66,3 +62,8 @@ excluded_tools: []
initial_prompt: ''
project_name: 'actions'
languages:
- bash
- python
included_optional_tools: []
encoding: utf-8

View File

@@ -86,7 +86,12 @@ Validation (validate-inputs)
**Validation**: `make update-validators`, `make update-validators-dry`
**References**: `make check-local-refs`, `make fix-local-refs`, `make fix-local-refs-dry`
**Versioning**:
- `make release [VERSION=vYYYY.MM.DD]` - Create release (auto-generates version from date if omitted)
- `make update-version-refs MAJOR=vYYYY` - Update action refs to version
- `make bump-major-version OLD=vYYYY NEW=vYYYY` - Annual version bump
- `make check-version-refs` - Verify current action references
### Linters
@@ -105,24 +110,38 @@ Violations cause runtime failures:
3. Sanitize `$GITHUB_OUTPUT`: use `printf '%s\n' "$val"` not `echo "$val"`
4. Pin external actions to SHA commits (not `@main`/`@v1`)
5. Quote shell vars: `"$var"`, `basename -- "$path"` (handles spaces)
6. Use local paths: `./action-name` (not `owner/repo/action@main`)
6. Use SHA-pinned refs for internal actions: `ivuorinen/actions/action-name@<SHA>`
(security, not `./` or `@main`)
7. Test regex edge cases (support `1.0.0-rc.1`, `1.0.0+build`)
8. Use `set -euo pipefail` at script start
8. Use `set -eu` (POSIX) in shell scripts (all scripts are POSIX sh, not bash)
9. Never nest `${{ }}` in quoted YAML strings (breaks hashFiles)
10. Provide tool fallbacks (macOS/Windows lack Linux tools)
### Core Requirements
- External actions SHA-pinned, use `${{ github.token }}`, `set -euo pipefail`
- All actions SHA-pinned (external + internal), use `${{ github.token }}`, POSIX shell (`set -eu`)
- EditorConfig: 2-space indent, UTF-8, LF, max 200 chars (120 for MD)
- Auto-gen README via `action-docs` (note: `npx action-docs --update-readme` doesn't work)
- Required error handling
- Required error handling, POSIX-compliant scripts
### Action References
`./action-name` | ❌ `../action-name` | ❌ `owner/repo/action@main`
**Internal actions (in action.yml)**: SHA-pinned full references
Check: `make check-local-refs`, `make fix-local-refs`
-`ivuorinen/actions/action-name@7061aafd35a2f21b57653e34f2b634b2a19334a9`
-`./action-name` (security risk, not portable when used externally)
-`owner/repo/action@main` (floating reference)
**Test workflows**: Local references
-`./action-name` (tests run within repo)
-`../action-name` (ambiguous paths)
**External users**: Version tags
-`ivuorinen/actions/action-name@v2025` (CalVer major version)
Check: `make check-version-refs`
## Validation System

View File

@@ -1,7 +1,7 @@
# Makefile for GitHub Actions repository
# Provides organized task management with parallel execution capabilities
.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
.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
.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 format lint precommit ## Generate docs, format, lint, and run pre-commit
all: install-tools update-validators docs update-catalog 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,6 +66,16 @@ 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 \
@@ -145,6 +155,65 @@ fix-local-refs-dry: ## Preview local action reference fixes (dry run)
exit 1; \
fi
# Version management targets
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
update-version-refs: ## Update all action references to a specific version tag (usage: make update-version-refs MAJOR=v2025)
@if [ -z "$(MAJOR)" ]; then \
echo "$(RED)❌ Error: MAJOR parameter required$(RESET)"; \
echo "Usage: make update-version-refs MAJOR=v2025"; \
exit 1; \
fi
@echo "$(BLUE)🔧 Updating action references to $(MAJOR)...$(RESET)"
@sh _tools/update-action-refs.sh "$(MAJOR)"
@echo "$(GREEN)✅ Action references updated$(RESET)"
bump-major-version: ## Replace one major version with another (usage: make bump-major-version OLD=v2025 NEW=v2026)
@if [ -z "$(OLD)" ] || [ -z "$(NEW)" ]; then \
echo "$(RED)❌ Error: OLD and NEW parameters required$(RESET)"; \
echo "Usage: make bump-major-version OLD=v2025 NEW=v2026"; \
exit 1; \
fi
@echo "$(BLUE)🔄 Bumping version from $(OLD) to $(NEW)...$(RESET)"
@sh _tools/bump-major-version.sh "$(OLD)" "$(NEW)"
@echo "$(GREEN)✅ Major version bumped$(RESET)"
check-version-refs: ## List all current SHA-pinned action references
@echo "$(BLUE)🔍 Checking action references...$(RESET)"
@sh _tools/check-version-refs.sh
# Formatting targets
format-markdown: ## Format markdown files
@echo "$(BLUE)📝 Formatting markdown...$(RESET)"
@@ -216,14 +285,17 @@ lint-yaml: ## Lint YAML files
lint-shell: ## Lint shell scripts
@echo "$(BLUE)🔍 Linting shell scripts...$(RESET)"
@if command -v shellcheck >/dev/null 2>&1; then \
if find . -name "*.sh" -not -path "./_tests/*" -exec shellcheck -x {} + 2>/dev/null; then \
echo "$(GREEN)✅ Shell linting passed$(RESET)"; \
else \
echo "$(YELLOW)⚠️ Shell linting issues found$(RESET)" | tee -a $(LOG_FILE); \
fi; \
@if ! command -v shellcheck >/dev/null 2>&1; then \
echo "$(RED)❌ shellcheck not found. Please install shellcheck:$(RESET)"; \
echo " brew install shellcheck"; \
echo " or: apt-get install shellcheck"; \
exit 1; \
fi
@if find . -name "*.sh" -not -path "./_tests/*" -exec shellcheck -x {} +; then \
echo "$(GREEN)✅ Shell linting passed$(RESET)"; \
else \
echo "$(BLUE) shellcheck not available, skipping shell script linting$(RESET)"; \
echo "$(RED)❌ Shell linting issues found$(RESET)"; \
exit 1; \
fi
lint-python: ## Lint Python files with ruff and pyright

182
README.md
View File

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

View File

@@ -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="351e7a63b8df47c07b022c19d21a167b85693f5eb549fa96e64f64844b680024"
local checksum="400d835466429a5fe6c77a62775a9173729d61dd43e05dfa893e8cf6cb511783"
# Ensure cleanup of the downloaded file
# Use ${tarball:-} to handle unbound variable when trap fires after function returns

94
_tools/bump-major-version.sh Executable file
View File

@@ -0,0 +1,94 @@
#!/bin/sh
# Bump from one major version to another (annual version bump)
set -eu
OLD_VERSION="${1:-}"
NEW_VERSION="${2:-}"
# 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
if [ -z "$OLD_VERSION" ] || [ -z "$NEW_VERSION" ]; then
printf '%b' "${RED}Error: OLD_VERSION and NEW_VERSION arguments required${NC}\n"
printf 'Usage: %s v2025 v2026\n' "$0"
exit 1
fi
# Validate major version format
if ! validate_major_version "$OLD_VERSION"; then
printf '%b' "${RED}Error: Invalid old version format: $OLD_VERSION${NC}\n"
printf 'Expected: vYYYY (e.g., v2025)\n'
exit 1
fi
if ! validate_major_version "$NEW_VERSION"; then
printf '%b' "${RED}Error: Invalid new version format: $NEW_VERSION${NC}\n"
printf 'Expected: vYYYY (e.g., v2026)\n'
exit 1
fi
printf '%b' "${BLUE}Bumping major version from $OLD_VERSION to $NEW_VERSION${NC}\n"
printf '\n'
# Get SHA for new version tag
if ! git rev-parse "$NEW_VERSION" >/dev/null 2>&1; then
printf '%b' "${YELLOW}Warning: Tag $NEW_VERSION not found${NC}\n"
printf 'Creating tag %s pointing to current HEAD...\n' "$NEW_VERSION"
if ! current_sha=$(git rev-parse HEAD 2>&1); then
printf '%b' "${RED}Error: Failed to get current HEAD SHA${NC}\n" >&2
printf 'Git command failed: git rev-parse HEAD\n' >&2
exit 1
fi
git tag -a "$NEW_VERSION" -m "Major version $NEW_VERSION"
printf '%b' "${GREEN}✓ Created tag $NEW_VERSION pointing to $current_sha${NC}\n"
printf '\n'
fi
if ! new_sha=$(git rev-list -n 1 "$NEW_VERSION" 2>&1); then
printf '%b' "${RED}Error: Failed to get SHA for tag $NEW_VERSION${NC}\n" >&2
printf 'Git command failed: git rev-list -n 1 "%s"\n' "$NEW_VERSION" >&2
exit 1
fi
if [ -z "$new_sha" ]; then
printf '%b' "${RED}Error: Empty SHA returned for tag $NEW_VERSION${NC}\n" >&2
exit 1
fi
printf '%b' "Target SHA for $NEW_VERSION: ${GREEN}$new_sha${NC}\n"
printf '\n'
# Update all action references
printf '%b' "${BLUE}Updating action references...${NC}\n"
"$SCRIPT_DIR/update-action-refs.sh" "$NEW_VERSION" "tag"
# Commit the changes
if ! git diff --quiet; then
git add -- */action.yml
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.
🤖 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
printf '%b' "${BLUE}No changes to commit${NC}\n"
fi
printf '\n'
printf '%b' "${GREEN}✅ Major version bumped successfully${NC}\n"
printf '\n'
printf '%b' "${YELLOW}Remember to update READMEs:${NC}\n"
printf ' make docs\n'

120
_tools/check-version-refs.sh Executable file
View File

@@ -0,0 +1,120 @@
#!/bin/sh
# Check and display all current SHA-pinned action references
set -eu
# Source shared utilities
# shellcheck source=_tools/shared.sh
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# shellcheck disable=SC1091
. "$SCRIPT_DIR/shared.sh"
# Warn once if git is not available
if ! has_git; then
printf '%b' "${YELLOW}Warning: git is not installed or not in PATH${NC}\n" >&2
printf 'Git tag information will not be available.\n' >&2
fi
# Check for required coreutils
for tool in find grep sed printf sort cut tr wc; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf '%b' "${RED}Error: Required tool '%s' is not installed or not in PATH${NC}\n" "$tool" >&2
printf 'Please install coreutils to use this script.\n' >&2
exit 1
fi
done
printf '%b' "${BLUE}Current SHA-pinned action references:${NC}\n"
printf '\n'
# Create temp files for processing
temp_file=$(safe_mktemp)
trap 'rm -f "$temp_file"' EXIT
temp_input=$(safe_mktemp)
trap 'rm -f "$temp_file" "$temp_input"' EXIT
# Find all action references and collect SHA|action pairs
# Use input redirection to avoid subshell issues with pipeline
find . -maxdepth 2 -name "action.yml" -path "*/action.yml" ! -path "./_*" ! -path "./.github/*" -exec grep -h "uses: ivuorinen/actions/" {} \; > "$temp_input"
while IFS= read -r line; do
# Extract action name and SHA using sed
action=$(echo "$line" | sed -n 's|.*ivuorinen/actions/\([a-z-]*\)@.*|\1|p')
sha=$(echo "$line" | sed -n 's|.*@\([a-f0-9]\{40\}\).*|\1|p')
if [ -n "$action" ] && [ -n "$sha" ]; then
printf '%s\n' "$sha|$action" >> "$temp_file"
fi
done < "$temp_input"
# Check if we found any references
if [ ! -s "$temp_file" ]; then
printf '%b' "${YELLOW}No SHA-pinned references found${NC}\n"
exit 0
fi
# Sort by SHA and group
sort "$temp_file" | uniq > "${temp_file}.sorted"
mv "${temp_file}.sorted" "$temp_file"
# Count unique SHAs
sha_count=$(cut -d'|' -f1 "$temp_file" | sort -u | wc -l | tr -d ' ')
if [ "$sha_count" -eq 1 ]; then
printf '%b' "${GREEN}✓ All references use the same SHA (consistent)${NC}\n"
printf '\n'
fi
# Process and display grouped by SHA
current_sha=""
actions_list=""
while IFS='|' read -r sha action; do
if [ "$sha" != "$current_sha" ]; then
# Print previous SHA group if exists
if [ -n "$current_sha" ]; then
# Try to find tags pointing to this SHA
if has_git; then
tags=$(git tag --points-at "$current_sha" 2>/dev/null | tr '\n' ', ' | sed 's/,$//')
else
tags=""
fi
printf '%b' "${GREEN}SHA: $current_sha${NC}\n"
if [ -n "$tags" ]; then
printf '%b' " Tags: ${BLUE}$tags${NC}\n"
fi
printf ' Actions: %s\n' "$actions_list"
printf '\n'
fi
# Start new SHA group
current_sha="$sha"
actions_list="$action"
else
# Add to current SHA group
actions_list="$actions_list, $action"
fi
done < "$temp_file"
# Print last SHA group
if [ -n "$current_sha" ]; then
if has_git; then
tags=$(git tag --points-at "$current_sha" 2>/dev/null | tr '\n' ', ' | sed 's/,$//')
else
tags=""
fi
printf '%b' "${GREEN}SHA: $current_sha${NC}\n"
if [ -n "$tags" ]; then
printf '%b' " Tags: ${BLUE}$tags${NC}\n"
fi
printf ' Actions: %s\n' "$actions_list"
printf '\n'
fi
printf '%b' "${BLUE}Summary:${NC}\n"
printf ' Unique SHAs: %s\n' "$sha_count"
if [ "$sha_count" -gt 1 ]; then
printf '%b' " ${YELLOW}⚠ Warning: Multiple SHAs in use (consider updating)${NC}\n"
fi

View File

@@ -1,15 +1,15 @@
#!/usr/bin/env bash
#!/bin/sh
# Build script for GitHub Actions Testing Docker Image
set -euo pipefail
set -eu
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
IMAGE_NAME="ghcr.io/ivuorinen/actions"
IMAGE_TAG="${1:-testing-tools}"
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
echo "Building GitHub Actions Testing Docker Image..."
echo "Image: $FULL_IMAGE_NAME"
printf 'Building GitHub Actions Testing Docker Image...\n'
printf 'Image: %s\n' "$FULL_IMAGE_NAME"
# Enable BuildKit for better caching and performance
export DOCKER_BUILDKIT=1
@@ -17,7 +17,7 @@ export DOCKER_BUILDKIT=1
# Build the multi-stage image
# Check for buildx support up front, then run the appropriate build command
if docker buildx version >/dev/null 2>&1; then
echo "Using buildx (multi-arch capable)"
printf 'Using buildx (multi-arch capable)\n'
docker buildx build \
--pull \
--tag "$FULL_IMAGE_NAME" \
@@ -26,7 +26,7 @@ if docker buildx version >/dev/null 2>&1; then
--load \
"$SCRIPT_DIR"
else
echo "⚠️ buildx not available, using standard docker build"
printf '⚠️ buildx not available, using standard docker build\n'
docker build \
--pull \
--tag "$FULL_IMAGE_NAME" \
@@ -35,22 +35,22 @@ else
"$SCRIPT_DIR"
fi
echo "Build completed successfully!"
echo ""
echo "Testing the image..."
printf 'Build completed successfully!\n'
printf '\n'
printf 'Testing the image...\n'
# Test basic functionality
docker run --rm "$FULL_IMAGE_NAME" whoami
docker run --rm "$FULL_IMAGE_NAME" shellspec --version
docker run --rm "$FULL_IMAGE_NAME" act --version
echo "Image tests passed!"
echo ""
echo "To test the image locally:"
echo " docker run --rm -it $FULL_IMAGE_NAME"
echo ""
echo "To push to registry:"
echo " docker push $FULL_IMAGE_NAME"
echo ""
echo "To use in GitHub Actions:"
echo " container: $FULL_IMAGE_NAME"
printf 'Image tests passed!\n'
printf '\n'
printf 'To test the image locally:\n'
printf ' docker run --rm -it %s\n' "$FULL_IMAGE_NAME"
printf '\n'
printf 'To push to registry:\n'
printf ' docker push %s\n' "$FULL_IMAGE_NAME"
printf '\n'
printf 'To use in GitHub Actions:\n'
printf ' container: %s\n' "$FULL_IMAGE_NAME"

41
_tools/get-action-sha.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/sh
# Get the SHA for a specific version tag
set -eu
VERSION="${1:-}"
# 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
if [ -z "$VERSION" ]; then
printf '%b' "${RED}Error: VERSION argument required${NC}\n" >&2
printf 'Usage: %s v2025\n' "$0" >&2
exit 1
fi
# Check if tag exists
if ! git rev-parse "$VERSION" >/dev/null 2>&1; then
printf '%b' "${RED}Error: Tag $VERSION not found${NC}\n" >&2
printf '\n' >&2
printf '%b' "${BLUE}Available tags:${NC}\n" >&2
git tag -l 'v*' | head -20 >&2
exit 1
fi
# Get SHA for the tag
sha=$(git rev-list -n 1 "$VERSION")
# Check if output is for terminal or pipe
if [ -t 1 ]; then
# Terminal output - show with colors
printf '%b' "${GREEN}$sha${NC}\n"
else
# Piped output - just the SHA
printf '%s\n' "$sha"
fi

152
_tools/release-undo.sh Executable file
View File

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

289
_tools/release.sh Executable file
View File

@@ -0,0 +1,289 @@
#!/bin/sh
# 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
# Source shared utilities
# shellcheck source=_tools/shared.sh
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# shellcheck disable=SC1091
. "$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'
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'
exit 1
fi
# Extract version components
# Remove leading 'v'
version_no_v="${VERSION#v}"
# Extract year, month, day
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"
# 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 ' 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 '\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
# 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
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
fi
# Create/update tags
printf '\n'
msg_info "Creating tags..."
# 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
# 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
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
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
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
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 '\n'
msg_plain "$YELLOW" "All tags point to: $current_sha"
printf '\n'
msg_info "Tags created:"
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

257
_tools/shared.sh Executable file
View File

@@ -0,0 +1,257 @@
#!/bin/sh
# Shared functions and utilities for _tools/ scripts
# This file is sourced by other scripts, not executed directly
# Colors (exported for use by sourcing scripts)
# shellcheck disable=SC2034
RED='\033[0;31m'
# shellcheck disable=SC2034
GREEN='\033[0;32m'
# shellcheck disable=SC2034
BLUE='\033[0;34m'
# shellcheck disable=SC2034
YELLOW='\033[1;33m'
# shellcheck disable=SC2034
NC='\033[0m' # No Color
# Validate CalVer version format: vYYYY.MM.DD (zero-padded)
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
return 1
fi
# Extract components
version_no_v="${version#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)
# Validate year (2020-2099)
if [ "$year" -lt 2020 ] || [ "$year" -gt 2099 ]; then
return 1
fi
# Validate month (01-12)
if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then
return 1
fi
# Validate day (01-31)
if [ "$day" -lt 1 ] || [ "$day" -gt 31 ]; then
return 1
fi
return 0
}
# Validate major version format: vYYYY
validate_major_version() {
version="$1"
# Check format: vYYYY using grep
if ! echo "$version" | grep -qE '^v[0-9]{4}$'; then
return 1
fi
# Extract year
year="${version#v}"
# Validate year (2020-2099)
if [ "$year" -lt 2020 ] || [ "$year" -gt 2099 ]; then
return 1
fi
return 0
}
# Validate minor version format: vYYYY.MM (zero-padded)
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
return 1
fi
# Extract components
version_no_v="${version#v}"
year=$(echo "$version_no_v" | cut -d'.' -f1)
month=$(echo "$version_no_v" | cut -d'.' -f2)
# Validate year (2020-2099)
if [ "$year" -lt 2020 ] || [ "$year" -gt 2099 ]; then
return 1
fi
# Validate month (01-12)
if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then
return 1
fi
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
}
# Check if git is available
has_git() {
command -v git >/dev/null 2>&1
}
# 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 'Please install git to use this script.\n' >&2
exit 1
fi
}
# Create temp file with error checking
safe_mktemp() {
_temp_file=""
if ! _temp_file=$(mktemp); then
msg_error "Failed to create temp file"
exit 1
fi
printf '%s' "$_temp_file"
}

71
_tools/update-action-refs.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/sh
# Update all action references to a specific version tag or SHA
set -eu
TARGET="${1:-}"
MODE="${2:-tag}" # 'tag' or 'direct'
# 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
if [ -z "$TARGET" ]; then
printf '%b' "${RED}Error: TARGET argument required${NC}\n"
printf 'Usage: %s v2025 [mode]\n' "$0"
printf ' mode: '\''tag'\'' (default) or '\''direct'\''\n'
exit 1
fi
# Get SHA based on mode
if [ "$MODE" = "direct" ]; then
# Direct SHA provided
target_sha="$TARGET"
printf '%b' "${BLUE}Using direct SHA: $target_sha${NC}\n"
elif [ "$MODE" = "tag" ]; then
# Resolve tag to SHA
if ! git rev-parse "$TARGET" >/dev/null 2>&1; then
printf '%b' "${RED}Error: Tag $TARGET not found${NC}\n"
exit 1
fi
target_sha=$(git rev-list -n 1 "$TARGET")
printf '%b' "${BLUE}Resolved $TARGET to SHA: $target_sha${NC}\n"
else
printf '%b' "${RED}Error: Invalid mode: $MODE${NC}\n"
printf 'Mode must be '\''tag'\'' or '\''direct'\''\n'
exit 1
fi
# Validate SHA format
if ! echo "$target_sha" | grep -qE '^[a-f0-9]{40}$'; then
printf '%b' "${RED}Error: Invalid SHA format: $target_sha${NC}\n"
exit 1
fi
printf '%b' "${BLUE}Updating action references...${NC}\n"
# Update all action.yml files (excluding tests and .github workflows)
# Create temp file to store results
temp_file=$(safe_mktemp)
trap 'rm -f "$temp_file"' EXIT
find . -maxdepth 2 -name "action.yml" -path "*/action.yml" ! -path "./_*" ! -path "./.github/*" | while IFS= read -r file; do
# Use .bak extension for cross-platform sed compatibility
if sed -i.bak "s|ivuorinen/actions/\([a-z-]*\)@[a-f0-9]\{40\}|ivuorinen/actions/\1@$target_sha|g" "$file"; then
rm -f "${file}.bak"
printf '%b' " ${GREEN}${NC} Updated: $file\n"
echo "$file" >> "$temp_file"
fi
done
printf '\n'
if [ -s "$temp_file" ]; then
updated_count=$(wc -l < "$temp_file" | tr -d ' ')
printf '%b' "${GREEN}✅ Updated $updated_count action files${NC}\n"
else
printf '%b' "${BLUE}No files needed updating${NC}\n"
fi

View File

@@ -0,0 +1,44 @@
# ivuorinen/actions/action-versioning
## Action Versioning
### Description
Automatically update SHA-pinned action references to match latest version tags
### Inputs
| name | description | required | default |
|-----------------|------------------------------------------------|----------|---------|
| `major-version` | <p>Major version tag to sync (e.g., v2025)</p> | `true` | `""` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
### Outputs
| name | description |
|---------------------|------------------------------------------------------------|
| `updated` | <p>Whether action references were updated (true/false)</p> |
| `commit-sha` | <p>SHA of the commit that was created (if any)</p> |
| `needs-annual-bump` | <p>Whether annual version bump is needed (true/false)</p> |
### Runs
This action is a `composite` action.
### Usage
```yaml
- uses: ivuorinen/actions/action-versioning@main
with:
major-version:
# Major version tag to sync (e.g., v2025)
#
# Required: true
# Default: ""
token:
# GitHub token for authentication
#
# Required: false
# Default: ""
```

View File

@@ -0,0 +1,165 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
# permissions:
# - contents: write # Required for creating commits
---
name: Action Versioning
description: 'Automatically update SHA-pinned action references to match latest version tags'
author: 'Ismo Vuorinen'
branding:
icon: git-commit
color: blue
inputs:
major-version:
description: 'Major version tag to sync (e.g., v2025)'
required: true
token:
description: 'GitHub token for authentication'
required: false
default: ''
outputs:
updated:
description: 'Whether action references were updated (true/false)'
value: ${{ steps.check-update.outputs.updated }}
commit-sha:
description: 'SHA of the commit that was created (if any)'
value: ${{ steps.commit.outputs.sha }}
needs-annual-bump:
description: 'Whether annual version bump is needed (true/false)'
value: ${{ steps.check-year.outputs.needs-bump }}
runs:
using: composite
steps:
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ inputs.token || github.token }}
fetch-depth: 0
- name: Check Current Year
id: check-year
shell: sh
env:
MAJOR_VERSION: ${{ inputs.major-version }}
run: |
set -eu
current_year=$(date +%Y)
version_year="${MAJOR_VERSION#v}"
if [ "$version_year" != "$current_year" ]; then
echo "::warning::Annual version bump needed: $MAJOR_VERSION -> v$current_year"
printf '%s\n' "needs-bump=true" >> "$GITHUB_OUTPUT"
else
printf '%s\n' "needs-bump=false" >> "$GITHUB_OUTPUT"
fi
- name: Fetch Version Tag SHA
id: fetch-sha
shell: sh
env:
MAJOR_VERSION: ${{ inputs.major-version }}
run: |
set -eu
# Fetch all tags
git fetch --tags --force
# Get SHA for the major version tag
if ! tag_sha=$(git rev-list -n 1 "$MAJOR_VERSION" 2>/dev/null); then
echo "::error::Tag $MAJOR_VERSION not found"
exit 1
fi
printf '%s\n' "tag-sha=$tag_sha" >> "$GITHUB_OUTPUT"
echo "Tag $MAJOR_VERSION points to: $tag_sha"
- name: Check if Update Needed
id: check-update
shell: sh
env:
TAG_SHA: ${{ steps.fetch-sha.outputs.tag-sha }}
run: |
set -eu
# Find all action references and check if any don't match the tag SHA
needs_update=false
# Create temp file for action references
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT
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=$(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)"
needs_update=true
fi
done < "$temp_file"
if [ "$needs_update" = "true" ]; then
printf '%s\n' "updated=true" >> "$GITHUB_OUTPUT"
echo "Update needed - references are outdated"
else
printf '%s\n' "updated=false" >> "$GITHUB_OUTPUT"
echo "No update needed - all references are current"
fi
- name: Update Action References
if: steps.check-update.outputs.updated == 'true'
shell: sh
env:
TAG_SHA: ${{ steps.fetch-sha.outputs.tag-sha }}
run: |
set -eu
echo "Updating all action references to SHA: $TAG_SHA"
# Update all action.yml files (excluding tests and .github)
# Use .bak extension for cross-platform sed compatibility
find . -maxdepth 2 -name "action.yml" -path "*/action.yml" ! -path "./_*" ! -path "./.github/*" -exec sed -i.bak \
"s|ivuorinen/actions/\([a-z-]*\)@[a-f0-9]\{40\}|ivuorinen/actions/\1@$TAG_SHA|g" {} \;
# Remove backup files
find . -maxdepth 2 -name "action.yml.bak" -path "*/action.yml.bak" ! -path "./_*" ! -path "./.github/*" -delete
echo "Action references updated successfully"
- name: Commit Changes
if: steps.check-update.outputs.updated == 'true'
id: commit
shell: sh
env:
MAJOR_VERSION: ${{ inputs.major-version }}
TAG_SHA: ${{ steps.fetch-sha.outputs.tag-sha }}
run: |
set -eu
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -- */action.yml
if git diff --staged --quiet; then
echo "No changes to commit"
printf '%s\n' "sha=" >> "$GITHUB_OUTPUT"
else
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 "" \
-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"
echo "Created commit: $commit_sha"
fi

View File

@@ -0,0 +1,37 @@
---
# Validation rules for action-versioning action
# Generated by update-validators.py v1.0.0 - DO NOT EDIT MANUALLY
# Schema version: 1.0
# Coverage: 100% (2/2 inputs)
#
# This file defines validation rules for the action-versioning GitHub Action.
# Rules are automatically applied by validate-inputs action when this
# action is used.
#
schema_version: '1.0'
action: action-versioning
description: Automatically update SHA-pinned action references to match latest version tags
generator_version: 1.0.0
required_inputs:
- major-version
optional_inputs:
- token
conventions:
major-version: semantic_version
token: github_token
overrides: {}
statistics:
total_inputs: 2
validated_inputs: 2
skipped_inputs: 0
coverage_percentage: 100
validation_coverage: 100
auto_detected: true
manual_review_required: false
quality_indicators:
has_required_inputs: true
has_token_validation: true
has_version_validation: true
has_file_validation: false
has_security_validation: true

View File

@@ -112,7 +112,7 @@ runs:
- name: Cache Python Dependencies
if: steps.check-files.outputs.files_found == 'true'
id: cache-pip
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'pip'
paths: '~/.cache/pip'
@@ -120,8 +120,9 @@ runs:
key-prefix: 'ansible-lint-fix'
- name: Install ansible-lint
id: install-ansible-lint
if: steps.check-files.outputs.files_found == 'true'
uses: ./common-retry
uses: ivuorinen/actions/common-retry@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
command: 'pip install ansible-lint==6.22.1'
max-retries: ${{ inputs.max-retries }}
@@ -159,8 +160,9 @@ runs:
exit "$lint_exit_code"
- name: Set Git Config for Fixes
id: set-git-config
if: steps.check-files.outputs.files_found == 'true'
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
@@ -182,6 +184,6 @@ runs:
- name: Upload SARIF Report
if: steps.check-files.outputs.files_found == 'true'
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: ansible-lint.sarif

View File

@@ -44,9 +44,9 @@ runs:
using: composite
steps:
- name: Validate Inputs (Centralized)
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action: biome-check
action-type: biome-check
- name: Validate Inputs (Additional)
id: validate
@@ -112,7 +112,7 @@ runs:
token: ${{ inputs.token }}
- name: Set Git Config
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
@@ -120,11 +120,11 @@ runs:
- name: Node Setup
id: node-setup
uses: ./node-setup
uses: ivuorinen/actions/node-setup@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Cache Node Dependencies
id: cache
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'npm'
paths: 'node_modules'
@@ -233,6 +233,6 @@ runs:
- name: Upload Biome Results
if: always()
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: biome-report.sarif

View File

@@ -41,44 +41,48 @@ runs:
steps:
- name: Validate Inputs
id: validate
shell: bash
shell: sh
env:
GITHUB_TOKEN: ${{ inputs.token }}
EMAIL: ${{ inputs.email }}
USERNAME: ${{ inputs.username }}
MAX_RETRIES: ${{ inputs.max-retries }}
run: |
set -euo pipefail
set -eu
# Validate GitHub token format (basic validation)
if [[ -n "$GITHUB_TOKEN" ]]; then
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
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)
if [[ "$EMAIL" != *"@"* ]] || [[ "$EMAIL" != *"."* ]]; then
echo "::error::Invalid email format: '$EMAIL'. Expected valid email address"
exit 1
fi
case "$EMAIL" in
*@*.*) ;;
*)
echo "::error::Invalid email format: '$EMAIL'. Expected valid email address"
exit 1
;;
esac
# Validate username format (prevent command injection)
if [[ "$USERNAME" =~ [;&|] ]]; then
if echo "$USERNAME" | grep -Eq '[;&|]'; 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"
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 ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then
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
@@ -91,7 +95,7 @@ runs:
token: ${{ inputs.token }}
- name: Set Git Config
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
@@ -99,11 +103,11 @@ runs:
- name: Node Setup
id: node-setup
uses: ./node-setup
uses: ivuorinen/actions/node-setup@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Cache Node Dependencies
id: cache
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'npm'
paths: 'node_modules'
@@ -111,12 +115,12 @@ runs:
key-prefix: 'biome-fix-${{ steps.node-setup.outputs.package-manager }}'
- name: Install Biome
shell: bash
shell: sh
env:
PACKAGE_MANAGER: ${{ steps.node-setup.outputs.package-manager }}
MAX_RETRIES: ${{ inputs.max-retries }}
run: |
set -euo pipefail
set -eu
# Check if biome is already installed
if command -v biome >/dev/null 2>&1; then
@@ -167,9 +171,9 @@ runs:
- name: Run Biome Fix
id: fix
shell: bash
shell: sh
run: |
set -euo pipefail
set -eu
echo "Running Biome fix..."

View File

@@ -112,7 +112,7 @@ runs:
using: composite
steps:
- name: Validate inputs
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action-type: codeql-analysis
language: ${{ inputs.language }}
@@ -189,7 +189,7 @@ runs:
echo "Using build mode: $build_mode"
- name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ inputs.language }}
queries: ${{ inputs.queries }}
@@ -202,12 +202,12 @@ runs:
threads: ${{ inputs.threads }}
- name: Autobuild
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
if: ${{ steps.set-build-mode.outputs.build-mode == 'autobuild' }}
- name: Perform CodeQL Analysis
id: analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: ${{ steps.set-category.outputs.category }}
upload: ${{ inputs.upload-results }}

View File

@@ -143,7 +143,7 @@ runs:
fi
- name: Set Git Config
id: set-git-config
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}

View File

@@ -50,7 +50,7 @@ runs:
- name: Detect .NET SDK Version
id: detect-dotnet-version
uses: ./dotnet-version-detect
uses: ivuorinen/actions/dotnet-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
default-version: "${{ inputs.dotnet-version || '7.0' }}"
@@ -61,7 +61,7 @@ runs:
- name: Cache NuGet packages
id: cache-nuget
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'nuget'
paths: '~/.nuget/packages'
@@ -70,7 +70,7 @@ runs:
- name: Restore Dependencies
if: steps.cache-nuget.outputs.cache-hit != 'true'
uses: ./common-retry
uses: ivuorinen/actions/common-retry@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
command: |
echo "Restoring .NET dependencies..."
@@ -124,7 +124,7 @@ runs:
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: csharp-test-results
path: |

View File

@@ -66,7 +66,7 @@ runs:
- name: Detect .NET SDK Version
id: detect-dotnet-version
uses: ./dotnet-version-detect
uses: ivuorinen/actions/dotnet-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
default-version: ${{ inputs.dotnet-version || '7.0' }}
@@ -111,6 +111,6 @@ runs:
fi
- name: Upload SARIF Report
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: dotnet-format.sarif

View File

@@ -51,7 +51,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action-type: 'csharp-publish'
token: ${{ inputs.token }}
@@ -60,7 +60,7 @@ runs:
- name: Detect .NET SDK Version
id: detect-dotnet-version
uses: ./dotnet-version-detect
uses: ivuorinen/actions/dotnet-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
default-version: '7.0'
@@ -71,7 +71,7 @@ runs:
- name: Cache NuGet packages
id: cache-nuget
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'nuget'
paths: '~/.nuget/packages'
@@ -116,11 +116,11 @@ runs:
if [ -n "$PACKAGE_FILE" ]; then
# Extract version from filename (assumes standard naming: PackageName.Version.nupkg)
VERSION=$(basename "$PACKAGE_FILE" .nupkg | sed 's/.*\.\([0-9]\+\.[0-9]\+\.[0-9]\+.*\)$/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "package_file=$PACKAGE_FILE" >> "$GITHUB_OUTPUT"
printf '%s\n' "version=$VERSION" >> "$GITHUB_OUTPUT"
printf '%s\n' "package_file=$PACKAGE_FILE" >> "$GITHUB_OUTPUT"
else
echo "version=unknown" >> "$GITHUB_OUTPUT"
echo "package_file=" >> "$GITHUB_OUTPUT"
printf '%s\n' "version=unknown" >> "$GITHUB_OUTPUT"
printf '%s\n' "package_file=" >> "$GITHUB_OUTPUT"
fi
- name: Publish Package
@@ -133,7 +133,7 @@ runs:
set -euo pipefail
PACKAGE_URL="https://github.com/$NAMESPACE/packages/nuget"
echo "package_url=$PACKAGE_URL" >> $GITHUB_OUTPUT
printf '%s\n' "package_url=$PACKAGE_URL" >> "$GITHUB_OUTPUT"
# First attempt
if ! dotnet nuget push ./artifacts/*.nupkg \
@@ -159,4 +159,4 @@ runs:
env:
PUBLISH_STATUS: ${{ steps.publish-package.outcome == 'success' && 'success' || 'failure' }}
run: |-
echo "status=$PUBLISH_STATUS" >> $GITHUB_OUTPUT
printf '%s\n' "status=$PUBLISH_STATUS" >> "$GITHUB_OUTPUT"

View File

@@ -147,7 +147,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action-type: 'docker-build'
image-name: ${{ inputs.image-name }}

View File

@@ -170,7 +170,7 @@ runs:
- name: Build Multi-Arch Docker Image
id: build
uses: ./docker-build
uses: ivuorinen/actions/docker-build@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
tag: ${{ steps.tags.outputs.all-tags }}
architectures: ${{ inputs.platforms }}
@@ -185,7 +185,7 @@ runs:
- name: Publish to Docker Hub
id: publish-dockerhub
if: contains(steps.dest.outputs.reg, 'dockerhub')
uses: ./docker-publish-hub
uses: ivuorinen/actions/docker-publish-hub@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
tags: ${{ steps.tags.outputs.all-tags }}
platforms: ${{ inputs.platforms }}
@@ -201,7 +201,7 @@ runs:
- name: Publish to GitHub Packages
id: publish-github
if: contains(steps.dest.outputs.reg, 'github')
uses: ./docker-publish-gh
uses: ivuorinen/actions/docker-publish-gh@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
tags: ${{ steps.tags.outputs.all-tags }}
platforms: ${{ inputs.platforms }}

View File

@@ -58,7 +58,7 @@ runs:
- name: Parse .NET Version
id: parse-version
uses: ./version-file-parser
uses: ivuorinen/actions/version-file-parser@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
language: 'dotnet'
tool-versions-key: 'dotnet'

View File

@@ -176,11 +176,11 @@ runs:
- name: Setup Node.js
id: node-setup
uses: ./node-setup
uses: ivuorinen/actions/node-setup@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Cache Node Dependencies
id: cache
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'npm'
paths: 'node_modules'
@@ -414,7 +414,7 @@ runs:
- name: Upload ESLint Results
if: always() && inputs.report-format == 'sarif'
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: ${{ inputs.working-directory }}/reports/eslint.sarif
category: eslint

View File

@@ -44,7 +44,7 @@ runs:
steps:
- name: Validate Inputs
id: validate
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action-type: 'eslint-fix'
token: ${{ inputs.token }}
@@ -58,7 +58,7 @@ runs:
token: ${{ inputs.token }}
- name: Set Git Config
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
@@ -66,11 +66,11 @@ runs:
- name: Node Setup
id: node-setup
uses: ./node-setup
uses: ivuorinen/actions/node-setup@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Cache Node Dependencies
id: cache
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'npm'
paths: 'node_modules'

View File

@@ -17,6 +17,7 @@ const CATEGORIES = {
'dotnet-version-detect': 'Setup',
// Utilities
'action-versioning': 'Utilities',
'version-file-parser': 'Utilities',
'version-validator': 'Utilities',
@@ -236,6 +237,14 @@ function generateQuickReference(actions) {
return markdownTable(rows, { align: ['c', 'l', 'l', 'l', 'l'] });
}
/**
* Generate per-category Markdown sections containing tables of actions and their brief details.
*
* Sections appear in a fixed priority order: Setup, Utilities, Linting, Testing, Build, Publishing, Repository, Validation.
*
* @param {Array<Object>} actions - Array of action metadata objects. Each object should include at least: `name`, `description`, `category`, `icon`, `languages` (array), and `features` (array).
* @returns {string} A Markdown string with one section per category (when present), each containing a table of actions with columns: Action, Description, Languages, and Features.
*/
function generateCategoryTables(actions) {
const categories = {};
@@ -257,8 +266,9 @@ function generateCategoryTables(actions) {
const categoryActions = categories[category];
const icon = CATEGORY_ICONS[category] || '📦';
const actionWord = categoryActions.length === 1 ? 'action' : 'actions';
output += `\n#### ${icon} ${category} (${categoryActions.length} actions)\n\n`;
output += `\n#### ${icon} ${category} (${categoryActions.length} ${actionWord})\n\n`;
const rows = [['Action', 'Description', 'Languages', 'Features']];
@@ -319,6 +329,15 @@ function generateReferenceLinks(actions) {
return `\n<!-- Reference Links -->\n${links}\n`;
}
/**
* Builds the complete Markdown catalog for all discovered actions in the repository.
*
* The generated content includes a quick reference, per-category tables, a feature matrix,
* language support matrix, usage examples with recommended pinned refs, action reference links,
* and a closing separator.
*
* @returns {string} The assembled catalog as a Markdown-formatted string.
*/
function generateCatalogContent() {
const actions = getAllActions();
const totalCount = actions.length;
@@ -341,10 +360,17 @@ function generateCatalogContent() {
content += `\n\n### Action Usage\n\n`;
content += 'All actions can be used independently in your workflows:\n\n';
content += '```yaml\n';
content += '- uses: ivuorinen/actions/action-name@main\n';
content += '# Recommended: Use pinned refs for supply-chain security\n';
content += '- uses: ivuorinen/actions/action-name@vYYYY-MM-DD # Date-based tag (example)\n';
content += ' with:\n';
content += ' # action-specific inputs\n';
content += '```\n';
content += '\n';
content += '# Alternative: Use commit SHA for immutability\n';
content += '- uses: ivuorinen/actions/action-name@abc123def456 # Full commit SHA\n';
content += ' with:\n';
content += ' # action-specific inputs\n';
content += '```\n\n';
content += '> **Security Note**: Always pin to specific tags or commit SHAs instead of `@main` to ensure reproducible workflows and supply-chain integrity.\n';
// Add reference links before the timestamp
content += generateReferenceLinks(actions);

View File

@@ -54,7 +54,7 @@ runs:
- name: Detect Go Version
id: detect-go-version
uses: ./go-version-detect
uses: ivuorinen/actions/go-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
default-version: "${{ inputs.go-version || '1.21' }}"
@@ -66,7 +66,7 @@ runs:
- name: Cache Go Dependencies
id: cache-go
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'go'
paths: '~/go/pkg/mod'
@@ -75,7 +75,7 @@ runs:
- name: Download Dependencies
if: steps.cache-go.outputs.cache-hit != 'true'
uses: ./common-retry
uses: ivuorinen/actions/common-retry@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
command: |
echo "Downloading Go dependencies..."
@@ -163,7 +163,7 @@ runs:
- name: Upload Build Artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: go-build-artifacts
path: |

View File

@@ -86,7 +86,7 @@ runs:
steps:
- name: Validate Inputs
id: validate
shell: bash
shell: sh
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
GOLANGCI_LINT_VERSION: ${{ inputs.golangci-lint-version }}
@@ -102,7 +102,7 @@ runs:
ENABLE_LINTERS: ${{ inputs.enable-linters }}
DISABLE_LINTERS: ${{ inputs.disable-linters }}
run: |
set -euo pipefail
set -eu
# Validate working directory exists
if [ ! -d "$WORKING_DIRECTORY" ]; then
@@ -111,49 +111,56 @@ runs:
fi
# Validate working directory path security (prevent traversal)
if [[ "$WORKING_DIRECTORY" == *".."* ]]; then
echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed"
exit 1
fi
case "$WORKING_DIRECTORY" in
*..*)
echo "::error::Invalid working directory path: '$WORKING_DIRECTORY'. Path traversal not allowed"
exit 1
;;
esac
# Validate golangci-lint version format
if [[ -n "$GOLANGCI_LINT_VERSION" ]] && [[ "$GOLANGCI_LINT_VERSION" != "latest" ]]; then
if ! [[ "$GOLANGCI_LINT_VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
if [ -n "$GOLANGCI_LINT_VERSION" ] && [ "$GOLANGCI_LINT_VERSION" != "latest" ]; then
if ! echo "$GOLANGCI_LINT_VERSION" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$'; then
echo "::error::Invalid golangci-lint-version format: '$GOLANGCI_LINT_VERSION'. Expected format: vX.Y.Z or 'latest' (e.g., v1.55.2, latest)"
exit 1
fi
fi
# Validate Go version format
if [[ -n "$GO_VERSION" ]] && [[ "$GO_VERSION" != "stable" ]]; then
if ! [[ "$GO_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then
if [ -n "$GO_VERSION" ] && [ "$GO_VERSION" != "stable" ]; then
if ! echo "$GO_VERSION" | grep -Eq '^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$'; then
echo "::error::Invalid go-version format: '$GO_VERSION'. Expected format: X.Y or X.Y.Z or 'stable' (e.g., 1.21, 1.21.5, stable)"
exit 1
fi
fi
# Validate config file path if not default
if [[ "$CONFIG_FILE" != ".golangci.yml" ]] && [[ "$CONFIG_FILE" == *".."* ]]; then
echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed"
exit 1
if [ "$CONFIG_FILE" != ".golangci.yml" ]; then
case "$CONFIG_FILE" in
*..*)
echo "::error::Invalid config file path: '$CONFIG_FILE'. Path traversal not allowed"
exit 1
;;
esac
fi
# Validate timeout format (duration with unit)
if ! [[ "$TIMEOUT" =~ ^[0-9]+(ns|us|µs|ms|s|m|h)$ ]]; then
if ! echo "$TIMEOUT" | grep -Eq '^[0-9]+(ns|us|µs|ms|s|m|h)$'; then
echo "::error::Invalid timeout format: '$TIMEOUT'. Expected format with unit: 5m, 1h, 300s (e.g., 5m, 30s, 2h)"
exit 1
fi
# Validate boolean inputs
validate_boolean() {
local value="$1"
local name="$2"
_value="$1"
_name="$2"
_value_lower=$(echo "$_value" | tr '[:upper:]' '[:lower:]')
case "${value,,}" in
case "$_value_lower" in
true|false)
;;
*)
echo "::error::Invalid boolean value for $name: '$value'. Expected: true or false"
echo "::error::Invalid boolean value for $_name: '$_value'. Expected: true or false"
exit 1
;;
esac
@@ -176,19 +183,19 @@ runs:
esac
# Validate max retries (positive integer with reasonable upper limit)
if ! [[ "$MAX_RETRIES" =~ ^[0-9]+$ ]] || [ "$MAX_RETRIES" -le 0 ] || [ "$MAX_RETRIES" -gt 10 ]; then
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
# Validate linter lists if provided
validate_linter_list() {
local linter_list="$1"
local name="$2"
_linter_list="$1"
_name="$2"
if [[ -n "$linter_list" ]]; then
if ! [[ "$linter_list" =~ ^[a-zA-Z0-9]+(,[a-zA-Z0-9]+)*$ ]]; then
echo "::error::Invalid $name format: '$linter_list'. Expected comma-separated linter names (e.g., gosec,govet,staticcheck)"
if [ -n "$_linter_list" ]; then
if ! echo "$_linter_list" | grep -Eq '^[a-zA-Z0-9]+(,[a-zA-Z0-9]+)*$'; then
echo "::error::Invalid $_name format: '$_linter_list'. Expected comma-separated linter names (e.g., gosec,govet,staticcheck)"
exit 1
fi
fi
@@ -211,7 +218,7 @@ runs:
- name: Set up Cache
id: cache
if: inputs.cache == 'true'
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'go'
paths: '~/.cache/golangci-lint,~/.cache/go-build'
@@ -220,26 +227,29 @@ runs:
restore-keys: '${{ runner.os }}-golangci-${{ inputs.golangci-lint-version }}-'
- name: Install golangci-lint
shell: bash
shell: sh
env:
MAX_RETRIES: ${{ inputs.max-retries }}
GOLANGCI_LINT_VERSION: ${{ inputs.golangci-lint-version }}
run: |
set -euo pipefail
set -eu
# Function to install golangci-lint with retries
install_golangci_lint() {
local attempt=1
local max_attempts="$MAX_RETRIES"
local version="$GOLANGCI_LINT_VERSION"
_attempt=1
_max_attempts="$MAX_RETRIES"
_version="$GOLANGCI_LINT_VERSION"
while [ $attempt -le $max_attempts ]; do
echo "Installation attempt $attempt of $max_attempts"
while [ $_attempt -le $_max_attempts ]; do
echo "Installation attempt $_attempt of $_max_attempts"
# Add 'v' prefix if version is not 'latest' and doesn't already have it
install_version="$version"
if [[ "$version" != "latest" ]] && [[ "$version" != v* ]]; then
install_version="v$version"
install_version="$_version"
if [ "$_version" != "latest" ]; then
case "$_version" in
v*) ;;
*) install_version="v$_version" ;;
esac
fi
if curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
@@ -247,14 +257,14 @@ runs:
return 0
fi
attempt=$((attempt + 1))
if [ $attempt -le $max_attempts ]; then
_attempt=$((_attempt + 1))
if [ $_attempt -le $_max_attempts ]; then
echo "Installation failed, waiting 10 seconds before retry..."
sleep 10
fi
done
echo "::error::Failed to install golangci-lint after $max_attempts attempts"
echo "::error::Failed to install golangci-lint after $_max_attempts attempts"
return 1
}
@@ -262,13 +272,13 @@ runs:
- name: Prepare Configuration
id: config
shell: bash
shell: sh
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
CONFIG_FILE: ${{ inputs.config-file }}
TIMEOUT: ${{ inputs.timeout }}
run: |
set -euo pipefail
set -eu
cd "$WORKING_DIRECTORY"
@@ -314,7 +324,7 @@ runs:
- name: Run golangci-lint
id: lint
shell: bash
shell: sh
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
DISABLE_ALL: ${{ inputs.disable-all }}
@@ -327,7 +337,7 @@ runs:
REPORT_FORMAT: ${{ inputs.report-format }}
FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
run: |
set -euo pipefail
set -eu
cd "$WORKING_DIRECTORY"
@@ -403,19 +413,19 @@ runs:
- name: Upload Lint Results
if: always() && inputs.report-format == 'sarif'
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: ${{ inputs.working-directory }}/reports/golangci-lint.sarif
category: golangci-lint
- name: Cleanup
if: always()
shell: bash
shell: sh
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
CACHE: ${{ inputs.cache }}
run: |-
set -euo pipefail
set -eu
cd "$WORKING_DIRECTORY"

View File

@@ -30,14 +30,14 @@ runs:
steps:
- name: Validate Inputs
id: validate
shell: bash
shell: sh
env:
DEFAULT_VERSION: ${{ inputs.default-version }}
run: |
set -euo pipefail
set -eu
# Validate default-version format
if ! [[ "$DEFAULT_VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
if ! echo "$DEFAULT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 1.22, 1.21.5)"
exit 1
fi
@@ -65,7 +65,7 @@ runs:
- name: Parse Go Version
id: parse-version
uses: ./version-file-parser
uses: ivuorinen/actions/version-file-parser@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
language: 'go'
tool-versions-key: 'golang'

View File

@@ -176,7 +176,7 @@ runs:
- name: Parse Node.js Version
id: version
uses: ./version-file-parser
uses: ivuorinen/actions/version-file-parser@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
language: 'node'
tool-versions-key: 'nodejs'
@@ -275,7 +275,8 @@ runs:
with:
node-version: ${{ steps.version.outputs.detected-version }}
registry-url: ${{ inputs.registry-url }}
cache: false
# Note: cache parameter removed for actions/setup-node@v6 compatibility
# Caching is handled separately via common-cache action (step: Cache Dependencies)
- name: Enable Corepack
id: corepack
@@ -299,7 +300,7 @@ runs:
- name: Cache Dependencies
if: inputs.cache == 'true'
id: deps-cache
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'npm'
paths: '~/.npm,~/.yarn/cache,~/.pnpm-store,~/.bun/install/cache,node_modules'
@@ -359,7 +360,7 @@ runs:
- name: Install Dependencies
if: inputs.install == 'true' && steps.deps-cache.outputs.cache-hit != 'true'
uses: ./common-retry
uses: ivuorinen/actions/common-retry@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
command: |
package_manager="$PACKAGE_MANAGER"

View File

@@ -10,10 +10,10 @@ Publishes the package to the NPM registry with configurable scope and registry U
| name | description | required | default |
|-------------------|----------------------------------------|----------|----------------------------------------|
| `npm_token` | <p>NPM token.</p> | `true` | `""` |
| `registry-url` | <p>Registry URL for publishing.</p> | `false` | `https://registry.npmjs.org/` |
| `scope` | <p>Package scope to use.</p> | `false` | `@ivuorinen` |
| `package-version` | <p>The version to publish.</p> | `false` | `${{ github.event.release.tag_name }}` |
| `npm_token` | <p>NPM token.</p> | `true` | `""` |
| `token` | <p>GitHub token for authentication</p> | `false` | `""` |
### Outputs
@@ -33,6 +33,12 @@ This action is a `composite` action.
```yaml
- uses: ivuorinen/actions/npm-publish@main
with:
npm_token:
# NPM token.
#
# Required: true
# Default: ""
registry-url:
# Registry URL for publishing.
#
@@ -51,12 +57,6 @@ This action is a `composite` action.
# Required: false
# Default: ${{ github.event.release.tag_name }}
npm_token:
# NPM token.
#
# Required: true
# Default: ""
token:
# GitHub token for authentication
#

View File

@@ -12,6 +12,9 @@ branding:
color: green
inputs:
npm_token:
description: 'NPM token.'
required: true
registry-url:
description: 'Registry URL for publishing.'
required: false
@@ -24,10 +27,6 @@ inputs:
description: 'The version to publish.'
required: false
default: ${{ github.event.release.tag_name }}
npm_token:
description: 'NPM token.'
required: true
default: ''
token:
description: 'GitHub token for authentication'
required: false
@@ -48,43 +47,44 @@ runs:
using: composite
steps:
- name: Mask Secrets
shell: bash
shell: sh
env:
NPM_TOKEN: ${{ inputs.npm_token }}
run: |
set -eu
echo "::add-mask::$NPM_TOKEN"
- name: Validate Inputs
id: validate
shell: bash
shell: sh
env:
REGISTRY_URL: ${{ inputs.registry-url }}
PACKAGE_SCOPE: ${{ inputs.scope }}
PACKAGE_VERSION: ${{ inputs.package-version }}
NPM_TOKEN: ${{ inputs.npm_token }}
run: |
set -euo pipefail
set -eu
# Validate registry URL format
if ! [[ "$REGISTRY_URL" =~ ^https?://[a-zA-Z0-9.-]+(/.*)?/?$ ]]; then
if ! echo "$REGISTRY_URL" | grep -Eq '^https?://[a-zA-Z0-9.-]+(/.*)?/?$'; then
echo "::error::Invalid registry URL format: '$REGISTRY_URL'. Expected http:// or https:// URL (e.g., 'https://registry.npmjs.org/')"
exit 1
fi
# Validate package version format (semver)
if ! [[ "$PACKAGE_VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
if ! echo "$PACKAGE_VERSION" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$'; then
echo "::error::Invalid package version format: '$PACKAGE_VERSION'. Expected semantic version (e.g., '1.2.3', 'v1.2.3-alpha', '1.2.3+build')"
exit 1
fi
# Validate scope format (if provided)
if [[ -n "$PACKAGE_SCOPE" ]] && ! [[ "$PACKAGE_SCOPE" =~ ^@[a-z0-9-~][a-z0-9-._~]*$ ]]; then
if [ -n "$PACKAGE_SCOPE" ] && ! echo "$PACKAGE_SCOPE" | grep -Eq '^@[a-z0-9-~][a-z0-9-._~]*$'; then
echo "::error::Invalid NPM scope format: '$PACKAGE_SCOPE'. Expected format: @scope-name (e.g., '@myorg', '@my-org')"
exit 1
fi
# Validate NPM token is provided
if [[ -z "$NPM_TOKEN" ]]; then
if [ -z "$NPM_TOKEN" ]; then
echo "::error::NPM token is required for publishing"
exit 1
fi
@@ -101,29 +101,29 @@ runs:
token: ${{ inputs.token || github.token }}
- name: Setup Node.js
uses: ./node-setup
uses: ivuorinen/actions/node-setup@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Authenticate NPM
shell: bash
shell: sh
env:
REGISTRY_URL: ${{ inputs.registry-url }}
NPM_TOKEN: ${{ inputs.npm_token }}
run: |
set -euo pipefail
set -eu
registry_host="$(echo "$REGISTRY_URL" | sed -E 's#^https?://##; s#/$##')"
echo "//${registry_host}/:_authToken=$NPM_TOKEN" > ~/.npmrc
echo "always-auth=true" >> ~/.npmrc
- name: Publish Package
shell: bash
shell: sh
env:
REGISTRY_URL: ${{ inputs.registry-url }}
PACKAGE_SCOPE: ${{ inputs.scope }}
PACKAGE_VERSION: ${{ inputs.package-version }}
NPM_TOKEN: ${{ inputs.npm_token }}
run: |-
set -euo pipefail
set -eu
pkg_version=$(node -p "require('./package.json').version")
input_version="$PACKAGE_VERSION"

1
package-lock.json generated
View File

@@ -1079,6 +1079,7 @@
"integrity": "sha512-/4Osri9QFGCZOCTkfA8qJF+XGjKYERSHkXzxSyS1hd3ZERJGjvsUao2h4wdnvpHp6Tu2Jh/bPHM0FE9JJza6ng==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"globby": "14.1.0",
"js-yaml": "4.1.0",

View File

@@ -79,9 +79,9 @@ runs:
- name: Validate Inputs
id: validate
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action: php-composer
action-type: php-composer
- name: Setup PHP
id: php
@@ -176,10 +176,10 @@ runs:
- name: Cache Composer packages
id: composer-cache
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'composer'
paths: 'vendor,~/.composer/cache${{ inputs.cache-directories != "" && format(",{0}", inputs.cache-directories) || "" }}'
paths: vendor,~/.composer/cache${{ inputs.cache-directories != "" && format(",{0}", inputs.cache-directories) || "" }}
key-prefix: 'php-${{ inputs.php }}-composer-${{ inputs.composer-version }}'
key-files: 'composer.lock,composer.json'
restore-keys: |
@@ -196,9 +196,9 @@ runs:
composer clear-cache
- name: Install Dependencies
uses: ./common-retry
uses: ivuorinen/actions/common-retry@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
command: 'composer install ${{ inputs.args }}'
command: composer install ${{ inputs.args }}
max-retries: ${{ inputs.max-retries }}
retry-delay: '30'
description: 'Installing PHP dependencies via Composer'

View File

@@ -60,7 +60,7 @@ runs:
- name: Detect PHP Version
id: php-version
uses: ./php-version-detect
uses: ivuorinen/actions/php-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
default-version: ${{ inputs.php-version }}

View File

@@ -86,14 +86,14 @@ runs:
token: ${{ inputs.token || github.token }}
- name: Set Git Config
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token != '' && inputs.token || github.token }}
username: ${{ inputs.username }}
email: ${{ inputs.email }}
- name: Composer Install
uses: ./php-composer
uses: ivuorinen/actions/php-composer@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Run PHPUnit Tests
id: test

View File

@@ -67,7 +67,7 @@ runs:
- name: Parse PHP Version
id: parse-version
uses: ./version-file-parser
uses: ivuorinen/actions/version-file-parser@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
language: 'php'
tool-versions-key: 'php'

View File

@@ -40,9 +40,9 @@ runs:
steps:
- name: Validate Inputs
id: validate
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action: pr-lint
action-type: pr-lint
token: ${{ inputs.token }}
username: ${{ inputs.username }}
email: ${{ inputs.email }}
@@ -64,9 +64,9 @@ runs:
# ╰──────────────────────────────────────────────────────────╯
- name: Setup Git Config
id: git-config
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
token: ${{ inputs.token || github.token }}
username: ${{ inputs.username }}
email: ${{ inputs.email }}
@@ -87,7 +87,7 @@ runs:
- name: Setup Node.js environment
if: steps.detect-node.outputs.found == 'true'
uses: ./node-setup
uses: ivuorinen/actions/node-setup@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
install: true
cache: true
@@ -106,7 +106,7 @@ runs:
- name: Detect PHP Version
if: steps.detect-php.outputs.found == 'true'
id: php-version
uses: ./php-version-detect
uses: ivuorinen/actions/php-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Setup PHP
if: steps.detect-php.outputs.found == 'true'
@@ -150,7 +150,7 @@ runs:
- name: Detect Python Version
if: steps.detect-python.outputs.found == 'true'
id: python-version
uses: ./python-version-detect
uses: ivuorinen/actions/python-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Setup Python
if: steps.detect-python.outputs.found == 'true'
@@ -181,7 +181,7 @@ runs:
- name: Detect Go Version
if: steps.detect-go.outputs.found == 'true'
id: go-version
uses: ./go-version-detect
uses: ivuorinen/actions/go-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Setup Go
if: steps.detect-go.outputs.found == 'true'
@@ -252,7 +252,7 @@ runs:
# Upload MegaLinter artifacts
- name: Archive production artifacts
if: success() || failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: MegaLinter reports
include-hidden-files: 'true'
@@ -264,7 +264,12 @@ runs:
- name: Set APPLY_FIXES_IF var
shell: bash
env:
APPLY_FIXES_CONDITION: ${{ steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) }}
APPLY_FIXES_CONDITION: >-
${{
steps.ml.outputs.has_updated_sources == 1 &&
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
}}
run: |
set -euo pipefail

View File

@@ -49,7 +49,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action-type: 'pre-commit'
token: ${{ inputs.token }}
@@ -58,7 +58,7 @@ runs:
email: ${{ inputs.commit_email }}
username: ${{ inputs.commit_user }}
- name: Set Git Config
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
username: ${{ inputs.commit_user }}

View File

@@ -202,11 +202,11 @@ runs:
- name: Setup Node.js
id: node-setup
uses: ./node-setup
uses: ivuorinen/actions/node-setup@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Set up Cache
id: cache
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
if: inputs.cache == 'true'
with:
type: 'npm'
@@ -432,7 +432,7 @@ runs:
- name: Upload Prettier Results
if: always() && inputs.report-format == 'sarif'
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: ${{ inputs.working-directory }}/reports/prettier.sarif
category: prettier

View File

@@ -91,7 +91,7 @@ runs:
token: ${{ inputs.token }}
- name: Set Git Config
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
@@ -99,11 +99,11 @@ runs:
- name: Node Setup
id: node-setup
uses: ./node-setup
uses: ivuorinen/actions/node-setup@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
- name: Cache npm Dependencies
id: cache-npm
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'npm'
paths: 'node_modules'

View File

@@ -155,7 +155,7 @@ runs:
- name: Detect Python Version
id: python-version
uses: ./python-version-detect
uses: ivuorinen/actions/python-version-detect@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
default-version: ${{ inputs.python-version }}
@@ -189,7 +189,7 @@ runs:
- name: Cache Python Dependencies
if: steps.check-files.outputs.result == 'found'
id: cache-pip
uses: ./common-cache
uses: ivuorinen/actions/common-cache@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
type: 'pip'
paths: '~/.cache/pip'
@@ -325,7 +325,7 @@ runs:
- name: Set Git Config for Fixes
if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }}
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token }}
username: ${{ inputs.username }}
@@ -370,7 +370,7 @@ runs:
- name: Upload SARIF Report
if: steps.check-files.outputs.result == 'found'
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: ${{ inputs.working-directory }}/reports/flake8.sarif
category: 'python-lint'

View File

@@ -33,17 +33,21 @@ runs:
steps:
- name: Validate Inputs
id: validate
shell: bash
shell: sh
env:
DEFAULT_VERSION: ${{ inputs.default-version }}
run: |
set -euo pipefail
set -eu
# Validate default-version format
if ! [[ "$DEFAULT_VERSION" =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?$ ]]; then
echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 3.12, 3.11.5)"
exit 1
fi
case "$DEFAULT_VERSION" in
[0-9]*.[0-9]* | [0-9]*.[0-9]*.[0-9]*)
;;
*)
echo "::error::Invalid default-version format: '$DEFAULT_VERSION'. Expected format: X.Y or X.Y.Z (e.g., 3.12, 3.11.5)"
exit 1
;;
esac
# Check for reasonable version range (prevent malicious inputs)
major_version=$(echo "$DEFAULT_VERSION" | cut -d'.' -f1)
@@ -68,7 +72,7 @@ runs:
- name: Parse Python Version
id: parse-version
uses: ./version-file-parser
uses: ivuorinen/actions/version-file-parser@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
language: 'python'
tool-versions-key: 'python'

View File

@@ -65,7 +65,7 @@ runs:
- name: Parse Python Version
id: parse-version
uses: ./version-file-parser
uses: ivuorinen/actions/version-file-parser@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
language: 'python'
tool-versions-key: 'python'

View File

@@ -88,16 +88,6 @@ runs:
GIT_USERNAME="$VALIDATED_GIT_USERNAME"
GIT_EMAIL="$VALIDATED_GIT_EMAIL"
# Function to clean up Git config
cleanup_git_config() {
git config --local --unset-all "url.https://x-access-token:${TOKEN}@github.com/.insteadof" || true
git config --local --unset-all "user.name" || true
git config --local --unset-all "user.email" || true
}
# Set up trap to ensure cleanup on exit
trap cleanup_git_config EXIT
# Store token in variable to avoid repeated exposure
TOKEN="$GITHUB_TOKEN"

View File

@@ -43,9 +43,9 @@ runs:
- name: Validate Inputs
id: validate
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action: 'stale'
action-type: 'stale'
token: ${{ inputs.token || github.token }}
days-before-stale: ${{ inputs.days-before-stale }}
days-before-close: ${{ inputs.days-before-close }}

View File

@@ -78,7 +78,7 @@ runs:
- name: Validate Inputs
id: validate
uses: ./validate-inputs
uses: ivuorinen/actions/validate-inputs@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
action-type: 'terraform-lint-fix'
token: ${{ inputs.token || github.token }}
@@ -270,7 +270,7 @@ runs:
- name: Set Git Config for Fixes
if: ${{ fromJSON(steps.fix.outputs.fixed_count) > 0 }}
uses: ./set-git-config
uses: ivuorinen/actions/set-git-config@2f1c73dd8b23ffec17bb93ebd0f32e7a7c7546fc
with:
token: ${{ inputs.token || github.token }}
username: ${{ inputs.username }}
@@ -302,7 +302,7 @@ runs:
- name: Upload SARIF Report
if: steps.check-files.outputs.found == 'true' && inputs.format == 'sarif'
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
sarif_file: ${{ env.VALIDATED_WORKING_DIR }}/reports/tflint.sarif
category: terraform-lint

View File

@@ -10,7 +10,7 @@ Centralized Python-based input validation for GitHub Actions with PCRE regex sup
| name | description | required | default |
|---------------------|------------------------------------------------------------------------------------|----------|---------|
| `action` | <p>Action name to validate (alias for action-type)</p> | `true` | `""` |
| `action` | <p>Action name to validate (alias for action-type)</p> | `false` | `""` |
| `action-type` | <p>Type of action to validate (e.g., csharp-publish, docker-build, eslint-fix)</p> | `false` | `""` |
| `rules-file` | <p>Path to validation rules file</p> | `false` | `""` |
| `fail-on-error` | <p>Whether to fail on validation errors</p> | `false` | `true` |
@@ -81,7 +81,7 @@ This action is a `composite` action.
action:
# Action name to validate (alias for action-type)
#
# Required: true
# Required: false
# Default: ""
action-type:

View File

@@ -13,7 +13,7 @@ branding:
inputs:
action:
description: 'Action name to validate (alias for action-type)'
required: true
required: false
action-type:
description: 'Type of action to validate (e.g., csharp-publish, docker-build, eslint-fix)'
required: false

View File

@@ -48,9 +48,47 @@ class TestConventionsValidator:
rules = validator._rules
assert rules["action_type"] == "nonexistent-action"
assert rules["required_inputs"] == []
assert isinstance(rules["optional_inputs"], dict)
assert isinstance(rules["optional_inputs"], list)
assert isinstance(rules["conventions"], dict)
def test_load_rules_with_dict_optional_inputs(self, tmp_path):
"""Test backward compatibility with dict format for optional_inputs."""
# Create a rules file with legacy dict format for optional_inputs
rules_file = tmp_path / "legacy_rules.yml"
rules_file.write_text("""
action_type: legacy-action
required_inputs: []
optional_inputs:
foo: int
bar: str
baz:
type: boolean
validator: boolean
conventions: {}
overrides: {}
""")
# Load rules and verify conventions are built from dict keys
validator = ConventionBasedValidator("legacy-action")
rules = validator.load_rules(rules_file)
# Verify optional_inputs is preserved as-is from YAML
assert "optional_inputs" in rules
assert isinstance(rules["optional_inputs"], dict)
# Verify conventions were auto-generated from optional_inputs dict keys
assert "conventions" in rules
assert isinstance(rules["conventions"], dict)
conventions_keys = set(rules["conventions"].keys())
optional_keys = set(rules["optional_inputs"].keys())
assert conventions_keys == optional_keys, (
f"Conventions keys {conventions_keys} should match optional_inputs keys {optional_keys}"
)
# Verify each key from the dict is in conventions
assert "foo" in rules["conventions"]
assert "bar" in rules["conventions"]
assert "baz" in rules["conventions"]
def test_load_rules_with_custom_path(self, tmp_path):
"""Test loading rules from custom path."""
rules_file = tmp_path / "custom_rules.yml"

View File

@@ -32,7 +32,7 @@ def collect_inputs() -> dict[str, str]:
"""
inputs = {}
for key, value in os.environ.items():
if key.startswith("INPUT_") and key != "INPUT_ACTION_TYPE":
if key.startswith("INPUT_") and key not in ("INPUT_ACTION_TYPE", "INPUT_ACTION"):
input_name = key[6:].lower()
inputs[input_name] = value
@@ -73,8 +73,11 @@ def write_output(status: str, action_type: str, **kwargs) -> None:
def main() -> None:
"""Main validation entry point."""
# Get the action type from environment
action_type = os.environ.get("INPUT_ACTION_TYPE", "").strip()
# Get the action type from environment (check both INPUT_ACTION_TYPE and INPUT_ACTION)
action_type = (
os.environ.get("INPUT_ACTION_TYPE", "").strip()
or os.environ.get("INPUT_ACTION", "").strip()
)
if not action_type:
logger.error("::error::No action type provided")
sys.exit(1)

View File

@@ -82,7 +82,7 @@ class ConventionBasedValidator(BaseValidator):
return {
"action_type": self.action_type,
"required_inputs": [],
"optional_inputs": {},
"optional_inputs": [],
"conventions": {},
"overrides": {},
}
@@ -93,16 +93,27 @@ class ConventionBasedValidator(BaseValidator):
# Ensure all expected keys exist
rules.setdefault("required_inputs", [])
rules.setdefault("optional_inputs", {})
rules.setdefault("optional_inputs", [])
rules.setdefault("conventions", {})
rules.setdefault("overrides", {})
# Build conventions from optional_inputs if not explicitly set
if not rules["conventions"] and rules["optional_inputs"]:
conventions = {}
for input_name, input_config in rules["optional_inputs"].items():
# Try to infer validator type from the input name or pattern
conventions[input_name] = self._infer_validator_type(input_name, input_config)
optional_inputs = rules["optional_inputs"]
# Handle both list and dict formats for optional_inputs
if isinstance(optional_inputs, list):
# List format: just input names
for input_name in optional_inputs:
conventions[input_name] = self._infer_validator_type(input_name, {})
elif isinstance(optional_inputs, dict):
# Dict format: input names with config
for input_name, input_config in optional_inputs.items():
conventions[input_name] = self._infer_validator_type(
input_name, input_config
)
rules["conventions"] = conventions
return rules
@@ -110,7 +121,7 @@ class ConventionBasedValidator(BaseValidator):
return {
"action_type": self.action_type,
"required_inputs": [],
"optional_inputs": {},
"optional_inputs": [],
"conventions": {},
"overrides": {},
}
@@ -285,6 +296,8 @@ class ConventionBasedValidator(BaseValidator):
# Get conventions and overrides from rules
conventions = self._rules.get("conventions", {})
overrides = self._rules.get("overrides", {})
optional_inputs = self._rules.get("optional_inputs", [])
required_inputs = self.get_required_inputs()
# Validate each input
for input_name, value in inputs.items():
@@ -292,12 +305,28 @@ class ConventionBasedValidator(BaseValidator):
if input_name in overrides and overrides[input_name] is None:
continue
# Check if input is defined in the action's rules
is_defined_input = (
input_name in required_inputs
or input_name in optional_inputs
or input_name in conventions
or input_name in overrides
)
# Skip validation for undefined inputs with empty values
# This prevents auto-validation of irrelevant inputs from the
# validate-inputs action's own input list
if not is_defined_input and (
not value or (isinstance(value, str) and not value.strip())
):
continue
# Get validator type from overrides or conventions
validator_type = self._get_validator_type(input_name, conventions, overrides)
if validator_type:
# Check if this is a required input
is_required = input_name in self.get_required_inputs()
is_required = input_name in required_inputs
valid &= self._apply_validator(
input_name, value, validator_type, is_required=is_required
)