From 28dc12a81711a263d716835663fff0e3c972f66b Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Sun, 2 Aug 2026 11:00:56 +0200 Subject: [PATCH 1/2] [FEATURE] Warn about tags TER already implies Every extension in TER is a TYPO3 extension, so tags such as typo3, typo3-extension, extension, cms or php narrow nothing down there. The same terms are valuable elsewhere -- on GitHub typo3-extension is what makes a repository findable at all, on Packagist typo3 still helps -- which is why a publishing pipeline reusing one vocabulary across registries ends up sending them. It shows in the listing today: the news extension carries the tag "extension". Warn when ter:update receives such a tag, and leave the request untouched. Tailor is a thin client over PUT /extension/{key}; silently dropping a value someone passed explicitly would be surprising, and which terms are "too generic" is a judgement that belongs to the caller. A hint at the point of use costs nothing. Also document it in the README, next to the --tags example, together with why it matters most for automated publishing: the whole list is replaced on every call, so a pipeline keeps overwriting curated tags. Resolves #96 Signed-off-by: Sebastian Mendel --- README.md | 13 +++++++ .../Extension/UpdateExtensionCommand.php | 28 +++++++++++++ src/Helper/CommandHelper.php | 39 +++++++++++++++++++ tests/Unit/Helper/CommandHelperTest.php | 28 +++++++++++++ 4 files changed, 108 insertions(+) diff --git a/README.md b/README.md index 798f7d5..22bc38d 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,19 @@ To update the tags: ./vendor/bin/tailor ter:update my_extension --tags=some-tag,another-tag ``` +Tags are what people search the extension listing by, so they are worth +choosing for TER specifically. Every extension in TER is a TYPO3 extension, +which means terms such as `typo3`, `typo3-extension`, `extension`, `cms` or +`php` narrow nothing down there — even though the very same terms are what +make a package findable on GitHub or Packagist. Prefer tags describing what +the extension does. Tailor warns when it recognises such a term, but still +sends whatever you pass. + +This matters most for automated publishing: because the whole list is +replaced on every call (see below), a pipeline that reuses one vocabulary +across registries will keep overwriting curated TER tags with terms that add +nothing. + Please use `./vendor/bin/tailor ter:update -h` to see the full list of available options. diff --git a/src/Command/Extension/UpdateExtensionCommand.php b/src/Command/Extension/UpdateExtensionCommand.php index ab70fdf..b609b5e 100644 --- a/src/Command/Extension/UpdateExtensionCommand.php +++ b/src/Command/Extension/UpdateExtensionCommand.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use TYPO3\Tailor\Command\AbstractClientRequestCommand; use TYPO3\Tailor\Dto\Messages; use TYPO3\Tailor\Dto\RequestConfiguration; @@ -57,9 +58,36 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $this->extensionKey = CommandHelper::getExtensionKeyFromInput($input); + $this->warnAboutTagsImpliedByTer($input, $output); + return parent::execute($input, $output); } + /** + * TER replaces the whole tag list with what is sent, so a pipeline reusing + * one vocabulary across registries silently publishes terms that say + * nothing here. Point them out without changing what gets sent. + */ + private function warnAboutTagsImpliedByTer(InputInterface $input, OutputInterface $output): void + { + $tags = $input->getOption('tags'); + if (!is_string($tags) || $tags === '') { + return; + } + + $implied = CommandHelper::getTagsImpliedByTer($tags); + if ($implied === []) { + return; + } + + (new SymfonyStyle($input, $output))->warning(sprintf( + 'Every extension in TER is a TYPO3 extension, so %s %s no discoverability there. ' + . 'Consider tags describing what the extension does instead.', + implode(', ', $implied), + count($implied) === 1 ? 'adds' : 'add' + )); + } + protected function getRequestConfiguration(): RequestConfiguration { return new RequestConfiguration( diff --git a/src/Helper/CommandHelper.php b/src/Helper/CommandHelper.php index d3a516b..82d9d4f 100644 --- a/src/Helper/CommandHelper.php +++ b/src/Helper/CommandHelper.php @@ -22,6 +22,25 @@ */ final class CommandHelper { + /** + * Tags that every TER listing already implies. TER only lists TYPO3 + * extensions, so these narrow nothing down there — unlike on GitHub or + * Packagist, where the same terms are what make a package findable at all. + * + * @var string[] + */ + private const TAGS_IMPLIED_BY_TER = [ + 'cms', + 'extension', + 'extensions', + 'php', + 'ter', + 'typo3', + 'typo3-cms', + 'typo3-extension', + 'typo3cms', + 'typo3ext', + ]; public static function getExtensionKeyFromInput(InputInterface $input): string { // 1. CLI argument has highest priority @@ -52,4 +71,24 @@ public static function getExtensionKeyFromInput(InputInterface $input): string 1605706548 ); } + + /** + * Returns those of the given tags that TER already implies, so the caller + * can point them out. Comparison is case-insensitive. + * + * @return string[] + */ + public static function getTagsImpliedByTer(string $tags): array + { + $implied = []; + + foreach (explode(',', $tags) as $tag) { + $tag = trim($tag); + if ($tag !== '' && in_array(strtolower($tag), self::TAGS_IMPLIED_BY_TER, true)) { + $implied[] = $tag; + } + } + + return $implied; + } } diff --git a/tests/Unit/Helper/CommandHelperTest.php b/tests/Unit/Helper/CommandHelperTest.php index c805a21..0376227 100644 --- a/tests/Unit/Helper/CommandHelperTest.php +++ b/tests/Unit/Helper/CommandHelperTest.php @@ -12,6 +12,7 @@ namespace TYPO3\Tailor\Tests\Unit\Helper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArrayInput; @@ -98,4 +99,31 @@ public function getExtensionKeyFromInputReturnsExtensionKeyFromEnvironmentVariab restore_error_handler(); } } + + /** + * @param string[] $expected + */ + #[Test] + #[DataProvider('tagsImpliedByTerDataProvider')] + public function getTagsImpliedByTerReturnsOnlyTermsTerAlreadyImplies(string $tags, array $expected): void + { + self::assertSame($expected, CommandHelper::getTagsImpliedByTer($tags)); + } + + /** + * @return array + */ + public static function tagsImpliedByTerDataProvider(): array + { + return [ + 'empty input' => ['', []], + 'only domain tags' => ['search,indexing,facets', []], + 'single implied tag' => ['typo3,search', ['typo3']], + 'several implied tags' => ['typo3,php,search,extension', ['typo3', 'php', 'extension']], + 'case is ignored' => ['TYPO3,Extension', ['TYPO3', 'Extension']], + 'surrounding whitespace' => [' typo3 , search ', ['typo3']], + 'empty segments' => ['typo3,,search,', ['typo3']], + 'substring is not a match' => ['typo3-solr,phpunit', []], + ]; + } } From 8705626e320afd1a48f4469ae265ed1a92386932 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Sun, 2 Aug 2026 11:14:27 +0200 Subject: [PATCH 2/2] [TASK] Match the tag list against what extensions actually publish The term list was reasoned about rather than measured. Checking 3000 TER extensions and the three vocabularies of the top 60 by downloads shows the reasoning held but the list missed spellings that dominate in practice: composer.json keywords favour "typo3 cms" with a space -- the second most common keyword overall -- and GitHub topics favour "typo3-cms-extension", the most common extension term there. Compare with separators stripped instead of enumerating permutations, so "typo3 cms", "typo3-cms" and "typo3cms" are one entry and future variants are covered. Domain tags that legitimately contain separators, such as "e-commerce" and "tt_news", are unaffected -- covered by a test. State the rule in the --tags description as well. The warning fires only after a pipeline has been written and run; the option description is what `ter:update -h` prints, which is where someone -- or a coding agent -- looks before writing it. Signed-off-by: Sebastian Mendel --- .../Extension/UpdateExtensionCommand.php | 9 ++++++- src/Helper/CommandHelper.php | 25 +++++++++++++------ tests/Unit/Helper/CommandHelperTest.php | 11 ++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/Command/Extension/UpdateExtensionCommand.php b/src/Command/Extension/UpdateExtensionCommand.php index b609b5e..5787076 100644 --- a/src/Command/Extension/UpdateExtensionCommand.php +++ b/src/Command/Extension/UpdateExtensionCommand.php @@ -52,7 +52,14 @@ protected function configure(): void ->addOption('repository', '', InputOption::VALUE_OPTIONAL, 'Link to the repository') ->addOption('manual', '', InputOption::VALUE_OPTIONAL, 'Link to the external manual') ->addOption('paypal', '', InputOption::VALUE_OPTIONAL, 'Link to sponsoring page (paypal)') - ->addOption('tags', '', InputOption::VALUE_OPTIONAL, 'Comma-separated list of tags'); + ->addOption( + 'tags', + '', + InputOption::VALUE_OPTIONAL, + 'Comma-separated list of tags. Replaces the existing list. Every extension in TER is a ' + . 'TYPO3 extension, so terms like typo3, typo3-extension, extension or php add no ' + . 'discoverability there - prefer tags describing what the extension does.' + ); } protected function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Helper/CommandHelper.php b/src/Helper/CommandHelper.php index 82d9d4f..f15a806 100644 --- a/src/Helper/CommandHelper.php +++ b/src/Helper/CommandHelper.php @@ -23,24 +23,30 @@ final class CommandHelper { /** - * Tags that every TER listing already implies. TER only lists TYPO3 - * extensions, so these narrow nothing down there — unlike on GitHub or - * Packagist, where the same terms are what make a package findable at all. + * Terms every TER listing already implies. TER only lists TYPO3 extensions, + * so these narrow nothing down there — unlike on GitHub or Packagist, where + * the same terms are what make a package findable at all. + * + * Compared after stripping separators, so the spelling variants that occur + * in practice — "typo3 cms" in composer.json keywords, "typo3-cms-extension" + * in GitHub topics — are all recognised. * * @var string[] */ private const TAGS_IMPLIED_BY_TER = [ 'cms', + 'cmsextension', 'extension', 'extensions', 'php', 'ter', 'typo3', - 'typo3-cms', - 'typo3-extension', 'typo3cms', + 'typo3cmsextension', 'typo3ext', + 'typo3extension', ]; + public static function getExtensionKeyFromInput(InputInterface $input): string { // 1. CLI argument has highest priority @@ -74,7 +80,7 @@ public static function getExtensionKeyFromInput(InputInterface $input): string /** * Returns those of the given tags that TER already implies, so the caller - * can point them out. Comparison is case-insensitive. + * can point them out. Case and separators are ignored. * * @return string[] */ @@ -84,7 +90,12 @@ public static function getTagsImpliedByTer(string $tags): array foreach (explode(',', $tags) as $tag) { $tag = trim($tag); - if ($tag !== '' && in_array(strtolower($tag), self::TAGS_IMPLIED_BY_TER, true)) { + if ($tag === '') { + continue; + } + + $normalized = strtolower((string)preg_replace('/[^a-z0-9]/i', '', $tag)); + if (in_array($normalized, self::TAGS_IMPLIED_BY_TER, true)) { $implied[] = $tag; } } diff --git a/tests/Unit/Helper/CommandHelperTest.php b/tests/Unit/Helper/CommandHelperTest.php index 0376227..894d4aa 100644 --- a/tests/Unit/Helper/CommandHelperTest.php +++ b/tests/Unit/Helper/CommandHelperTest.php @@ -124,6 +124,17 @@ public static function tagsImpliedByTerDataProvider(): array 'surrounding whitespace' => [' typo3 , search ', ['typo3']], 'empty segments' => ['typo3,,search,', ['typo3']], 'substring is not a match' => ['typo3-solr,phpunit', []], + // Spelling variants observed in the wild: composer.json keywords + // favour "typo3 cms", GitHub topics "typo3-cms-extension". + 'separator variants' => [ + 'typo3 cms,typo3-cms,typo3cms', + ['typo3 cms', 'typo3-cms', 'typo3cms'], + ], + 'extension variants' => [ + 'typo3-cms-extension,typo3-extension,cms-extension', + ['typo3-cms-extension', 'typo3-extension', 'cms-extension'], + ], + 'domain tag with separator survives' => ['e-commerce,tt_news', []], ]; } }