Skip to content

Commit cdca3f7

Browse files
authored
Add pre-release scripting (#42)
1 parent 9b63946 commit cdca3f7

File tree

6 files changed

+1282
-48
lines changed

6 files changed

+1282
-48
lines changed

.husky/pre-commit

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
#!/bin/sh
2-
. .husky/pre-commit-phpcbf.sh
2+
. .husky/pre-commit-phpcbf.sh
3+
composer normalize
4+
npm run sort-package-json

bin/prepare-release.php

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
#!/usr/bin/env php
2+
<?php
3+
/**
4+
* Prepare a release of Secure Custom Fields
5+
*
6+
* @package wordpress/secure-custom-fields
7+
*/
8+
9+
// phpcs:disable WordPress.PHP.DiscouragedPHPFunctions
10+
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
11+
// phpcs:disable WordPress.WP.AlternativeFunctions
12+
13+
namespace WordPress\SCF\Scripts;
14+
15+
// Ensure we're in the right directory.
16+
chdir( dirname( __DIR__ ) );
17+
18+
// Check if required PHP functions are available.
19+
$required_functions = array( 'exec', 'passthru' );
20+
foreach ( $required_functions as $function ) {
21+
if ( ! function_exists( $function ) || in_array( $function, explode( ',', ini_get( 'disable_functions' ) ), true ) ) {
22+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
23+
echo "Error: {$function}() is required but not available\n";
24+
exit( 1 );
25+
}
26+
}
27+
28+
/**
29+
* Handles the release preparation process for Secure Custom Fields.
30+
*
31+
* This class manages version updates, changelog verification, and creating
32+
* pull requests for new releases.
33+
*/
34+
class Release_Preparation {
35+
/**
36+
* Main plugin file
37+
*
38+
* @var string
39+
*/
40+
const PLUGIN_FILE = 'secure-custom-fields.php';
41+
42+
/**
43+
* Run the release preparation
44+
*/
45+
public function run() {
46+
$this->check_requirements();
47+
$this->build_assets();
48+
$this->run_tests();
49+
$this->generate_docs();
50+
$this->commit_changes();
51+
52+
$current_version = $this->get_current_version();
53+
$latest_tag = $this->get_latest_tag();
54+
55+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
56+
echo "\nCurrent version: {$current_version}";
57+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
58+
echo "\nLatest tag: {$latest_tag}\n";
59+
60+
$new_version = $this->prompt_for_version();
61+
$this->validate_version( $new_version );
62+
63+
$changelog = $this->get_changelog( $new_version );
64+
if ( $changelog ) {
65+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
66+
echo "\nChangelog found for {$new_version}:\n\n{$changelog}\n";
67+
if ( ! $this->confirm( 'Is this changelog correct?' ) ) {
68+
exit( 1 );
69+
}
70+
} elseif ( ! $this->confirm( "No changelog found for {$new_version}. Is this expected?" ) ) {
71+
exit( 1 );
72+
}
73+
74+
$this->update_version( $new_version );
75+
$this->commit_version( $new_version );
76+
77+
$current_stable = $this->get_stable_tag();
78+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
79+
echo "\nCurrent stable tag: {$current_stable}\n";
80+
if ( $this->confirm( "Update stable tag to {$new_version}?" ) ) {
81+
$this->update_stable_tag( $new_version );
82+
$this->commit_stable_tag( $new_version );
83+
}
84+
85+
if ( $this->confirm( 'Create PR for this release?' ) ) {
86+
$this->create_pr( $new_version, $changelog );
87+
}
88+
89+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
90+
echo "\nRelease preparation complete!\n";
91+
}
92+
93+
/**
94+
* Check if required tools are available
95+
*/
96+
private function check_requirements() {
97+
$required = array( 'npm', 'composer', 'gh' );
98+
foreach ( $required as $tool ) {
99+
exec( "which {$tool}", $output, $return );
100+
if ( 0 !== $return ) {
101+
echo "Error: {$tool} is required but not found\n";
102+
exit( 1 );
103+
}
104+
}
105+
}
106+
107+
/**
108+
* Build assets using npm
109+
*/
110+
private function build_assets() {
111+
echo "Installing npm dependencies...\n";
112+
passthru( 'npm install', $return );
113+
if ( 0 !== $return ) {
114+
exit( $return );
115+
}
116+
117+
echo "Building assets...\n";
118+
passthru( 'npm run build', $return );
119+
if ( 0 !== $return ) {
120+
exit( $return );
121+
}
122+
}
123+
124+
/**
125+
* Run tests
126+
*/
127+
private function run_tests() {
128+
echo "Running tests...\n";
129+
passthru( 'composer test', $return );
130+
if ( 0 !== $return ) {
131+
exit( $return );
132+
}
133+
}
134+
135+
/**
136+
* Generate documentation
137+
*/
138+
private function generate_docs() {
139+
echo "Generating documentation...\n";
140+
passthru( 'composer docs', $return );
141+
if ( 0 !== $return ) {
142+
exit( $return );
143+
}
144+
}
145+
146+
/**
147+
* Commit any changes from build/tests/docs
148+
*/
149+
private function commit_changes() {
150+
exec( 'git status --porcelain', $output );
151+
if ( ! empty( $output ) ) {
152+
echo "Committing changes...\n";
153+
passthru( 'git add .', $return );
154+
if ( 0 !== $return ) {
155+
exit( $return );
156+
}
157+
passthru( 'git commit -m "Build assets and documentation"', $return );
158+
if ( 0 !== $return ) {
159+
exit( $return );
160+
}
161+
}
162+
}
163+
164+
/**
165+
* Get current version from plugin file
166+
*/
167+
private function get_current_version() {
168+
$plugin_data = file_get_contents( self::PLUGIN_FILE );
169+
preg_match( '/\$version = \'([^\']+)\'/', $plugin_data, $matches );
170+
return $matches[1] ?? 'unknown';
171+
}
172+
173+
/**
174+
* Get latest git tag
175+
*/
176+
private function get_latest_tag() {
177+
exec( 'git describe --tags --abbrev=0', $output, $return );
178+
return 0 === $return ? $output[0] : 'none';
179+
}
180+
181+
/**
182+
* Prompt for new version
183+
*/
184+
private function prompt_for_version() {
185+
echo 'Enter new version number: ';
186+
$handle = fopen( 'php://stdin', 'r' );
187+
$version = trim( fgets( $handle ) );
188+
fclose( $handle );
189+
return $version;
190+
}
191+
192+
/**
193+
* Validate version format
194+
*
195+
* @param string $version Version string to validate.
196+
*/
197+
private function validate_version( $version ) {
198+
if ( ! preg_match( '/^\d+\.\d+\.\d+(?:-beta\d+)?$/', $version ) ) {
199+
echo "Error: Invalid version format. Expected X.Y.Z or X.Y.Z-betaN\n";
200+
exit( 1 );
201+
}
202+
}
203+
204+
/**
205+
* Get changelog entry for version
206+
*
207+
* @param string $version Version to get changelog for.
208+
* @return string Changelog entry or empty string if not found.
209+
*/
210+
private function get_changelog( $version ) {
211+
$readme = file_get_contents( 'readme.txt' );
212+
if ( preg_match( "/= {$version} =\s*\n(.*?)(?=\n=|$)/s", $readme, $matches ) ) {
213+
return trim( $matches[1] );
214+
}
215+
return '';
216+
}
217+
218+
/**
219+
* Update version in plugin file
220+
*
221+
* @param string $version New version number.
222+
*/
223+
private function update_version( $version ) {
224+
$plugin_data = file_get_contents( self::PLUGIN_FILE );
225+
226+
// Update version in docblock using line-by-line replacement
227+
$lines = explode( "\n", $plugin_data );
228+
foreach ( $lines as &$line ) {
229+
if ( strpos( $line, '* Version:' ) !== false ) {
230+
$line = ' * Version: ' . $version;
231+
}
232+
}
233+
$plugin_data = implode( "\n", $lines );
234+
235+
// Update version property
236+
$plugin_data = preg_replace(
237+
'/public \$version = \'[^\']+\'/',
238+
'public $version = \'' . $version . '\'',
239+
$plugin_data
240+
);
241+
242+
file_put_contents( self::PLUGIN_FILE, $plugin_data );
243+
}
244+
245+
/**
246+
* Commit version update
247+
*
248+
* @param string $version Version being committed.
249+
*/
250+
private function commit_version( $version ) {
251+
passthru( 'git add ' . self::PLUGIN_FILE );
252+
passthru( "git commit -m 'Update version to {$version}'" );
253+
}
254+
255+
/**
256+
* Get current stable tag from readme.txt
257+
*/
258+
private function get_stable_tag() {
259+
$readme = file_get_contents( 'readme.txt' );
260+
if ( preg_match( '/Stable tag: ([^\s\n]+)/', $readme, $matches ) ) {
261+
return $matches[1];
262+
}
263+
return 'unknown';
264+
}
265+
266+
/**
267+
* Update stable tag in readme.txt
268+
*
269+
* @param string $version Version to set as stable.
270+
*/
271+
private function update_stable_tag( $version ) {
272+
$readme = file_get_contents( 'readme.txt' );
273+
$readme = preg_replace(
274+
'/(Stable tag: )[^\s\n]+/',
275+
'$1' . $version,
276+
$readme
277+
);
278+
file_put_contents( 'readme.txt', $readme );
279+
}
280+
281+
/**
282+
* Commit stable tag update
283+
*
284+
* @param string $version Version being set as stable.
285+
*/
286+
private function commit_stable_tag( $version ) {
287+
passthru( 'git add readme.txt' );
288+
passthru( "git commit -m 'Update stable tag to {$version}'" );
289+
}
290+
291+
/**
292+
* Create PR using gh cli
293+
*
294+
* @param string $version Version being released.
295+
* @param string $changelog Changelog content for the PR body.
296+
*/
297+
private function create_pr( $version, $changelog ) {
298+
$title = "Prepare {$version} Release";
299+
$body = $changelog ? $changelog : "Changelog entry pending for {$version}";
300+
301+
passthru( "gh pr create --title \"{$title}\" --body \"{$body}\"" );
302+
}
303+
304+
/**
305+
* Prompt for confirmation
306+
*
307+
* @param string $message Message to display.
308+
* @return bool True if confirmed, false otherwise.
309+
*/
310+
private function confirm( $message ) {
311+
echo "{$message} [y/N] ";
312+
$handle = fopen( 'php://stdin', 'r' );
313+
$line = strtolower( trim( fgets( $handle ) ) );
314+
fclose( $handle );
315+
return 'y' === $line;
316+
}
317+
}
318+
319+
// Run the script
320+
$preparation = new Release_Preparation();
321+
$preparation->run();

0 commit comments

Comments
 (0)