diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca91f4a..be3ed48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,77 +1,105 @@ name: Release Gate +# A pushed v* tag is the release for slim-guard (there is no publish step). +# This workflow only confirms that the tagged commit is good. Nothing is +# linted or tested here: the tagged commit must already have passed the push +# workflows listed in REQUIRED_WORKFLOWS below (ci.yml, static-analysis.yml, live-smoke.yml). A release +# can therefore never fail on a check its pull request did not run. +# +# Composer audit is not repeated here: it is a job of ci.yml, which runs it +# on the same commit against the same composer.json, so a vulnerable +# dependency set already fails ci.yml above. Packagist serves the tag as +# soon as it is pushed, so no check has to wait for the tag to exist. +# +# Re-check: run this workflow by hand (Actions > Release Gate > Run workflow) +# with the tag, for example v1.6.0. It checks out that tag, so a fixed +# workflow on the default branch can re-check an existing tag. on: push: tags: - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to check, for example v1.6.0' + required: true permissions: {} +concurrency: + group: release-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false + +env: + REQUIRED_WORKFLOWS: >- + .github/workflows/ci.yml + .github/workflows/static-analysis.yml + .github/workflows/live-smoke.yml + jobs: - test: - name: Test at tag (php ${{ matrix.php }}) + verify: + name: Tagged commit passed CI runs-on: ubuntu-latest permissions: contents: read - strategy: - fail-fast: false - matrix: - php: ['8.2', '8.3', '8.4'] - services: - redis: - image: redis:7-alpine - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 5s - --health-timeout 3s - --health-retries 10 + actions: read + outputs: + tag: ${{ steps.tag.outputs.tag }} steps: - - name: Checkout tag - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up PHP - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 - with: - php-version: ${{ matrix.php }} - extensions: mbstring - coverage: none - - - name: Install dependencies - run: | - composer install --no-interaction --no-progress - - - name: php -l sweep - run: for f in $(find src bin -name '*.php'); do php -l "$f" > /dev/null || exit 1; done && echo LINT_OK - - - name: test_slim + - name: Resolve the tag + id: tag env: - REDIS_HOST: 127.0.0.1 - run: php bin/test_slim.php + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + REF_NAME: ${{ github.ref_name }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + tag="$INPUT_TAG" + else + tag="$REF_NAME" + fi + if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::tag '$tag' is not of the form v1.2.3" + exit 1 + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" - composer-audit: - name: Composer audit at tag - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout tag + - name: Checkout the tag uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: refs/tags/${{ steps.tag.outputs.tag }} persist-credentials: false - - name: Set up PHP - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 - with: - php-version: '8.3' - coverage: none - - - name: Install dependencies + # The newest push run of each required workflow on the tagged commit + # must have succeeded. A tag pushed right after a merge waits for them + # (up to an hour) instead of failing. + - name: Require green CI on the tagged commit + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} run: | - composer install --no-interaction --no-progress - - - name: Composer audit - run: composer audit + sha=$(git rev-parse HEAD) + for _ in $(seq 1 60); do + runs=$(gh api "repos/$REPO/actions/runs?head_sha=$sha&event=push&per_page=100" \ + --jq '[.workflow_runs | group_by(.path)[] | max_by(.run_number) | {path, status, conclusion}]') + waiting=0 + for wf in $REQUIRED_WORKFLOWS; do + run=$(jq -c --arg p "$wf" '.[] | select(.path == $p)' <<< "$runs") + if [ -z "$run" ]; then + echo "$wf: no push run on $sha yet"; waiting=1; continue + fi + status=$(jq -r .status <<< "$run"); conclusion=$(jq -r .conclusion <<< "$run") + if [ "$status" != "completed" ]; then + echo "$wf: $status"; waiting=1 + elif [ "$conclusion" != "success" ]; then + echo "::error::$wf concluded $conclusion on $sha; fix the default branch and release a new tag" + exit 1 + else + echo "ok: $wf success on $sha" + fi + done + [ "$waiting" = "0" ] && exit 0 + sleep 60 + done + echo "::error::required workflows did not finish on $sha within an hour" + exit 1 diff --git a/AGENTS.md b/AGENTS.md index 4297608..e723af6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ Composer scripts (composer.json `scripts`; these are the only two): | `docker compose -f examples/simple_app/docker-compose.yml up --build -d --wait` | Bring up the simple example app plus Redis; then run the curl assertions from `.github/workflows/live-smoke.yml` | | `docker compose -f examples/advanced_app/docker-compose.yml up --build -d --wait` | Same for the advanced example (assertions in `examples/advanced_app/README.md`) | -Direct commands used by CI (verified in `.github/workflows/ci.yml`; the same install, lint, and test steps appear in `release.yml` and `scheduled-lint.yml`): +Direct commands used by CI (verified in `.github/workflows/ci.yml`; the same install, lint, and test steps appear in `scheduled-lint.yml`): - `composer install --no-interaction --no-progress` - `composer update rennf93/guard-core-php --no-interaction` (CI deliberately refreshes the core dependency on every run) @@ -101,7 +101,7 @@ Direct commands used by CI (verified in `.github/workflows/ci.yml`; the same ins .github/labels.yml Label registry for sync-labels .github/labeler.yml PR area-label rules for labeler .github/workflows/ci.yml CI: test matrix php 8.2/8.3/8.4 + redis service + composer audit -.github/workflows/release.yml Release Gate: same suite, runs on v* tags +.github/workflows/release.yml Release Gate: on v* tags, requires green ci.yml, static-analysis.yml and live-smoke.yml on the tagged commit .github/workflows/scheduled-lint.yml Weekly cron (Mon 04:00 UTC): php -l sweep + composer audit .github/workflows/issue-link.yml PR must close an open issue or carry no-issue .github/workflows/summary.yml AI issue summary on the needs-summary label diff --git a/CLAUDE.md b/CLAUDE.md index e59e0f5..5dd3ab5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ Composer scripts (composer.json `scripts`; these are the only two): | `composer test` | `php bin/test_slim.php` | | `composer lint` | `for f in $(find src bin -name '*.php'); do php -l "$f" > /dev/null || exit 1; done && echo LINT_OK` | -Direct commands used by CI (verified in `.github/workflows/ci.yml`; the same install, lint, and test steps appear in `release.yml` and `scheduled-lint.yml`): +Direct commands used by CI (verified in `.github/workflows/ci.yml`; the same install, lint, and test steps appear in `scheduled-lint.yml`): - `composer install --no-interaction --no-progress` - `composer update rennf93/guard-core-php --no-interaction` (CI deliberately refreshes the core dependency on every run) @@ -95,7 +95,7 @@ Direct commands used by CI (verified in `.github/workflows/ci.yml`; the same ins ``` .github/dependabot.yml Weekly dependabot: github-actions + composer (grouped) .github/workflows/ci.yml CI: test matrix php 8.2/8.3/8.4 + redis service + composer audit -.github/workflows/release.yml Release Gate: same suite, runs on v* tags +.github/workflows/release.yml Release Gate: on v* tags, requires green ci.yml, static-analysis.yml and live-smoke.yml on the tagged commit .github/workflows/scheduled-lint.yml Weekly cron (Mon 04:00 UTC): php -l sweep + composer audit bin/test_slim.php Entire test suite, plain PHP runner with a T assertion harness composer.json Package metadata, autoload, scripts, repositories, platform pin