From 7597ddaea7a260838a296a10411e324e03440e77 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:51:14 +0200 Subject: [PATCH] ci: make tag-triggered releases work, and only publish complete ones (#715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.yaml has never run. It listens for "v*" tags while every tag ever cut is bare — 3.0.0-alpha.1, 2.4.2, 2.4.3-beta.1 — so tagging by the established convention produced silence, and whoever cuts 3.0.0 would have hit that first. It now accepts both spellings. It also hand-copied the release file list, which docker.yaml then rebuilt via `make clean dist` and uploaded with overwrite: true. The two lists were identical, so nothing had broken yet, but which zip users received was decided by job ordering. `make dist` owns the artifact now and the duplicate is gone. The release is created as a draft with the package already attached and published in a separate step, so `release: published` fires only once the tests have passed and the asset is in place. A failed run leaves no release at all, rather than a published one nobody can download — which is what 2.4.2 left behind. Registration can no longer race the upload either, so docker.yaml drops its build job, and register_release loses both that dependency and the always() dance it needed to survive the job being skipped on a manual dispatch. GitHub's "Latest release" pointer is gated on LATEST_MAJOR, exactly as docker.yaml gates its moving image tags: a 3.0.0 alpha can no longer displace 2.4.x as the release users are shown first. Release notes come from CHANGELOG.md instead of being generated from commits, matching where the updater already reads them, and a tag with no section fails before anything is published. The test job moves to a reusable workflow rather than being written out in both places — the duplicated file list above is the argument. --- .github/workflows/docker.yaml | 96 ++---------------- .github/workflows/release.yaml | 177 ++++++++++++++++----------------- .github/workflows/tests.yaml | 39 ++++++++ 3 files changed, 133 insertions(+), 179 deletions(-) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 59017c4d..7f6ceefb 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -46,88 +46,7 @@ jobs: if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' needs: - php_syntax_errors - runs-on: ubuntu-latest - strategy: - matrix: - php-version: - - 8.4 - env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: ${{ env.extensions }} - coverage: none - tools: pecl, composer - - - name: Install Composer dependencies - uses: ramsey/composer-install@4.0.0 - - - name: Apply tests ${{ matrix.php-version }} (parallel) - run: php artisan test --parallel --exclude-group=modules - - - name: Apply module tests ${{ matrix.php-version }} (serial) - run: php artisan test --group=modules - - release_artifact_build: - name: 🏗️ Build / Upload - Release File - if: github.event_name == 'release' - needs: - - tests - runs-on: ubuntu-latest - env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.4 - extensions: ${{ env.extensions }} - coverage: none - - - name: Install Composer dependencies - uses: ramsey/composer-install@4.0.0 - with: - composer-options: --no-dev - - - name: Install pnpm - uses: pnpm/action-setup@v6 - - - name: Use Node.js 24 - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - - name: Install - run: pnpm install --frozen-lockfile - - - name: Compile Front-end - run: pnpm build - - - name: Build Dist - run: | - make clean dist - - - name: Upload package - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ github.token }} - file: InvoiceShelf.zip - asset_name: InvoiceShelf.zip - tag: ${{ github.ref }} - overwrite: true - + 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 @@ -139,14 +58,13 @@ jobs: # injection from the release body. register_release: name: 📡 Register release on the updater - # always() is required because release_artifact_build is skipped on a manual - # dispatch, and a job needing a skipped job is skipped too. + # release.yaml attaches the package before publishing, so by the time this + # 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: >- - always() && - ((github.event_name == 'release' && needs.release_artifact_build.result == 'success') || - (github.event_name == 'workflow_dispatch' && inputs.register_tag != '')) - needs: - - release_artifact_build + github.event_name == 'release' || + (github.event_name == 'workflow_dispatch' && inputs.register_tag != '') runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3f557305..48e3bdcb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,100 +1,97 @@ name: Release +# Tagging is the trigger. Both spellings are accepted because every tag to date +# is bare (3.0.0-alpha.1, 2.4.2) while this workflow previously listened only for +# "v*" — so it had never once fired, and tagging by the established convention +# produced silence. on: - push: - tags: - - "v*" + push: + tags: + - '[0-9]*' + - 'v[0-9]*' permissions: - contents: write + 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. +env: + LATEST_MAJOR: "2" jobs: - release: - name: Build & Release - runs-on: ubuntu-latest + tests: + uses: ./.github/workflows/tests.yaml + + release: + name: Build & Release + needs: + - tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip + tools: composer + + - name: Install pnpm + uses: pnpm/action-setup@v6 + + - name: Use Node.js 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + + # `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 + # ordering deciding what users received. + - name: Build the release package + run: make clean dist + + - name: Read the release notes from CHANGELOG.md env: - extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip + TAG: ${{ github.ref_name }} + run: | + if ! php .github/scripts/changelog-section.php "$TAG" > /tmp/notes.md; then + echo "::error::No CHANGELOG.md section for $TAG — add one before tagging." + exit 1 + fi + echo "Using the CHANGELOG.md section for $TAG" - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 + # Created as a draft with the package already attached, then published as a + # separate step. `release: published` therefore fires only once the tests + # have passed and the asset is in place, so downstream registration can + # never race the upload — and a failed run leaves no release at all rather + # than a published one nobody can download. + - name: Create the draft release + uses: softprops/action-gh-release@v3 + with: + files: InvoiceShelf.zip + body_path: /tmp/notes.md + draft: true + prerelease: ${{ contains(github.ref_name, '-') }} - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.4 - extensions: ${{ env.extensions }} - tools: composer - - - name: Install Composer dependencies - run: composer install --no-dev --optimize-autoloader --no-interaction - - - name: Install pnpm - uses: pnpm/action-setup@v6 - - - name: Use Node.js 24 - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: pnpm - - - name: Install npm dependencies - run: pnpm install --frozen-lockfile - - - name: Build frontend - run: pnpm build - - - name: Prepare release directory - run: | - mkdir -p /tmp/InvoiceShelf/public - - cp -r app /tmp/InvoiceShelf/ - cp -r bootstrap /tmp/InvoiceShelf/ - cp -r config /tmp/InvoiceShelf/ - cp -r database /tmp/InvoiceShelf/ - cp -r public/build /tmp/InvoiceShelf/public/ - cp -r public/favicons /tmp/InvoiceShelf/public/ - cp public/.htaccess /tmp/InvoiceShelf/public/ - cp public/index.php /tmp/InvoiceShelf/public/ - cp public/robots.txt /tmp/InvoiceShelf/public/ - cp public/web.config /tmp/InvoiceShelf/public/ - cp -r resources /tmp/InvoiceShelf/ - cp -r lang /tmp/InvoiceShelf/ - cp -r routes /tmp/InvoiceShelf/ - cp -r storage /tmp/InvoiceShelf/ - cp -r vendor /tmp/InvoiceShelf/ 2>/dev/null || true - cp -r scripts /tmp/InvoiceShelf/ - cp version.md /tmp/InvoiceShelf/ - cp .env.example /tmp/InvoiceShelf/ - cp artisan /tmp/InvoiceShelf/ - cp composer.json /tmp/InvoiceShelf/ - cp composer.lock /tmp/InvoiceShelf/ - cp LICENSE /tmp/InvoiceShelf/ - cp readme.md /tmp/InvoiceShelf/ - cp SECURITY.md /tmp/InvoiceShelf/ - cp server.php /tmp/InvoiceShelf/ - - # Clean up runtime artifacts - find /tmp/InvoiceShelf -wholename '*/[Tt]ests/*' -delete - find /tmp/InvoiceShelf -wholename '*/[Tt]est/*' -delete - rm -rf /tmp/InvoiceShelf/storage/framework/cache/data/* 2>/dev/null || true - rm -f /tmp/InvoiceShelf/storage/framework/sessions/* 2>/dev/null || true - rm -f /tmp/InvoiceShelf/storage/framework/views/* 2>/dev/null || true - rm -f /tmp/InvoiceShelf/storage/logs/* 2>/dev/null || true - touch /tmp/InvoiceShelf/storage/logs/laravel.log - - - name: Generate manifest - run: php scripts/generate-manifest.php /tmp/InvoiceShelf - - - name: Create zip - working-directory: /tmp - run: zip -r InvoiceShelf.zip InvoiceShelf/ - - - name: Create GitHub Release - uses: softprops/action-gh-release@v3 - with: - files: /tmp/InvoiceShelf.zip - generate_release_notes: true - make_latest: true + - name: Publish it + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + # GitHub's "Latest release" pointer, gated exactly as docker.yaml gates + # its moving image tags — so a 3.0.0 alpha cannot displace 2.4.x as the + # release users are shown first while 2.x is still the stable line. + IS_LATEST: ${{ !contains(github.ref_name, '-') && startsWith(github.ref_name, format('{0}.', env.LATEST_MAJOR)) }} + run: | + if [ "$IS_LATEST" = "true" ]; then + gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest + else + gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest=false + fi + echo "Published $TAG (latest=$IS_LATEST)" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..2e52cbdb --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,39 @@ +name: Tests + +# Called by docker.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: + workflow_call: + +jobs: + tests: + name: PHP Tests + runs-on: ubuntu-latest + strategy: + matrix: + php-version: + - 8.4 + env: + extensions: bcmath, curl, dom, gd, imagick, json, libxml, mbstring, pcntl, pdo, pdo_mysql, zip + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup PHP Action + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: ${{ env.extensions }} + coverage: none + tools: pecl, composer + + - name: Install Composer dependencies + uses: ramsey/composer-install@4.0.0 + + - name: Apply tests ${{ matrix.php-version }} (parallel) + run: php artisan test --parallel --exclude-group=modules + + - name: Apply module tests ${{ matrix.php-version }} (serial) + run: php artisan test --group=modules