From ef4637ec44045480c9d8ffe291f18c4968db3e89 Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 22 Feb 2026 12:09:47 +0200 Subject: [PATCH 1/5] ci: consolidate quality gates into single workflow Merge pest, phpstan, pint, rector, and ci-canary into a single quality-gates.yml with sequential job dependencies. Downstream bats-vm-ubuntu24 now triggers off one workflow_run signal instead of aggregating five, removing the manual github-script quality gate job. Simplify bats-cloud-janitor fork guard accordingly. --- .github/workflows/bats-cloud-janitor.yml | 1 - .github/workflows/bats-vm-ubuntu24.yml | 60 +-------- .github/workflows/ci-canary.yml | 65 ---------- .github/workflows/pest.yml | 46 ------- .github/workflows/phpstan.yml | 44 ------- .github/workflows/pint.yml | 48 -------- .github/workflows/quality-gates.yml | 149 +++++++++++++++++++++++ .github/workflows/rector.yml | 56 --------- 8 files changed, 151 insertions(+), 318 deletions(-) delete mode 100644 .github/workflows/ci-canary.yml delete mode 100644 .github/workflows/pest.yml delete mode 100644 .github/workflows/phpstan.yml delete mode 100644 .github/workflows/pint.yml create mode 100644 .github/workflows/quality-gates.yml delete mode 100644 .github/workflows/rector.yml diff --git a/.github/workflows/bats-cloud-janitor.yml b/.github/workflows/bats-cloud-janitor.yml index 15c66ed..094cfab 100644 --- a/.github/workflows/bats-cloud-janitor.yml +++ b/.github/workflows/bats-cloud-janitor.yml @@ -42,7 +42,6 @@ on: jobs: janitor: - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.head_repository.full_name == github.repository }} runs-on: ubuntu-24.04 timeout-minutes: 12 concurrency: diff --git a/.github/workflows/bats-vm-ubuntu24.yml b/.github/workflows/bats-vm-ubuntu24.yml index fd1b193..22cd8bf 100644 --- a/.github/workflows/bats-vm-ubuntu24.yml +++ b/.github/workflows/bats-vm-ubuntu24.yml @@ -6,69 +6,13 @@ permissions: on: workflow_run: - workflows: ["Pest", "PHPStan", "Pint", "Rector", "CI Canary"] + workflows: ["Quality Gates"] types: [completed] jobs: - quality-gate: - name: wait-for-quality-gates - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository }} - runs-on: ubuntu-latest - timeout-minutes: 3 - outputs: - ready: ${{ steps.evaluate.outputs.ready }} - - steps: - - name: Verify all quality workflows succeeded - id: evaluate - uses: actions/github-script@v7 - with: - script: | - const requiredWorkflows = ["Pest", "PHPStan", "Pint", "Rector", "CI Canary"]; - const headSha = context.payload.workflow_run.head_sha; - const eventName = context.payload.workflow_run.event; - const { owner, repo } = context.repo; - - const runsResponse = await github.rest.actions.listWorkflowRunsForRepo({ - owner, - repo, - event: eventName, - head_sha: headSha, - per_page: 100, - }); - - const latestRunByName = new Map(); - for (const run of runsResponse.data.workflow_runs) { - if (!requiredWorkflows.includes(run.name)) { - continue; - } - - const current = latestRunByName.get(run.name); - if (!current || run.id > current.id) { - latestRunByName.set(run.name, run); - } - } - - const missing = requiredWorkflows.filter((name) => !latestRunByName.has(name)); - const failed = requiredWorkflows - .map((name) => latestRunByName.get(name)) - .filter(Boolean) - .filter((run) => run.conclusion !== "success") - .map((run) => `${run.name}=${run.conclusion ?? "unknown"}`); - - const ready = missing.length === 0 && failed.length === 0; - core.setOutput("ready", ready ? "true" : "false"); - - if (!ready) { - core.notice( - `Quality gates not ready for ${headSha}: missing=[${missing.join(", ")}] failed=[${failed.join(", ")}]`, - ); - } - vm-tests-ubuntu24: - needs: quality-gate name: vm-tests (ubuntu24) - if: ${{ needs.quality-gate.outputs.ready == 'true' }} + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository }} runs-on: ubuntu-24.04 timeout-minutes: 12 concurrency: diff --git a/.github/workflows/ci-canary.yml b/.github/workflows/ci-canary.yml deleted file mode 100644 index 1e84e2f..0000000 --- a/.github/workflows/ci-canary.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: CI Canary - -permissions: - contents: read - -on: - pull_request: - paths: - - "tests/CICanary.php" - - "pint.json" - - "phpstan.neon" - - "rector-canary.php" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/ci-canary.yml" - push: - branches: - - main - paths: - - "tests/CICanary.php" - - "pint.json" - - "phpstan.neon" - - "rector-canary.php" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/ci-canary.yml" - -jobs: - canary: - runs-on: ubuntu-latest - timeout-minutes: 3 - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Verify Pint fails on CICanary class - run: | - if vendor/bin/pint --test tests/CICanary.php; then - echo "ERROR: Pint should have failed on CICanary" - exit 1 - fi - - - name: Verify Rector fails on CICanary class - run: | - mkdir -p /tmp/rector - if vendor/bin/rector --dry-run tests/CICanary.php --config rector-canary.php; then - echo "ERROR: Rector should have failed on CICanary" - exit 1 - fi - - - name: Verify PHPStan fails on CICanary class - run: | - if vendor/bin/phpstan analyse tests/CICanary.php --level=10; then - echo "ERROR: PHPStan should have failed on CICanary" - exit 1 - fi diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml deleted file mode 100644 index af8a55a..0000000 --- a/.github/workflows/pest.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Pest - -permissions: - contents: read - -on: - pull_request: - paths: - - "app/**" - - "tests/**" - - "phpunit.xml" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/pest.yml" - push: - branches: - - main - paths: - - "app/**" - - "tests/**" - - "phpunit.xml" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/pest.yml" - -jobs: - pest: - runs-on: ubuntu-latest - timeout-minutes: 3 - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - with: - coverage: "true" - - - name: Run tests with coverage - run: vendor/bin/pest --parallel --coverage --coverage-text diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml deleted file mode 100644 index b8ea45c..0000000 --- a/.github/workflows/phpstan.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: PHPStan - -permissions: - contents: read - -on: - pull_request: - paths: - - "app/**" - - "bin/**" - - "phpstan.neon" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/phpstan.yml" - push: - branches: - - main - paths: - - "app/**" - - "bin/**" - - "phpstan.neon" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/phpstan.yml" - -jobs: - phpstan: - runs-on: ubuntu-latest - timeout-minutes: 3 - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Run static analysis - run: vendor/bin/phpstan analyse --error-format=github diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml deleted file mode 100644 index 49b10aa..0000000 --- a/.github/workflows/pint.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Pint - -permissions: - contents: read - -on: - pull_request: - paths: - - "app/**" - - "bin/**" - - "tests/**" - - "*.php" - - "pint.json" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/pint.yml" - push: - branches: - - main - paths: - - "app/**" - - "bin/**" - - "tests/**" - - "*.php" - - "pint.json" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/pint.yml" - -jobs: - pint: - runs-on: ubuntu-latest - timeout-minutes: 3 - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Run code style check - run: vendor/bin/pint --test --parallel diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml new file mode 100644 index 0000000..2645c5d --- /dev/null +++ b/.github/workflows/quality-gates.yml @@ -0,0 +1,149 @@ +name: Quality Gates + +permissions: + contents: read + +on: + pull_request: + paths: + - ".github/**/*.yml" + - "app/**" + - "bin/**" + - "playbooks/**" + - "scaffolds/**" + - "tests/**" + - "**/*.php" + - "**/*.sh" + - "composer.json" + - "composer.lock" + - "phpstan.neon" + - "phpunit.xml" + - "pint.json" + push: + branches: + - main + paths: + - ".github/**/*.yml" + - "app/**" + - "bin/**" + - "playbooks/**" + - "scaffolds/**" + - "tests/**" + - "**/*.php" + - "**/*.sh" + - "composer.json" + - "composer.lock" + - "phpstan.neon" + - "phpunit.xml" + - "pint.json" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci-canary: + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Verify Pint fails on CICanary class + run: | + if vendor/bin/pint --test tests/CICanary.php; then + echo "ERROR: Pint should have failed on CICanary" + exit 1 + fi + + - name: Verify Rector fails on CICanary class + run: | + mkdir -p /tmp/rector + if vendor/bin/rector --dry-run tests/CICanary.php --config rector-canary.php; then + echo "ERROR: Rector should have failed on CICanary" + exit 1 + fi + + - name: Verify PHPStan fails on CICanary class + run: | + if vendor/bin/phpstan analyse tests/CICanary.php --level=10; then + echo "ERROR: PHPStan should have failed on CICanary" + exit 1 + fi + + pint: + needs: ci-canary + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Run code style check + run: vendor/bin/pint --test --parallel + + phpstan: + needs: pint + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Run static analysis + run: vendor/bin/phpstan analyse --error-format=github + + rector: + needs: phpstan + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - run: mkdir -p /tmp/rector + + - name: Cache Rector tmp dir + uses: actions/cache@v4 + with: + path: /tmp/rector + key: ${{ runner.os }}-rector-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-rector- + + - name: Run modern refactoring code checks + run: vendor/bin/rector --dry-run + + pest: + needs: rector + if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} + runs-on: ubuntu-latest + timeout-minutes: 3 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + with: + coverage: "true" + + - name: Run tests with coverage + run: vendor/bin/pest --parallel --coverage --coverage-text diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml deleted file mode 100644 index 346b2c0..0000000 --- a/.github/workflows/rector.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Rector - -permissions: - contents: read - -on: - pull_request: - paths: - - "app/**" - - "bin/**" - - "tests/**" - - "rector.php" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/rector.yml" - push: - branches: - - main - paths: - - "app/**" - - "bin/**" - - "tests/**" - - "rector.php" - - "composer.json" - - "composer.lock" - - ".github/actions/setup-php-composer/**" - - ".github/workflows/rector.yml" - -jobs: - rector: - runs-on: ubuntu-latest - timeout-minutes: 3 - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - run: mkdir -p /tmp/rector - - - name: Cache Rector tmp dir - uses: actions/cache@v4 - with: - path: /tmp/rector - key: ${{ runner.os }}-rector-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-rector- - - - name: Run modern refactoring code checks - run: vendor/bin/rector --dry-run From 8d636e13e5f014145f7f998c0ad646a73c1ba15c Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 22 Feb 2026 12:17:59 +0200 Subject: [PATCH 2/5] ci: run quality gate jobs in parallel Remove sequential needs: chain so pint, phpstan, rector, and pest all run concurrently instead of waiting on each other. --- .github/workflows/quality-gates.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml index 2645c5d..c75d4f8 100644 --- a/.github/workflows/quality-gates.yml +++ b/.github/workflows/quality-gates.yml @@ -76,7 +76,6 @@ jobs: fi pint: - needs: ci-canary runs-on: ubuntu-latest timeout-minutes: 3 @@ -91,7 +90,6 @@ jobs: run: vendor/bin/pint --test --parallel phpstan: - needs: pint runs-on: ubuntu-latest timeout-minutes: 3 @@ -106,7 +104,6 @@ jobs: run: vendor/bin/phpstan analyse --error-format=github rector: - needs: phpstan runs-on: ubuntu-latest timeout-minutes: 3 @@ -131,7 +128,6 @@ jobs: run: vendor/bin/rector --dry-run pest: - needs: rector if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} runs-on: ubuntu-latest timeout-minutes: 3 From 0527739550e322ef8d3bcacf4526c8a2deb7c95b Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 22 Feb 2026 12:24:46 +0200 Subject: [PATCH 3/5] fixup: temporary bats bridge --- .github/workflows/bats-vm-ubuntu24.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bats-vm-ubuntu24.yml b/.github/workflows/bats-vm-ubuntu24.yml index 22cd8bf..ae1783b 100644 --- a/.github/workflows/bats-vm-ubuntu24.yml +++ b/.github/workflows/bats-vm-ubuntu24.yml @@ -5,6 +5,10 @@ permissions: actions: read on: + # TEMP bridge for validating Quality Gates migration before merge. + pull_request: + branches: + - main workflow_run: workflows: ["Quality Gates"] types: [completed] @@ -12,18 +16,30 @@ on: jobs: vm-tests-ubuntu24: name: vm-tests (ubuntu24) - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository }} + if: >- + ${{ + ( + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_repository.full_name == github.repository + ) || + ( + github.event_name == 'pull_request' && + github.event.pull_request.head.ref == 'ci/consolidate-quality-gates' && + github.event.pull_request.head.repo.full_name == github.repository + ) + }} runs-on: ubuntu-24.04 timeout-minutes: 12 concurrency: - group: bats-vm-ubuntu24-${{ github.event.workflow_run.head_sha }} + group: bats-vm-ubuntu24-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.pull_request.head.sha }} cancel-in-progress: true steps: - name: Checkout repository uses: actions/checkout@v4 with: - ref: ${{ github.event.workflow_run.head_sha }} + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.pull_request.head.sha }} - name: Setup PHP and Composer uses: ./.github/actions/setup-php-composer From 8a2b07c3111b5b60111c0388ac82c35ed313e87e Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 22 Feb 2026 12:55:15 +0200 Subject: [PATCH 4/5] ci: remove old workflows and pin action SHAs Replace separate quality-gates, bats-vm, bats-cloud-aws, bats-cloud-do, and bats-cloud-janitor workflows with consolidated ci.yml. Pin setup-php and composer-install actions to SHA hashes for supply-chain security. Add CODEOWNERS rule for .github/workflows/. --- .github/CODEOWNERS | 2 + .github/actions/setup-php-composer/action.yml | 4 +- .github/workflows/bats-cloud-aws.yml | 109 ---- .github/workflows/bats-cloud-do.yml | 109 ---- .github/workflows/bats-cloud-janitor.yml | 118 ---- .github/workflows/bats-vm-ubuntu24.yml | 70 --- .github/workflows/ci.yml | 586 ++++++++++++++++++ .github/workflows/quality-gates.yml | 145 ----- 8 files changed, 590 insertions(+), 553 deletions(-) delete mode 100644 .github/workflows/bats-cloud-aws.yml delete mode 100644 .github/workflows/bats-cloud-do.yml delete mode 100644 .github/workflows/bats-cloud-janitor.yml delete mode 100644 .github/workflows/bats-vm-ubuntu24.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/quality-gates.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf7359b..a912189 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,3 +2,5 @@ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners * @loadinglucian + +/.github/workflows/ @loadinglucian diff --git a/.github/actions/setup-php-composer/action.yml b/.github/actions/setup-php-composer/action.yml index 39883b1..88f553e 100644 --- a/.github/actions/setup-php-composer/action.yml +++ b/.github/actions/setup-php-composer/action.yml @@ -15,13 +15,13 @@ runs: using: 'composite' steps: - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2 with: php-version: ${{ inputs.php-version }} extensions: json, mbstring, yaml${{ inputs.coverage == 'true' && ', xdebug' || '' }} coverage: ${{ inputs.coverage == 'true' && 'xdebug' || 'none' }} - name: Install Composer dependencies (cached) - uses: ramsey/composer-install@v3 + uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # 3.1.1 with: composer-options: --no-interaction --no-progress --prefer-dist --no-ansi --no-scripts diff --git a/.github/workflows/bats-cloud-aws.yml b/.github/workflows/bats-cloud-aws.yml deleted file mode 100644 index ff200db..0000000 --- a/.github/workflows/bats-cloud-aws.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: BATS Cloud Tests (AWS) - -permissions: - contents: read - actions: read - -on: - workflow_run: - workflows: ["BATS VM Tests (Ubuntu 24.04)"] - types: [completed] - -jobs: - vm-gate: - name: verify-vm-success - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository }} - runs-on: ubuntu-latest - timeout-minutes: 3 - outputs: - ready: ${{ steps.evaluate.outputs.ready }} - - steps: - - name: Verify VM job concluded with success - id: evaluate - uses: actions/github-script@v7 - with: - script: | - const { owner, repo } = context.repo; - const workflowRunId = context.payload.workflow_run.id; - const expectedJobName = "vm-tests (ubuntu24)"; - - const jobsResponse = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: workflowRunId, - per_page: 100, - }); - - const vmJob = jobsResponse.data.jobs.find((job) => job.name === expectedJobName); - const ready = vmJob?.conclusion === "success"; - core.setOutput("ready", ready ? "true" : "false"); - - if (!ready) { - core.notice( - `Skipping cloud run because '${expectedJobName}' was not successful (conclusion=${vmJob?.conclusion ?? "missing"})`, - ); - } - - cloud-tests-aws: - needs: vm-gate - name: cloud-tests (aws) - if: ${{ needs.vm-gate.outputs.ready == 'true' }} - runs-on: ubuntu-24.04 - timeout-minutes: 12 - concurrency: - group: bats-cloud-aws-${{ github.event.workflow_run.head_sha }} - cancel-in-progress: true - - env: - CI: "true" - BATS_RUN_SUFFIX: ${{ github.run_id }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Install BATS and dependencies - run: | - sudo apt-get update - sudo apt-get install -y bats jq curl - aws --version - - - name: Setup environment file - run: | - cat <<'EOF' > .env - ${{ secrets.DOTENV_FILE }} - EOF - - - name: Setup SSH key for cloud provisioning - run: | - mkdir -p ~/.ssh tests/bats/fixtures/keys - echo "${{ secrets.SSH_PRIVATE_KEY_B64 }}" | base64 -d > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub - cp ~/.ssh/id_ed25519.pub tests/bats/fixtures/keys/id_test.pub - - - name: Run AWS tests - run: | - set -a - # shellcheck disable=SC1091 - source .env - set +a - ./bats.sh ci cloud aws - - - name: Cleanup cloud resources (backstop) - if: always() - run: | - set -a - # shellcheck disable=SC1091 - source .env - set +a - tests/bats/lib/cloud-janitor.sh \ - --mode=targeted \ - --suffix="${BATS_RUN_SUFFIX}" \ - --providers="aws" diff --git a/.github/workflows/bats-cloud-do.yml b/.github/workflows/bats-cloud-do.yml deleted file mode 100644 index 5c23c34..0000000 --- a/.github/workflows/bats-cloud-do.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: BATS Cloud Tests (DO) - -permissions: - contents: read - actions: read - -on: - workflow_run: - workflows: ["BATS VM Tests (Ubuntu 24.04)"] - types: [completed] - -jobs: - vm-gate: - name: verify-vm-success - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository }} - runs-on: ubuntu-latest - timeout-minutes: 3 - outputs: - ready: ${{ steps.evaluate.outputs.ready }} - - steps: - - name: Verify VM job concluded with success - id: evaluate - uses: actions/github-script@v7 - with: - script: | - const { owner, repo } = context.repo; - const workflowRunId = context.payload.workflow_run.id; - const expectedJobName = "vm-tests (ubuntu24)"; - - const jobsResponse = await github.rest.actions.listJobsForWorkflowRun({ - owner, - repo, - run_id: workflowRunId, - per_page: 100, - }); - - const vmJob = jobsResponse.data.jobs.find((job) => job.name === expectedJobName); - const ready = vmJob?.conclusion === "success"; - core.setOutput("ready", ready ? "true" : "false"); - - if (!ready) { - core.notice( - `Skipping cloud run because '${expectedJobName}' was not successful (conclusion=${vmJob?.conclusion ?? "missing"})`, - ); - } - - cloud-tests-do: - needs: vm-gate - name: cloud-tests (do) - if: ${{ needs.vm-gate.outputs.ready == 'true' }} - runs-on: ubuntu-24.04 - timeout-minutes: 12 - concurrency: - group: bats-cloud-do-${{ github.event.workflow_run.head_sha }} - cancel-in-progress: true - - env: - CI: "true" - BATS_RUN_SUFFIX: ${{ github.run_id }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Install BATS and dependencies - run: | - sudo apt-get update - sudo apt-get install -y bats jq curl - aws --version - - - name: Setup environment file - run: | - cat <<'EOF' > .env - ${{ secrets.DOTENV_FILE }} - EOF - - - name: Setup SSH key for cloud provisioning - run: | - mkdir -p ~/.ssh tests/bats/fixtures/keys - echo "${{ secrets.SSH_PRIVATE_KEY_B64 }}" | base64 -d > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub - cp ~/.ssh/id_ed25519.pub tests/bats/fixtures/keys/id_test.pub - - - name: Run DO tests - run: | - set -a - # shellcheck disable=SC1091 - source .env - set +a - ./bats.sh ci cloud do - - - name: Cleanup cloud resources (backstop) - if: always() - run: | - set -a - # shellcheck disable=SC1091 - source .env - set +a - tests/bats/lib/cloud-janitor.sh \ - --mode=targeted \ - --suffix="${BATS_RUN_SUFFIX}" \ - --providers="do" diff --git a/.github/workflows/bats-cloud-janitor.yml b/.github/workflows/bats-cloud-janitor.yml deleted file mode 100644 index 094cfab..0000000 --- a/.github/workflows/bats-cloud-janitor.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: BATS Cloud Janitor - -permissions: - contents: read - actions: read - -on: - workflow_run: - workflows: ["BATS Cloud Tests (AWS)", "BATS Cloud Tests (DO)"] - types: [completed] - schedule: - - cron: "0 * * * *" - workflow_dispatch: - inputs: - mode: - description: "Cleanup mode" - required: true - default: sweep - type: choice - options: - - sweep - - targeted - suffixes: - description: "Suffix list (required for targeted mode, comma-separated)" - required: false - type: string - providers: - description: "Providers (csv: aws,do,cf)" - required: false - default: "aws,do,cf" - type: string - min_age_minutes: - description: "Sweep protection window in minutes" - required: false - default: "30" - type: string - dry_run: - description: "Print actions without deleting" - required: false - default: false - type: boolean - -jobs: - janitor: - runs-on: ubuntu-24.04 - timeout-minutes: 12 - concurrency: - group: bats-cloud-janitor - cancel-in-progress: false - - env: - GITHUB_TOKEN: ${{ github.token }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Install janitor dependencies - run: | - sudo apt-get update - sudo apt-get install -y jq curl - aws --version - - - name: Setup environment file - run: | - cat <<'ENVEOF' > .env - ${{ secrets.DOTENV_FILE }} - ENVEOF - - - name: Janitor cleanup after BATS workflow completion - if: ${{ github.event_name == 'workflow_run' }} - run: | - set -a - # shellcheck disable=SC1091 - source .env - set +a - tests/bats/lib/cloud-janitor.sh \ - --mode=targeted \ - --suffix="${{ github.event.workflow_run.id }}" \ - --providers="aws,do,cf" - - - name: Janitor scheduled sweep - if: ${{ github.event_name == 'schedule' }} - run: | - set -a - # shellcheck disable=SC1091 - source .env - set +a - tests/bats/lib/cloud-janitor.sh \ - --mode=sweep \ - --providers="aws,do,cf" \ - --min-age-minutes=30 - - - name: Janitor manual run - if: ${{ github.event_name == 'workflow_dispatch' }} - run: | - set -a - # shellcheck disable=SC1091 - source .env - set +a - ARGS=( - "--mode=${{ inputs.mode }}" - "--providers=${{ inputs.providers }}" - "--min-age-minutes=${{ inputs.min_age_minutes }}" - ) - - if [[ -n "${{ inputs.suffixes }}" ]]; then - ARGS+=("--suffixes=${{ inputs.suffixes }}") - fi - - if [[ "${{ inputs.dry_run }}" == "true" ]]; then - ARGS+=("--dry-run") - fi - - tests/bats/lib/cloud-janitor.sh "${ARGS[@]}" diff --git a/.github/workflows/bats-vm-ubuntu24.yml b/.github/workflows/bats-vm-ubuntu24.yml deleted file mode 100644 index ae1783b..0000000 --- a/.github/workflows/bats-vm-ubuntu24.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: BATS VM Tests (Ubuntu 24.04) - -permissions: - contents: read - actions: read - -on: - # TEMP bridge for validating Quality Gates migration before merge. - pull_request: - branches: - - main - workflow_run: - workflows: ["Quality Gates"] - types: [completed] - -jobs: - vm-tests-ubuntu24: - name: vm-tests (ubuntu24) - if: >- - ${{ - ( - github.event_name == 'workflow_run' && - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.head_repository.full_name == github.repository - ) || - ( - github.event_name == 'pull_request' && - github.event.pull_request.head.ref == 'ci/consolidate-quality-gates' && - github.event.pull_request.head.repo.full_name == github.repository - ) - }} - runs-on: ubuntu-24.04 - timeout-minutes: 12 - concurrency: - group: bats-vm-ubuntu24-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.pull_request.head.sha }} - cancel-in-progress: true - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event.pull_request.head.sha }} - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Install BATS and dependencies - run: | - sudo apt-get update - sudo apt-get install -y bats jq - - - name: Setup Lima - uses: lima-vm/lima-actions/setup@v1 - - - name: Setup test fixtures - run: | - mkdir -p tests/bats/fixtures/keys tests/bats/fixtures/inventory - ssh-keygen -t ed25519 -f tests/bats/fixtures/keys/id_test -N "" -C "deployer-bats-test" - - - name: Setup environment file - run: | - cat <<'EOF' > .env - ${{ secrets.DOTENV_FILE }} - EOF - - - name: Run VM tests for ubuntu24 - env: - CI: "true" - BATS_DISTRO: ubuntu24 - run: ./bats.sh ci vm ubuntu24 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e59d7bd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,586 @@ +name: CI + +permissions: + contents: read + actions: read + +on: + pull_request: + paths: + - ".github/**/*.yml" + - "app/**" + - "bin/**" + - "playbooks/**" + - "scaffolds/**" + - "tests/**" + - "**/*.php" + - "**/*.sh" + - "composer.json" + - "composer.lock" + - "phpstan.neon" + - "phpunit.xml" + - "pint.json" + push: + branches: + - main + paths: + - ".github/**/*.yml" + - "app/**" + - "bin/**" + - "playbooks/**" + - "scaffolds/**" + - "tests/**" + - "**/*.php" + - "**/*.sh" + - "composer.json" + - "composer.lock" + - "phpstan.neon" + - "phpunit.xml" + - "pint.json" + schedule: + - cron: "0 * * * *" + workflow_dispatch: + inputs: + mode: + description: "Cleanup mode" + required: true + default: sweep + type: choice + options: + - sweep + - targeted + suffixes: + description: "Suffix list (required for targeted mode, comma-separated)" + required: false + type: string + providers: + description: "Providers (csv: aws,do,cf)" + required: false + default: "aws,do,cf" + type: string + min_age_minutes: + description: "Sweep protection window in minutes" + required: false + default: "30" + type: string + dry_run: + description: "Print actions without deleting" + required: false + default: false + type: boolean + +jobs: + trust-context: + name: trust-context + runs-on: ubuntu-latest + timeout-minutes: 3 + outputs: + is_pr: ${{ steps.classify.outputs.is_pr }} + is_push_main: ${{ steps.classify.outputs.is_push_main }} + is_same_repo_pr: ${{ steps.classify.outputs.is_same_repo_pr }} + is_fork_pr: ${{ steps.classify.outputs.is_fork_pr }} + run_quality: ${{ steps.classify.outputs.run_quality }} + run_secret_jobs: ${{ steps.classify.outputs.run_secret_jobs }} + run_sweep: ${{ steps.classify.outputs.run_sweep }} + run_manual_janitor: ${{ steps.classify.outputs.run_manual_janitor }} + cache_scope: ${{ steps.classify.outputs.cache_scope }} + + steps: + - name: Classify trust context + id: classify + env: + EVENT_NAME: ${{ github.event_name }} + REF: ${{ github.ref }} + REPOSITORY: ${{ github.repository }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || '' }} + run: | + is_pr=false + is_push_main=false + is_same_repo_pr=false + is_fork_pr=false + run_quality=false + run_secret_jobs=false + run_sweep=false + run_manual_janitor=false + cache_scope=other + + if [[ "$EVENT_NAME" == "pull_request" ]]; then + is_pr=true + run_quality=true + if [[ "$PR_HEAD_REPO" == "$REPOSITORY" ]]; then + is_same_repo_pr=true + run_secret_jobs=true + cache_scope=same-repo-pr + else + is_fork_pr=true + cache_scope=fork-pr + fi + elif [[ "$EVENT_NAME" == "push" && "$REF" == "refs/heads/main" ]]; then + is_push_main=true + run_quality=true + run_secret_jobs=true + cache_scope=main-push + elif [[ "$EVENT_NAME" == "schedule" ]]; then + run_sweep=true + cache_scope=schedule + elif [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + run_manual_janitor=true + cache_scope=manual + fi + + { + echo "is_pr=$is_pr" + echo "is_push_main=$is_push_main" + echo "is_same_repo_pr=$is_same_repo_pr" + echo "is_fork_pr=$is_fork_pr" + echo "run_quality=$run_quality" + echo "run_secret_jobs=$run_secret_jobs" + echo "run_sweep=$run_sweep" + echo "run_manual_janitor=$run_manual_janitor" + echo "cache_scope=$cache_scope" + } >> "$GITHUB_OUTPUT" + + ci-canary: + needs: trust-context + if: ${{ needs.trust-context.outputs.run_quality == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 3 + concurrency: + group: ci-canary-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Verify Pint fails on CICanary class + run: | + if vendor/bin/pint --test tests/CICanary.php; then + echo "ERROR: Pint should have failed on CICanary" + exit 1 + fi + + - name: Verify Rector fails on CICanary class + run: | + mkdir -p /tmp/rector + if vendor/bin/rector --dry-run tests/CICanary.php --config rector-canary.php; then + echo "ERROR: Rector should have failed on CICanary" + exit 1 + fi + + - name: Verify PHPStan fails on CICanary class + run: | + if vendor/bin/phpstan analyse tests/CICanary.php --level=10; then + echo "ERROR: PHPStan should have failed on CICanary" + exit 1 + fi + + pint: + needs: trust-context + if: ${{ needs.trust-context.outputs.run_quality == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 3 + concurrency: + group: pint-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Run code style check + run: vendor/bin/pint --test --parallel + + phpstan: + needs: trust-context + if: ${{ needs.trust-context.outputs.run_quality == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 3 + concurrency: + group: phpstan-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Run static analysis + run: vendor/bin/phpstan analyse --error-format=github + + rector: + needs: trust-context + if: ${{ needs.trust-context.outputs.run_quality == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 3 + concurrency: + group: rector-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - run: mkdir -p /tmp/rector + + - name: Cache Rector tmp dir + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: /tmp/rector + key: ${{ runner.os }}-rector-${{ needs.trust-context.outputs.cache_scope }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-rector-${{ needs.trust-context.outputs.cache_scope }}- + + - name: Run modern refactoring code checks + run: vendor/bin/rector --dry-run + + pest: + needs: trust-context + if: ${{ needs.trust-context.outputs.run_secret_jobs == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 3 + concurrency: + group: pest-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + with: + coverage: "true" + + - name: Run tests with coverage + run: vendor/bin/pest --parallel --coverage --coverage-text + + vm-tests-ubuntu24: + name: vm-tests (ubuntu24) + needs: [trust-context, ci-canary, pint, phpstan, rector, pest] + if: ${{ needs.trust-context.outputs.run_secret_jobs == 'true' }} + runs-on: ubuntu-24.04 + timeout-minutes: 12 + concurrency: + group: bats-vm-ubuntu24-${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + cancel-in-progress: true + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Install BATS and dependencies + run: | + sudo apt-get update + sudo apt-get install -y bats jq + + - name: Setup Lima + uses: lima-vm/lima-actions/setup@55627e31b78637bf254a8b2a14da8ea7d12564e5 # v1 + + - name: Setup test fixtures + run: | + mkdir -p tests/bats/fixtures/keys tests/bats/fixtures/inventory + ssh-keygen -t ed25519 -f tests/bats/fixtures/keys/id_test -N "" -C "deployer-bats-test" + + - name: Setup environment file + run: | + cat <<'EOF' > .env + ${{ secrets.DOTENV_FILE }} + EOF + + - name: Run VM tests for ubuntu24 + env: + CI: "true" + BATS_DISTRO: ubuntu24 + run: ./bats.sh ci vm ubuntu24 + + cloud-tests-do: + name: cloud-tests (do) + needs: [trust-context, vm-tests-ubuntu24] + if: ${{ needs.trust-context.outputs.run_secret_jobs == 'true' && needs.vm-tests-ubuntu24.result == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 12 + concurrency: + group: bats-cloud-do-${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + cancel-in-progress: true + + env: + CI: "true" + BATS_RUN_SUFFIX: ${{ github.run_id }} + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Install BATS and dependencies + run: | + sudo apt-get update + sudo apt-get install -y bats jq curl + aws --version + + - name: Setup environment file + run: | + cat <<'EOF' > .env + ${{ secrets.DOTENV_FILE }} + EOF + + - name: Setup SSH key for cloud provisioning + run: | + mkdir -p ~/.ssh tests/bats/fixtures/keys + echo "${{ secrets.SSH_PRIVATE_KEY_B64 }}" | base64 -d > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub + cp ~/.ssh/id_ed25519.pub tests/bats/fixtures/keys/id_test.pub + + - name: Run DO tests + run: | + set -a + # shellcheck disable=SC1091 + source .env + set +a + ./bats.sh ci cloud do + + - name: Cleanup cloud resources (backstop) + if: always() + run: | + set -a + # shellcheck disable=SC1091 + source .env + set +a + tests/bats/lib/cloud-janitor.sh \ + --mode=targeted \ + --suffix="${BATS_RUN_SUFFIX}" \ + --providers="do" + + cloud-tests-aws: + name: cloud-tests (aws) + needs: [trust-context, vm-tests-ubuntu24] + if: ${{ needs.trust-context.outputs.run_secret_jobs == 'true' && needs.vm-tests-ubuntu24.result == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 12 + concurrency: + group: bats-cloud-aws-${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + cancel-in-progress: true + + env: + CI: "true" + BATS_RUN_SUFFIX: ${{ github.run_id }} + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Install BATS and dependencies + run: | + sudo apt-get update + sudo apt-get install -y bats jq curl + aws --version + + - name: Setup environment file + run: | + cat <<'EOF' > .env + ${{ secrets.DOTENV_FILE }} + EOF + + - name: Setup SSH key for cloud provisioning + run: | + mkdir -p ~/.ssh tests/bats/fixtures/keys + echo "${{ secrets.SSH_PRIVATE_KEY_B64 }}" | base64 -d > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub + cp ~/.ssh/id_ed25519.pub tests/bats/fixtures/keys/id_test.pub + + - name: Run AWS tests + run: | + set -a + # shellcheck disable=SC1091 + source .env + set +a + ./bats.sh ci cloud aws + + - name: Cleanup cloud resources (backstop) + if: always() + run: | + set -a + # shellcheck disable=SC1091 + source .env + set +a + tests/bats/lib/cloud-janitor.sh \ + --mode=targeted \ + --suffix="${BATS_RUN_SUFFIX}" \ + --providers="aws" + + janitor-targeted: + name: janitor-targeted + needs: [trust-context, cloud-tests-do, cloud-tests-aws] + if: >- + ${{ + always() && + needs.trust-context.outputs.run_secret_jobs == 'true' && + (needs['cloud-tests-do'].result != 'skipped' || needs['cloud-tests-aws'].result != 'skipped') + }} + runs-on: ubuntu-24.04 + timeout-minutes: 12 + concurrency: + group: bats-cloud-janitor-targeted-${{ github.run_id }} + cancel-in-progress: false + + env: + GITHUB_TOKEN: ${{ github.token }} + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Install janitor dependencies + run: | + sudo apt-get update + sudo apt-get install -y jq curl + aws --version + + - name: Setup environment file + run: | + cat <<'ENVEOF' > .env + ${{ secrets.DOTENV_FILE }} + ENVEOF + + - name: Janitor cleanup after cloud jobs + run: | + set -a + # shellcheck disable=SC1091 + source .env + set +a + tests/bats/lib/cloud-janitor.sh \ + --mode=targeted \ + --suffix="${{ github.run_id }}" \ + --providers="aws,do,cf" + + janitor-sweep: + needs: trust-context + if: ${{ needs.trust-context.outputs.run_sweep == 'true' }} + runs-on: ubuntu-24.04 + timeout-minutes: 12 + concurrency: + group: bats-cloud-janitor + cancel-in-progress: false + + env: + GITHUB_TOKEN: ${{ github.token }} + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Install janitor dependencies + run: | + sudo apt-get update + sudo apt-get install -y jq curl + aws --version + + - name: Setup environment file + run: | + cat <<'ENVEOF' > .env + ${{ secrets.DOTENV_FILE }} + ENVEOF + + - name: Janitor scheduled sweep + run: | + set -a + # shellcheck disable=SC1091 + source .env + set +a + tests/bats/lib/cloud-janitor.sh \ + --mode=sweep \ + --providers="aws,do,cf" \ + --min-age-minutes=30 + + janitor-manual: + needs: trust-context + if: ${{ needs.trust-context.outputs.run_manual_janitor == 'true' }} + runs-on: ubuntu-24.04 + timeout-minutes: 12 + concurrency: + group: bats-cloud-janitor + cancel-in-progress: false + + env: + GITHUB_TOKEN: ${{ github.token }} + MODE: ${{ inputs.mode }} + PROVIDERS: ${{ inputs.providers }} + MIN_AGE_MINUTES: ${{ inputs.min_age_minutes }} + SUFFIXES: ${{ inputs.suffixes }} + DRY_RUN: ${{ inputs.dry_run }} + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer + + - name: Install janitor dependencies + run: | + sudo apt-get update + sudo apt-get install -y jq curl + aws --version + + - name: Setup environment file + run: | + cat <<'ENVEOF' > .env + ${{ secrets.DOTENV_FILE }} + ENVEOF + + - name: Janitor manual run + run: | + set -a + # shellcheck disable=SC1091 + source .env + set +a + + ARGS=( + "--mode=${MODE}" + "--providers=${PROVIDERS}" + "--min-age-minutes=${MIN_AGE_MINUTES}" + ) + + if [[ -n "$SUFFIXES" ]]; then + ARGS+=("--suffixes=${SUFFIXES}") + fi + + if [[ "$DRY_RUN" == "true" ]]; then + ARGS+=("--dry-run") + fi + + tests/bats/lib/cloud-janitor.sh "${ARGS[@]}" diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml deleted file mode 100644 index c75d4f8..0000000 --- a/.github/workflows/quality-gates.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: Quality Gates - -permissions: - contents: read - -on: - pull_request: - paths: - - ".github/**/*.yml" - - "app/**" - - "bin/**" - - "playbooks/**" - - "scaffolds/**" - - "tests/**" - - "**/*.php" - - "**/*.sh" - - "composer.json" - - "composer.lock" - - "phpstan.neon" - - "phpunit.xml" - - "pint.json" - push: - branches: - - main - paths: - - ".github/**/*.yml" - - "app/**" - - "bin/**" - - "playbooks/**" - - "scaffolds/**" - - "tests/**" - - "**/*.php" - - "**/*.sh" - - "composer.json" - - "composer.lock" - - "phpstan.neon" - - "phpunit.xml" - - "pint.json" - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - ci-canary: - runs-on: ubuntu-latest - timeout-minutes: 3 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Verify Pint fails on CICanary class - run: | - if vendor/bin/pint --test tests/CICanary.php; then - echo "ERROR: Pint should have failed on CICanary" - exit 1 - fi - - - name: Verify Rector fails on CICanary class - run: | - mkdir -p /tmp/rector - if vendor/bin/rector --dry-run tests/CICanary.php --config rector-canary.php; then - echo "ERROR: Rector should have failed on CICanary" - exit 1 - fi - - - name: Verify PHPStan fails on CICanary class - run: | - if vendor/bin/phpstan analyse tests/CICanary.php --level=10; then - echo "ERROR: PHPStan should have failed on CICanary" - exit 1 - fi - - pint: - runs-on: ubuntu-latest - timeout-minutes: 3 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Run code style check - run: vendor/bin/pint --test --parallel - - phpstan: - runs-on: ubuntu-latest - timeout-minutes: 3 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - name: Run static analysis - run: vendor/bin/phpstan analyse --error-format=github - - rector: - runs-on: ubuntu-latest - timeout-minutes: 3 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - - - run: mkdir -p /tmp/rector - - - name: Cache Rector tmp dir - uses: actions/cache@v4 - with: - path: /tmp/rector - key: ${{ runner.os }}-rector-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-rector- - - - name: Run modern refactoring code checks - run: vendor/bin/rector --dry-run - - pest: - if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }} - runs-on: ubuntu-latest - timeout-minutes: 3 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP and Composer - uses: ./.github/actions/setup-php-composer - with: - coverage: "true" - - - name: Run tests with coverage - run: vendor/bin/pest --parallel --coverage --coverage-text From 3f379c7b867c107a33bc61ed80f683e879213a2d Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 22 Feb 2026 13:05:39 +0200 Subject: [PATCH 5/5] ci: DRY path filters and centralize checkout_ref Use YAML anchors to share path filters between pull_request and push triggers. Centralize checkout_ref computation in trust-context so concurrency groups and checkout steps reference a single output instead of repeating the inline ternary. Drop unused intermediate outputs (is_pr, is_push_main, is_same_repo_pr, is_fork_pr). Revert redundant CODEOWNERS workflows rule. --- .github/CODEOWNERS | 2 -- .github/workflows/ci.yml | 50 +++++++++++----------------------------- 2 files changed, 13 insertions(+), 39 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a912189..cf7359b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,5 +2,3 @@ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners * @loadinglucian - -/.github/workflows/ @loadinglucian diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e59d7bd..883c834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ permissions: on: pull_request: - paths: + paths: &ci_paths - ".github/**/*.yml" - "app/**" - "bin/**" @@ -23,20 +23,7 @@ on: push: branches: - main - paths: - - ".github/**/*.yml" - - "app/**" - - "bin/**" - - "playbooks/**" - - "scaffolds/**" - - "tests/**" - - "**/*.php" - - "**/*.sh" - - "composer.json" - - "composer.lock" - - "phpstan.neon" - - "phpunit.xml" - - "pint.json" + paths: *ci_paths schedule: - cron: "0 * * * *" workflow_dispatch: @@ -75,15 +62,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 3 outputs: - is_pr: ${{ steps.classify.outputs.is_pr }} - is_push_main: ${{ steps.classify.outputs.is_push_main }} - is_same_repo_pr: ${{ steps.classify.outputs.is_same_repo_pr }} - is_fork_pr: ${{ steps.classify.outputs.is_fork_pr }} run_quality: ${{ steps.classify.outputs.run_quality }} run_secret_jobs: ${{ steps.classify.outputs.run_secret_jobs }} run_sweep: ${{ steps.classify.outputs.run_sweep }} run_manual_janitor: ${{ steps.classify.outputs.run_manual_janitor }} cache_scope: ${{ steps.classify.outputs.cache_scope }} + checkout_ref: ${{ steps.classify.outputs.checkout_ref }} steps: - name: Classify trust context @@ -93,30 +77,25 @@ jobs: REF: ${{ github.ref }} REPOSITORY: ${{ github.repository }} PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || '' }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || '' }} run: | - is_pr=false - is_push_main=false - is_same_repo_pr=false - is_fork_pr=false run_quality=false run_secret_jobs=false run_sweep=false run_manual_janitor=false cache_scope=other + checkout_ref="$GITHUB_SHA" if [[ "$EVENT_NAME" == "pull_request" ]]; then - is_pr=true run_quality=true + checkout_ref="$PR_HEAD_SHA" if [[ "$PR_HEAD_REPO" == "$REPOSITORY" ]]; then - is_same_repo_pr=true run_secret_jobs=true cache_scope=same-repo-pr else - is_fork_pr=true cache_scope=fork-pr fi elif [[ "$EVENT_NAME" == "push" && "$REF" == "refs/heads/main" ]]; then - is_push_main=true run_quality=true run_secret_jobs=true cache_scope=main-push @@ -129,15 +108,12 @@ jobs: fi { - echo "is_pr=$is_pr" - echo "is_push_main=$is_push_main" - echo "is_same_repo_pr=$is_same_repo_pr" - echo "is_fork_pr=$is_fork_pr" echo "run_quality=$run_quality" echo "run_secret_jobs=$run_secret_jobs" echo "run_sweep=$run_sweep" echo "run_manual_janitor=$run_manual_janitor" echo "cache_scope=$cache_scope" + echo "checkout_ref=$checkout_ref" } >> "$GITHUB_OUTPUT" ci-canary: @@ -273,14 +249,14 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 12 concurrency: - group: bats-vm-ubuntu24-${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + group: bats-vm-ubuntu24-${{ needs.trust-context.outputs.checkout_ref }} cancel-in-progress: true steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + ref: ${{ needs.trust-context.outputs.checkout_ref }} - name: Setup PHP and Composer uses: ./.github/actions/setup-php-composer @@ -317,7 +293,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 12 concurrency: - group: bats-cloud-do-${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + group: bats-cloud-do-${{ needs.trust-context.outputs.checkout_ref }} cancel-in-progress: true env: @@ -328,7 +304,7 @@ jobs: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + ref: ${{ needs.trust-context.outputs.checkout_ref }} - name: Setup PHP and Composer uses: ./.github/actions/setup-php-composer @@ -380,7 +356,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 12 concurrency: - group: bats-cloud-aws-${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + group: bats-cloud-aws-${{ needs.trust-context.outputs.checkout_ref }} cancel-in-progress: true env: @@ -391,7 +367,7 @@ jobs: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + ref: ${{ needs.trust-context.outputs.checkout_ref }} - name: Setup PHP and Composer uses: ./.github/actions/setup-php-composer