diff --git a/.github/workflows/docker.yaml b/.github/workflows/publish.yaml similarity index 78% rename from .github/workflows/docker.yaml rename to .github/workflows/publish.yaml index 7f6ceefb..30022ab8 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/publish.yaml @@ -1,18 +1,13 @@ -name: Docker Build and Push +name: Publish Release on: release: types: [published] workflow_dispatch: inputs: - tag: - description: 'Docker tag' - required: true - default: 'latest' register_tag: - description: 'Release tag to (re-)register on the updater, e.g. 2.4.2. Leave blank to skip.' - required: false - default: '' + description: 'Release tag to (re-)register on the updater, e.g. 2.4.2.' + required: true # Single source of truth for which major owns the moving stable tags # (:latest, :{major}, :{major}.{minor}) and the temporary :nightly alias. @@ -22,32 +17,6 @@ env: LATEST_MAJOR: "2" jobs: - php_syntax_errors: - name: 1️⃣ PHP Code Style errors - if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - steps: - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.4 - - - name: Checkout code - uses: actions/checkout@v6 - - - name: Install dependencies - uses: ramsey/composer-install@4.0.0 - - - name: Check source code for syntax errors - run: ./vendor/bin/pint --test - - tests: - name: 2️⃣ PHP Tests - if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' - needs: - - php_syntax_errors - uses: ./.github/workflows/tests.yaml - # Registration lives in its own job, and fetches the published asset rather than # reusing the build job's working directory. That decoupling is deliberate: the # previous in-line step read `changelog.txt` relative to the checkout while writing @@ -62,9 +31,7 @@ jobs: # runs the asset is already there and no build dependency is needed. That also # retires the always() dance this condition previously required to survive a # skipped build job on a manual dispatch. - if: >- - github.event_name == 'release' || - (github.event_name == 'workflow_dispatch' && inputs.register_tag != '') + if: github.event_name == 'release' || inputs.register_tag != '' runs-on: ubuntu-latest steps: @@ -122,8 +89,6 @@ jobs: MIN_PHP=$(php -r '$c=require "config/installer.php"; echo $c["core"]["minPhpVersion"] ?? "";') EXTS=$(php -r '$c=require "config/installer.php"; echo implode(", ", $c["requirements"]["php"] ?? []);') - # Read the notes and pre-release flag from the release itself, so this behaves - # identically whether triggered by a publish or re-run later by hand. # CHANGELOG.md is the source: it is written and reviewed alongside the # change itself, so what installs are offered cannot drift from what was # merged. A release with no section fails here rather than registering an @@ -178,10 +143,24 @@ jobs: release_docker_build: name: 🐳 Release Docker Build if: github.event_name == 'release' - needs: - - tests runs-on: ubuntu-latest steps: + # Releases are cut by pushing a tag: release.yaml runs the tests, builds the + # package and attaches it to a draft. A release without that package was made + # by hand and has been through none of it, so it does not get images. The + # updater is already protected — registration downloads this same asset and + # fails without it. + - name: Refuse a release that did not come from the pipeline + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: | + if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' \ + | grep -qx 'InvoiceShelf.zip'; then + echo "::error::$TAG has no InvoiceShelf.zip. Releases are cut by pushing a tag, not created by hand — see the Releasing section in AGENTS.md." + exit 1 + fi + - name: Checkout code uses: actions/checkout@v6 @@ -227,39 +206,3 @@ jobs: cache-to: type=gha,mode=max secrets: | composer_auth={"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}} - - manual_docker_build: - name: 🛠️ Manual Docker Build - # A dispatch carrying register_tag is asking to register a release, not to - # build an image. Without this guard it would also run, and `tag` defaults to - # "latest" — so re-registering a release would rebuild from this branch and - # overwrite the published :latest image. - if: github.event_name == 'workflow_dispatch' && inputs.register_tag == '' - needs: - - tests - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - - name: Build and push Docker image - uses: docker/build-push-action@v7 - with: - context: . - file: docker/production/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: invoiceshelf/invoiceshelf:${{ github.event.inputs.tag }} - cache-from: type=gha - cache-to: type=gha,mode=max - secrets: | - composer_auth={"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ce1e3e37..d4c6d52d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,8 +14,8 @@ permissions: contents: write # Which major owns GitHub's "Latest release" pointer. Same value and same meaning -# as in docker.yaml, which gates the moving :latest image tags on it — bump both -# on this branch, and docker.yaml on 2.x, when 3.0.0 GA is tagged. +# as in publish.yaml, which gates the moving :latest image tags on it — bump both +# on this branch, and publish.yaml on 2.x, when 3.0.0 GA is tagged. env: LATEST_MAJOR: "2" @@ -62,7 +62,7 @@ jobs: # `make dist` owns the artifact: it installs composer/pnpm dependencies, # builds the frontend, assembles the package and generates the manifest. # This workflow used to hand-copy the same file list a second time, with - # docker.yaml then overwriting the result — two copies of one list, and job + # publish.yaml then overwriting the result — two copies of one list, and job # ordering deciding what users received. - name: Build the release package run: make clean dist @@ -73,7 +73,7 @@ jobs: # never race the upload — and a failed run leaves no release at all rather # than a published one nobody can download. # The draft is where this workflow stops. Publishing is left to a person, - # because a release published by the workflow would never reach docker.yaml: + # because a release published by the workflow would never reach publish.yaml: # GitHub does not start workflow runs from events created with GITHUB_TOKEN, # so `release: published` fires as github-actions[bot] and triggers nothing. # 2.4.3-beta.2 was published that way and got no registration and no images. diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2e52cbdb..e0a248d7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,6 +1,6 @@ name: Tests -# Called by docker.yaml and release.yaml rather than duplicated in both. The +# Called by publish.yaml and release.yaml rather than duplicated in both. The # release artifact's file list used to exist in two places and drift was only a # matter of time; the test definition should not repeat that. on: diff --git a/AGENTS.md b/AGENTS.md index 1c7f0408..bc2c59b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,7 +171,7 @@ zip attached. It stops there and prints the draft URL in the run summary. **You publish the draft yourself.** That is deliberate, not an omission: GitHub does not start workflow runs from events created with `GITHUB_TOKEN`, so a release published by the workflow reaches nothing downstream. Pressing Publish fires -`release: published` under your own identity, which triggers `docker.yaml` to +`release: published` under your own identity, which triggers `publish.yaml` to register the release on the updater and build the Docker images. **Until you publish, no install is offered anything.** @@ -184,7 +184,7 @@ Notes on the mechanics: channel so ordinary installs are not offered it. "Latest" is gated on `LATEST_MAJOR` in the workflows — bump it there when 3.x becomes the stable line. - **If registration fails**, re-run it without cutting a new release: run the - `Docker Build and Push` workflow manually with `register_tag` set to the version. + `Publish Release` workflow manually with `register_tag` set to the version. That path is idempotent and does not rebuild the Docker images. - `.github/scripts/changelog-section.php ` prints what the updater will be sent, so you can check the notes locally before tagging.