From 3308f3b2fa99d23c488b3f4dd6e57a548f0a9ec7 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:04:30 +0200 Subject: [PATCH] ci: source release notes from CHANGELOG.md (#713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of the 2.x change to 3.x, with the same extractor script. 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 fails the job rather than registering an empty changelog. A manual dispatch falls back to the release body, since re-registering a release older than this file is legitimate — which covers 3.0.0-alpha.1. Backfilled with 3.0.0-alpha.1. --- .github/scripts/changelog-section.php | 71 +++++++++++++++++++++++++++ .github/workflows/docker.yaml | 17 ++++++- CHANGELOG.md | 16 ++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/changelog-section.php create mode 100644 CHANGELOG.md diff --git a/.github/scripts/changelog-section.php b/.github/scripts/changelog-section.php new file mode 100644 index 00000000..4c607db1 --- /dev/null +++ b/.github/scripts/changelog-section.php @@ -0,0 +1,71 @@ + [changelog-path] + * + * Exits 1 when the version has no section, so a release whose notes were + * forgotten stops the pipeline rather than registering an empty changelog on + * the updater. + * + * Section boundaries are found by matching *version* headings, not any "## ", + * because release notes routinely contain their own second-level headings. A + * section therefore runs until the next heading that looks like a version. + */ +$version = $argv[1] ?? ''; +$path = $argv[2] ?? dirname(__DIR__, 2).'/CHANGELOG.md'; + +if ($version === '') { + fwrite(STDERR, "usage: changelog-section.php [changelog-path]\n"); + exit(2); +} + +if (! is_readable($path)) { + fwrite(STDERR, "changelog not readable: {$path}\n"); + exit(2); +} + +$lines = preg_split('/\R/', (string) file_get_contents($path)); + +// A leading "v" is tolerated on either side so v2.4.2 and 2.4.2 both resolve. +$isVersionHeading = static fn (string $line): bool => (bool) preg_match('/^##\s+v?\d+\.\d+\.\d+/i', $line); +$wanted = ltrim($version, 'vV'); + +$section = []; +$capturing = false; + +foreach ($lines as $line) { + if ($isVersionHeading($line)) { + if ($capturing) { + break; + } + + // "## 2.4.2 — 2026-07-29" and "## 2.4.2" both match; a date or any other + // trailing text is ignored, but 2.4.2 must not match 2.4.20. + $capturing = (bool) preg_match( + '/^##\s+v?'.preg_quote($wanted, '/').'(?![\w.-])/i', + $line + ); + + continue; + } + + if ($capturing) { + $section[] = $line; + } +} + +$body = trim(implode("\n", $section)); + +if (! $capturing && $body === '') { + fwrite(STDERR, "no CHANGELOG.md section found for {$version}\n"); + exit(1); +} + +if ($body === '') { + fwrite(STDERR, "CHANGELOG.md section for {$version} is empty\n"); + exit(1); +} + +echo $body, "\n"; diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 17142bd1..59017c4d 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -206,7 +206,22 @@ jobs: # 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. - gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body --jq '.body' > /tmp/changelog.txt + # 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 + # empty changelog — except on a manual dispatch, where re-registering a + # release older than the file is legitimate and the GitHub body stands in. + if php .github/scripts/changelog-section.php "$TAG" > /tmp/changelog.txt; then + echo "Using the CHANGELOG.md section for $TAG" + elif [ "$EVENT" = "release" ]; then + echo "::error::No CHANGELOG.md section for $TAG — add one and re-run this job." + exit 1 + else + echo "::warning::No CHANGELOG.md section for $TAG — falling back to the release body." + gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body --jq '.body' > /tmp/changelog.txt + fi + # The pre-release flag and timestamp come from the release itself, so this + # behaves identically whether triggered by a publish or re-run by hand. PRERELEASE=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isPrerelease --jq '.isPrerelease') PUBLISHED=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json publishedAt --jq '.publishedAt') diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..0e30f458 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +Release notes for the 3.x line. Each `##` heading is a released version, and the +section beneath it is what CI publishes to the updater — see +`.github/scripts/changelog-section.php`. + +The 2.x line has its own CHANGELOG.md on the `2.x` branch. Releases are also on +GitHub: https://github.com/InvoiceShelf/InvoiceShelf/releases + +## 3.0.0-alpha.1 — 2026-06-14 + +First public alpha of InvoiceShelf 3.0 — the next-generation rewrite (Laravel 13 / PHP 8.4, Vue 3 + TypeScript, Tailwind v4). + +⚠️ **Pre-release — not for production.** For evaluation and testing only. + +Docker: `invoiceshelf/invoiceshelf:3.0.0-alpha.1` (also `:next`).