Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .castor/context.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Symfony\Component\Process\Process;

use function Castor\log;
use function worktree\get_worktree_name;

#[AsContext(default: true)]
function create_default_context(): Context
Expand Down Expand Up @@ -49,6 +50,11 @@ function create_default_context(): Context
$data['user_id'] = 1000;
}

$worktreeName = get_worktree_name($data['root_dir']);
if (null !== $worktreeName) {
$data['project_name'] .= '-wt-' . $worktreeName;
}
Comment thread
bastnic marked this conversation as resolved.

return new Context(
$data,
pty: Process::isPtySupported(),
Expand Down
57 changes: 55 additions & 2 deletions .castor/docker.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@
use function Castor\open;
use function Castor\run;
use function Castor\variable;
use function worktree\get_worktree_name;
use function worktree\get_worktree_ports;

/** @return array<string, array{env: string, default: int, label: string}> */
function get_port_specs(): array
{
return [
'http' => ['env' => 'PROJECT_HTTP_PORT', 'default' => 80, 'label' => 'HTTP'],
'https' => ['env' => 'PROJECT_HTTPS_PORT', 'default' => 443, 'label' => 'HTTPS'],
'admin' => ['env' => 'PROJECT_ADMIN_PORT', 'default' => 8080, 'label' => 'Traefik admin'],
];
}

#[AsTask(description: 'Displays some help and available urls for the current project', namespace: '')]
function about(): void
Expand All @@ -36,9 +48,12 @@ function about(): void
io()->section('Available URLs for this project:');
$urls = [variable('root_domain'), ...variable('extra_domains')];

$worktreeName = get_worktree_name();
$adminPort = get_worktree_ports($worktreeName)['admin'];

try {
$routers = http_client()
->request('GET', \sprintf('http://%s:8080/api/http/routers', variable('root_domain')))
->request('GET', \sprintf('http://%s:%d/api/http/routers', variable('root_domain'), $adminPort))
->toArray()
;
$projectName = variable('project_name');
Expand All @@ -58,7 +73,12 @@ function about(): void
} catch (HttpExceptionInterface) {
}

io()->listing(array_map(static fn ($url) => "https://{$url}", array_unique($urls)));
$httpsPort = null !== $worktreeName ? get_worktree_ports($worktreeName)['https'] : null;
$formatUrl = static function (string $host) use ($httpsPort): string {
return null !== $httpsPort ? "https://{$host}:{$httpsPort}" : "https://{$host}";
};

io()->listing(array_map($formatUrl, array_unique($urls)));
}

#[AsTask(description: 'Opens the project in your browser', namespace: '', aliases: ['open'])]
Expand Down Expand Up @@ -379,6 +399,16 @@ function docker_compose(array $subCommand, ?Context $c = null, array $profiles =
'REGISTRY' => $c['registry'] ?? '',
]);

$worktreeName = get_worktree_name();
if ($worktreeName) {
$ports = get_worktree_ports($worktreeName);
$portsEnv = [];
foreach (get_port_specs() as $key => $spec) {
$portsEnv[$spec['env']] = (string) $ports[$key];
}
$c = $c->withEnvironment($portsEnv);
}

if ($c['APP_ENV'] ?? false) {
$c = $c->withEnvironment([
'APP_ENV' => $c['APP_ENV'] ?? '',
Expand Down Expand Up @@ -608,3 +638,26 @@ function get_service_names(?string $profile = null): array
{
return array_keys(get_services($profile));
}

#[AsTask(description: 'Displays the ports allocated for the current project', namespace: 'docker')]
function ports(): void
{
$project = variable('project_name');
$domain = variable('root_domain');
$name = get_worktree_name();
$ports = get_worktree_ports($name);

io()->title('Ports');
$list = [['Project' => $project]];
foreach (get_port_specs() as $key => $spec) {
$scheme = 'admin' === $key ? 'http' : $key;
$host = 'admin' === $key ? 'localhost' : $domain;
$list[] = [$spec['label'] => "{$scheme}://{$host}:{$ports[$key]}"];
}

if ($name) {
array_unshift($list, ['Worktree' => $name]);
}

io()->definitionList(...$list);
}
62 changes: 62 additions & 0 deletions .castor/worktree.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace worktree;

use function Castor\variable;
use function docker\get_port_specs;

function read_worktree_gitdir(?string $rootDir = null): ?string
{
$rootDir ??= variable('root_dir');
$gitFile = $rootDir . '/.git';

if (!is_file($gitFile)) {
return null;
}

$contents = file_get_contents($gitFile);

if (false === $contents || !str_starts_with($contents, 'gitdir:')) {
return null;
}

return trim(substr($contents, 7));
}

function get_worktree_name(?string $rootDir = null): ?string
{
$gitdir = read_worktree_gitdir($rootDir);

if (null === $gitdir) {
return null;
}

return strtolower(basename(rtrim($gitdir, '/'))) ?: null;
}

/** @return array<string, int> */
function get_worktree_ports(?string $worktreeName): array
{
$specs = get_port_specs();

if (null === $worktreeName) {
$defaults = [];
foreach ($specs as $key => $spec) {
$defaults[$key] = $spec['default'];
}

return $defaults;
}

$hash = crc32($worktreeName) & 0x7FFFFFFF;
$base = 10000 + ($hash % 50000);

$i = 0;
$ports = [];
foreach ($specs as $key => $spec) {
$ports[$key] = $base + $i;
++$i;
}

return $ports;
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
* Do not store certificates in the router image
* Do not hardcode a user in the Dockerfile (and so the image), map it dynamically
* Mount the project in `/var/www` instead of `/home/app`
* Add git worktree support (auto-isolated project name, ports, volumes, networks)
* Add support for caching image cache in a registry
* Upgrade base to Debian Bookworm (12.5)

Expand Down
10 changes: 10 additions & 0 deletions README.dist.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ You can run `castor docker:generate-certificates --force` to recreate new certif
if some were already generated. Remember to restart the infrastructure to make
use of the new certificates with `castor build && castor up` or `castor start`.

### Git worktree support

This stack supports [git worktrees](https://git-scm.com/docs/git-worktree)
out of the box. If you run `castor start` inside a worktree, the infrastructure
is fully isolated (project name, volumes, networks, ports) without any
configuration.

Ports are displayed at the end of `castor start`, or run `castor docker:ports`
to see them at any time.

### Builder

Having some composer, yarn or other modifications to make on the project?
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ Will give you `https://app.test`, `https://www.app.test`,
> Some castor tasks have been added for DX purposes. Checkout and adapt
> the tasks `install`, `migrate` and `cache_clear` to your project.

## Git worktree support

When running `castor` commands inside a [git worktree](https://git-scm.com/docs/git-worktree),
the stack is **automatically isolated** from other worktrees:

- Docker Compose **project name**, **volumes**, and **networks** are suffixed with `-wt-<worktree-name>`
- **Ports** are derived from the worktree name (use `castor docker:ports` to display them)

This allows running multiple feature branches in parallel without conflicts.

## Usage documentation

We provide a [README.dist.md](./README.dist.md) to bootstrap your project
Expand Down
6 changes: 3 additions & 3 deletions infrastructure/docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ services:
- "/var/run/docker.sock:/var/run/docker.sock"
- "./services/router/certs:/etc/ssl/certs"
ports:
- "80:80"
- "443:443"
- "8080:8080"
- "${PROJECT_HTTP_PORT:-80}:80"
- "${PROJECT_HTTPS_PORT:-443}:443"
- "${PROJECT_ADMIN_PORT:-8080}:8080"
networks:
- default
profiles:
Expand Down
Loading