From eb58a5d4b208a29f067ae33d1c9b4430f14714b9 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:30:20 -0500 Subject: [PATCH 1/6] Route Core Trac notifications to each component. Core Trac notifications post to `#core` and the component discussion channels, such as `#core-media` and `#core-restapi`. As activity has grown, this has made those channels too noisy for discussion. Core components with their own Slack channel now get three read-only channels: - `{channel}-firehose` for all commits, new tickets and ticket updates - `{channel}-newtickets` for new tickets - `{channel}-commits` for commits. `#core-firehose`, `#core-newtickets` and `#core-commits` keep receiving everything, `#core` stops receiving Trac notifications, and the discussion channels no longer get commits or new tickets. --- .../includes/slack/trac/comment-handler.php | 15 +- common/includes/slack/trac/config.php | 100 +++++++++---- common/includes/slack/trac/trac.php | 49 +++++++ common/includes/tests/slack/trac/channels.php | 135 ++++++++++++++++++ .../tests/slack/trac/comment-handler.php | 26 ++++ 5 files changed, 299 insertions(+), 26 deletions(-) create mode 100644 common/includes/tests/slack/trac/channels.php diff --git a/common/includes/slack/trac/comment-handler.php b/common/includes/slack/trac/comment-handler.php index ae47f599eb..44f91ad571 100644 --- a/common/includes/slack/trac/comment-handler.php +++ b/common/includes/slack/trac/comment-handler.php @@ -15,6 +15,7 @@ class Comment_Handler { protected $ticket_url; protected $comment_id; protected $comment_url; + protected $component; /** * Workflow keywords added to the ticket by this change. @@ -63,6 +64,11 @@ function run() { $this->send->send( $firehose ); } + $component_firehose = $this->trac->get_component_firehose_channel( $this->component ); + if ( $component_firehose ) { + $this->send->send( $component_firehose ); + } + // Send notifications for Workflow Keyword additions. $this->send_keyword_notifications(); } @@ -200,9 +206,13 @@ function process_message() { } // Remove top border of table. array_shift( $lines ); - // Remove ticket properties table. + // Remove ticket properties table, keeping the component. It can be in either column. + $component = ''; while ( $lines && 0 !== strpos( current( $lines ), '------' ) ) { - array_shift( $lines ); + $line = array_shift( $lines ); + if ( preg_match( '/(?:^|\|)\s*Component:\s+([^|]*?)\s*(?:\||$)/', $line, $matches ) ) { + $component = $matches[1]; + } } // Remove bottom border of table. array_shift( $lines ); @@ -290,6 +300,7 @@ function process_message() { $this->author = $author; $this->comment = $comment; $this->changes = $changes; + $this->component = $component; $this->ticket_id = $ticket_id; $this->ticket_url = $ticket_url; $this->comment_id = $comment_id; diff --git a/common/includes/slack/trac/config.php b/common/includes/slack/trac/config.php index a4e0d1b4b4..fb2b212649 100644 --- a/common/includes/slack/trac/config.php +++ b/common/includes/slack/trac/config.php @@ -5,13 +5,10 @@ class Core extends Trac { protected $name = 'WordPress'; - protected $primary_channel = '#core'; protected $commits_channel = '#core-commits'; protected $tickets_channel = '#core-newtickets'; protected $firehose_channel = '#core-firehose'; - protected $primary_channel_ticket_format = 'title'; - /** * File paths that cause commits to be piped to particular channels. * Start regex matches with # as your delimiter. @@ -54,7 +51,7 @@ class Core extends Trac { 'webpack.config.js' => '#core-build-test-tools', 'wp-tests-config-sample.php' => '#core-build-test-tools', '#\.n(v|p)mrc#' => '#core-build-test-tools', - '#\.version-support-[a-z]+\.json' => '#core-build-test-tools', + '#\.version-support-[a-z]+\.json#' => '#core-build-test-tools', '#^package(-lock)?\.json$#' => '#core-build-test-tools', '#^php[a-z]+\.xml\.dist$#' => '#core-build-test-tools', ); @@ -70,34 +67,89 @@ class Core extends Trac { ); /** - * Components or focuses that cause new tickets to be piped to particular channels. + * Maps each Core Trac component to its component specific channel. + * + * Notifications for a component go to that channel's -firehose, -newtickets and -commits + * channels, not to the channel itself. + * + * Subcomponents map to their parent's channel, unless they have their own. + * + * Components without a channel have an empty value, and only post to the global channels. + * + * @var array */ - protected $ticket_component_filters = array( + protected $component_channels = array( + 'AI' => '#core-ai', 'Build/Test Tools' => '#core-build-test-tools', 'Bundled Theme' => '#core-themes', - 'Customize' => '#core-customize', - 'Date/Time' => '#core-datetime', - 'Press This' => '#core-pressthis', - 'multisite' => '#core-multisite', - 'Networks and Sites' => '#core-multisite', - 'REST API' => '#core-restapi', - 'rest-api' => '#core-restapi', + 'Editor' => '#core-editor', + 'HTML API' => '#core-html-api', 'I18N' => '#core-i18n', + 'Import' => '#core-importers', + 'Interactivity API' => '#core-interactivity-api', 'Media' => '#core-media', - 'docs' => '#core-docs', - 'css' => '#core-css', - 'javascript' => '#core-js', - 'coding-standards' => '#core-coding-standards', - 'performance' => '#core-performance', + 'Networks and Sites' => '#core-multisite', 'Privacy' => '#core-privacy', - 'privacy' => '#core-privacy', - 'Site Health' => '#core-site-health', - 'Sitemaps' => '#core-sitemaps', + 'REST API' => '#core-restapi', 'Upgrade/Install' => '#core-upgrade-install', - 'Application Passwords' => [ '#core-passwords' => true, '#core-restapi' => true ], + + // Subcomponents. + 'Application Passwords' => '#core-restapi', + 'Autosave' => '#core-editor', + 'Embeds' => '#core-media', + 'Gallery' => '#core-media', 'Login and Registration' => '#core-passwords', - 'HTML API' => '#core-html-api', - 'Interactivity API' => '#core-interactivity-api', + 'Multisite' => '#core-multisite', + 'Notes' => '#core-notes', + 'Quick/Bulk Edit' => '#core-editor', + 'TinyMCE' => '#core-editor', + 'Upload' => '#core-media', + + // Components without a component specific channel. + 'Administration' => '', + 'Abilities API' => '', + 'Bootstrap/Load' => '', + 'Cache API' => '', + 'Comments' => '', + 'Pings/Trackbacks' => '', + 'Cron API' => '', + 'Customize' => '', + 'Database' => '', + 'Date/Time' => '', + 'Export' => '', + 'External Libraries' => '', + 'Feeds' => '', + 'Filesystem API' => '', + 'Formatting' => '', + 'Charset' => '', + 'Emoji' => '', + 'Shortcodes' => '', + 'General' => '', + 'Help/About' => '', + 'HTTP API' => '', + 'Mail' => '', + 'Options, Meta APIs' => '', + 'Permalinks' => '', + 'Canonical' => '', + 'Rewrite Rules' => '', + 'Plugins' => '', + 'Posts, Post Types' => '', + 'Post Formats' => '', + 'Post Thumbnails' => '', + 'Revisions' => '', + 'Query' => '', + 'Script Loader' => '', + 'Security' => '', + 'Site Health' => '', + 'Sitemaps' => '', + 'Taxonomy' => '', + 'Themes' => '', + 'Menus' => '', + 'Widgets' => '', + 'Toolbar' => '', + 'Users' => '', + 'Role/Capability' => '', + 'XML-RPC' => '', ); } diff --git a/common/includes/slack/trac/trac.php b/common/includes/slack/trac/trac.php index 34314412eb..432379b0ea 100644 --- a/common/includes/slack/trac/trac.php +++ b/common/includes/slack/trac/trac.php @@ -36,6 +36,19 @@ class Trac implements User { */ protected $ticket_keyword_filters = array(); + /** + * Maps Trac components (or commit message prefixes) to component specific channels. + * + * Notifications for a component go to that channel's -firehose, -newtickets and -commits + * channels, not to the channel itself. This includes commits that $commit_path_filters + * sends to one of these channels. + * + * Components with an empty value have no component channels. + * + * @var array + */ + protected $component_channels = array(); + protected $color = '#0073aa'; protected $icon = ':wordpress:'; @@ -250,6 +263,21 @@ function get_commit_channels( $changed_files = null, $log_text = null ) { } } + foreach ( $this->component_channels as $component => $channel ) { + if ( $channel && 0 === stripos( $log_text, $component ) ) { + $channels[ $channel ] = true; + } + } + + // Commits for a component go to its -commits and -firehose channels rather than the component channel itself. + foreach ( array_unique( array_filter( $this->component_channels ) ) as $channel ) { + if ( ! empty( $channels[ $channel ] ) ) { + unset( $channels[ $channel ] ); + $channels[ $channel . '-commits' ] = true; + $channels[ $channel . '-firehose' ] = true; + } + } + if ( $this->firehose_channel ) { $channels[ $this->firehose_channel ] = true; } @@ -291,6 +319,14 @@ function get_ticket_channels( $ticket = null ) { } } + // New tickets in a component with a channel go to that channel's -newtickets and -firehose channels. + if ( isset( $ticket->component ) && ! empty( $this->component_channels[ $ticket->component ] ) ) { + $channel = $this->component_channels[ $ticket->component ]; + + $channels[ $channel . '-newtickets' ] = true; + $channels[ $channel . '-firehose' ] = true; + } + if ( $this->firehose_channel ) { $channels[ $this->firehose_channel ] = true; } @@ -302,6 +338,19 @@ function get_firehose_channel() { return $this->firehose_channel; } + /** + * Gets the firehose channel for a component. + * + * @param string $component Trac component name. + * @return string|false Channel name, or false if the component has no channels. + */ + public function get_component_firehose_channel( $component ) { + if ( ! empty( $this->component_channels[ $component ] ) ) { + return $this->component_channels[ $component ] . '-firehose'; + } + return false; + } + /** * Gets the channels a ticket update should be posted to when a workflow keyword is added. * diff --git a/common/includes/tests/slack/trac/channels.php b/common/includes/tests/slack/trac/channels.php new file mode 100644 index 0000000000..65d3f3ed6a --- /dev/null +++ b/common/includes/tests/slack/trac/channels.php @@ -0,0 +1,135 @@ +get_commit_channels( $changed_files, $log ); + + sort( $channels ); + sort( $expected ); + $this->assertSame( $expected, $channels ); + } + + /** + * Data provider for test_core_commit_channels(). + * + * @return array[] + */ + public static function data_core_commit_channels(): array { + $global = array( '#core-commits', '#core-firehose' ); + + return array( + 'component prefix' => array( + 'trunk/src/wp-admin/import.php', + "Import: Check the file type.\n\nFixes #12345.", + array_merge( $global, array( '#core-importers-commits', '#core-importers-firehose' ) ), + ), + 'component without channels' => array( + 'trunk/src/wp-includes/class-wp-query.php', + 'Query: Cache the results.', + $global, + ), + 'subcomponent prefix' => array( + 'trunk/src/wp-includes/embed.php', + 'Embeds: Fix the iframe title.', + array_merge( $global, array( '#core-media-commits', '#core-media-firehose' ) ), + ), + 'bundled themes' => array( + 'trunk/src/wp-includes/theme.php', + 'Bundled Themes: Fix a typo.', + array_merge( $global, array( '#core-themes-commits', '#core-themes-firehose' ) ), + ), + 'path filter' => array( + 'trunk/src/wp-includes/html-api/class-wp-html-processor.php', + 'Fix a typo.', + array_merge( $global, array( '#core-html-api-commits', '#core-html-api-firehose' ) ), + ), + 'path filter and prefix' => array( + 'trunk/src/wp-includes/media.php', + 'I18N: Add context to a string.', + array_merge( $global, array( '#core-i18n-commits', '#core-i18n-firehose', '#core-media-commits', '#core-media-firehose' ) ), + ), + 'no component' => array( + 'trunk/src/wp-includes/formatting.php', + 'Docs: Correct a parameter type.', + $global, + ), + ); + } + + /** + * New Core tickets post to the global channels and the component's channels. + */ + public function test_core_new_ticket_channels(): void { + $ticket = (object) array( + 'component' => 'Gallery', + 'focuses' => 'css, javascript', + 'keywords' => 'has-patch needs-testing', + ); + + $channels = Trac::get( 'core' )->get_ticket_channels( $ticket ); + + sort( $channels ); + $this->assertSame( + array( '#core-firehose', '#core-media-firehose', '#core-media-newtickets', '#core-newtickets', '#core-test' ), + $channels + ); + } + + /** + * New Core tickets in a component without channels post only to the global channels. + */ + public function test_core_new_ticket_component_without_channels(): void { + $channels = Trac::get( 'core' )->get_ticket_channels( (object) array( 'component' => 'Query' ) ); + + sort( $channels ); + $this->assertSame( array( '#core-firehose', '#core-newtickets' ), $channels ); + } + + /** + * Core ticket updates post to the component's firehose. + */ + public function test_core_component_firehose_channel(): void { + $trac = Trac::get( 'core' ); + + $this->assertSame( '#core-passwords-firehose', $trac->get_component_firehose_channel( 'Login and Registration' ) ); + $this->assertFalse( $trac->get_component_firehose_channel( 'Query' ) ); + $this->assertFalse( $trac->get_component_firehose_channel( 'Not A Component' ) ); + } + + /** + * Meta has no component channels, so its routing is unchanged. + */ + public function test_meta_commit_channels_are_unchanged(): void { + $channels = Trac::get( 'meta' )->get_commit_channels( + 'sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php', + 'Plugin Directory: Fix a thing.' + ); + + sort( $channels ); + $this->assertSame( array( '#meta', '#meta-commits', '#meta-firehose', '#pluginreview' ), $channels ); + } +} diff --git a/common/includes/tests/slack/trac/comment-handler.php b/common/includes/tests/slack/trac/comment-handler.php index 9cf7faf8a0..c4dbbe764c 100644 --- a/common/includes/tests/slack/trac/comment-handler.php +++ b/common/includes/tests/slack/trac/comment-handler.php @@ -60,6 +60,32 @@ public function test_last_line_is_kept( array $body, string $comment, array $cha $this->assertSame( $changes, $handler->changes ); } + /** + * The ticket's component is read from either column of the properties table. + */ + public function test_component_is_read(): void { + $body = array( + '', + 'Comment (by desrosj):', + '', + ' Does anyone have any idea why Nacin would make this change?', + ); + + $handler = new \Dotorg\Slack\Trac\Comment_Handler( new \Dotorg\Slack\Send( '' ), $this->get_email( $body ) ); + $handler->process_message(); + $this->assertSame( 'Trac', $handler->component ); + + $email = str_replace( + array( 'Component: Trac | Resolution:', ' Type: defect (bug) | Status: new' ), + array( ' Type: defect (bug) | Resolution:', ' Priority: normal | Component: Build/Test Tools' ), + $this->get_email( $body ) + ); + + $handler = new \Dotorg\Slack\Trac\Comment_Handler( new \Dotorg\Slack\Send( '' ), $email ); + $handler->process_message(); + $this->assertSame( 'Build/Test Tools', $handler->component ); + } + /** * Data provider for test_last_line_is_kept(). * From 608e8482f57c026f242c5e5739ec8a1735cb9e1a Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:31:07 -0500 Subject: [PATCH 2/6] Note which component channels have been archived. --- common/includes/slack/trac/config.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/common/includes/slack/trac/config.php b/common/includes/slack/trac/config.php index fb2b212649..abf12c8a8d 100644 --- a/common/includes/slack/trac/config.php +++ b/common/includes/slack/trac/config.php @@ -108,14 +108,14 @@ class Core extends Trac { // Components without a component specific channel. 'Administration' => '', 'Abilities API' => '', - 'Bootstrap/Load' => '', + 'Bootstrap/Load' => '', // #core-bootstrap is archived. 'Cache API' => '', - 'Comments' => '', + 'Comments' => '', // #core-comments is archived. 'Pings/Trackbacks' => '', 'Cron API' => '', - 'Customize' => '', + 'Customize' => '', // #core-customize is archived. 'Database' => '', - 'Date/Time' => '', + 'Date/Time' => '', // #core-datetime is archived. 'Export' => '', 'External Libraries' => '', 'Feeds' => '', @@ -126,7 +126,7 @@ class Core extends Trac { 'Shortcodes' => '', 'General' => '', 'Help/About' => '', - 'HTTP API' => '', + 'HTTP API' => '', // #core-http-api is archived. 'Mail' => '', 'Options, Meta APIs' => '', 'Permalinks' => '', @@ -140,8 +140,8 @@ class Core extends Trac { 'Query' => '', 'Script Loader' => '', 'Security' => '', - 'Site Health' => '', - 'Sitemaps' => '', + 'Site Health' => '', // #core-site-health is archived. + 'Sitemaps' => '', // #core-sitemaps is archived. 'Taxonomy' => '', 'Themes' => '', 'Menus' => '', From 069c381861ddbdee053267343f9777a5ddf2673d Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:32:03 -0500 Subject: [PATCH 3/6] Address PHPCS warnings. --- common/includes/slack/trac/comment-handler.php | 6 ++++++ common/includes/slack/trac/trac.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/common/includes/slack/trac/comment-handler.php b/common/includes/slack/trac/comment-handler.php index 44f91ad571..8278bd8f99 100644 --- a/common/includes/slack/trac/comment-handler.php +++ b/common/includes/slack/trac/comment-handler.php @@ -15,6 +15,12 @@ class Comment_Handler { protected $ticket_url; protected $comment_id; protected $comment_url; + + /** + * The ticket's component, read from the notification's ticket properties table. + * + * @var string + */ protected $component; /** diff --git a/common/includes/slack/trac/trac.php b/common/includes/slack/trac/trac.php index 432379b0ea..33f1de88ee 100644 --- a/common/includes/slack/trac/trac.php +++ b/common/includes/slack/trac/trac.php @@ -42,7 +42,7 @@ class Trac implements User { * Notifications for a component go to that channel's -firehose, -newtickets and -commits * channels, not to the channel itself. This includes commits that $commit_path_filters * sends to one of these channels. - * + * * Components with an empty value have no component channels. * * @var array From 788aa4821bdc1c6efa7dddabb811379eb1cd984a Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:43:10 -0500 Subject: [PATCH 4/6] Give every Core component its own notification channels. Components without a discussion channel, or with an archived one, now map to a base channel name too, so their notifications go to `{channel}-firehose`, `{channel}-newtickets` and `{channel}-commits`. Subcomponents use their parent's channels. Themes, Menus and Widgets share `#core-themes` with Bundled Theme. Retired components, such as Press This, have an empty value and only post to the global channels. Co-Authored-By: Claude Opus 5.5 --- common/includes/slack/trac/config.php | 104 ++++++++++-------- common/includes/tests/slack/trac/channels.php | 30 +++-- 2 files changed, 74 insertions(+), 60 deletions(-) diff --git a/common/includes/slack/trac/config.php b/common/includes/slack/trac/config.php index abf12c8a8d..eb1737b8c1 100644 --- a/common/includes/slack/trac/config.php +++ b/common/includes/slack/trac/config.php @@ -74,7 +74,10 @@ class Core extends Trac { * * Subcomponents map to their parent's channel, unless they have their own. * - * Components without a channel have an empty value, and only post to the global channels. + * Some components have no discussion channel, or an archived one, but still have + * -firehose, -newtickets and -commits channels. + * + * Retired components have an empty value, and only post to the global channels. * * @var array */ @@ -91,65 +94,70 @@ class Core extends Trac { 'Networks and Sites' => '#core-multisite', 'Privacy' => '#core-privacy', 'REST API' => '#core-restapi', + 'Themes' => '#core-themes', 'Upgrade/Install' => '#core-upgrade-install', // Subcomponents. + 'Abilities API' => '#core-ai', 'Application Passwords' => '#core-restapi', 'Autosave' => '#core-editor', + 'Canonical' => '#core-permalinks', + 'Charset' => '#core-formatting', 'Embeds' => '#core-media', + 'Emoji' => '#core-formatting', 'Gallery' => '#core-media', 'Login and Registration' => '#core-passwords', + 'Menus' => '#core-themes', 'Multisite' => '#core-multisite', 'Notes' => '#core-notes', + 'Pings/Trackbacks' => '#core-comments', + 'Post Formats' => '#core-posts-post-types', + 'Post Thumbnails' => '#core-posts-post-types', 'Quick/Bulk Edit' => '#core-editor', - 'TinyMCE' => '#core-editor', + 'Revisions' => '#core-posts-post-types', + 'Rewrite Rules' => '#core-permalinks', + 'Role/Capability' => '#core-users', + 'Shortcodes' => '#core-formatting', + 'TinyMCE' => '#core-editor', // #core-tinymce has been archived since 2025-06-24. 'Upload' => '#core-media', - - // Components without a component specific channel. - 'Administration' => '', - 'Abilities API' => '', - 'Bootstrap/Load' => '', // #core-bootstrap is archived. - 'Cache API' => '', - 'Comments' => '', // #core-comments is archived. - 'Pings/Trackbacks' => '', - 'Cron API' => '', - 'Customize' => '', // #core-customize is archived. - 'Database' => '', - 'Date/Time' => '', // #core-datetime is archived. - 'Export' => '', - 'External Libraries' => '', - 'Feeds' => '', - 'Filesystem API' => '', - 'Formatting' => '', - 'Charset' => '', - 'Emoji' => '', - 'Shortcodes' => '', - 'General' => '', - 'Help/About' => '', - 'HTTP API' => '', // #core-http-api is archived. - 'Mail' => '', - 'Options, Meta APIs' => '', - 'Permalinks' => '', - 'Canonical' => '', - 'Rewrite Rules' => '', - 'Plugins' => '', - 'Posts, Post Types' => '', - 'Post Formats' => '', - 'Post Thumbnails' => '', - 'Revisions' => '', - 'Query' => '', - 'Script Loader' => '', - 'Security' => '', - 'Site Health' => '', // #core-site-health is archived. - 'Sitemaps' => '', // #core-sitemaps is archived. - 'Taxonomy' => '', - 'Themes' => '', - 'Menus' => '', - 'Widgets' => '', - 'Toolbar' => '', - 'Users' => '', - 'Role/Capability' => '', - 'XML-RPC' => '', + 'Widgets' => '#core-themes', + + // Components with an archived component specific channel. + 'Bootstrap/Load' => '#core-bootstrap', + 'Comments' => '#core-comments', + 'Customize' => '#core-customize', + 'Date/Time' => '#core-datetime', + 'HTTP API' => '#core-http-api', + 'Site Health' => '#core-site-health', + 'Sitemaps' => '#core-sitemaps', + + // Components that have never had a component specific channel. + 'Administration' => '#core-administration', + 'Cache API' => '#core-cache-api', + 'Cron API' => '#core-cron-api', + 'Database' => '#core-database', + 'Export' => '#core-export', + 'External Libraries' => '#core-external-libraries', + 'Feeds' => '#core-feeds', + 'Filesystem API' => '#core-filesystem-api', + 'Formatting' => '#core-formatting', + 'General' => '#core-general', + 'Help/About' => '#core-help-about', + 'Mail' => '#core-mail', + 'Options, Meta APIs' => '#core-options-meta-apis', + 'Permalinks' => '#core-permalinks', + 'Plugins' => '#core-plugins', + 'Posts, Post Types' => '#core-posts-post-types', + 'Query' => '#core-query', + 'Script Loader' => '#core-script-loader', + 'Security' => '#core-security', + 'Taxonomy' => '#core-taxonomy', + 'Toolbar' => '#core-toolbar', + 'Users' => '#core-users', + 'XML-RPC' => '#core-xml-rpc', + + // Retired components. + 'Press This' => '', // #core-pressthis is archived. ); } diff --git a/common/includes/tests/slack/trac/channels.php b/common/includes/tests/slack/trac/channels.php index 65d3f3ed6a..457fb96709 100644 --- a/common/includes/tests/slack/trac/channels.php +++ b/common/includes/tests/slack/trac/channels.php @@ -42,37 +42,42 @@ public static function data_core_commit_channels(): array { $global = array( '#core-commits', '#core-firehose' ); return array( - 'component prefix' => array( + 'component prefix' => array( 'trunk/src/wp-admin/import.php', "Import: Check the file type.\n\nFixes #12345.", array_merge( $global, array( '#core-importers-commits', '#core-importers-firehose' ) ), ), - 'component without channels' => array( + 'component without a discussion channel' => array( 'trunk/src/wp-includes/class-wp-query.php', 'Query: Cache the results.', - $global, + array_merge( $global, array( '#core-query-commits', '#core-query-firehose' ) ), + ), + 'archived channel path filter' => array( + 'trunk/src/wp-includes/sitemaps/class-wp-sitemaps.php', + 'Fix a typo.', + array_merge( $global, array( '#core-sitemaps-commits', '#core-sitemaps-firehose' ) ), ), - 'subcomponent prefix' => array( + 'subcomponent prefix' => array( 'trunk/src/wp-includes/embed.php', 'Embeds: Fix the iframe title.', array_merge( $global, array( '#core-media-commits', '#core-media-firehose' ) ), ), - 'bundled themes' => array( + 'bundled themes' => array( 'trunk/src/wp-includes/theme.php', 'Bundled Themes: Fix a typo.', array_merge( $global, array( '#core-themes-commits', '#core-themes-firehose' ) ), ), - 'path filter' => array( + 'path filter' => array( 'trunk/src/wp-includes/html-api/class-wp-html-processor.php', 'Fix a typo.', array_merge( $global, array( '#core-html-api-commits', '#core-html-api-firehose' ) ), ), - 'path filter and prefix' => array( + 'path filter and prefix' => array( 'trunk/src/wp-includes/media.php', 'I18N: Add context to a string.', array_merge( $global, array( '#core-i18n-commits', '#core-i18n-firehose', '#core-media-commits', '#core-media-firehose' ) ), ), - 'no component' => array( + 'no component' => array( 'trunk/src/wp-includes/formatting.php', 'Docs: Correct a parameter type.', $global, @@ -100,10 +105,10 @@ public function test_core_new_ticket_channels(): void { } /** - * New Core tickets in a component without channels post only to the global channels. + * New Core tickets in a component that isn't listed post only to the global channels. */ - public function test_core_new_ticket_component_without_channels(): void { - $channels = Trac::get( 'core' )->get_ticket_channels( (object) array( 'component' => 'Query' ) ); + public function test_core_new_ticket_unlisted_component(): void { + $channels = Trac::get( 'core' )->get_ticket_channels( (object) array( 'component' => 'Text Changes' ) ); sort( $channels ); $this->assertSame( array( '#core-firehose', '#core-newtickets' ), $channels ); @@ -116,7 +121,8 @@ public function test_core_component_firehose_channel(): void { $trac = Trac::get( 'core' ); $this->assertSame( '#core-passwords-firehose', $trac->get_component_firehose_channel( 'Login and Registration' ) ); - $this->assertFalse( $trac->get_component_firehose_channel( 'Query' ) ); + $this->assertSame( '#core-formatting-firehose', $trac->get_component_firehose_channel( 'Shortcodes' ) ); + $this->assertFalse( $trac->get_component_firehose_channel( 'Text Changes' ) ); $this->assertFalse( $trac->get_component_firehose_channel( 'Not A Component' ) ); } From 08fe1cbc098d74f23e93562eca0c49fd44a239a4 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:58:43 -0500 Subject: [PATCH 5/6] Keep posting Core commits and new tickets to #core. Core commits and new tickets go to `#core` again, as well as to the component channels. New tickets still show there as a title only. Co-Authored-By: Claude Opus 5.5 --- common/includes/slack/trac/config.php | 3 +++ common/includes/tests/slack/trac/channels.php | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/common/includes/slack/trac/config.php b/common/includes/slack/trac/config.php index eb1737b8c1..7be9c27257 100644 --- a/common/includes/slack/trac/config.php +++ b/common/includes/slack/trac/config.php @@ -5,10 +5,13 @@ class Core extends Trac { protected $name = 'WordPress'; + protected $primary_channel = '#core'; protected $commits_channel = '#core-commits'; protected $tickets_channel = '#core-newtickets'; protected $firehose_channel = '#core-firehose'; + protected $primary_channel_ticket_format = 'title'; + /** * File paths that cause commits to be piped to particular channels. * Start regex matches with # as your delimiter. diff --git a/common/includes/tests/slack/trac/channels.php b/common/includes/tests/slack/trac/channels.php index 457fb96709..84c66e045f 100644 --- a/common/includes/tests/slack/trac/channels.php +++ b/common/includes/tests/slack/trac/channels.php @@ -39,7 +39,7 @@ public function test_core_commit_channels( string $changed_files, string $log, a * @return array[] */ public static function data_core_commit_channels(): array { - $global = array( '#core-commits', '#core-firehose' ); + $global = array( '#core', '#core-commits', '#core-firehose' ); return array( 'component prefix' => array( @@ -99,7 +99,7 @@ public function test_core_new_ticket_channels(): void { sort( $channels ); $this->assertSame( - array( '#core-firehose', '#core-media-firehose', '#core-media-newtickets', '#core-newtickets', '#core-test' ), + array( '#core', '#core-firehose', '#core-media-firehose', '#core-media-newtickets', '#core-newtickets', '#core-test' ), $channels ); } @@ -111,7 +111,7 @@ public function test_core_new_ticket_unlisted_component(): void { $channels = Trac::get( 'core' )->get_ticket_channels( (object) array( 'component' => 'Text Changes' ) ); sort( $channels ); - $this->assertSame( array( '#core-firehose', '#core-newtickets' ), $channels ); + $this->assertSame( array( '#core', '#core-firehose', '#core-newtickets' ), $channels ); } /** From b32661753ace54480b31b9c1f1ff9ef04c45d0b1 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 10 Nov 2026 16:05:49 -0500 Subject: [PATCH 6/6] Post only the commit summary to #core. `#core` now gets the first line of each Core commit message. The other commit channels still get the full message. Adds `$primary_channel_commit_format`, which works like `$primary_channel_ticket_format`. Co-Authored-By: Claude Opus 5.5 --- common/includes/slack/trac/commit-handler.php | 59 ++++--- common/includes/slack/trac/commit.php | 16 ++ common/includes/slack/trac/config.php | 7 + common/includes/slack/trac/trac.php | 22 +++ .../tests/slack/trac/commit-handler.php | 160 ++++++++++++++++++ 5 files changed, 245 insertions(+), 19 deletions(-) create mode 100644 common/includes/tests/slack/trac/commit-handler.php diff --git a/common/includes/slack/trac/commit-handler.php b/common/includes/slack/trac/commit-handler.php index cf22dbc45a..492bae15ab 100644 --- a/common/includes/slack/trac/commit-handler.php +++ b/common/includes/slack/trac/commit-handler.php @@ -20,9 +20,14 @@ public function __construct( Send $send, $trac, $repo, $rev ) { } public function run() { - $this->generate_payload(); + // One payload per commit format, so each channel gets the full message or just the summary. + $payloads = array(); foreach ( $this->trac->get_commit_channels( $this->svnlook( 'changed' ), $this->svnlook( 'log' ) ) as $channel ) { - $this->send->send( $channel ); + $format = $this->trac->get_commit_format( $channel ); + if ( ! isset( $payloads[ $format ] ) ) { + $payloads[ $format ] = $this->generate_payload( $format ); + } + $payloads[ $format ]->send( $channel ); } } @@ -34,9 +39,20 @@ public function set_svnlook_executable( $path ) { $this->svnlook = $path; } - protected function generate_payload() { + /** + * Builds the Slack message for the commit. + * + * @param string $format 'full' for the whole commit message, 'summary' for its first line. + * @return Send A copy of the Send object with the commit attached. + */ + protected function generate_payload( $format = 'full' ) { + $log = $this->svnlook( 'log' ); + if ( 'summary' === $format ) { + $log = Commit::get_summary( $log ); + } + $author = $this->svnlook( 'author' ); - $message = Commit::format_commit_for_slack( $this->trac, $this->svnlook( 'log' ) ); + $message = Commit::format_commit_for_slack( $this->trac, $log ); $date = $this->svnlook( 'date' ); $url = $this->trac->get_commit_template( $this->rev ); $revision = 'r' . $this->rev; @@ -46,21 +62,26 @@ protected function generate_payload() { $title = "$username $revision"; $fallback = "$username $revision by $author"; - $this->send->set_username( $username ); - $this->send->set_icon( $icon ); - $this->send->add_attachment( array( - 'title' => $title, - 'title_link' => $url, - 'author_name' => $author, - 'author_icon' => sprintf( 'https://wordpress.org/grav-redirect.php?user=%s&s=32', $author ), - 'color' => $color, - 'text' => $message, - 'fallback' => $fallback, - 'mrkdwn_in' => array( 'text' ), - 'ts' => $date, - 'footer' => sprintf( '<%s|%s>', $this->trac->get_url(), $this->trac->get_name() ), - 'footer_icon' => sprintf( '%s/chrome/common/trac.ico', $this->trac->get_url() ), - ) ); + $send = clone $this->send; + $send->set_username( $username ); + $send->set_icon( $icon ); + $send->add_attachment( + array( + 'title' => $title, + 'title_link' => $url, + 'author_name' => $author, + 'author_icon' => sprintf( 'https://wordpress.org/grav-redirect.php?user=%s&s=32', $author ), + 'color' => $color, + 'text' => $message, + 'fallback' => $fallback, + 'mrkdwn_in' => array( 'text' ), + 'ts' => $date, + 'footer' => sprintf( '<%s|%s>', $this->trac->get_url(), $this->trac->get_name() ), + 'footer_icon' => sprintf( '%s/chrome/common/trac.ico', $this->trac->get_url() ), + ) + ); + + return $send; } protected function svnlook( $subcommand ) { diff --git a/common/includes/slack/trac/commit.php b/common/includes/slack/trac/commit.php index 067a6c789a..cef5b5ba3d 100644 --- a/common/includes/slack/trac/commit.php +++ b/common/includes/slack/trac/commit.php @@ -92,6 +92,22 @@ function fetch() { return $this->data; } + /** + * Gets the summary of a commit message: its first non-empty line. + * + * @param string $message Commit message. + * @return string + */ + public static function get_summary( $message ) { + foreach ( explode( "\n", $message ) as $line ) { + $line = trim( $line ); + if ( '' !== $line ) { + return $line; + } + } + return ''; + } + static function format_commit_for_slack( Trac $trac, $message ) { // Convert ASCII numbers to an UTF-8 character, like ?\226?\128?\148 => — (m-dash). $message = preg_replace_callback( '/(?:\?\\\\(\d{1,3}))/', function( $matches ) { diff --git a/common/includes/slack/trac/config.php b/common/includes/slack/trac/config.php index 7be9c27257..cbeef0202e 100644 --- a/common/includes/slack/trac/config.php +++ b/common/includes/slack/trac/config.php @@ -12,6 +12,13 @@ class Core extends Trac { protected $primary_channel_ticket_format = 'title'; + /** + * Post only the first line of each commit message to #core. + * + * @var string + */ + protected $primary_channel_commit_format = 'summary'; + /** * File paths that cause commits to be piped to particular channels. * Start regex matches with # as your delimiter. diff --git a/common/includes/slack/trac/trac.php b/common/includes/slack/trac/trac.php index 33f1de88ee..1eb4829e5e 100644 --- a/common/includes/slack/trac/trac.php +++ b/common/includes/slack/trac/trac.php @@ -23,6 +23,15 @@ class Trac implements User { // 'title', 'fields', 'description' protected $primary_channel_ticket_format = 'description'; + /** + * How much of the commit message to post to the primary channel. + * + * 'full' posts the whole commit message, 'summary' only its first line. + * + * @var string + */ + protected $primary_channel_commit_format = 'full'; + protected $bypass_primary_channel_for_commit_filter_matches = false; protected $bypass_primary_channel_for_ticket_filter_matches = false; @@ -372,6 +381,19 @@ function get_ticket_format( $channel ) { return 'description'; } + /** + * Gets how much of the commit message to post to a channel. + * + * @param string $channel Channel name. + * @return string 'full' or 'summary'. + */ + public function get_commit_format( $channel ) { + if ( $channel === $this->primary_channel ) { + return $this->primary_channel_commit_format; + } + return 'full'; + } + /** * Stop message text people typed from being read as Slack markup. * diff --git a/common/includes/tests/slack/trac/commit-handler.php b/common/includes/tests/slack/trac/commit-handler.php new file mode 100644 index 0000000000..6d9756f528 --- /dev/null +++ b/common/includes/tests/slack/trac/commit-handler.php @@ -0,0 +1,160 @@ +sent = $sent; + } + + public function send( $channel, $thread = false ) { + $attachments = $this->get_attachments(); + $this->sent[ $channel ] = $attachments[0]['text']; + } + }; + + $handler = new class( $send, $trac, '/repo', 1 ) extends Commit_Handler { + public $svnlook_output = array(); + + protected function svnlook( $subcommand ) { + return $this->svnlook_output[ $subcommand ]; + } + }; + + $handler->svnlook_output = array( + 'changed' => $changed_files, + 'log' => $log, + 'author' => 'desrosj', + 'date' => '1762732800', + ); + $handler->run(); + + return $sent->getArrayCopy(); + } + + /** + * #core gets the commit summary; every other channel gets the full message. + */ + public function test_core_commit_summary_only_goes_to_core(): void { + $sent = $this->run_handler( 'core', 'trunk/src/wp-includes/media.php', self::LOG ); + + $this->assertSame( + array( '#core', '#core-commits', '#core-firehose', '#core-media-commits', '#core-media-firehose' ), + $this->sorted_keys( $sent ) + ); + + $this->assertSame( 'Media: Add a missing alt attribute.', $sent['#core'] ); + + foreach ( array( '#core-commits', '#core-firehose', '#core-media-commits', '#core-media-firehose' ) as $channel ) { + $this->assertStringContainsString( 'Props desrosj.', $sent[ $channel ], "$channel should get the full commit message." ); + } + } + + /** + * Meta posts the full commit message everywhere, including its primary channel. + */ + public function test_meta_commit_is_unchanged(): void { + $sent = $this->run_handler( + 'meta', + 'sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php', + "Plugin Directory: Fix a thing.\n\nProps desrosj." + ); + + $this->assertNotEmpty( $sent ); + foreach ( $sent as $channel => $text ) { + $this->assertStringContainsString( 'Props desrosj.', $text, "$channel should get the full commit message." ); + } + } + + /** + * Only Core's primary channel uses the summary format. + */ + public function test_get_commit_format(): void { + $core = Trac::get( 'core' ); + + $this->assertSame( 'summary', $core->get_commit_format( '#core' ) ); + $this->assertSame( 'full', $core->get_commit_format( '#core-commits' ) ); + $this->assertSame( 'full', $core->get_commit_format( '#core-media-commits' ) ); + $this->assertSame( 'full', Trac::get( 'meta' )->get_commit_format( '#meta' ) ); + } + + /** + * The summary is the first non-empty line of the commit message. + */ + public function test_get_summary(): void { + $this->assertSame( 'Media: Add a missing alt attribute.', Commit::get_summary( self::LOG ) ); + $this->assertSame( 'Docs: Fix a typo.', Commit::get_summary( "\n Docs: Fix a typo. \n\nProps someone." ) ); + $this->assertSame( 'One line.', Commit::get_summary( 'One line.' ) ); + $this->assertSame( '', Commit::get_summary( '' ) ); + } + + /** + * Every -newtickets channel gets the same format as #core-newtickets: the full ticket. + * Only #core gets the title alone. + */ + public function test_newtickets_channels_match_core_newtickets_format(): void { + $core = Trac::get( 'core' ); + $map = ( new ReflectionProperty( $core, 'component_channels' ) )->getValue( $core ); + + $this->assertSame( 'title', $core->get_ticket_format( '#core' ) ); + $this->assertSame( 'description', $core->get_ticket_format( '#core-newtickets' ) ); + + foreach ( array_unique( array_filter( $map ) ) as $channel ) { + $this->assertSame( + $core->get_ticket_format( '#core-newtickets' ), + $core->get_ticket_format( $channel . '-newtickets' ), + "$channel-newtickets should match #core-newtickets." + ); + } + } + + /** + * Returns an array's keys, sorted. + * + * @param array $array Array. + * @return string[] + */ + private function sorted_keys( array $array ): array { + $keys = array_keys( $array ); + sort( $keys ); + return $keys; + } +}