From 3dbb9b321af1224610cc5a022fa21a187968916c Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 18 Aug 2026 22:32:00 +0000 Subject: [PATCH] Plugin Directory: Don't queue a deleted /trunk for import. A commit that deletes /trunk was recorded as having touched it, so the importer queued a ZIP build against a URL that no longer exists. Release workflows that delete trunk in the tag commit and re-create it from that tag moments later hit this whenever the watcher polls in between, failing the build with "URL '.../trunk' doesn't exist". The re-creation is a change of its own and queues trunk again once it's there. Mirrors the existing handling for deleted tags, except that trunk isn't recorded in tags_deleted: that removes the matching release, and trunk has no release of its own to remove. Co-Authored-By: Claude Opus 5 --- .../cli/class-svn-watcher.php | 9 ++- .../tests/SVN_Watcher_Log_Summary_Test.php | 63 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-svn-watcher.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-svn-watcher.php index 195e3713d8..1cafc3b668 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-svn-watcher.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-svn-watcher.php @@ -174,8 +174,13 @@ protected function summarize_plugin_changes( $logs ) { } if ( 'trunk' == $path_parts[1] ) { - $plugin['tags_touched'][] = 'trunk'; - + /* + * A deleted /trunk has nothing left to export. It isn't recorded in tags_deleted + * either, as that removes the matching release, and trunk has no release of its own. + */ + if ( ! $is_deletion || isset( $path_parts[2] ) /* a file within trunk */ ) { + $plugin['tags_touched'][] = 'trunk'; + } } elseif ( 'tags' == $path_parts[1] && isset( $path_parts[2] ) ) { if ( $is_deletion && ! isset( $path_parts[3] ) /* not a file deletion */ ) { $plugin['tags_deleted'][] = $path_parts[2]; diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/SVN_Watcher_Log_Summary_Test.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/SVN_Watcher_Log_Summary_Test.php index f047911f17..ec4c09bbf1 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/SVN_Watcher_Log_Summary_Test.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/tests/SVN_Watcher_Log_Summary_Test.php @@ -185,6 +185,69 @@ public function test_tag_deletion_is_distinguished_from_a_file_deletion_within_a $this->assertSame( array( '2.0' ), $plugins['plugin-a']['tags_touched'] ); } + /** + * Deleting /trunk leaves nothing to export, so it mustn't be queued as a touched version. + * + * This is a real release pattern: the tag is added and trunk deleted in one commit, then + * trunk is re-created from that tag a few seconds later. An import queued from the first + * commit runs while trunk is missing, and the ZIP build fails against a URL that's gone. + */ + public function test_trunk_deletion_is_not_queued_as_a_touched_version() { + $plugins = $this->summarize( + array( + $this->log_entry( + 3653586, + array( + '/wp-better-permalinks/tags/4.3.1' => 'A', + '/wp-better-permalinks/tags/4.3.1/readme.txt' => 'A', + '/wp-better-permalinks/trunk' => 'D', + ) + ), + ) + ); + + $this->assertSame( array( '4.3.1' ), $plugins['wp-better-permalinks']['tags_touched'] ); + $this->assertNotContains( 'trunk', $plugins['wp-better-permalinks']['tags_touched'] ); + + // trunk has no release of its own, so there's nothing for the importer to remove either. + $this->assertSame( array(), $plugins['wp-better-permalinks']['tags_deleted'] ); + } + + /** + * Deleting a file inside /trunk is a change to trunk, not the removal of it. + */ + public function test_file_deletion_within_trunk_still_touches_trunk() { + $plugins = $this->summarize( + array( + $this->log_entry( 600, array( '/plugin-a/trunk/deprecated.php' => 'D' ) ), + ) + ); + + $this->assertSame( array( 'trunk' ), $plugins['plugin-a']['tags_touched'] ); + } + + /** + * Once trunk is back, it's a change like any other — including when both commits land in + * the same batch of revisions, where the deletion and the re-creation are seen together. + */ + public function test_recreated_trunk_is_queued_again() { + $plugins = $this->summarize( + array( + $this->log_entry( + 3653586, + array( + '/plugin-a/tags/1.0' => 'A', + '/plugin-a/trunk' => 'D', + ) + ), + $this->log_entry( 3653587, array( '/plugin-a/trunk' => 'A' ) ), + ) + ); + + $this->assertEqualsCanonicalizing( array( '1.0', 'trunk' ), $plugins['plugin-a']['tags_touched'] ); + $this->assertSame( array( 3653586, 3653587 ), $plugins['plugin-a']['revisions'] ); + } + /** * Paths that don't name anything below the plugin root carry no importable change. */