diff --git a/.github/examples/workflow-consumption-wordpress-plugin.yml b/.github/examples/workflow-consumption-wordpress-plugin.yml index 177c922ae6..09f6679d3d 100644 --- a/.github/examples/workflow-consumption-wordpress-plugin.yml +++ b/.github/examples/workflow-consumption-wordpress-plugin.yml @@ -1,81 +1,60 @@ -# Example: Using LightSpeed reusable workflows in a WordPress block plugin +# Example: CI for a WordPress block plugin # -# Place this in: .github/workflows/ci.yml (in your wordpress-block-plugin repo) +# Place this in: .github/workflows/ci.yml (in your plugin repo, e.g. one +# generated from lightspeedwp/block-plugin-scaffold). # -# This example shows how to consume reusable workflows from lightspeedwp/.github +# - Every action is pinned to a full commit SHA with its version as a +# comment; let Dependabot (github-actions ecosystem) keep the pins current. +# - `npm ci` and the npm cache need a committed `package-lock.json`. The +# scaffold's .gitignore excludes it: remove that line, run `npm install` +# and commit the lockfile before enabling this workflow. +# - Organisation reusable workflows from lightspeedwp/.github are not +# available yet (tracked in #3480). Once they are, call them with +# `uses: lightspeedwp/.github/.github/workflows/.yml@`. -name: Plugin CI/CD +name: Plugin CI on: push: branches: [main, develop] - paths: - - '**.js' - - '**.php' - - 'package.json' - - 'composer.json' pull_request: branches: [main, develop] workflow_dispatch: -jobs: - # Run AI feedback validation on PRs - ai-feedback: - if: github.event_name == 'pull_request' - uses: lightspeedwp/.github/workflows/ai-feedback-validation@main - with: - context: wordpress-plugin - severity: warning - secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Run integration tests using PR creation agent - integration-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - cache: npm - - uses: shivammathur/setup-php@v2 - with: - php-version: '8.1' +permissions: + contents: read - # Use the reusable workflow if it supports your test setup - - name: Run integration tests - uses: lightspeedwp/.github/workflows/pr-creation-agent-integration-tests@main - secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true - # PHP linting +jobs: php-lint: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v4 - - uses: shivammathur/setup-php@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - php-version: '8.1' - - run: composer install + persist-credentials: false + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + # Use a PHP release in active support that your plugin supports. + php-version: '8.4' + tools: composer + - run: composer install --no-interaction --no-progress - run: composer run lint - # JavaScript linting - js-lint: + js-lint-and-test: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc cache: npm - - run: npm ci + - run: npm ci --ignore-scripts --no-audit --no-fund - run: npm run lint - - # Phase progression (on main branch pushes) - update-phase: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: lightspeedwp/.github/workflows/orchestrate-phase-progression@main - with: - repo-context: wordpress-plugin - secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: npm test diff --git a/.github/examples/workflow-consumption-wordpress-theme.yml b/.github/examples/workflow-consumption-wordpress-theme.yml index 58be241940..0457b717f5 100644 --- a/.github/examples/workflow-consumption-wordpress-theme.yml +++ b/.github/examples/workflow-consumption-wordpress-theme.yml @@ -1,10 +1,18 @@ -# Example: Using LightSpeed reusable workflows in a WordPress block theme +# Example: CI for a WordPress block theme # -# Place this in: .github/workflows/ci.yml (in your wordpress-block-theme repo) +# Place this in: .github/workflows/ci.yml (in your theme repo, e.g. one +# generated from lightspeedwp/block-theme-scaffold). # -# This example shows how to consume reusable workflows from lightspeedwp/.github +# - Every action is pinned to a full commit SHA with its version as a +# comment; let Dependabot (github-actions ecosystem) keep the pins current. +# - `npm ci` and the npm cache need a committed `package-lock.json`. The +# scaffold's .gitignore excludes it: remove that line, run `npm install` +# and commit the lockfile before enabling this workflow. +# - Organisation reusable workflows from lightspeedwp/.github are not +# available yet (tracked in #3480). Once they are, call them with +# `uses: lightspeedwp/.github/.github/workflows/.yml@`. -name: CI/CD Pipeline +name: Theme CI on: push: @@ -13,50 +21,34 @@ on: branches: [main, develop] workflow_dispatch: -jobs: - # Validate issue labels (if this is a PR) - validate-labels: - if: github.event_name == 'pull_request' - uses: lightspeedwp/.github/workflows/validate-issue-labels@main - # Note: workflow must be public or you must have access to lightspeedwp/.github - - # Collect AI feedback on PR changes - ai-feedback: - if: github.event_name == 'pull_request' - uses: lightspeedwp/.github/workflows/ai-feedback-validation@main - with: - context: wordpress-theme - secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +permissions: + contents: read - # Run orchestrated phase progression (if applicable to your project) - phase-progression: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: lightspeedwp/.github/workflows/orchestrate-phase-progression@main - with: - repo-context: wordpress-theme - secrets: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true - # Your custom jobs below - lint: +jobs: + lint-and-test: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - node-version-file: .nvmrc - cache: npm - - run: npm ci - - run: npm run lint - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: .nvmrc cache: npm - - run: npm ci + # The scaffold's `npm run lint` and `npm test` also run `composer run + # lint` (phpcs) and `composer run test` (phpunit), so PHP and the + # Composer dependencies must be installed first. + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + # Use a PHP release in active support that your theme supports. + php-version: '8.4' + tools: composer + - run: composer install --no-interaction --no-progress + - run: npm ci --ignore-scripts --no-audit --no-fund + - run: npm run lint - run: npm test diff --git a/.github/workflows/branch-name-validation.yml b/.github/workflows/branch-name-validation.yml index 0082865e8d..37e2f5180f 100644 --- a/.github/workflows/branch-name-validation.yml +++ b/.github/workflows/branch-name-validation.yml @@ -46,7 +46,7 @@ jobs: id: extract-branch run: | BRANCH_NAME="${GITHUB_REF#refs/heads/}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT" # Branch names can contain shell metacharacters ($, parentheses), so # every value derived from them is passed through env, never inlined. @@ -78,16 +78,16 @@ jobs: ERRORS=$(echo "$OUTPUT" | jq -r '.errors | join(", ")') SUGGESTED=$(echo "$OUTPUT" | jq -r '.suggested_name // empty') - echo "valid=$VALID" >> $GITHUB_OUTPUT - [ -n "$ERRORS" ] && echo "errors=$ERRORS" >> $GITHUB_OUTPUT - [ -n "$SUGGESTED" ] && echo "suggested_name=$SUGGESTED" >> $GITHUB_OUTPUT + echo "valid=$VALID" >> "$GITHUB_OUTPUT" + [ -n "$ERRORS" ] && echo "errors=$ERRORS" >> "$GITHUB_OUTPUT" + [ -n "$SUGGESTED" ] && echo "suggested_name=$SUGGESTED" >> "$GITHUB_OUTPUT" else # Fallback: simple string matching echo "Using fallback parsing" if echo "$OUTPUT" | grep -q '"valid": true'; then - echo "valid=true" >> $GITHUB_OUTPUT + echo "valid=true" >> "$GITHUB_OUTPUT" else - echo "valid=false" >> $GITHUB_OUTPUT + echo "valid=false" >> "$GITHUB_OUTPUT" fi fi diff --git a/.github/workflows/branch-validation-metrics-aggregator.yml b/.github/workflows/branch-validation-metrics-aggregator.yml index ba17e33135..8379aff150 100644 --- a/.github/workflows/branch-validation-metrics-aggregator.yml +++ b/.github/workflows/branch-validation-metrics-aggregator.yml @@ -159,10 +159,24 @@ jobs: - name: Prune old metrics artifacts shell: bash run: | + set -euo pipefail keep=30 - mapfile -t stale < <(find metrics-artifacts -maxdepth 1 -type f -name '*.json' -printf '%T@\t%p\n' 2>/dev/null | sort -rn -k1,1 -k2,2 | tail -n +$((keep + 1)) | cut -f2- || true) + if [ ! -d metrics-artifacts ]; then + echo "No metrics-artifacts directory; nothing to prune." + exit 0 + fi + # Newest first by filename: names start with a UTC timestamp + # (YYYYMMDD-HHMMSS-.json). mtime is useless here because + # checkout gives every file the same one. The pipeline runs + # synchronously into a NUL-delimited file so a failure fails this + # step (process substitution would hide it). + list=$(mktemp) + find metrics-artifacts -maxdepth 1 -type f -name '*.json' -print0 \ + | sort -z -r | tail -z -n +$((keep + 1)) > "$list" + mapfile -d '' -t stale < "$list" + rm -f -- "$list" if [ "${#stale[@]}" -gt 0 ]; then - printf '%s\n' "${stale[@]}" | xargs rm -f -- + rm -f -- "${stale[@]}" echo "Pruned ${#stale[@]} artifact(s), kept latest $keep." else echo "Nothing to prune (≤ $keep artifacts present)." diff --git a/.github/workflows/changelog-unified.yml b/.github/workflows/changelog-unified.yml index 63f5ae6462..bc10cbdbbf 100644 --- a/.github/workflows/changelog-unified.yml +++ b/.github/workflows/changelog-unified.yml @@ -214,12 +214,14 @@ jobs: cd .github/validation/changelog if [ "$CHANGELOG_CHANGED" == "false" ]; then - echo "passed=0" >> $GITHUB_OUTPUT - echo "failed=0" >> $GITHUB_OUTPUT - echo "warnings=0" >> $GITHUB_OUTPUT - echo "new_failed=0" >> $GITHUB_OUTPUT - echo "base_failed=0" >> $GITHUB_OUTPUT - echo "result=No changelog files modified in this PR" >> $GITHUB_OUTPUT + { + echo "passed=0" + echo "failed=0" + echo "warnings=0" + echo "new_failed=0" + echo "base_failed=0" + echo "result=No changelog files modified in this PR" + } >> "$GITHUB_OUTPUT" exit 0 fi @@ -245,21 +247,25 @@ jobs: RESULT=$(jq -r '.recommendation' validation-report.json) GATE_RESULT=$(jq -r '.ci_gate_result' validation-report.json) - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "failed=$FAILED" >> $GITHUB_OUTPUT - echo "warnings=0" >> $GITHUB_OUTPUT - echo "result=$RESULT" >> $GITHUB_OUTPUT - echo "gate_result=$GATE_RESULT" >> $GITHUB_OUTPUT + { + echo "passed=$PASSED" + echo "failed=$FAILED" + echo "warnings=0" + echo "result=$RESULT" + echo "gate_result=$GATE_RESULT" + } >> "$GITHUB_OUTPUT" cp validation-report.json ../../../.github/reports/validation-audits/latest-report.json 2>/dev/null || true else echo "❌ Validation report missing or malformed; failing closed" [ -f validation-stderr.log ] && cat validation-stderr.log - echo "passed=0" >> $GITHUB_OUTPUT - echo "failed=1" >> $GITHUB_OUTPUT - echo "warnings=0" >> $GITHUB_OUTPUT - echo "result=ERROR" >> $GITHUB_OUTPUT - echo "gate_result=fail" >> $GITHUB_OUTPUT + { + echo "passed=0" + echo "failed=1" + echo "warnings=0" + echo "result=ERROR" + echo "gate_result=fail" + } >> "$GITHUB_OUTPUT" FAILED=1 GATE_RESULT=fail fi @@ -286,8 +292,8 @@ jobs: NEW_FAILED=$((FAILED - BASE_FAILED)) if [ "$NEW_FAILED" -lt 0 ]; then NEW_FAILED=0; fi - echo "base_failed=$BASE_FAILED" >> $GITHUB_OUTPUT - echo "new_failed=$NEW_FAILED" >> $GITHUB_OUTPUT + echo "base_failed=$BASE_FAILED" >> "$GITHUB_OUTPUT" + echo "new_failed=$NEW_FAILED" >> "$GITHUB_OUTPUT" echo "PR failures: $FAILED, base failures: $BASE_FAILED, new: $NEW_FAILED" # Gate on new failures only; pre-existing Unreleased debt never blocks. diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index e12e57c88f..2e1baa4d0a 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -120,7 +120,7 @@ jobs: if: contains(fromJson('["all", "staleness"]'), env.AUDIT_SCOPE) run: | echo "Checking for stale README files (>6 months old)..." - find . -name "README.md" -type f | while read file; do + find . -name "README.md" -type f | while read -r file; do echo "Checking staleness for $file..." done @@ -465,10 +465,17 @@ jobs: - name: Maintenance summary if: always() + env: + JOB_STATUS: ${{ job.status }} + HAS_CHANGES: ${{ steps.changes.outputs.has_changes }} run: | - echo "## Documentation Maintenance Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY - echo "**Scope**: ${{ env.AUDIT_SCOPE }}" >> $GITHUB_STEP_SUMMARY - echo "**Dry Run**: ${{ env.DRY_RUN }}" >> $GITHUB_STEP_SUMMARY - echo "**Changes Made**: ${{ steps.changes.outputs.has_changes }}" >> $GITHUB_STEP_SUMMARY + # AUDIT_SCOPE and DRY_RUN are already job env; read them as shell + # variables rather than inlining expressions into the script. + { + echo "## Documentation Maintenance Summary" + echo "" + echo "**Status**: $JOB_STATUS" + echo "**Scope**: $AUDIT_SCOPE" + echo "**Dry Run**: $DRY_RUN" + echo "**Changes Made**: $HAS_CHANGES" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/pr-template-routing.yml b/.github/workflows/pr-template-routing.yml index 98448a65dd..98736c5044 100644 --- a/.github/workflows/pr-template-routing.yml +++ b/.github/workflows/pr-template-routing.yml @@ -107,10 +107,12 @@ jobs: echo "DEBUG: labels='$ALL_LABELS'" echo "DEBUG: scope='$SCOPE'" - echo "branch_type=$BRANCH_TYPE" >> $GITHUB_OUTPUT - echo "template=$TEMPLATE" >> $GITHUB_OUTPUT - echo "labels=$ALL_LABELS" >> $GITHUB_OUTPUT - echo "scope=$SCOPE" >> $GITHUB_OUTPUT + { + echo "branch_type=$BRANCH_TYPE" + echo "template=$TEMPLATE" + echo "labels=$ALL_LABELS" + echo "scope=$SCOPE" + } >> "$GITHUB_OUTPUT" echo "DEBUG: Outputs written successfully" @@ -124,10 +126,10 @@ jobs: TEMPLATE_FILE=".github/PULL_REQUEST_TEMPLATE/${ROUTE_TEMPLATE}.md" if [ -f "$TEMPLATE_FILE" ]; then echo "✓ Template found: $TEMPLATE_FILE" - echo "exists=true" >> $GITHUB_OUTPUT + echo "exists=true" >> "$GITHUB_OUTPUT" else echo "⚠ Template not found: $TEMPLATE_FILE" - echo "exists=false" >> $GITHUB_OUTPUT + echo "exists=false" >> "$GITHUB_OUTPUT" fi - name: Verify PR body matches template diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index da4124f79f..6e7c30f0ce 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -52,10 +52,9 @@ jobs: ./actionlint --version - name: Run actionlint on active workflows - env: - # shellcheck is preinstalled on ubuntu runners; error-level only - # so pre-existing style findings elsewhere do not gate this check. - SHELLCHECK_OPTS: --severity=error + # shellcheck (preinstalled on ubuntu runners) runs at its default + # severity: active workflows are clean at every level (#3478), so + # style and info findings gate this check too. run: | # Explicit file list, not repo-wide. Archived workflows are # frozen history and stay unlinted. Every active workflow must be diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e2caea4a4..fd79213fa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **PR Agent Consolidation & Portability** — Merged `agents/pr-creation-agent/` into `agents/pr-agent/` and restructured all six skills into the [Agent Skills specification](https://agentskills.io/specification) shape (`SKILL.md` + `scripts/` + `scripts/__tests__/` per skill), completing User Story 1 of spec 015. ([PR #3400](https://github.com/lightspeedwp/.github/pull/3400), [PR #3401](https://github.com/lightspeedwp/.github/pull/3401), [PR #3403](https://github.com/lightspeedwp/.github/pull/3403), [LS-4214](https://linear.app/lightspeedwp/issue/LS-4214/aiops-pr-agent-consolidate-and-make-portable-for-github-control-plane)) - **Faster, Safer Workflows** — Workflows run faster, stop the metrics loop and pin every action to a fixed commit. Dependabot, Mergify and CodeRabbit now skip dead or generated paths. (#3474, #3476) +- **Stricter Workflow Linting** — Workflow scripts now pass shell linting at every level, and the plugin and theme examples are valid and pinned to fixed commits. (#3478) ### Removed diff --git a/actionlint b/actionlint new file mode 100755 index 0000000000..c05eefe53e Binary files /dev/null and b/actionlint differ