diff --git a/.github/workflows/database.yaml b/.github/workflows/database.yaml index e97b8f39..914e7776 100644 --- a/.github/workflows/database.yaml +++ b/.github/workflows/database.yaml @@ -102,7 +102,7 @@ jobs: - name: Verify existing migrations are not modified or deleted run: | BASE_REF="${{ steps.base.outputs.base_ref }}" - CHANGED=$(git diff --name-only --diff-filter=MD origin/$BASE_REF...HEAD -- migrations/) + CHANGED=$(git diff --name-only --diff-filter=MD origin/$BASE_REF...HEAD -- migrations/ | grep -E 'Version[0-9]{14}\.php$' || true) if [ -n "$CHANGED" ]; then echo "Error: The following existing migrations were modified or deleted:" echo "$CHANGED" @@ -110,6 +110,29 @@ jobs: fi echo "No existing migrations were modified or deleted." + - name: Verify new migrations follow naming convention + run: | + BASE_REF="${{ steps.base.outputs.base_ref }}" + NEW_FILES=$(git diff --name-only --diff-filter=A origin/$BASE_REF...HEAD -- migrations/ | grep '\.php$' || true) + if [ -z "$NEW_FILES" ]; then + echo "No new migrations added." + exit 0 + fi + + FAILED=false + for file in $NEW_FILES; do + if ! echo "$file" | grep -qE 'Version[0-9]{14}\.php$'; then + echo "Error: $file does not follow the migration naming convention (Version{14digits}.php)" + FAILED=true + else + echo "OK: $file" + fi + done + + if [ "$FAILED" = true ]; then + exit 1 + fi + - name: Verify new migrations have higher version numbers than existing migrations run: | BASE_REF="${{ steps.base.outputs.base_ref }}" @@ -128,11 +151,10 @@ jobs: FAILED=false for file in $NEW_MIGRATIONS; do - VERSION=$(echo "$file" | grep -oE '[0-9]{14}') - if [ -z "$VERSION" ]; then - echo "Warning: Could not extract version number from $file" + if ! echo "$file" | grep -qE 'Version[0-9]{14}\.php$'; then continue fi + VERSION=$(echo "$file" | grep -oE '[0-9]{14}') if [ "$VERSION" -le "$BASE_MAX" ]; then echo "Error: $file has version $VERSION which is not higher than the existing maximum $BASE_MAX" FAILED=true diff --git a/docs/configuration-reference.md b/docs/configuration-reference.md index 039597c1..e7c8118b 100644 --- a/docs/configuration-reference.md +++ b/docs/configuration-reference.md @@ -35,6 +35,14 @@ dirigent: dev_packages: false metadata: mirror_vcs_repositories: false + retain_pruned_versions: + enabled: true + tagged_versions: true + dev_versions: false + retain_stale_revisions: + enabled: true + tagged_versions: true + dev_versions: false ``` ## dirigent (root) @@ -131,6 +139,46 @@ Fetch mirrored packages from their VCS repositories by default when possible. Sets the fetch strategy of new mirrored packages to **Fetch from VCS**. +### retain_pruned_versions + +#### enabled + +Type: `boolean` | Default: `true` + +Whether to enable or disable retaining pruned versions of packages. + +#### tagged_versions + +Type: `boolean` | Default: `true` + +Retain pruned tagged package versions. + +#### dev_versions + +Type: `boolean` | Default: `false` + +Retain pruned development package versions. + +### retain_stale_revisions + +#### enabled + +Type: `boolean` | Default: `true` + +Whether to enable or disable retaining stale revisions of packages. + +#### tagged_versions + +Type: `boolean` | Default: `true` + +Retain stale revisions of tagged package versions. + +#### dev_versions + +Type: `boolean` | Default: `false` + +Retain stale revisions of development package versions. + [iso-8601-durations]: https://en.wikipedia.org/wiki/ISO_8601#Durations [symfony]: https://symfony.com [symfony-docs-config]: https://symfony.com/doc/current/configuration.html diff --git a/migrations/AGENTS.md b/migrations/AGENTS.md new file mode 100644 index 00000000..0398138f --- /dev/null +++ b/migrations/AGENTS.md @@ -0,0 +1,19 @@ +# Agent guidelines for Dirigent development: migrations/ + +## Generate new migrations + +To generate a new migration, execute the `symfony console doctrine:migrations:diff --nowdoc --formatted` command. + +## Coding style + +- Migration files must follow the naming convention of `Version[0-9]{14}.php`. +- Migrations must have a non-empty description. +- Queries should be wrapped in nowdoc by default. Only if a PHP variable is used in the query is it allowed to be wrapped in heredoc. + +## Required columns + +If a required (non-nullable) column is added to the schema, add it with the following queries: + +1. Add a nullable column. +2. Set a default value for every row in the table. +3. Remove the nullable flag from the column. diff --git a/migrations/CLAUDE.md b/migrations/CLAUDE.md new file mode 100644 index 00000000..d07b8227 --- /dev/null +++ b/migrations/CLAUDE.md @@ -0,0 +1,3 @@ +# Agent guidelines for Dirigent development in Claude Code: migrations/ + +@AGENTS.md diff --git a/migrations/Version20260416081737.php b/migrations/Version20260416081737.php new file mode 100644 index 00000000..e61a729a --- /dev/null +++ b/migrations/Version20260416081737.php @@ -0,0 +1,36 @@ +addSql(<<<'SQL' + ALTER TABLE version ADD pruned BOOLEAN DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + UPDATE version SET pruned = false + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE version ALTER pruned SET NOT NULL + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE version DROP pruned + SQL); + } +} diff --git a/src/DependencyInjection/DirigentConfiguration.php b/src/DependencyInjection/DirigentConfiguration.php index af1f62e6..00b6807f 100644 --- a/src/DependencyInjection/DirigentConfiguration.php +++ b/src/DependencyInjection/DirigentConfiguration.php @@ -93,6 +93,32 @@ private function addMetadataSection(ArrayNodeDefinition|NodeDefinition $rootNode ->defaultFalse() ->info('Fetch mirrored packages from their VCS repositories by default when possible.') ->end() + ->arrayNode('retain_pruned_versions') + ->canBeDisabled('Retain pruned package versions.') + ->children() + ->booleanNode('tagged_versions') + ->defaultTrue() + ->info('Retain pruned tagged package versions.') + ->end() + ->booleanNode('dev_versions') + ->defaultFalse() + ->info('Retain pruned development package versions.') + ->end() + ->end() + ->end() + ->arrayNode('retain_stale_revisions') + ->canBeDisabled('Retain stale revisions of package versions.') + ->children() + ->booleanNode('tagged_versions') + ->defaultTrue() + ->info('Retain stale revisions of tagged package versions.') + ->end() + ->booleanNode('dev_versions') + ->defaultFalse() + ->info('Retain stale revisions of development package versions.') + ->end() + ->end() + ->end() ->end() ->end(); } diff --git a/src/DependencyInjection/DirigentExtension.php b/src/DependencyInjection/DirigentExtension.php index 2332e579..252a782c 100644 --- a/src/DependencyInjection/DirigentExtension.php +++ b/src/DependencyInjection/DirigentExtension.php @@ -56,11 +56,31 @@ private function registerEncryptionConfiguration(array $config, ContainerBuilder } /** - * @param array{mirror_vcs_repositories: bool} $config + * @param array{mirror_vcs_repositories: bool, retain_stale_revisions: array{enabled: bool, tagged_versions: bool, dev_versions: bool}, retain_pruned_versions: array{enabled: bool, tagged_versions: bool, dev_versions: bool}} $config */ private function registerMetadataConfiguration(array $config, ContainerBuilder $container): void { $container->setParameter('dirigent.metadata.mirror_vcs_repositories', $config['mirror_vcs_repositories']); + + $retainPrunedVersions = $config['retain_pruned_versions']['enabled']; + $container->setParameter( + name: 'dirigent.metadata.retain_pruned_versions.tagged_versions', + value: $retainPrunedVersions && $config['retain_pruned_versions']['tagged_versions'], + ); + $container->setParameter( + name: 'dirigent.metadata.retain_pruned_versions.dev_versions', + value: $retainPrunedVersions && $config['retain_pruned_versions']['dev_versions'], + ); + + $retainStaleRevisions = $config['retain_stale_revisions']['enabled']; + $container->setParameter( + name: 'dirigent.metadata.retain_stale_revisions.tagged_versions', + value: $retainStaleRevisions && $config['retain_stale_revisions']['tagged_versions'], + ); + $container->setParameter( + name: 'dirigent.metadata.retain_stale_revisions.dev_versions', + value: $retainStaleRevisions && $config['retain_stale_revisions']['dev_versions'], + ); } /** diff --git a/src/Doctrine/Entity/Version.php b/src/Doctrine/Entity/Version.php index 85546f54..bb5ec140 100644 --- a/src/Doctrine/Entity/Version.php +++ b/src/Doctrine/Entity/Version.php @@ -23,6 +23,9 @@ class Version extends TrackedEntity implements \Stringable #[ORM\Column] private bool $development; + #[ORM\Column] + private bool $pruned = false; + #[ORM\Column] private bool $defaultBranch = false; @@ -91,6 +94,16 @@ public function setDevelopment(bool $development): void $this->development = $development; } + public function isPruned(): bool + { + return $this->pruned; + } + + public function setPruned(bool $pruned): void + { + $this->pruned = $pruned; + } + public function isDefaultBranch(): bool { return $this->defaultBranch; diff --git a/src/Package/PackageMetadataResolver.php b/src/Package/PackageMetadataResolver.php index a34cf8ae..de0f9cb8 100644 --- a/src/Package/PackageMetadataResolver.php +++ b/src/Package/PackageMetadataResolver.php @@ -23,6 +23,7 @@ use Composer\Pcre\Preg; use Composer\Repository\Vcs\VcsDriverInterface; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; use Symfony\Component\Messenger\Stamp\TransportNamesStamp; @@ -36,6 +37,14 @@ public function __construct( private KeywordRepository $keywordRepository, private RegistryRepository $registryRepository, private PackageRepository $packageRepository, + #[Autowire(param: 'dirigent.metadata.retain_stale_revisions.tagged_versions')] + private bool $retainStaleRevisionsTagged, + #[Autowire(param: 'dirigent.metadata.retain_stale_revisions.dev_versions')] + private bool $retainStaleRevisionsDev, + #[Autowire(param: 'dirigent.metadata.retain_pruned_versions.tagged_versions')] + private bool $retainPrunedVersionsTagged, + #[Autowire(param: 'dirigent.metadata.retain_pruned_versions.dev_versions')] + private bool $retainPrunedVersionsDev, ) { } @@ -204,7 +213,15 @@ private function updatePackage(Package $package, array $composerPackages, ?VcsDr // Remove outdated versions foreach ($existingVersionMetadata as $version) { - $this->entityManager->remove($version); + $removeVersion = $version->isDevelopment() ? !$this->retainPrunedVersionsDev : !$this->retainPrunedVersionsTagged; + if ($removeVersion) { + $this->entityManager->remove($version); + } elseif (!$version->isPruned()) { + $version->setPruned(true); + $version->setUpdatedAt(new \DateTimeImmutable()); + + $this->entityManager->persist($version); + } } $package->setUpdatedAt(new \DateTimeImmutable()); @@ -214,15 +231,22 @@ private function updatePackage(Package $package, array $composerPackages, ?VcsDr private function updateVersion(Version $version, CompletePackageInterface $data, ?VcsDriverInterface $driver = null): void { + $currentMetadata = $version->hasCurrentMetadata() ? $version->getCurrentMetadata() : null; $metadata = $this->createMetadata($version, $data, $driver); - if (!$version->hasCurrentMetadata() || $this->hasMetadataChanged($version->getCurrentMetadata(), $metadata)) { + if (null === $currentMetadata || $this->hasMetadataChanged($currentMetadata, $metadata)) { $version->setCurrentMetadata($metadata); $this->entityManager->persist($metadata); + + $removePreviousMetadata = $version->isDevelopment() ? !$this->retainStaleRevisionsDev : !$this->retainStaleRevisionsTagged; + if (null !== $currentMetadata && $removePreviousMetadata) { + $this->entityManager->remove($currentMetadata); + } } $version->setDefaultBranch($data->isDefaultBranch()); + $version->setPruned(false); $version->setUpdatedAt(new \DateTimeImmutable()); $this->entityManager->persist($version);