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
20 changes: 20 additions & 0 deletions .docker/migrator/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
ARG PHP_VERSION=8.3

FROM ghcr.io/kuaukutsu/php:${PHP_VERSION}-cli

# Database drivers the library can use via dbschemix/pdo
RUN install-php-extensions pdo_mysql pdo_pgsql pdo_sqlite

# Entrypoint shim (the library itself comes from the user's mounted vendor/)
COPY migrator.php /usr/local/bin/migrator
RUN chmod +x /usr/local/bin/migrator

# Unprivileged user; override with `-u $(id -u)` to match host file ownership
RUN adduser -D -H -s /sbin/nologin migrator

ENV MIGRATOR_CONFIG=/app/migrator.php
WORKDIR /app
USER migrator

ENTRYPOINT ["php", "/usr/local/bin/migrator"]
CMD []
42 changes: 42 additions & 0 deletions .docker/migrator/migrator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

/**
* Container entrypoint for dbschemix/migrator.
*
* Flow: resolve config path -> require user's config (it registers its own
* autoload, incl. the library from the user's vendor/) -> validate the
* returned Migrator -> hand argv-driven control to Console.
*/

$configPath = getenv('MIGRATOR_CONFIG');
if ($configPath === false || $configPath === '') {
$configPath = '/app/migrator.php';
}

if (!is_file($configPath) || !is_readable($configPath)) {
fwrite(
STDERR,
"migrator: config not found or not readable: {$configPath}\n"
. "Set MIGRATOR_CONFIG or mount your project so the file is reachable.\n"
);
exit(1);
}

try {
/** @psalm-suppress UnresolvableInclude */
$config = require $configPath;
} catch (\Throwable $e) {
fwrite(STDERR, 'migrator: failed to load config: ' . $e->getMessage() . "\n");
exit(1);
}

try {
$migrator = \dbschemix\migrator\cmd\Bootstrap::assertMigrator($config);
} catch (\Throwable $e) {
fwrite(STDERR, 'migrator: ' . $e->getMessage() . "\n");
exit(1);
}

\dbschemix\migrator\cmd\Console::run($migrator);
49 changes: 49 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Docker Publish

on:
release:
types: [ published ]

jobs:
image:
name: Build and push image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/dbschemix/migrator
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .docker/migrator
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
50 changes: 50 additions & 0 deletions .github/workflows/docker-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Docker Runtime

on: [ pull_request ]

jobs:
runtime:
name: runtime image smoke test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
extensions: pdo pdo_sqlite
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Composer install dependencies
uses: ramsey/composer-install@v3
with:
dependency-versions: "highest"
composer-options: "--optimize-autoloader"

- name: Build runtime image
run: docker build -t migrator-runtime:ci .docker/migrator

- name: migrate:init through the image
run: |
docker run --rm -u "$(id -u)" \
-e MIGRATOR_CONFIG=/app/example/docker/migrator.php \
-v "$PWD:/app" migrator-runtime:ci migrate:init

- name: migrate:up through the image
run: |
docker run --rm -u "$(id -u)" \
-e MIGRATOR_CONFIG=/app/example/docker/migrator.php \
-v "$PWD:/app" migrator-runtime:ci migrate:up

- name: Bad config contract fails with exit 1
run: |
set +e
docker run --rm -u "$(id -u)" \
-e MIGRATOR_CONFIG=/app/composer.json \
-v "$PWD:/app" migrator-runtime:ci migrate:up
code=$?
set -e
test "$code" -eq 1
23 changes: 12 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,24 +65,25 @@ fix: phpcbf rector ## run fix tools

check: phpcs psalm phpstan ## run analysis tools

## Smoke test

docker-runtime: ## Build runtime image and run the sqlite example through it
docker build -t migrator-runtime:dev .docker/migrator
docker run --rm -u $(USER) -e MIGRATOR_CONFIG=/app/example/docker/migrator.php \
-v "$$(pwd):/app" migrator-runtime:dev migrate:init
docker run --rm -u $(USER) -e MIGRATOR_CONFIG=/app/example/docker/migrator.php \
-v "$$(pwd):/app" migrator-runtime:dev migrate:up
docker run --rm -u $(USER) -e MIGRATOR_CONFIG=/app/example/docker/migrator.php \
-v "$$(pwd):/app" migrator-runtime:dev migrate:down
git clean -fd example/data

## Application

cli:
$(DOCKER_RUN) -w /app/example \
ghcr.io/kuaukutsu/php:${PHP_VERSION}-cli \
sh -l

example:
VERSION=$(VERSION) USER=$(USER) \
docker compose run --rm -u $(USER) -w /example app sh -l
make stop

stop: ## Stop server
docker compose -f ./docker-compose.yml stop

down: stop
docker compose -f ./docker-compose.yml down --remove-orphans

remove: down _image_remove _container_remove _volume_remove

_image_remove:
Expand Down
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,67 @@ INSERT INTO ededede
202603070850_test2.sql: SQLSTATE[HY000]: General error: 1 incomplete input
```

### Docker / docker-compose

The library ships a thin runtime image. The image contains PHP, the
`pdo_mysql` / `pdo_pgsql` / `pdo_sqlite` extensions and an entrypoint — it
does **not** contain the library. The library and your custom code
(e.g. `eventSubscribers`) come from your project's mounted `vendor/`.

**Config contract.** Provide a PHP file that returns the `Migrator`:

```php
<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use dbschemix\pdo\Driver;
use dbschemix\core\{Migration, Migrator};
use dbschemix\migrator\tools\PrettyConsoleOutput;

$migrator = new Migrator(
list: [
new Migration(
path: __DIR__ . '/migration/pgsql/main',
driver: new Driver('pgsql:host=postgres;port=5432;dbname=main', 'postgres', 'postgres'),
),
],
eventSubscribers: [
new PrettyConsoleOutput(),
],
);

return $migrator;
```

The file is responsible for its own autoload and must end with
`return $migrator;`. Resolve paths with `__DIR__` so they work inside the
mounted container. `eventSubscribers` is plain PHP — list instances of any
class (including your own), no special notation.

**docker-compose service.** Mount your project, point `MIGRATOR_CONFIG` at
the config file, and pass the migration command via `command:`:

```yaml
services:
migrator:
image: ghcr.io/dbschemix/migrator:1
init: true
environment:
MIGRATOR_CONFIG: /app/migrator.php
volumes:
- ./:/app
command: ["migrate:up", "--limit=1"]
depends_on:
postgres:
condition: service_healthy
```

`MIGRATOR_CONFIG` defaults to `/app/migrator.php`. `init: true` ensures
signals (e.g. `docker compose stop`) are delivered cleanly. Any
`migrate:*` command and its options are accepted, exactly as in the CLI.

A runnable sqlite example lives in `example/docker/migrator.php`; build and
exercise it locally with `make docker-runtime`.
Loading
Loading