diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82608b9..97457b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,13 @@ on: branches: [main] pull_request: workflow_dispatch: + # by release.yml, for a version tag: everything is built and tested from + # the tag, with the com_bfstop tag of the same name + workflow_call: + outputs: + version: + description: version of the package built (artifact bfstop-) + value: ${{ jobs.package.outputs.version }} permissions: contents: read @@ -97,14 +104,25 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - # use the com_bfstop branch of the same name if there is one and it - # has commits main hasn't (for changes spanning both), else main: a - # branch which was merged and not deleted is a stale copy of an old main + # a release tag: the com_bfstop tag of the same name, which has to + # exist (tag com_bfstop first). Otherwise the com_bfstop branch of the + # same name if there is one and it has commits main hasn't (for changes + # spanning both), else main: a branch which was merged and not deleted + # is a stale copy of an old main - name: Determine com_bfstop branch id: com env: GH_TOKEN: ${{ github.token }} run: | + if [ "$GITHUB_REF_TYPE" = tag ]; then + if ! git ls-remote --exit-code --tags https://github.com/codeling/com_bfstop.git "refs/tags/$GITHUB_REF_NAME" > /dev/null; then + echo "::error::com_bfstop has no tag $GITHUB_REF_NAME - tag the component first" + exit 1 + fi + echo "ref=refs/tags/$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" + echo "Using com_bfstop tag $GITHUB_REF_NAME" + exit 0 + fi branch="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" ref=main ahead=none @@ -128,6 +146,9 @@ jobs: with: php-version: '8.4' coverage: none + - name: Versions are those of the release + if: github.ref_type == 'tag' + run: COM_BFSTOP_ROOT="$RUNNER_TEMP/com_bfstop" php tests/lint/check-versions.php --release="$GITHUB_REF_NAME" - name: Build plugin, component and package id: build run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0d90096 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release + +# Pushing a version tag (tag com_bfstop with the same name first) builds the +# release from the tags and tests it like every change (ci.yml: all Joomla +# sites install the package, and a site of the previous release is updated +# with it), then creates a *draft* GitHub release with the zips, their SHA-256 +# sums and notes from the CHANGELOG - published by hand once it was checked. +# +# X.Y.Z a release: the CHANGELOG entry needs its date +# X.Y.Z-alphaN/-betaN/-rcN a test release: marked as pre-release on +# GitHub; never offered by the update server +# +# The versions in the manifests have to be the tag (tests/lint/check-versions.php), +# so a tag on the wrong commit fails before anything is built. +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+-*' + +permissions: + contents: read + +jobs: + ci: + name: Build and test + uses: ./.github/workflows/ci.yml + + release: + name: Draft GitHub release + needs: ci + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 + with: + php-version: '8.4' + coverage: none + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: bfstop-${{ needs.ci.outputs.version }} + path: build + - name: Create the draft release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + VERSION: ${{ needs.ci.outputs.version }} + run: | + if [ "$TAG" != "$VERSION" ]; then + echo "::error::the package built is version $VERSION, the tag is $TAG" + exit 1 + fi + php package/release-notes.php "$VERSION" build > "$RUNNER_TEMP/notes.md" + flags=(--draft --verify-tag --title "BFStop $VERSION" --notes-file "$RUNNER_TEMP/notes.md") + if php tests/lint/check-versions.php --is-prerelease="$VERSION"; then + flags+=(--prerelease) + fi + gh release create "$VERSION" "${flags[@]}" \ + "build/pkg_bfstop-$VERSION.zip" "build/bfstop-$VERSION.zip" \ + "build/com_bfstop-$VERSION.zip" "build/SHA256SUMS-$VERSION" + echo "Draft release created: check it, then publish it on GitHub" >> "$GITHUB_STEP_SUMMARY" diff --git a/package/README.md b/package/README.md index 7395732..07c7dca 100644 --- a/package/README.md +++ b/package/README.md @@ -26,10 +26,37 @@ site of the previous release with it (see `tests/README.md`). ## Versions -Plugin, component and package are released together, with one version number. -It is written in `bfstop.xml` of both repositories, in `pkg_bfstop.xml` and -`update.xml`, and in the first entry of `CHANGELOG`, which says `(unreleased)` -until the release, then the date. `tests/lint/check-versions.php` checks that -they agree (CI runs it, and `build.sh` refuses to build otherwise); -`--release=` additionally checks them against the tag and requires the -CHANGELOG date. The `deploy.sh` scripts take the version from their manifest. +Plugin, component and package are released together, with one version number, +written in `bfstop.xml` of both repositories, in `pkg_bfstop.xml` and in the +first entry of `CHANGELOG` - which says `(unreleased)` until the release, then +the date. `tests/lint/check-versions.php` checks that they agree (CI runs it, +and `build.sh` refuses to build otherwise). The `deploy.sh` scripts take the +version from their manifest. + +A version is `X.Y.Z`, or `X.Y.Z-betaN` / `-rcN` / `-alphaN` for a **test +release**. A test release carries that version in the manifests (so testers see +what they have installed), and the CHANGELOG entry stays the one of the release +it leads to (`2.1.0 (unreleased)` for `2.1.0-rc1`). Joomla sorts them before the +release (`2.1.0-beta1 < 2.1.0-rc1 < 2.1.0`), so a site with a test release is +updated to the release normally. + +`update.xml` is not part of this: it says what the update server offers, which +is the latest *release*, never a test release. + +## Releasing + +1. Set the versions (both repositories) and, for a release, the date of the + CHANGELOG entry; merge. +2. Tag com_bfstop, then this repository, with the version (`2.1.0-rc1`, + `2.1.0`) and push the tags. +3. `.github/workflows/release.yml` runs on the tag: it checks the versions + against the tag (`check-versions.php --release`), builds from the two tags, + runs all of CI on that build (every Joomla site installs the package, a site + of the previous release is updated with it), and creates a **draft** GitHub + release with the three zips, the SHA-256 sums and notes from the CHANGELOG + (`release-notes.php`). A test release is marked as pre-release and says so + in its notes. +4. Check the draft and publish it. +5. Only for a release: update the update server (`update.xml` and the + details file) - and put the release into `tests/ci/previous-release.sha256`, + so that CI tests the update from it from then on. diff --git a/package/release-notes.php b/package/release-notes.php new file mode 100644 index 0000000..b2dd38f --- /dev/null +++ b/package/release-notes.php @@ -0,0 +1,79 @@ + +**/ + +if ($argc !== 3) +{ + fwrite(STDERR, "Usage: php {$argv[0]} \n"); + exit(2); +} +[, $version, $buildDir] = $argv; +$root = dirname(__DIR__); + +if (!preg_match('/^(\d+\.\d+\.\d+)(?:-(alpha|beta|rc)[1-9]\d*)?$/', $version, $match)) +{ + fwrite(STDERR, "Not a version: $version\n"); + exit(1); +} +$base = $match[1]; +$prerelease = isset($match[2]); + +// the CHANGELOG entry: from its "X.Y.Z (date)" line to the next one +$entry = array(); +$inEntry = false; +foreach (file($root.'/CHANGELOG', FILE_IGNORE_NEW_LINES) as $line) +{ + if (preg_match('/^(\S+) \((.+)\)$/', $line, $header)) + { + if ($inEntry) + { + break; + } + $inEntry = ($header[1] === $base); + continue; + } + if ($inEntry) + { + $entry[] = $line; + } +} +if (count($entry) === 0) +{ + fwrite(STDERR, "No CHANGELOG entry for $base\n"); + exit(1); +} + +$script = (string) file_get_contents($root.'/package/script.php'); +$joomla = preg_match('/\$minimumJoomla = \'([0-9.]+)\'/', $script, $m) ? $m[1] : null; +$php = preg_match('/\$minimumPhp = \'([0-9.]+)\'/', $script, $m) ? $m[1] : null; + +$sums = glob(rtrim($buildDir, '/').'/SHA256SUMS-'.$version); + +if ($prerelease) +{ + echo "> [!WARNING]\n". + "> **Test release** of BFStop $base, for trying it out before the release - preferably on a test site.\n". + "> It is not offered through Joomla's update. A site which installs it is offered $base there once that is released; later test releases have to be installed the same way as this one.\n\n"; +} +echo "Install **pkg_bfstop-$version.zip** (plugin and component) through Joomla's extension installer"; +if ($joomla !== null && $php !== null) +{ + echo "; it needs Joomla! ".preg_replace('/\.0(\.0)?$/', '', $joomla)." or later and PHP ".preg_replace('/\.0$/', '', $php)." or later"; +} +echo ".\n\n"; +echo "## Changes".($prerelease ? " in $base" : '')."\n\n```text\n".rtrim(implode("\n", $entry))."\n```\n"; +if (count($sums) === 1) +{ + echo "\n## SHA-256\n\n```text\n".rtrim((string) file_get_contents($sums[0]))."\n```\n"; +} diff --git a/tests/Integration/InstallerScriptTest.php b/tests/Integration/InstallerScriptTest.php index a792afa..aabcedf 100644 --- a/tests/Integration/InstallerScriptTest.php +++ b/tests/Integration/InstallerScriptTest.php @@ -84,6 +84,7 @@ public function testSettingsMovedHintOnlyForUpdatesFromBefore200() Factory::getApplication()->getLanguage()->load('plg_system_bfstop', JPATH_ADMINISTRATOR); $this->assertTrue($this->updateHintShown('1.5.2')); $this->assertFalse($this->updateHintShown('2.0.0'), 'the settings did not move again'); + $this->assertFalse($this->updateHintShown('2.0.0-rc1'), 'a test release of 2.0.0 had them where they are'); } public function testRefusesToReplaceANewerVersion() diff --git a/tests/Upgrade/UpgradeTest.php b/tests/Upgrade/UpgradeTest.php index 2367991..ec230e9 100644 --- a/tests/Upgrade/UpgradeTest.php +++ b/tests/Upgrade/UpgradeTest.php @@ -104,7 +104,7 @@ public function testUpdateMessages() { // the CLI wraps the messages $log = preg_replace('/\s+/', ' ', (string) file_get_contents(getenv('UPGRADE_LOG'))); - $settingsMoved = version_compare(getenv('UPGRADE_FROM'), '2.0.0', '<') ? 1 : 0; + $settingsMoved = version_compare(getenv('UPGRADE_FROM'), '2.0.0-dev', '<') ? 1 : 0; $this->assertSame($settingsMoved, substr_count($log, 'major configuration changes'), 'the "configuration has moved" hint, only when updating from before 2.0.0'); $this->assertSame(0, substr_count($log, 'protects your site with its default settings'), diff --git a/tests/lint/check-versions.php b/tests/lint/check-versions.php index d1119f2..48eee08 100644 --- a/tests/lint/check-versions.php +++ b/tests/lint/check-versions.php @@ -9,18 +9,28 @@ * number, which is written in several places. This checks that they agree: * * - bfstop.xml (the plugin) - * - package/pkg_bfstop.xml and package/update.xml (the package) - * - the first entry of CHANGELOG ("2.0.0 (unreleased)" or "2.0.0 (2026-11-06)") + * - package/pkg_bfstop.xml (the package) * - bfstop.xml of the component, if COM_BFSTOP_ROOT is set + * - the first entry of CHANGELOG ("2.0.0 (unreleased)" or "2.0.0 (2026-11-06)") + * + * A version is X.Y.Z, or X.Y.Z-alphaN, -betaN or -rcN for a test release + * (which the update server never offers). The CHANGELOG entry of a test + * release is that of the release it leads to: 2.1.0-rc1 has "2.1.0 (...)". * * Usage: * - * php tests/lint/check-versions.php all agree - * php tests/lint/check-versions.php --release= ... are , and the - * CHANGELOG entry has a date - * php tests/lint/check-versions.php --print-version prints the plugin's version + * php tests/lint/check-versions.php they agree + * php tests/lint/check-versions.php --release= ... and are ; for a + * release (not a test release) + * the CHANGELOG entry has its date + * php tests/lint/check-versions.php --print-version prints the plugin's version + * php tests/lint/check-versions.php --is-prerelease= + * exit status 0 if is + * a test release, 1 if not **/ +const VersionPattern = '/^(\d+\.\d+\.\d+)(?:-(alpha|beta|rc)[1-9]\d*)?$/'; + $root = dirname(__DIR__, 2); $release = null; foreach (array_slice($argv, 1) as $arg) @@ -30,12 +40,17 @@ echo manifestVersion($root.'/bfstop.xml')."\n"; exit(0); } + if (str_starts_with($arg, '--is-prerelease=')) + { + $version = substr($arg, strlen('--is-prerelease=')); + exit(preg_match(VersionPattern, $version, $m) && isset($m[2]) ? 0 : 1); + } if (str_starts_with($arg, '--release=')) { $release = substr($arg, strlen('--release=')); continue; } - fwrite(STDERR, "Usage: php {$argv[0]} [--release= | --print-version]\n"); + fwrite(STDERR, "Usage: php {$argv[0]} [--release= | --print-version | --is-prerelease=]\n"); exit(2); } @@ -54,9 +69,13 @@ function fail($message) 'bfstop.xml' => manifestVersion($root.'/bfstop.xml'), 'package/pkg_bfstop.xml' => manifestVersion($root.'/package/pkg_bfstop.xml'), ); -$update = @simplexml_load_file($root.'/package/update.xml'); -$versions['package/update.xml'] = $update === false ? null : (string) $update->extension['version']; +$componentRoot = getenv('COM_BFSTOP_ROOT'); +if ($componentRoot) +{ + $versions['com_bfstop bfstop.xml'] = manifestVersion($componentRoot.'/bfstop.xml'); +} +$changelogVersion = null; $changelogDate = null; foreach (file($root.'/CHANGELOG', FILE_IGNORE_NEW_LINES) as $line) { @@ -67,24 +86,23 @@ function fail($message) // the first line which isn't empty is the newest entry if (preg_match('/^(\S+) \((.+)\)$/', $line, $match)) { - $versions['CHANGELOG'] = $match[1]; + $changelogVersion = $match[1]; $changelogDate = $match[2]; } - else - { - $versions['CHANGELOG'] = null; - } break; } -$componentRoot = getenv('COM_BFSTOP_ROOT'); -if ($componentRoot) +$errors = 0; +$expected = $release ?? $versions['bfstop.xml']; +if ($expected === null || !preg_match(VersionPattern, $expected, $match)) { - $versions['com_bfstop bfstop.xml'] = manifestVersion($componentRoot.'/bfstop.xml'); + fail(($release !== null ? "the tag" : "bfstop.xml")." has no valid version: '$expected' ". + "(X.Y.Z, or X.Y.Z-alphaN, -betaN, -rcN for a test release)"); + exit(1); } +$base = $match[1]; +$prerelease = isset($match[2]); -$errors = 0; -$expected = $release ?? $versions['bfstop.xml']; foreach ($versions as $where => $version) { if ($version === null || $version === '') @@ -98,12 +116,19 @@ function fail($message) ++$errors; } } -if ($release !== null && $changelogDate !== null && +if ($changelogVersion !== $base) +{ + fail("the first CHANGELOG entry has to be '$base (...)'".($prerelease ? " (that of the release $expected leads to)" : ''). + ", it is ".($changelogVersion === null ? 'missing' : "'$changelogVersion'")); + ++$errors; +} +elseif ($release !== null && !$prerelease && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $changelogDate) || strtotime($changelogDate) === false)) { fail("the CHANGELOG entry of $release has no release date: '$changelogDate' (write it as YYYY-MM-DD)"); ++$errors; } -echo count($versions)." versions checked".($release !== null ? " against release $release" : '').", $errors problem(s)\n"; +echo (count($versions) + 1)." versions checked".($release !== null ? " against ".($prerelease ? 'test release ' : 'release ').$release : ''). + ", $errors problem(s)\n"; exit($errors > 0 ? 1 : 0); diff --git a/updatescript.php b/updatescript.php index 3cd12e2..0c4dc75 100644 --- a/updatescript.php +++ b/updatescript.php @@ -104,9 +104,10 @@ private function renameWhitelistTable() public function postflight($type, $adapter) { // only for the update from a version before the settings moved, not - // for every later update + // for every later update - nor from a test release of 2.0.0 (2.0.0-dev + // sorts before 2.0.0-alpha1, -beta1 and -rc1) if ($type === 'update' && $this->previousVersion !== null && - version_compare($this->previousVersion, '2.0.0', '<')) + version_compare($this->previousVersion, '2.0.0-dev', '<')) { $lang = Factory::getApplication()->getLanguage(); $lang->load('plg_system_bfstop', JPATH_ADMINISTRATOR);