From fdbed5c52a7093c0fa7485fa39ae6083760d6946 Mon Sep 17 00:00:00 2001 From: Paul Isaris Date: Fri, 23 Oct 2026 10:32:28 +0300 Subject: [PATCH] Add an optional MySQL or MariaDB service to the Laravel CI Repositories whose tests need MySQL or MariaDB could not use laravel-ci.yml: it had no database service, so they kept their own CI. - New input database-image (for example mariadb:11.8 or mysql:8.4). The Backend tests and Browser tests jobs start it as a service on 127.0.0.1:3306 and wait for its health check. Empty means no service, as before; SQLite needs none. - New input database-name (default testing): the database that the service creates. - A first step exports DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME and DB_PASSWORD for every later step, and fails with a message for an image that is not MySQL or MariaDB. pdo_mysql is added to the PHP extensions. - The caller's phpunit.xml or env file still chooses DB_CONNECTION. - Self-check: laravel-mariadb and laravel-mysql run migrations against the service and check the server, version and exported values with db:show --json. - Guide, template, AGENTS.md and the fixtures README describe the inputs. PostgreSQL is not supported. --- .github/workflows/README.md | 60 ++++++++++++++++++++-------- .github/workflows/laravel-ci.yml | 66 ++++++++++++++++++++++++++++--- .github/workflows/self-check.yml | 30 ++++++++++++++ AGENTS.md | 8 ++-- tests/fixtures/README.md | 2 +- workflow-templates/laravel-ci.yml | 8 +++- 6 files changed, 147 insertions(+), 27 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 9240d27..28f5af9 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -32,9 +32,9 @@ These six rules prevent the common mistakes: workflow is evidence only if the shared workflow gives the same environment: for example, `pdo_mysql` from an old workflow with a MySQL service is not. -5. **Check the test database first.** The shared workflows start no MySQL or - PostgreSQL service. If the tests use one (read `phpunit.xml`, then - `.env.testing` or `.env.example`), this is a blocker: see +5. **Check the test database first.** Read `phpunit.xml`, then `.env.testing` + or `.env.example`. SQLite needs no setting. For MySQL or MariaDB, set + `database-image`. PostgreSQL is not supported: see [Environment and database](#environment-and-database) before you continue. 6. **Verify.** Run the repository's own checks, open a pull request, and read the job logs. The required check becomes `ci / CI`. @@ -193,7 +193,8 @@ workflows. It applies to people and to AI coding agents. | Browser tests in `test-command` | They run in `Backend tests`, which has no browser, and fail | Exclude the browser suite, for example `--exclude-testsuite=Browser` | | The required check still has the old name | Every new pull request waits for a check that never runs; Dependabot auto-merge stops | Require `ci / CI` (step 8) | | A green job with nothing checked | For example "No package.json, skipping" | Read the job logs (step 7) | -| The tests need MySQL or PostgreSQL | Every job fails: the workflow has no database service | See [Environment and database](#environment-and-database) | +| The tests need MySQL or MariaDB, and `database-image` is empty | The tests fail: no database server runs | Set `database-image`. See [Environment and database](#environment-and-database) | +| The tests need PostgreSQL | The workflow has no PostgreSQL service | See [Environment and database](#environment-and-database) | | `storage/framework/views` is missing in a clean checkout | Tests fail in CI only with "Please provide a valid cache path.": your local folder exists, but git does not track it | Commit the folder with a `.gitignore` that ignores everything except itself, as the other `storage/` folders do | | Old local copies of the actions stay | They drift and keep bugs that the shared copies fixed | Delete them (step 5) | @@ -207,8 +208,8 @@ for a Laravel application. | Job | What it does | | --- | --- | | `Static checks` | Code style, lint and static analysis. See [auto-detection](#laravel-auto-detection). | -| `Backend tests` | Builds the frontend assets (optional), runs `test-command`, then `npm run test` if `package.json` defines it. Uploads coverage when `coverage-file` is set. | -| `Browser tests` | Builds the assets, installs Playwright Chromium, runs `browser-test-command`. Runs only with `browser-tests: true`. | +| `Backend tests` | Starts the database service when `database-image` is set. Builds the frontend assets (optional), runs `test-command`, then `npm run test` if `package.json` defines it. Uploads coverage when `coverage-file` is set. | +| `Browser tests` | Starts the database service when `database-image` is set. Builds the assets, installs Playwright Chromium, runs `browser-test-command`. Runs only with `browser-tests: true`. | | `CI` | Aggregate check. | Every job starts with the same setup steps. They install PHP and Composer @@ -223,6 +224,8 @@ dependencies, optionally Node.js and npm dependencies, and prepare `.env` and | `php-version` | `'8.4'` | `'8.3'` | | `php-extensions` | `mbstring, dom, fileinfo, intl, pdo_sqlite, sqlite3, gd, zip, bcmath` | `mbstring, intl, pdo_mysql, redis` | | `env-file` | `''` (`.env.testing` if present, else `.env.example`) | `.env.testing`, `.env.ci` (relative to `working-directory`) | +| `database-image` | `''` (no service; SQLite needs none) | `mariadb:11.8`, `mysql:8.4` | +| `database-name` | `testing` | `app_test` (used only with `database-image`) | | `frontend` | `true` | `false` for a repository without `package.json` | | `build-frontend-for-tests` | `true` | `false` when tests do not need the Vite manifest | | `node-version-file` | `.nvmrc` (relative to the repository root) | `backend/.nvmrc`, `package.json` | @@ -265,18 +268,43 @@ The automatic Pint step writes its cache to the runner's temporary folder. If The `.env` file comes from `env-file`. An existing `.env` is kept. The action then runs `php artisan key:generate`. -The database comes from your `phpunit.xml` and `.env` file. The Laravel default, -SQLite in memory, needs nothing else. The workflow has no MySQL or PostgreSQL -service container. +The database comes from your `phpunit.xml` and `.env` file. -If the tests need MySQL or PostgreSQL, choose one of these before you adopt the -workflow: +**SQLite.** The Laravel default, SQLite in memory, needs nothing else. An SQLite +file also works. Leave `database-image` empty: `php-extensions` already has +`pdo_sqlite` and `sqlite3`. -- **Move the tests to SQLite in memory.** Set `DB_CONNECTION` and `DB_DATABASE` - in `phpunit.xml`. Use `RefreshDatabase`, and seeding (`protected $seed = - true;` in `TestCase`) when the tests expect reference data. Tests of - database-specific SQL, such as `DATE_FORMAT`, can fail on SQLite. -- **Keep the old CI** for the tests, and add only `security.yml`. +**MySQL or MariaDB.** Set `database-image` to the server that production uses, +for example `mariadb:11.8` or `mysql:8.4`. The `Backend tests` and `Browser +tests` jobs then: + +- start that image as a service on `127.0.0.1:3306`, and wait until it is + healthy, +- create the database `database-name` (default `testing`), +- add `pdo_mysql` to the PHP extensions, +- export `DB_HOST=127.0.0.1`, `DB_PORT=3306`, `DB_DATABASE`, `DB_USERNAME=root` + and `DB_PASSWORD=root` for every later step. + +Laravel reads these variables before `.env`. Your `phpunit.xml` or `env-file` +still chooses `DB_CONNECTION` (`mysql` or `mariadb`). A `` line or a +`` line in `phpunit.xml` overrides the exported values, so its +`DB_DATABASE` must match `database-name`. The `Static checks` job has no +database. + +```yaml +jobs: + ci: + uses: scify/.github/.github/workflows/laravel-ci.yml@v0.1 + with: + database-image: mariadb:11.8 # the version that production runs + test-command: vendor/bin/phpunit +``` + +**PostgreSQL** is not supported. Move the tests to SQLite, or keep your own CI +for the tests and add only `security.yml`. To move to SQLite: set +`DB_CONNECTION` and `DB_DATABASE` in `phpunit.xml`, use `RefreshDatabase`, and +seed (`protected $seed = true;` in `TestCase`) when the tests expect reference +data. Tests of database-specific SQL, such as `DATE_FORMAT`, can fail on SQLite. Every job also runs `php artisan key:generate`, which boots the application. If the application reads the database while it boots (for example `Schema::hasTable()` diff --git a/.github/workflows/laravel-ci.yml b/.github/workflows/laravel-ci.yml index 70e3f32..5083ebe 100644 --- a/.github/workflows/laravel-ci.yml +++ b/.github/workflows/laravel-ci.yml @@ -4,9 +4,10 @@ # (never an exact release such as @v0.1.4). Procedure for people and AI agents: # https://github.com/scify/.github/blob/main/.github/workflows/README.md#adopt-in-an-existing-repository # -# No database service: the tests must run on SQLite (the Laravel default in -# phpunit.xml). If they need MySQL or PostgreSQL, read "Environment and database" -# in the guide before you adopt this workflow. +# Database: SQLite (the Laravel default in phpunit.xml) needs no service. For +# MySQL or MariaDB, set database-image: the test and e2e jobs then start that +# database and export DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME and DB_PASSWORD. +# PostgreSQL is not supported. Read "Environment and database" in the guide. # # Minimal call (every value below is a default and can be left out): # @@ -26,6 +27,8 @@ # php-version: '8.4' # php-extensions: mbstring, dom, fileinfo, intl, pdo_sqlite, sqlite3, gd, zip, bcmath # env-file: .env.testing +# database-image: mariadb:11.8 +# database-name: testing # frontend: true # build-frontend-for-tests: true # node-version-file: .nvmrc @@ -53,7 +56,8 @@ # failed or was cancelled. A skipped e2e job counts as green. # # Require the check "ci / CI" in branch protection (caller job id / this job name). -# The database for tests comes from the repository's phpunit.xml and .env file. +# The database for tests comes from the repository's phpunit.xml and .env file, +# and from the exported DB_* variables when database-image is set. # Every command runs inside working-directory. # # The setup steps are YAML anchors (&name) in the lint job, reused (*name) by @@ -79,6 +83,14 @@ on: description: "File copied to .env, relative to working-directory. Empty means .env.testing if present, else .env.example. Example: .env.testing, .env.ci" type: string default: '' + database-image: + description: "Docker image of a MySQL or MariaDB service for the test and e2e jobs. Empty means no service (SQLite needs none). Example: mariadb:11.8, mysql:8.4" + type: string + default: '' + database-name: + description: 'Database that the service creates. The jobs export it as DB_DATABASE. Used only with database-image. Example: testing, app_test' + type: string + default: testing frontend: description: 'Install Node.js and run the npm checks. Set to false for repositories without package.json. One of: true, false' type: boolean @@ -150,7 +162,7 @@ jobs: uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0 with: php-version: ${{ inputs.php-version }} - extensions: ${{ inputs.php-extensions }} + extensions: ${{ inputs.database-image != '' && format('{0}, pdo_mysql', inputs.php-extensions) || inputs.php-extensions }} coverage: ${{ inputs.coverage-file != '' && 'pcov' || 'none' }} tools: composer:v2 @@ -276,11 +288,52 @@ jobs: name: Backend tests runs-on: ubuntu-latest timeout-minutes: 20 + # With an empty image, GitHub starts no service container. An image that is + # not MySQL or MariaDB also gets none, and the next step fails with a message. + services: + database: &database-service + image: ${{ (contains(inputs.database-image, 'mysql') || contains(inputs.database-image, 'mariadb')) && inputs.database-image || '' }} + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: ${{ inputs.database-name }} + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: ${{ inputs.database-name }} + ports: + - 3306:3306 + options: >- + --health-cmd "${{ contains(inputs.database-image, 'mariadb') && 'healthcheck.sh --connect --innodb_initialized' || 'mysqladmin ping -h 127.0.0.1 -uroot -proot' }}" + --health-interval 5s + --health-timeout 5s + --health-retries 20 defaults: run: working-directory: ${{ inputs.working-directory }} steps: - *checkout + + # Real environment variables take precedence over .env in Laravel. + - &database-env + name: Connect to the database service + if: inputs.database-image != '' + env: + DATABASE_IMAGE: ${{ inputs.database-image }} + DATABASE_NAME: ${{ inputs.database-name }} + run: | + case "$DATABASE_IMAGE" in + *mysql* | *mariadb*) ;; + *) + echo "::error::database-image '$DATABASE_IMAGE' is not a MySQL or MariaDB image" + exit 1 + ;; + esac + { + echo "DB_HOST=127.0.0.1" + echo "DB_PORT=3306" + echo "DB_DATABASE=$DATABASE_NAME" + echo "DB_USERNAME=root" + echo "DB_PASSWORD=root" + } >> "$GITHUB_ENV" + echo "The tests use $DATABASE_IMAGE, database $DATABASE_NAME, on 127.0.0.1:3306" - *setup-php - *prepare-env - *composer-cache-dir @@ -321,11 +374,14 @@ jobs: if: inputs.browser-tests runs-on: ubuntu-latest timeout-minutes: 20 + services: + database: *database-service defaults: run: working-directory: ${{ inputs.working-directory }} steps: - *checkout + - *database-env - *setup-php - *prepare-env - *composer-cache-dir diff --git a/.github/workflows/self-check.yml b/.github/workflows/self-check.yml index 7c9a8ca..bd8919f 100644 --- a/.github/workflows/self-check.yml +++ b/.github/workflows/self-check.yml @@ -209,6 +209,36 @@ jobs: types-command: php -l app/Models/User.php test-command: php artisan test --testsuite=Unit + # The fixture tests use SQLite, so these jobs prove the service with migrations + # and `db:show --json`: grep fails the job when the server, its version or the + # exported DB_* values are not the requested ones. + laravel-mariadb: + name: Laravel CI, MariaDB service + uses: ./.github/workflows/laravel-ci.yml + with: + working-directory: tests/fixtures/laravel + frontend: false + database-image: mariadb:11.8 + test-command: >- + export DB_CONNECTION=mariadb && php artisan migrate --force && + out=$(php artisan db:show --json) && echo "$out" && + grep -q '"name":"MariaDB","connection":"mariadb","version":"11\.8' <<< "$out" && + grep -q '"host":"127.0.0.1","port":"3306","database":"testing"' <<< "$out" + + laravel-mysql: + name: Laravel CI, MySQL service + uses: ./.github/workflows/laravel-ci.yml + with: + working-directory: tests/fixtures/laravel + frontend: false + database-image: mysql:8.4 + database-name: app_test + test-command: >- + export DB_CONNECTION=mysql && php artisan migrate --force && + out=$(php artisan db:show --json) && echo "$out" && + grep -q '"name":"MySQL","connection":"mysql","version":"8\.4' <<< "$out" && + grep -q '"host":"127.0.0.1","port":"3306","database":"app_test"' <<< "$out" + node-defaults: name: Node CI, defaults uses: ./.github/workflows/node-ci.yml diff --git a/AGENTS.md b/AGENTS.md index a156ee4..6c4db39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,9 +51,11 @@ Rules that cause failures when you break them: from the old workflow is evidence only if the shared workflow gives the same environment (for example, `pdo_mysql` from an old workflow with a MySQL service is not). - **The workflow has no database service**: if the tests need MySQL or - PostgreSQL, stop and ask the user (see the guide's "Environment and - database"). + **Check the test database first.** SQLite needs no input. For MySQL or + MariaDB, set `database-image` to the version that production runs, with + evidence (for example the old CI service or `.ddev/config.yaml`). + PostgreSQL is not supported: stop and ask the user (see the guide's + "Environment and database"). 2. **Workflows by tag, actions by SHA.** Call a reusable workflow with `@v0.1`. The organisation requires every action in a step to be pinned by full commit diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index ff66bda..9d436ce 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -12,7 +12,7 @@ order. | Folder | Contents | Smoke-test jobs | | --- | --- | --- | -| `laravel/` | The `laravel/laravel` skeleton, with `pint.json` and `.nvmrc` added | `laravel-defaults`, `laravel-commands` | +| `laravel/` | The `laravel/laravel` skeleton, with `pint.json` and `.nvmrc` added | `laravel-defaults`, `laravel-commands`, `laravel-mariadb`, `laravel-mysql` | | `node/` | A project without dependencies, with `lint`, `type-check`, `test` and `build` scripts | `node-defaults`, `node-commands`, `security-actions` | | `dev-configs/` | A `.claude` hook script and editor settings | `security-actions` | | `security/` | `composer.lock` without dependencies; `package-lock.json` with one dev dependency, `braces` 3.0.3, which has a high advisory without a fix; `.nvmrc` and a `devEngines` rule that allows only that Node.js version; a hardened `.npmrc`; and a Jinja `.env` template. The audit fails unless it runs on the `.nvmrc` version and with `npm-audit-omit-dev: true` | `security-fixture` | diff --git a/workflow-templates/laravel-ci.yml b/workflow-templates/laravel-ci.yml index 68add7f..f2e58b1 100644 --- a/workflow-templates/laravel-ci.yml +++ b/workflow-templates/laravel-ci.yml @@ -5,8 +5,8 @@ # Each commented-out input shows its default. Uncomment a line only to change it. # The "e.g." comment shows other accepted values. # In branch protection, require the check "ci / CI". -# No database service: the tests must run on SQLite. If they need MySQL or -# PostgreSQL, read "Environment and database" in the guide first. +# Database: SQLite needs no input. For MySQL or MariaDB, set database-image. +# PostgreSQL is not supported. See "Environment and database" in the guide. name: CI on: @@ -33,6 +33,10 @@ jobs: # php-extensions: mbstring, dom, fileinfo, intl, pdo_sqlite, sqlite3, gd, zip, bcmath # env-file: '' # e.g. .env.testing, .env.ci ('' = .env.testing, else .env.example) + # Database ('' = no service; SQLite needs none) + # database-image: '' # e.g. mariadb:11.8, mysql:8.4 + # database-name: testing # e.g. app_test (the service creates it; exported as DB_DATABASE) + # Frontend # frontend: true # false: repository without package.json # build-frontend-for-tests: true # false: tests do not need the Vite manifest