Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>)
value: ${{ jobs.package.outputs.version }}

permissions:
contents: read
Expand Down Expand Up @@ -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
Expand All @@ -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: |
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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"
41 changes: 34 additions & 7 deletions package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<tag>` 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.
79 changes: 79 additions & 0 deletions package/release-notes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
/*
* @package BFStop for Joomla!
* @author Bernhard Froehler
* @copyright (C) Bernhard Froehler
* @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
*
* Writes the notes of a GitHub release (Markdown) to stdout: what the release
* needs (from package/script.php), its CHANGELOG entry, and the SHA-256 sums
* from the build directory. A test release (X.Y.Z-alphaN, -betaN, -rcN) gets
* the entry of the release it leads to, and says that it is one.
*
* Usage: php package/release-notes.php <version> <build directory>
**/

if ($argc !== 3)
{
fwrite(STDERR, "Usage: php {$argv[0]} <version> <build directory>\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";
}
1 change: 1 addition & 0 deletions tests/Integration/InstallerScriptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion tests/Upgrade/UpgradeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
67 changes: 46 additions & 21 deletions tests/lint/check-versions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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=<tag> ... are <tag>, 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=<tag> ... and are <tag>; 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=<version>
* exit status 0 if <version> 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)
Expand All @@ -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=<tag> | --print-version]\n");
fwrite(STDERR, "Usage: php {$argv[0]} [--release=<tag> | --print-version | --is-prerelease=<version>]\n");
exit(2);
}

Expand All @@ -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)
{
Expand All @@ -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 === '')
Expand All @@ -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);
5 changes: 3 additions & 2 deletions updatescript.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading