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`).