ci: reduce the release workflow to what it still does, rename to publish.yaml (#724)

Most of docker.yaml was doing work that was duplicated or hazardous now
that releases are cut from a tag.

The tests were duplicated exactly: release.yaml and docker.yaml called
the same reusable tests.yaml, on the same commit — once before drafting,
again after publishing. No new information, and the image build queued
behind it. Pint likewise: check.yaml already style-checked the commit
when it landed. Both jobs go; coverage is unchanged.

manual_docker_build goes too, and it was worse than redundant. Its
checkout took no ref, so it built the dispatched branch while tagging
with whatever string was typed — an image could be labelled 2.4.2 while
containing 2.x HEAD — and `tag` defaulted to "latest", so a careless
dispatch republished the moving stable tag from a branch. #710 had to add
a guard purely to stop the registration dispatch doing that by accident.
It was last used in September 2025 to push the legacy and alpha tags
during the Docker distribution work; that is finished, and releases
produce images now. A patched base image is better served by a patch
release than by silently changing what a pinned tag contains.

That cascade removes the tag input and the #710 guard as well, leaving
register_tag as the only dispatch input and two jobs in the file.

Losing the test gate would leave the image build ungated, so the release
build now refuses a release with no InvoiceShelf.zip. A release either
came from the tested pipeline or it gets no images — the updater is
already protected this way, since registration downloads that same asset.
GitHub offers no way to forbid hand-made releases; this is the closest
thing, which is to make them inert.

"Docker" no longer describes a workflow that registers on the updater and
publishes images, so it becomes publish.yaml — matching its trigger and
pairing with release.yaml, which prepares what this distributes. The
recovery instructions in AGENTS.md name this workflow and would have
broken silently, so they move with it.
This commit is contained in:
Darko Gjorgjijoski
2026-07-29 18:27:18 +02:00
committed by GitHub
parent e958cada30
commit 3a989923d1
4 changed files with 27 additions and 84 deletions

View File

@@ -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 }}"}}

View File

@@ -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.

View File

@@ -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: