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
60 changes: 44 additions & 16 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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) |

Expand All @@ -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
Expand All @@ -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` |
Expand Down Expand Up @@ -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 `<server>` line or a
`<env force="true">` 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()`
Expand Down
66 changes: 61 additions & 5 deletions .github/workflows/laravel-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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):
#
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/self-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
8 changes: 6 additions & 2 deletions workflow-templates/laravel-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
Loading