Skip to content

Commit 3a9bc4a

Browse files
Merge pull request #36 from move-elevator/feat/slash-branch-names
feat: support feature branch names containing slashes
2 parents 081cd26 + cb44b01 commit 3a9bc4a

9 files changed

Lines changed: 103 additions & 24 deletions

File tree

deployer/dev/task/sync.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
if ($target !== 'prod' && $target !== 'stage') {
3939
$dbSyncToolSync = get('dev_db_sync_tool_default_sync');
4040

41-
$dbSyncToolOriginPath = str_replace('<feature>', $target, get('dev_db_sync_tool_origin_path'));
41+
// the remote path carries the instance name, not the raw branch name
42+
$dbSyncToolOriginPath = str_replace('<feature>', getFeatureName($target), get('dev_db_sync_tool_origin_path'));
4243
$additionalOptions = "--origin-path $dbSyncToolOriginPath";
4344
} else {
4445
$dbSyncToolSync = $target === 'prod' ? get('dev_db_sync_tool_prod_sync') : get('dev_db_sync_tool_default_sync');

deployer/feature/task/feature_init.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ function initFeature(?string $feature = null): ?string
5555
return $array[2];
5656
}, listFeatureInstances()));
5757
}
58+
// branch names may contain path separators ("feature/ABC-12"), the instance name must stay flat
59+
$feature = getFeatureName($feature);
5860
set('feature', $feature);
5961

6062
if (isUrlShortener()) {

deployer/feature/task/feature_setup.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ function renderRemoteTemplates(): void
6767
{
6868
debug('Rendering remote template');
6969
$databaseName = DbUtility::getDatabaseManager()->getDatabaseName();
70-
$feature = input()->getOption('feature');
70+
// the normalized name, matching the instance directory and url segment
71+
$feature = get('feature');
7172
$templates = get('feature_templates');
7273

7374
if (!$templates) {
@@ -144,13 +145,3 @@ function uploadTemplate($localTemplate, $remoteTarget, $arguments): void {
144145
upload($temporaryFileName,get('deploy_path') . $remoteTarget);
145146
unlink($temporaryFileName);
146147
}
147-
148-
/**
149-
* @param ?string $feature
150-
* @return array|string|string[]|null
151-
*/
152-
function getFeatureName(?string $feature = null) {
153-
$feature = $feature ?: input()->getOption('feature');
154-
155-
return preg_replace('/[^A-Za-z0-9\_\-.]/', '', $feature);
156-
}

deployer/feature/task/feature_stop.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
*/
3434
function deleteFeature(?string $feature = null, $needConfirmation = false): void
3535
{
36-
$feature = $feature ?: input()->getOption('feature');
36+
$feature = getFeatureName($feature);
3737

3838
$filesRemoveCommand = "rm -rf " . get('deploy_path');
3939

deployer/functions.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Deployer;
44

55
use Deployer\Exception\RunException;
6+
use MoveElevator\DeployerTools\Utility\FeatureUtility;
67
use Symfony\Component\Console\Output\OutputInterface;
78

89
/**
@@ -55,6 +56,26 @@ function featureRequested(): bool
5556
return null !== $feature && '' !== trim((string)$feature);
5657
}
5758

59+
/**
60+
* Normalize a feature identifier into the flat instance name used for the deploy
61+
* directory, the public url segment, the url shortener symlink and the database name.
62+
*
63+
* Branch names may carry path separators ("feature/ABC-12"), which would otherwise
64+
* nest the instance directory and break listing, cleanup and deletion. Identifiers
65+
* that already consist of allowed characters only are returned unchanged.
66+
*
67+
* @param ?string $feature
68+
* @return string
69+
*/
70+
function getFeatureName(?string $feature = null): string
71+
{
72+
if (null === $feature || '' === trim($feature)) {
73+
$feature = featureRequested() ? (string)input()->getOption('feature') : '';
74+
}
75+
76+
return FeatureUtility::normalize($feature);
77+
}
78+
5879
/**
5980
* Extend the deployer configuration with available environment variables (starting with "DEPLOYER_CONFIG_"):
6081
*

docs/FEATURE.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ The `feature:setup` command represent the initialization of a new feature branch
5555
$ vendor/bin/dep feature:setup stage --feature=TEST-01
5656
```
5757

58+
The `--feature=` value may be a full branch name. Path separators are replaced by a hyphen so the instance stays a single flat directory, url segment and database suffix:
59+
60+
| `--feature=` | instance name |
61+
|----------------------|----------------------|
62+
| `TEST-01` | `TEST-01` |
63+
| `feature/TEST-01` | `feature-TEST-01` |
64+
| `bugfix/TEST-01` | `bugfix-TEST-01` |
65+
| `release/1.2.0` | `release-1.2.0` |
66+
67+
Names that already consist of letters, digits, `_`, `-` and `.` are used unchanged, so existing instances and their databases stay reachable. Since the branch prefix is kept, `feature/TEST-01` and `bugfix/TEST-01` remain two separate instances. The same normalization is applied by `feature:cleanup` when it compares remote git branches with the deployed instances.
68+
69+
A `--feature=` value that yields no usable name is rejected instead of falling back to the base instance, which the feature scaffolding would otherwise overwrite. That covers values without any allowed character as well as `.` and `..`.
70+
71+
> Because the separator is replaced rather than encoded, `feature/TEST-01` and a branch literally named `feature-TEST-01` map to the same instance. Do not use both spellings for different branches in one project.
72+
5873
The recipe already wires this task into the deploy flow, together with a `feature:init` before `deploy:info`. That ordering matters: `deploy:info` resolves `{{release_name}}` and deployer caches the result for the rest of the run, so the feature instance has to be known before it runs. Do not hook `feature:setup` any earlier yourself.
5974

6075
> Upgrading: if your `deploy.php` carries a `before('deploy:info', 'feature:init')` (or an equivalent `feature:setup` hook) as a workaround for that ordering, remove it — the recipe registers it now and the hook would otherwise run twice.
@@ -75,7 +90,7 @@ This configuration defines the local template file as well as the remote target
7590
| `DEPLOYER_CONFIG_DATABASE_PORT` | default is `3306`, overwrite with deployer `set('database_port', '3306');` |
7691
| `DEPLOYER_CONFIG_DATABASE_USER` | should be defined with `database_user` in the host configuration |
7792
| `DEPLOYER_CONFIG_DATABASE_NAME` | will be dynamically generated |
78-
| `DEPLOYER_CONFIG_FEATURE_NAME` | will be provide with the `--feature=` command line argument |
93+
| `DEPLOYER_CONFIG_FEATURE_NAME` | the normalized instance name derived from the `--feature=` command line argument |
7994
| `DEPLOYER_CONFIG_FEATURE_URL` | will be dynamically generated |
8095
| `DEPLOYER_CONFIG_FEATURE_PATH` | will be dynamically generated |
8196

src/Database/Manager/AbstractManager.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace MoveElevator\DeployerTools\Database\Manager;
66

77
use MoveElevator\DeployerTools\Database\Exception\DatabaseException;
8+
use MoveElevator\DeployerTools\Utility\FeatureUtility;
89
use MoveElevator\DeployerTools\Utility\VarUtility;
910

1011
use function Deployer\get;
@@ -41,16 +42,21 @@ public function run(string $command, bool $useDoubleQuotes = true): string
4142
*/
4243
public function getDatabaseName(?string $feature = null): string
4344
{
44-
$feature = $feature ?: input()->getOption('feature');
45-
$project = get('project');
46-
return substr($this->getFeatureName("{$project}--{$feature}"), 0, 63);
45+
// both parts are normalized separately, so the "--" separator survives
46+
$project = FeatureUtility::normalize((string) get('project'));
47+
$feature = $this->getFeatureName($feature);
48+
49+
return substr($project . '--' . $feature, 0, 63);
4750
}
4851

4952

5053
public function getFeatureName(?string $feature = null): string
5154
{
52-
$feature = $feature ?: input()->getOption('feature');
55+
// only null and blank count as absent, "0" is a valid instance name
56+
if (null === $feature || '' === trim($feature)) {
57+
$feature = (string) input()->getOption('feature');
58+
}
5359

54-
return preg_replace('/[^A-Za-z0-9\_\-.]/', '', (string) $feature);
60+
return FeatureUtility::normalize($feature);
5561
}
5662
}

src/Database/Manager/Simple.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use function Deployer\set;
1212
use function Deployer\has;
1313
use function Deployer\run;
14-
use function Deployer\input;
1514
use function Deployer\upload;
1615
use function Deployer\runExtended;
1716
use function Deployer\test;
@@ -50,7 +49,7 @@ public function delete(string $feature): void
5049
debug('Deleting database');
5150
$this->ensureDatabasePoolExists();
5251
$this->initDatabaseConfiguration(feature: $feature);
53-
$this->assignmentManager->removeAssignment($feature);
52+
$this->assignmentManager->removeAssignment($this->getFeatureName($feature));
5453
$this->run($this->generateDropTablesQuery($this->getDatabaseName($feature)));
5554
}
5655

@@ -78,8 +77,7 @@ public function exists(?string $feature = null): bool
7877

7978
public function getDatabaseName(?string $feature = null): string
8079
{
81-
$feature = $feature ?: input()->getOption('feature');
82-
$databaseAssignment = $this->assignmentManager->getAssignment($feature);
80+
$databaseAssignment = $this->assignmentManager->getAssignment($this->getFeatureName($feature));
8381

8482
if (!$databaseAssignment) {
8583
return '';
@@ -115,7 +113,7 @@ private function initDatabaseConfiguration(?string $database = null, ?string $fe
115113
{
116114
$pool = get('database_pool');
117115
if (!$database) {
118-
$database = $this->assignmentManager->getAssignment($feature ?: $this->getFeatureName());
116+
$database = $this->assignmentManager->getAssignment($this->getFeatureName($feature));
119117
}
120118

121119
if (!isset($pool[$database])) {

src/Utility/FeatureUtility.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MoveElevator\DeployerTools\Utility;
6+
7+
/**
8+
* Normalizes feature identifiers into a name that is safe to use as a directory,
9+
* URL segment, symlink and database name suffix.
10+
*/
11+
final class FeatureUtility
12+
{
13+
/**
14+
* Turn a feature identifier (typically a git branch name) into a flat instance name.
15+
*
16+
* Path separators become hyphens instead of being dropped, so "feature/ABC-12" and
17+
* "bugfix/ABC-12" stay distinct instances. Names that already consist of allowed
18+
* characters only are returned unchanged, which keeps existing instances and their
19+
* databases reachable.
20+
*
21+
* @throws \InvalidArgumentException if a non-blank identifier normalizes to an empty
22+
* name or to a relative path segment, either of which
23+
* would silently address the base instance
24+
*/
25+
public static function normalize(?string $feature): string
26+
{
27+
$feature = trim((string) $feature);
28+
29+
if ('' === $feature) {
30+
return '';
31+
}
32+
33+
$normalized = str_replace(['/', '\\'], '-', $feature);
34+
$normalized = (string) preg_replace('/[^A-Za-z0-9_\-.]/', '', $normalized);
35+
36+
// "" would resolve to the base instance, "." and ".." to it or its parent
37+
if ('' === trim($normalized, '.')) {
38+
throw new \InvalidArgumentException(
39+
sprintf('The feature name "%s" does not yield a usable instance name.', $feature)
40+
);
41+
}
42+
43+
return $normalized;
44+
}
45+
}

0 commit comments

Comments
 (0)