diff --git a/.maintenance/src/GitHub.php b/.maintenance/src/GitHub.php index ca43a78..eefc7c7 100644 --- a/.maintenance/src/GitHub.php +++ b/.maintenance/src/GitHub.php @@ -9,10 +9,18 @@ class GitHub { const API_ROOT = 'https://api.github.com/'; /** - * Minimum number of seconds between two write requests, as GitHub asks - * of integrations to stay clear of its secondary rate limits. + * Default minimum number of seconds between two write requests, as GitHub + * asks of integrations to stay clear of its secondary rate limits. */ - const WRITE_INTERVAL = 1; + const DEFAULT_WRITE_INTERVAL = 1.0; + + /** + * Minimum number of seconds between two write requests, or null until it + * is resolved from the WP_CLI_GITHUB_WRITE_INTERVAL environment variable. + * + * @var float|null + */ + private static $write_interval; /** * Credentials in use, resolved on first use. @@ -589,10 +597,38 @@ private static function get_error_hint( $code, $source ) { } /** - * Waits until enough time has passed since the last write request. + * Gets the minimum number of seconds between two write requests. + * + * Set the WP_CLI_GITHUB_WRITE_INTERVAL environment variable to space the + * writes out further when GitHub still applies secondary rate limits. + * + * @return float + */ + public static function get_write_interval() { + if ( null === self::$write_interval ) { + $interval = getenv( 'WP_CLI_GITHUB_WRITE_INTERVAL' ); + self::$write_interval = is_numeric( $interval ) && $interval >= 0 ? (float) $interval : self::DEFAULT_WRITE_INTERVAL; + } + + return self::$write_interval; + } + + /** + * Sets the minimum number of seconds between two write requests, such as + * 0 for tests against a fake API. + * + * @param float $seconds + */ + public static function set_write_interval( $seconds ) { + self::$write_interval = max( 0.0, (float) $seconds ); + } + + /** + * Waits until the write interval has passed since the last write request. + * When it already has, the write goes out right away. */ private static function pace_writes() { - $wait = self::$last_write + self::WRITE_INTERVAL - microtime( true ); + $wait = self::$last_write + self::get_write_interval() - microtime( true ); if ( $wait > 0 ) { usleep( (int) ( $wait * 1000000 ) ); diff --git a/.maintenance/src/Release_Command.php b/.maintenance/src/Release_Command.php index 3c11b00..e263d71 100644 --- a/.maintenance/src/Release_Command.php +++ b/.maintenance/src/Release_Command.php @@ -48,6 +48,23 @@ public function close_released( $args, $assoc_args ) { /** * Generate a new release out of an open milestone * + * Tags the default branch with the version of the milestone, publishes a + * release whose notes list the pull requests merged for that milestone, + * and closes the milestone. + * + * A milestone is skipped, with a warning, when it still has open issues or + * pull requests, has no merged pull requests, is not a version newer than + * the latest tag, or was already tagged. When a repository has several + * milestones that could be released, it is skipped unless --milestone says + * which one, as they would all tag the same commit. + * + * It is also skipped unless `wp maintenance release plan` reports it as + * ready for that milestone: the tag ships everything merged into the + * default branch since the latest tag, so all of it has to belong to the + * milestone, and all of the milestone's pull requests have to be in it. + * + * Run `wp maintenance release plan` first to see what is ready. + * * ## OPTIONS * * [...] @@ -60,52 +77,91 @@ public function close_released( $args, $assoc_args ) { * [--all] * : Generate releases for all repositories in the wp-cli organization. * + * [--milestone=] + * : Only release the milestone with this title. Needs a single repository. + * + * [--notes-file=] + * : Use the contents of this file as release notes, instead of the + * generated list of pull requests. Useful to publish notes that were + * reviewed and tidied up first. Needs --milestone. + * + * [--yes] + * : Publish without asking for confirmation. + * + * ## EXAMPLES + * + * # Review the generated notes, then publish them after fixing a typo. + * $ wp maintenance release-notes cache-command 3.0.1 --source=pull-request > notes.md + * $ wp maintenance release generate cache-command --milestone=3.0.1 --notes-file=notes.md --yes + * * @when before_wp_load */ public function generate( $args, $assoc_args ) { + $only_milestone = Utils\get_flag_value( $assoc_args, 'milestone' ); + $notes_file = Utils\get_flag_value( $assoc_args, 'notes-file' ); + $confirmed = Utils\get_flag_value( $assoc_args, 'yes', false ); + $repos = $this->get_repos( $args, $assoc_args ); - foreach ( $this->get_repos( $args, $assoc_args ) as $repo ) { + if ( null !== $only_milestone && 1 !== count( $repos ) ) { + WP_CLI::error( '--milestone needs exactly one repository.' ); + } + + if ( null !== $notes_file ) { + if ( null === $only_milestone ) { + WP_CLI::error( '--notes-file needs --milestone.' ); + } + + $custom_notes = is_readable( $notes_file ) ? trim( (string) file_get_contents( $notes_file ) ) : ''; + if ( '' === $custom_notes ) { + WP_CLI::error( "Could not read release notes from '{$notes_file}'." ); + } + } + + foreach ( $repos as $repo ) { WP_CLI::log( "--- {$repo} ---" ); - $releases = GitHub::get_project_releases( $repo ); - $milestones = GitHub::get_project_milestones( $repo ); + $candidates = $this->get_releasable_milestones( $repo, $only_milestone ); - foreach ( $milestones as $milestone ) { - WP_CLI::log( "Checking milestone '{$milestone->title}'..." ); - foreach ( $releases as $release ) { - if ( $release->tag_name === $milestone->title || "v{$milestone->title}" === $release->tag_name ) { - WP_CLI::log( "Found matching release '{$release->tag_name}', skipping milestone '{$milestone->title}'..." ); - continue 2; - } - } - WP_CLI::log( "Milestone '{$milestone->title}' does not have a matching release, generating one..." ); + if ( count( $candidates ) > 1 ) { + WP_CLI::warning( + "Skipping {$repo} as several milestones could be released: " + . implode( ', ', array_keys( $candidates ) ) . '. Pass --milestone to pick one.' + ); + continue; + } - if ( $this->has_open_items_on_milestone( $repo, $milestone->number ) ) { - WP_CLI::warning( "Skipping milestone '{$milestone->title}' as it has open issues/PRs assigned to it." ); - continue; - } + if ( $candidates && ! $this->agrees_with_plan( $repo, $candidates ) ) { + continue; + } + + foreach ( $candidates as $candidate ) { + list( $milestone, $notes ) = $candidate; $title = "Version {$milestone->title}"; - $tag = "v{$milestone->title}"; - $release_notes = Release_Notes::render( - [ Release_Notes::get( $repo, [ $milestone ], 'pull-request' ) ], - 'markdown', - false - ); + $tag = 'v' . ltrim( $milestone->title, 'v' ); + $release_notes = isset( $custom_notes ) + ? $custom_notes . "\n" + : Release_Notes::render( [ $notes ], 'markdown', false ); WP_CLI::log( 'Generating the following release:' ); WP_CLI::log( '-----' ); WP_CLI::log( "{$title} ({$tag})\n{$release_notes}" ); WP_CLI::log( '-----' ); - fwrite( STDOUT, 'Is the above correct? [y/n] ' ); - $answer = fgets( STDIN ); - if ( false === $answer ) { - WP_CLI::error( 'No answer received. This command needs an interactive terminal.' ); - } - if ( 'y' !== strtolower( trim( $answer ) ) ) { - continue; + if ( ! $confirmed ) { + fwrite( STDOUT, 'Is the above correct? [y/n] ' ); + $answer = fgets( STDIN ); + if ( false === $answer ) { + WP_CLI::error( 'No answer received. Run this command in an interactive terminal, or pass --yes.' ); + } + // A piped answer is not echoed, so end the prompt's line here. + if ( ! stream_isatty( STDIN ) ) { + fwrite( STDOUT, "\n" ); + } + if ( 'y' !== strtolower( trim( $answer ) ) ) { + continue; + } } $default_branch = GitHub::get_default_branch( $repo ); @@ -119,6 +175,129 @@ public function generate( $args, $assoc_args ) { } } + /** + * Checks a milestone about to be released against the release plan of its + * repository, which looks at what the default branch actually contains. + * + * The tag goes on the default branch, so it ships every change merged there + * since the latest tag. Those changes must all belong to this milestone, and + * every pull request in the release notes must be among them. + * + * @param string $repo Repository name, including the org. + * @param array $candidates The one milestone to release, see + * get_releasable_milestones(). + * + * @return bool Whether the release can go ahead. Warns when it can't. + */ + private function agrees_with_plan( $repo, array $candidates ) { + $title = (string) key( $candidates ); + $plan = Release_Plan::fetch( $repo ); + + if ( Release_Plan::READY !== $plan['status'] ) { + WP_CLI::warning( "Skipping milestone '{$title}' as {$repo} needs a maintainer's decision first:" ); + foreach ( $plan['blockers'] as $blocker ) { + WP_CLI::log( " - {$blocker['message']}" ); + } + return false; + } + + if ( $plan['release']['milestone'] !== $title ) { + WP_CLI::warning( "Skipping milestone '{$title}' as the changes on the default branch belong to milestone '{$plan['release']['milestone']}'." ); + return false; + } + + $on_branch = array_column( $plan['pull_requests'], 'number' ); + $in_notes = array_column( $candidates[ $title ][1]['milestones'][0]['pull_requests'], 'number' ); + $missing = array_diff( $in_notes, $on_branch ); + + if ( $missing ) { + WP_CLI::warning( + "Skipping milestone '{$title}' as #" . implode( ', #', $missing ) + . " was not merged into the default branch since {$plan['latest_tag']}, so the release would not contain it." + ); + return false; + } + + return true; + } + + /** + * Finds the open milestones of a repository that can be released, and + * warns about the ones that cannot. + * + * @param string $repo Repository name, including the org. + * @param string|null $only_milestone Title of the only milestone to look at. + * + * @return array Milestone title => milestone and its notes. + */ + private function get_releasable_milestones( $repo, $only_milestone ) { + $releases = array_map( + static function ( $release ) { + return $release->tag_name; + }, + GitHub::get_project_releases( $repo ) + ); + $tags = array_map( + static function ( $tag ) { + return $tag->name; + }, + GitHub::get_tags( $repo ) + ); + + $milestones = GitHub::get_project_milestones( $repo ); + + if ( null !== $only_milestone ) { + $milestones = array_filter( + $milestones, + static function ( $milestone ) use ( $only_milestone ) { + return $milestone->title === $only_milestone; + } + ); + + if ( ! $milestones ) { + WP_CLI::warning( "No open milestone '{$only_milestone}' in {$repo}." ); + } + } + + $candidates = []; + foreach ( $milestones as $milestone ) { + WP_CLI::log( "Checking milestone '{$milestone->title}'..." ); + + $tag_status = Release_Plan::get_tag_status( $milestone->title, $releases, $tags ); + + if ( Release_Plan::RELEASED === $tag_status ) { + WP_CLI::log( "Found a matching release, skipping milestone '{$milestone->title}'..." ); + continue; + } + + if ( Release_Plan::TAGGED === $tag_status ) { + WP_CLI::warning( "Skipping milestone '{$milestone->title}' as it was already tagged. Its pull requests need a new milestone." ); + continue; + } + + if ( Release_Plan::NOT_NEWER === $tag_status ) { + WP_CLI::warning( "Skipping milestone '{$milestone->title}' as it is not a version newer than the latest tag." ); + continue; + } + + if ( $this->has_open_items_on_milestone( $repo, $milestone->number ) ) { + WP_CLI::warning( "Skipping milestone '{$milestone->title}' as it has open issues/PRs assigned to it." ); + continue; + } + + $notes = Release_Notes::get( $repo, [ $milestone ], 'pull-request' ); + + if ( empty( $notes['milestones'][0]['pull_requests'] ) ) { + WP_CLI::warning( "Skipping milestone '{$milestone->title}' as it has no merged pull requests." ); + continue; + } + + $candidates[ $milestone->title ] = [ $milestone, $notes ]; + } + + return $candidates; + } + /** * Checks which repositories have unreleased changes, and whether their * milestones are ready to release them. diff --git a/.maintenance/src/Release_Plan.php b/.maintenance/src/Release_Plan.php index 49b9dff..fce1bd8 100644 --- a/.maintenance/src/Release_Plan.php +++ b/.maintenance/src/Release_Plan.php @@ -14,6 +14,11 @@ final class Release_Plan { const READY = 'ready'; const NEEDS_HUMAN = 'needs_human'; + const RELEASED = 'released'; + const TAGGED = 'tagged'; + const NOT_NEWER = 'not_newer'; + const TAGGABLE = 'taggable'; + /** * Authors of routine pull requests that do not call for a release by * themselves, unless a maintainer added them to a milestone. @@ -177,6 +182,37 @@ public static function latest_version_tag( array $tags ) { return $latest; } + /** + * Tells whether a milestone can still be tagged: not when it has a + * release or a tag already, or when it is not a version newer than the + * latest tag. + * + * @param string $title Milestone title. + * @param string[] $releases Tag names of the published releases. + * @param string[] $tags Names of all tags. + * + * @return string One of RELEASED, TAGGED, NOT_NEWER or TAGGABLE. + */ + public static function get_tag_status( $title, array $releases, array $tags ) { + $version = ltrim( $title, 'v' ); + $latest = self::latest_version_tag( $tags ); + + if ( self::has_tag( $releases, $version ) ) { + return self::RELEASED; + } + + if ( self::has_tag( $tags, $version ) ) { + return self::TAGGED; + } + + if ( ! Bundle::is_version( $version ) + || ( null !== $latest && ! version_compare( $version, ltrim( $latest, 'v' ), '>' ) ) ) { + return self::NOT_NEWER; + } + + return self::TAGGABLE; + } + /** * Checks whether a version has been tagged, with or without a leading 'v'. * diff --git a/.typos.toml b/.typos.toml index 46364c5..7a69107 100644 --- a/.typos.toml +++ b/.typos.toml @@ -15,6 +15,7 @@ extend-exclude = [ "process/*", "restful/*", "spyc/*", + "tests/fixtures/*", "vendor/*", "wordpress-core/*", "wp-cli/*", diff --git a/composer.json b/composer.json index a8db8d5..d2cf4da 100644 --- a/composer.json +++ b/composer.json @@ -312,7 +312,8 @@ }, "autoload-dev": { "psr-4": { - "WP_CLI\\Maintenance\\": ".maintenance/src/" + "WP_CLI\\Maintenance\\": ".maintenance/src/", + "WP_CLI\\Maintenance\\Tests\\": "tests/includes/" }, "files": [ ".maintenance/bootstrap.php" @@ -338,6 +339,7 @@ "phpstan": "run-phpstan-tests", "phpunit": "run-php-unit-tests", "prepare-tests": "install-package-tests", + "update-github-fixtures": "php tests/fixtures/github/update.php", "test": [ "@lint", "@phpcs", diff --git a/tests/GitHubApiTest.php b/tests/GitHubApiTest.php new file mode 100644 index 0000000..0e6b94d --- /dev/null +++ b/tests/GitHubApiTest.php @@ -0,0 +1,165 @@ +respond( + 'GET', + 'repos/wp-cli/block-command/tags?per_page=100', + $this->fixture( 'tags-page-1' ), + 200, + [ 'Link' => '; rel="next", ; rel="last"' ] + ); + $this->respond( 'GET', 'repositories/1/tags?page=2', $this->fixture( 'tags-page-2' ) ); + + $tags = array_column( GitHub::get_tags( 'wp-cli/block-command' ), 'name' ); + + $this->assertSame( + array_merge( + array_column( $this->fixture( 'tags-page-1' ), 'name' ), + array_column( $this->fixture( 'tags-page-2' ), 'name' ) + ), + $tags + ); + $this->assertCount( 2, $this->requests ); + } + + public function test_a_missing_release_is_null(): void { + $this->respond( 'GET', 'repos/wp-cli/block-command/releases/tags/v0.0.0', $this->fixture( 'release-absent' ), 404 ); + + $this->assertNull( GitHub::get_release_by_tag( 'wp-cli/block-command', 'v0.0.0' ) ); + } + + public function test_a_failed_request_stops_with_an_error_code(): void { + $this->respond( + 'GET', + 'repos/wp-cli/block-command', + [ 'message' => 'API rate limit exceeded' ], + 403, + [ 'X-RateLimit-Remaining' => '0' ] + ); + + try { + GitHub::get_default_branch( 'wp-cli/block-command' ); + $this->fail( 'The request should have failed.' ); + } catch ( ExitException $exception ) { + $this->assertSame( 1, $exception->getCode() ); + } + + $this->assertStringContainsString( 'GitHub API request failed (rate_limited)', $this->logger->stderr ); + } + + public function test_writes_are_sent_as_json(): void { + $this->respond( 'PATCH', 'repos/wp-cli/block-command/milestones/4', $this->fixture( 'milestone' ) ); + + GitHub::close_milestone( 'wp-cli/block-command', 4 ); + + $this->assertSame( [ 'PATCH https://api.github.com/repos/wp-cli/block-command/milestones/4' ], $this->requests ); + $this->assertEquals( (object) [ 'state' => 'closed' ], $this->sent_data[0] ); + } + + public function test_writes_need_credentials(): void { + $auth = new ReflectionProperty( GitHub::class, 'auth' ); + if ( PHP_VERSION_ID < 80100 ) { + $auth->setAccessible( true ); + } + $auth->setValue( + null, + [ + 'token' => null, + 'source' => null, + ] + ); + + try { + GitHub::close_milestone( 'wp-cli/block-command', 4 ); + $this->fail( 'The write should have been refused.' ); + } catch ( ExitException $exception ) { + $this->assertSame( [], $this->requests ); + } + + $this->assertStringContainsString( 'No GitHub credentials found', $this->logger->stderr ); + } + + public function test_writes_wait_only_for_what_is_left_of_the_interval(): void { + $this->respond( 'PATCH', 'repos/wp-cli/block-command/milestones/4', $this->fixture( 'milestone' ) ); + GitHub::set_write_interval( 0.2 ); + + $start = microtime( true ); + GitHub::close_milestone( 'wp-cli/block-command', 4 ); + GitHub::close_milestone( 'wp-cli/block-command', 4 ); + $paced = microtime( true ) - $start; + + usleep( 250000 ); + + $start = microtime( true ); + GitHub::close_milestone( 'wp-cli/block-command', 4 ); + $unpaced = microtime( true ) - $start; + + $this->assertGreaterThanOrEqual( 0.19, $paced, 'The second write should wait for the interval.' ); + $this->assertLessThan( 0.1, $unpaced, 'A write after the interval has passed should not wait.' ); + } + + public function test_merged_pull_requests_since_a_tag(): void { + $comparison = $this->fixture( 'comparison' ); + $tagged_at = $comparison->base_commit->commit->committer->date; + $in_range = $comparison->commits[0]->sha; + + $this->respond( 'GET', 'repos/wp-cli/block-command/compare/v1.0.1...main', $comparison ); + $this->respond( + 'GET', + 'repos/wp-cli/block-command/pulls?state=closed&base=main', + [ + $this->pull_request( 0, 30, '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z', $in_range ), + // Merged into another branch, so its merge commit is not on main. + $this->pull_request( 1, 29, '2026-08-01T00:00:00Z', '2026-08-01T00:00:00Z', str_repeat( 'f', 40 ) ), + // Closed without merging. + $this->pull_request( 2, 28, '2026-07-01T00:00:00Z', null, null ), + // Last updated before the tag, which ends the listing. + $this->pull_request( 0, 27, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', $in_range ), + ], + 200, + [ 'Link' => '; rel="next"' ] + ); + + $pull_requests = GitHub::get_pull_requests_merged_since( 'wp-cli/block-command', 'v1.0.1', 'main' ); + + $this->assertGreaterThan( '2026-01-01T00:00:00Z', $tagged_at ); + $this->assertSame( [ 30 ], array_column( $pull_requests, 'number' ) ); + // The second page is never requested. + $this->assertCount( 2, $this->requests ); + } + + public function test_merged_pull_requests_fall_back_to_the_merge_date_past_250_commits(): void { + $comparison = $this->fixture( 'comparison' ); + $comparison->total_commits = 300; + + $this->respond( 'GET', 'repos/wp-cli/block-command/compare/v1.0.1...main', $comparison ); + $this->respond( + 'GET', + 'repos/wp-cli/block-command/pulls?state=closed&base=main', + [ $this->pull_request( 0, 30, '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z', str_repeat( 'f', 40 ) ) ] + ); + + $pull_requests = GitHub::get_pull_requests_merged_since( 'wp-cli/block-command', 'v1.0.1', 'main' ); + + $this->assertSame( [ 30 ], array_column( $pull_requests, 'number' ) ); + } + + private function pull_request( $index, $number, $updated_at, $merged_at, $merge_commit_sha ) { + return $this->fixture_object( + 'pull-requests', + $index, + [ + 'number' => $number, + 'updated_at' => $updated_at, + 'merged_at' => $merged_at, + 'merge_commit_sha' => $merge_commit_sha, + ] + ); + } +} diff --git a/tests/ReleaseCommandTest.php b/tests/ReleaseCommandTest.php new file mode 100644 index 0000000..8c7dfcf --- /dev/null +++ b/tests/ReleaseCommandTest.php @@ -0,0 +1,245 @@ + $milestones Milestone number => title. + * @param string[] $tags Tag names, latest first. + * @param array $on_branch Pull requests merged into the + * default branch since the latest + * tag: number => milestone title. + * Defaults to #18 and #19 on the + * first milestone. + * @param array $open_items Milestone number => open items. + */ + private function repository( array $milestones, array $tags = [ 'v1.0.0' ], $on_branch = null, array $open_items = [] ) { + $comparison = $this->fixture( 'comparison' ); + if ( null === $on_branch ) { + $on_branch = array_fill_keys( [ 18, 19 ], reset( $milestones ) ); + } + + $this->respond( 'GET', self::REPO . '/releases', [] ); + $this->respond( + 'GET', + self::REPO . '/tags', + array_map( + function ( $tag ) { + return $this->fixture_object( 'tags-page-1', 0, [ 'name' => $tag ] ); + }, + $tags + ) + ); + $this->respond( 'GET', self::REPO . "/compare/{$tags[0]}...main", $comparison ); + + $pull_requests = []; + $index = 0; + foreach ( $on_branch as $number => $milestone ) { + $pull_requests[] = $this->fixture_object( + 'pull-requests', + 0, + [ + 'number' => $number, + 'updated_at' => '2026-09-01T00:00:00Z', + 'merged_at' => '2026-09-01T00:00:00Z', + 'merge_commit_sha' => $comparison->commits[ $index++ ]->sha, + 'milestone' => [ 'title' => $milestone ], + ] + ); + } + $this->respond( 'GET', self::REPO . '/pulls', $pull_requests ); + + $open = []; + foreach ( $milestones as $number => $title ) { + $count = isset( $open_items[ $number ] ) ? $open_items[ $number ] : 0; + $open[] = $this->fixture_object( + 'milestone', + null, + [ + 'number' => $number, + 'title' => $title, + 'state' => 'open', + 'open_issues' => $count, + ] + ); + $this->respond( 'GET', self::REPO . "/issues?milestone={$number}&state=open", array_slice( $this->fixture( 'issues' ), 0, $count ) ); + $this->respond( 'GET', self::REPO . "/issues?milestone={$number}&state=all", $this->fixture( 'issues' ) ); + $this->respond( 'PATCH', self::REPO . "/milestones/{$number}", $this->fixture( 'milestone' ) ); + } + + // The plan asks for all milestones, `generate` for the open ones. + $this->respond( 'GET', self::REPO . '/milestones?state=all', $open ); + $this->respond( 'GET', self::REPO . '/milestones', $open ); + $this->respond( 'GET', self::REPO, $this->fixture( 'repository' ) ); + $this->respond( 'POST', self::REPO . '/releases', $this->fixture( 'release' ) ); + } + + /** + * @return array Write requests, as "METHOD URL". + */ + private function writes() { + return array_values( + array_filter( + $this->requests, + static function ( $request ) { + return 0 !== strpos( $request, 'GET ' ); + } + ) + ); + } + + public function test_generate_publishes_the_release_and_closes_the_milestone(): void { + $this->repository( [ 4 => '1.0.1' ] ); + + ( new Release_Command() )->generate( [ 'block-command' ], [ 'yes' => true ] ); + + $this->assertSame( + [ + 'POST https://api.github.com/repos/wp-cli/block-command/releases', + 'PATCH https://api.github.com/repos/wp-cli/block-command/milestones/4', + ], + $this->writes() + ); + + $release = $this->sent_data[ array_search( $this->writes()[0], $this->requests, true ) ]; + $this->assertSame( 'v1.0.1', $release->tag_name ); + $this->assertSame( 'Version 1.0.1', $release->name ); + $this->assertSame( 'main', $release->target_commitish ); + $this->assertStringContainsString( '[#18](https://github.com/wp-cli/block-command/pull/18)', $release->body ); + $this->assertStringContainsString( '[#19](https://github.com/wp-cli/block-command/pull/19)', $release->body ); + } + + public function test_generate_publishes_the_notes_file(): void { + $this->repository( [ 4 => '1.0.1' ] ); + $notes = tempnam( sys_get_temp_dir(), 'notes' ); + file_put_contents( $notes, "- Reviewed notes\n" ); + + ( new Release_Command() )->generate( + [ 'block-command' ], + [ + 'milestone' => '1.0.1', + 'notes-file' => $notes, + 'yes' => true, + ] + ); + unlink( $notes ); + + $release = $this->sent_data[ array_search( $this->writes()[0], $this->requests, true ) ]; + $this->assertSame( "- Reviewed notes\n", $release->body ); + } + + public function test_generate_skips_a_milestone_that_was_already_tagged(): void { + $this->repository( [ 4 => '1.0.1' ], [ 'v1.0.1', 'v1.0.0' ] ); + + ( new Release_Command() )->generate( [ 'block-command' ], [ 'yes' => true ] ); + + $this->assertSame( [], $this->writes() ); + $this->assertStringContainsString( "Skipping milestone '1.0.1' as it was already tagged.", $this->logger->stderr ); + } + + public function test_generate_needs_a_choice_between_several_milestones(): void { + $this->repository( + [ + 4 => '1.0.1', + 5 => '1.1.0', + ] + ); + + ( new Release_Command() )->generate( [ 'block-command' ], [ 'yes' => true ] ); + + $this->assertSame( [], $this->writes() ); + $this->assertStringContainsString( 'several milestones could be released: 1.0.1, 1.1.0', $this->logger->stderr ); + } + + public function test_generate_refuses_when_changes_of_another_milestone_would_ship(): void { + $this->repository( + [ + 4 => '1.0.1', + 5 => '1.1.0', + ], + [ 'v1.0.0' ], + [ + 18 => '1.0.1', + 19 => '1.0.1', + 30 => '1.1.0', + ], + [ 5 => 1 ] + ); + + ( new Release_Command() )->generate( [ 'block-command' ], [ 'yes' => true ] ); + + $this->assertSame( [], $this->writes() ); + $this->assertStringContainsString( "Skipping milestone '1.0.1' as wp-cli/block-command needs a maintainer's decision first", $this->logger->stderr ); + $this->assertStringContainsString( 'spread over the milestones 1.0.1, 1.1.0', $this->logger->stdout ); + } + + public function test_generate_refuses_notes_for_pull_requests_missing_from_the_branch(): void { + $this->repository( [ 4 => '1.0.1' ], [ 'v1.0.0' ], [ 18 => '1.0.1' ] ); + + ( new Release_Command() )->generate( [ 'block-command' ], [ 'yes' => true ] ); + + $this->assertSame( [], $this->writes() ); + $this->assertStringContainsString( "Skipping milestone '1.0.1' as #19 was not merged into the default branch since v1.0.0", $this->logger->stderr ); + } + + public function test_plan_reports_a_ready_milestone(): void { + $comparison = $this->fixture( 'comparison' ); + + $this->respond( 'GET', self::REPO . '/tags', [ $this->fixture_object( 'tags-page-1', 0, [ 'name' => 'v1.0.1' ] ) ] ); + $this->respond( + 'GET', + self::REPO . '/milestones?state=all', + [ + $this->fixture_object( + 'milestone', + null, + [ + 'title' => '1.0.2', + 'state' => 'open', + 'open_issues' => 0, + ] + ), + ] + ); + $this->respond( 'GET', self::REPO, $this->fixture( 'repository' ) ); + $this->respond( 'GET', self::REPO . '/compare/v1.0.1...main', $comparison ); + $this->respond( + 'GET', + self::REPO . '/pulls', + [ + $this->fixture_object( + 'pull-requests', + 0, + [ + 'number' => 18, + 'updated_at' => '2026-09-01T00:00:00Z', + 'merged_at' => '2026-09-01T00:00:00Z', + 'merge_commit_sha' => $comparison->commits[0]->sha, + 'milestone' => [ 'title' => '1.0.2' ], + ] + ), + ] + ); + + $plan = Release_Plan::fetch( 'wp-cli/block-command' ); + + $this->assertSame( Release_Plan::READY, $plan['status'] ); + $this->assertSame( + [ + 'milestone' => '1.0.2', + 'tag' => 'v1.0.2', + ], + $plan['release'] + ); + $this->assertSame( [ 18 ], array_column( $plan['pull_requests'], 'number' ) ); + } +} diff --git a/tests/ReleasePlanTest.php b/tests/ReleasePlanTest.php index 95fec71..12b8e48 100644 --- a/tests/ReleasePlanTest.php +++ b/tests/ReleasePlanTest.php @@ -162,4 +162,20 @@ public function test_a_repository_without_version_tags_needs_a_decision(): void $this->assertSame( Release_Plan::NEEDS_HUMAN, $plan['status'] ); $this->assertSame( [ 'no_version_tag' => [] ], $this->blockers( $plan ) ); } + + public function test_tag_status_of_a_milestone(): void { + $releases = [ 'v1.0.1' ]; + $tags = [ 'v1.0.1', 'v1.0.2', 'v1.0.3' ]; + + $this->assertSame( Release_Plan::RELEASED, Release_Plan::get_tag_status( '1.0.1', $releases, $tags ) ); + $this->assertSame( Release_Plan::TAGGED, Release_Plan::get_tag_status( '1.0.3', $releases, $tags ) ); + $this->assertSame( Release_Plan::NOT_NEWER, Release_Plan::get_tag_status( '1.0.0', $releases, $tags ) ); + $this->assertSame( Release_Plan::NOT_NEWER, Release_Plan::get_tag_status( 'Future', $releases, $tags ) ); + $this->assertSame( Release_Plan::TAGGABLE, Release_Plan::get_tag_status( '1.0.4', $releases, $tags ) ); + $this->assertSame( Release_Plan::TAGGABLE, Release_Plan::get_tag_status( 'v1.1.0', $releases, $tags ) ); + } + + public function test_a_first_release_can_be_tagged(): void { + $this->assertSame( Release_Plan::TAGGABLE, Release_Plan::get_tag_status( '1.0.0', [], [] ) ); + } } diff --git a/tests/fixtures/github/comparison.json b/tests/fixtures/github/comparison.json new file mode 100644 index 0000000..cebeaf9 --- /dev/null +++ b/tests/fixtures/github/comparison.json @@ -0,0 +1,1052 @@ +{ + "request": "GET /repos/wp-cli/block-command/compare/v1.0.1...v1.0.3", + "status": 200, + "headers": {}, + "body": { + "url": "https://api.github.com/repos/wp-cli/block-command/compare/v1.0.1...v1.0.3", + "html_url": "https://github.com/wp-cli/block-command/compare/v1.0.1...v1.0.3", + "permalink_url": "https://github.com/wp-cli/block-command/compare/wp-cli:259b8da...wp-cli:f7dd14d", + "diff_url": "https://github.com/wp-cli/block-command/compare/v1.0.1...v1.0.3.diff", + "patch_url": "https://github.com/wp-cli/block-command/compare/v1.0.1...v1.0.3.patch", + "base_commit": { + "sha": "259b8da86f84a386e262189be3f4b912183f563d", + "node_id": "C_kwDOQl0QSdoAKDI1OWI4ZGE4NmY4NGEzODZlMjYyMTg5YmUzZjRiOTEyMTgzZjU2M2Q", + "commit": { + "author": { + "name": "Pascal Birchler", + "email": "pascal.birchler@gmail.com", + "date": "2026-03-28T12:09:20Z" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "date": "2026-03-28T12:09:20Z" + }, + "message": "Tests: improve Windows compatibility (#15)", + "tree": { + "sha": "de8ec7aa4f0e6df9ae195fe2db6d91e2116aac61", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/de8ec7aa4f0e6df9ae195fe2db6d91e2116aac61" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/259b8da86f84a386e262189be3f4b912183f563d", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJpx8TwCRC1aQ7uu5UhlAAAVWwQAEB+Bl6IHIZfDyqQm0irqHIc\nqraRvaz+ZVPM59xpW9kEevGDbmXNweicQlSVkDPu7DQG+ZjN0zX0Aiq0G9AYt1Mb\nxf6dguGSr+Qnh/wM7QKi9RgjSnZMCvk1m/FUD9aeo0B+XBnOTpoDhdBALV6PaLmG\n/aqoPZuQeaXKNVRunm1ToPq6J+UCDVolcE9CmkKqH+NridPjS/zDUmBXWfqRk3TG\nY1DJfoos/abg+ARfuWV4Qtmvz5KgnYsyzLSXKm6ZCdtZ/hrgK48NQw9yNWbMGcye\nFCJPS+PwOMWZjyu2kBO5+DipeWb4gfEkufEJJ6u2ZgZ1Bv9w1SDJ8PLkNs7bUskV\nxU0U2jhu6mhG2f0KJGuxr8jaz3wYFzL+s+pEZE6aH+fzeJtB3kjGLQPBseL85rT0\neIgmoER6BcaVZndpBCjaRpYEzAnyk4pPXVJbViZGsvplpTAw9dvaxupiufxtYoJG\nMqpq9GR53OgU+SzSAIExdvi/BmL/tCp25DeJ6NJM1tkhK2FWsot23ejMsKkcrCWh\n2cMIDdr0EjrIxiul3jlauxU88/dAjnZrD41ufqh5Spsh+TZnY8bRFwyFDGTplmKl\nvoFR3usz8XpvS+jdhXONw/YNAlYjNSa2H6K8RtabufQo9smLaKCFCUJ91c2/MiLa\nS0l+sVOqVqw8vJhqNfsS\n=/ZjC\n-----END PGP SIGNATURE-----\n", + "payload": "tree de8ec7aa4f0e6df9ae195fe2db6d91e2116aac61\nparent 020d47a834d94fb7d5e34084d20dc13b5594cfdd\nauthor Pascal Birchler 1774699760 +0100\ncommitter GitHub 1774699760 +0100\n\nTests: improve Windows compatibility (#15)", + "verified_at": "2026-03-28T12:09:21Z" + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/259b8da86f84a386e262189be3f4b912183f563d", + "html_url": "https://github.com/wp-cli/block-command/commit/259b8da86f84a386e262189be3f4b912183f563d", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/259b8da86f84a386e262189be3f4b912183f563d/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "web-flow", + "id": 19864447, + "node_id": "MDQ6VXNlcjE5ODY0NDQ3", + "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/web-flow", + "html_url": "https://github.com/web-flow", + "followers_url": "https://api.github.com/users/web-flow/followers", + "following_url": "https://api.github.com/users/web-flow/following{/other_user}", + "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", + "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", + "organizations_url": "https://api.github.com/users/web-flow/orgs", + "repos_url": "https://api.github.com/users/web-flow/repos", + "events_url": "https://api.github.com/users/web-flow/events{/privacy}", + "received_events_url": "https://api.github.com/users/web-flow/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "020d47a834d94fb7d5e34084d20dc13b5594cfdd", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/020d47a834d94fb7d5e34084d20dc13b5594cfdd", + "html_url": "https://github.com/wp-cli/block-command/commit/020d47a834d94fb7d5e34084d20dc13b5594cfdd" + } + ] + }, + "merge_base_commit": { + "sha": "259b8da86f84a386e262189be3f4b912183f563d", + "node_id": "C_kwDOQl0QSdoAKDI1OWI4ZGE4NmY4NGEzODZlMjYyMTg5YmUzZjRiOTEyMTgzZjU2M2Q", + "commit": { + "author": { + "name": "Pascal Birchler", + "email": "pascal.birchler@gmail.com", + "date": "2026-03-28T12:09:20Z" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "date": "2026-03-28T12:09:20Z" + }, + "message": "Tests: improve Windows compatibility (#15)", + "tree": { + "sha": "de8ec7aa4f0e6df9ae195fe2db6d91e2116aac61", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/de8ec7aa4f0e6df9ae195fe2db6d91e2116aac61" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/259b8da86f84a386e262189be3f4b912183f563d", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJpx8TwCRC1aQ7uu5UhlAAAVWwQAEB+Bl6IHIZfDyqQm0irqHIc\nqraRvaz+ZVPM59xpW9kEevGDbmXNweicQlSVkDPu7DQG+ZjN0zX0Aiq0G9AYt1Mb\nxf6dguGSr+Qnh/wM7QKi9RgjSnZMCvk1m/FUD9aeo0B+XBnOTpoDhdBALV6PaLmG\n/aqoPZuQeaXKNVRunm1ToPq6J+UCDVolcE9CmkKqH+NridPjS/zDUmBXWfqRk3TG\nY1DJfoos/abg+ARfuWV4Qtmvz5KgnYsyzLSXKm6ZCdtZ/hrgK48NQw9yNWbMGcye\nFCJPS+PwOMWZjyu2kBO5+DipeWb4gfEkufEJJ6u2ZgZ1Bv9w1SDJ8PLkNs7bUskV\nxU0U2jhu6mhG2f0KJGuxr8jaz3wYFzL+s+pEZE6aH+fzeJtB3kjGLQPBseL85rT0\neIgmoER6BcaVZndpBCjaRpYEzAnyk4pPXVJbViZGsvplpTAw9dvaxupiufxtYoJG\nMqpq9GR53OgU+SzSAIExdvi/BmL/tCp25DeJ6NJM1tkhK2FWsot23ejMsKkcrCWh\n2cMIDdr0EjrIxiul3jlauxU88/dAjnZrD41ufqh5Spsh+TZnY8bRFwyFDGTplmKl\nvoFR3usz8XpvS+jdhXONw/YNAlYjNSa2H6K8RtabufQo9smLaKCFCUJ91c2/MiLa\nS0l+sVOqVqw8vJhqNfsS\n=/ZjC\n-----END PGP SIGNATURE-----\n", + "payload": "tree de8ec7aa4f0e6df9ae195fe2db6d91e2116aac61\nparent 020d47a834d94fb7d5e34084d20dc13b5594cfdd\nauthor Pascal Birchler 1774699760 +0100\ncommitter GitHub 1774699760 +0100\n\nTests: improve Windows compatibility (#15)", + "verified_at": "2026-03-28T12:09:21Z" + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/259b8da86f84a386e262189be3f4b912183f563d", + "html_url": "https://github.com/wp-cli/block-command/commit/259b8da86f84a386e262189be3f4b912183f563d", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/259b8da86f84a386e262189be3f4b912183f563d/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "web-flow", + "id": 19864447, + "node_id": "MDQ6VXNlcjE5ODY0NDQ3", + "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/web-flow", + "html_url": "https://github.com/web-flow", + "followers_url": "https://api.github.com/users/web-flow/followers", + "following_url": "https://api.github.com/users/web-flow/following{/other_user}", + "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", + "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", + "organizations_url": "https://api.github.com/users/web-flow/orgs", + "repos_url": "https://api.github.com/users/web-flow/repos", + "events_url": "https://api.github.com/users/web-flow/events{/privacy}", + "received_events_url": "https://api.github.com/users/web-flow/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "020d47a834d94fb7d5e34084d20dc13b5594cfdd", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/020d47a834d94fb7d5e34084d20dc13b5594cfdd", + "html_url": "https://github.com/wp-cli/block-command/commit/020d47a834d94fb7d5e34084d20dc13b5594cfdd" + } + ] + }, + "status": "ahead", + "ahead_by": 10, + "behind_by": 0, + "total_commits": 10, + "commits": [ + { + "sha": "0354f94deba582b6c0549cb2788eb7201ac00d0d", + "node_id": "C_kwDOQl0QSdoAKDAzNTRmOTRkZWJhNTgyYjZjMDU0OWNiMjc4OGViNzIwMWFjMDBkMGQ", + "commit": { + "author": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-04-22T08:45:37Z" + }, + "committer": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-04-22T08:45:37Z" + }, + "message": "Update file(s) from wp-cli/.github", + "tree": { + "sha": "18e32df3e1da81ba076caf85d3e19913563fb830", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/18e32df3e1da81ba076caf85d3e19913563fb830" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/0354f94deba582b6c0549cb2788eb7201ac00d0d", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null, + "verified_at": null + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/0354f94deba582b6c0549cb2788eb7201ac00d0d", + "html_url": "https://github.com/wp-cli/block-command/commit/0354f94deba582b6c0549cb2788eb7201ac00d0d", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/0354f94deba582b6c0549cb2788eb7201ac00d0d/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "259b8da86f84a386e262189be3f4b912183f563d", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/259b8da86f84a386e262189be3f4b912183f563d", + "html_url": "https://github.com/wp-cli/block-command/commit/259b8da86f84a386e262189be3f4b912183f563d" + } + ] + }, + { + "sha": "4fb8257e8f74ce390b19bed03134a25072501f16", + "node_id": "C_kwDOQl0QSdoAKDRmYjgyNTdlOGY3NGNlMzkwYjE5YmVkMDMxMzRhMjUwNzI1MDFmMTY", + "commit": { + "author": { + "name": "ernilambar", + "email": "ernilambar@users.noreply.github.com", + "date": "2026-05-14T09:14:07Z" + }, + "committer": { + "name": "ernilambar", + "email": "ernilambar@users.noreply.github.com", + "date": "2026-05-14T09:14:07Z" + }, + "message": "Update file(s) from wp-cli/.github", + "tree": { + "sha": "2327102a9662e70d88e793890f61a24cbc15777d", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/2327102a9662e70d88e793890f61a24cbc15777d" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/4fb8257e8f74ce390b19bed03134a25072501f16", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null, + "verified_at": null + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/4fb8257e8f74ce390b19bed03134a25072501f16", + "html_url": "https://github.com/wp-cli/block-command/commit/4fb8257e8f74ce390b19bed03134a25072501f16", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/4fb8257e8f74ce390b19bed03134a25072501f16/comments", + "author": { + "login": "ernilambar", + "id": 2098823, + "node_id": "MDQ6VXNlcjIwOTg4MjM=", + "avatar_url": "https://avatars.githubusercontent.com/u/2098823?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ernilambar", + "html_url": "https://github.com/ernilambar", + "followers_url": "https://api.github.com/users/ernilambar/followers", + "following_url": "https://api.github.com/users/ernilambar/following{/other_user}", + "gists_url": "https://api.github.com/users/ernilambar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ernilambar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ernilambar/subscriptions", + "organizations_url": "https://api.github.com/users/ernilambar/orgs", + "repos_url": "https://api.github.com/users/ernilambar/repos", + "events_url": "https://api.github.com/users/ernilambar/events{/privacy}", + "received_events_url": "https://api.github.com/users/ernilambar/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "ernilambar", + "id": 2098823, + "node_id": "MDQ6VXNlcjIwOTg4MjM=", + "avatar_url": "https://avatars.githubusercontent.com/u/2098823?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ernilambar", + "html_url": "https://github.com/ernilambar", + "followers_url": "https://api.github.com/users/ernilambar/followers", + "following_url": "https://api.github.com/users/ernilambar/following{/other_user}", + "gists_url": "https://api.github.com/users/ernilambar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ernilambar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ernilambar/subscriptions", + "organizations_url": "https://api.github.com/users/ernilambar/orgs", + "repos_url": "https://api.github.com/users/ernilambar/repos", + "events_url": "https://api.github.com/users/ernilambar/events{/privacy}", + "received_events_url": "https://api.github.com/users/ernilambar/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "0354f94deba582b6c0549cb2788eb7201ac00d0d", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/0354f94deba582b6c0549cb2788eb7201ac00d0d", + "html_url": "https://github.com/wp-cli/block-command/commit/0354f94deba582b6c0549cb2788eb7201ac00d0d" + } + ] + }, + { + "sha": "27bbb207275386f0e44f679085eee95fae60694d", + "node_id": "C_kwDOQl0QSdoAKDI3YmJiMjA3Mjc1Mzg2ZjBlNDRmNjc5MDg1ZWVlOTVmYWU2MDY5NGQ", + "commit": { + "author": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-06-02T15:40:06Z" + }, + "committer": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-06-02T15:40:06Z" + }, + "message": "Update file(s) from wp-cli/.github", + "tree": { + "sha": "8d8c176a5d70e6c48e0812e16cb14a85a9911831", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/8d8c176a5d70e6c48e0812e16cb14a85a9911831" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/27bbb207275386f0e44f679085eee95fae60694d", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null, + "verified_at": null + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/27bbb207275386f0e44f679085eee95fae60694d", + "html_url": "https://github.com/wp-cli/block-command/commit/27bbb207275386f0e44f679085eee95fae60694d", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/27bbb207275386f0e44f679085eee95fae60694d/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "4fb8257e8f74ce390b19bed03134a25072501f16", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/4fb8257e8f74ce390b19bed03134a25072501f16", + "html_url": "https://github.com/wp-cli/block-command/commit/4fb8257e8f74ce390b19bed03134a25072501f16" + } + ] + }, + { + "sha": "7184f64c815e4f2198de72e87f24028491d05e06", + "node_id": "C_kwDOQl0QSdoAKDcxODRmNjRjODE1ZTRmMjE5OGRlNzJlODdmMjQwMjg0OTFkMDVlMDY", + "commit": { + "author": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-06-08T16:57:17Z" + }, + "committer": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-06-08T16:57:17Z" + }, + "message": "Update file(s) from wp-cli/.github", + "tree": { + "sha": "9bc3c60b23a6b750a4f60b145f8a24f7e0790ec8", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/9bc3c60b23a6b750a4f60b145f8a24f7e0790ec8" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/7184f64c815e4f2198de72e87f24028491d05e06", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null, + "verified_at": null + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/7184f64c815e4f2198de72e87f24028491d05e06", + "html_url": "https://github.com/wp-cli/block-command/commit/7184f64c815e4f2198de72e87f24028491d05e06", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/7184f64c815e4f2198de72e87f24028491d05e06/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "27bbb207275386f0e44f679085eee95fae60694d", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/27bbb207275386f0e44f679085eee95fae60694d", + "html_url": "https://github.com/wp-cli/block-command/commit/27bbb207275386f0e44f679085eee95fae60694d" + } + ] + }, + { + "sha": "e7a945c8b974457926a50df67d5f509220c6535a", + "node_id": "C_kwDOQl0QSdoAKGU3YTk0NWM4Yjk3NDQ1NzkyNmE1MGRmNjdkNWY1MDkyMjBjNjUzNWE", + "commit": { + "author": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-06-18T14:38:13Z" + }, + "committer": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-06-18T14:38:13Z" + }, + "message": "Update file(s) from wp-cli/.github", + "tree": { + "sha": "545abdf4fdcdda0560617bc796fb5b187d058c86", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/545abdf4fdcdda0560617bc796fb5b187d058c86" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/e7a945c8b974457926a50df67d5f509220c6535a", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null, + "verified_at": null + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/e7a945c8b974457926a50df67d5f509220c6535a", + "html_url": "https://github.com/wp-cli/block-command/commit/e7a945c8b974457926a50df67d5f509220c6535a", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/e7a945c8b974457926a50df67d5f509220c6535a/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "7184f64c815e4f2198de72e87f24028491d05e06", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/7184f64c815e4f2198de72e87f24028491d05e06", + "html_url": "https://github.com/wp-cli/block-command/commit/7184f64c815e4f2198de72e87f24028491d05e06" + } + ] + }, + { + "sha": "0219166f30bb4164e4bfc783ae692e913364e7ff", + "node_id": "C_kwDOQl0QSdoAKDAyMTkxNjZmMzBiYjQxNjRlNGJmYzc4M2FlNjkyZTkxMzM2NGU3ZmY", + "commit": { + "author": { + "name": "github-actions[bot]", + "email": "41898282+github-actions[bot]@users.noreply.github.com", + "date": "2026-07-13T10:00:50Z" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "date": "2026-07-13T10:00:50Z" + }, + "message": "Regenerate README file (#17)", + "tree": { + "sha": "a4482e951e31e49d89f2c0f96c44127b792436fe", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/a4482e951e31e49d89f2c0f96c44127b792436fe" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/0219166f30bb4164e4bfc783ae692e913364e7ff", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJqVLdSCRC1aQ7uu5UhlAAAcTgQACZw6DU1tc3EaPPI/tRAYrxr\nZ3T+kvW3aPY3zdB2tTkAa8dlEAargVTGvE/aAZyhoJYjR29Hd1Tjl5hMhSN4Ho5A\nZ/hJwAO9x2/dqpuRO2hVJh+45skDVtQGsODM0eUrFo+3lTgvDyq2W2JfaAHcx4uS\nDweuFxOleu4hOjlign6+hjyHvkffFPcVWYQpgjIYPmiSepBjPqlHY0pprK92w3T2\n9oth09wWk20xlohztjBzSE5vMhhw70ertAjk2TNzL9WacsmmBhnKDRt7MdizI1ZB\nti/RjDgC3MLciopi/WlMGz00GAcZKMeynS3ZE1cMeAO7hNSFi1wdFBuvbO0m/M5e\nZIz5DxSD61dwtS3tqtVhHASDvicZeLm7n8v0uyLqltqye1ybIFGa5j+Y0w5a00gt\njxuKYK1ACzZiH+J3jUoXx9hbY3ui9AB0o60Qd1/Q5KTOgI79VBD2UTyecVKdOBtf\n7BD7VZ+TEKnSFl1u+dpuxX77b/TY+zh2jFRb/vBcVYTN5u2C2dF13H0o6iQlVGx7\n2z5Q6LaKYX012R3hMQkRWb0WOikVTZ6rB9qFgxbtaNgzcMp31lOsTfZn3DHDQdFc\nCML9+C1xu85nOtN6Cm3xZYLCcF/mY/QqbPsVeVkVpaYpeC7ak9OBNsGn9PwQX18I\n0YolJ+BUfhvsRc5ku/Jf\n=T9Ey\n-----END PGP SIGNATURE-----\n", + "payload": "tree a4482e951e31e49d89f2c0f96c44127b792436fe\nparent e7a945c8b974457926a50df67d5f509220c6535a\nauthor github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 1783936850 +0200\ncommitter GitHub 1783936850 +0200\n\nRegenerate README file (#17)", + "verified_at": "2026-07-13T10:00:50Z" + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/0219166f30bb4164e4bfc783ae692e913364e7ff", + "html_url": "https://github.com/wp-cli/block-command/commit/0219166f30bb4164e4bfc783ae692e913364e7ff", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/0219166f30bb4164e4bfc783ae692e913364e7ff/comments", + "author": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "web-flow", + "id": 19864447, + "node_id": "MDQ6VXNlcjE5ODY0NDQ3", + "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/web-flow", + "html_url": "https://github.com/web-flow", + "followers_url": "https://api.github.com/users/web-flow/followers", + "following_url": "https://api.github.com/users/web-flow/following{/other_user}", + "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", + "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", + "organizations_url": "https://api.github.com/users/web-flow/orgs", + "repos_url": "https://api.github.com/users/web-flow/repos", + "events_url": "https://api.github.com/users/web-flow/events{/privacy}", + "received_events_url": "https://api.github.com/users/web-flow/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "e7a945c8b974457926a50df67d5f509220c6535a", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/e7a945c8b974457926a50df67d5f509220c6535a", + "html_url": "https://github.com/wp-cli/block-command/commit/e7a945c8b974457926a50df67d5f509220c6535a" + } + ] + }, + { + "sha": "ba292917f20f6071561332e2488b566f83b6a79b", + "node_id": "C_kwDOQl0QSdoAKGJhMjkyOTE3ZjIwZjYwNzE1NjEzMzJlMjQ4OGI1NjZmODNiNmE3OWI", + "commit": { + "author": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-07-20T16:12:58Z" + }, + "committer": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-07-20T16:12:58Z" + }, + "message": "Update file(s) from wp-cli/.github", + "tree": { + "sha": "a1c728a4bf6a7c3a50d16d9328437c3c475a626d", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/a1c728a4bf6a7c3a50d16d9328437c3c475a626d" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/ba292917f20f6071561332e2488b566f83b6a79b", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null, + "verified_at": null + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/ba292917f20f6071561332e2488b566f83b6a79b", + "html_url": "https://github.com/wp-cli/block-command/commit/ba292917f20f6071561332e2488b566f83b6a79b", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/ba292917f20f6071561332e2488b566f83b6a79b/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "0219166f30bb4164e4bfc783ae692e913364e7ff", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/0219166f30bb4164e4bfc783ae692e913364e7ff", + "html_url": "https://github.com/wp-cli/block-command/commit/0219166f30bb4164e4bfc783ae692e913364e7ff" + } + ] + }, + { + "sha": "bf948b431170ad6828770f2b77bbb1df9b5a7172", + "node_id": "C_kwDOQl0QSdoAKGJmOTQ4YjQzMTE3MGFkNjgyODc3MGYyYjc3YmJiMWRmOWI1YTcxNzI", + "commit": { + "author": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-07-29T14:28:57Z" + }, + "committer": { + "name": "swissspidy", + "email": "swissspidy@users.noreply.github.com", + "date": "2026-07-29T14:28:57Z" + }, + "message": "Update file(s) from wp-cli/.github", + "tree": { + "sha": "f1226bfb16a007be69de270bd23506ab3e43ba21", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/f1226bfb16a007be69de270bd23506ab3e43ba21" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/bf948b431170ad6828770f2b77bbb1df9b5a7172", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null, + "verified_at": null + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/bf948b431170ad6828770f2b77bbb1df9b5a7172", + "html_url": "https://github.com/wp-cli/block-command/commit/bf948b431170ad6828770f2b77bbb1df9b5a7172", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/bf948b431170ad6828770f2b77bbb1df9b5a7172/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "ba292917f20f6071561332e2488b566f83b6a79b", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/ba292917f20f6071561332e2488b566f83b6a79b", + "html_url": "https://github.com/wp-cli/block-command/commit/ba292917f20f6071561332e2488b566f83b6a79b" + } + ] + }, + { + "sha": "ea751d8a375ce8ef6e0848441b993e833fbe8911", + "node_id": "C_kwDOQl0QSdoAKGVhNzUxZDhhMzc1Y2U4ZWY2ZTA4NDg0NDFiOTkzZTgzM2ZiZTg5MTE", + "commit": { + "author": { + "name": "Pascal Birchler", + "email": "pascalb@google.com", + "date": "2026-08-04T08:32:16Z" + }, + "committer": { + "name": "Pascal Birchler", + "email": "pascalb@google.com", + "date": "2026-08-04T08:32:16Z" + }, + "message": "Require WP-CLI v3", + "tree": { + "sha": "fdda82e79fee21c36482c3ffcaf3ac346ebd4278", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/fdda82e79fee21c36482c3ffcaf3ac346ebd4278" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/ea751d8a375ce8ef6e0848441b993e833fbe8911", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEc4kNaWiCqZigEBEVDeznPddOiy8FAmpxo5AACgkQDeznPddO\niy9QrQ//brKhr7cylwJTazqaXu14/SdC52xUTsRSxQhhvmJnVUgkVOs59NI8csqe\noO7azEGbRY5TLD+fopn1TzHG+SguIu437m2VfRmxYh11xtXt1Asc45z6eD6tIp+e\n/xnHM3NnfNAaNdjTGXXxPD85vvaxaSt1w8qI2ktszxcrTUrfcQDON/BQ8Z+1Z/uZ\nuLoWB9LVc8EDDFGuI56cZ5p5NzPPyxYuWKYT49V+4QBT5T1wARb5PMbWPLKvQDR/\nfKckoiC/qiL0hnHsBidUH6W7hxd1zSSnGsTqEtEBQ9IfVZA7M1RiruCm7cM/ETRo\ncXUgsdFO01NnLfJ9Dokv1L+7ESVBZ4XgWbqvYLOPtDccHRaTKmLoJnhj3LGhcwa9\ns+9+FRtcg1vWlf7JwDIA/ky9jK/dp9sZsnzF+aLI5dlJYjR1M7cUYPk+KMfGzQUp\nXvR252EP6VHZNUXyrG/MlfXsqNYFfGsd+wuVAGjMo07s1NuDBdHP4ReaV8251Tio\nv99smqddAB13RLyvJZHM7ybAtQOIWQg99oKwpiwZsWElMlMCueUoqt2xRVnTeDJi\nfxxs5zXdL7WOYsPRYeKm3nCKx7+ueirxNFZMnv/nTtc/1ZrlmvzIDGzFm4gQnFLC\nIDRu+DeVmvsj/8nf+qDHUWdmBE44X7IHCIrCd14nBpc6Um5/ovk=\n=xvGL\n-----END PGP SIGNATURE-----", + "payload": "tree fdda82e79fee21c36482c3ffcaf3ac346ebd4278\nparent bf948b431170ad6828770f2b77bbb1df9b5a7172\nauthor Pascal Birchler 1785832336 +0200\ncommitter Pascal Birchler 1785832336 +0200\n\nRequire WP-CLI v3\n", + "verified_at": "2026-08-04T08:32:20Z" + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/ea751d8a375ce8ef6e0848441b993e833fbe8911", + "html_url": "https://github.com/wp-cli/block-command/commit/ea751d8a375ce8ef6e0848441b993e833fbe8911", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/ea751d8a375ce8ef6e0848441b993e833fbe8911/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "bf948b431170ad6828770f2b77bbb1df9b5a7172", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/bf948b431170ad6828770f2b77bbb1df9b5a7172", + "html_url": "https://github.com/wp-cli/block-command/commit/bf948b431170ad6828770f2b77bbb1df9b5a7172" + } + ] + }, + { + "sha": "f7dd14da742b8722877d0517326320cc5041bafb", + "node_id": "C_kwDOQl0QSdoAKGY3ZGQxNGRhNzQyYjg3MjI4NzdkMDUxNzMyNjMyMGNjNTA0MWJhZmI", + "commit": { + "author": { + "name": "Pascal Birchler", + "email": "pascalb@google.com", + "date": "2026-08-04T11:49:18Z" + }, + "committer": { + "name": "Pascal Birchler", + "email": "pascalb@google.com", + "date": "2026-08-04T11:49:18Z" + }, + "message": "Update package requirements", + "tree": { + "sha": "2f1b8eaecbd62e9b4c6d8ac8e4c839c6ee17a02f", + "url": "https://api.github.com/repos/wp-cli/block-command/git/trees/2f1b8eaecbd62e9b4c6d8ac8e4c839c6ee17a02f" + }, + "url": "https://api.github.com/repos/wp-cli/block-command/git/commits/f7dd14da742b8722877d0517326320cc5041bafb", + "comment_count": 0, + "verification": { + "verified": true, + "reason": "valid", + "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEc4kNaWiCqZigEBEVDeznPddOiy8FAmpx0b4ACgkQDeznPddO\niy9FKw//X/ht9utvFRQWAu4XpE187zJqJH0ePQn0zaRgWkmwMFgiS3LHTIOfuTo1\nZ0R5iR2F+5nj/9dGP+f90GAQ80MkqBxmlAPuZidTYPd8/BnsP6f6sx0JubJLH3pR\nKu2ayZY1cBFxaBNgS09I6ZvC+XxCcHc4tqyAvn4D5atmXkx8giVuGiMhA+apGNrt\nucmdvV0UeSSeUfjsJz/NyeP90HNT+QHcPZX9AhA9nXVqZIVqkv5JKC1dgn+Z95fl\nSh3mcUT/vjf0gR+iRoOx5x701Yt3wEYY50B4eB1IQs9vBGt4xvKhjOI3c75uRZWQ\nNq8/1hQeYAJbtRo/SlvviuxuV7Bj62AalAexd/vdqga+grcj34xqVVByI6rbmgfK\njqOO+aVIWzr2iA9y0Dr0397LlmcvPzHFuDUyEdoXRag9yNFeuHdj08zb0CrIyjbd\nqY96A1vCmSBKRyCGHrFqlJG5uO0IaHi4SrvJ1YPeqNM/KbU4jZC1czxlL7lFqFJy\nUIYU4GKYEYB5Ff74DY952z4svL2aPBeT61o3tbX4Rwirb4Rb2AgSTDFd7L+GjqzZ\n4XKCnN9qnYJ+hUNgd0h+l65tStpBk6tLFcVXqilm92us2+Aj8CGLZKtx2mhcoMrS\ncuA3nPr2+gzDeZJMkL5ThpFXDjQsxiUx3xLyetycf5Acr/n6FLg=\n=HrJn\n-----END PGP SIGNATURE-----", + "payload": "tree 2f1b8eaecbd62e9b4c6d8ac8e4c839c6ee17a02f\nparent ea751d8a375ce8ef6e0848441b993e833fbe8911\nauthor Pascal Birchler 1785844158 +0200\ncommitter Pascal Birchler 1785844158 +0200\n\nUpdate package requirements\n", + "verified_at": "2026-08-04T11:49:24Z" + } + }, + "url": "https://api.github.com/repos/wp-cli/block-command/commits/f7dd14da742b8722877d0517326320cc5041bafb", + "html_url": "https://github.com/wp-cli/block-command/commit/f7dd14da742b8722877d0517326320cc5041bafb", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/commits/f7dd14da742b8722877d0517326320cc5041bafb/comments", + "author": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "ea751d8a375ce8ef6e0848441b993e833fbe8911", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/ea751d8a375ce8ef6e0848441b993e833fbe8911", + "html_url": "https://github.com/wp-cli/block-command/commit/ea751d8a375ce8ef6e0848441b993e833fbe8911" + } + ] + } + ], + "files": [ + { + "sha": "bf50ffcf0dae6931101e22a104f9a090e7534f55", + "filename": ".github/dependabot.yml", + "status": "modified", + "additions": 4, + "deletions": 0, + "changes": 4, + "blob_url": "https://github.com/wp-cli/block-command/blob/f7dd14da742b8722877d0517326320cc5041bafb/.github%2Fdependabot.yml", + "raw_url": "https://github.com/wp-cli/block-command/raw/f7dd14da742b8722877d0517326320cc5041bafb/.github%2Fdependabot.yml", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/.github%2Fdependabot.yml?ref=f7dd14da742b8722877d0517326320cc5041bafb", + "patch": "@@ -7,11 +7,15 @@ updates:\n open-pull-requests-limit: 10\n labels:\n - scope:distribution\n+ cooldown:\n+ default-days: 7\n - package-ecosystem: github-actions\n directory: \"/\"\n schedule:\n interval: daily\n open-pull-requests-limit: 10\n labels:\n - scope:distribution\n+ cooldown:\n+ default-days: 7\n " + }, + { + "sha": "c703112780bf8637c174896df34af8fd60e330bd", + "filename": ".github/workflows/copilot-setup-steps.yml", + "status": "modified", + "additions": 9, + "deletions": 3, + "changes": 12, + "blob_url": "https://github.com/wp-cli/block-command/blob/f7dd14da742b8722877d0517326320cc5041bafb/.github%2Fworkflows%2Fcopilot-setup-steps.yml", + "raw_url": "https://github.com/wp-cli/block-command/raw/f7dd14da742b8722877d0517326320cc5041bafb/.github%2Fworkflows%2Fcopilot-setup-steps.yml", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/.github%2Fworkflows%2Fcopilot-setup-steps.yml?ref=f7dd14da742b8722877d0517326320cc5041bafb", + "patch": "@@ -9,23 +9,29 @@ on:\n paths:\n - .github/workflows/copilot-setup-steps.yml\n \n+permissions:\n+ contents: read\n+\n jobs:\n copilot-setup-steps:\n+ name: Setup environment\n runs-on: ubuntu-latest\n permissions:\n contents: read\n \n steps:\n - name: Checkout code\n- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1\n+ with:\n+ persist-credentials: false\n \n - name: Check existence of composer.json file\n id: check_composer_file\n run: echo \"files_exists=$(test -f composer.json && echo true || echo false)\" >> \"$GITHUB_OUTPUT\"\n \n - name: Set up PHP environment\n if: steps.check_composer_file.outputs.files_exists == 'true'\n- uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2\n+ uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2\n with:\n php-version: 'latest'\n ini-values: zend.assertions=1, error_reporting=-1, display_errors=On\n@@ -36,6 +42,6 @@ jobs:\n \n - name: Install Composer dependencies & cache dependencies\n if: steps.check_composer_file.outputs.files_exists == 'true'\n- uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4\n+ uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0\n env:\n COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }}" + }, + { + "sha": "102bd8455e80ed6dad671eaae71073b984e4dee5", + "filename": "README.md", + "status": "modified", + "additions": 1, + "deletions": 1, + "changes": 2, + "blob_url": "https://github.com/wp-cli/block-command/blob/f7dd14da742b8722877d0517326320cc5041bafb/README.md", + "raw_url": "https://github.com/wp-cli/block-command/raw/f7dd14da742b8722877d0517326320cc5041bafb/README.md", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/README.md?ref=f7dd14da742b8722877d0517326320cc5041bafb", + "patch": "@@ -1215,7 +1215,7 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file\n \n ## Support\n \n-GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support\n+GitHub issues aren't for general support questions. For support resources and next steps, see the WP-CLI Support page: https://make.wordpress.org/cli/handbook/support/\n \n \n *This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.*" + }, + { + "sha": "ebfd38d3d383ba825831570216ef73b9666f9f63", + "filename": "composer.json", + "status": "modified", + "additions": 2, + "deletions": 2, + "changes": 4, + "blob_url": "https://github.com/wp-cli/block-command/blob/f7dd14da742b8722877d0517326320cc5041bafb/composer.json", + "raw_url": "https://github.com/wp-cli/block-command/raw/f7dd14da742b8722877d0517326320cc5041bafb/composer.json", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/composer.json?ref=f7dd14da742b8722877d0517326320cc5041bafb", + "patch": "@@ -6,10 +6,10 @@\n \"license\": \"MIT\",\n \"authors\": [],\n \"require\": {\n- \"wp-cli/wp-cli\": \"^2.13\"\n+ \"wp-cli/wp-cli\": \"^3.0\"\n },\n \"require-dev\": {\n- \"wp-cli/extension-command\": \"^2\",\n+ \"wp-cli/extension-command\": \"^2 || ^3\",\n \"wp-cli/wp-cli-tests\": \"^5\"\n },\n \"config\": {" + } + ] + } +} diff --git a/tests/fixtures/github/issues.json b/tests/fixtures/github/issues.json new file mode 100644 index 0000000..822d17f --- /dev/null +++ b/tests/fixtures/github/issues.json @@ -0,0 +1,357 @@ +{ + "request": "GET /repos/wp-cli/block-command/issues?milestone=4&state=all", + "status": 200, + "headers": {}, + "body": [ + { + "url": "https://api.github.com/repos/wp-cli/block-command/issues/19", + "repository_url": "https://api.github.com/repos/wp-cli/block-command", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/issues/19/labels{/name}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/issues/19/comments", + "events_url": "https://api.github.com/repos/wp-cli/block-command/issues/19/events", + "html_url": "https://github.com/wp-cli/block-command/pull/19", + "id": 5310757919, + "node_id": "PR_kwDOQl0QSc8AAAABBu2hMw", + "number": 19, + "title": "Fix PHPCS errors in Behat test files", + "user": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 9802158517, + "node_id": "LA_kwDOQl0QSc8AAAACSEERtQ", + "url": "https://api.github.com/repos/wp-cli/block-command/labels/scope:testing", + "name": "scope:testing", + "color": "FEF2C0", + "default": false, + "description": "Related to testing", + "archived_at": null, + "archived_by": null + } + ], + "state": "closed", + "locked": false, + "assignees": [], + "milestone": { + "url": "https://api.github.com/repos/wp-cli/block-command/milestones/4", + "html_url": "https://github.com/wp-cli/block-command/milestone/4", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/milestones/4/labels", + "id": 17482346, + "node_id": "MI_kwDOQl0QSc4BCsJq", + "number": 4, + "title": "1.0.4", + "description": null, + "creator": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "open_issues": 0, + "closed_issues": 2, + "state": "closed", + "created_at": "2026-08-26T07:42:22Z", + "updated_at": "2026-10-30T17:02:21Z", + "due_on": null, + "closed_at": "2026-10-30T17:02:21Z" + }, + "comments": 2, + "created_at": "2026-09-01T10:34:22Z", + "updated_at": "2026-09-01T10:38:52Z", + "closed_at": "2026-09-01T10:37:24Z", + "assignee": null, + "author_association": "MEMBER", + "issue_field_values": [], + "type": null, + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/wp-cli/block-command/pulls/19", + "html_url": "https://github.com/wp-cli/block-command/pull/19", + "diff_url": "https://github.com/wp-cli/block-command/pull/19.diff", + "patch_url": "https://github.com/wp-cli/block-command/pull/19.patch", + "merged_at": "2026-09-01T10:37:24Z" + }, + "body": "\n\n\n\n## Summary by CodeRabbit\n\n* **Style**\n * Reformatted block style registration examples for improved readability.\n * No changes to functionality or expected behavior.\n\n", + "closed_by": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/wp-cli/block-command/issues/19/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/wp-cli/block-command/issues/19/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/wp-cli/block-command/issues/18", + "repository_url": "https://api.github.com/repos/wp-cli/block-command", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/issues/18/labels{/name}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/issues/18/comments", + "events_url": "https://api.github.com/repos/wp-cli/block-command/issues/18/events", + "html_url": "https://github.com/wp-cli/block-command/pull/18", + "id": 5254509215, + "node_id": "PR_kwDOQl0QSc8AAAABBCtdNg", + "number": 18, + "title": "Exclude the wp-compat false positives from the PHPStan config", + "user": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 9802158517, + "node_id": "LA_kwDOQl0QSc8AAAACSEERtQ", + "url": "https://api.github.com/repos/wp-cli/block-command/labels/scope:testing", + "name": "scope:testing", + "color": "FEF2C0", + "default": false, + "description": "Related to testing", + "archived_at": null, + "archived_by": null + } + ], + "state": "closed", + "locked": false, + "assignees": [], + "milestone": { + "url": "https://api.github.com/repos/wp-cli/block-command/milestones/4", + "html_url": "https://github.com/wp-cli/block-command/milestone/4", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/milestones/4/labels", + "id": 17482346, + "node_id": "MI_kwDOQl0QSc4BCsJq", + "number": 4, + "title": "1.0.4", + "description": null, + "creator": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "open_issues": 0, + "closed_issues": 2, + "state": "closed", + "created_at": "2026-08-26T07:42:22Z", + "updated_at": "2026-10-30T17:02:21Z", + "due_on": null, + "closed_at": "2026-10-30T17:02:21Z" + }, + "comments": 2, + "created_at": "2026-08-26T07:33:38Z", + "updated_at": "2026-08-26T07:42:22Z", + "closed_at": "2026-08-26T07:42:15Z", + "assignee": null, + "author_association": "MEMBER", + "issue_field_values": [], + "type": null, + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/wp-cli/block-command/pulls/18", + "html_url": "https://github.com/wp-cli/block-command/pull/18", + "diff_url": "https://github.com/wp-cli/block-command/pull/18.diff", + "patch_url": "https://github.com/wp-cli/block-command/pull/18.patch", + "merged_at": "2026-08-26T07:42:15Z" + }, + "body": "The `johnbillion/wp-compat` PHPStan extension that now ships with `wp-cli-tests` reported 28 errors. It checks every WordPress symbol against the WordPress 4.9 baseline that `wp-cli-tests` configures, and only recognizes `function_exists()` and `method_exists()` guards, so it cannot see the per-command `before_invoke` checks in `block-command.php`.\n\nAll 28 are false positives, and this package made that unusually easy to verify: it already gates each subcommand on its own version, so every finding can be checked against the gate of the command that actually contains it. Every symbol sits at or below its gate.\n\n| Command | Gate | Symbols flagged | Version |\n| --- | --- | --- | --- |\n| `wp block type` | 5.0 | `WP_Block_Type_Registry::get_instance()`, `::get_all_registered()`, `::get_registered()`, `::is_registered()`, `WP_Block_Type::is_dynamic()` | 5.0.0 |\n| `wp block synced-pattern` | 5.0 | `parse_blocks()` | 5.0.0 |\n| `wp block style` | 5.3 | `WP_Block_Styles_Registry::get_instance()`, `::get_all_registered()`, `::get_registered_styles_for_block()` | 5.3.0 |\n| `wp block pattern` | 5.5 | `WP_Block_Patterns_Registry::get_instance()`, `::get_all_registered()`, `::get_registered()` | 5.5.0 |\n| `wp block pattern-category` | 5.5 | `WP_Block_Pattern_Categories_Registry::get_instance()`, `::get_all_registered()`, `::get_registered()` | 5.5.0 |\n| `wp block template` | 5.9 | `get_block_template()`, `get_block_templates()` | 5.8.0 |\n| `wp block binding` | 6.5 | `get_block_bindings_source()`, `get_all_registered_block_bindings_sources()` | 6.5.0 |\n\nIgnored per file, matching on the message as well as the identifier so each entry pins the version its command is gated on. That matters here more than anywhere: with seven different gates, a file-wide `WPCompat.methodNotAvailable` ignore would let a symbol newer than any given command's gate pass unnoticed. Pinned this way, such a call still gets reported.\n\nNo source changes: nothing here is an actual compatibility problem.\n\n## Verification\n\nRun locally against the same dependency versions CI resolves (`johnbillion/wp-compat` 2.0.0, `php-stubs/wordpress-stubs` v6.9.4, `wp-cli/wp-cli-tests` v5.2.3):\n\n- `composer phpstan` — `[OK] No errors`, with no unmatched ignore entries\n- As a control, adding a deliberately dead ignore entry does produce an `ignore.unmatched` error, confirming the entries above match real errors rather than sitting there unused\n\nGitHub Actions was failing to allocate runners across the org while this was written, so CI may need a re-run once that clears.\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_01JSjnuDoRgWkj4DjRgHbtNB)_\n\n\n\n## Summary by CodeRabbit\n\n* **Chores**\n * Updated static analysis configuration to account for supported WordPress block APIs across multiple WordPress versions.\n * Reduced false-positive compatibility warnings for block-related commands.\n\n", + "closed_by": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/wp-cli/block-command/issues/18/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/wp-cli/block-command/issues/18/timeline", + "performed_via_github_app": { + "id": 1236702, + "client_id": "Iv23liqTIFEtdIu6Vn1r", + "slug": "claude", + "node_id": "A_kwHOBIuudM4AEt7e", + "owner": { + "login": "anthropics", + "id": 76263028, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjc2MjYzMDI4", + "avatar_url": "https://avatars.githubusercontent.com/u/76263028?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/anthropics", + "html_url": "https://github.com/anthropics", + "followers_url": "https://api.github.com/users/anthropics/followers", + "following_url": "https://api.github.com/users/anthropics/following{/other_user}", + "gists_url": "https://api.github.com/users/anthropics/gists{/gist_id}", + "starred_url": "https://api.github.com/users/anthropics/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/anthropics/subscriptions", + "organizations_url": "https://api.github.com/users/anthropics/orgs", + "repos_url": "https://api.github.com/users/anthropics/repos", + "events_url": "https://api.github.com/users/anthropics/events{/privacy}", + "received_events_url": "https://api.github.com/users/anthropics/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "name": "Claude", + "description": "Run Claude Code from your GitHub Pull Requests and Issues to respond to reviewer feedback, fix CI errors, or modify code, turning it into a virtual teammate that works alongside your development pipelines.\r\n\r\nThis is built on the publicly available Claude Code SDK.", + "external_url": "https://anthropic.com/claude-code", + "html_url": "https://github.com/apps/claude", + "created_at": "2025-04-30T17:54:24Z", + "updated_at": "2026-09-30T19:04:00Z", + "permissions": { + "actions": "write", + "administration": "read", + "checks": "write", + "contents": "write", + "discussions": "write", + "issues": "write", + "members": "read", + "merge_queues": "read", + "metadata": "read", + "pull_requests": "write", + "repository_hooks": "write", + "statuses": "read", + "workflows": "write" + }, + "events": [ + "branch_protection_rule", + "check_run", + "check_suite", + "commit_comment", + "discussion", + "discussion_comment", + "issues", + "issue_comment", + "merge_group", + "merge_queue_entry", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "pull_request_review_thread", + "push", + "release", + "repository_ruleset", + "status", + "sub_issues" + ] + }, + "state_reason": null + } + ] +} diff --git a/tests/fixtures/github/milestone.json b/tests/fixtures/github/milestone.json new file mode 100644 index 0000000..6066ee2 --- /dev/null +++ b/tests/fixtures/github/milestone.json @@ -0,0 +1,43 @@ +{ + "request": "GET /repos/wp-cli/block-command/milestones/4", + "status": 200, + "headers": {}, + "body": { + "url": "https://api.github.com/repos/wp-cli/block-command/milestones/4", + "html_url": "https://github.com/wp-cli/block-command/milestone/4", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/milestones/4/labels", + "id": 17482346, + "node_id": "MI_kwDOQl0QSc4BCsJq", + "number": 4, + "title": "1.0.4", + "description": null, + "creator": { + "login": "swissspidy", + "id": 841956, + "node_id": "MDQ6VXNlcjg0MTk1Ng==", + "avatar_url": "https://avatars.githubusercontent.com/u/841956?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/swissspidy", + "html_url": "https://github.com/swissspidy", + "followers_url": "https://api.github.com/users/swissspidy/followers", + "following_url": "https://api.github.com/users/swissspidy/following{/other_user}", + "gists_url": "https://api.github.com/users/swissspidy/gists{/gist_id}", + "starred_url": "https://api.github.com/users/swissspidy/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/swissspidy/subscriptions", + "organizations_url": "https://api.github.com/users/swissspidy/orgs", + "repos_url": "https://api.github.com/users/swissspidy/repos", + "events_url": "https://api.github.com/users/swissspidy/events{/privacy}", + "received_events_url": "https://api.github.com/users/swissspidy/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "open_issues": 0, + "closed_issues": 2, + "state": "closed", + "created_at": "2026-08-26T07:42:22Z", + "updated_at": "2026-10-30T17:02:21Z", + "due_on": null, + "closed_at": "2026-10-30T17:02:21Z" + } +} diff --git a/tests/fixtures/github/pull-requests.json b/tests/fixtures/github/pull-requests.json new file mode 100644 index 0000000..a7ce3cf --- /dev/null +++ b/tests/fixtures/github/pull-requests.json @@ -0,0 +1,1183 @@ +{ + "request": "GET /repos/wp-cli/block-command/pulls?state=closed&base=main&sort=created&direction=asc&per_page=3", + "status": 200, + "headers": { + "link": "; rel=\"next\", ; rel=\"last\"" + }, + "body": [ + { + "url": "https://api.github.com/repos/wp-cli/block-command/pulls/1", + "id": 3087604457, + "node_id": "PR_kwDOQl0QSc64CRrp", + "html_url": "https://github.com/wp-cli/block-command/pull/1", + "diff_url": "https://github.com/wp-cli/block-command/pull/1.diff", + "patch_url": "https://github.com/wp-cli/block-command/pull/1.patch", + "issue_url": "https://api.github.com/repos/wp-cli/block-command/issues/1", + "number": 1, + "state": "closed", + "locked": false, + "title": "Regenerate README file", + "user": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "body": "**This is an automated pull-request**\n\nRefreshes the `README.md` file with the latest changes to the docblocks in the source code.", + "created_at": "2025-12-10T01:12:23Z", + "updated_at": "2025-12-10T01:17:34Z", + "closed_at": "2025-12-10T01:17:31Z", + "merged_at": "2025-12-10T01:17:31Z", + "merge_commit_sha": "a4776755e608161d13a1e0085655cf21436572a6", + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [ + { + "id": 9801693960, + "node_id": "LA_kwDOQl0QSc8AAAACSDn7CA", + "url": "https://api.github.com/repos/wp-cli/block-command/labels/scope:documentation", + "name": "scope:documentation", + "color": "FEF2C0", + "default": false, + "description": "Related to documentation", + "archived_at": null, + "archived_by": null + } + ], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/wp-cli/block-command/pulls/1/commits", + "review_comments_url": "https://api.github.com/repos/wp-cli/block-command/pulls/1/comments", + "review_comment_url": "https://api.github.com/repos/wp-cli/block-command/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/issues/1/comments", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/0d76df9b2c8c0fc1e7285a833666d082182c78c5", + "head": { + "label": "wp-cli:regenerate-readme", + "ref": "regenerate-readme", + "sha": "0d76df9b2c8c0fc1e7285a833666d082182c78c5", + "user": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 1113395273, + "node_id": "R_kgDOQl0QSQ", + "name": "block-command", + "full_name": "wp-cli/block-command", + "private": false, + "owner": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/wp-cli/block-command", + "description": "Manages WordPress block editor blocks and related entities.", + "fork": false, + "url": "https://api.github.com/repos/wp-cli/block-command", + "forks_url": "https://api.github.com/repos/wp-cli/block-command/forks", + "keys_url": "https://api.github.com/repos/wp-cli/block-command/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/wp-cli/block-command/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/wp-cli/block-command/teams", + "hooks_url": "https://api.github.com/repos/wp-cli/block-command/hooks", + "issue_events_url": "https://api.github.com/repos/wp-cli/block-command/issues/events{/number}", + "events_url": "https://api.github.com/repos/wp-cli/block-command/events", + "assignees_url": "https://api.github.com/repos/wp-cli/block-command/assignees{/user}", + "branches_url": "https://api.github.com/repos/wp-cli/block-command/branches{/branch}", + "tags_url": "https://api.github.com/repos/wp-cli/block-command/tags", + "blobs_url": "https://api.github.com/repos/wp-cli/block-command/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/wp-cli/block-command/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/wp-cli/block-command/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/wp-cli/block-command/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/{sha}", + "languages_url": "https://api.github.com/repos/wp-cli/block-command/languages", + "stargazers_url": "https://api.github.com/repos/wp-cli/block-command/stargazers", + "contributors_url": "https://api.github.com/repos/wp-cli/block-command/contributors", + "subscribers_url": "https://api.github.com/repos/wp-cli/block-command/subscribers", + "subscription_url": "https://api.github.com/repos/wp-cli/block-command/subscription", + "commits_url": "https://api.github.com/repos/wp-cli/block-command/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/wp-cli/block-command/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/wp-cli/block-command/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/{+path}", + "compare_url": "https://api.github.com/repos/wp-cli/block-command/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/wp-cli/block-command/merges", + "archive_url": "https://api.github.com/repos/wp-cli/block-command/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/wp-cli/block-command/downloads", + "issues_url": "https://api.github.com/repos/wp-cli/block-command/issues{/number}", + "pulls_url": "https://api.github.com/repos/wp-cli/block-command/pulls{/number}", + "milestones_url": "https://api.github.com/repos/wp-cli/block-command/milestones{/number}", + "notifications_url": "https://api.github.com/repos/wp-cli/block-command/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/labels{/name}", + "releases_url": "https://api.github.com/repos/wp-cli/block-command/releases{/id}", + "deployments_url": "https://api.github.com/repos/wp-cli/block-command/deployments", + "created_at": "2025-12-09T23:20:52Z", + "updated_at": "2026-10-30T14:13:43Z", + "pushed_at": "2026-10-30T17:02:20Z", + "git_url": "git://github.com/wp-cli/block-command.git", + "ssh_url": "git@github.com:wp-cli/block-command.git", + "clone_url": "https://github.com/wp-cli/block-command.git", + "svn_url": "https://github.com/wp-cli/block-command", + "homepage": "", + "size": 107, + "stargazers_count": 2, + "watchers_count": 2, + "language": "PHP", + "has_issues": true, + "has_projects": false, + "has_downloads": false, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "has_pull_requests": true, + "pull_request_creation_policy": "all", + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 3, + "watchers": 2, + "default_branch": "main" + } + }, + "base": { + "label": "wp-cli:main", + "ref": "main", + "sha": "832785d18491746bfabf553669bf939ec5c85327", + "user": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 1113395273, + "node_id": "R_kgDOQl0QSQ", + "name": "block-command", + "full_name": "wp-cli/block-command", + "private": false, + "owner": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/wp-cli/block-command", + "description": "Manages WordPress block editor blocks and related entities.", + "fork": false, + "url": "https://api.github.com/repos/wp-cli/block-command", + "forks_url": "https://api.github.com/repos/wp-cli/block-command/forks", + "keys_url": "https://api.github.com/repos/wp-cli/block-command/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/wp-cli/block-command/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/wp-cli/block-command/teams", + "hooks_url": "https://api.github.com/repos/wp-cli/block-command/hooks", + "issue_events_url": "https://api.github.com/repos/wp-cli/block-command/issues/events{/number}", + "events_url": "https://api.github.com/repos/wp-cli/block-command/events", + "assignees_url": "https://api.github.com/repos/wp-cli/block-command/assignees{/user}", + "branches_url": "https://api.github.com/repos/wp-cli/block-command/branches{/branch}", + "tags_url": "https://api.github.com/repos/wp-cli/block-command/tags", + "blobs_url": "https://api.github.com/repos/wp-cli/block-command/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/wp-cli/block-command/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/wp-cli/block-command/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/wp-cli/block-command/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/{sha}", + "languages_url": "https://api.github.com/repos/wp-cli/block-command/languages", + "stargazers_url": "https://api.github.com/repos/wp-cli/block-command/stargazers", + "contributors_url": "https://api.github.com/repos/wp-cli/block-command/contributors", + "subscribers_url": "https://api.github.com/repos/wp-cli/block-command/subscribers", + "subscription_url": "https://api.github.com/repos/wp-cli/block-command/subscription", + "commits_url": "https://api.github.com/repos/wp-cli/block-command/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/wp-cli/block-command/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/wp-cli/block-command/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/{+path}", + "compare_url": "https://api.github.com/repos/wp-cli/block-command/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/wp-cli/block-command/merges", + "archive_url": "https://api.github.com/repos/wp-cli/block-command/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/wp-cli/block-command/downloads", + "issues_url": "https://api.github.com/repos/wp-cli/block-command/issues{/number}", + "pulls_url": "https://api.github.com/repos/wp-cli/block-command/pulls{/number}", + "milestones_url": "https://api.github.com/repos/wp-cli/block-command/milestones{/number}", + "notifications_url": "https://api.github.com/repos/wp-cli/block-command/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/labels{/name}", + "releases_url": "https://api.github.com/repos/wp-cli/block-command/releases{/id}", + "deployments_url": "https://api.github.com/repos/wp-cli/block-command/deployments", + "created_at": "2025-12-09T23:20:52Z", + "updated_at": "2026-10-30T14:13:43Z", + "pushed_at": "2026-10-30T17:02:20Z", + "git_url": "git://github.com/wp-cli/block-command.git", + "ssh_url": "git@github.com:wp-cli/block-command.git", + "clone_url": "https://github.com/wp-cli/block-command.git", + "svn_url": "https://github.com/wp-cli/block-command", + "homepage": "", + "size": 107, + "stargazers_count": 2, + "watchers_count": 2, + "language": "PHP", + "has_issues": true, + "has_projects": false, + "has_downloads": false, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "has_pull_requests": true, + "pull_request_creation_policy": "all", + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 3, + "watchers": 2, + "default_branch": "main" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/1" + }, + "html": { + "href": "https://github.com/wp-cli/block-command/pull/1" + }, + "issue": { + "href": "https://api.github.com/repos/wp-cli/block-command/issues/1" + }, + "comments": { + "href": "https://api.github.com/repos/wp-cli/block-command/issues/1/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/1/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/1/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/wp-cli/block-command/statuses/0d76df9b2c8c0fc1e7285a833666d082182c78c5" + } + }, + "author_association": "CONTRIBUTOR", + "auto_merge": null, + "assignee": null, + "active_lock_reason": null + }, + { + "url": "https://api.github.com/repos/wp-cli/block-command/pulls/2", + "id": 3087614852, + "node_id": "PR_kwDOQl0QSc64CUOE", + "html_url": "https://github.com/wp-cli/block-command/pull/2", + "diff_url": "https://github.com/wp-cli/block-command/pull/2.diff", + "patch_url": "https://github.com/wp-cli/block-command/pull/2.patch", + "issue_url": "https://api.github.com/repos/wp-cli/block-command/issues/2", + "number": 2, + "state": "closed", + "locked": false, + "title": "Turn block into a command namespace", + "user": { + "login": "schlessera", + "id": 83631, + "node_id": "MDQ6VXNlcjgzNjMx", + "avatar_url": "https://avatars.githubusercontent.com/u/83631?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/schlessera", + "html_url": "https://github.com/schlessera", + "followers_url": "https://api.github.com/users/schlessera/followers", + "following_url": "https://api.github.com/users/schlessera/following{/other_user}", + "gists_url": "https://api.github.com/users/schlessera/gists{/gist_id}", + "starred_url": "https://api.github.com/users/schlessera/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/schlessera/subscriptions", + "organizations_url": "https://api.github.com/users/schlessera/orgs", + "repos_url": "https://api.github.com/users/schlessera/repos", + "events_url": "https://api.github.com/users/schlessera/events{/privacy}", + "received_events_url": "https://api.github.com/users/schlessera/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "body": "The empty `block` command would be better as a command namespace.", + "created_at": "2025-12-10T01:19:46Z", + "updated_at": "2025-12-10T18:28:40Z", + "closed_at": "2025-12-10T11:31:10Z", + "merged_at": "2025-12-10T11:31:10Z", + "merge_commit_sha": "23d231801b00ab970f002f671aa9cbf438ade1f9", + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [ + { + "id": 9801345255, + "node_id": "LA_kwDOQl0QSc8AAAACSDSo5w", + "url": "https://api.github.com/repos/wp-cli/block-command/labels/enhancement", + "name": "enhancement", + "color": "a2eeef", + "default": true, + "description": "New feature or request", + "archived_at": null, + "archived_by": null + } + ], + "milestone": { + "url": "https://api.github.com/repos/wp-cli/block-command/milestones/1", + "html_url": "https://github.com/wp-cli/block-command/milestone/1", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/milestones/1/labels", + "id": 14316582, + "node_id": "MI_kwDOQl0QSc4A2nQm", + "number": 1, + "title": "1.0.0", + "description": null, + "creator": { + "login": "schlessera", + "id": 83631, + "node_id": "MDQ6VXNlcjgzNjMx", + "avatar_url": "https://avatars.githubusercontent.com/u/83631?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/schlessera", + "html_url": "https://github.com/schlessera", + "followers_url": "https://api.github.com/users/schlessera/followers", + "following_url": "https://api.github.com/users/schlessera/following{/other_user}", + "gists_url": "https://api.github.com/users/schlessera/gists{/gist_id}", + "starred_url": "https://api.github.com/users/schlessera/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/schlessera/subscriptions", + "organizations_url": "https://api.github.com/users/schlessera/orgs", + "repos_url": "https://api.github.com/users/schlessera/repos", + "events_url": "https://api.github.com/users/schlessera/events{/privacy}", + "received_events_url": "https://api.github.com/users/schlessera/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "open_issues": 0, + "closed_issues": 6, + "state": "closed", + "created_at": "2025-12-10T01:20:25Z", + "updated_at": "2026-03-29T10:21:22Z", + "due_on": null, + "closed_at": "2026-03-19T08:50:31Z" + }, + "draft": false, + "commits_url": "https://api.github.com/repos/wp-cli/block-command/pulls/2/commits", + "review_comments_url": "https://api.github.com/repos/wp-cli/block-command/pulls/2/comments", + "review_comment_url": "https://api.github.com/repos/wp-cli/block-command/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/issues/2/comments", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/68e9559a5a277a13e6c4c03c13252d0d3e22d344", + "head": { + "label": "wp-cli:fix/change-block-into-command-namespace", + "ref": "fix/change-block-into-command-namespace", + "sha": "68e9559a5a277a13e6c4c03c13252d0d3e22d344", + "user": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 1113395273, + "node_id": "R_kgDOQl0QSQ", + "name": "block-command", + "full_name": "wp-cli/block-command", + "private": false, + "owner": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/wp-cli/block-command", + "description": "Manages WordPress block editor blocks and related entities.", + "fork": false, + "url": "https://api.github.com/repos/wp-cli/block-command", + "forks_url": "https://api.github.com/repos/wp-cli/block-command/forks", + "keys_url": "https://api.github.com/repos/wp-cli/block-command/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/wp-cli/block-command/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/wp-cli/block-command/teams", + "hooks_url": "https://api.github.com/repos/wp-cli/block-command/hooks", + "issue_events_url": "https://api.github.com/repos/wp-cli/block-command/issues/events{/number}", + "events_url": "https://api.github.com/repos/wp-cli/block-command/events", + "assignees_url": "https://api.github.com/repos/wp-cli/block-command/assignees{/user}", + "branches_url": "https://api.github.com/repos/wp-cli/block-command/branches{/branch}", + "tags_url": "https://api.github.com/repos/wp-cli/block-command/tags", + "blobs_url": "https://api.github.com/repos/wp-cli/block-command/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/wp-cli/block-command/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/wp-cli/block-command/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/wp-cli/block-command/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/{sha}", + "languages_url": "https://api.github.com/repos/wp-cli/block-command/languages", + "stargazers_url": "https://api.github.com/repos/wp-cli/block-command/stargazers", + "contributors_url": "https://api.github.com/repos/wp-cli/block-command/contributors", + "subscribers_url": "https://api.github.com/repos/wp-cli/block-command/subscribers", + "subscription_url": "https://api.github.com/repos/wp-cli/block-command/subscription", + "commits_url": "https://api.github.com/repos/wp-cli/block-command/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/wp-cli/block-command/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/wp-cli/block-command/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/{+path}", + "compare_url": "https://api.github.com/repos/wp-cli/block-command/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/wp-cli/block-command/merges", + "archive_url": "https://api.github.com/repos/wp-cli/block-command/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/wp-cli/block-command/downloads", + "issues_url": "https://api.github.com/repos/wp-cli/block-command/issues{/number}", + "pulls_url": "https://api.github.com/repos/wp-cli/block-command/pulls{/number}", + "milestones_url": "https://api.github.com/repos/wp-cli/block-command/milestones{/number}", + "notifications_url": "https://api.github.com/repos/wp-cli/block-command/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/labels{/name}", + "releases_url": "https://api.github.com/repos/wp-cli/block-command/releases{/id}", + "deployments_url": "https://api.github.com/repos/wp-cli/block-command/deployments", + "created_at": "2025-12-09T23:20:52Z", + "updated_at": "2026-10-30T14:13:43Z", + "pushed_at": "2026-10-30T17:02:20Z", + "git_url": "git://github.com/wp-cli/block-command.git", + "ssh_url": "git@github.com:wp-cli/block-command.git", + "clone_url": "https://github.com/wp-cli/block-command.git", + "svn_url": "https://github.com/wp-cli/block-command", + "homepage": "", + "size": 107, + "stargazers_count": 2, + "watchers_count": 2, + "language": "PHP", + "has_issues": true, + "has_projects": false, + "has_downloads": false, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "has_pull_requests": true, + "pull_request_creation_policy": "all", + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 3, + "watchers": 2, + "default_branch": "main" + } + }, + "base": { + "label": "wp-cli:main", + "ref": "main", + "sha": "a969b308a1bb4171df09463f4fbfa0abc3498a18", + "user": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 1113395273, + "node_id": "R_kgDOQl0QSQ", + "name": "block-command", + "full_name": "wp-cli/block-command", + "private": false, + "owner": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/wp-cli/block-command", + "description": "Manages WordPress block editor blocks and related entities.", + "fork": false, + "url": "https://api.github.com/repos/wp-cli/block-command", + "forks_url": "https://api.github.com/repos/wp-cli/block-command/forks", + "keys_url": "https://api.github.com/repos/wp-cli/block-command/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/wp-cli/block-command/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/wp-cli/block-command/teams", + "hooks_url": "https://api.github.com/repos/wp-cli/block-command/hooks", + "issue_events_url": "https://api.github.com/repos/wp-cli/block-command/issues/events{/number}", + "events_url": "https://api.github.com/repos/wp-cli/block-command/events", + "assignees_url": "https://api.github.com/repos/wp-cli/block-command/assignees{/user}", + "branches_url": "https://api.github.com/repos/wp-cli/block-command/branches{/branch}", + "tags_url": "https://api.github.com/repos/wp-cli/block-command/tags", + "blobs_url": "https://api.github.com/repos/wp-cli/block-command/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/wp-cli/block-command/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/wp-cli/block-command/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/wp-cli/block-command/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/{sha}", + "languages_url": "https://api.github.com/repos/wp-cli/block-command/languages", + "stargazers_url": "https://api.github.com/repos/wp-cli/block-command/stargazers", + "contributors_url": "https://api.github.com/repos/wp-cli/block-command/contributors", + "subscribers_url": "https://api.github.com/repos/wp-cli/block-command/subscribers", + "subscription_url": "https://api.github.com/repos/wp-cli/block-command/subscription", + "commits_url": "https://api.github.com/repos/wp-cli/block-command/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/wp-cli/block-command/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/wp-cli/block-command/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/{+path}", + "compare_url": "https://api.github.com/repos/wp-cli/block-command/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/wp-cli/block-command/merges", + "archive_url": "https://api.github.com/repos/wp-cli/block-command/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/wp-cli/block-command/downloads", + "issues_url": "https://api.github.com/repos/wp-cli/block-command/issues{/number}", + "pulls_url": "https://api.github.com/repos/wp-cli/block-command/pulls{/number}", + "milestones_url": "https://api.github.com/repos/wp-cli/block-command/milestones{/number}", + "notifications_url": "https://api.github.com/repos/wp-cli/block-command/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/labels{/name}", + "releases_url": "https://api.github.com/repos/wp-cli/block-command/releases{/id}", + "deployments_url": "https://api.github.com/repos/wp-cli/block-command/deployments", + "created_at": "2025-12-09T23:20:52Z", + "updated_at": "2026-10-30T14:13:43Z", + "pushed_at": "2026-10-30T17:02:20Z", + "git_url": "git://github.com/wp-cli/block-command.git", + "ssh_url": "git@github.com:wp-cli/block-command.git", + "clone_url": "https://github.com/wp-cli/block-command.git", + "svn_url": "https://github.com/wp-cli/block-command", + "homepage": "", + "size": 107, + "stargazers_count": 2, + "watchers_count": 2, + "language": "PHP", + "has_issues": true, + "has_projects": false, + "has_downloads": false, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "has_pull_requests": true, + "pull_request_creation_policy": "all", + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 3, + "watchers": 2, + "default_branch": "main" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/2" + }, + "html": { + "href": "https://github.com/wp-cli/block-command/pull/2" + }, + "issue": { + "href": "https://api.github.com/repos/wp-cli/block-command/issues/2" + }, + "comments": { + "href": "https://api.github.com/repos/wp-cli/block-command/issues/2/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/2/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/2/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/wp-cli/block-command/statuses/68e9559a5a277a13e6c4c03c13252d0d3e22d344" + } + }, + "author_association": "MEMBER", + "auto_merge": null, + "assignee": null, + "active_lock_reason": null + }, + { + "url": "https://api.github.com/repos/wp-cli/block-command/pulls/3", + "id": 3087809135, + "node_id": "PR_kwDOQl0QSc64DDpv", + "html_url": "https://github.com/wp-cli/block-command/pull/3", + "diff_url": "https://github.com/wp-cli/block-command/pull/3.diff", + "patch_url": "https://github.com/wp-cli/block-command/pull/3.patch", + "issue_url": "https://api.github.com/repos/wp-cli/block-command/issues/3", + "number": 3, + "state": "closed", + "locked": false, + "title": "Add more edge case testing", + "user": { + "login": "schlessera", + "id": 83631, + "node_id": "MDQ6VXNlcjgzNjMx", + "avatar_url": "https://avatars.githubusercontent.com/u/83631?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/schlessera", + "html_url": "https://github.com/schlessera", + "followers_url": "https://api.github.com/users/schlessera/followers", + "following_url": "https://api.github.com/users/schlessera/following{/other_user}", + "gists_url": "https://api.github.com/users/schlessera/gists{/gist_id}", + "starred_url": "https://api.github.com/users/schlessera/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/schlessera/subscriptions", + "organizations_url": "https://api.github.com/users/schlessera/orgs", + "repos_url": "https://api.github.com/users/schlessera/repos", + "events_url": "https://api.github.com/users/schlessera/events{/privacy}", + "received_events_url": "https://api.github.com/users/schlessera/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "body": "This PR hardens a few test cases and adds more test coverage.", + "created_at": "2025-12-10T03:31:58Z", + "updated_at": "2025-12-10T18:29:04Z", + "closed_at": "2025-12-10T10:22:01Z", + "merged_at": "2025-12-10T10:22:01Z", + "merge_commit_sha": "75d2e082e8e6ff06a0a989fe312e43750b606aa4", + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [ + { + "id": 9801345255, + "node_id": "LA_kwDOQl0QSc8AAAACSDSo5w", + "url": "https://api.github.com/repos/wp-cli/block-command/labels/enhancement", + "name": "enhancement", + "color": "a2eeef", + "default": true, + "description": "New feature or request", + "archived_at": null, + "archived_by": null + }, + { + "id": 9802158517, + "node_id": "LA_kwDOQl0QSc8AAAACSEERtQ", + "url": "https://api.github.com/repos/wp-cli/block-command/labels/scope:testing", + "name": "scope:testing", + "color": "FEF2C0", + "default": false, + "description": "Related to testing", + "archived_at": null, + "archived_by": null + } + ], + "milestone": { + "url": "https://api.github.com/repos/wp-cli/block-command/milestones/1", + "html_url": "https://github.com/wp-cli/block-command/milestone/1", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/milestones/1/labels", + "id": 14316582, + "node_id": "MI_kwDOQl0QSc4A2nQm", + "number": 1, + "title": "1.0.0", + "description": null, + "creator": { + "login": "schlessera", + "id": 83631, + "node_id": "MDQ6VXNlcjgzNjMx", + "avatar_url": "https://avatars.githubusercontent.com/u/83631?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/schlessera", + "html_url": "https://github.com/schlessera", + "followers_url": "https://api.github.com/users/schlessera/followers", + "following_url": "https://api.github.com/users/schlessera/following{/other_user}", + "gists_url": "https://api.github.com/users/schlessera/gists{/gist_id}", + "starred_url": "https://api.github.com/users/schlessera/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/schlessera/subscriptions", + "organizations_url": "https://api.github.com/users/schlessera/orgs", + "repos_url": "https://api.github.com/users/schlessera/repos", + "events_url": "https://api.github.com/users/schlessera/events{/privacy}", + "received_events_url": "https://api.github.com/users/schlessera/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "open_issues": 0, + "closed_issues": 6, + "state": "closed", + "created_at": "2025-12-10T01:20:25Z", + "updated_at": "2026-03-29T10:21:22Z", + "due_on": null, + "closed_at": "2026-03-19T08:50:31Z" + }, + "draft": false, + "commits_url": "https://api.github.com/repos/wp-cli/block-command/pulls/3/commits", + "review_comments_url": "https://api.github.com/repos/wp-cli/block-command/pulls/3/comments", + "review_comment_url": "https://api.github.com/repos/wp-cli/block-command/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/issues/3/comments", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/a7e9371531e086bd43c7197e8a0f2ffcf4d08943", + "head": { + "label": "wp-cli:add/more-edge-case-testing", + "ref": "add/more-edge-case-testing", + "sha": "a7e9371531e086bd43c7197e8a0f2ffcf4d08943", + "user": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 1113395273, + "node_id": "R_kgDOQl0QSQ", + "name": "block-command", + "full_name": "wp-cli/block-command", + "private": false, + "owner": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/wp-cli/block-command", + "description": "Manages WordPress block editor blocks and related entities.", + "fork": false, + "url": "https://api.github.com/repos/wp-cli/block-command", + "forks_url": "https://api.github.com/repos/wp-cli/block-command/forks", + "keys_url": "https://api.github.com/repos/wp-cli/block-command/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/wp-cli/block-command/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/wp-cli/block-command/teams", + "hooks_url": "https://api.github.com/repos/wp-cli/block-command/hooks", + "issue_events_url": "https://api.github.com/repos/wp-cli/block-command/issues/events{/number}", + "events_url": "https://api.github.com/repos/wp-cli/block-command/events", + "assignees_url": "https://api.github.com/repos/wp-cli/block-command/assignees{/user}", + "branches_url": "https://api.github.com/repos/wp-cli/block-command/branches{/branch}", + "tags_url": "https://api.github.com/repos/wp-cli/block-command/tags", + "blobs_url": "https://api.github.com/repos/wp-cli/block-command/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/wp-cli/block-command/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/wp-cli/block-command/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/wp-cli/block-command/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/{sha}", + "languages_url": "https://api.github.com/repos/wp-cli/block-command/languages", + "stargazers_url": "https://api.github.com/repos/wp-cli/block-command/stargazers", + "contributors_url": "https://api.github.com/repos/wp-cli/block-command/contributors", + "subscribers_url": "https://api.github.com/repos/wp-cli/block-command/subscribers", + "subscription_url": "https://api.github.com/repos/wp-cli/block-command/subscription", + "commits_url": "https://api.github.com/repos/wp-cli/block-command/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/wp-cli/block-command/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/wp-cli/block-command/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/{+path}", + "compare_url": "https://api.github.com/repos/wp-cli/block-command/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/wp-cli/block-command/merges", + "archive_url": "https://api.github.com/repos/wp-cli/block-command/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/wp-cli/block-command/downloads", + "issues_url": "https://api.github.com/repos/wp-cli/block-command/issues{/number}", + "pulls_url": "https://api.github.com/repos/wp-cli/block-command/pulls{/number}", + "milestones_url": "https://api.github.com/repos/wp-cli/block-command/milestones{/number}", + "notifications_url": "https://api.github.com/repos/wp-cli/block-command/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/labels{/name}", + "releases_url": "https://api.github.com/repos/wp-cli/block-command/releases{/id}", + "deployments_url": "https://api.github.com/repos/wp-cli/block-command/deployments", + "created_at": "2025-12-09T23:20:52Z", + "updated_at": "2026-10-30T14:13:43Z", + "pushed_at": "2026-10-30T17:02:20Z", + "git_url": "git://github.com/wp-cli/block-command.git", + "ssh_url": "git@github.com:wp-cli/block-command.git", + "clone_url": "https://github.com/wp-cli/block-command.git", + "svn_url": "https://github.com/wp-cli/block-command", + "homepage": "", + "size": 107, + "stargazers_count": 2, + "watchers_count": 2, + "language": "PHP", + "has_issues": true, + "has_projects": false, + "has_downloads": false, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "has_pull_requests": true, + "pull_request_creation_policy": "all", + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 3, + "watchers": 2, + "default_branch": "main" + } + }, + "base": { + "label": "wp-cli:main", + "ref": "main", + "sha": "a4776755e608161d13a1e0085655cf21436572a6", + "user": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "repo": { + "id": 1113395273, + "node_id": "R_kgDOQl0QSQ", + "name": "block-command", + "full_name": "wp-cli/block-command", + "private": false, + "owner": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/wp-cli/block-command", + "description": "Manages WordPress block editor blocks and related entities.", + "fork": false, + "url": "https://api.github.com/repos/wp-cli/block-command", + "forks_url": "https://api.github.com/repos/wp-cli/block-command/forks", + "keys_url": "https://api.github.com/repos/wp-cli/block-command/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/wp-cli/block-command/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/wp-cli/block-command/teams", + "hooks_url": "https://api.github.com/repos/wp-cli/block-command/hooks", + "issue_events_url": "https://api.github.com/repos/wp-cli/block-command/issues/events{/number}", + "events_url": "https://api.github.com/repos/wp-cli/block-command/events", + "assignees_url": "https://api.github.com/repos/wp-cli/block-command/assignees{/user}", + "branches_url": "https://api.github.com/repos/wp-cli/block-command/branches{/branch}", + "tags_url": "https://api.github.com/repos/wp-cli/block-command/tags", + "blobs_url": "https://api.github.com/repos/wp-cli/block-command/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/wp-cli/block-command/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/wp-cli/block-command/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/wp-cli/block-command/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/{sha}", + "languages_url": "https://api.github.com/repos/wp-cli/block-command/languages", + "stargazers_url": "https://api.github.com/repos/wp-cli/block-command/stargazers", + "contributors_url": "https://api.github.com/repos/wp-cli/block-command/contributors", + "subscribers_url": "https://api.github.com/repos/wp-cli/block-command/subscribers", + "subscription_url": "https://api.github.com/repos/wp-cli/block-command/subscription", + "commits_url": "https://api.github.com/repos/wp-cli/block-command/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/wp-cli/block-command/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/wp-cli/block-command/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/{+path}", + "compare_url": "https://api.github.com/repos/wp-cli/block-command/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/wp-cli/block-command/merges", + "archive_url": "https://api.github.com/repos/wp-cli/block-command/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/wp-cli/block-command/downloads", + "issues_url": "https://api.github.com/repos/wp-cli/block-command/issues{/number}", + "pulls_url": "https://api.github.com/repos/wp-cli/block-command/pulls{/number}", + "milestones_url": "https://api.github.com/repos/wp-cli/block-command/milestones{/number}", + "notifications_url": "https://api.github.com/repos/wp-cli/block-command/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/labels{/name}", + "releases_url": "https://api.github.com/repos/wp-cli/block-command/releases{/id}", + "deployments_url": "https://api.github.com/repos/wp-cli/block-command/deployments", + "created_at": "2025-12-09T23:20:52Z", + "updated_at": "2026-10-30T14:13:43Z", + "pushed_at": "2026-10-30T17:02:20Z", + "git_url": "git://github.com/wp-cli/block-command.git", + "ssh_url": "git@github.com:wp-cli/block-command.git", + "clone_url": "https://github.com/wp-cli/block-command.git", + "svn_url": "https://github.com/wp-cli/block-command", + "homepage": "", + "size": 107, + "stargazers_count": 2, + "watchers_count": 2, + "language": "PHP", + "has_issues": true, + "has_projects": false, + "has_downloads": false, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "has_pull_requests": true, + "pull_request_creation_policy": "all", + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 3, + "watchers": 2, + "default_branch": "main" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/3" + }, + "html": { + "href": "https://github.com/wp-cli/block-command/pull/3" + }, + "issue": { + "href": "https://api.github.com/repos/wp-cli/block-command/issues/3" + }, + "comments": { + "href": "https://api.github.com/repos/wp-cli/block-command/issues/3/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/3/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/wp-cli/block-command/pulls/3/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/wp-cli/block-command/statuses/a7e9371531e086bd43c7197e8a0f2ffcf4d08943" + } + }, + "author_association": "MEMBER", + "auto_merge": null, + "assignee": null, + "active_lock_reason": null + } + ] +} diff --git a/tests/fixtures/github/release-absent.json b/tests/fixtures/github/release-absent.json new file mode 100644 index 0000000..2058dca --- /dev/null +++ b/tests/fixtures/github/release-absent.json @@ -0,0 +1,10 @@ +{ + "request": "GET /repos/wp-cli/block-command/releases/tags/v0.0.0", + "status": 404, + "headers": {}, + "body": { + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/releases/releases#get-a-release-by-tag-name", + "status": "404" + } +} diff --git a/tests/fixtures/github/release.json b/tests/fixtures/github/release.json new file mode 100644 index 0000000..690b72c --- /dev/null +++ b/tests/fixtures/github/release.json @@ -0,0 +1,47 @@ +{ + "request": "GET /repos/wp-cli/block-command/releases/tags/v1.0.4", + "status": 200, + "headers": {}, + "body": { + "url": "https://api.github.com/repos/wp-cli/block-command/releases/429284756", + "assets_url": "https://api.github.com/repos/wp-cli/block-command/releases/429284756/assets", + "upload_url": "https://uploads.github.com/repos/wp-cli/block-command/releases/429284756/assets{?name,label}", + "html_url": "https://github.com/wp-cli/block-command/releases/tag/v1.0.4", + "id": 429284756, + "author": { + "login": "schlessera", + "id": 83631, + "node_id": "MDQ6VXNlcjgzNjMx", + "avatar_url": "https://avatars.githubusercontent.com/u/83631?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/schlessera", + "html_url": "https://github.com/schlessera", + "followers_url": "https://api.github.com/users/schlessera/followers", + "following_url": "https://api.github.com/users/schlessera/following{/other_user}", + "gists_url": "https://api.github.com/users/schlessera/gists{/gist_id}", + "starred_url": "https://api.github.com/users/schlessera/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/schlessera/subscriptions", + "organizations_url": "https://api.github.com/users/schlessera/orgs", + "repos_url": "https://api.github.com/users/schlessera/repos", + "events_url": "https://api.github.com/users/schlessera/events{/privacy}", + "received_events_url": "https://api.github.com/users/schlessera/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "node_id": "RE_kwDOQl0QSc4Zll2U", + "tag_name": "v1.0.4", + "target_commitish": "main", + "name": "Version 1.0.4", + "draft": false, + "immutable": false, + "prerelease": false, + "created_at": "2026-10-30T14:13:37Z", + "updated_at": "2026-10-30T17:02:20Z", + "published_at": "2026-10-30T17:02:20Z", + "assets": [], + "tarball_url": "https://api.github.com/repos/wp-cli/block-command/tarball/v1.0.4", + "zipball_url": "https://api.github.com/repos/wp-cli/block-command/zipball/v1.0.4", + "body": "- Fix PHPCS errors in Behat test files [[#19](https://github.com/wp-cli/block-command/pull/19)]\n- Exclude the wp-compat false positives from the PHPStan config [[#18](https://github.com/wp-cli/block-command/pull/18)]\n" + } +} diff --git a/tests/fixtures/github/repository.json b/tests/fixtures/github/repository.json new file mode 100644 index 0000000..16ad82a --- /dev/null +++ b/tests/fixtures/github/repository.json @@ -0,0 +1,174 @@ +{ + "request": "GET /repos/wp-cli/block-command", + "status": 200, + "headers": {}, + "body": { + "id": 1113395273, + "node_id": "R_kgDOQl0QSQ", + "name": "block-command", + "full_name": "wp-cli/block-command", + "private": false, + "owner": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/wp-cli/block-command", + "description": "Manages WordPress block editor blocks and related entities.", + "fork": false, + "url": "https://api.github.com/repos/wp-cli/block-command", + "forks_url": "https://api.github.com/repos/wp-cli/block-command/forks", + "keys_url": "https://api.github.com/repos/wp-cli/block-command/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/wp-cli/block-command/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/wp-cli/block-command/teams", + "hooks_url": "https://api.github.com/repos/wp-cli/block-command/hooks", + "issue_events_url": "https://api.github.com/repos/wp-cli/block-command/issues/events{/number}", + "events_url": "https://api.github.com/repos/wp-cli/block-command/events", + "assignees_url": "https://api.github.com/repos/wp-cli/block-command/assignees{/user}", + "branches_url": "https://api.github.com/repos/wp-cli/block-command/branches{/branch}", + "tags_url": "https://api.github.com/repos/wp-cli/block-command/tags", + "blobs_url": "https://api.github.com/repos/wp-cli/block-command/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/wp-cli/block-command/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/wp-cli/block-command/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/wp-cli/block-command/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/wp-cli/block-command/statuses/{sha}", + "languages_url": "https://api.github.com/repos/wp-cli/block-command/languages", + "stargazers_url": "https://api.github.com/repos/wp-cli/block-command/stargazers", + "contributors_url": "https://api.github.com/repos/wp-cli/block-command/contributors", + "subscribers_url": "https://api.github.com/repos/wp-cli/block-command/subscribers", + "subscription_url": "https://api.github.com/repos/wp-cli/block-command/subscription", + "commits_url": "https://api.github.com/repos/wp-cli/block-command/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/wp-cli/block-command/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/wp-cli/block-command/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/wp-cli/block-command/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/wp-cli/block-command/contents/{+path}", + "compare_url": "https://api.github.com/repos/wp-cli/block-command/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/wp-cli/block-command/merges", + "archive_url": "https://api.github.com/repos/wp-cli/block-command/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/wp-cli/block-command/downloads", + "issues_url": "https://api.github.com/repos/wp-cli/block-command/issues{/number}", + "pulls_url": "https://api.github.com/repos/wp-cli/block-command/pulls{/number}", + "milestones_url": "https://api.github.com/repos/wp-cli/block-command/milestones{/number}", + "notifications_url": "https://api.github.com/repos/wp-cli/block-command/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/wp-cli/block-command/labels{/name}", + "releases_url": "https://api.github.com/repos/wp-cli/block-command/releases{/id}", + "deployments_url": "https://api.github.com/repos/wp-cli/block-command/deployments", + "created_at": "2025-12-09T23:20:52Z", + "updated_at": "2026-10-30T14:13:43Z", + "pushed_at": "2026-10-30T17:02:20Z", + "git_url": "git://github.com/wp-cli/block-command.git", + "ssh_url": "git@github.com:wp-cli/block-command.git", + "clone_url": "https://github.com/wp-cli/block-command.git", + "svn_url": "https://github.com/wp-cli/block-command", + "homepage": "", + "size": 107, + "stargazers_count": 2, + "watchers_count": 2, + "language": "PHP", + "has_issues": true, + "has_projects": false, + "has_downloads": false, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "has_pull_requests": true, + "pull_request_creation_policy": "all", + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 3, + "watchers": 2, + "default_branch": "main", + "permissions": { + "admin": true, + "maintain": true, + "push": true, + "triage": true, + "pull": true + }, + "temp_clone_token": "", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": false, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "custom_properties": {}, + "organization": { + "login": "wp-cli", + "id": 1570774, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE1NzA3NzQ=", + "avatar_url": "https://avatars.githubusercontent.com/u/1570774?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/wp-cli", + "html_url": "https://github.com/wp-cli", + "followers_url": "https://api.github.com/users/wp-cli/followers", + "following_url": "https://api.github.com/users/wp-cli/following{/other_user}", + "gists_url": "https://api.github.com/users/wp-cli/gists{/gist_id}", + "starred_url": "https://api.github.com/users/wp-cli/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/wp-cli/subscriptions", + "organizations_url": "https://api.github.com/users/wp-cli/orgs", + "repos_url": "https://api.github.com/users/wp-cli/repos", + "events_url": "https://api.github.com/users/wp-cli/events{/privacy}", + "received_events_url": "https://api.github.com/users/wp-cli/received_events", + "type": "Organization", + "user_view_type": "public", + "site_admin": false + }, + "security_and_analysis": { + "secret_scanning": { + "status": "disabled" + }, + "secret_scanning_push_protection": { + "status": "disabled" + }, + "dependabot_security_updates": { + "status": "disabled" + }, + "secret_scanning_non_provider_patterns": { + "status": "disabled" + }, + "secret_scanning_validity_checks": { + "status": "disabled" + } + }, + "network_count": 1, + "subscribers_count": 1 + } +} diff --git a/tests/fixtures/github/tags-page-1.json b/tests/fixtures/github/tags-page-1.json new file mode 100644 index 0000000..4a210eb --- /dev/null +++ b/tests/fixtures/github/tags-page-1.json @@ -0,0 +1,29 @@ +{ + "request": "GET /repos/wp-cli/block-command/tags?per_page=2", + "status": 200, + "headers": { + "link": "; rel=\"next\", ; rel=\"last\"" + }, + "body": [ + { + "name": "v1.0.4", + "zipball_url": "https://api.github.com/repos/wp-cli/block-command/zipball/refs/tags/v1.0.4", + "tarball_url": "https://api.github.com/repos/wp-cli/block-command/tarball/refs/tags/v1.0.4", + "commit": { + "sha": "f0c08ac2d65c663a30715b16d6f82b358d676fd0", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/f0c08ac2d65c663a30715b16d6f82b358d676fd0" + }, + "node_id": "REF_kwDOQl0QSbByZWZzL3RhZ3MvdjEuMC40" + }, + { + "name": "v1.0.3", + "zipball_url": "https://api.github.com/repos/wp-cli/block-command/zipball/refs/tags/v1.0.3", + "tarball_url": "https://api.github.com/repos/wp-cli/block-command/tarball/refs/tags/v1.0.3", + "commit": { + "sha": "f7dd14da742b8722877d0517326320cc5041bafb", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/f7dd14da742b8722877d0517326320cc5041bafb" + }, + "node_id": "REF_kwDOQl0QSbByZWZzL3RhZ3MvdjEuMC4z" + } + ] +} diff --git a/tests/fixtures/github/tags-page-2.json b/tests/fixtures/github/tags-page-2.json new file mode 100644 index 0000000..dc30b0d --- /dev/null +++ b/tests/fixtures/github/tags-page-2.json @@ -0,0 +1,29 @@ +{ + "request": "GET /repos/wp-cli/block-command/tags?per_page=2&page=2", + "status": 200, + "headers": { + "link": "; rel=\"prev\", ; rel=\"next\", ; rel=\"last\", ; rel=\"first\"" + }, + "body": [ + { + "name": "v1.0.2", + "zipball_url": "https://api.github.com/repos/wp-cli/block-command/zipball/refs/tags/v1.0.2", + "tarball_url": "https://api.github.com/repos/wp-cli/block-command/tarball/refs/tags/v1.0.2", + "commit": { + "sha": "ea751d8a375ce8ef6e0848441b993e833fbe8911", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/ea751d8a375ce8ef6e0848441b993e833fbe8911" + }, + "node_id": "REF_kwDOQl0QSbByZWZzL3RhZ3MvdjEuMC4y" + }, + { + "name": "v1.0.1", + "zipball_url": "https://api.github.com/repos/wp-cli/block-command/zipball/refs/tags/v1.0.1", + "tarball_url": "https://api.github.com/repos/wp-cli/block-command/tarball/refs/tags/v1.0.1", + "commit": { + "sha": "259b8da86f84a386e262189be3f4b912183f563d", + "url": "https://api.github.com/repos/wp-cli/block-command/commits/259b8da86f84a386e262189be3f4b912183f563d" + }, + "node_id": "REF_kwDOQl0QSbByZWZzL3RhZ3MvdjEuMC4x" + } + ] +} diff --git a/tests/fixtures/github/update.php b/tests/fixtures/github/update.php new file mode 100644 index 0000000..6aba7f6 --- /dev/null +++ b/tests/fixtures/github/update.php @@ -0,0 +1,100 @@ + 'repos/wp-cli/block-command', + 'tags-page-1' => 'repos/wp-cli/block-command/tags?per_page=2', + 'tags-page-2' => 'repos/wp-cli/block-command/tags?per_page=2&page=2', + 'milestone' => 'repos/wp-cli/block-command/milestones/4', + 'issues' => 'repos/wp-cli/block-command/issues?milestone=4&state=all', + 'pull-requests' => 'repos/wp-cli/block-command/pulls?state=closed&base=main&sort=created&direction=asc&per_page=3', + 'comparison' => 'repos/wp-cli/block-command/compare/v1.0.1...v1.0.3', + 'release' => 'repos/wp-cli/block-command/releases/tags/v1.0.4', + 'release-absent' => 'repos/wp-cli/block-command/releases/tags/v0.0.0', +]; + +/** + * Headers worth keeping. The rest change on every request. + */ +const KEPT_HEADERS = [ 'link' ]; + +$token = trim( (string) shell_exec( 'gh auth token --hostname github.com 2>/dev/null' ) ); +if ( '' === $token ) { + $token = (string) ( getenv( 'GH_TOKEN' ) ?: getenv( 'GITHUB_TOKEN' ) ); +} + +if ( '' === $token ) { + fwrite( STDERR, "No GitHub credentials found. Log in with `gh auth login`, or set GH_TOKEN or GITHUB_TOKEN.\n" ); + exit( 1 ); +} + +/** + * Statuses the recordings must have. Anything else, such as an expired token, + * leaves the existing fixture untouched. + */ +const EXPECTED_STATUSES = [ 'release-absent' => 404 ]; + +foreach ( FIXTURES as $name => $path ) { + $headers = []; + $curl = curl_init( "https://api.github.com/{$path}" ); + curl_setopt_array( + $curl, + [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Accept: application/vnd.github+json', + "Authorization: Bearer {$token}", + 'User-Agent: wp-cli-dev test fixtures', + 'X-GitHub-Api-Version: 2022-11-28', + ], + CURLOPT_HEADERFUNCTION => static function ( $curl, $line ) use ( &$headers ) { + $parts = explode( ':', $line, 2 ); + if ( 2 === count( $parts ) && in_array( strtolower( trim( $parts[0] ) ), KEPT_HEADERS, true ) ) { + $headers[ strtolower( trim( $parts[0] ) ) ] = trim( $parts[1] ); + } + return strlen( $line ); + }, + ] + ); + + $body = curl_exec( $curl ); + $status = (int) curl_getinfo( $curl, CURLINFO_RESPONSE_CODE ); + $error = curl_error( $curl ); + + $expected = array_key_exists( $name, EXPECTED_STATUSES ) ? EXPECTED_STATUSES[ $name ] : 200; + + if ( ! is_string( $body ) || $expected !== $status ) { + fwrite( STDERR, "Could not record {$name}.json: GET /{$path} returned " . ( $status ?: $error ) . " instead of {$expected}. The fixture is unchanged.\n" ); + exit( 1 ); + } + + $fixture = [ + 'request' => "GET /{$path}", + 'status' => $status, + 'headers' => (object) $headers, + 'body' => json_decode( $body ), + ]; + + file_put_contents( + __DIR__ . "/{$name}.json", + json_encode( $fixture, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . "\n" + ); + + echo "Recorded {$name}.json ({$status})\n"; +} diff --git a/tests/includes/Fake_Transport.php b/tests/includes/Fake_Transport.php new file mode 100644 index 0000000..b22b64d --- /dev/null +++ b/tests/includes/Fake_Transport.php @@ -0,0 +1,52 @@ + + */ + protected $sent_data = []; + + /** + * Output of WP_CLI::log() and friends during the test. + * + * @var Execution + */ + protected $logger; + + /** + * Registered responses, matched in order. + * + * @var array + */ + private $responses = []; + + /** + * @var array + */ + private $original_statics = []; + + public static function set_up_before_class(): void { + parent::set_up_before_class(); + + // The HTTP layer of WP-CLI is not part of its Composer autoloader. + $wp_cli_root = dirname( (string) ( new ReflectionClass( WP_CLI::class ) )->getFileName(), 2 ); + require_once "{$wp_cli_root}/php/utils.php"; + require_once "{$wp_cli_root}/bundle/rmccue/requests/src/Autoload.php"; + \WpOrg\Requests\Autoload::register(); + + WP_CLI::add_hook( 'http_request_options', [ Fake_Transport::class, 'use_for_request' ] ); + } + + protected function set_up(): void { + parent::set_up(); + + Fake_Transport::$handler = [ $this, 'handle_request' ]; + + $this->logger = new Execution(); + // Loggers read the --debug flag from the runner's configuration, which + // only a real command run fills in. + $runner = WP_CLI::get_runner(); + $config = new ReflectionProperty( $runner, 'config' ); + if ( PHP_VERSION_ID < 80100 ) { + $config->setAccessible( true ); + } + $config->setValue( $runner, array_merge( [ 'debug' => false ], (array) $config->getValue( $runner ) ) ); + $this->swap_static( WP_CLI::class, 'logger', $this->logger ); + $this->swap_static( WP_CLI::class, 'capture_exit', true ); + $this->swap_static( + GitHub::class, + 'auth', + [ + 'token' => 'test-token', + 'source' => 'GH_TOKEN', + ] + ); + $this->swap_static( GitHub::class, 'write_interval', null ); + $this->swap_static( GitHub::class, 'last_write', 0.0 ); + GitHub::set_write_interval( 0 ); + } + + protected function tear_down(): void { + foreach ( $this->original_statics as $key => $value ) { + list( $class_name, $property ) = explode( '::', $key ); + $this->static_property( $class_name, $property )->setValue( null, $value ); + } + + Fake_Transport::$handler = null; + + parent::tear_down(); + } + + /** + * Registers the response to a request. + * + * @param string $method HTTP method. + * @param string $path Path below the API root, with any query arguments + * the request must contain. + * @param mixed $body Response body, encoded as JSON. + * @param int $status HTTP status. + * @param array $headers Response headers. + */ + protected function respond( $method, $path, $body, $status = 200, array $headers = [] ) { + $parts = explode( '?', $path, 2 ); + parse_str( isset( $parts[1] ) ? $parts[1] : '', $query ); + + $this->responses[] = [ + 'method' => $method, + 'path' => '/' . $parts[0], + 'query' => $query, + 'status' => $status, + 'headers' => $headers, + 'body' => $body, + ]; + } + + /** + * Gets the body of a recorded response. + * + * @param string $name Fixture name, see tests/fixtures/github/update.php. + * + * @return mixed Decoded JSON. + */ + protected function fixture( $name ) { + $fixture = json_decode( (string) file_get_contents( dirname( __DIR__ ) . "/fixtures/github/{$name}.json" ) ); + + return $fixture->body; + } + + /** + * Takes a recorded object and overrides some of its fields. + * + * @param string $name Fixture name. + * @param int $index Index of the object, for fixtures holding a list. + * @param array $overrides Field => value, objects given as arrays. + * + * @return object + */ + protected function fixture_object( $name, $index, array $overrides ) { + $body = $this->fixture( $name ); + $object = null === $index ? $body : $body[ $index ]; + + foreach ( $overrides as $field => $value ) { + $object->$field = is_array( $value ) ? json_decode( (string) json_encode( $value ) ) : $value; + } + + return $object; + } + + /** + * Answers a request from the fake transport. + * + * @param string $method + * @param string $url + * @param mixed $data + * + * @return string Raw HTTP response. + */ + public function handle_request( $method, $url, $data ) { + $path = (string) parse_url( $url, PHP_URL_PATH ); + parse_str( (string) parse_url( $url, PHP_URL_QUERY ), $query ); + + // Requests hands the arguments of a GET request over separately. + if ( 'GET' === $method && is_array( $data ) && $data ) { + $query = array_merge( $query, $data ); + $url = strtok( $url, '?' ) . '?' . http_build_query( $query ); + $data = null; + } + + $this->requests[] = "{$method} {$url}"; + $this->sent_data[] = is_string( $data ) ? json_decode( $data ) : $data; + + foreach ( $this->responses as $response ) { + if ( $response['method'] !== $method || $response['path'] !== $path ) { + continue; + } + + if ( array_intersect_assoc( $response['query'], $query ) !== $response['query'] ) { + continue; + } + + $headers = [ 'Content-Type: application/json' ]; + foreach ( $response['headers'] as $name => $value ) { + $headers[] = "{$name}: {$value}"; + } + + return "HTTP/1.1 {$response['status']} Status\r\n" . implode( "\r\n", $headers ) . "\r\n\r\n" . json_encode( $response['body'] ); + } + + throw new RuntimeException( "Unexpected request: {$method} {$url}" ); + } + + /** + * Sets a static property for the duration of the test. + * + * @param string $class_name + * @param string $property + * @param mixed $value + */ + private function swap_static( $class_name, $property, $value ) { + $reflection = $this->static_property( $class_name, $property ); + + $this->original_statics[ "{$class_name}::{$property}" ] = $reflection->getValue(); + $reflection->setValue( null, $value ); + } + + private function static_property( $class_name, $property ) { + $reflection = new ReflectionProperty( $class_name, $property ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + + return $reflection; + } +}