diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb52537..2010292 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,19 +66,26 @@ jobs: if: matrix.php != '8.1' run: phpunit -c tests/phpunit.xml.dist --testsuite unit + # the Joomla!/PHP/database combinations live in tests/ci/matrix.json, a file + # which the joomla-versions workflow can change (a workflow file it can't) + matrix: + name: Test matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.read.outputs.matrix }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - id: read + run: | + echo "matrix=$(jq -c '{include: .include}' tests/ci/matrix.json)" >> "$GITHUB_OUTPUT" + integration: name: Joomla ${{ matrix.joomla }}, ${{ matrix.db }}, PHP ${{ matrix.php }} + needs: matrix runs-on: ubuntu-latest strategy: fail-fast: false - matrix: - include: - - { joomla: 5.4.8, php: '8.2', db: 'mysql:8.0' } - - { joomla: 5.4.8, php: '8.2', db: 'mariadb:10.11' } - - { joomla: 5.4.8, php: '8.2', db: 'postgres:12' } - - { joomla: 6.1.3, php: '8.4', db: 'mysql:8.4' } - - { joomla: 6.1.3, php: '8.4', db: 'mariadb:11.4' } - - { joomla: 6.1.3, php: '8.4', db: 'postgres:17' } + matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} env: JOOMLA_VERSION: ${{ matrix.joomla }} DB_HOST: 127.0.0.1 diff --git a/.github/workflows/joomla-versions.yml b/.github/workflows/joomla-versions.yml index 688311c..f146a86 100644 --- a/.github/workflows/joomla-versions.yml +++ b/.github/workflows/joomla-versions.yml @@ -1,7 +1,10 @@ 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 +# (the matrix in tests/ci/matrix.json), each with a SHA-256 in +# tests/ci/joomla-checksums.sha256. The matrix is a data file, not part of the +# workflow file, because the token of a workflow can't push changes to workflow +# files (GitHub requires a separate `workflows` permission it can't have). 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 @@ -52,7 +55,7 @@ jobs: 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 - url="$(gh issue create --title "$title" --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.")" + url="$(gh issue create --title "$title" --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 \`tests/ci/matrix.json\` (and in com_bfstop's \`ci.yml\`), 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.")" # the label is a nicety: the repository may not have it gh issue edit "$url" --add-label dependencies || true fi @@ -107,7 +110,7 @@ jobs: 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 + && git diff --quiet FETCH_HEAD HEAD -- tests/ci/matrix.json tests/ci/joomla-checksums.sha256; then echo "The branch joomla-versions has these versions already." exit 0 fi diff --git a/tests/README.md b/tests/README.md index 2ce31ff..05e6f9d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -38,11 +38,14 @@ branch that was merged and not deleted is therefore ignored, not tested against. ### 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 +The matrix runs one Joomla version per major version. It is `tests/ci/matrix.json` +(which `ci.yml` reads in a first job), each version with its SHA-256 in +`tests/ci/joomla-checksums.sha256`; raise a version in both files, the lint +checks that they agree. 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): +workflow - not "Re-run" of an old run, which uses the workflow file of that +run): - 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 diff --git a/tests/Unit/JoomlaUpdatesTest.php b/tests/Unit/JoomlaUpdatesTest.php index 8c1ca9c..347f7f1 100644 --- a/tests/Unit/JoomlaUpdatesTest.php +++ b/tests/Unit/JoomlaUpdatesTest.php @@ -20,10 +20,11 @@ */ 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 Matrix = "{\n \"_\": \"note\",\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" + ." ]\n}\n"; private const Hash8 = '2c9dc7e6f451c83c43bbb7eb5c9348cb889ba802b72915c5bbd899f9d2de3f3d'; private const Hash13 = '184f8c582cde5981693de7c28547c6e834c48c50cb377c7b8421bbfd33bbdf6f'; @@ -54,7 +55,14 @@ private static function release($tag, $options = array()) public function testMatrixVersions() { - $this->assertSame(array('5.4.8', '6.1.3'), matrixVersions(self::Workflow)); + $this->assertSame(array('5.4.8', '6.1.3'), matrixVersions(self::Matrix)); + } + + public function testMatrixVersionsOfUnusableFiles() + { + $this->assertSame(array(), matrixVersions('not json')); + $this->assertSame(array(), matrixVersions('{"include": "x"}')); + $this->assertSame(array('5.4.8'), matrixVersions('{"include": [{"joomla": "5.4.8"}, {"joomla": "5.x"}, {"php": "8.2"}, 7]}')); } public function testStableReleasesIgnoreDraftsPrereleasesOddTagsAndMissingPackages() @@ -100,10 +108,12 @@ public function testNothingToDoWhenCurrent() 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,')); + list($matrix, $checksums) = apply(self::Matrix, self::Checksums, '5.4.8', '5.4.9', self::Hash9); + $this->assertSame(0, substr_count($matrix, '5.4.8')); + $this->assertSame(2, substr_count($matrix, '"joomla": "5.4.9"')); + $this->assertSame(1, substr_count($matrix, '"joomla": "6.1.3"')); + $this->assertSame(strtr(self::Matrix, array('5.4.8' => '5.4.9')), $matrix, 'nothing else changes, formatting included'); + $this->assertIsArray(json_decode($matrix, true), 'still JSON'); $lines = explode("\n", rtrim($checksums)); $this->assertContains(self::Hash8.' Joomla_5.4.8-Stable-Full_Package.tar.gz', $lines, 'the old entry stays'); @@ -120,8 +130,8 @@ public function testApplyReplacesTheVersionAndAddsTheChecksum() 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)); + list($matrix, $checksums) = apply(self::Matrix, self::Checksums, '6.1.3', '6.1.4', self::Hash9); + $this->assertSame(array('5.4.8', '6.1.4'), matrixVersions($matrix)); foreach (explode("\n", rtrim($checksums)) as $line) { $this->assertTrue($line === '' || $line[0] === '#' @@ -133,7 +143,7 @@ public function testApplyKeepsWhatLintAndInstallerRead() public function testApplyRefusesWhatItCannotDoSafely($from, $to, $hash, $checksums = self::Checksums) { $this->expectException(InvalidArgumentException::class); - apply(self::Workflow, $checksums, $from, $to, $hash); + apply(self::Matrix, $checksums, $from, $to, $hash); } public static function badApplications() diff --git a/tests/ci/joomla-checksums.sha256 b/tests/ci/joomla-checksums.sha256 index 16da92e..d18475f 100644 --- a/tests/ci/joomla-checksums.sha256 +++ b/tests/ci/joomla-checksums.sha256 @@ -1,6 +1,6 @@ # SHA-256 of the Joomla! full packages the integration tests install # (tests/ci/download-joomla.sh refuses a package that doesn't match). The same -# versions have to be listed here as in the matrix of .github/workflows/ci.yml +# versions have to be listed here as in the matrix tests/ci/matrix.json # (checked by tests/lint/check-joomla-checksums.php). The workflow of com_bfstop # installs through the same script, so a version it uses has to be listed here. # diff --git a/tests/ci/joomla-updates.php b/tests/ci/joomla-updates.php index 411ae45..a805d6e 100644 --- a/tests/ci/joomla-updates.php +++ b/tests/ci/joomla-updates.php @@ -5,8 +5,9 @@ * @copyright (C) Bernhard Froehler * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html **/ -// Finds newer Joomla! releases for the matrix of .github/workflows/ci.yml and -// applies them; used by .github/workflows/joomla-versions.yml. +// Finds newer Joomla! releases for the matrix of the integration tests +// (tests/ci/matrix.json, read by .github/workflows/ci.yml) and applies them; +// used by .github/workflows/joomla-versions.yml. // // php tests/ci/joomla-updates.php plan RELEASES.json // RELEASES.json is the answer of the GitHub API for @@ -24,11 +25,20 @@ const JoomlaUpdatesPackage = 'Joomla_%s-Stable-Full_Package.tar.gz'; -/** @return string[] the Joomla! versions of the matrix, ascending */ -function matrixVersions($workflow) +/** @return string[] the Joomla! versions of the matrix (the text of tests/ci/matrix.json), ascending */ +function matrixVersions($matrixJson) { - preg_match_all('/\bjoomla:\s*(\d+\.\d+\.\d+)\b/', $workflow, $matches); - $versions = array_values(array_unique($matches[1])); + $matrix = json_decode($matrixJson, true); + $entries = is_array($matrix) && is_array($matrix['include'] ?? null) ? $matrix['include'] : array(); + $versions = array(); + foreach ($entries as $entry) + { + if (is_array($entry) && isset($entry['joomla']) && preg_match('/^\d+\.\d+\.\d+$/', $entry['joomla'])) + { + $versions[$entry['joomla']] = true; + } + } + $versions = array_keys($versions); usort($versions, 'version_compare'); return $versions; } @@ -104,8 +114,8 @@ function plan(array $matrix, array $stable) return array('matrix' => $matrix, 'bumps' => $bumps, 'new_lines' => $newLines); } -/** @return array(string, string) workflow and checksum file with $to instead of $from */ -function apply($workflow, $checksums, $from, $to, $sha256) +/** @return array(string, string) matrix and checksum file with $to instead of $from */ +function apply($matrixJson, $checksums, $from, $to, $sha256) { foreach (array($from, $to) as $version) { @@ -122,7 +132,7 @@ function apply($workflow, $checksums, $from, $to, $sha256) { throw new InvalidArgumentException("$to is not a newer release of the $from line"); } - $workflow = preg_replace('/\bjoomla:(\s*)'.preg_quote($from, '/').'\b/', 'joomla:${1}'.$to, $workflow, -1, $count); + $matrixJson = preg_replace('/("joomla":\s*")'.preg_quote($from, '/').'(")/', '${1}'.$to.'${2}', $matrixJson, -1, $count); if ($count === 0) { throw new InvalidArgumentException("Joomla $from is not in the matrix"); @@ -144,13 +154,13 @@ function apply($workflow, $checksums, $from, $to, $sha256) $note = "# $to taken from the GitHub release asset by the joomla-versions workflow;\n" ."# compare with downloads.joomla.org/cms/joomla$major/".str_replace('.', '-', $to); $checksums = preg_replace('/^(?!#)/m', $note."\n", $checksums, 1); - return array($workflow, $checksums); + return array($matrixJson, $checksums); } function main(array $argv) { $root = dirname(__DIR__, 2); - $workflowFile = $root.'/.github/workflows/ci.yml'; + $matrixFile = $root.'/tests/ci/matrix.json'; $checksumFile = $root.'/tests/ci/joomla-checksums.sha256'; $command = $argv[1] ?? ''; if ($command === 'plan' && count($argv) === 3) @@ -161,14 +171,14 @@ function main(array $argv) fwrite(STDERR, "$argv[2] is not a list of releases\n"); return 2; } - echo json_encode(plan(matrixVersions(file_get_contents($workflowFile)), stableReleases($releases)), JSON_PRETTY_PRINT)."\n"; + echo json_encode(plan(matrixVersions(file_get_contents($matrixFile)), stableReleases($releases)), JSON_PRETTY_PRINT)."\n"; return 0; } if ($command === 'apply' && count($argv) === 5) { try { - list($workflow, $checksums) = apply(file_get_contents($workflowFile), file_get_contents($checksumFile), + list($matrix, $checksums) = apply(file_get_contents($matrixFile), file_get_contents($checksumFile), $argv[2], $argv[3], $argv[4]); } catch (InvalidArgumentException $e) @@ -176,7 +186,7 @@ function main(array $argv) fwrite(STDERR, $e->getMessage()."\n"); return 1; } - file_put_contents($workflowFile, $workflow); + file_put_contents($matrixFile, $matrix); file_put_contents($checksumFile, $checksums); return 0; } diff --git a/tests/ci/matrix.json b/tests/ci/matrix.json new file mode 100644 index 0000000..b1e11ad --- /dev/null +++ b/tests/ci/matrix.json @@ -0,0 +1,11 @@ +{ + "_": "Matrix of the integration jobs in .github/workflows/ci.yml. The Joomla versions need a SHA-256 in tests/ci/joomla-checksums.sha256 (checked by tests/lint/check-joomla-checksums.php). .github/workflows/joomla-versions.yml raises the patch versions; keep one entry per line.", + "include": [ + { "joomla": "5.4.8", "php": "8.2", "db": "mysql:8.0" }, + { "joomla": "5.4.8", "php": "8.2", "db": "mariadb:10.11" }, + { "joomla": "5.4.8", "php": "8.2", "db": "postgres:12" }, + { "joomla": "6.1.3", "php": "8.4", "db": "mysql:8.4" }, + { "joomla": "6.1.3", "php": "8.4", "db": "mariadb:11.4" }, + { "joomla": "6.1.3", "php": "8.4", "db": "postgres:17" } + ] +} diff --git a/tests/lint/check-joomla-checksums.php b/tests/lint/check-joomla-checksums.php index 7e589f1..1d11da7 100644 --- a/tests/lint/check-joomla-checksums.php +++ b/tests/lint/check-joomla-checksums.php @@ -10,8 +10,10 @@ // The integration tests only install a Joomla! package whose SHA-256 is in // tests/ci/joomla-checksums.sha256 (see tests/ci/download-joomla.sh). This // checks that the file is well-formed and lists every Joomla! version of the -// matrix of .github/workflows/ci.yml, so that raising a version without its -// checksum fails here, not in half a dozen integration jobs. +// matrix of the integration tests (tests/ci/matrix.json, read by +// .github/workflows/ci.yml; also checked for being well-formed), so that +// raising a version without its checksum fails here, not in half a dozen +// integration jobs. // // Exits with status 1 on an error. @@ -41,24 +43,47 @@ } $used = array(); -preg_match_all('/\bjoomla:\s*(\d+\.\d+\.\d+)\b/', file_get_contents($root.'/.github/workflows/ci.yml'), $matches); -foreach (array_unique($matches[1]) as $version) +$matrix = json_decode(file_get_contents($root.'/tests/ci/matrix.json'), true); +if (!is_array($matrix) || !is_array($matrix['include'] ?? null) || count($matrix['include']) === 0) { + echo "ERROR: tests/ci/matrix.json is not JSON with a non-empty \"include\" list\n"; + ++$errors; + $matrix = array('include' => array()); +} +foreach ($matrix['include'] as $number => $entry) +{ + $where = "tests/ci/matrix.json: entry ".($number + 1); + foreach (array('joomla', 'php', 'db') as $key) + { + if (!is_array($entry) || !isset($entry[$key]) || !is_string($entry[$key]) || $entry[$key] === '') + { + echo "ERROR: $where has no \"$key\" (string)\n"; + ++$errors; + continue 2; + } + } + $version = $entry['joomla']; + if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) + { + echo "ERROR: $where: \"$version\" is not a Joomla version like 5.4.8\n"; + ++$errors; + continue; + } $used[$version] = true; if (!isset($listed[$version])) { - echo "ERROR: Joomla $version is in the matrix of .github/workflows/ci.yml, but has no checksum in tests/ci/joomla-checksums.sha256\n"; + echo "ERROR: Joomla $version is in tests/ci/matrix.json, but has no checksum in tests/ci/joomla-checksums.sha256\n"; ++$errors; } } if (count($used) === 0) { - echo "ERROR: no Joomla version found in the matrix of .github/workflows/ci.yml\n"; + echo "ERROR: no Joomla version found in tests/ci/matrix.json\n"; ++$errors; } foreach (array_diff_key($listed, $used) as $version => $unused) { - echo "NOTE: Joomla $version has a checksum, but is not in the matrix of ci.yml\n"; + echo "NOTE: Joomla $version has a checksum, but is not in tests/ci/matrix.json\n"; } echo count($used)." version(s) of the matrix checked against ".count($listed)." checksum(s), $errors error(s)\n"; exit($errors > 0 ? 1 : 0);