Skip to content

Commit ef94bbe

Browse files
committed
NEW Create action
1 parent ca71e53 commit ef94bbe

File tree

3 files changed

+380
-1
lines changed

3 files changed

+380
-1
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
# gha-generate-matrix
1+
# GitHub Actions - Generate matrix
2+
3+
Create a dynamic Silverstripe CI matrix
4+
5+
See [gha-ci](https://github.com/silverstripe/gha-ci)

action.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Generate Matrix
2+
description: GitHub Action to create a dynamic Silverstripe CI matrix
3+
inputs:
4+
# extra jobs must be multi-line string, as there's no support for type: array for inputs
5+
extra_jobs:
6+
type: string
7+
required: false
8+
default: ''
9+
# simple matrix will only run a single php 7.4 mysql 5.7 job instead of a full matrix
10+
simple_matrix:
11+
type: boolean
12+
default: false
13+
endtoend:
14+
type: boolean
15+
default: true
16+
phpcoverage:
17+
type: boolean
18+
# modules on silverstripe account will ignore this and always run codecov
19+
default: false
20+
phplinting:
21+
type: boolean
22+
default: true
23+
phpunit:
24+
type: boolean
25+
default: true
26+
js:
27+
type: boolean
28+
default: true
29+
# https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
30+
outputs:
31+
matrix:
32+
description: JSON matrix
33+
value: ${{ steps.php-script.outputs.matrix }}
34+
runs:
35+
using: composite
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 #v2
39+
- name: Install PHP
40+
uses: shivammathur/setup-php@aa1fe473f9c687b6fb896056d771232c0bc41161 #v2
41+
with:
42+
php-version: '7.4'
43+
extensions: yaml
44+
- name: Create __inputs.yml
45+
shell: bash
46+
# Add string inputs to memory instead of using string substituion in shell script
47+
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
48+
env:
49+
EXTRA_JOBS: ${{ inputs.extra_jobs }}
50+
GITHUB_REPOSITORY: ${{ github.repository }}
51+
# github.base_ref is the target branch on a pull-request
52+
# github.ref_name is the name of the branch on push, and the tag on tag
53+
GITHUB_MY_REF: ${{ github.base_ref && github.base_ref || github.ref_name }}
54+
run: |
55+
if [ -f __inputs.yml ]; then rm __inputs.yml; fi
56+
touch __inputs.yml
57+
echo "endtoend: ${{ inputs.endtoend }}" >> __inputs.yml
58+
echo "js: ${{ inputs.js }}" >> __inputs.yml
59+
echo "phpcoverage: ${{ inputs.phpcoverage }}" >> __inputs.yml
60+
echo "phplinting: ${{ inputs.phplinting }}" >> __inputs.yml
61+
echo "phpunit: ${{ inputs.phpunit }}" >> __inputs.yml
62+
echo "simple_matrix: ${{ inputs.simple_matrix }}" >> __inputs.yml
63+
echo "github_repository: $GITHUB_REPOSITORY" >> __inputs.yml
64+
echo "github_my_ref: $GITHUB_MY_REF" >> __inputs.yml
65+
if [[ "$EXTRA_JOBS" != "" ]]; then echo "extra_jobs:" >> __inputs.yml; fi
66+
if [[ "$EXTRA_JOBS" != "" ]]; then echo "$EXTRA_JOBS" >> __inputs.yml; fi
67+
echo "cat __inputs.yml"
68+
cat __inputs.yml
69+
- name: Run php script
70+
id: php-script
71+
shell: bash
72+
run: |
73+
MATRIX_JSON=$(php ${{ github.action_path }}/script.php)
74+
echo "MATRIX_JSON: $MATRIX_JSON"
75+
echo "::set-output name=matrix::${MATRIX_JSON}"

script.php

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
<?php
2+
3+
const DB_MYSQL_57 = 'mysql57';
4+
const DB_MYSQL_57_PDO = 'mysql57pdo';
5+
const DB_MYSQL_80 = 'mysql80';
6+
const DB_PGSQL = 'pgsql';
7+
8+
# Manually update this after each minor CMS release
9+
$installerToPhpVersions = [
10+
'4.9' => [
11+
'7.1',
12+
'7.2',
13+
'7.3',
14+
'7.4'
15+
],
16+
'4.10' => [
17+
'7.3',
18+
'7.4',
19+
'8.0',
20+
],
21+
'4.11' => [
22+
'7.4',
23+
'8.0',
24+
'8.1',
25+
],
26+
'4' => [
27+
'7.4',
28+
'8.0',
29+
'8.1',
30+
],
31+
];
32+
33+
function isLockedStepped($repo)
34+
{
35+
return in_array($repo, [
36+
'silverstripe-admin',
37+
'silverstripe-asset-admin',
38+
'silverstripe-assets',
39+
'silverstripe-campaign-admin',
40+
'silverstripe-cms',
41+
'silverstripe-errorpage',
42+
'silverstripe-framework',
43+
'silverstripe-reports',
44+
'silverstripe-siteconfig',
45+
'silverstripe-versioned',
46+
'silverstripe-versioned-admin',
47+
// recipe-solr-search is not a true recipe, doesn't include recipe-cms/core
48+
'recipe-solr-search',
49+
]);
50+
}
51+
52+
function shouldNotRequireInstaller($repo)
53+
{
54+
// these include recipe-cms/core, so we don't want to composer require installer
55+
// in .travis.yml they use the 'self' provision rather than 'standard'
56+
return in_array($repo, [
57+
'recipe-authoring-tools',
58+
'recipe-blog',
59+
'recipe-ccl',
60+
'recipe-cms',
61+
'recipe-collaboration',
62+
'recipe-content-blocks',
63+
'recipe-core',
64+
'recipe-form-building',
65+
'recipe-kitchen-sink',
66+
'recipe-reporting-tools',
67+
'recipe-services',
68+
'silverstripe-installer',
69+
// vendor-plugin is not a recipe, though we also do not want installer
70+
'vendor-plugin'
71+
]);
72+
}
73+
74+
function getInstallerVersion($githubRepository, $branch)
75+
{
76+
global $installerToPhpVersions;
77+
$repo = explode('/', $githubRepository)[1];
78+
if (shouldNotRequireInstaller($repo)) {
79+
return '';
80+
}
81+
$v = explode('.', $branch);
82+
if (count($v) == 1) {
83+
return '4.x-dev';
84+
}
85+
if (isLockedStepped($repo)) {
86+
return '4' . $v[1] . 'x-dev';
87+
} else {
88+
// use the latest minor version of installer
89+
$a = array_keys($installerToPhpVersions);
90+
$a = array_map(fn($k) => (int) $k, $a);
91+
sort($a);
92+
return $a[count($a) - 1] . 'x-dev';
93+
}
94+
}
95+
96+
function createJob($phpNum, $opts)
97+
{
98+
global $installerToPhpVersions, $installerVersion;
99+
$v = str_replace('.x-dev', '', $installerVersion);
100+
$v = $v ?: '4';
101+
$phpVersions = $installerToPhpVersions[$v];
102+
$default = [
103+
# ensure there's a default value for all possible return keys
104+
# this allows use to use `if [ "${{ matrix.key }}" == "true" ]; then` in github-actions-ci-cd/ci.yml
105+
'installer_version' => $installerVersion,
106+
'php' => $phpVersions[$phpNum] ?? $phpVersions[count($phpVersions) - 1],
107+
'db' => DB_MYSQL_57,
108+
'composer_require_extra' => '',
109+
'composer_args' => '',
110+
'name_suffix' => '',
111+
'phpunit' => false,
112+
'phpunit_suite' => 'all',
113+
'phplinting' => false,
114+
'phpcoverage' => false,
115+
'endtoend' => false,
116+
'endtoend_suite' => 'root',
117+
'endtoend_config' => '',
118+
'js' => false,
119+
];
120+
return array_merge($default, $opts);
121+
}
122+
123+
function parseBoolValue($value)
124+
{
125+
return ($value === true || $value === 'true');
126+
}
127+
128+
// Reads inputs.yml and creates a new json matrix
129+
$inputs = yaml_parse(file_get_contents('__inputs.yml'));
130+
131+
// $myRef will either be a branch for push (i.e cron) and pull-request (target branch), or a semver tag
132+
$myRef = $inputs['github_my_ref'];
133+
$isTag = preg_match('#^[0-9]+\.[0-9]+\.[0-9]+$#', $myRef, $m);
134+
$branch = $isTag ? sprintf('%d.%d', $m[1], $m[2]) : $myRef;
135+
136+
$githubRepository = $inputs['github_repository'];
137+
$installerVersion = getInstallerVersion($githubRepository, $branch);
138+
139+
$run = [];
140+
$extraJobs = [];
141+
$simpleMatrix = false;
142+
foreach ($inputs as $input => $value) {
143+
if (in_array($input, ['endtoend', 'js', 'phpunit', 'phpcoverage', 'phplinting'])) {
144+
$run[$input] = parseBoolValue($value);
145+
} else if ($input === 'extra_jobs') {
146+
if ($value === 'none') {
147+
$value = [];
148+
}
149+
$extraJobs = $value;
150+
foreach ($extraJobs as $job) {
151+
$job = createJob(3, $job);
152+
}
153+
} else if ($input === 'simple_matrix') {
154+
$simpleMatrix = parseBoolValue($value);
155+
}
156+
}
157+
$matrix = ['include' => []];
158+
if ((file_exists('phpunit.xml') || file_exists('phpunit.xml.dist')) && $run['phpunit']) {
159+
$d = new DOMDocument();
160+
$d->preserveWhiteSpace = false;
161+
$fn = file_exists('phpunit.xml') ? 'phpunit.xml' : 'phpunit.xml.dist';
162+
$d->load($fn);
163+
$x = new DOMXPath($d);
164+
$tss = $x->query('//testsuite');
165+
// phpunit.xml has defined testsuites
166+
foreach ($tss as $ts) {
167+
if (!$ts->hasAttribute('name') || $ts->getAttribute('name') == 'Default') {
168+
continue;
169+
}
170+
if ($simpleMatrix) {
171+
$matrix['include'][] = createJob(0, [
172+
'phpunit' => true,
173+
'phpunit_suite' => $ts->getAttribute('name'),
174+
]);
175+
} else {
176+
$matrix['include'][] = createJob(0, [
177+
'composer_args' => '--prefer-lowest',
178+
'phpunit' => true,
179+
'phpunit_suite' => $ts->getAttribute('name'),
180+
]);
181+
$matrix['include'][] = createJob(1, [
182+
'db' => 'pgsql',
183+
'phpunit' => true,
184+
'phpunit_suite' => $ts->getAttribute('name')
185+
]);
186+
$matrix['include'][] = createJob(3, [
187+
'db' => DB_MYSQL_80,
188+
'phpunit' => true,
189+
'phpunit_suite' => $ts->getAttribute('name')
190+
]);
191+
}
192+
}
193+
// phpunit.xml has no defined testsuites
194+
if (count($matrix['include']) == 0) {
195+
if ($simpleMatrix) {
196+
$matrix['include'][] = createJob(0, [
197+
'phpunit' => true,
198+
'phpunit_suite' => 'all'
199+
]);
200+
} else {
201+
$matrix['include'][] = createJob(0, [
202+
'composer_args' => '--prefer-lowest',
203+
'phpunit' => true,
204+
'phpunit_suite' => 'all'
205+
]);
206+
$matrix['include'][] = createJob(1, [
207+
'db' => DB_PGSQL,
208+
'phpunit' => true,
209+
'phpunit_suite' => 'all'
210+
]);
211+
$matrix['include'][] = createJob(3, [
212+
'db' => DB_MYSQL_80,
213+
'phpunit' => true,
214+
'phpunit_suite' => 'all'
215+
]);
216+
}
217+
}
218+
}
219+
// skip phpcs on silverstripe-installer which include sample file for use in projects
220+
if ((file_exists('phpcs.xml') || file_exists('phpcs.xml.dist')) && !preg_match('#/silverstripe-installer$#', $githubRepository)) {
221+
$matrix['include'][] = createJob(0, [
222+
'phplinting' => true
223+
]);
224+
}
225+
// phpcoverage also runs unit tests
226+
// always run on silverstripe account
227+
if ($run['phpcoverage'] || preg_match('#^silverstripe/#', $githubRepository)) {
228+
if ($simpleMatrix) {
229+
$matrix['include'][] = createJob(0, [
230+
'phpcoverage' => true
231+
]);
232+
} else {
233+
$matrix['include'][] = createJob(2, [
234+
'db' => DB_MYSQL_57_PDO,
235+
'phpcoverage' => true
236+
]);
237+
}
238+
}
239+
// endtoend / behat
240+
if ($run['endtoend'] && file_exists('behat.yml')) {
241+
$matrix['include'][] = createJob(0, [
242+
'endtoend' => true,
243+
'endtoend_suite' => 'root'
244+
]);
245+
if (!$simpleMatrix) {
246+
$matrix['include'][] = createJob(3, [
247+
'db' => DB_MYSQL_80,
248+
'endtoend' => true,
249+
'endtoend_suite' => 'root'
250+
]);
251+
}
252+
}
253+
// javascript tests
254+
if (file_exists('package.json') && $run['js']) {
255+
$matrix['include'][] = createJob(0, [
256+
'js' => true
257+
]);
258+
}
259+
// extra jobs
260+
foreach ($extraJobs as $arr) {
261+
$matrix['include'][] = createJob(0, $arr);
262+
}
263+
264+
// convert everything to strings and sanatise values
265+
foreach ($matrix['include'] as $i => $job) {
266+
foreach ($job as $key => $val) {
267+
if ($val === true) {
268+
$val = 'true';
269+
}
270+
if ($val === false) {
271+
$val = 'false';
272+
}
273+
// all values must be strings
274+
$val = (string) $val;
275+
// remove any dodgy characters
276+
$val = str_replace(["\r", "\n", "\t", "'", '"', '&', '|'], '', $val);
277+
// ascii chars only - https://www.regular-expressions.info/posixbrackets.html
278+
$val = preg_replace('#[^\x20-\x7E]#', '', $val);
279+
// limit name_suffix length and be strict as it is used in the artifact file name
280+
if ($key === 'name_suffix') {
281+
if (strlen($val) > 20) {
282+
$val = preg_replace('#[^a-zA-Z0-9_\- ]#', '', $val);
283+
$val = substr($val, 0, 20);
284+
}
285+
}
286+
// be strict with composer_require_extra
287+
if ($key === 'composer_require_extra') {
288+
$val = preg_replace('#[^A-Za-z0-9\-\.\^\/~: ]#', '', $val);
289+
}
290+
// add value back to matrix
291+
$matrix['include'][$i][$key] = $val;
292+
}
293+
}
294+
295+
// output json, keep it on a single line so do not use pretty print
296+
$json = json_encode($matrix);
297+
$json = preg_replace("#\n +#", "\n", $json);
298+
$json = str_replace('\\/', '/', $json);
299+
$json = str_replace("\n", '', $json);
300+
echo trim($json);

0 commit comments

Comments
 (0)