Skip to content

Commit c2b5efb

Browse files
committed
GH Actions: auto-deploy website on push to stable
This commit: * Adds a new `update-website.yml` workflow which will automatically update the website whenever a commit is pushed to the `stable` branch. The script will also execute whenever changes are made to the workflow or the build scripts, but in that case, it will only do a _dry run_. * This workflow will call the new `.github/build/update-website.php` script to prepare files to be updated on the website. * ... which will in turn call the `.github/build/Website.php` class which does the actually preparing. Initially, the build script will: * Create a `deploy` directory to store files to be moved to the website. * Copy the `README.md` file to `index.md`. * Edit this file to: - Add frontmatter to indicate to Jekyll the file should be processed. - Remove the title and description as those would be duplicate. - Remove most of the badges. - For the remaining badges, turn them into HTML as Jekyll doesn't handle them correctly. The build script has been set up using modern PHP and expects PHP 7.2 or higher to run (using PHP 8.1 in the workflow). Includes: * Adding a new `lintlt72` script to the `composer.json` file which will exclude the website build scripts from linting. * Implementing use of the `lintlt72` script in the appropriate places in the `quicktest` and `test` workflows. * Minor tweak to the `README` to ensure a URL will be hyperlinked. Ref: * [Action runner which handles moving the prepared files to the gh-pages branch](https://github.com/crazy-max/ghaction-github-pages)
1 parent 5d00339 commit c2b5efb

File tree

8 files changed

+429
-8
lines changed

8 files changed

+429
-8
lines changed

.github/build/Website.php

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
<?php
2+
/**
3+
* PHPCSDevTools, tools for PHP_CodeSniffer sniff developers.
4+
*
5+
* @package PHPCSDevTools\GHPages
6+
* @copyright 2019 PHPCSDevTools Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSDevTools
9+
*/
10+
11+
namespace PHPCSDevTools\Build;
12+
13+
use RuntimeException;
14+
15+
/**
16+
* Prepare the website pages for deploy to GH Pages.
17+
*
18+
* {@internal This functionality has a minimum PHP requirement of PHP 7.2.}
19+
*
20+
* @internal
21+
*
22+
* @phpcs:disable PHPCompatibility.FunctionDeclarations.NewParamTypeDeclarations.stringFound
23+
* @phpcs:disable PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.intFound
24+
* @phpcs:disable PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.stringFound
25+
* @phpcs:disable PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.voidFound
26+
* @phpcs:disable PHPCompatibility.InitialValue.NewConstantArraysUsingConst.Found
27+
* @phpcs:disable PHPCompatibility.InitialValue.NewConstantScalarExpressions.constFound
28+
*/
29+
final class Website
30+
{
31+
32+
/**
33+
* Path to project root (without trailing slash).
34+
*
35+
* @var string
36+
*/
37+
const PROJECT_ROOT = __DIR__ . '/../..';
38+
39+
/**
40+
* Relative path to target directory off project root (without trailing slash).
41+
*
42+
* @var string
43+
*/
44+
const TARGET_DIR = '/deploy';
45+
46+
/**
47+
* Files to copy.
48+
*
49+
* Source should be the relative path from the project root.
50+
* Target should be the relative path in the target directory.
51+
* If target is left empty, the target will be the same as the source.
52+
*
53+
* @var array<string => string target>
54+
*/
55+
const FILES_TO_COPY = [
56+
'README.md' => 'index.md',
57+
];
58+
59+
/**
60+
* Frontmatter.
61+
*
62+
* @var string
63+
*/
64+
const FRONTMATTER = '---
65+
---
66+
';
67+
68+
/**
69+
* Resolved path to project root (with trailing slash).
70+
*
71+
* @var string
72+
*/
73+
private $realRoot;
74+
75+
/**
76+
* Resolved path to target directory (with trailing slash).
77+
*
78+
* @var string
79+
*/
80+
private $realTarget;
81+
82+
/**
83+
* Constructor
84+
*
85+
* @return void
86+
*/
87+
public function __construct()
88+
{
89+
// Check if the target directory exists and if not, create it.
90+
$targetDir = self::PROJECT_ROOT . self::TARGET_DIR;
91+
92+
if (@\is_dir($targetDir) === false) {
93+
if (@\mkdir($targetDir, 0777, true) === false) {
94+
throw new RuntimeException(\sprintf('Failed to create the %s directory.', $targetDir));
95+
}
96+
}
97+
98+
$realPath = \realpath($targetDir);
99+
if ($realPath === false) {
100+
throw new RuntimeException(\sprintf('Failed to find the %s directory.', $targetDir));
101+
}
102+
103+
$this->realRoot = \realpath(self::PROJECT_ROOT) . '/';
104+
$this->realTarget = $realPath . '/';
105+
}
106+
107+
/**
108+
* Run the transformation.
109+
*
110+
* @return int Exit code.
111+
*/
112+
public function run(): int
113+
{
114+
$exitcode = 0;
115+
116+
try {
117+
$this->copyFiles();
118+
$this->transformIndex();
119+
} catch (RuntimeException $e) {
120+
echo 'ERROR: ', $e->getMessage(), \PHP_EOL;
121+
$exitcode = 1;
122+
}
123+
124+
return $exitcode;
125+
}
126+
127+
/**
128+
* Copy files to the target directory.
129+
*
130+
* @return void
131+
*/
132+
private function copyFiles(): void
133+
{
134+
foreach (self::FILES_TO_COPY as $source => $target) {
135+
$source = $this->realRoot . $source;
136+
if (empty($target)) {
137+
$target = $this->realTarget . $source;
138+
} else {
139+
$target = $this->realTarget . $target;
140+
}
141+
142+
// Bit round-about way of copying the files, but we need to make sure the target dir exists.
143+
$contents = $this->getContents($source);
144+
$this->putContents($target, $contents);
145+
}
146+
}
147+
148+
/**
149+
* Transform the README to a usable homepage.
150+
*
151+
* - Remove the title and subtitle as those would become duplicate.
152+
* - Remove most of the badges, except for the first three.
153+
* - Transform those badges into HTML.
154+
* - Add frontmatter.
155+
*
156+
* @return void
157+
*
158+
* @throws \RuntimeException When any of the regexes do not yield any results.
159+
*/
160+
private function transformIndex(): void
161+
{
162+
// Read the file.
163+
$target = $this->realTarget . '/index.md';
164+
$contents = $this->getContents($target);
165+
166+
// Grab the start of the document.
167+
$matched = \preg_match('`^(.+)\* \[Installation\]`s', $contents, $matches);
168+
if ($matched !== 1) {
169+
throw new RuntimeException('Failed to match start of document. Adjust the regex');
170+
}
171+
172+
$startOfDoc = $matches[1];
173+
174+
// Grab the first few badges from the start of the document.
175+
$matched = \preg_match(
176+
'`((?:\[!\[[^\]]+\]\([^\)]+\)\]\([^\)]+\)[\n\r]+)+):construction:`',
177+
$startOfDoc,
178+
$matches
179+
);
180+
if ($matched !== 1) {
181+
throw new RuntimeException('Failed to match badges. Adjust the regex');
182+
}
183+
184+
$badges = \explode("\n", $matches[1]);
185+
$badges = \array_filter($badges);
186+
$badges = \array_map([$this, 'mdBadgeToHtml'], $badges);
187+
$badges = \implode("\n ", $badges);
188+
189+
$replacement = \sprintf(
190+
'%s
191+
192+
<div id="badges" aria-hidden="true">
193+
194+
%s
195+
196+
</div>
197+
198+
',
199+
self::FRONTMATTER,
200+
' ' . $badges
201+
);
202+
203+
$contents = \str_replace($startOfDoc, $replacement, $contents);
204+
205+
$this->putContents($target, $contents);
206+
}
207+
208+
/**
209+
* Transform markdown badges into HTML badges.
210+
*
211+
* Jekyll runs into trouble doing this when we also want to keep the wrapper div with aria-hidden="true".
212+
*
213+
* @param string $mdBadge Markdown badge code.
214+
*
215+
* @return string
216+
*/
217+
private function mdBadgeToHtml(string $mdBadge): string
218+
{
219+
$mdBadge = trim($mdBadge);
220+
221+
$matched = \preg_match(
222+
'`^\[!\[(?<alt>[^\]]+)\]\((?<imgurl>[^\)]+)\)\]\((?<href>[^\)]+)\)$`',
223+
$mdBadge,
224+
$matches
225+
);
226+
if ($matched !== 1) {
227+
throw new RuntimeException(\sprintf('Failed to parse the badge. Adjust the regex. Received: %s', $mdBadge));
228+
}
229+
230+
return \sprintf(
231+
'<a href="%s"><img src="%s" alt="%s" class="badge"></a>',
232+
$matches['href'],
233+
$matches['imgurl'],
234+
$matches['alt']
235+
);
236+
}
237+
238+
/**
239+
* Retrieve the contents of a file.
240+
*
241+
* @param string $source Path to the source file.
242+
*
243+
* @return string
244+
*
245+
* @throws \RuntimeException When the contents of the file could not be retrieved.
246+
*/
247+
private function getContents(string $source): string
248+
{
249+
$contents = \file_get_contents($source);
250+
if (!$contents) {
251+
throw new RuntimeException(\sprintf('Failed to read doc file: %s', $source));
252+
}
253+
254+
return $contents;
255+
}
256+
257+
/**
258+
* Write a string to a file.
259+
*
260+
* @param string $target Path to the target file.
261+
* @param string $contents File contents to write.
262+
*
263+
* @return void
264+
*
265+
* @throws \RuntimeException When the target directory could not be created.
266+
* @throws \RuntimeException When the file could not be written to the target directory.
267+
*/
268+
private function putContents(string $target, string $contents): void
269+
{
270+
// Check if the target directory exists and if not, create it.
271+
$targetDir = \dirname($target);
272+
273+
if (@\is_dir($targetDir) === false) {
274+
if (@\mkdir($targetDir, 0777, true) === false) {
275+
throw new RuntimeException(\sprintf('Failed to create the %s directory.', $targetDir));
276+
}
277+
}
278+
279+
// Make sure the file always ends on a new line.
280+
$contents = \rtrim($contents) . "\n";
281+
if (\file_put_contents($target, $contents) === false) {
282+
throw new RuntimeException(\sprintf('Failed to write to target location: %s', $target));
283+
}
284+
}
285+
}

.github/build/update-website.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env php
2+
<?php
3+
/**
4+
* PHPCSDevTools, tools for PHP_CodeSniffer sniff developers.
5+
*
6+
* Website deploy preparation script.
7+
*
8+
* Grabs files which will be used in the website, adjusts if needed and places them in a target directory.
9+
*
10+
* {@internal This functionality has a minimum PHP requirement of PHP 7.2.}
11+
*
12+
* @internal
13+
*
14+
* @package PHPCSDevTools\GHPages
15+
* @copyright 2019 PHPCSDevTools Contributors
16+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
17+
* @link https://github.com/PHPCSStandards/PHPCSDevTools
18+
*/
19+
20+
namespace PHPCSDevTools\Build;
21+
22+
require_once __DIR__ . '/Website.php';
23+
24+
$websiteUpdater = new Website();
25+
$websiteUpdateSuccess = $websiteUpdater->run();
26+
27+
exit($websiteUpdateSuccess);

.github/workflows/quicktest.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,12 @@ jobs:
2525
matrix:
2626
php: ['5.4', 'latest']
2727
phpcs_version: ['dev-master']
28-
lint: [true]
2928

3029
include:
3130
- php: '7.2'
3231
phpcs_version: '3.1.0'
33-
lint: false
3432
- php: '5.4'
3533
phpcs_version: '3.1.0'
36-
lint: false
3734

3835
name: "QTest${{ matrix.lint && ' + Lint' || '' }}: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}"
3936

@@ -71,10 +68,14 @@ jobs:
7168
- name: Install Composer dependencies
7269
uses: "ramsey/composer-install@v2"
7370

74-
- name: Lint against parse errors
75-
if: ${{ matrix.lint }}
71+
- name: Lint against parse errors (PHP 7.2+)
72+
if: ${{ matrix.phpcs_version == 'dev-master' && matrix.php >= '7.2' }}
7673
run: composer lint
7774

75+
- name: Lint against parse errors (PHP < 7.2)
76+
if: ${{ matrix.phpcs_version == 'dev-master' && matrix.php < '7.2' }}
77+
run: composer lintlt72
78+
7879
# Check that any sniffs available are feature complete.
7980
# This also acts as an integration test for the feature completeness script,
8081
# which is why it is run against various PHP versions and not in the "Sniff" stage.

.github/workflows/test.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,14 @@ jobs:
120120
with:
121121
composer-options: --ignore-platform-reqs
122122

123-
- name: Lint against parse errors
124-
if: matrix.phpcs_version == 'dev-master'
123+
- name: Lint against parse errors (PHP 7.2+)
124+
if: ${{ matrix.phpcs_version == 'dev-master' && matrix.php >= '7.2' }}
125125
run: composer lint -- --checkstyle | cs2pr
126126

127+
- name: Lint against parse errors (PHP < 7.2)
128+
if: ${{ matrix.phpcs_version == 'dev-master' && matrix.php < '7.2' }}
129+
run: composer lintlt72 -- --checkstyle | cs2pr
130+
127131
# Check that any sniffs available are feature complete.
128132
# This also acts as an integration test for the feature completeness script,
129133
# which is why it is run against various PHP versions and not in the "Sniff" stage.

0 commit comments

Comments
 (0)