Registration sent the GitHub release body to the updater, so the notes
every install sees were written at publish time — after review, outside
the repo, with nothing checking they existed or matched what shipped.
CHANGELOG.md becomes the source. It is written and reviewed alongside the
change itself, so what installs are offered cannot drift from what was
merged, and the release body can simply point at it.
A release with no section for its tag now fails the job rather than
registering an empty changelog — the same reasoning as the token check
added in #708. A manual dispatch falls back to the release body instead,
since re-registering a release older than this file is legitimate.
Section boundaries are matched on version headings rather than any "## ",
because release notes routinely contain their own second-level headings:
5 of the 12 historical 2.x releases do. Verified by round-tripping 2.4.2
(2504 bytes, byte-identical) and 2.4.0 (5 inner headings, no content
lost), and that "2.4" does not match the "2.4.2" section.
Backfilled with the 2.4.x line; older releases stay on GitHub.
manual_docker_build fires on any workflow_dispatch, and pushes
invoiceshelf/invoiceshelf:${{ inputs.tag }} where tag is required and
defaults to "latest". Dispatching to re-register a release therefore also
rebuilt from the dispatched branch and overwrote the published :latest
image.
The registration path added in #708 is meant to be a safe recovery route,
so triggering it must not have that side effect. A dispatch carrying
register_tag now runs registration only.
The 2.4.2 release published, uploaded its zip, and built its images — then
failed to register on the updater, so no install was offered the update:
Registering 2.4.2 (channel=stable, min_php=8.2.0) on the updater
curl: (26) Failed to open/read local data from file/application
The step wrote the changelog to /tmp/changelog.txt but told curl to read
`changelog.txt`, a relative path resolved against the checkout. `changelog`
is required server-side, so nothing would have been accepted regardless.
The path is the bug; the reason it reached production is that the step only
ever ran during a real release, so it could not be exercised beforehand.
Three changes address that:
Registration moves into its own job that downloads the published asset,
rather than reusing the build job's working directory — the coupling that
made the relative path look reasonable. It runs on a release, or on demand
against any existing tag, so a failure no longer needs production shell
access to repair and the path can be rehearsed deliberately.
A missing WEBSITE_RELEASE_TOKEN is now fatal on a release. Warning and
exiting 0 meant a release could look completely successful while reaching
nobody — the same silent failure as the bug itself.
A verification step follows the POST: /releases/download/{tag} 404s unless
the Release row exists and its zip is retrievable from storage, so it proves
the whole chain rather than trusting a 2xx. It would have caught this.
Append a step to the release_artifact_build job that POSTs the freshly built
InvoiceShelf.zip + metadata to the website updater's /api/releases endpoint
(Bearer WEBSITE_RELEASE_TOKEN) right after the asset upload, so deployed installs
are offered the release automatically instead of a manual kubectl+tinker import.
- Runs only on release events; skips with a warning if WEBSITE_RELEASE_TOKEN is unset
- Channel derived from the prerelease flag / "-" tag suffix (GA->stable, pre->insider)
- min_php + extensions read from config/installer.php; release fields passed via env
to avoid shell injection from the release body
- Idempotent (the endpoint upserts per version)
Claude-Session: https://claude.ai/code/session_012tpgisKcrC4D4mCbGTeTKz
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multi-arch builds run composer (incl. the merge-plugin's update) twice and exhausted GitHub's unauthenticated API rate limit, failing with 'Could not authenticate against github.com'. Pass the Actions token as a build secret and feed it to composer via COMPOSER_AUTH (build-time only, never in the image).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove scheduled nightly/alpha builds; gate :latest on LATEST_MAJOR; keep a transitional :nightly alias on stable releases. Fix the production Dockerfile so a stale host public/build cannot clobber the freshly built frontend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #676: composer-install@v2 is composite but internally calls
actions/cache@v3 (Node 20), the last Node-20 deprecation on 2.x CI. 4.0.0
uses actions/cache v5.0.3 (Node 24) and keeps the composer-options input.
GitHub removes Node 20 runners on 2026-09-16; bump every Node-20 action
so v2 CI keeps working through the 2.x security-support window. checkout
v4->v6, setup-node v4->v6, paths-filter v3->v4, cancel-workflow-action
0.12.1->0.13.1, pnpm/action-setup v4->v6, docker/{setup-buildx v3->v4,
login v3->v4, metadata v5->v6, build-push v5->v7}. setup-php@v2,
ramsey/composer-install@v2 and svenstaro/upload-release-action@v2 are
already on node24.
* build: migrate frontend tooling from yarn to pnpm
The Dockerfiles ran `yarn && yarn build`, which broke on node:24 (yarn no
longer on PATH; the corepack yarn shim made `npm i -g yarn` fail EEXIST),
while CI + Makefile used npm and only a yarn.lock was committed — an
inconsistent yarn/npm split. Standardize on pnpm, pinned via the
packageManager field + corepack.
- package.json: packageManager pnpm@11.6.0.
- pnpm-workspace.yaml: nodeLinker: hoisted (flat node_modules, npm/yarn-like, so
directly-imported transitive deps like flatpickr resolve) + allow vue-demi's
postinstall (it selects the Vue 3 entry). pnpm 11 reads these here, not .npmrc.
- Generate pnpm-lock.yaml (imported from yarn.lock); delete yarn.lock.
- Dockerfiles (dev/nginx/production): node:24 + `corepack enable && pnpm install --frozen-lockfile && pnpm build`.
- CI (check.yaml, docker.yaml): pnpm/action-setup + setup-node cache:pnpm; pnpm install --frozen-lockfile / pnpm build.
- Makefile, composer.json dev script, CLAUDE.md: npm/yarn -> pnpm.
pnpm build verified on a clean install (1425 modules, hoisted node_modules).
* fix(build): pin vite to 8.0.3 to fix rolldown chunk regression
vite 8.0.16 (pulled in by #653) bundles rolldown 1.0.3, which emits a
lazy chunk referencing an undefined Vue runtime-init function
(init_runtime_dom_esm_bundler), breaking the SPA at runtime. The build
succeeds so CI never caught it. Pin vite to 8.0.3 (the version 2.3.3
shipped, rolldown 1.0.0) which produces a correct bundle.
Update Node.js from 20 to 24 across CI workflows, Dockerfiles,
package.json engines field, and add .node-version file for consistent
local development.