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
118 changes: 118 additions & 0 deletions .github/workflows/joomla-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Joomla versions

# The integration tests of ci.yml run on one Joomla! version per major version
# (the matrix), each with a SHA-256 in tests/ci/joomla-checksums.sha256. Dependabot
# can't follow those, so this weekly job looks at the releases of joomla/joomla-cms:
#
# - a newer patch release of a version in the matrix: a pull request that raises
# the matrix and adds the checksum (branch joomla-versions, rewritten by this
# job while the pull request is open)
# - a release of a newer minor/major line than the matrix has: an issue, because
# whether BFStop supports it is a decision, not a version bump
#
# See tests/README.md, "Joomla versions of the CI matrix".

on:
schedule:
- cron: '41 5 * * 2'
workflow_dispatch:

permissions:
contents: write # the branch joomla-versions
pull-requests: write
issues: write
actions: write # to start ci.yml on that branch (see below)

concurrency: joomla-versions

jobs:
check:
name: Newer Joomla! releases
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
with:
php-version: '8.4'
coverage: none
- name: Look at the releases of joomla/joomla-cms
run: |
set -euo pipefail
gh api "repos/joomla/joomla-cms/releases?per_page=100" > "$RUNNER_TEMP/releases.json"
php tests/ci/joomla-updates.php plan "$RUNNER_TEMP/releases.json" | tee "$RUNNER_TEMP/plan.json"
- name: Issue for a newer minor/major line
run: |
set -euo pipefail
while read -r row; do
line="$(jq -r .line <<< "$row")"
version="$(jq -r .version <<< "$row")"
title="Joomla $line is available: add it to the CI matrix?"
known="$(gh issue list --state all --search "in:title Joomla $line" --json number,title \
| jq -r --arg t "$title" 'map(select(.title == $t)) | .[0].number // empty')"
if [ -z "$known" ]; then
gh issue create --title "$title" --label dependencies --body "Joomla! $version (line $line) is released and newer than the versions the CI matrix tests. To support it: raise or extend the matrix in \`.github/workflows/ci.yml\` (and in com_bfstop), add the package's SHA-256 to \`tests/ci/joomla-checksums.sha256\` (the file says how to check it), and see what the tests say. Opened by the weekly job $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID; it doesn't ask again for this line."
fi
done < <(jq -c '.new_lines[]' "$RUNNER_TEMP/plan.json")
- name: Pull request for newer patch releases
run: |
set -euo pipefail
if [ "$(jq '.bumps | length' "$RUNNER_TEMP/plan.json")" -eq 0 ]; then
echo "The matrix has the newest patch release of each of its versions."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B joomla-versions
body="$RUNNER_TEMP/body.md"
versions=""
{
echo "The matrix of the integration tests runs older patch releases than are available. This raises it and adds the SHA-256 of each package to \`tests/ci/joomla-checksums.sha256\`."
echo
} > "$body"
while read -r row; do
from="$(jq -r .from <<< "$row")"
to="$(jq -r .to <<< "$row")"
digest="$(jq -r .digest <<< "$row")"
package="Joomla_${to}-Stable-Full_Package.tar.gz"
curl -fsSL -o "$RUNNER_TEMP/$package" "https://github.com/joomla/joomla-cms/releases/download/$to/$package"
actual="$(sha256sum "$RUNNER_TEMP/$package" | cut -d' ' -f1)"
if [ -n "$digest" ] && [ "$digest" != "$actual" ]; then
echo "::error::The download of $package has SHA-256 $actual, but the GitHub API lists $digest for the asset. Not touching the matrix."
exit 1
fi
php tests/ci/joomla-updates.php apply "$from" "$to" "$actual"
versions="${versions:+$versions, }$to"
if [ -n "$digest" ]; then
echo "- **$from -> $to**: SHA-256 \`$actual\`, the same as the digest GitHub lists for the release asset." >> "$body"
else
echo "- **$from -> $to**: SHA-256 \`$actual\` of the downloaded package (GitHub listed no digest for the asset)." >> "$body"
fi
done < <(jq -c '.bumps[]' "$RUNNER_TEMP/plan.json")
{
echo
echo "**Before merging, please compare the MD5/SHA1 of each package with the ones on https://downloads.joomla.org** (the page of the version lists them): the SHA-256 comes from the GitHub release, not from Joomla's own page."
echo
echo "- The entries of the previous versions stay in the checksum file: the com_bfstop workflow installs through the same file. Remove them once both repositories have moved on."
echo "- com_bfstop has its own matrix; raise it there if you want the same versions."
echo "- This branch (\`joomla-versions\`) is rewritten by the weekly job while the pull request is open. A push made with the workflow's token doesn't start the \`pull_request\` workflows, so the job starts \`ci.yml\` on the branch itself; its runs are listed on the branch's latest commit."
echo
echo "Opened by $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
} >> "$body"
git commit -a -q -m "CI: Joomla $versions"
# nothing new since the last run (the matrix and checksum file are what differs)?
if git fetch -q origin joomla-versions 2> /dev/null \
&& git diff --quiet FETCH_HEAD HEAD -- .github/workflows/ci.yml tests/ci/joomla-checksums.sha256; then
echo "The branch joomla-versions has these versions already."
exit 0
fi
git push --force origin joomla-versions
number="$(gh pr list --head joomla-versions --state open --json number --jq '.[0].number // empty')"
if [ -z "$number" ]; then
gh pr create --base main --head joomla-versions --title "CI: Joomla $versions" --body-file "$body"
gh pr edit joomla-versions --add-label dependencies || true
else
gh pr edit "$number" --title "CI: Joomla $versions" --body-file "$body"
fi
gh workflow run ci.yml --ref joomla-versions
24 changes: 24 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@ the site only needs to be set up again when the database schema changes.
GitHub Actions runs everything on each push and pull request, on Joomla 5 and
6 with MySQL, MariaDB and PostgreSQL, see `.github/workflows/ci.yml`.

### Joomla versions of the CI matrix

The matrix runs one Joomla version per major version, each with its SHA-256 in
`tests/ci/joomla-checksums.sha256`. Dependabot can't follow those, so
`.github/workflows/joomla-versions.yml` looks at the releases of
`joomla/joomla-cms` every week (and on demand: Actions, "Joomla versions", Run
workflow):

- a newer **patch release** of a version in the matrix gets a pull request
(branch `joomla-versions`) that raises the matrix and adds the checksum. The
hash is the one of the downloaded package, which must equal the digest GitHub
lists for the release asset. Compare MD5/SHA1 with downloads.joomla.org
before merging; that is the check against Joomla's own page. A push with the
workflow's token doesn't start `pull_request` workflows, so the job starts
`ci.yml` on the branch itself.
- a release of a **newer minor/major line** (e.g. 6.2 when the matrix has 6.1)
only gets an issue: whether to support it is a decision.

For the pull requests, the repository setting "Allow GitHub Actions to create
and approve pull requests" has to be on. The planning and the edits are
`tests/ci/joomla-updates.php` (unit-tested in `tests/Unit/JoomlaUpdatesTest.php`).
Entries of old versions stay in the checksum file as long as com_bfstop's
matrix, which installs through the same file, may still use them.

## Running locally

Unit tests only:
Expand Down
153 changes: 153 additions & 0 deletions tests/Unit/JoomlaUpdatesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php
/*
* @package BFStop Plugin (bfstop) for Joomla!
* @author Bernhard Froehler
* @copyright (C) Bernhard Froehler
* @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
**/
namespace Codeling\Bfstop\Tests\Unit;

use InvalidArgumentException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

require_once dirname(__DIR__).'/ci/joomla-updates.php';

/**
* tests/ci/joomla-updates.php decides which Joomla! releases the weekly
* workflow proposes: patch releases of the matrix' versions are taken over,
* newer minor/major lines are only reported.
*/
class JoomlaUpdatesTest extends TestCase
{
private const Workflow = " matrix:\n include:\n"
." - { joomla: 5.4.8, php: '8.2', db: 'mysql:8.0' }\n"
." - { joomla: 5.4.8, php: '8.2', db: 'postgres:12' }\n"
." - { joomla: 6.1.3, php: '8.4', db: 'mysql:8.4' }\n";

private const Hash8 = '2c9dc7e6f451c83c43bbb7eb5c9348cb889ba802b72915c5bbd899f9d2de3f3d';
private const Hash13 = '184f8c582cde5981693de7c28547c6e834c48c50cb377c7b8421bbfd33bbdf6f';
private const Hash9 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';

private const Checksums = "# header\n#\n"
."# 5.4.8 MD5 and SHA1 equal those on downloads.joomla.org/cms/joomla5/5-4-8\n"
."# 6.1.3 taken from the GitHub release asset (Joomla's page for the version\n"
."# could not be consulted); check it against downloads.joomla.org\n"
."2c9dc7e6f451c83c43bbb7eb5c9348cb889ba802b72915c5bbd899f9d2de3f3d Joomla_5.4.8-Stable-Full_Package.tar.gz\n"
."184f8c582cde5981693de7c28547c6e834c48c50cb377c7b8421bbfd33bbdf6f Joomla_6.1.3-Stable-Full_Package.tar.gz\n";

private static function release($tag, $options = array())
{
$digest = array_key_exists('digest', $options) ? $options['digest'] : 'sha256:'.str_repeat('b', 64);
$asset = array('name' => "Joomla_$tag-Stable-Full_Package.tar.gz");
if ($digest !== null)
{
$asset['digest'] = $digest;
}
return array(
'tag_name' => $tag,
'draft' => $options['draft'] ?? false,
'prerelease' => $options['prerelease'] ?? false,
'assets' => ($options['assets'] ?? true) ? array(array('name' => 'other.zip'), $asset) : array(),
);
}

public function testMatrixVersions()
{
$this->assertSame(array('5.4.8', '6.1.3'), matrixVersions(self::Workflow));
}

public function testStableReleasesIgnoreDraftsPrereleasesOddTagsAndMissingPackages()
{
$stable = stableReleases(array(
self::release('6.1.4'),
self::release('6.2.0', array('prerelease' => true)),
self::release('6.2.0-beta1'),
self::release('6.3.0', array('draft' => true)),
self::release('5.4.9', array('assets' => false)),
self::release('3.10.12-elts'),
self::release('5.4.10', array('digest' => null)),
array('tag_name' => '5.4.11', 'assets' => array(array('name' => 'Joomla_5.4.12-Stable-Full_Package.tar.gz'))),
));
$this->assertSame(array('6.1.4' => str_repeat('b', 64), '5.4.10' => ''), $stable);
}

public function testPatchReleaseIsABumpOfItsLineOnly()
{
$stable = array('5.4.8' => '', '5.4.9' => 'x', '5.4.10' => 'y', '6.1.3' => '', '6.0.9' => 'z');
$plan = plan(array('5.4.8', '6.1.3'), $stable);
$this->assertSame(array(array('from' => '5.4.8', 'to' => '5.4.10', 'digest' => 'y')), $plan['bumps']);
$this->assertSame(array(), $plan['new_lines'], 'older lines (6.0) are not news');
}

public function testNewerLinesAreOnlyReportedOncePerLine()
{
$stable = array('5.4.8' => '', '6.1.3' => '', '6.1.4' => 'a', '6.2.0' => 'b', '6.2.1' => 'c', '7.0.0' => 'd');
$plan = plan(array('5.4.8', '6.1.3'), $stable);
$this->assertSame(array(array('from' => '6.1.3', 'to' => '6.1.4', 'digest' => 'a')), $plan['bumps']);
$this->assertSame(array(
array('line' => '6.2', 'version' => '6.2.1'),
array('line' => '7.0', 'version' => '7.0.0'),
), $plan['new_lines']);
}

public function testNothingToDoWhenCurrent()
{
$plan = plan(array('5.4.8', '6.1.3'), array('5.4.8' => '', '6.1.3' => '', '5.3.0' => ''));
$this->assertSame(array(), $plan['bumps']);
$this->assertSame(array(), $plan['new_lines']);
}

public function testApplyReplacesTheVersionAndAddsTheChecksum()
{
list($workflow, $checksums) = apply(self::Workflow, self::Checksums, '5.4.8', '5.4.9', self::Hash9);
$this->assertSame(0, substr_count($workflow, '5.4.8'));
$this->assertSame(2, substr_count($workflow, 'joomla: 5.4.9,'));
$this->assertSame(1, substr_count($workflow, 'joomla: 6.1.3,'));

$lines = explode("\n", rtrim($checksums));
$this->assertContains(self::Hash8.' Joomla_5.4.8-Stable-Full_Package.tar.gz', $lines, 'the old entry stays');
$this->assertContains(self::Hash9.' Joomla_5.4.9-Stable-Full_Package.tar.gz', $lines);
$this->assertLessThan(
array_search(self::Hash13.' Joomla_6.1.3-Stable-Full_Package.tar.gz', $lines),
array_search(self::Hash9.' Joomla_5.4.9-Stable-Full_Package.tar.gz', $lines),
'the new entry is behind that of the old version');
$this->assertStringContainsString("# 5.4.9 taken from the GitHub release asset", $checksums);
$this->assertStringContainsString('downloads.joomla.org/cms/joomla5/5-4-9', $checksums);
// the notes stay in the comment block, in front of the checksums
$this->assertLessThan(strpos($checksums, self::Hash8), strpos($checksums, '# 5.4.9'));
}

public function testApplyKeepsWhatLintAndInstallerRead()
{
list($workflow, $checksums) = apply(self::Workflow, self::Checksums, '6.1.3', '6.1.4', self::Hash9);
$this->assertSame(array('5.4.8', '6.1.4'), matrixVersions($workflow));
foreach (explode("\n", rtrim($checksums)) as $line)
{
$this->assertTrue($line === '' || $line[0] === '#'
|| preg_match('/^[0-9a-f]{64} Joomla_\d+\.\d+\.\d+-Stable-Full_Package\.tar\.gz$/', $line) === 1, $line);
}
}

#[DataProvider('badApplications')]
public function testApplyRefusesWhatItCannotDoSafely($from, $to, $hash, $checksums = self::Checksums)
{
$this->expectException(InvalidArgumentException::class);
apply(self::Workflow, $checksums, $from, $to, $hash);
}

public static function badApplications()
{
return array(
'other line' => array('5.4.8', '5.5.0', self::Hash9),
'older' => array('6.1.3', '6.1.2', self::Hash9),
'same' => array('6.1.3', '6.1.3', self::Hash9),
'not in the matrix' => array('5.4.7', '5.4.9', self::Hash9),
'garbage version' => array('5.4.8', '5.4.9; rm -rf /', self::Hash9),
'short hash' => array('5.4.8', '5.4.9', 'abc'),
'upper case hash' => array('5.4.8', '5.4.9', strtoupper(self::Hash9)),
'already listed' => array('5.4.8', '5.4.9', self::Hash9,
self::Checksums.self::Hash9." Joomla_5.4.9-Stable-Full_Package.tar.gz\n"),
);
}
}
Loading
Loading