From 6ebc5a21d579f6ac6d3c5584de48ab03212a3c30 Mon Sep 17 00:00:00 2001 From: Ismo Vuorinen Date: Thu, 23 Oct 2025 23:24:20 +0300 Subject: [PATCH] fix: local references, release workflow (#301) * fix: local references, release workflow * chore: apply cr comments --- .github/workflows/action-security.yml | 6 +- .github/workflows/pr-lint.yml | 4 +- .github/workflows/test-actions.yml | 4 +- .github/workflows/version-maintenance.yml | 127 ++++++++++++ .pre-commit-config.yaml | 2 +- .serena/memories/code_style_conventions.md | 141 ++++++++----- .serena/memories/development_standards.md | 104 +++++++++- .serena/memories/versioning_system.md | 219 +++++++++++++++++++++ CLAUDE.md | 33 +++- Makefile | 54 ++++- _tools/bump-major-version.sh | 94 +++++++++ _tools/check-version-refs.sh | 120 +++++++++++ _tools/docker-testing-tools/build.sh | 40 ++-- _tools/get-action-sha.sh | 41 ++++ _tools/release.sh | 102 ++++++++++ _tools/shared.sh | 124 ++++++++++++ _tools/update-action-refs.sh | 71 +++++++ action-versioning/README.md | 44 +++++ action-versioning/action.yml | 165 ++++++++++++++++ action-versioning/rules.yml | 37 ++++ ansible-lint-fix/action.yml | 8 +- biome-check/action.yml | 8 +- biome-fix/action.yml | 42 ++-- codeql-analysis/action.yml | 2 +- compress-images/action.yml | 2 +- csharp-build/action.yml | 6 +- csharp-lint-check/action.yml | 2 +- csharp-publish/action.yml | 18 +- docker-build/action.yml | 2 +- docker-publish/action.yml | 6 +- dotnet-version-detect/action.yml | 2 +- eslint-check/action.yml | 4 +- eslint-fix/action.yml | 8 +- go-build/action.yml | 6 +- go-lint/action.yml | 98 ++++----- go-version-detect/action.yml | 8 +- node-setup/action.yml | 6 +- npm-publish/action.yml | 32 +-- php-composer/action.yml | 10 +- php-laravel-phpunit/action.yml | 2 +- php-tests/action.yml | 4 +- php-version-detect/action.yml | 2 +- pr-lint/action.yml | 12 +- pre-commit/action.yml | 4 +- prettier-check/action.yml | 4 +- prettier-fix/action.yml | 6 +- python-lint-fix/action.yml | 6 +- python-version-detect-v2/action.yml | 18 +- python-version-detect/action.yml | 2 +- stale/action.yml | 2 +- terraform-lint-fix/action.yml | 4 +- 51 files changed, 1604 insertions(+), 264 deletions(-) create mode 100644 .github/workflows/version-maintenance.yml create mode 100644 .serena/memories/versioning_system.md create mode 100755 _tools/bump-major-version.sh create mode 100755 _tools/check-version-refs.sh create mode 100755 _tools/get-action-sha.sh create mode 100755 _tools/release.sh create mode 100755 _tools/shared.sh create mode 100755 _tools/update-action-refs.sh create mode 100644 action-versioning/README.md create mode 100644 action-versioning/action.yml create mode 100644 action-versioning/rules.yml diff --git a/.github/workflows/action-security.yml b/.github/workflows/action-security.yml index dae9ce8..93ae831 100644 --- a/.github/workflows/action-security.yml +++ b/.github/workflows/action-security.yml @@ -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 . &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 . &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 diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 0b33c06..99dbaad 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -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" diff --git a/.github/workflows/test-actions.yml b/.github/workflows/test-actions.yml index e9ecef2..61476fe 100644 --- a/.github/workflows/test-actions.yml +++ b/.github/workflows/test-actions.yml @@ -125,10 +125,10 @@ 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 diff --git a/.github/workflows/version-maintenance.yml b/.github/workflows/version-maintenance.yml new file mode 100644 index 0000000..a78e7c9 --- /dev/null +++ b/.github/workflows/version-maintenance.yml @@ -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@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5 + 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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + 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'] + }); diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36b0363..12884f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md index be1e3d5..a79e279 100644 --- a/.serena/memories/code_style_conventions.md +++ b/.serena/memories/code_style_conventions.md @@ -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@ - 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@ - 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 diff --git a/.serena/memories/development_standards.md b/.serena/memories/development_standards.md index 6f258c1..cdb55fa 100644 --- a/.serena/memories/development_standards.md +++ b/.serena/memories/development_standards.md @@ -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-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 diff --git a/.serena/memories/versioning_system.md b/.serena/memories/versioning_system.md new file mode 100644 index 0000000..deb2d9f --- /dev/null +++ b/.serena/memories/versioning_system.md @@ -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@` + +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. diff --git a/CLAUDE.md b/CLAUDE.md index 322a498..60642b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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@` + (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 diff --git a/Makefile b/Makefile index 0a0a185..83f26fa 100644 --- a/Makefile +++ b/Makefile @@ -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 lint format check clean install-tools test test-unit test-integration test-coverage generate-tests generate-tests-dry test-generate-tests docker-build docker-push docker-test docker-login docker-all release update-version-refs bump-major-version check-version-refs .DEFAULT_GOAL := help # Colors for output @@ -145,6 +145,41 @@ 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"; \ + echo "$(GREEN)โœ… Release created$(RESET)"; \ + echo ""; \ + echo "$(YELLOW)Next steps:$(RESET)"; \ + echo " 1. Review changes: git show HEAD"; \ + echo " 2. Push tags: git push origin main --tags --force-with-lease" + +update-version-refs: ## Update all action references to a specific version tag (usage: make update-version-refs MAJOR=v2025) + @if [ -z "$(MAJOR)" ]; then \ + 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 +251,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 diff --git a/_tools/bump-major-version.sh b/_tools/bump-major-version.sh new file mode 100755 index 0000000..6144789 --- /dev/null +++ b/_tools/bump-major-version.sh @@ -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 " + + 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' diff --git a/_tools/check-version-refs.sh b/_tools/check-version-refs.sh new file mode 100755 index 0000000..7e1f283 --- /dev/null +++ b/_tools/check-version-refs.sh @@ -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 diff --git a/_tools/docker-testing-tools/build.sh b/_tools/docker-testing-tools/build.sh index 043aa93..46a1e3d 100755 --- a/_tools/docker-testing-tools/build.sh +++ b/_tools/docker-testing-tools/build.sh @@ -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" diff --git a/_tools/get-action-sha.sh b/_tools/get-action-sha.sh new file mode 100755 index 0000000..eb42b7e --- /dev/null +++ b/_tools/get-action-sha.sh @@ -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 diff --git a/_tools/release.sh b/_tools/release.sh new file mode 100755 index 0000000..b282352 --- /dev/null +++ b/_tools/release.sh @@ -0,0 +1,102 @@ +#!/bin/sh +# Release script for creating versioned tags and updating action references +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" + +if [ -z "$VERSION" ]; then + printf '%b' "${RED}Error: VERSION argument required${NC}\n" + printf 'Usage: %s v2025.10.18\n' "$0" + exit 1 +fi + +# Validate version format +if ! validate_version "$VERSION"; then + printf '%b' "${RED}Error: Invalid version format: $VERSION${NC}\n" + printf 'Expected: vYYYY.MM.DD (e.g., v2025.10.18)\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" + +printf '%b' "${BLUE}Creating release $VERSION${NC}\n" +printf ' Major: %s\n' "$major" +printf ' Minor: %s\n' "$minor" +printf ' Patch: %s\n' "$patch" +printf '\n' + +# Get current commit SHA +current_sha=$(git rev-parse HEAD) +printf '%b' "Current HEAD: ${GREEN}$current_sha${NC}\n" +printf '\n' + +# Update all action references to current SHA +printf '%b' "${BLUE}Updating action references to $current_sha...${NC}\n" +"$SCRIPT_DIR/update-action-refs.sh" "$current_sha" "direct" + +# Commit the changes +if ! git diff --quiet; then + git add -- */action.yml + git commit -m "chore: update action references for release $VERSION + +This commit updates all internal action references to point to the current +commit SHA in preparation for release $VERSION." + + # Update SHA since we just created a new commit + current_sha=$(git rev-parse HEAD) + printf '%b' "${GREEN}โœ… Committed updated action references${NC}\n" + printf '%b' "New HEAD: ${GREEN}$current_sha${NC}\n" +else + printf '%b' "${BLUE}No changes to commit${NC}\n" +fi + +# Create/update tags +printf '%b' "${BLUE}Creating tags...${NC}\n" + +# Create patch tag +git tag -a "$patch" -m "Release $patch" +printf '%b' " ${GREEN}โœ“${NC} Created tag: $patch\n" + +# Move/create minor tag +if git rev-parse "$minor" >/dev/null 2>&1; then + git tag -f -a "$minor" -m "Latest $minor release: $patch" + printf '%b' " ${GREEN}โœ“${NC} Updated tag: $minor (force)\n" +else + git tag -a "$minor" -m "Latest $minor release: $patch" + printf '%b' " ${GREEN}โœ“${NC} Created tag: $minor\n" +fi + +# Move/create major tag +if git rev-parse "$major" >/dev/null 2>&1; then + git tag -f -a "$major" -m "Latest $major release: $patch" + printf '%b' " ${GREEN}โœ“${NC} Updated tag: $major (force)\n" +else + git tag -a "$major" -m "Latest $major release: $patch" + printf '%b' " ${GREEN}โœ“${NC} Created tag: $major\n" +fi + +printf '\n' +printf '%b' "${GREEN}โœ… Release $VERSION created successfully${NC}\n" +printf '\n' +printf '%b' "${YELLOW}All tags point to: $current_sha${NC}\n" +printf '\n' +printf '%b' "${BLUE}Tags created:${NC}\n" +printf ' %s\n' "$patch" +printf ' %s\n' "$minor" +printf ' %s\n' "$major" diff --git a/_tools/shared.sh b/_tools/shared.sh new file mode 100755 index 0000000..f577f67 --- /dev/null +++ b/_tools/shared.sh @@ -0,0 +1,124 @@ +#!/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 +validate_version() { + version="$1" + + # Check format: vYYYY.MM.DD using grep + if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}$'; then + return 1 + fi + + # 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 (1-12) + if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then + return 1 + fi + + # Validate day (1-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 +validate_minor_version() { + version="$1" + + # Check format: vYYYY.MM using grep + if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{1,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 (1-12) + if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then + return 1 + fi + + return 0 +} + +# 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 + printf '%b' "${RED}Error: git is not installed or not in PATH${NC}\n" >&2 + printf 'Please install git to use this script.\n' >&2 + exit 1 + fi +} + +# Create temp file with error checking +safe_mktemp() { + _temp_file="" + if ! _temp_file=$(mktemp); then + printf '%b' "${RED}Error: Failed to create temp file${NC}\n" >&2 + exit 1 + fi + printf '%s' "$_temp_file" +} diff --git a/_tools/update-action-refs.sh b/_tools/update-action-refs.sh new file mode 100755 index 0000000..fe64cac --- /dev/null +++ b/_tools/update-action-refs.sh @@ -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 diff --git a/action-versioning/README.md b/action-versioning/README.md new file mode 100644 index 0000000..fee7d5e --- /dev/null +++ b/action-versioning/README.md @@ -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` |

Major version tag to sync (e.g., v2025)

| `true` | `""` | +| `token` |

GitHub token for authentication

| `false` | `""` | + +### Outputs + +| name | description | +|---------------------|------------------------------------------------------------| +| `updated` |

Whether action references were updated (true/false)

| +| `commit-sha` |

SHA of the commit that was created (if any)

| +| `needs-annual-bump` |

Whether annual version bump is needed (true/false)

| + +### 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: "" +``` diff --git a/action-versioning/action.yml b/action-versioning/action.yml new file mode 100644 index 0000000..d0717fe --- /dev/null +++ b/action-versioning/action.yml @@ -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 " + + commit_sha=$(git rev-parse HEAD) + printf '%s\n' "sha=$commit_sha" >> "$GITHUB_OUTPUT" + echo "Created commit: $commit_sha" + fi diff --git a/action-versioning/rules.yml b/action-versioning/rules.yml new file mode 100644 index 0000000..62aedb3 --- /dev/null +++ b/action-versioning/rules.yml @@ -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 diff --git a/ansible-lint-fix/action.yml b/ansible-lint-fix/action.yml index 1b576d1..3780451 100644 --- a/ansible-lint-fix/action.yml +++ b/ansible-lint-fix/action.yml @@ -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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: token: ${{ inputs.token }} username: ${{ inputs.username }} diff --git a/biome-check/action.yml b/biome-check/action.yml index 8b76b84..2ddb648 100644 --- a/biome-check/action.yml +++ b/biome-check/action.yml @@ -44,7 +44,7 @@ runs: using: composite steps: - name: Validate Inputs (Centralized) - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: action: biome-check @@ -112,7 +112,7 @@ runs: token: ${{ inputs.token }} - name: Set Git Config - uses: ./set-git-config + uses: ivuorinen/actions/set-git-config@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - name: Cache Node Dependencies id: cache - uses: ./common-cache + uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: type: 'npm' paths: 'node_modules' diff --git a/biome-fix/action.yml b/biome-fix/action.yml index 3b156cc..150a598 100644 --- a/biome-fix/action.yml +++ b/biome-fix/action.yml @@ -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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - name: Cache Node Dependencies id: cache - uses: ./common-cache + uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9 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..." diff --git a/codeql-analysis/action.yml b/codeql-analysis/action.yml index 9ff21cb..5fc35ad 100644 --- a/codeql-analysis/action.yml +++ b/codeql-analysis/action.yml @@ -112,7 +112,7 @@ runs: using: composite steps: - name: Validate inputs - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: action-type: codeql-analysis language: ${{ inputs.language }} diff --git a/compress-images/action.yml b/compress-images/action.yml index e67ab31..f154582 100644 --- a/compress-images/action.yml +++ b/compress-images/action.yml @@ -143,7 +143,7 @@ runs: fi - name: Set Git Config id: set-git-config - uses: ./set-git-config + uses: ivuorinen/actions/set-git-config@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: token: ${{ inputs.token }} username: ${{ inputs.username }} diff --git a/csharp-build/action.yml b/csharp-build/action.yml index 7953b17..87aa39c 100644 --- a/csharp-build/action.yml +++ b/csharp-build/action.yml @@ -50,7 +50,7 @@ runs: - name: Detect .NET SDK Version id: detect-dotnet-version - uses: ./dotnet-version-detect + uses: ivuorinen/actions/dotnet-version-detect@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: command: | echo "Restoring .NET dependencies..." diff --git a/csharp-lint-check/action.yml b/csharp-lint-check/action.yml index c5da0df..bca2150 100644 --- a/csharp-lint-check/action.yml +++ b/csharp-lint-check/action.yml @@ -66,7 +66,7 @@ runs: - name: Detect .NET SDK Version id: detect-dotnet-version - uses: ./dotnet-version-detect + uses: ivuorinen/actions/dotnet-version-detect@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: default-version: ${{ inputs.dotnet-version || '7.0' }} diff --git a/csharp-publish/action.yml b/csharp-publish/action.yml index d066352..8ce8216 100644 --- a/csharp-publish/action.yml +++ b/csharp-publish/action.yml @@ -51,7 +51,7 @@ runs: - name: Validate Inputs id: validate - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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" diff --git a/docker-build/action.yml b/docker-build/action.yml index 0327dfe..0f3b329 100644 --- a/docker-build/action.yml +++ b/docker-build/action.yml @@ -147,7 +147,7 @@ runs: - name: Validate Inputs id: validate - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: action-type: 'docker-build' image-name: ${{ inputs.image-name }} diff --git a/docker-publish/action.yml b/docker-publish/action.yml index 47494fb..db4666f 100644 --- a/docker-publish/action.yml +++ b/docker-publish/action.yml @@ -170,7 +170,7 @@ runs: - name: Build Multi-Arch Docker Image id: build - uses: ./docker-build + uses: ivuorinen/actions/docker-build@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: tags: ${{ steps.tags.outputs.all-tags }} platforms: ${{ inputs.platforms }} diff --git a/dotnet-version-detect/action.yml b/dotnet-version-detect/action.yml index 7445a08..b66579b 100644 --- a/dotnet-version-detect/action.yml +++ b/dotnet-version-detect/action.yml @@ -58,7 +58,7 @@ runs: - name: Parse .NET Version id: parse-version - uses: ./version-file-parser + uses: ivuorinen/actions/version-file-parser@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: language: 'dotnet' tool-versions-key: 'dotnet' diff --git a/eslint-check/action.yml b/eslint-check/action.yml index a12f299..0b2e55c 100644 --- a/eslint-check/action.yml +++ b/eslint-check/action.yml @@ -176,11 +176,11 @@ runs: - name: Setup Node.js id: node-setup - uses: ./node-setup + uses: ivuorinen/actions/node-setup@7061aafd35a2f21b57653e34f2b634b2a19334a9 - name: Cache Node Dependencies id: cache - uses: ./common-cache + uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: type: 'npm' paths: 'node_modules' diff --git a/eslint-fix/action.yml b/eslint-fix/action.yml index 5790129..1edd8b1 100644 --- a/eslint-fix/action.yml +++ b/eslint-fix/action.yml @@ -44,7 +44,7 @@ runs: steps: - name: Validate Inputs id: validate - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - name: Cache Node Dependencies id: cache - uses: ./common-cache + uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: type: 'npm' paths: 'node_modules' diff --git a/go-build/action.yml b/go-build/action.yml index c42906a..4c8397e 100644 --- a/go-build/action.yml +++ b/go-build/action.yml @@ -54,7 +54,7 @@ runs: - name: Detect Go Version id: detect-go-version - uses: ./go-version-detect + uses: ivuorinen/actions/go-version-detect@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: command: | echo "Downloading Go dependencies..." diff --git a/go-lint/action.yml b/go-lint/action.yml index 8571544..e56a725 100644 --- a/go-lint/action.yml +++ b/go-lint/action.yml @@ -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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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" @@ -410,12 +420,12 @@ runs: - 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" diff --git a/go-version-detect/action.yml b/go-version-detect/action.yml index fa90bfc..5914562 100644 --- a/go-version-detect/action.yml +++ b/go-version-detect/action.yml @@ -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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: language: 'go' tool-versions-key: 'golang' diff --git a/node-setup/action.yml b/node-setup/action.yml index 558b65c..cc240ae 100644 --- a/node-setup/action.yml +++ b/node-setup/action.yml @@ -176,7 +176,7 @@ runs: - name: Parse Node.js Version id: version - uses: ./version-file-parser + uses: ivuorinen/actions/version-file-parser@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: language: 'node' tool-versions-key: 'nodejs' @@ -299,7 +299,7 @@ runs: - name: Cache Dependencies if: inputs.cache == 'true' id: deps-cache - uses: ./common-cache + uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: type: 'npm' paths: '~/.npm,~/.yarn/cache,~/.pnpm-store,~/.bun/install/cache,node_modules' @@ -359,7 +359,7 @@ runs: - name: Install Dependencies if: inputs.install == 'true' && steps.deps-cache.outputs.cache-hit != 'true' - uses: ./common-retry + uses: ivuorinen/actions/common-retry@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: command: | package_manager="$PACKAGE_MANAGER" diff --git a/npm-publish/action.yml b/npm-publish/action.yml index bea7c91..bd274af 100644 --- a/npm-publish/action.yml +++ b/npm-publish/action.yml @@ -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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - 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" diff --git a/php-composer/action.yml b/php-composer/action.yml index de21881..58b6b24 100644 --- a/php-composer/action.yml +++ b/php-composer/action.yml @@ -79,7 +79,7 @@ runs: - name: Validate Inputs id: validate - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: action: php-composer @@ -176,10 +176,10 @@ runs: - name: Cache Composer packages id: composer-cache - uses: ./common-cache + uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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' diff --git a/php-laravel-phpunit/action.yml b/php-laravel-phpunit/action.yml index b66d256..0b52573 100644 --- a/php-laravel-phpunit/action.yml +++ b/php-laravel-phpunit/action.yml @@ -60,7 +60,7 @@ runs: - name: Detect PHP Version id: php-version - uses: ./php-version-detect + uses: ivuorinen/actions/php-version-detect@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: default-version: ${{ inputs.php-version }} diff --git a/php-tests/action.yml b/php-tests/action.yml index 412d58d..b6269a7 100644 --- a/php-tests/action.yml +++ b/php-tests/action.yml @@ -86,14 +86,14 @@ runs: token: ${{ inputs.token || github.token }} - name: Set Git Config - uses: ./set-git-config + uses: ivuorinen/actions/set-git-config@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - name: Run PHPUnit Tests id: test diff --git a/php-version-detect/action.yml b/php-version-detect/action.yml index 656ed34..e7df490 100644 --- a/php-version-detect/action.yml +++ b/php-version-detect/action.yml @@ -67,7 +67,7 @@ runs: - name: Parse PHP Version id: parse-version - uses: ./version-file-parser + uses: ivuorinen/actions/version-file-parser@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: language: 'php' tool-versions-key: 'php' diff --git a/pr-lint/action.yml b/pr-lint/action.yml index b266c30..ae1c37b 100644 --- a/pr-lint/action.yml +++ b/pr-lint/action.yml @@ -40,7 +40,7 @@ runs: steps: - name: Validate Inputs id: validate - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: action: pr-lint token: ${{ inputs.token }} @@ -64,7 +64,7 @@ runs: # โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ - name: Setup Git Config id: git-config - uses: ./set-git-config + uses: ivuorinen/actions/set-git-config@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: token: ${{ inputs.token }} username: ${{ inputs.username }} @@ -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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - name: Setup Go if: steps.detect-go.outputs.found == 'true' diff --git a/pre-commit/action.yml b/pre-commit/action.yml index 39ab50f..3a2588a 100644 --- a/pre-commit/action.yml +++ b/pre-commit/action.yml @@ -49,7 +49,7 @@ runs: - name: Validate Inputs id: validate - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: token: ${{ inputs.token }} username: ${{ inputs.commit_user }} diff --git a/prettier-check/action.yml b/prettier-check/action.yml index 394513c..15c9542 100644 --- a/prettier-check/action.yml +++ b/prettier-check/action.yml @@ -202,11 +202,11 @@ runs: - name: Setup Node.js id: node-setup - uses: ./node-setup + uses: ivuorinen/actions/node-setup@7061aafd35a2f21b57653e34f2b634b2a19334a9 - name: Set up Cache id: cache - uses: ./common-cache + uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9 if: inputs.cache == 'true' with: type: 'npm' diff --git a/prettier-fix/action.yml b/prettier-fix/action.yml index e16f473..fcc420a 100644 --- a/prettier-fix/action.yml +++ b/prettier-fix/action.yml @@ -91,7 +91,7 @@ runs: token: ${{ inputs.token }} - name: Set Git Config - uses: ./set-git-config + uses: ivuorinen/actions/set-git-config@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 - name: Cache npm Dependencies id: cache-npm - uses: ./common-cache + uses: ivuorinen/actions/common-cache@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: type: 'npm' paths: 'node_modules' diff --git a/python-lint-fix/action.yml b/python-lint-fix/action.yml index 0621bbf..642f445 100644 --- a/python-lint-fix/action.yml +++ b/python-lint-fix/action.yml @@ -155,7 +155,7 @@ runs: - name: Detect Python Version id: python-version - uses: ./python-version-detect + uses: ivuorinen/actions/python-version-detect@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: token: ${{ inputs.token }} username: ${{ inputs.username }} diff --git a/python-version-detect-v2/action.yml b/python-version-detect-v2/action.yml index e2288f7..221a75d 100644 --- a/python-version-detect-v2/action.yml +++ b/python-version-detect-v2/action.yml @@ -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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: language: 'python' tool-versions-key: 'python' diff --git a/python-version-detect/action.yml b/python-version-detect/action.yml index e4f7e34..127122a 100644 --- a/python-version-detect/action.yml +++ b/python-version-detect/action.yml @@ -65,7 +65,7 @@ runs: - name: Parse Python Version id: parse-version - uses: ./version-file-parser + uses: ivuorinen/actions/version-file-parser@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: language: 'python' tool-versions-key: 'python' diff --git a/stale/action.yml b/stale/action.yml index 57027b5..2a8c9bc 100644 --- a/stale/action.yml +++ b/stale/action.yml @@ -43,7 +43,7 @@ runs: - name: Validate Inputs id: validate - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: action: 'stale' token: ${{ inputs.token || github.token }} diff --git a/terraform-lint-fix/action.yml b/terraform-lint-fix/action.yml index cbb0f8a..61cdca6 100644 --- a/terraform-lint-fix/action.yml +++ b/terraform-lint-fix/action.yml @@ -78,7 +78,7 @@ runs: - name: Validate Inputs id: validate - uses: ./validate-inputs + uses: ivuorinen/actions/validate-inputs@7061aafd35a2f21b57653e34f2b634b2a19334a9 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@7061aafd35a2f21b57653e34f2b634b2a19334a9 with: token: ${{ inputs.token || github.token }} username: ${{ inputs.username }}