name: CI
on:
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
workflow-lint:
name: Workflow lint
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install and run actionlint
shell: bash
run: |
set -euo pipefail
curl --fail --location --silent --show-error \
--output actionlint.tar.gz \
https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz
printf '%s %s\n' '8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8' 'actionlint.tar.gz' | sha256sum --check
tar -xzf actionlint.tar.gz actionlint
./actionlint -color
quality:
name: Quality (PHP 8.2)
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.2'
extensions: pdo, pdo_mysql
coverage: none
tools: composer:2.10.2
- name: Print tool versions
shell: bash
run: |
set -euo pipefail
php -v
composer --version
- name: Validate and resolve dependencies
shell: bash
run: |
set -euo pipefail
composer validate --strict
composer update \
--no-interaction \
--prefer-dist \
--no-progress
composer check-platform-reqs
composer audit \
--no-interaction \
--abandoned=fail
- name: PHP syntax check
shell: bash
run: |
set -euo pipefail
shopt -s globstar nullglob
files=(src/**/*.php tests/**/*.php .php-cs-fixer.php)
if [[ -d examples ]]; then
files+=(examples/**/*.php)
fi
for file in "${files[@]}"; do
php -l "$file"
done
- name: PHPStan
shell: bash
run: |
set -euo pipefail
vendor/bin/phpstan analyse -c phpstan.neon --no-progress
- name: Code style
shell: bash
run: |
set -euo pipefail
vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Repository integrity
shell: bash
run: |
set -euo pipefail
git diff --exit-code
unit-regression:
name: Unit/Regression (PHP ${{ matrix.php }})
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
php:
- '8.2'
- '8.3'
- '8.4'
- '8.5'
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php }}
extensions: pdo, pdo_mysql
coverage: none
tools: composer:2.10.2
- name: Print tool versions
shell: bash
run: |
set -euo pipefail
php -v
composer --version
- name: Validate and resolve dependencies
shell: bash
run: |
set -euo pipefail
composer validate --strict
composer update \
--no-interaction \
--prefer-dist \
--no-progress
composer check-platform-reqs
composer audit \
--no-interaction \
--abandoned=fail
- name: Run Unit and Regression suites
shell: bash
run: |
set -euo pipefail
vendor/bin/phpunit --testsuite unit
vendor/bin/phpunit --testsuite regression
- name: Repository integrity
shell: bash
run: |
set -euo pipefail
git diff --exit-code
lowest-dependencies:
name: Lowest dependencies (PHP 8.2, MySQL 8.4.10)
runs-on: ubuntu-24.04
timeout-minutes: 25
services:
mysql:
image: mysql:8.4.10
env:
MYSQL_DATABASE: maatify_persistence_test
MYSQL_USER: maatify_persistence
MYSQL_PASSWORD: persistence_ci_password
MYSQL_ROOT_PASSWORD: root_ci_password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -proot_ci_password"
--health-interval=10s
--health-timeout=5s
--health-retries=30
env:
PERSISTENCE_TEST_MYSQL_DSN: mysql:host=127.0.0.1;port=3306;dbname=maatify_persistence_test;charset=utf8mb4
PERSISTENCE_TEST_MYSQL_USER: maatify_persistence
PERSISTENCE_TEST_MYSQL_PASSWORD: persistence_ci_password
steps:
- name: Checkout
id: checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup PHP
id: setup-php
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.2'
extensions: pdo, pdo_mysql
coverage: none
tools: composer:2.10.2
- name: Print tool versions
shell: bash
run: |
set -euo pipefail
php -v
composer --version
- name: Validate and resolve lowest dependencies
shell: bash
run: |
set -euo pipefail
composer validate --strict
composer update \
--prefer-lowest \
--prefer-stable \
--no-interaction \
--prefer-dist \
--no-progress
composer check-platform-reqs
composer audit \
--no-interaction \
--abandoned=fail
- name: Static analysis and code style
shell: bash
run: |
set -euo pipefail
vendor/bin/phpstan analyse -c phpstan.neon --no-progress
vendor/bin/php-cs-fixer fix --dry-run --diff
- name: Configure MySQL trigger creation policy
shell: bash
run: |
set -euo pipefail
container_id='${{ job.services.mysql.id }}'
value="$(docker exec \
-e MYSQL_PWD=root_ci_password \
"$container_id" \
mysql \
--user=root \
--batch \
--skip-column-names \
--execute="
SET GLOBAL log_bin_trust_function_creators = 1;
SELECT @@GLOBAL.log_bin_trust_function_creators;
")"
if [[ "$value" != "1" ]]; then
echo "Expected @@GLOBAL.log_bin_trust_function_creators to be 1, got: $value"
exit 1
fi
- name: Run test suites
shell: bash
run: |
set -euo pipefail
vendor/bin/phpunit --testsuite unit
vendor/bin/phpunit --testsuite regression
vendor/bin/phpunit --testsuite integration
vendor/bin/phpunit --testsuite integration
vendor/bin/phpunit
- name: Verify MySQL residue
if: ${{ always() && !cancelled() && steps.setup-php.outcome == 'success' }}
shell: bash
run: |
set -euo pipefail
php <<'PHP'
<?php
declare(strict_types=1);
$dsn = getenv('PERSISTENCE_TEST_MYSQL_DSN');
$user = getenv('PERSISTENCE_TEST_MYSQL_USER');
$password = getenv('PERSISTENCE_TEST_MYSQL_PASSWORD');
if (!is_string($dsn) || $dsn === '' || !is_string($user) || $user === '' || !is_string($password)) {
fwrite(STDERR, "Missing MySQL test environment configuration.\n");
exit(1);
}
$pdo = new PDO($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$schema = (string) $pdo->query('SELECT DATABASE()')->fetchColumn();
$tables = [
'maa_persistence_test_global_ordering',
'maa_persistence_test_scoped_ordering',
'maa_persistence_test_pagination_items',
];
$triggers = [
'maa_persistence_test_fail_global_target_update',
'maa_persistence_test_fail_scoped_target_update',
];
$tableStatement = $pdo->prepare(
'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (?, ?, ?) ORDER BY TABLE_NAME'
);
$tableStatement->execute([$schema, ...$tables]);
$remainingTables = $tableStatement->fetchAll(PDO::FETCH_COLUMN);
$triggerStatement = $pdo->prepare(
'SELECT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME IN (?, ?) ORDER BY TRIGGER_NAME'
);
$triggerStatement->execute([$schema, ...$triggers]);
$remainingTriggers = $triggerStatement->fetchAll(PDO::FETCH_COLUMN);
if ($remainingTables !== [] || $remainingTriggers !== []) {
fwrite(STDERR, "MySQL residue detected.\n");
if ($remainingTables !== []) {
fwrite(STDERR, 'Remaining tables: ' . implode(', ', $remainingTables) . "\n");
}
if ($remainingTriggers !== []) {
fwrite(STDERR, 'Remaining triggers: ' . implode(', ', $remainingTriggers) . "\n");
}
exit(1);
}
echo "MySQL residue verification passed: no package test tables or triggers remain.\n";
PHP
- name: Repository integrity
if: ${{ always() && !cancelled() && steps.checkout.outcome == 'success' }}
shell: bash
run: |
set -euo pipefail
git diff --exit-code
integration:
name: Integration (PHP ${{ matrix.php }}, MySQL 8.4.10)
runs-on: ubuntu-24.04
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
php:
- '8.2'
- '8.5'
services:
mysql:
image: mysql:8.4.10
env:
MYSQL_DATABASE: maatify_persistence_test
MYSQL_USER: maatify_persistence
MYSQL_PASSWORD: persistence_ci_password
MYSQL_ROOT_PASSWORD: root_ci_password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -proot_ci_password"
--health-interval=10s
--health-timeout=5s
--health-retries=30
env:
PERSISTENCE_TEST_MYSQL_DSN: mysql:host=127.0.0.1;port=3306;dbname=maatify_persistence_test;charset=utf8mb4
PERSISTENCE_TEST_MYSQL_USER: maatify_persistence
PERSISTENCE_TEST_MYSQL_PASSWORD: persistence_ci_password
steps:
- name: Checkout
id: checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup PHP
id: setup-php
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php }}
extensions: pdo, pdo_mysql
coverage: none
tools: composer:2.10.2
- name: Print tool versions
shell: bash
run: |
set -euo pipefail
php -v
composer --version
- name: Validate and resolve dependencies
shell: bash
run: |
set -euo pipefail
composer validate --strict
composer update \
--no-interaction \
--prefer-dist \
--no-progress
composer check-platform-reqs
composer audit \
--no-interaction \
--abandoned=fail
- name: Configure MySQL trigger creation policy
shell: bash
run: |
set -euo pipefail
container_id='${{ job.services.mysql.id }}'
value="$(docker exec \
-e MYSQL_PWD=root_ci_password \
"$container_id" \
mysql \
--user=root \
--batch \
--skip-column-names \
--execute="
SET GLOBAL log_bin_trust_function_creators = 1;
SELECT @@GLOBAL.log_bin_trust_function_creators;
")"
if [[ "$value" != "1" ]]; then
echo "Expected @@GLOBAL.log_bin_trust_function_creators to be 1, got: $value"
exit 1
fi
- name: Run Integration and full suites
shell: bash
run: |
set -euo pipefail
vendor/bin/phpunit --testsuite integration
vendor/bin/phpunit --testsuite integration
vendor/bin/phpunit
- name: Verify MySQL residue
if: ${{ always() && !cancelled() && steps.setup-php.outcome == 'success' }}
shell: bash
run: |
set -euo pipefail
php <<'PHP'
<?php
declare(strict_types=1);
$dsn = getenv('PERSISTENCE_TEST_MYSQL_DSN');
$user = getenv('PERSISTENCE_TEST_MYSQL_USER');
$password = getenv('PERSISTENCE_TEST_MYSQL_PASSWORD');
if (!is_string($dsn) || $dsn === '' || !is_string($user) || $user === '' || !is_string($password)) {
fwrite(STDERR, "Missing MySQL test environment configuration.\n");
exit(1);
}
$pdo = new PDO($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$schema = (string) $pdo->query('SELECT DATABASE()')->fetchColumn();
$tables = [
'maa_persistence_test_global_ordering',
'maa_persistence_test_scoped_ordering',
'maa_persistence_test_pagination_items',
];
$triggers = [
'maa_persistence_test_fail_global_target_update',
'maa_persistence_test_fail_scoped_target_update',
];
$tableStatement = $pdo->prepare(
'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (?, ?, ?) ORDER BY TABLE_NAME'
);
$tableStatement->execute([$schema, ...$tables]);
$remainingTables = $tableStatement->fetchAll(PDO::FETCH_COLUMN);
$triggerStatement = $pdo->prepare(
'SELECT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME IN (?, ?) ORDER BY TRIGGER_NAME'
);
$triggerStatement->execute([$schema, ...$triggers]);
$remainingTriggers = $triggerStatement->fetchAll(PDO::FETCH_COLUMN);
if ($remainingTables !== [] || $remainingTriggers !== []) {
fwrite(STDERR, "MySQL residue detected.\n");
if ($remainingTables !== []) {
fwrite(STDERR, 'Remaining tables: ' . implode(', ', $remainingTables) . "\n");
}
if ($remainingTriggers !== []) {
fwrite(STDERR, 'Remaining triggers: ' . implode(', ', $remainingTriggers) . "\n");
}
exit(1);
}
echo "MySQL residue verification passed: no package test tables or triggers remain.\n";
PHP
- name: Repository integrity
if: ${{ always() && !cancelled() && steps.checkout.outcome == 'success' }}
shell: bash
run: |
set -euo pipefail
git diff --exit-code
ci-gate:
name: CI Gate
runs-on: ubuntu-24.04
timeout-minutes: 5
needs:
- workflow-lint
- quality
- unit-regression
- lowest-dependencies
- integration
if: ${{ always() }}
steps:
- name: Check required job results
shell: bash
env:
WORKFLOW_LINT_RESULT: ${{ needs.workflow-lint.result }}
QUALITY_RESULT: ${{ needs.quality.result }}
UNIT_REGRESSION_RESULT: ${{ needs.unit-regression.result }}
LOWEST_DEPENDENCIES_RESULT: ${{ needs.lowest-dependencies.result }}
INTEGRATION_RESULT: ${{ needs.integration.result }}
run: |
set -euo pipefail
declare -A results=(
[workflow-lint]="$WORKFLOW_LINT_RESULT"
[quality]="$QUALITY_RESULT"
[unit-regression]="$UNIT_REGRESSION_RESULT"
[lowest-dependencies]="$LOWEST_DEPENDENCIES_RESULT"
[integration]="$INTEGRATION_RESULT"
)
failed=0
for job in workflow-lint quality unit-regression lowest-dependencies integration; do
result="${results[$job]}"
echo "$job: $result"
if [[ "$result" != "success" ]]; then
failed=1
fi
done
if [[ "$failed" -ne 0 ]]; then
echo "CI Gate failed because at least one required job did not conclude with success."
exit 1
fi
echo "CI Gate passed: all required jobs concluded with success."
|