diff --git a/readme.md b/readme.md index ba50acd..45a3f5f 100644 --- a/readme.md +++ b/readme.md @@ -60,6 +60,11 @@ This command has these options: * `--directory ` to specify the folder to create or look for this project in. If you don't specify this, it will install to the path specified by `./release-` in the current directory. * `--repository ` will allow a custom composer package url to be specified. E.g. `http://packages.cwp.govt.nz` +* `--branching ` will specify a branching strategy. This allows these options: + * `auto` - Default option, will branch to the minor version (e.g. 1.1) unless doing a non-stable tag (e.g. rc1) + * `major` - Branch all repos to the major version (e.g. 1) unless already on a more-specific minor version. + * `minor` - Branch all repos to the minor semver branch (e.g. 1.1) + * `none` - Release from the current branch and do no branching. `release` actually has several sub-commands which can be run independently. These are as below: diff --git a/src/Commands/Release/Branch.php b/src/Commands/Release/Branch.php index b3b7734..78a49fb 100644 --- a/src/Commands/Release/Branch.php +++ b/src/Commands/Release/Branch.php @@ -17,14 +17,65 @@ class Branch extends Release protected $description = 'Branch all modules'; + /** + * Will branch to minor version (e.g. 1.1) for all stable releases only + */ + const AUTO = 'auto'; // Automatic branching + + /** + * Auto description + */ + const AUTO_DESCRIPTION = 'Uses minor for stable tags, none for unstable tags'; + + /** + * Branch to major version (e.g. 1) unless already on specific minor branch + */ + const MAJOR = 'major'; + + /** + * Major description + */ + const MAJOR_DESCRIPTION = 'Branch to major version (e.g. 1) unless already on specific minor branch'; + + /** + * Branch to minor version (E.g. 1.1) + */ + const MINOR = 'minor'; + + /** + * Minor description + */ + const MINOR_DESCRIPTION = 'Branch to minor version (E.g. 1.1)'; + + /** + * No branching will occur + */ + const NONE = 'none'; + + /** + * None description + */ + const NONE_DESCRIPTION = 'No branching will occur'; + + /** + * List of valid options + */ + const OPTIONS = [ + self::AUTO => self::AUTO_DESCRIPTION, + self::MAJOR => self::MAJOR_DESCRIPTION, + self::MINOR => self::MINOR_DESCRIPTION, + self::NONE => self::NONE_DESCRIPTION, + ]; + protected function fire() { // Get arguments $version = $this->getInputVersion(); $project = $this->getProject(); + $branching = $this->getBranching(); // Build and confirm release plan - $buildPlan = new PlanRelease($this, $project, $version); + $buildPlan = new PlanRelease($this, $project, $version, $branching); $buildPlan->run($this->input, $this->output); $releasePlan = $buildPlan->getReleasePlan(); diff --git a/src/Commands/Release/Changelog.php b/src/Commands/Release/Changelog.php index 49d1348..0b0e3ed 100644 --- a/src/Commands/Release/Changelog.php +++ b/src/Commands/Release/Changelog.php @@ -25,9 +25,10 @@ protected function fire() // Get arguments $version = $this->getInputVersion(); $project = $this->getProject(); + $branching = $this->getBranching(); // Build and confirm release plan - $buildPlan = new PlanRelease($this, $project, $version); + $buildPlan = new PlanRelease($this, $project, $version, $branching); $buildPlan->run($this->input, $this->output); $releasePlan = $buildPlan->getReleasePlan(); diff --git a/src/Commands/Release/Plan.php b/src/Commands/Release/Plan.php index fb73bdf..b6b60ac 100644 --- a/src/Commands/Release/Plan.php +++ b/src/Commands/Release/Plan.php @@ -19,9 +19,10 @@ protected function fire() // Get arguments $version = $this->getInputVersion(); $project = $this->getProject(); + $branching = $this->getBranching(); // Build and confirm release plan - $buildPlan = new PlanRelease($this, $project, $version); + $buildPlan = new PlanRelease($this, $project, $version, $branching); $buildPlan->run($this->input, $this->output); } } diff --git a/src/Commands/Release/Release.php b/src/Commands/Release/Release.php index 38087c1..ec30457 100644 --- a/src/Commands/Release/Release.php +++ b/src/Commands/Release/Release.php @@ -27,11 +27,18 @@ class Release extends Command protected function configureOptions() { + $branchOptions = implode('|', array_keys(Branch::OPTIONS)); $this ->addArgument('version', InputArgument::REQUIRED, 'Exact version tag to release this project as') ->addArgument('recipe', InputArgument::OPTIONAL, 'Recipe to release', 'silverstripe/installer') ->addOption('repository', "r", InputOption::VALUE_REQUIRED, "Custom repository url") - ->addOption('directory', 'd', InputOption::VALUE_REQUIRED, 'Optional directory to release project from'); + ->addOption('directory', 'd', InputOption::VALUE_REQUIRED, 'Optional directory to release project from') + ->addOption( + 'branching', + 'b', + InputOption::VALUE_REQUIRED, + "Branching strategy. One of [{$branchOptions}]" + ); } protected function fire() @@ -46,9 +53,10 @@ protected function fire() $createProject = new CreateProject($this, $version, $recipe, $directory, $repository); $createProject->run($this->input, $this->output); $project = $this->getProject(); + $branching = $this->getBranching(); // Build and confirm release plan - $buildPlan = new PlanRelease($this, $project, $version); + $buildPlan = new PlanRelease($this, $project, $version, $branching); $buildPlan->run($this->input, $this->output); $releasePlan = $buildPlan->getReleasePlan(); @@ -160,6 +168,20 @@ protected function getProject() return new Project($directory); } + /** + * Get branching strategy + * + * @return string Branching strategy, or null to inherit / default + */ + protected function getBranching() + { + $branch = $this->input->getOption('branching'); + if ($branch && !array_key_exists($branch, Branch::OPTIONS)) { + throw new InvalidArgumentException("Invalid branching option {$branch}"); + } + return $branch; + } + /** * Get command to suggest to publish this release * diff --git a/src/Commands/Release/Translate.php b/src/Commands/Release/Translate.php index 4e458a7..2b7b892 100644 --- a/src/Commands/Release/Translate.php +++ b/src/Commands/Release/Translate.php @@ -21,9 +21,10 @@ protected function fire() // Get arguments $version = $this->getInputVersion(); $project = $this->getProject(); + $branching = $this->getBranching(); // Build and confirm release plan - $buildPlan = new PlanRelease($this, $project, $version); + $buildPlan = new PlanRelease($this, $project, $version, $branching); $buildPlan->run($this->input, $this->output); $releasePlan = $buildPlan->getReleasePlan(); diff --git a/src/Model/Modules/Library.php b/src/Model/Modules/Library.php index 276290f..6813dec 100644 --- a/src/Model/Modules/Library.php +++ b/src/Model/Modules/Library.php @@ -966,6 +966,12 @@ protected function unserialisePlan($serialisedPlan) if (!empty($data['Items'])) { $libraryRelease->addItems($this->unserialisePlan($data['Items'])); } + + // Set branching + if (!empty($data['Branching'])) { + $libraryRelease->setBranching($data['Branching']); + } + $releases[$name] = $libraryRelease; } return $releases; @@ -984,7 +990,8 @@ public function serialisePlan(LibraryRelease $plan) $content[$name] = [ 'Version' => $plan->getVersion()->getValue(), 'Changelog' => $plan->getChangelog(), - 'Items' => [] + 'Items' => [], + 'Branching' => $plan->getBranching(null), // Only store internal value don't failover ]; foreach ($plan->getItems() as $item) { $content[$name]['Items'] = array_merge( diff --git a/src/Model/Modules/Module.php b/src/Model/Modules/Module.php index b1a9cc0..2a8cd2b 100644 --- a/src/Model/Modules/Module.php +++ b/src/Model/Modules/Module.php @@ -88,6 +88,21 @@ public function getRelativeMainDirectory() return substr($dir, strlen($base) + 1); } + /** + * Get parameter to pass to i18n text collector + * + * @return string + */ + public function getI18nTextCollectorName() + { + $dir = $this->getRelativeMainDirectory(); + // If short name has any slashes just use composer name instead + if (preg_match('#\w[\\/]\w#', $dir)) { + return $this->getName(); + } + return $dir; + } + /** * Determine if this project has a .tx configured * diff --git a/src/Model/Modules/Project.php b/src/Model/Modules/Project.php index 540f757..c8a4f65 100644 --- a/src/Model/Modules/Project.php +++ b/src/Model/Modules/Project.php @@ -2,6 +2,7 @@ namespace SilverStripe\Cow\Model\Modules; +use BadMethodCallException; use Generator; use InvalidArgumentException; @@ -110,4 +111,24 @@ public function getProject() { return $this; } + + /** + * Get path to sake executable + * + * @return string + */ + public function getSakePath() + { + $candidates = [ + $this->getDirectory() . '/vendor/bin/sake', // New standard location + $this->getDirectory() . '/vendor/silverstripe/framework/sake', + $this->getDirectory() . '/framework/sake', + ]; + foreach ($candidates as $candidate) { + if (file_exists($candidate)) { + return $candidate; + } + } + throw new BadMethodCallException("sake bin could not be found in this project"); + } } diff --git a/src/Model/Release/LibraryRelease.php b/src/Model/Release/LibraryRelease.php index e084b5b..eb00540 100644 --- a/src/Model/Release/LibraryRelease.php +++ b/src/Model/Release/LibraryRelease.php @@ -4,6 +4,7 @@ namespace SilverStripe\Cow\Model\Release; use Generator; +use SilverStripe\Cow\Commands\Release\Branch; use SilverStripe\Cow\Model\Modules\Library; /** @@ -18,6 +19,13 @@ class LibraryRelease */ protected $items = []; + /** + * Default branching strategy (only valid on root release) + * + * @var string + */ + protected $branching = null; + /** * The module being released * @@ -230,4 +238,27 @@ public function setChangelog($changelog) $this->changelog = $changelog; return $this; } + + /** + * Get branching strategy + * + * @param string $default Default value to get if no value found + * @return string + */ + public function getBranching($default = Branch::AUTO) + { + return $this->branching ?: $default; + } + + /** + * Set branching strategy + * + * @param string $branching + * @return $this + */ + public function setBranching($branching) + { + $this->branching = $branching; + return $this; + } } diff --git a/src/Steps/Release/PlanRelease.php b/src/Steps/Release/PlanRelease.php index e871ef1..e7b5ee2 100644 --- a/src/Steps/Release/PlanRelease.php +++ b/src/Steps/Release/PlanRelease.php @@ -4,6 +4,7 @@ use Exception; use SilverStripe\Cow\Commands\Command; +use SilverStripe\Cow\Commands\Release\Branch; use SilverStripe\Cow\Model\Modules\Library; use SilverStripe\Cow\Model\Release\LibraryRelease; use SilverStripe\Cow\Model\Modules\Project; @@ -33,6 +34,13 @@ class PlanRelease extends Step */ protected $releasePlan; + /** + * Branching strategy + * + * @var string + */ + protected $branching = null; + /** * @return LibraryRelease */ @@ -51,11 +59,20 @@ public function setReleasePlan($releasePlan) return $this; } - public function __construct(Command $command, Project $project, Version $version) + /** + * Build new plan step + * + * @param Command $command + * @param Project $project + * @param Version $version + * @param string $branching Override branching strategy + */ + public function __construct(Command $command, Project $project, Version $version, $branching) { parent::__construct($command); $this->setProject($project); $this->setVersion($version); + $this->setBranching($branching); } public function getStepName() @@ -85,8 +102,15 @@ protected function buildInitialPlan(OutputInterface $output) { // Load cached value $moduleRelease = $this->getProject()->loadCachedPlan(); + $branching = $this->getBranching(); if ($moduleRelease) { $this->log($output, 'Loading cached release plan from prior session'); + // Note: Branching can be overridden on the CLI. Save this to cached plan in this case + if ($branching && $branching !== $moduleRelease->getBranching()) { + $this->log($output, "Updating branching to {$branching}"); + $moduleRelease->setBranching($branching); + $this->getProject()->saveCachedPlan($moduleRelease); + } $this->setReleasePlan($moduleRelease); return; } @@ -96,6 +120,11 @@ protected function buildInitialPlan(OutputInterface $output) $moduleRelease = new LibraryRelease($this->getProject(), $this->getVersion()); $this->generateChildReleases($moduleRelease); + // Set branching if specified on CLI + if ($branching) { + $moduleRelease->setBranching($branching); + } + // Save plan $this->getProject()->saveCachedPlan($moduleRelease); $this->setReleasePlan($moduleRelease); @@ -283,6 +312,28 @@ public function getVersion() return $this->version; } + /** + * Get branching strategy + * + * @return string + */ + public function getBranching() + { + return $this->branching; + } + + /** + * Set branching strategy + * + * @param string $branching + * @return $this + */ + public function setBranching($branching) + { + $this->branching = $branching; + return $this; + } + /** * Interactively confirm a plan with the user * @@ -292,12 +343,15 @@ public function getVersion() protected function reviewPlan(OutputInterface $output, InputInterface $input) { // Get user-descriptive output for plan - $releaseLines = $this->getReleaseOptions($this->getReleasePlan()); + $libraryRelease = $this->getReleasePlan(); + $branching = $libraryRelease->getBranching(); + $releaseLines = $this->getReleaseOptions($libraryRelease); // If not interactive, simply output read-only list of versions $message = "The below release plan has been generated for this project"; if (!$input->isInteractive()) { $this->log($output, $message); + $this->log($output, "branching ({$branching})"); foreach ($releaseLines as $line) { $this->log($output, $line); } @@ -308,7 +362,10 @@ protected function reviewPlan(OutputInterface $output, InputInterface $input) $question = new ChoiceQuestion( "{$message}; Please confirm any manual changes below, or type a module name to edit the tag:", array_merge( - ["continue" => "continue"], + [ + "continue" => "continue", + "branching" => "modify branching strategy ({$branching})", + ], $releaseLines ), "continue" @@ -316,14 +373,21 @@ protected function reviewPlan(OutputInterface $output, InputInterface $input) $selectedLibrary = $this->getQuestionHelper()->ask($input, $output, $question); // Break if plan is accepted - if ($selectedLibrary === 'continue') { - return; + switch ($selectedLibrary) { + case 'continue': + // Good job! + return; + case 'branching': + // Modify branching strategy + $this->reviewBranching($output, $input); + break; + default: + // Modify selected dependency + $selectedRelease = $libraryRelease->getItem($selectedLibrary); + $this->reviewLibraryVersion($output, $input, $selectedRelease); + break; } - // Modify selected dependency - $selectedRelease = $this->getReleasePlan()->getItem($selectedLibrary); - $this->reviewLibraryVersion($output, $input, $selectedRelease); - // Recursively update plan $this->reviewPlan($output, $input); } @@ -381,6 +445,28 @@ protected function reviewLibraryVersion( $this->reviewLibraryVersion($output, $input, $selectedVersion); } + /** + * Select new branching strategy + * + * @param OutputInterface $output + * @param InputInterface $input + */ + protected function reviewBranching(OutputInterface $output, InputInterface $input) + { + $current = $this->getReleasePlan()->getBranching(); + $question = new ChoiceQuestion( + "Select branching strategy (current: {$current})", + Branch::OPTIONS, + $current + ); + $branching = $this->getQuestionHelper()->ask($input, $output, $question); + + // Update and save update + $this->getReleasePlan()->setBranching($branching); + $this->setBranching($branching); + $this->getProject()->saveCachedPlan($this->getReleasePlan()); + } + /** * Build user-visible option selection list based on a prepared plan * diff --git a/src/Steps/Release/RewriteReleaseBranches.php b/src/Steps/Release/RewriteReleaseBranches.php index 45623d8..0407b56 100644 --- a/src/Steps/Release/RewriteReleaseBranches.php +++ b/src/Steps/Release/RewriteReleaseBranches.php @@ -4,6 +4,8 @@ namespace SilverStripe\Cow\Steps\Release; use Exception; +use InvalidArgumentException; +use SilverStripe\Cow\Commands\Release\Branch; use SilverStripe\Cow\Model\Modules\Library; use SilverStripe\Cow\Model\Release\LibraryRelease; use SilverStripe\Cow\Model\Release\Version; @@ -33,7 +35,8 @@ public function run(InputInterface $input, OutputInterface $output) $this->log($output, "Updating branches and aliases"); $release = $this->getReleasePlan(); - $this->recursiveBranchLibrary($output, $release); + $branching = $release->getBranching(); + $this->recursiveBranchLibrary($output, $release, $branching); $this->log($output, "Branches updated"); } @@ -43,18 +46,19 @@ public function run(InputInterface $input, OutputInterface $output) * * @param OutputInterface $output * @param LibraryRelease $libraryRelease + * @param string $branching Branching strategy */ - protected function recursiveBranchLibrary(OutputInterface $output, LibraryRelease $libraryRelease) + protected function recursiveBranchLibrary(OutputInterface $output, LibraryRelease $libraryRelease, $branching) { // Recursively rewrite and branch child dependencies first foreach ($libraryRelease->getItems() as $childLibrary) { - $this->recursiveBranchLibrary($output, $childLibrary); + $this->recursiveBranchLibrary($output, $childLibrary, $branching); } // Skip if not tagging this library (upgrade only) if ($libraryRelease->getIsNewRelease()) { // Update this library - $this->branchLibrary($output, $libraryRelease); + $this->branchLibrary($output, $libraryRelease, $branching); // Update dev dependencies for the given module $this->incrementDevDependencies($output, $libraryRelease); @@ -68,36 +72,52 @@ protected function recursiveBranchLibrary(OutputInterface $output, LibraryReleas * * @param OutputInterface $output * @param LibraryRelease $libraryRelease + * @param string $branching Branching strategy * @throws Exception */ - protected function branchLibrary(OutputInterface $output, LibraryRelease $libraryRelease) + protected function branchLibrary(OutputInterface $output, LibraryRelease $libraryRelease, $branching) { // Get info on current status $library = $libraryRelease->getLibrary(); $currentBranch = $library->getBranch(); $libraryName = $library->getName(); - // Guess branches to use + // Calculate candidate branch names $version = $libraryRelease->getVersion(); - $canCheckout = $this->canCheckout($currentBranch, $version); - if ($canCheckout) { - // Check versions to checkout - $majorBranch = $libraryRelease->getVersion()->getMajor(); - $minorBranch = $majorBranch . "." . $libraryRelease->getVersion()->getMinor(); + + // Calculate branch to switch to + $target = $this->getTargetBranch($version, $branching, $currentBranch); + + // Either branch, or simply log current branch + if (empty($target) || $target === $currentBranch) { $this->log( $output, - "Branching library {$libraryName} to {$minorBranch} (new branch)" + "Releasing library {$libraryName} from branch {$currentBranch}" ); - - // Branch both major and minor versions - $library->checkout($output, $majorBranch, 'origin', true); - $library->checkout($output, $minorBranch, 'origin', true); - $this->removeComposerAlias($output, $library); } else { + // Check versions to checkout $this->log( $output, - "Releasing library {$libraryName} from branch {$currentBranch}" + "Branching library {$libraryName} as {$target} " + . "(from {$currentBranch})" ); + + // If branching minor version, checkout major as well along the way. + // If switching master -> 1.0 it can be better to branch from an existing 1 + // instead of master + $majorBranch = $version->getMajor(); + $minorBranch = $majorBranch . "." . $version->getMinor(); + if ($target === $minorBranch) { + $library->checkout($output, $majorBranch, 'origin', true); + } + + // Checkout branch + $library->checkout($output, $target, 'origin', true); + + // If branching to minor version, remove alias + if ($target === $minorBranch) { + $this->removeComposerAlias($output, $library); + } } // Synchronise local branch with upstream @@ -119,37 +139,6 @@ protected function checkoutLibrary(OutputInterface $output, Library $library, Ve $library->resetToTag($output, $version); } - /** - * Determine if the current branch should be changed - * - * @param string $currentBranch Note, this can be empty - * @param Version $version - * @return bool Whether the branch should be changed - */ - protected function canCheckout($currentBranch, Version $version) - { - // Get expected major and minor branches - $minorBranch = $version->getMajor() . "." . $version->getMinor(); - - // Already on ideal branch - if ($currentBranch === $minorBranch) { - return false; - } - - // Don't branch on version < 1.0 (even stable) - if ($version->getMajor() < 1) { - return false; - } - - // Temp: Enforce branching if doing stable release only - // See https://github.com/silverstripe/cow/issues/53 - if ($version->isStable()) { - return true; - } - - return false; - } - /** * Remove composer alias from composer.json * @@ -246,4 +235,46 @@ protected function incrementDevDependencies(OutputInterface $output, LibraryRele } } } + + /** + * Get branch to branch to, or null if no branching should occur + * + * @param Version $version + * @param string $branching Branching strategy + * @param string $currentBranch + * @return string Branch target name + */ + protected function getTargetBranch($version, $branching, $currentBranch) + { + $majorBranch = $version->getMajor(); + $minorBranch = $majorBranch . "." . $version->getMinor(); + + // If already on minor branch stay on this in all situations + if ($currentBranch === $minorBranch) { + return null; + } + + // Don't branch on pre-1.0 in any situation + if ($majorBranch < 1) { + return null; + } + + // Determine destination branch + switch ($branching) { + case Branch::NONE: + return null; + case Branch::MINOR: + return $minorBranch; + case Branch::MAJOR: + return $majorBranch; + case Branch::AUTO: + // Auto disables branching for unstable tags + if (!$version->isStable()) { + return null; + } + return $minorBranch; + default: + throw new InvalidArgumentException("Invalid branching strategy $branching"); + } + } } diff --git a/src/Steps/Release/UpdateTranslations.php b/src/Steps/Release/UpdateTranslations.php index d2b0c9e..403dbaf 100644 --- a/src/Steps/Release/UpdateTranslations.php +++ b/src/Steps/Release/UpdateTranslations.php @@ -279,12 +279,13 @@ protected function collectStrings(OutputInterface $output, $modules) // Get code dirs for each module $dirs = array(); foreach ($modules as $module) { - $dirs[] = $module->getRelativeMainDirectory(); + $dirs[] = $module->getI18nTextCollectorName(); } $sakeCommand = sprintf( - '(cd %s && ./framework/sake dev/tasks/i18nTextCollectorTask "flush=all" "merge=1" "module=%s")', + '(cd %s && %s dev/tasks/i18nTextCollectorTask "flush=all" "merge=1" "module=%s")', $this->getProject()->getDirectory(), + $this->getProject()->getSakePath(), implode(',', $dirs) ); $this->runCommand($output, $sakeCommand, "Error encountered running i18nTextCollectorTask"); diff --git a/src/Steps/Step.php b/src/Steps/Step.php index e9a4e97..21ec83d 100644 --- a/src/Steps/Step.php +++ b/src/Steps/Step.php @@ -20,6 +20,14 @@ abstract class Step */ protected $command; + /** + * Default env vars to set + * @var array + */ + protected $envs = [ + 'SS_VENDOR_METHOD' => 'copy', // Ensure that vendor copies, rather than symlinks, assets + ]; + public function __construct(Command $command) { $this->setCommand($command); @@ -108,6 +116,10 @@ public function runCommand(OutputInterface $output, $command, $error = null, $ex $process = new Process($command); } + // Set all default env vars + $process->inheritEnvironmentVariables(); + $process->setEnv($this->envs); + // Run it $process->setTimeout(null); $result = $helper->run($output, $process, $error);