From 086923f8beeb1129d3ed2e802f19e3277dcfb20f Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 09:13:03 +1000 Subject: [PATCH 01/10] [#2596] Added per-major stable resolution and cross-major gate to installer. --- .../installer/src/Command/InstallCommand.php | 39 +++++- .../src/Downloader/RepositoryDownloader.php | 16 ++- .vortex/installer/src/Utils/Version.php | 102 +++++++++++++++ .../Functional/Command/InstallCommandTest.php | 73 +++++++++++ .../Downloader/RepositoryDownloaderTest.php | 54 ++++++++ .vortex/installer/tests/Unit/VersionTest.php | 118 ++++++++++++++++++ 6 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 .vortex/installer/src/Utils/Version.php create mode 100644 .vortex/installer/tests/Unit/VersionTest.php diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php index 4d1f5151b..e8620f894 100644 --- a/.vortex/installer/src/Command/InstallCommand.php +++ b/.vortex/installer/src/Command/InstallCommand.php @@ -25,6 +25,7 @@ use DrevOps\VortexInstaller\Utils\FileManager; use DrevOps\VortexInstaller\Utils\OptionsResolver; use DrevOps\VortexInstaller\Utils\Tui; +use DrevOps\VortexInstaller\Utils\Version; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -186,6 +187,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->presenter->header($this->artifact, $this->getApplication()->getVersion()); + $this->assertMajorCompatibility(); + // Only validate if using custom repository or custom reference. if (!$this->artifact->isDefault()) { Task::action( @@ -218,7 +221,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int Task::action( label: 'Downloading Vortex', action: function (): string { - $version = $this->getRepositoryDownloader()->download($this->artifact, $this->config->get(Config::TMP)); + $release_prefix = Version::releasePrefix($this->getApplication()->getVersion()); + $version = $this->getRepositoryDownloader()->download($this->artifact, $this->config->get(Config::TMP), $release_prefix); $this->config->set(Config::VERSION, $version); return $version; }, @@ -388,6 +392,39 @@ protected function runBuildCommand(OutputInterface $output): bool { return $runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; } + /** + * Refuse to operate across major versions. + * + * Each installer build serves a single major line. Updating an existing + * project of a different major would cross a breaking boundary, so stop and + * point the user at the matching installer instead. Fresh installs and + * projects whose major cannot be determined are treated as compatible. + * + * @throws \RuntimeException + * When the destination project's major differs from this installer's major. + */ + protected function assertMajorCompatibility(): void { + if (!$this->config->isVortexProject()) { + return; + } + + $installer_major = Version::major($this->getApplication()->getVersion()); + if ($installer_major === NULL) { + return; + } + + $project_major = Version::detectProjectMajor((string) $this->config->getDst()); + if ($project_major === NULL || $project_major === $installer_major) { + return; + } + + throw new \RuntimeException(sprintf( + 'This installer targets Vortex %1$d.x, but the destination is a Vortex %2$d.x project. Update it with the %2$d.x installer instead: https://www.vortextemplate.com/v%2$d/install', + $installer_major, + $project_major, + )); + } + /** * Clean up installer artifacts. */ diff --git a/.vortex/installer/src/Downloader/RepositoryDownloader.php b/.vortex/installer/src/Downloader/RepositoryDownloader.php index abb33378a..c1f21b24f 100644 --- a/.vortex/installer/src/Downloader/RepositoryDownloader.php +++ b/.vortex/installer/src/Downloader/RepositoryDownloader.php @@ -46,9 +46,9 @@ public function __construct( ) { } - public function download(Artifact $artifact, ?string $dst = NULL): string { + public function download(Artifact $artifact, ?string $dst = NULL, ?string $release_prefix = NULL): string { if ($artifact->isRemote()) { - $version = $this->downloadFromRemote($artifact, $dst); + $version = $this->downloadFromRemote($artifact, $dst, $release_prefix); } else { $version = $this->downloadFromLocal($artifact, $dst); @@ -97,7 +97,7 @@ public function validate(Artifact $artifact): void { } } - protected function downloadFromRemote(Artifact $artifact, ?string $destination): string { + protected function downloadFromRemote(Artifact $artifact, ?string $destination, ?string $release_prefix = NULL): string { if ($destination === NULL) { throw new \InvalidArgumentException('Destination cannot be null for remote downloads.'); } @@ -108,7 +108,15 @@ protected function downloadFromRemote(Artifact $artifact, ?string $destination): $version = $artifact->getRef(); if ($artifact->getRef() === RepositoryDownloader::REF_STABLE) { - $ref = $this->discoverLatestReleaseRemote($repo_url); + $ref = $this->discoverLatestReleaseRemote($repo_url, $release_prefix); + + if ($ref === NULL && $release_prefix !== NULL) { + // No tagged release exists yet for this major (for example, an + // unreleased next major). Fall back to the major development branch + // (for example "2.x") so the line is still installable. + $ref = rtrim($release_prefix, '.') . '.x'; + $this->validateRemoteRefExists($repo_url, $ref); + } if ($ref === NULL) { throw new \RuntimeException(sprintf('Unable to discover the latest release for "%s".', $repo_url)); diff --git a/.vortex/installer/src/Utils/Version.php b/.vortex/installer/src/Utils/Version.php new file mode 100644 index 000000000..60be5ca28 --- /dev/null +++ b/.vortex/installer/src/Utils/Version.php @@ -0,0 +1,102 @@ +createMock(ExecutableFinder::class); + $executable_finder->method('find')->willReturnCallback(fn(string $command): string => '/usr/bin/' . $command); + + $install_command = new InstallCommand(); + $install_command->setExecutableFinder($executable_finder); + + if ($mock_download_fail) { + $mock_downloader = $this->createMock(RepositoryDownloader::class); + $mock_downloader->method('download')->willThrowException(new \RuntimeException('Failed to download Vortex.')); + $install_command->setRepositoryDownloader($mock_downloader); + } + + // Pre-populate the destination so it looks like an existing Vortex project: + // the README badge flags it as a Vortex project and the composer.json + // 'drevops/vortex-tooling' constraint carries the project's major. + file_put_contents(self::$sut . '/README.md', '[![Vortex](https://img.shields.io/badge/Vortex-1.40.0-65ACBC.svg)](https://github.com/drevops/vortex)'); + file_put_contents(self::$sut . '/composer.json', $composer_json); + + static::applicationInitFromCommand($install_command); + + if ($version !== NULL) { + $this->applicationGet()->setVersion($version); + } + + $this->applicationRun([ + '--' . InstallCommand::OPTION_NO_INTERACTION => TRUE, + '--' . InstallCommand::OPTION_URI => File::dir(static::$root), + '--' . InstallCommand::OPTION_DESTINATION => self::$sut, + ], [], TRUE); + + $this->assertApplicationAnyOutputContainsOrNot($expected_message); + } + + /** + * Data provider for testInstallCommandMajorGate(). + * + * @return array + * Test data. + */ + public static function dataProviderMajorGate(): array { + return [ + 'v1 installer refuses v2 project' => [ + '1.40.0', + '{"require": {"drevops/vortex-tooling": "^2.0.0"}}', + FALSE, + 'https://www.vortextemplate.com/v2/install', + ], + 'v1 installer accepts v1 project' => [ + '1.40.0', + '{"require": {"drevops/vortex-tooling": "^1.1.0"}}', + TRUE, + 'Failed to download Vortex.', + ], + 'unstamped installer skips gate' => [ + NULL, + '{"require": {"drevops/vortex-tooling": "^2.0.0"}}', + TRUE, + 'Failed to download Vortex.', + ], + 'undetectable project major skips gate' => [ + '1.40.0', + '{"require": {"php": ">=8.3"}}', + TRUE, + 'Failed to download Vortex.', + ], + ]; + } + } diff --git a/.vortex/installer/tests/Unit/Downloader/RepositoryDownloaderTest.php b/.vortex/installer/tests/Unit/Downloader/RepositoryDownloaderTest.php index 6a2638fe1..eeef6f88a 100644 --- a/.vortex/installer/tests/Unit/Downloader/RepositoryDownloaderTest.php +++ b/.vortex/installer/tests/Unit/Downloader/RepositoryDownloaderTest.php @@ -169,6 +169,60 @@ public function testDownloadFromRemoteWithGitSuffix(): void { $this->assertEquals('develop', $version); } + public function testDownloadStableWithReleasePrefixSelectsMajor(): void { + $release_json = (string) json_encode([ + ['tag_name' => '2.0.0', 'draft' => FALSE], + ['tag_name' => '1.40.0', 'draft' => FALSE], + ['tag_name' => '1.39.0', 'draft' => FALSE], + ]); + + $mock_http_client = $this->createMock(ClientInterface::class); + $mock_http_client->method('request')->willReturnCallback(function (string $method, string $url) use ($release_json): ResponseInterface { + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $body = $this->createMock(StreamInterface::class); + $body->method('getContents')->willReturn(str_contains($url, '/releases') ? $release_json : 'ok'); + $response->method('getBody')->willReturn($body); + return $response; + }); + + $mock_archiver = $this->createMock(ArchiverInterface::class); + $mock_file_downloader = $this->createMockFileDownloader(); + $destination = self::$tmp . '/destination_' . uniqid(); + File::mkdir($destination); + File::dump($destination . '/composer.json', '{}'); + + $downloader = new RepositoryDownloader($mock_http_client, $mock_archiver, NULL, $mock_file_downloader); + $version = $downloader->download(Artifact::create('https://github.com/user/repo', 'stable'), $destination, '1.'); + $this->assertEquals('1.40.0', $version); + } + + public function testDownloadStableWithReleasePrefixFallsBackToBranch(): void { + $release_json = (string) json_encode([ + ['tag_name' => '1.40.0', 'draft' => FALSE], + ]); + + $mock_http_client = $this->createMock(ClientInterface::class); + $mock_http_client->method('request')->willReturnCallback(function (string $method, string $url) use ($release_json): ResponseInterface { + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $body = $this->createMock(StreamInterface::class); + $body->method('getContents')->willReturn(str_contains($url, '/releases') ? $release_json : 'ok'); + $response->method('getBody')->willReturn($body); + return $response; + }); + + $mock_archiver = $this->createMock(ArchiverInterface::class); + $mock_file_downloader = $this->createMockFileDownloader(); + $destination = self::$tmp . '/destination_' . uniqid(); + File::mkdir($destination); + File::dump($destination . '/composer.json', '{}'); + + $downloader = new RepositoryDownloader($mock_http_client, $mock_archiver, NULL, $mock_file_downloader); + $version = $downloader->download(Artifact::create('https://github.com/user/repo', 'stable'), $destination, '2.'); + $this->assertEquals('2.x', $version); + } + #[DataProvider('dataProviderDownloadWithNullDestination')] public function testDownloadWithNullDestination(string $repo, string $expectedMessage): void { $downloader = new RepositoryDownloader(); diff --git a/.vortex/installer/tests/Unit/VersionTest.php b/.vortex/installer/tests/Unit/VersionTest.php new file mode 100644 index 000000000..504cf3e78 --- /dev/null +++ b/.vortex/installer/tests/Unit/VersionTest.php @@ -0,0 +1,118 @@ +assertSame($expected, Version::major($version)); + } + + /** + * Data provider for testMajor(). + * + * @return array + * Test data. + */ + public static function dataProviderMajor(): array { + return [ + 'null' => [NULL, NULL], + 'empty' => ['', NULL], + 'develop' => ['develop', NULL], + 'unstamped token' => ['@vortex-installer-version@', NULL], + 'semver 1.x' => ['1.40.0', 1], + 'semver with v prefix' => ['v1.2.3', 1], + 'semver 2.x' => ['2.0.0', 2], + 'dev branch' => ['2.x-dev', 2], + 'semver+calver' => ['1.0.0+2025.11.0', 1], + 'legacy calver' => ['25.10.0', 25], + 'leading whitespace' => [' 1.0.0', 1], + ]; + } + + #[DataProvider('dataProviderReleasePrefix')] + public function testReleasePrefix(?string $version, ?string $expected): void { + $this->assertSame($expected, Version::releasePrefix($version)); + } + + /** + * Data provider for testReleasePrefix(). + * + * @return array + * Test data. + */ + public static function dataProviderReleasePrefix(): array { + return [ + 'null' => [NULL, NULL], + 'develop' => ['develop', NULL], + 'major 1' => ['1.40.0', '1.'], + 'major 2 dev' => ['2.x-dev', '2.'], + 'legacy calver' => ['25.10.0', '25.'], + ]; + } + + #[DataProvider('dataProviderMajorFromConstraint')] + public function testMajorFromConstraint(?string $constraint, ?int $expected): void { + $this->assertSame($expected, Version::majorFromConstraint($constraint)); + } + + /** + * Data provider for testMajorFromConstraint(). + * + * @return array + * Test data. + */ + public static function dataProviderMajorFromConstraint(): array { + return [ + 'null' => [NULL, NULL], + 'empty' => ['', NULL], + 'no digits' => ['dev-main', NULL], + 'caret 1' => ['^1.1.0', 1], + 'caret 2' => ['^2.0.0', 2], + 'tilde 2' => ['~2.0', 2], + 'dev branch' => ['2.x-dev', 2], + 'range' => ['>=1.2 <3.0', 1], + ]; + } + + #[DataProvider('dataProviderDetectProjectMajor')] + public function testDetectProjectMajor(?string $composer_json, ?int $expected): void { + $dir = self::$tmp . '/project_' . uniqid(); + File::mkdir($dir); + + if ($composer_json !== NULL) { + File::dump($dir . '/composer.json', $composer_json); + } + + $this->assertSame($expected, Version::detectProjectMajor($dir)); + } + + /** + * Data provider for testDetectProjectMajor(). + * + * @return array + * Test data. + */ + public static function dataProviderDetectProjectMajor(): array { + return [ + 'no composer.json' => [NULL, NULL], + 'invalid json' => ['not json', NULL], + 'empty object' => ['{}', NULL], + 'no require' => ['{"name": "test/test"}', NULL], + 'no tooling' => ['{"require": {"php": ">=8.3"}}', NULL], + 'tooling v1' => ['{"require": {"drevops/vortex-tooling": "^1.1.0"}}', 1], + 'tooling v2' => ['{"require": {"drevops/vortex-tooling": "^2.0.0"}}', 2], + 'tooling non-string' => ['{"require": {"drevops/vortex-tooling": 1}}', NULL], + ]; + } + +} From 220041d68a5876cff34d7d839920443c63a5b66e Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 09:26:28 +1000 Subject: [PATCH 02/10] [#2596] Pinned 'update-vortex' to the major-specific '/v1/install' URL. --- .vortex/tooling/src/update-vortex | 6 +++- .vortex/tooling/tests/unit/update-vortex.bats | 32 +++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.vortex/tooling/src/update-vortex b/.vortex/tooling/src/update-vortex index d4cdc0f4d..e16e25434 100755 --- a/.vortex/tooling/src/update-vortex +++ b/.vortex/tooling/src/update-vortex @@ -29,7 +29,11 @@ set -eu VORTEX_INSTALLER_TEMPLATE_REPO="${VORTEX_INSTALLER_TEMPLATE_REPO:-https://github.com/drevops/vortex.git#stable}" # The URL of the installer script. -VORTEX_INSTALLER_URL="${VORTEX_INSTALLER_URL:-https://www.vortextemplate.com/install}" +# +# Pinned to the major-specific path so an existing project always updates +# within its own major line. The bare '/install' tracks the active major and +# would jump a project across a major boundary once the next major is released. +VORTEX_INSTALLER_URL="${VORTEX_INSTALLER_URL:-https://www.vortextemplate.com/v1/install}" # Cache busting parameter for the installer URL. VORTEX_INSTALLER_URL_CACHE_BUST="${VORTEX_INSTALLER_URL_CACHE_BUST:-"$(date +%s)"}" diff --git a/.vortex/tooling/tests/unit/update-vortex.bats b/.vortex/tooling/tests/unit/update-vortex.bats index 42e4fcb6f..4c094877e 100644 --- a/.vortex/tooling/tests/unit/update-vortex.bats +++ b/.vortex/tooling/tests/unit/update-vortex.bats @@ -18,9 +18,9 @@ load ../_helper.bash # Test default values when no environment variables are set. declare -a STEPS=( - "@curl -fsSL https://www.vortextemplate.com/install?1234567890 -o installer.php # 0" + "@curl -fsSL https://www.vortextemplate.com/v1/install?1234567890 -o installer.php # 0" "@php installer.php --no-interaction --uri=https://github.com/drevops/vortex.git\#stable # 0" - "Using installer script from URL: https://www.vortextemplate.com/install" + "Using installer script from URL: https://www.vortextemplate.com/v1/install" "Downloading installer to installer.php" ) @@ -42,9 +42,9 @@ load ../_helper.bash export VORTEX_INSTALLER_URL_CACHE_BUST="1234567890" declare -a STEPS=( - "@curl -fsSL https://www.vortextemplate.com/install?1234567890 -o installer.php # 0" + "@curl -fsSL https://www.vortextemplate.com/v1/install?1234567890 -o installer.php # 0" "@php installer.php --no-interaction --uri=https://github.com/custom/repo.git\#main # 0" - "Using installer script from URL: https://www.vortextemplate.com/install" + "Using installer script from URL: https://www.vortextemplate.com/v1/install" "Downloading installer to installer.php" ) @@ -117,9 +117,9 @@ load ../_helper.bash export VORTEX_INSTALLER_URL_CACHE_BUST="1234567890" declare -a STEPS=( - "@curl -fsSL https://www.vortextemplate.com/install?1234567890 -o installer.php # 0" + "@curl -fsSL https://www.vortextemplate.com/v1/install?1234567890 -o installer.php # 0" "@php installer.php --no-interaction --uri=file:///local/path/to/vortex # 0" - "Using installer script from URL: https://www.vortextemplate.com/install" + "Using installer script from URL: https://www.vortextemplate.com/v1/install" "Downloading installer to installer.php" ) @@ -141,9 +141,9 @@ load ../_helper.bash export VORTEX_INSTALLER_URL_CACHE_BUST="1234567890" declare -a STEPS=( - "@curl -fsSL https://www.vortextemplate.com/install?1234567890 -o installer.php # 0" + "@curl -fsSL https://www.vortextemplate.com/v1/install?1234567890 -o installer.php # 0" "@php installer.php --no-interaction --uri=/local/path/to/vortex\#stable # 0" - "Using installer script from URL: https://www.vortextemplate.com/install" + "Using installer script from URL: https://www.vortextemplate.com/v1/install" "Downloading installer to installer.php" ) @@ -165,9 +165,9 @@ load ../_helper.bash export VORTEX_INSTALLER_URL_CACHE_BUST="1234567890" declare -a STEPS=( - "@curl -fsSL https://www.vortextemplate.com/install?1234567890 -o installer.php # 0" + "@curl -fsSL https://www.vortextemplate.com/v1/install?1234567890 -o installer.php # 0" "@php installer.php --no-interaction --uri=git@github.com:drevops/vortex.git\#v1.2.3 # 0" - "Using installer script from URL: https://www.vortextemplate.com/install" + "Using installer script from URL: https://www.vortextemplate.com/v1/install" "Downloading installer to installer.php" ) @@ -189,9 +189,9 @@ load ../_helper.bash export VORTEX_INSTALLER_URL_CACHE_BUST="1234567890" declare -a STEPS=( - "@curl -fsSL https://www.vortextemplate.com/install?1234567890 -o installer.php # 0" + "@curl -fsSL https://www.vortextemplate.com/v1/install?1234567890 -o installer.php # 0" "@php installer.php --no-interaction --uri=https://github.com/drevops/vortex.git\#stable # 1" - "Using installer script from URL: https://www.vortextemplate.com/install" + "Using installer script from URL: https://www.vortextemplate.com/v1/install" "Downloading installer to installer.php" ) @@ -213,9 +213,9 @@ load ../_helper.bash export VORTEX_INSTALLER_URL_CACHE_BUST="1234567890" declare -a STEPS=( - "@curl -fsSL https://www.vortextemplate.com/install?1234567890 -o installer.php # 0" + "@curl -fsSL https://www.vortextemplate.com/v1/install?1234567890 -o installer.php # 0" "@php installer.php --uri=https://github.com/drevops/vortex.git\#stable # 0" - "Using installer script from URL: https://www.vortextemplate.com/install" + "Using installer script from URL: https://www.vortextemplate.com/v1/install" "Downloading installer to installer.php" ) @@ -237,9 +237,9 @@ load ../_helper.bash export VORTEX_INSTALLER_URL_CACHE_BUST="1234567890" declare -a STEPS=( - "@curl -fsSL https://www.vortextemplate.com/install?1234567890 -o installer.php # 0" + "@curl -fsSL https://www.vortextemplate.com/v1/install?1234567890 -o installer.php # 0" "@php installer.php --uri=https://github.com/custom/repo.git\#main # 0" - "Using installer script from URL: https://www.vortextemplate.com/install" + "Using installer script from URL: https://www.vortextemplate.com/v1/install" "Downloading installer to installer.php" ) From beb1698a6ef3794c88b2b37715269819e8ec4eea Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 09:26:36 +1000 Subject: [PATCH 03/10] [#2596] Documented per-major installer URLs and the 2.x branch naming. --- .../content/contributing/maintenance/release.mdx | 6 +++--- .vortex/docs/content/installation.mdx | 13 +++++++++++++ .vortex/docs/content/updating-vortex.mdx | 10 ++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.vortex/docs/content/contributing/maintenance/release.mdx b/.vortex/docs/content/contributing/maintenance/release.mdx index d0c9f5187..d5ee85100 100644 --- a/.vortex/docs/content/contributing/maintenance/release.mdx +++ b/.vortex/docs/content/contributing/maintenance/release.mdx @@ -44,11 +44,11 @@ When creating a new release, determine the version based on the changes: ### Branch strategy - **`main`** - Current stable release branch (always the latest major version) -- **`project/X.x`** - Development branch for the next major version (e.g., `project/2.x`) -- **`X.x`** - Maintenance branches for older major versions (e.g., `1.x` after `2.x` is released) +- **`X.x`** - Development branch for the next major version, ahead of `main` (e.g., `2.x`) +- **`X.x`** - Maintenance branch for an older major version, behind `main` (e.g., `1.x` after `2.x` is released) When a new major version is ready: -1. The next major version development in `project/X.x` is merged into `main` +1. The next major version development in `X.x` is merged into `main` 2. The previous major version code is preserved in its own `X.x` branch for maintenance :::note diff --git a/.vortex/docs/content/installation.mdx b/.vortex/docs/content/installation.mdx index 002aadc08..c54e5d5ed 100644 --- a/.vortex/docs/content/installation.mdx +++ b/.vortex/docs/content/installation.mdx @@ -44,6 +44,19 @@ curl -SsL https://www.vortextemplate.com/install > installer.php && php installe ::: +:::tip Choosing a Vortex version + +The command above installs the latest stable **Vortex** release. To target a +specific major version instead, use its dedicated path: + +- `https://www.vortextemplate.com/v1/install` - the current `1.x` line. +- `https://www.vortextemplate.com/v2/install` - the `2.x` line, currently in development. + +Once installed, `ahoy update-vortex` keeps your project on its major version - it +never jumps across a major release. See [Updating Vortex](./updating-vortex). + +::: + ### 2. Commit the initial project structure ```shell diff --git a/.vortex/docs/content/updating-vortex.mdx b/.vortex/docs/content/updating-vortex.mdx index 34847ea18..7faa6fe3f 100644 --- a/.vortex/docs/content/updating-vortex.mdx +++ b/.vortex/docs/content/updating-vortex.mdx @@ -21,6 +21,16 @@ After the update, review and commit the changes to your project to maintain vers We recommend updating **Vortex** monthly or at least quarterly to keep your project secure and up-to-date. +:::note Staying on your major version + +`ahoy update-vortex` updates your project **within its current major version** - a +`1.x` project receives the latest `1.x` release and is never moved to `2.x` +automatically. Moving to a new major is a deliberate step: run that major's +installer (for example `https://www.vortextemplate.com/v2/install`) against your +project. + +::: + ## Before updating Check the release notes for the latest version to understand what has changed From 3e3ac4f1efe3aed8ea2fe39d5aec718b4fd1b45e Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 11:12:06 +1000 Subject: [PATCH 04/10] [#2596] Built and published per-major installers at '/v1/install' and '/v2/install' in CI. --- .github/workflows/vortex-release.yml | 47 ++++++++++++++++++++++++-- .github/workflows/vortex-test-docs.yml | 26 ++++++++++++++ .vortex/docs/.gitignore | 4 ++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/.github/workflows/vortex-release.yml b/.github/workflows/vortex-release.yml index a799de9cc..023f71179 100644 --- a/.github/workflows/vortex-release.yml +++ b/.github/workflows/vortex-release.yml @@ -92,6 +92,32 @@ jobs: path: .vortex/installer/build/installer.phar if-no-files-found: error + # Build the next-major (2.x) installer from the '2.x' branch so it can be + # published alongside the current one. Checked out into a separate path so + # the current installer build above is untouched. Mirrors how the docs job + # pulls the '2.x' branch content. + - name: Checkout 2.x branch + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: 2.x + path: vortex-next + persist-credentials: false + + - name: Build 2.x installer + run: | + composer install + sed -i "s/\"vortex-installer-version\": \"development\"/\"vortex-installer-version\": \"2.x-dev\"/g" box.json + composer build + ./build/installer.phar --version + working-directory: vortex-next/.vortex/installer + + - name: Upload 2.x installer artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: vortex-installer-next + path: vortex-next/.vortex/installer/build/installer.phar + if-no-files-found: error + vortex-release-docs: needs: vortex-release-installer @@ -134,15 +160,30 @@ jobs: with: php-version: 8.3 - - name: Download installer + - name: Download current installer uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: vortex-installer + path: installer-current - - name: Copy installer to docs + - name: Download 2.x installer + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: vortex-installer-next + path: installer-next + + # Publish both installers. '/v1/install' and '/v2/install' are the stable + # per-major pins; the bare '/install' tracks the active major, selected by + # the 'VORTEX_INSTALLER_ACTIVE_MAJOR' repository variable (default 1). + - name: Copy installers to docs run: | - cp ../installer.phar ../.vortex/docs/static/install + mkdir -p ../.vortex/docs/static/v1 ../.vortex/docs/static/v2 + cp ../installer-current/installer.phar ../.vortex/docs/static/v1/install + cp ../installer-next/installer.phar ../.vortex/docs/static/v2/install + cp "../.vortex/docs/static/v${ACTIVE_MAJOR}/install" ../.vortex/docs/static/install php ../.vortex/docs/static/install --version + env: + ACTIVE_MAJOR: ${{ vars.VORTEX_INSTALLER_ACTIVE_MAJOR || '1' }} - name: Check docs up-to-date run: | diff --git a/.github/workflows/vortex-test-docs.yml b/.github/workflows/vortex-test-docs.yml index b98016932..3c56e9a47 100644 --- a/.github/workflows/vortex-test-docs.yml +++ b/.github/workflows/vortex-test-docs.yml @@ -102,6 +102,32 @@ jobs: git -C "${{ github.workspace }}" checkout origin/2.x -- .vortex/docs/content || { echo "Failed to check out content from 2.x."; exit 1; } working-directory: '${{ github.workspace }}/.vortex/docs' + # On the main deploy, publish both installers to match the multi-version + # docs: '/v1/install' and '/v2/install' are the stable per-major pins and + # the bare '/install' tracks the active major (the + # 'VORTEX_INSTALLER_ACTIVE_MAJOR' repository variable, default 1). The 2.x + # installer is built from the '2.x' branch. Mirrors 'vortex-release.yml'. + - name: Checkout 2.x branch + if: github.event.workflow_run.head_branch == 'main' + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: 2.x + path: vortex-next + persist-credentials: false + + - name: Build and publish 2.x installer + if: github.event.workflow_run.head_branch == 'main' + run: | + composer --working-dir=vortex-next/.vortex/installer install + sed -i "s/\"vortex-installer-version\": \"development\"/\"vortex-installer-version\": \"2.x-dev\"/g" vortex-next/.vortex/installer/box.json + composer --working-dir=vortex-next/.vortex/installer build + mkdir -p .vortex/docs/static/v1 .vortex/docs/static/v2 + cp .vortex/docs/static/install .vortex/docs/static/v1/install + cp vortex-next/.vortex/installer/build/installer.phar .vortex/docs/static/v2/install + cp ".vortex/docs/static/v${ACTIVE_MAJOR}/install" .vortex/docs/static/install + env: + ACTIVE_MAJOR: ${{ vars.VORTEX_INSTALLER_ACTIVE_MAJOR || '1' }} + - name: Build documentation site run: yarn run build working-directory: '${{ github.workspace }}/.vortex/docs' diff --git a/.vortex/docs/.gitignore b/.vortex/docs/.gitignore index 88b8f32d2..7fa5d971f 100644 --- a/.vortex/docs/.gitignore +++ b/.vortex/docs/.gitignore @@ -28,5 +28,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -# Copied installer file. +# Copied installer files (the bare active-major path and the per-major pins). static/install +static/v1/install +static/v2/install From 4215065ac94a899a1722eb118b25d1e7400e4886 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 11:26:51 +1000 Subject: [PATCH 05/10] [#2596] Routed the bare installer path through the active-major CI variable. --- .github/workflows/vortex-release.yml | 10 +++++++--- .github/workflows/vortex-test-docs.yml | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/vortex-release.yml b/.github/workflows/vortex-release.yml index 023f71179..b090231b3 100644 --- a/.github/workflows/vortex-release.yml +++ b/.github/workflows/vortex-release.yml @@ -172,13 +172,17 @@ jobs: name: vortex-installer-next path: installer-next - # Publish both installers. '/v1/install' and '/v2/install' are the stable - # per-major pins; the bare '/install' tracks the active major, selected by + # Publish both installers. 'installer-current' is built from the release + # ref - the active major (the v1 line today); 'installer-next' is built + # from the '2.x' branch (v2). '/v1/install' and '/v2/install' are the + # stable per-major pins; the bare '/install' tracks the active major via # the 'VORTEX_INSTALLER_ACTIVE_MAJOR' repository variable (default 1). + # Promoting 2.x to active is a deliberate future change to that variable + # and to the build sources above, mirroring the docs 'lastVersion' flip. - name: Copy installers to docs run: | mkdir -p ../.vortex/docs/static/v1 ../.vortex/docs/static/v2 - cp ../installer-current/installer.phar ../.vortex/docs/static/v1/install + cp ../installer-current/installer.phar "../.vortex/docs/static/v${ACTIVE_MAJOR}/install" cp ../installer-next/installer.phar ../.vortex/docs/static/v2/install cp "../.vortex/docs/static/v${ACTIVE_MAJOR}/install" ../.vortex/docs/static/install php ../.vortex/docs/static/install --version diff --git a/.github/workflows/vortex-test-docs.yml b/.github/workflows/vortex-test-docs.yml index 3c56e9a47..94118b979 100644 --- a/.github/workflows/vortex-test-docs.yml +++ b/.github/workflows/vortex-test-docs.yml @@ -122,7 +122,7 @@ jobs: sed -i "s/\"vortex-installer-version\": \"development\"/\"vortex-installer-version\": \"2.x-dev\"/g" vortex-next/.vortex/installer/box.json composer --working-dir=vortex-next/.vortex/installer build mkdir -p .vortex/docs/static/v1 .vortex/docs/static/v2 - cp .vortex/docs/static/install .vortex/docs/static/v1/install + cp .vortex/docs/static/install ".vortex/docs/static/v${ACTIVE_MAJOR}/install" cp vortex-next/.vortex/installer/build/installer.phar .vortex/docs/static/v2/install cp ".vortex/docs/static/v${ACTIVE_MAJOR}/install" .vortex/docs/static/install env: From d495bb91bf5f488dfdc2186a20f60bca8a185c7f Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 11:27:12 +1000 Subject: [PATCH 06/10] [#2596] Clarified that the bare installer URL targets the current major. --- .vortex/docs/content/installation.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vortex/docs/content/installation.mdx b/.vortex/docs/content/installation.mdx index c54e5d5ed..286526e39 100644 --- a/.vortex/docs/content/installation.mdx +++ b/.vortex/docs/content/installation.mdx @@ -46,8 +46,8 @@ curl -SsL https://www.vortextemplate.com/install > installer.php && php installe :::tip Choosing a Vortex version -The command above installs the latest stable **Vortex** release. To target a -specific major version instead, use its dedicated path: +The command above installs the current stable **Vortex** release (the `1.x` line +today). To target a specific major version explicitly, use its dedicated path: - `https://www.vortextemplate.com/v1/install` - the current `1.x` line. - `https://www.vortextemplate.com/v2/install` - the `2.x` line, currently in development. From 8497755e2f19baa8c0b6bb6ef67dbd2723f61e95 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 11:55:00 +1000 Subject: [PATCH 07/10] [#2596] Aligned installer test data providers with project naming and yield conventions. --- .../Functional/Command/InstallCommandTest.php | 54 ++++++------ .vortex/installer/tests/Unit/VersionTest.php | 88 +++++++++---------- 2 files changed, 66 insertions(+), 76 deletions(-) diff --git a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php index 66e0cbdd1..b689a2ec7 100644 --- a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php @@ -499,7 +499,7 @@ public static function dataProviderInstallCommand(): \Iterator { /** * Test the cross-major compatibility gate. */ - #[DataProvider('dataProviderMajorGate')] + #[DataProvider('dataProviderInstallCommandMajorGate')] public function testInstallCommandMajorGate(?string $version, string $composer_json, bool $mock_download_fail, string $expected_message): void { $executable_finder = $this->createMock(ExecutableFinder::class); $executable_finder->method('find')->willReturnCallback(fn(string $command): string => '/usr/bin/' . $command); @@ -537,35 +537,33 @@ public function testInstallCommandMajorGate(?string $version, string $composer_j /** * Data provider for testInstallCommandMajorGate(). * - * @return array + * @return \Iterator * Test data. */ - public static function dataProviderMajorGate(): array { - return [ - 'v1 installer refuses v2 project' => [ - '1.40.0', - '{"require": {"drevops/vortex-tooling": "^2.0.0"}}', - FALSE, - 'https://www.vortextemplate.com/v2/install', - ], - 'v1 installer accepts v1 project' => [ - '1.40.0', - '{"require": {"drevops/vortex-tooling": "^1.1.0"}}', - TRUE, - 'Failed to download Vortex.', - ], - 'unstamped installer skips gate' => [ - NULL, - '{"require": {"drevops/vortex-tooling": "^2.0.0"}}', - TRUE, - 'Failed to download Vortex.', - ], - 'undetectable project major skips gate' => [ - '1.40.0', - '{"require": {"php": ">=8.3"}}', - TRUE, - 'Failed to download Vortex.', - ], + public static function dataProviderInstallCommandMajorGate(): \Iterator { + yield 'v1 installer refuses v2 project' => [ + '1.40.0', + '{"require": {"drevops/vortex-tooling": "^2.0.0"}}', + FALSE, + 'https://www.vortextemplate.com/v2/install', + ]; + yield 'v1 installer accepts v1 project' => [ + '1.40.0', + '{"require": {"drevops/vortex-tooling": "^1.1.0"}}', + TRUE, + 'Failed to download Vortex.', + ]; + yield 'unstamped installer skips gate' => [ + NULL, + '{"require": {"drevops/vortex-tooling": "^2.0.0"}}', + TRUE, + 'Failed to download Vortex.', + ]; + yield 'undetectable project major skips gate' => [ + '1.40.0', + '{"require": {"php": ">=8.3"}}', + TRUE, + 'Failed to download Vortex.', ]; } diff --git a/.vortex/installer/tests/Unit/VersionTest.php b/.vortex/installer/tests/Unit/VersionTest.php index 504cf3e78..25fee2290 100644 --- a/.vortex/installer/tests/Unit/VersionTest.php +++ b/.vortex/installer/tests/Unit/VersionTest.php @@ -20,23 +20,21 @@ public function testMajor(?string $version, ?int $expected): void { /** * Data provider for testMajor(). * - * @return array + * @return \Iterator * Test data. */ - public static function dataProviderMajor(): array { - return [ - 'null' => [NULL, NULL], - 'empty' => ['', NULL], - 'develop' => ['develop', NULL], - 'unstamped token' => ['@vortex-installer-version@', NULL], - 'semver 1.x' => ['1.40.0', 1], - 'semver with v prefix' => ['v1.2.3', 1], - 'semver 2.x' => ['2.0.0', 2], - 'dev branch' => ['2.x-dev', 2], - 'semver+calver' => ['1.0.0+2025.11.0', 1], - 'legacy calver' => ['25.10.0', 25], - 'leading whitespace' => [' 1.0.0', 1], - ]; + public static function dataProviderMajor(): \Iterator { + yield 'null' => [NULL, NULL]; + yield 'empty' => ['', NULL]; + yield 'develop' => ['develop', NULL]; + yield 'unstamped token' => ['@vortex-installer-version@', NULL]; + yield 'semver 1.x' => ['1.40.0', 1]; + yield 'semver with v prefix' => ['v1.2.3', 1]; + yield 'semver 2.x' => ['2.0.0', 2]; + yield 'dev branch' => ['2.x-dev', 2]; + yield 'semver+calver' => ['1.0.0+2025.11.0', 1]; + yield 'legacy calver' => ['25.10.0', 25]; + yield 'leading whitespace' => [' 1.0.0', 1]; } #[DataProvider('dataProviderReleasePrefix')] @@ -47,17 +45,15 @@ public function testReleasePrefix(?string $version, ?string $expected): void { /** * Data provider for testReleasePrefix(). * - * @return array + * @return \Iterator * Test data. */ - public static function dataProviderReleasePrefix(): array { - return [ - 'null' => [NULL, NULL], - 'develop' => ['develop', NULL], - 'major 1' => ['1.40.0', '1.'], - 'major 2 dev' => ['2.x-dev', '2.'], - 'legacy calver' => ['25.10.0', '25.'], - ]; + public static function dataProviderReleasePrefix(): \Iterator { + yield 'null' => [NULL, NULL]; + yield 'develop' => ['develop', NULL]; + yield 'major 1' => ['1.40.0', '1.']; + yield 'major 2 dev' => ['2.x-dev', '2.']; + yield 'legacy calver' => ['25.10.0', '25.']; } #[DataProvider('dataProviderMajorFromConstraint')] @@ -68,20 +64,18 @@ public function testMajorFromConstraint(?string $constraint, ?int $expected): vo /** * Data provider for testMajorFromConstraint(). * - * @return array + * @return \Iterator * Test data. */ - public static function dataProviderMajorFromConstraint(): array { - return [ - 'null' => [NULL, NULL], - 'empty' => ['', NULL], - 'no digits' => ['dev-main', NULL], - 'caret 1' => ['^1.1.0', 1], - 'caret 2' => ['^2.0.0', 2], - 'tilde 2' => ['~2.0', 2], - 'dev branch' => ['2.x-dev', 2], - 'range' => ['>=1.2 <3.0', 1], - ]; + public static function dataProviderMajorFromConstraint(): \Iterator { + yield 'null' => [NULL, NULL]; + yield 'empty' => ['', NULL]; + yield 'no digits' => ['dev-main', NULL]; + yield 'caret 1' => ['^1.1.0', 1]; + yield 'caret 2' => ['^2.0.0', 2]; + yield 'tilde 2' => ['~2.0', 2]; + yield 'dev branch' => ['2.x-dev', 2]; + yield 'range' => ['>=1.2 <3.0', 1]; } #[DataProvider('dataProviderDetectProjectMajor')] @@ -99,20 +93,18 @@ public function testDetectProjectMajor(?string $composer_json, ?int $expected): /** * Data provider for testDetectProjectMajor(). * - * @return array + * @return \Iterator * Test data. */ - public static function dataProviderDetectProjectMajor(): array { - return [ - 'no composer.json' => [NULL, NULL], - 'invalid json' => ['not json', NULL], - 'empty object' => ['{}', NULL], - 'no require' => ['{"name": "test/test"}', NULL], - 'no tooling' => ['{"require": {"php": ">=8.3"}}', NULL], - 'tooling v1' => ['{"require": {"drevops/vortex-tooling": "^1.1.0"}}', 1], - 'tooling v2' => ['{"require": {"drevops/vortex-tooling": "^2.0.0"}}', 2], - 'tooling non-string' => ['{"require": {"drevops/vortex-tooling": 1}}', NULL], - ]; + public static function dataProviderDetectProjectMajor(): \Iterator { + yield 'no composer.json' => [NULL, NULL]; + yield 'invalid json' => ['not json', NULL]; + yield 'empty object' => ['{}', NULL]; + yield 'no require' => ['{"name": "test/test"}', NULL]; + yield 'no tooling' => ['{"require": {"php": ">=8.3"}}', NULL]; + yield 'tooling v1' => ['{"require": {"drevops/vortex-tooling": "^1.1.0"}}', 1]; + yield 'tooling v2' => ['{"require": {"drevops/vortex-tooling": "^2.0.0"}}', 2]; + yield 'tooling non-string' => ['{"require": {"drevops/vortex-tooling": 1}}', NULL]; } } From 3c4e2b6276199da83b5bfedb32580a69509bd206 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 12:14:43 +1000 Subject: [PATCH 08/10] [#2596] Validated active-major CI input, aligned downloader interface, and clarified branch docs. --- .github/workflows/vortex-release.yml | 4 ++++ .github/workflows/vortex-test-docs.yml | 4 ++++ .vortex/docs/content/contributing/maintenance/release.mdx | 8 ++++---- .../src/Downloader/RepositoryDownloaderInterface.php | 6 +++++- .../tests/Functional/Command/InstallCommandTest.php | 4 ++-- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/vortex-release.yml b/.github/workflows/vortex-release.yml index b090231b3..d2f09d9cc 100644 --- a/.github/workflows/vortex-release.yml +++ b/.github/workflows/vortex-release.yml @@ -181,6 +181,10 @@ jobs: # and to the build sources above, mirroring the docs 'lastVersion' flip. - name: Copy installers to docs run: | + case "${ACTIVE_MAJOR}" in + 1|2) ;; + *) echo "Invalid VORTEX_INSTALLER_ACTIVE_MAJOR='${ACTIVE_MAJOR}'. Expected 1 or 2."; exit 1 ;; + esac mkdir -p ../.vortex/docs/static/v1 ../.vortex/docs/static/v2 cp ../installer-current/installer.phar "../.vortex/docs/static/v${ACTIVE_MAJOR}/install" cp ../installer-next/installer.phar ../.vortex/docs/static/v2/install diff --git a/.github/workflows/vortex-test-docs.yml b/.github/workflows/vortex-test-docs.yml index 94118b979..7461fa884 100644 --- a/.github/workflows/vortex-test-docs.yml +++ b/.github/workflows/vortex-test-docs.yml @@ -121,6 +121,10 @@ jobs: composer --working-dir=vortex-next/.vortex/installer install sed -i "s/\"vortex-installer-version\": \"development\"/\"vortex-installer-version\": \"2.x-dev\"/g" vortex-next/.vortex/installer/box.json composer --working-dir=vortex-next/.vortex/installer build + case "${ACTIVE_MAJOR}" in + 1|2) ;; + *) echo "Invalid VORTEX_INSTALLER_ACTIVE_MAJOR='${ACTIVE_MAJOR}'. Expected 1 or 2."; exit 1 ;; + esac mkdir -p .vortex/docs/static/v1 .vortex/docs/static/v2 cp .vortex/docs/static/install ".vortex/docs/static/v${ACTIVE_MAJOR}/install" cp vortex-next/.vortex/installer/build/installer.phar .vortex/docs/static/v2/install diff --git a/.vortex/docs/content/contributing/maintenance/release.mdx b/.vortex/docs/content/contributing/maintenance/release.mdx index d5ee85100..caf57773e 100644 --- a/.vortex/docs/content/contributing/maintenance/release.mdx +++ b/.vortex/docs/content/contributing/maintenance/release.mdx @@ -44,12 +44,12 @@ When creating a new release, determine the version based on the changes: ### Branch strategy - **`main`** - Current stable release branch (always the latest major version) -- **`X.x`** - Development branch for the next major version, ahead of `main` (e.g., `2.x`) -- **`X.x`** - Maintenance branch for an older major version, behind `main` (e.g., `1.x` after `2.x` is released) +- **`N.x`** - Development branch for the next major version, ahead of `main` (e.g., `2.x`) +- **`N-1.x`** - Maintenance branch for the previous major version, behind `main` (e.g., `1.x` after `2.x` is released) When a new major version is ready: -1. The next major version development in `X.x` is merged into `main` -2. The previous major version code is preserved in its own `X.x` branch for maintenance +1. The next major version development branch (`N.x`) is merged into `main` +2. The previous major version code is preserved in its own maintenance branch (`N-1.x`) :::note diff --git a/.vortex/installer/src/Downloader/RepositoryDownloaderInterface.php b/.vortex/installer/src/Downloader/RepositoryDownloaderInterface.php index 2e4dc3a42..1d35425f1 100644 --- a/.vortex/installer/src/Downloader/RepositoryDownloaderInterface.php +++ b/.vortex/installer/src/Downloader/RepositoryDownloaderInterface.php @@ -17,6 +17,10 @@ interface RepositoryDownloaderInterface { * @param string|null $dst * The destination directory. If NULL, a temporary directory will be used * for local repositories. + * @param string|null $release_prefix + * When the reference is "stable", restrict release discovery to tags + * starting with this prefix (for example "1." for the 1.x line). If no + * matching release exists, the "{major}.x" development branch is used. * * @return string * The version/reference that was downloaded. @@ -26,6 +30,6 @@ interface RepositoryDownloaderInterface { * @throws \InvalidArgumentException * If the destination is null for remote downloads. */ - public function download(Artifact $artifact, ?string $dst = NULL): string; + public function download(Artifact $artifact, ?string $dst = NULL, ?string $release_prefix = NULL): string; } diff --git a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php index b689a2ec7..a80cf409e 100644 --- a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php @@ -516,8 +516,8 @@ public function testInstallCommandMajorGate(?string $version, string $composer_j // Pre-populate the destination so it looks like an existing Vortex project: // the README badge flags it as a Vortex project and the composer.json // 'drevops/vortex-tooling' constraint carries the project's major. - file_put_contents(self::$sut . '/README.md', '[![Vortex](https://img.shields.io/badge/Vortex-1.40.0-65ACBC.svg)](https://github.com/drevops/vortex)'); - file_put_contents(self::$sut . '/composer.json', $composer_json); + $this->assertNotFalse(file_put_contents(self::$sut . '/README.md', '[![Vortex](https://img.shields.io/badge/Vortex-1.40.0-65ACBC.svg)](https://github.com/drevops/vortex)')); + $this->assertNotFalse(file_put_contents(self::$sut . '/composer.json', $composer_json)); static::applicationInitFromCommand($install_command); From 0233e8f3fec79b76b44018aa74795331482e0417 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 12:31:42 +1000 Subject: [PATCH 09/10] Re-triggered CI to clear a flaky check. From 868cf0402fa29aeee7d60fd14f4657d4d3f9ebd8 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 10 Jun 2026 13:29:42 +1000 Subject: [PATCH 10/10] [#2596] Made the major flip a single 'VORTEX_CURRENT_MAJOR' variable across docs and installer. --- .github/workflows/vortex-release.yml | 94 ++++++++++--------- .github/workflows/vortex-test-docs.yml | 56 ++++++----- .../contributing/maintenance/release.mdx | 14 +-- .vortex/docs/docusaurus.config.js | 39 ++++---- .../src/Downloader/RepositoryDownloader.php | 2 +- 5 files changed, 115 insertions(+), 90 deletions(-) diff --git a/.github/workflows/vortex-release.yml b/.github/workflows/vortex-release.yml index d2f09d9cc..0db1a74ce 100644 --- a/.github/workflows/vortex-release.yml +++ b/.github/workflows/vortex-release.yml @@ -30,6 +30,13 @@ jobs: outputs: release-version: ${{ steps.version.outputs.value }} + # The current major (built from this ref) and the other major (built from + # its '{N}.x' branch) are both derived from 'VORTEX_CURRENT_MAJOR' (default + # 1), so a single variable bump promotes a new major with no workflow edits. + env: + CURRENT_MAJOR: ${{ vars.VORTEX_CURRENT_MAJOR || '1' }} + OTHER_MAJOR: ${{ (vars.VORTEX_CURRENT_MAJOR || '1') == '1' && '2' || '1' }} + steps: - name: Checkout code uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -85,37 +92,37 @@ jobs: ./build/installer.phar --no-interaction --no-cleanup --destination=test || exit 1 working-directory: .vortex/installer - - name: Upload artifact + - name: Upload v${{ env.CURRENT_MAJOR }} installer artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: vortex-installer + name: vortex-installer-v${{ env.CURRENT_MAJOR }} path: .vortex/installer/build/installer.phar if-no-files-found: error - # Build the next-major (2.x) installer from the '2.x' branch so it can be + # Build the other major's installer from its '{N}.x' branch so it is # published alongside the current one. Checked out into a separate path so # the current installer build above is untouched. Mirrors how the docs job - # pulls the '2.x' branch content. - - name: Checkout 2.x branch + # pulls the other major's branch content. + - name: Checkout v${{ env.OTHER_MAJOR }} branch uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: - ref: 2.x - path: vortex-next + ref: ${{ env.OTHER_MAJOR }}.x + path: vortex-v${{ env.OTHER_MAJOR }} persist-credentials: false - - name: Build 2.x installer + - name: Build v${{ env.OTHER_MAJOR }} installer run: | composer install - sed -i "s/\"vortex-installer-version\": \"development\"/\"vortex-installer-version\": \"2.x-dev\"/g" box.json + sed -i "s/\"vortex-installer-version\": \"development\"/\"vortex-installer-version\": \"${OTHER_MAJOR}.x-dev\"/g" box.json composer build ./build/installer.phar --version - working-directory: vortex-next/.vortex/installer + working-directory: vortex-v${{ env.OTHER_MAJOR }}/.vortex/installer - - name: Upload 2.x installer artifact + - name: Upload v${{ env.OTHER_MAJOR }} installer artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: vortex-installer-next - path: vortex-next/.vortex/installer/build/installer.phar + name: vortex-installer-v${{ env.OTHER_MAJOR }} + path: vortex-v${{ env.OTHER_MAJOR }}/.vortex/installer/build/installer.phar if-no-files-found: error vortex-release-docs: @@ -135,6 +142,10 @@ jobs: run: working-directory: docs + env: + CURRENT_MAJOR: ${{ vars.VORTEX_CURRENT_MAJOR || '1' }} + OTHER_MAJOR: ${{ (vars.VORTEX_CURRENT_MAJOR || '1') == '1' && '2' || '1' }} + steps: - name: Checkout code uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -160,38 +171,34 @@ jobs: with: php-version: 8.3 - - name: Download current installer + - name: Download v1 installer uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: vortex-installer - path: installer-current + name: vortex-installer-v1 + path: installer-v1 - - name: Download 2.x installer + - name: Download v2 installer uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: vortex-installer-next - path: installer-next - - # Publish both installers. 'installer-current' is built from the release - # ref - the active major (the v1 line today); 'installer-next' is built - # from the '2.x' branch (v2). '/v1/install' and '/v2/install' are the - # stable per-major pins; the bare '/install' tracks the active major via - # the 'VORTEX_INSTALLER_ACTIVE_MAJOR' repository variable (default 1). - # Promoting 2.x to active is a deliberate future change to that variable - # and to the build sources above, mirroring the docs 'lastVersion' flip. + name: vortex-installer-v2 + path: installer-v2 + + # Publish both installers. '/v1/install' and '/v2/install' are the stable + # per-major pins, always built from each major's own source. The bare + # '/install' is a copy of the current major's pin, selected by the + # 'VORTEX_CURRENT_MAJOR' repository variable (default 1) - bumping that one + # variable is the only action needed to promote a new major. - name: Copy installers to docs run: | - case "${ACTIVE_MAJOR}" in + case "${CURRENT_MAJOR}" in 1|2) ;; - *) echo "Invalid VORTEX_INSTALLER_ACTIVE_MAJOR='${ACTIVE_MAJOR}'. Expected 1 or 2."; exit 1 ;; + *) echo "Invalid VORTEX_CURRENT_MAJOR='${CURRENT_MAJOR}'. Expected 1 or 2."; exit 1 ;; esac mkdir -p ../.vortex/docs/static/v1 ../.vortex/docs/static/v2 - cp ../installer-current/installer.phar "../.vortex/docs/static/v${ACTIVE_MAJOR}/install" - cp ../installer-next/installer.phar ../.vortex/docs/static/v2/install - cp "../.vortex/docs/static/v${ACTIVE_MAJOR}/install" ../.vortex/docs/static/install + cp ../installer-v1/installer.phar ../.vortex/docs/static/v1/install + cp ../installer-v2/installer.phar ../.vortex/docs/static/v2/install + cp "../.vortex/docs/static/v${CURRENT_MAJOR}/install" ../.vortex/docs/static/install php ../.vortex/docs/static/install --version - env: - ACTIVE_MAJOR: ${{ vars.VORTEX_INSTALLER_ACTIVE_MAJOR || '1' }} - name: Check docs up-to-date run: | @@ -203,22 +210,25 @@ jobs: run: yarn install --frozen-lockfile working-directory: .vortex/docs - # Assemble the multi-version site for production: snapshot the released - # docs as v1 (served at the bare '/docs') and pull the '2.x' - # docs in as v2 (at '/docs/v2'). Mirrors the same step in - # 'vortex-test-docs.yml' (development / Netlify). + # Assemble the multi-version site for production: snapshot this ref's docs + # as the current major (served at the bare '/docs') and pull the other + # major's '{N}.x' branch docs in at '/docs/v{other}'. All driven by + # 'VORTEX_CURRENT_MAJOR'. Mirrors the same step in 'vortex-test-docs.yml' + # (development / Netlify). - name: Assemble versioned docs run: | - yarn docusaurus docs:version 1.x - git -C "${{ github.workspace }}" fetch origin 2.x --depth=1 || { echo "Failed to fetch the 2.x branch."; exit 1; } - git -C "${{ github.workspace }}" rev-parse --verify origin/2.x || { echo "The 2.x branch does not exist."; exit 1; } + yarn docusaurus docs:version "${CURRENT_MAJOR}.x" + git -C "${{ github.workspace }}" fetch origin "${OTHER_MAJOR}.x" --depth=1 || { echo "Failed to fetch the ${OTHER_MAJOR}.x branch."; exit 1; } + git -C "${{ github.workspace }}" rev-parse --verify "origin/${OTHER_MAJOR}.x" || { echo "The ${OTHER_MAJOR}.x branch does not exist."; exit 1; } rm -rf content - git -C "${{ github.workspace }}" checkout origin/2.x -- .vortex/docs/content || { echo "Failed to check out content from 2.x."; exit 1; } + git -C "${{ github.workspace }}" checkout "origin/${OTHER_MAJOR}.x" -- .vortex/docs/content || { echo "Failed to check out content from ${OTHER_MAJOR}.x."; exit 1; } working-directory: .vortex/docs - name: Build documentation site run: yarn run build working-directory: .vortex/docs + env: + VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} - name: Generate video for installer (not used in the final artifact) run: | diff --git a/.github/workflows/vortex-test-docs.yml b/.github/workflows/vortex-test-docs.yml index 7461fa884..680595502 100644 --- a/.github/workflows/vortex-test-docs.yml +++ b/.github/workflows/vortex-test-docs.yml @@ -19,6 +19,10 @@ jobs: statuses: write # Post pending/final commit statuses via 'gh api repos/.../statuses/...'. pull-requests: write # Post the Netlify preview link comment on the originating PR. + env: + CURRENT_MAJOR: ${{ vars.VORTEX_CURRENT_MAJOR || '1' }} + OTHER_MAJOR: ${{ (vars.VORTEX_CURRENT_MAJOR || '1') == '1' && '2' || '1' }} + steps: # Post pending status to the PR commit. # Workflows triggered by workflow_run don't automatically report status @@ -88,53 +92,55 @@ jobs: working-directory: '${{ github.workspace }}/.vortex/docs' # On the main (development) deploy, assemble the multi-version site: - # snapshot main's docs as v1 (served at the bare '/docs') and pull the - # '2.x' docs in as v2 (at '/docs/v2'). Skipped on PR/branch - # builds, which stay single-version previews. Mirrors the same step in - # 'vortex-release.yml' (production / GitHub Pages). + # snapshot main's docs as the current major (served at the bare '/docs') + # and pull the other major's '{N}.x' branch docs in at '/docs/v{other}'. + # Driven by 'VORTEX_CURRENT_MAJOR'. Skipped on PR/branch builds, which stay + # single-version previews. Mirrors the same step in 'vortex-release.yml' + # (production / GitHub Pages). - name: Assemble versioned docs if: github.event.workflow_run.head_branch == 'main' run: | - yarn docusaurus docs:version 1.x - git -C "${{ github.workspace }}" fetch origin 2.x --depth=1 || { echo "Failed to fetch the 2.x branch."; exit 1; } - git -C "${{ github.workspace }}" rev-parse --verify origin/2.x || { echo "The 2.x branch does not exist."; exit 1; } + yarn docusaurus docs:version "${CURRENT_MAJOR}.x" + git -C "${{ github.workspace }}" fetch origin "${OTHER_MAJOR}.x" --depth=1 || { echo "Failed to fetch the ${OTHER_MAJOR}.x branch."; exit 1; } + git -C "${{ github.workspace }}" rev-parse --verify "origin/${OTHER_MAJOR}.x" || { echo "The ${OTHER_MAJOR}.x branch does not exist."; exit 1; } rm -rf content - git -C "${{ github.workspace }}" checkout origin/2.x -- .vortex/docs/content || { echo "Failed to check out content from 2.x."; exit 1; } + git -C "${{ github.workspace }}" checkout "origin/${OTHER_MAJOR}.x" -- .vortex/docs/content || { echo "Failed to check out content from ${OTHER_MAJOR}.x."; exit 1; } working-directory: '${{ github.workspace }}/.vortex/docs' # On the main deploy, publish both installers to match the multi-version # docs: '/v1/install' and '/v2/install' are the stable per-major pins and - # the bare '/install' tracks the active major (the - # 'VORTEX_INSTALLER_ACTIVE_MAJOR' repository variable, default 1). The 2.x - # installer is built from the '2.x' branch. Mirrors 'vortex-release.yml'. - - name: Checkout 2.x branch + # the bare '/install' is a copy of the current major's pin (the + # 'VORTEX_CURRENT_MAJOR' repository variable, default 1). The downloaded + # installer above is the current major (built from 'main'); the other + # major is built from its '{N}.x' branch. Mirrors 'vortex-release.yml'. + - name: Checkout v${{ env.OTHER_MAJOR }} branch if: github.event.workflow_run.head_branch == 'main' uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: - ref: 2.x - path: vortex-next + ref: ${{ env.OTHER_MAJOR }}.x + path: vortex-v${{ env.OTHER_MAJOR }} persist-credentials: false - - name: Build and publish 2.x installer + - name: Build and publish v${{ env.OTHER_MAJOR }} installer if: github.event.workflow_run.head_branch == 'main' run: | - composer --working-dir=vortex-next/.vortex/installer install - sed -i "s/\"vortex-installer-version\": \"development\"/\"vortex-installer-version\": \"2.x-dev\"/g" vortex-next/.vortex/installer/box.json - composer --working-dir=vortex-next/.vortex/installer build - case "${ACTIVE_MAJOR}" in + case "${CURRENT_MAJOR}" in 1|2) ;; - *) echo "Invalid VORTEX_INSTALLER_ACTIVE_MAJOR='${ACTIVE_MAJOR}'. Expected 1 or 2."; exit 1 ;; + *) echo "Invalid VORTEX_CURRENT_MAJOR='${CURRENT_MAJOR}'. Expected 1 or 2."; exit 1 ;; esac + composer --working-dir="vortex-v${OTHER_MAJOR}/.vortex/installer" install + sed -i "s/\"vortex-installer-version\": \"development\"/\"vortex-installer-version\": \"${OTHER_MAJOR}.x-dev\"/g" "vortex-v${OTHER_MAJOR}/.vortex/installer/box.json" + composer --working-dir="vortex-v${OTHER_MAJOR}/.vortex/installer" build mkdir -p .vortex/docs/static/v1 .vortex/docs/static/v2 - cp .vortex/docs/static/install ".vortex/docs/static/v${ACTIVE_MAJOR}/install" - cp vortex-next/.vortex/installer/build/installer.phar .vortex/docs/static/v2/install - cp ".vortex/docs/static/v${ACTIVE_MAJOR}/install" .vortex/docs/static/install - env: - ACTIVE_MAJOR: ${{ vars.VORTEX_INSTALLER_ACTIVE_MAJOR || '1' }} + cp .vortex/docs/static/install ".vortex/docs/static/v${CURRENT_MAJOR}/install" + cp "vortex-v${OTHER_MAJOR}/.vortex/installer/build/installer.phar" ".vortex/docs/static/v${OTHER_MAJOR}/install" + cp ".vortex/docs/static/v${CURRENT_MAJOR}/install" .vortex/docs/static/install - name: Build documentation site run: yarn run build working-directory: '${{ github.workspace }}/.vortex/docs' + env: + VORTEX_CURRENT_MAJOR: ${{ env.CURRENT_MAJOR }} # This workflow runs via 'workflow_run', where 'github.ref' is always the # default branch, so the action's branch-based production detection cannot diff --git a/.vortex/docs/content/contributing/maintenance/release.mdx b/.vortex/docs/content/contributing/maintenance/release.mdx index caf57773e..2995e1805 100644 --- a/.vortex/docs/content/contributing/maintenance/release.mdx +++ b/.vortex/docs/content/contributing/maintenance/release.mdx @@ -43,13 +43,15 @@ When creating a new release, determine the version based on the changes: ### Branch strategy -- **`main`** - Current stable release branch (always the latest major version) -- **`N.x`** - Development branch for the next major version, ahead of `main` (e.g., `2.x`) -- **`N-1.x`** - Maintenance branch for the previous major version, behind `main` (e.g., `1.x` after `2.x` is released) +- **`main`** - the current major version under active development (`1.x` today) +- **`2.x`** - a development branch for an upcoming major version, ahead of `main` (future majors follow the same pattern: `3.x`, `4.x`, ...) +- **`1.x`** - a maintenance branch for a superseded major version, behind `main` (for example `1.x` once `main` holds `2.x`) -When a new major version is ready: -1. The next major version development branch (`N.x`) is merged into `main` -2. The previous major version code is preserved in its own maintenance branch (`N-1.x`) +Promoting a new major version is a single action: set the `VORTEX_CURRENT_MAJOR` repository variable to its number. The documentation default and the installer's bare `/install` URL both follow it, while the per-major docs and `/v{N}/install` pins are always published. + +When `main` advances to a new major version: +1. The upcoming major's branch (for example `2.x`) is merged into `main` +2. The superseded major continues on its own branch (for example `1.x`) for maintenance :::note diff --git a/.vortex/docs/docusaurus.config.js b/.vortex/docs/docusaurus.config.js index d1741b7ec..16d6f338a 100644 --- a/.vortex/docs/docusaurus.config.js +++ b/.vortex/docs/docusaurus.config.js @@ -18,6 +18,16 @@ import {themes as prismThemes} from 'prism-react-renderer'; // CI; it is never committed to a branch. const versioned = fs.existsSync('versioned_docs'); +// The current major (the 'VORTEX_CURRENT_MAJOR' repository variable, default 1) +// drives the whole site: its docs are a snapshot under 'versioned_docs/' served +// as the default at the bare '/docs', and the live 'content/' (pulled from the +// other major's '{N}.x' branch in CI) is served at '/docs/v{other}'. Bumping +// that one variable promotes a new major - nothing else changes here. +const currentMajor = process.env.VORTEX_CURRENT_MAJOR || '1'; +const otherMajor = currentMajor === '1' ? '2' : '1'; +const currentDocsVersion = `${currentMajor}.x`; +const otherIsNewer = Number(otherMajor) > Number(currentMajor); + /** @type {import('@docusaurus/types').Config} */ const config = { title: 'Vortex - Drupal project template', @@ -58,22 +68,20 @@ const config = { // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: 'https://github.com/drevops/vortex/tree/main/.vortex/docs/', - // In versioned (aggregate) builds, v1 is the snapshot in - // 'versioned_docs/' served at the bare '/docs' (the default), and - // the current 'content/' is v2 at '/docs/v2'. To flip the default - // when 2.x is ready: set `lastVersion: 'current'`, give '1.x' a - // `path: 'v1'`, drop the v2 `path` so 'current' serves at the bare - // '/docs', and swap the '/docs/v1' redirect below for '/docs/v2'. + // In versioned (aggregate) builds the current major is the snapshot + // in 'versioned_docs/' served at the bare '/docs' (the default), and + // the live 'content/' is the other major at '/docs/v{other}'. Both + // are derived from 'VORTEX_CURRENT_MAJOR' - no manual edits to flip. ...(versioned ? { - lastVersion: '1.x', + lastVersion: currentDocsVersion, versions: { - '1.x': { - label: 'v1', + [currentDocsVersion]: { + label: `v${currentMajor}`, }, current: { - label: 'v2', - path: 'v2', - banner: 'unreleased', + label: `v${otherMajor}`, + path: `v${otherMajor}`, + banner: otherIsNewer ? 'unreleased' : 'unmaintained', }, }, } : {}), @@ -233,11 +241,10 @@ const config = { '@docusaurus/plugin-client-redirects', { redirects: [ - // In versioned builds, 'v1' is the default at the bare '/docs', so - // its explicit path redirects there. When the default flips to - // 'v2', change this to redirect '/docs/v2' instead. + // The current major is the default at the bare '/docs', so its + // explicit '/docs/v{current}' path redirects there. ...(versioned ? [{ - from: '/docs/v1', + from: `/docs/v${currentMajor}`, to: '/docs', }] : []), { diff --git a/.vortex/installer/src/Downloader/RepositoryDownloader.php b/.vortex/installer/src/Downloader/RepositoryDownloader.php index c1f21b24f..afcc07dc0 100644 --- a/.vortex/installer/src/Downloader/RepositoryDownloader.php +++ b/.vortex/installer/src/Downloader/RepositoryDownloader.php @@ -112,7 +112,7 @@ protected function downloadFromRemote(Artifact $artifact, ?string $destination, if ($ref === NULL && $release_prefix !== NULL) { // No tagged release exists yet for this major (for example, an - // unreleased next major). Fall back to the major development branch + // unreleased newer major). Fall back to the major development branch // (for example "2.x") so the line is still installable. $ref = rtrim($release_prefix, '.') . '.x'; $this->validateRemoteRefExists($repo_url, $ref);