From c450edd722341831414fb7caa862826555e9b4be Mon Sep 17 00:00:00 2001 From: dimarik82 Date: Fri, 15 May 2026 18:23:19 +0000 Subject: [PATCH 01/12] docs: add docker-compose service design spec Design for shipping dbschemix/migrator as a thin ghcr runtime image consumed via docker-compose image:/command:, with a direct PHP config contract (return $migrator) and self-managed autoload. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...026-05-15-docker-compose-service-design.md | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-15-docker-compose-service-design.md diff --git a/docs/superpowers/specs/2026-05-15-docker-compose-service-design.md b/docs/superpowers/specs/2026-05-15-docker-compose-service-design.md new file mode 100644 index 0000000..7d5b5de --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-docker-compose-service-design.md @@ -0,0 +1,216 @@ +# Дизайн: подключение мигратора как сервиса в docker-compose + +- **Дата:** 2026-05-15 +- **Статус:** утверждён (готов к написанию плана реализации) + +## Цель + +Дать возможность удобно подключать библиотеку `dbschemix/migrator` в docker-compose +окружение через публикуемый ghcr-образ, подключаемый как сервис (`image:` + `command:`), +без написания пользователем собственного Dockerfile. + +## Зафиксированные решения + +- **Формат конфига:** прямой PHP-файл, возвращающий `return $migrator;`. Слоя парсинга + YAML/JSON нет. +- **Autoload:** конфиг пользователя сам делает `require '.../vendor/autoload.php'` + (включая саму библиотеку и кастомные классы). +- **Образ:** тонкий runtime (PHP CLI + pdo-расширения + bootstrap-скрипт), без библиотеки. + Версия библиотеки определяется composer'ом пользователя, не тегом образа. +- **PHP-код библиотеки не меняется** — `Console::run()` уже делает `exit($code)`, + что корректно для контейнера. +- **Механика entrypoint:** PHP-bootstrap как `ENTRYPOINT`, docker `command:` попадает + в `$argv`, Symfony Console читает его сам. +- **Обнаружение конфига:** env-переменная `MIGRATOR_CONFIG`, значение по умолчанию + `/app/migrator.php`. +- **Deliverables:** Dockerfile + entrypoint, CI-публикация в ghcr, пример интеграции, + раздел README. + +## 1. Архитектура + +Поток запуска: + +``` +docker compose run migrator migrate:up --limit=1 + │ + ▼ +ENTRYPOINT ["php","/usr/local/bin/migrator"] argv = [script, "migrate:up", "--limit=1"] + │ + ▼ +migrator (bootstrap): + 1. $config = getenv('MIGRATOR_CONFIG') ?: '/app/migrator.php' + 2. проверки: файл существует и читается + 3. $migrator = require $config; // конфиг сам require'ит vendor/autoload.php + 4. проверка: $migrator instanceof \dbschemix\core\MigratorInterface + 5. \dbschemix\migrator\cmd\Console::run($migrator) + │ + ▼ +Symfony Application читает $argv → выполняет команду → exit(code) +``` + +Границы ответственности: + +- **Образ** — воспроизводимая среда выполнения (PHP + pdo + bootstrap). +- **Конфиг пользователя** — wiring: autoload, `Migration[]`, драйверы, `eventSubscribers`. +- **docker `command:`** — какую миграционную команду выполнить. + +## 2. Компоненты + +### 2.1. Bootstrap-скрипт `migrator` + +Расположение в репозитории: `.docker/migrator/migrator`. + +PHP-скрипт (~40 строк). Единственная логика: резолв пути к конфигу, валидация +контракта, делегирование в `Console`. Сам **не** require'ит autoload — это делает +конфиг. На `\dbschemix\migrator\cmd\Console` ссылается строкой полного FQCN уже +**после** `require $config`, поэтому отсутствие библиотеки на этапе парсинга +скрипта не является проблемой. + +Логика резолва/валидации выносится в маленькую тестируемую единицу (функция или +класс), а тонкий `migrator` лишь вызывает её и затем `Console::run()`. + +Контракт конфиг-файла (документируется в README): + +- файл заканчивается `return $migrator;`; +- конфиг сам отвечает за `require '.../vendor/autoload.php'`; +- пути миграций задаются через `__DIR__` (резолвятся внутри контейнера при + монтировании проекта пользователя). + +### 2.2. Dockerfile + +Расположение: `.docker/migrator/Dockerfile`. Контекст сборки — `.docker/migrator` +(как у существующих сервисов `postgresql`/`mysql`), поэтому корневой `.dockerignore` +(исключающий `.docker`) не мешает `COPY`. + +- база: `ghcr.io/kuaukutsu/php:${PHP_VERSION}-cli` (уже используется в проекте, + `PHP_VERSION` по умолчанию `8.3`); +- `install-php-extensions pdo_mysql pdo_pgsql pdo_sqlite`; +- `COPY migrator /usr/local/bin/migrator` + `chmod +x`; +- непривилегированный пользователь; +- `ENV MIGRATOR_CONFIG=/app/migrator.php`; +- `ENTRYPOINT ["php","/usr/local/bin/migrator"]`, `CMD []`. + +### 2.3. CI-workflow + +Расположение: `.github/workflows/docker-publish.yml`. Триггер: `release: [published]` +(как у существующего `published.yml`). + +- `permissions: packages: write`; +- `docker/login-action` с `GITHUB_TOKEN`; +- `docker/metadata-action` → теги: семвер из релиза (`X.Y.Z`, `X.Y`, `X`) + `latest`; +- `docker/build-push-action`, контекст `.docker/migrator`, + платформы `linux/amd64,linux/arm64`; +- образ: `ghcr.io/dbschemix/migrator`. + +### 2.4. Пример интеграции + +- `example/docker/migrator.php` — конфиг в новом контракте + (`require autoload` + `return new Migrator(...)`), на базе существующего + `example/cli.php` (sqlite), с `PrettyConsoleOutput` в `eventSubscribers`. +- Сниппет `docker-compose` в README. + +### 2.5. README + +Новый раздел «Docker / docker-compose»: контракт конфига, `MIGRATOR_CONFIG`, +монтирование проекта, примеры `command:`, заметка про `init: true` +(сигналы / PID 1). + +## 3. Поток данных и пример использования + +Конфиг пользователя `migrator.php`: + +```php + Date: Fri, 15 May 2026 18:30:31 +0000 Subject: [PATCH 02/12] docs: add docker-compose service implementation plan Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-15-docker-compose-service.md | 703 ++++++++++++++++++ 1 file changed, 703 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-15-docker-compose-service.md diff --git a/docs/superpowers/plans/2026-05-15-docker-compose-service.md b/docs/superpowers/plans/2026-05-15-docker-compose-service.md new file mode 100644 index 0000000..5a70ae4 --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-docker-compose-service.md @@ -0,0 +1,703 @@ +# Docker-compose Service Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship `dbschemix/migrator` as a thin, publishable ghcr runtime image that runs migration commands from a user's PHP config (`return $migrator;`) when wired into docker-compose via `image:` + `command:`. + +**Architecture:** A standalone PHP bootstrap shim is `ENTRYPOINT` in a thin runtime image (PHP CLI + pdo extensions, no library). The shim resolves the config path, `require`s the user's config (which registers the user's autoload, including the library from their `vendor/`), validates the returned value through a small testable library class `dbschemix\migrator\cmd\Bootstrap`, then delegates to the existing `Console::run()`. Docker `command:` arrives as `$argv` and is read by Symfony Console. + +**Tech Stack:** PHP 8.3, Symfony Console (existing), PHPUnit 12, Docker, GitHub Actions (docker/build-push-action), ghcr.io. + +--- + +## File Structure + +| File | Responsibility | Action | +|---|---|---| +| `src/cmd/Bootstrap.php` | Pure, testable validation of the config return value (`assertMigrator`) | Create | +| `tests/cmd/BootstrapTest.php` | Unit tests for `Bootstrap::assertMigrator` (both branches) | Create | +| `.docker/migrator/migrator` | Standalone entrypoint shim: resolve path → require config → validate → run Console (infra, not statically analyzed; no `.php` extension) | Create | +| `.docker/migrator/Dockerfile` | Thin runtime image definition | Create | +| `example/docker/migrator.php` | Example config in the new contract (sqlite, `return $migrator;`) | Create | +| `.github/workflows/docker-publish.yml` | Build & push multi-arch image to ghcr on release | Create | +| `.github/workflows/docker-runtime.yml` | PR integration job: build image, run `migrate:init`/`migrate:up` against the sqlite example, assert exit 0 | Create | +| `Makefile` | `docker-runtime` target for local build/run | Modify | +| `README.md` | New "Docker / docker-compose" section | Modify | + +**Design note vs. spec:** the spec said the library PHP code "скорее всего не требует изменений". This plan adds **one small additive class** `src/cmd/Bootstrap.php` — this is exactly the "маленькая тестируемая единица" the spec §2.1 calls for. `Console` is unchanged. The path-resolution and file-readable checks stay inline in the shim (trivial, exercised by the integration job) because they must run *before* any autoload exists in the thin runtime. + +--- + +## Task 1: `Bootstrap::assertMigrator` (library, TDD) + +**Files:** +- Create: `src/cmd/Bootstrap.php` +- Test: `tests/cmd/BootstrapTest.php` + +- [ ] **Step 1: Write the failing test** + +Create `tests/cmd/BootstrapTest.php`: + +```php +expectException(RuntimeException::class); + $this->expectExceptionMessage('return $migrator'); + + Bootstrap::assertMigrator(null); + } + + #[Test] + public function assert_migrator_throws_when_config_returned_other_object(): void + { + // Given / When / Then + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('MigratorInterface'); + + Bootstrap::assertMigrator(new stdClass()); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `vendor/bin/phpunit --filter BootstrapTest` +Expected: FAIL — `Class "dbschemix\migrator\cmd\Bootstrap" not found`. + +- [ ] **Step 3: Write minimal implementation** + +Create `src/cmd/Bootstrap.php`: + +```php + 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); +``` + +- [ ] **Step 2: Syntax-check the shim** + +Run: `php -l .docker/migrator/migrator` +Expected: `No syntax errors detected in .docker/migrator/migrator` + +- [ ] **Step 3: Commit** + +```bash +git add .docker/migrator/migrator +git commit -m "feat: add container entrypoint shim" +``` + +--- + +## Task 3: Runtime Dockerfile + +**Files:** +- Create: `.docker/migrator/Dockerfile` + +- [ ] **Step 1: Create the Dockerfile** + +Create `.docker/migrator/Dockerfile` (build context is `.docker/migrator`, mirroring the existing `postgresql`/`mysql` services, so the repo-root `.dockerignore` is irrelevant): + +```dockerfile +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 /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 [] +``` + +- [ ] **Step 2: Build the image** + +Run: `docker build -t migrator-runtime:dev .docker/migrator` +Expected: build succeeds; final line `naming to docker.io/library/migrator-runtime:dev`. + +- [ ] **Step 3: Verify entrypoint fails cleanly with no config** + +Run: `docker run --rm migrator-runtime:dev migrate:up` +Expected: stderr `migrator: config not found or not readable: /app/migrator.php` and exit code `1`. + +Verify exit code: `docker run --rm migrator-runtime:dev migrate:up; echo $?` +Expected: prints `1`. + +- [ ] **Step 4: Commit** + +```bash +git add .docker/migrator/Dockerfile +git commit -m "feat: add thin runtime Dockerfile" +``` + +--- + +## Task 4: Example config in the new contract + +**Files:** +- Create: `example/docker/migrator.php` + +- [ ] **Step 1: Confirm the sqlite example assets exist** + +Run: `ls example/migration/sqlite/memory && ls -d example/data/sqlite` +Expected: lists the sqlite migration files and the `example/data/sqlite` directory (used by the existing `example/cli.php`). + +- [ ] **Step 2: Create the example config** + +Create `example/docker/migrator.php` (file lives at `/app/example/docker/migrator.php` inside the container; project root is two levels up): + +```php + Note: the trailing `@var` line is a no-op comment after `return`; if `composer check` (phpstan over `example/`) complains about unreachable/dead code, delete that final comment line — it is only documentation. Keep the explicit `MigratorInterface` import so the contract is greppable. + +- [ ] **Step 3: Syntax + static analysis** + +Run: `php -l example/docker/migrator.php && composer check` +Expected: no syntax errors; `composer check` passes (phpstan analyzes `example/`). If phpstan flags the trailing `@var` comment line, remove it and re-run until green. + +- [ ] **Step 4: Run the example through the runtime image** + +Run: +```bash +docker run --rm -u "$(id -u)" -e MIGRATOR_CONFIG=/app/example/docker/migrator.php \ + -v "$PWD:/app" migrator-runtime:dev migrate:init +docker run --rm -u "$(id -u)" -e MIGRATOR_CONFIG=/app/example/docker/migrator.php \ + -v "$PWD:/app" migrator-runtime:dev migrate:up +``` +Expected: `migrate:init` prints initialization output; `migrate:up` prints `[sqlite/db] up: ...` lines; both exit `0`. + +- [ ] **Step 5: Clean up generated sqlite state** + +Run: `git status --porcelain example/data` and remove any generated `db.sqlite3` so it is not committed: +```bash +git checkout -- example/data 2>/dev/null || true +git clean -fd example/data +``` +Expected: `example/data` back to its committed state. + +- [ ] **Step 6: Commit** + +```bash +git add example/docker/migrator.php +git commit -m "feat: add docker example config (return \$migrator contract)" +``` + +--- + +## Task 5: Makefile `docker-runtime` target + +**Files:** +- Modify: `Makefile` (add target under the `## Application` section, before `stop:`) + +- [ ] **Step 1: Add the target** + +In `Makefile`, immediately after the `example:` target block and before `stop: ## Stop server`, insert: + +```makefile +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 + git clean -fd example/data + +``` + +(`USER = $$(id -u)` is already defined at the top of the Makefile.) + +- [ ] **Step 2: Verify the target is listed and runs** + +Run: `make help | grep docker-runtime` +Expected: shows `docker-runtime Build runtime image and run the sqlite example through it`. + +Run: `make docker-runtime` +Expected: image builds; `migrate:init` and `migrate:up` succeed; ends cleanly. + +- [ ] **Step 3: Commit** + +```bash +git add Makefile +git commit -m "chore: add docker-runtime make target" +``` + +--- + +## Task 6: GitHub Actions — publish to ghcr + +**Files:** +- Create: `.github/workflows/docker-publish.yml` + +- [ ] **Step 1: Create the workflow** + +Create `.github/workflows/docker-publish.yml`: + +```yaml +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 }} +``` + +- [ ] **Step 2: Validate workflow YAML** + +Run: `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/docker-publish.yml')); print('ok')"` +Expected: prints `ok`. + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/docker-publish.yml +git commit -m "ci: publish runtime image to ghcr on release" +``` + +--- + +## Task 7: GitHub Actions — PR integration job + +**Files:** +- Create: `.github/workflows/docker-runtime.yml` + +- [ ] **Step 1: Create the workflow** + +Create `.github/workflows/docker-runtime.yml`: + +```yaml +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 +``` + +- [ ] **Step 2: Validate workflow YAML** + +Run: `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/docker-runtime.yml')); print('ok')"` +Expected: prints `ok`. + +- [ ] **Step 3: Locally reproduce the integration steps** + +Run: +```bash +docker build -t migrator-runtime:ci .docker/migrator +docker run --rm -u "$(id -u)" -e MIGRATOR_CONFIG=/app/example/docker/migrator.php -v "$PWD:/app" migrator-runtime:ci migrate:init +docker run --rm -u "$(id -u)" -e MIGRATOR_CONFIG=/app/example/docker/migrator.php -v "$PWD:/app" migrator-runtime:ci migrate:up +docker run --rm -u "$(id -u)" -e MIGRATOR_CONFIG=/app/composer.json -v "$PWD:/app" migrator-runtime:ci migrate:up; echo "exit=$?" +git clean -fd example/data +``` +Expected: init/up succeed; the `composer.json` run prints `migrator: config must end with "return $migrator;"...` and `exit=1`. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/docker-runtime.yml +git commit -m "ci: smoke-test runtime image on pull requests" +``` + +--- + +## Task 8: README documentation + +**Files:** +- Modify: `README.md` (append a new section after the existing `### Example` block, end of file) + +- [ ] **Step 1: Append the Docker section** + +Append to the end of `README.md`: + +```markdown + +### 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 + Date: Fri, 15 May 2026 18:57:37 +0000 Subject: [PATCH 03/12] feat: add Bootstrap::assertMigrator config-contract guard --- src/cmd/Bootstrap.php | 36 ++++++++++++++++++++++++++ tests/cmd/BootstrapTest.php | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/cmd/Bootstrap.php create mode 100644 tests/cmd/BootstrapTest.php diff --git a/src/cmd/Bootstrap.php b/src/cmd/Bootstrap.php new file mode 100644 index 0000000..9ec88f7 --- /dev/null +++ b/src/cmd/Bootstrap.php @@ -0,0 +1,36 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('return $migrator'); + + Bootstrap::assertMigrator(null); + } + + #[Test] + public function assert_migrator_throws_when_config_returned_other_object(): void + { + // Given / When / Then + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('MigratorInterface'); + + Bootstrap::assertMigrator(new stdClass()); + } +} From 2de4e2792b24fe9b569efdb9e638e13f40147dee Mon Sep 17 00:00:00 2001 From: dimarik82 Date: Fri, 15 May 2026 19:01:27 +0000 Subject: [PATCH 04/12] feat: add container entrypoint shim --- .docker/migrator/migrator | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .docker/migrator/migrator diff --git a/.docker/migrator/migrator b/.docker/migrator/migrator new file mode 100644 index 0000000..f80419f --- /dev/null +++ b/.docker/migrator/migrator @@ -0,0 +1,42 @@ + 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); From c0ed177e67a9b5c9df0c6238bc6334737fb0eefc Mon Sep 17 00:00:00 2001 From: dimarik82 Date: Fri, 15 May 2026 19:05:19 +0000 Subject: [PATCH 05/12] feat: add thin runtime Dockerfile --- .docker/migrator/Dockerfile | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .docker/migrator/Dockerfile diff --git a/.docker/migrator/Dockerfile b/.docker/migrator/Dockerfile new file mode 100644 index 0000000..96026d5 --- /dev/null +++ b/.docker/migrator/Dockerfile @@ -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 /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 [] From 472f496176ea103acc969956d4575c757eb29ff8 Mon Sep 17 00:00:00 2001 From: dimarik82 Date: Fri, 15 May 2026 19:36:26 +0000 Subject: [PATCH 06/12] feat: add docker example config (return $migrator contract) Co-Authored-By: Claude Opus 4.7 (1M context) --- example/docker/migrator.php | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 example/docker/migrator.php diff --git a/example/docker/migrator.php b/example/docker/migrator.php new file mode 100644 index 0000000..64822cc --- /dev/null +++ b/example/docker/migrator.php @@ -0,0 +1,36 @@ + Date: Fri, 15 May 2026 19:36:26 +0000 Subject: [PATCH 07/12] chore: add docker-runtime make target Co-Authored-By: Claude Opus 4.7 (1M context) --- Makefile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Makefile b/Makefile index 2e9f985..58f3e22 100644 --- a/Makefile +++ b/Makefile @@ -77,6 +77,14 @@ example: docker compose run --rm -u $(USER) -w /example app sh -l make stop +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 + git clean -fd example/data + stop: ## Stop server docker compose -f ./docker-compose.yml stop From b72688e7c5fc4fb8cb62a4be3229be78cd783404 Mon Sep 17 00:00:00 2001 From: dimarik82 Date: Fri, 15 May 2026 19:36:26 +0000 Subject: [PATCH 08/12] ci: publish runtime image to ghcr on release Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/docker-publish.yml | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..8500fff --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -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 }} From 9d74865c48ad900f0303066128fec14429ea495f Mon Sep 17 00:00:00 2001 From: dimarik82 Date: Fri, 15 May 2026 19:36:26 +0000 Subject: [PATCH 09/12] ci: smoke-test runtime image on pull requests Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/docker-runtime.yml | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/docker-runtime.yml diff --git a/.github/workflows/docker-runtime.yml b/.github/workflows/docker-runtime.yml new file mode 100644 index 0000000..578db05 --- /dev/null +++ b/.github/workflows/docker-runtime.yml @@ -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 From d7e234f0d567ce4777f98e77650f4d21fc6b232e Mon Sep 17 00:00:00 2001 From: dimarik82 Date: Fri, 15 May 2026 19:36:26 +0000 Subject: [PATCH 10/12] docs: document docker-compose usage Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/README.md b/README.md index e87513d..1eadddc 100644 --- a/README.md +++ b/README.md @@ -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 + Date: Fri, 15 May 2026 22:43:37 +0300 Subject: [PATCH 11/12] smoke tests --- .docker/migrator/Dockerfile | 2 +- .docker/migrator/{migrator => migrator.php} | 0 Makefile | 23 +++++++-------------- example/docker/migrator.php | 3 +-- 4 files changed, 10 insertions(+), 18 deletions(-) rename .docker/migrator/{migrator => migrator.php} (100%) diff --git a/.docker/migrator/Dockerfile b/.docker/migrator/Dockerfile index 96026d5..6163858 100644 --- a/.docker/migrator/Dockerfile +++ b/.docker/migrator/Dockerfile @@ -6,7 +6,7 @@ FROM ghcr.io/kuaukutsu/php:${PHP_VERSION}-cli RUN install-php-extensions pdo_mysql pdo_pgsql pdo_sqlite # Entrypoint shim (the library itself comes from the user's mounted vendor/) -COPY migrator /usr/local/bin/migrator +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 diff --git a/.docker/migrator/migrator b/.docker/migrator/migrator.php similarity index 100% rename from .docker/migrator/migrator rename to .docker/migrator/migrator.php diff --git a/Makefile b/Makefile index 58f3e22..04a1098 100644 --- a/Makefile +++ b/Makefile @@ -65,17 +65,7 @@ fix: phpcbf rector ## run fix tools check: phpcs psalm phpstan ## run analysis tools -## 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 +## Smoke test docker-runtime: ## Build runtime image and run the sqlite example through it docker build -t migrator-runtime:dev .docker/migrator @@ -83,13 +73,16 @@ docker-runtime: ## Build runtime image and run the sqlite example through it -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 -stop: ## Stop server - docker compose -f ./docker-compose.yml stop +## Application -down: stop - docker compose -f ./docker-compose.yml down --remove-orphans +cli: + $(DOCKER_RUN) -w /app/example \ + ghcr.io/kuaukutsu/php:${PHP_VERSION}-cli \ + sh -l remove: down _image_remove _container_remove _volume_remove diff --git a/example/docker/migrator.php b/example/docker/migrator.php index 64822cc..c328e32 100644 --- a/example/docker/migrator.php +++ b/example/docker/migrator.php @@ -31,6 +31,5 @@ ], ); -return $migrator; - /** @var MigratorInterface $migrator phpstan: documents the contract */ +return $migrator; From 9b6c61c9aa9075ea661d060521db6e9a0a1a6952 Mon Sep 17 00:00:00 2001 From: dimarik82 Date: Fri, 15 May 2026 22:48:02 +0300 Subject: [PATCH 12/12] fix style --- example/docker/migrator.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/example/docker/migrator.php b/example/docker/migrator.php index c328e32..d854e1b 100644 --- a/example/docker/migrator.php +++ b/example/docker/migrator.php @@ -7,7 +7,6 @@ use dbschemix\pdo\Driver; use dbschemix\core\Migration; use dbschemix\core\Migrator; -use dbschemix\core\MigratorInterface; use dbschemix\migrator\tools\PrettyConsoleOutput; require dirname(__DIR__, 2) . '/vendor/autoload.php'; @@ -17,7 +16,7 @@ * It is responsible for its own autoload (line above) and for resolving * paths via __DIR__ so they work inside the mounted container. */ -$migrator = new Migrator( +return new Migrator( list: [ new Migration( path: dirname(__DIR__) . '/migration/sqlite/memory', @@ -30,6 +29,3 @@ new PrettyConsoleOutput(), ], ); - -/** @var MigratorInterface $migrator phpstan: documents the contract */ -return $migrator;