|
| 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