Skip to content

Commit

Permalink
feat: add dynamic test matrixes, concurrency checks and more options
Browse files Browse the repository at this point in the history
- Dynamic matrixes: workflows will check the version.php file which
  should always exist, and run checks based on the supported versions.
  It will apply a moodle_branches input check, or check the supported
  range, or fallback and check the plugin->requires range. This should
  cover most cases and other edge cases could be fixed by setting the
  appropriate plugin version settings (e.g. requires or supported)
- Concurrency check: This prevents multiple tests for the same
  combination from running in the workflow. This can happen when
  performing another push to the same PR, this will effectively cancel
  the previous run and should reduce the time taken to run the most
  current workflow.
- Added master branch to the list of branches to run workflows against
  by default (except when a support range is provided). This can be
  disabled explicitly by providing an input option disable_master
- Full matrix options is now managed by a single yaml file currently
  located at `.github/actions/matrix/matrix_includes.yml`
- Automating releases: to test if releases will trigger under the
  appropriate conditions, I've added an additional job to allow us to
  monitor if the release job will be triggered as expected. If so it
  would be safe then, to add the real release job to the workflow, and
  then we can remove the need for having 2 workflows (ci + release) and
  have them run inside a single workflow file.
  • Loading branch information
keevan committed Apr 22, 2022
1 parent 6b74b35 commit ad99c75
Show file tree
Hide file tree
Showing 49 changed files with 5,992 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .github/actions/matrix/matrix_includes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
include:
# Versions 3.3 - 3.4 are not LTS, so only test a single combo for each.
- {moodle-branch: 'MOODLE_33_STABLE', php: '7.1', node: '14.15', database: 'mariadb'}
- {moodle-branch: 'MOODLE_34_STABLE', php: '7.1', node: '14.15', database: 'pgsql'}
# Test all combinations for version 3.5 (as it is LTS).
- {moodle-branch: 'MOODLE_35_STABLE', php: '7.1', node: '14.15', database: 'mariadb'}
- {moodle-branch: 'MOODLE_35_STABLE', php: '7.1', node: '14.15', database: 'pgsql'}
- {moodle-branch: 'MOODLE_35_STABLE', php: '7.2', node: '14.15', database: 'mariadb'}
- {moodle-branch: 'MOODLE_35_STABLE', php: '7.2', node: '14.15', database: 'pgsql'}
# Versions 3.6 - 3.8 are not LTS, so only test a single combo for each.
- {moodle-branch: 'MOODLE_36_STABLE', php: '7.1', node: '14.15', database: 'mariadb'}
- {moodle-branch: 'MOODLE_37_STABLE', php: '7.2', node: '14.15', database: 'pgsql'}
- {moodle-branch: 'MOODLE_38_STABLE', php: '7.3', node: '14.15', database: 'mariadb'}
# Also test all variants of 3.9 (LTS).
- {moodle-branch: 'MOODLE_39_STABLE', php: '7.2', node: '14.15', database: 'mariadb'}
- {moodle-branch: 'MOODLE_39_STABLE', php: '7.2', node: '14.15', database: 'pgsql'}
- {moodle-branch: 'MOODLE_39_STABLE', php: '7.3', node: '14.15', database: 'mariadb'}
- {moodle-branch: 'MOODLE_39_STABLE', php: '7.3', node: '14.15', database: 'pgsql'}
# 3.10 - 4.00 are not LTS, so only test a single combo for each.
- {moodle-branch: 'MOODLE_310_STABLE', php: '7.2', node: '14.15', database: 'pgsql'}
- {moodle-branch: 'MOODLE_311_STABLE', php: '7.3', node: '14.15', database: 'mariadb'}
- {moodle-branch: 'MOODLE_400_STABLE', php: '7.3', node: '14.15', database: 'pgsql'}
# Always include master (issue #18) - disable-able by config by setting 'disable_master' to true
- {moodle-branch: 'master', php: '7.3', node: '14.15', database: 'pgsql'}
130 changes: 130 additions & 0 deletions .github/actions/parse-version/script.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

require __DIR__ . '../../../../vendor/autoload.php';

use Symfony\Component\Yaml\Yaml;

/**
* Outputs a value and binds it to a name provided.
*
* Note that arrays/objects should be JSON encoded, and read via the fromJson
* method as described here:
* https://docs.github.com/en/actions/learn-github-actions/expressions#fromjson
*
* @param string $name name of the output
* @param string $value value of the output - preferably JSON encoded if its an object/array
* @author Kevin Pham <[email protected]>
* @copyright Catalyst IT, 2022
*/
function output(string $name, string $value) {
echo PHP_EOL;
echo "::set-output name=$name::$value";
echo PHP_EOL;
echo "Setting output.. $name = $value";
echo PHP_EOL;
}

// Greet the user.
echo "Hello from PHP!";
$workspace = $_SERVER['GITHUB_WORKSPACE'] ?? '';

// This check is only possible with the version.php file, so end early if it doesn't exist.
$versionFilePath = "$workspace/plugin/version.php";
if (!file_exists($versionFilePath)) {
echo "version.php does not exist";
exit(1);
}

// Some moodle constants to prevent errors when trying to require the version.php file.
define('MOODLE_INTERNAL', 1);
/** Software maturity level - internals can be tested using white box techniques. */
define('MATURITY_ALPHA', 50);
/** Software maturity level - feature complete, ready for preview and testing. */
define('MATURITY_BETA', 100);
/** Software maturity level - tested, will be released unless there are fatal bugs. */
define('MATURITY_RC', 150);
/** Software maturity level - ready for production deployment. */
define('MATURITY_STABLE', 200);
/** Any version - special value that can be used in $plugin->dependencies in version.php files. */
define('ANY_VERSION', 'any');

$plugin = new \stdClass();

require_once($versionFilePath);


// All supported matrix includes:
$matrixYaml = file_get_contents("$workspace/ci/.github/actions/matrix/matrix_includes.yml");
$matrix = Yaml::parse($matrixYaml);

// Version breakpoints are sourced from:
// https://download.moodle.org/api/1.3/updates.php?format=json&version=0.0&branch=$lowestSupportedBranch
$updates = json_decode(file_get_contents('https://download.moodle.org/api/1.3/updates.php?format=json&version=0.0&branch=3.3'), true);
$updates = $updates['updates']['core'] ?? [];

$preparedMatrix = array_filter($matrix['include'], function($entry) use($plugin, $updates) {
// Regex and replacement templates - Partially generated from https://regex101.com/
$re = '/MOODLE_(.*)_STABLE/m';
$subst = '$1';
$coreVersion = preg_replace($re, $subst, $entry['moodle-branch']);
$disable_master = !empty($_SERVER['INPUT_DISABLE_MASTER']);

// If a fixed support range is set, use this.
if (!empty($plugin->supported)) {
[$lower, $upper] = $plugin->supported;
if ($lower <= $coreVersion && $coreVersion <= $upper) {
return true;
}
// For non master branches, immediately return false as they should not be included in the run.
if ($entry['moodle-branch'] !== 'master') {
return false;
}
}

if (isset($_SERVER['INPUT_FILTER'])) {
$filter = $_SERVER['INPUT_FILTER'];
$filter = preg_split('/\s+/', $filter);
// Otherwise if not defined, it should check if the INPUT_FILTER variable has been provided, and use the matching versions there instead.
if (in_array($entry['moodle-branch'], $filter)) {
return true;
}
}

// If that hasn't been provided either, it should fallback and do a
// requirement check on the version itself. Noting that this is probably the
// worst option as it may need to go all the way down to version 3.3 to
// support Totara and is probably wrong, as it also assumes the range is
// open e.g. 35+
if (!empty($plugin->requires)) {
// Notes:
// - $plugin->requires = lowest version supported
// - Use $plugin->requires to ensure the version checked (matching based
// on regex) is higher than the lowest supported, if so, include it in
// the matrix.
foreach ($updates as $apiVersion) {
$branchValue = str_replace('.', '', $apiVersion['branch']);
$branch = "MOODLE_{$branchValue}_STABLE";
if ($entry['moodle-branch'] === $branch && $plugin->requires <= $apiVersion['version']) {
return true;
}
}
}

// Determine whether or not to include the master/dev branch
if ($entry['moodle-branch'] === 'master' && !$disable_master) {
return true;
}

return false;
});

$jsonMatrix = json_encode(['include' => array_values($preparedMatrix)], JSON_UNESCAPED_SLASHES);
output('matrix', $jsonMatrix);

// Output the component / plugin name (which would be useful e.g. for a release)
output('component', $plugin->component);

// Output the highest available moodle branch in this set, which will be used to
// determine whether or not various tests/tasks will run, such as grunt.
output('highest_moodle_branch', end($preparedMatrix)['moodle-branch'] ?? '');

11 changes: 11 additions & 0 deletions .github/actions/parse-version/script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash

# Get the current file/script path
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )

# Output debugging
echo "Hello from bash!"
php -v

# Run the PHP script
php $SCRIPT_DIR/script.php
174 changes: 174 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# .github/workflows/ci.yml
name: ci

on:
workflow_call:
inputs:
extra_php_extensions:
type: string
extra_plugin_runners:
type: string
disable_behat:
type: boolean
disable_phplint:
type: boolean
disable_phpunit:
type: boolean
disable_grunt:
type: boolean
disable_master:
description: 'If true, this will skip testing against moodle/master branch'
type: boolean
disable_release:
description: 'If true, this will skip the release job'
type: boolean
default: false
release_branches:
description: 'Required if the branch that should process releases is in a non-standard format (e.g. main or MOODLE_XX_STABLE)'
type: string
moodle_branches:
description: 'Specify the MOODLE_XX_STABLE branch you want to test against'
type: string

jobs:
prepare_matrix:
name: prepare test matrix
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
runs-on: 'ubuntu-latest'
outputs:
matrix: ${{ steps.parse-version.outputs.matrix }}
component: ${{ steps.parse-version.outputs.component }}
highest_moodle_branch: ${{ steps.parse-version.outputs.highest_moodle_branch }}
release_required: ${{ (
contains(github.event_name, 'push') &&
steps.check-version.outputs.any_changed == 'true' &&
steps.check-branch.outputs.publishable == 'true'
) }}
steps:
- name: Check if currently on a publishable branch
id: check-branch
run: |
publishable=${{ github.event.repository.fork == false &&
inputs.disable_release == false && (
(startsWith(github.ref, 'refs/heads/MOODLE_') &&
endsWith(github.ref, '_STABLE')) ||
github.ref == 'refs/heads/main' || (
inputs.release_branches != '' &&
endsWith(github.ref, inputs.release_branches)
)
) }}
echo '::set-output name=publishable::$publishable'
- name: Check out CI code
uses: actions/checkout@v2
with:
path: ci
repository: catalyst/catalyst-moodle-workflows
ref: v2
token: ${{ github.token }}
- name: Check out plugin code
uses: actions/checkout@v2
with:
# Needed for 'changed-files' actions (alternatively could be a fixed
# large number but may cause issues if limited).
fetch-depth: ${{ (contains(github.event_name, 'push') && steps.check-branch.outputs.publishable == 'true') && 0 || 1 }}
path: plugin
- name: Check if release is required (version.php changes)
if: contains(github.event_name, 'push') && steps.check-branch.outputs.publishable == 'true'
uses: tj-actions/[email protected]
id: check-version
with:
path: plugin
files: version.php
since_last_remote_commit: "true"
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
coverage: none
# extensions: yaml # FYI: makes the workflow very slow
- name: Determine test requirements and plugin info
id: parse-version
run: |
chmod +x "${GITHUB_WORKSPACE}/ci/.github/actions/parse-version/script.sh"
"${GITHUB_WORKSPACE}/ci/.github/actions/parse-version/script.sh"
env:
disable_master: ${{ inputs.disable_master }}
filter: ${{ inputs.moodle_branches }}

# fake_test:
# needs: prepare_matrix
# runs-on: 'ubuntu-latest'
# steps:
# - run: |
# echo "testing..."
# exit 0

setup:
name: ${{ matrix.moodle-branch }} - ${{ matrix.database }} - php ${{ matrix.php }} - ${{ needs.prepare_matrix.outputs.component }}
needs: prepare_matrix
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare_matrix.outputs.matrix) }}
env:
IGNORE_PATHS: tests/fixtures
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.moodle-branch }}-${{ matrix.database }}-${{ matrix.php }}-${{ needs.prepare_matrix.outputs.component }}
cancel-in-progress: true
runs-on: 'ubuntu-latest'
services:
postgres:
image: postgres:10
env:
POSTGRES_USER: 'postgres'
POSTGRES_HOST_AUTH_METHOD: 'trust'
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 3
ports:
- 5432:5432
mariadb:
image: mariadb:10.5
env:
MYSQL_USER: 'root'
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 3
steps:
- name: Run plugin setup
uses: catalyst/catalyst-moodle-workflows/.github/plugin/setup@main
with:
extra_php_extensions: ${{ inputs.extra_php_extensions }}
extra_plugin_runners: ${{ inputs.extra_plugin_runners }}
disable_behat: ${{ inputs.disable_behat }}
disable_phplint: ${{ inputs.disable_phplint }}
disable_phpunit: ${{ inputs.disable_phpunit }}
disable_grunt: ${{ inputs.disable_grunt }}
highest_moodle_branch: ${{ needs.prepare_matrix.outputs.highest_moodle_branch }}

fake_release:
name: release (check test only)
needs: setup
# needs: fake_test
# If it matches a standard branch naming convention, it should permit a
# release to happen, otherwise this step should be skipped.
# Patterns allowed:
# - MOODLE_XX_STABLE
# - MOODLE_XXX_STABLE
# - main
if: needs.prepare_matrix.outputs.release_required == 'true'
runs-on: 'ubuntu-latest'
steps:
- run: |
echo "Only testing if releasing would occur. This step will always succeed."
exit 0
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"require": {
"symfony/yaml": "^5.4"
}
}
Loading

0 comments on commit ad99c75

Please sign in to comment.