Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
701736c
add parameterized docker environment under resources/
niden Jul 15, 2026
84f2c1b
move entry point to public/ per pds/skeleton
niden Jul 15, 2026
3e2a010
consolidate compose into single parameterized stack
niden Jul 15, 2026
aebf7eb
remove superseded 8.0/8.1 docker stack
niden Jul 15, 2026
b808d43
strip composer.json to bare, raise php floor to 8.1
niden Jul 15, 2026
5f3ac12
add runtime dependencies: phpdotenv, fractal, phinx
niden Jul 15, 2026
6626127
add dev tools: phpstan, codesniffer, ide-stubs, pds packages
niden Jul 15, 2026
8824c95
remove psalm and codeception configs
niden Jul 15, 2026
1f623fd
add phalcon/phalcon v6, phpstan config and composer scripts
niden Jul 15, 2026
e217556
add talon test runner
niden Jul 15, 2026
64b66b0
use dirname(__FILE__) for path resolution
niden Jul 15, 2026
b394d2b
add talon scaffolding: bootstrap, env and per-suite phpunit configs
niden Jul 15, 2026
5237807
convert unit suite to talon format
niden Jul 15, 2026
d77f54f
[#42] - upgrade talon to v0.8.0 for rest test support
niden Jul 15, 2026
8a002e1
[#42] - convert api suite to talon format
niden Jul 15, 2026
2a02e3f
[#42] - cast cache lifetime to int
niden Jul 15, 2026
c7ff658
[#42] - validate the submitted token against the expected claims
niden Jul 16, 2026
d610063
[#42] - pin phalcon to the v6.0.0 branch
niden Jul 16, 2026
6a2c9b0
[#42] - cast resource ids to string in api expectations
niden Jul 16, 2026
2d0bcaa
[#42] - convert cli suite to talon format
niden Jul 16, 2026
4383cbb
[#42] - remove codeception suites, support classes and configs
niden Jul 16, 2026
1ffaa2f
[#42] - map talon suites to a single phpunit config
niden Jul 16, 2026
95cb9c7
[#42] - add php-cs-fixer config and package
niden Jul 16, 2026
90d050c
[#42] - reorganize source under src/ per pds/skeleton
niden Jul 16, 2026
5e7b64c
[#42] - apply phpcs and php-cs-fixer fixes
niden Jul 16, 2026
9361cf0
[#42] - update stale test expectations for app url and log levels
niden Jul 16, 2026
419ba99
[#42] - merge the token middleware into one authentication middleware
niden Jul 16, 2026
7801b73
[#42] - replace the query trait with an injected query service and us…
niden Jul 16, 2026
0b0f117
[#42] - let each model declare the fields it publishes
niden Jul 16, 2026
4687015
[#42] - hash user passwords instead of storing them in clear
niden Jul 16, 2026
1b1c942
[#42] - address code review findings
niden Jul 16, 2026
e30f97b
[#42] - consolidate duplicated logic in config, transformers and tests
niden Jul 16, 2026
92aea91
[#42] - replace the stale workflow with quality, tests and coverage jobs
niden Jul 16, 2026
8c98e9e
[#42] - wait for the application to answer before running the tests
niden Jul 16, 2026
428b27b
[#42] - let dotenv supply the application database host in ci
niden Jul 16, 2026
cb1ea26
[#42] - read the real environment before dotenv in envValue
niden Jul 16, 2026
e7cd1fc
[#42] - add phql and register composer before the phalcon loader
niden Jul 16, 2026
93ae556
[#42] - resolve octocov paths from the resources directory
niden Jul 16, 2026
00cc627
[#42] - address sonarqube findings on the new code
niden Jul 16, 2026
9b1b089
[#42] - cover the transformers and the user token; sanitize on read only
niden Jul 16, 2026
0a47c65
[#42] - type the transformers, controllers and response for static an…
niden Jul 16, 2026
f7449db
[#42] - clear static analysis at level 7 and gate it in ci
niden Jul 16, 2026
459e068
[#42] - adding coc
niden Jul 16, 2026
6647b11
[#42] - raise static analysis to level 8
niden Jul 16, 2026
9b6953d
[#42] - updating composer
niden Jul 16, 2026
75acc7c
[#42] - make integration tests idempotent via db truncation and cache…
niden Jul 16, 2026
886f2c4
[#42] - add infection config and harden tests to kill mutants
niden Jul 16, 2026
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 .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"

# Keep the SHA-pinned GitHub Actions current. Dependabot opens reviewable
# pull requests that bump both the commit SHA and the trailing "# version"
# comment; it never merges on its own.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "docker"
directory: "/resources/docker"
schedule:
interval: "weekly"
331 changes: 269 additions & 62 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,96 +1,303 @@
name: Testing Suite
name: "REST API CI"

on: [push, pull_request]
on:
push:
paths-ignore:
- '**.md'
- '**.txt'
pull_request:
workflow_dispatch:

env:
DATA_API_MYSQL_HOST: '127.0.0.1'
DATA_API_REDIS_HOST: '127.0.0.1'
# Phalcon v5 C extension constraint (installed from source via PIE)
PHALCON_CONSTRAINT: "^5.0"
EXTENSIONS: "mbstring, intl, json, openssl, redis, pdo_mysql"
# The application reads DATA_API_*; Talon reads DATA_* for its own settings.
# Both resolve getenv() before $_ENV, so these beat the files that point at
# the compose service names (.env for the app, tests/.env.test for Talon).
DATA_API_MYSQL_HOST: "127.0.0.1"
DATA_API_REDIS_HOST: "127.0.0.1"
DATA_MYSQL_HOST: "127.0.0.1"
DATA_REDIS_HOST: "127.0.0.1"
# The api suite crosses a real HTTP boundary; this is the built-in server
# started below. APP_URL stays http://localhost:8080 - it is what the app
# echoes back in `links`, and the expectations must match the app.
TALON_REST_URL: "http://127.0.0.1:8080"

permissions: { }

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

jobs:
run:
quality:
name: "Code quality"
permissions:
contents: read
runs-on: ubuntu-latest
name: Workflow - PHP-${{ matrix.php }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 1

- name: "Setup PHP"
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.3'
extensions: ${{ env.EXTENSIONS }}
tools: composer:v2

# The quality tools do not need the Phalcon extension: phpstan analyses the
# phalcon/phalcon (v6) source pulled in by Composer.
- name: "Validate composer"
run: composer validate --no-check-all --no-check-publish

- name: "Install dependencies"
uses: ramsey/composer-install@26d8a556604053a9612623447203a691f406fbe6 # v4
with:
composer-options: "--prefer-dist"

- name: "PHPCS"
run: composer cs

- name: "PHP CS Fixer (dry-run)"
run: composer cs-fixer

- name: "PHPStan"
run: composer analyze

tests:
name: "Tests (PHP ${{ matrix.php }}, Phalcon ${{ matrix.variant }})"
permissions:
contents: read
runs-on: ubuntu-latest
needs:
- quality
strategy:
fail-fast: false
matrix:
php:
- '8.1'
- '8.2'
- '8.3'
- '8.4'
variant:
# v5 = cphalcon C extension; v6 = phalcon/phalcon composer package
- 'v5'
- 'v6'
services:
mysql:
image: mysql:5.7
image: mysql:8.0
ports:
- "3306:3306"
env:
MYSQL_ROOT_PASSWORD: secret
MYSQL_USER: phalcon
MYSQL_DATABASE: phalcon_api
MYSQL_PASSWORD: secret
options: >-
--health-cmd "mysqladmin ping --silent"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:5-alpine
image: redis:alpine
ports:
- "6379:6379"

strategy:
fail-fast: false
matrix:
php: ['8.0', '8.1' ]

options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v1
- name: Setup PHP
uses: shivammathur/setup-php@v2
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 1

- name: "Setup PHP"
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: ${{ matrix.php }}
tools: pecl
extensions: mbstring, intl, json, phalcon-5.0.0RC4
coverage: xdebug
extensions: ${{ env.EXTENSIONS }}
tools: composer:v2

# v5 = cphalcon C extension, built from source via PIE (pecl caps at PHP 8.4).
# v6 needs no extension: it runs on the phalcon/phalcon composer package (a require-dev dep).
- name: "Install Phalcon v5 (cphalcon) via PIE"
if: matrix.variant == 'v5'
run: |
curl -fsSL https://github.com/php/pie/releases/latest/download/pie.phar -o pie.phar
sudo "$(which php)" pie.phar install --no-interaction "phalcon/cphalcon:${{ env.PHALCON_CONSTRAINT }}"
php -m | grep -i phalcon

- name: "Install dependencies"
uses: ramsey/composer-install@26d8a556604053a9612623447203a691f406fbe6 # v4
with:
composer-options: "--prefer-dist"

- name: "Prepare environment"
run: |
cp resources/.env.example .env
sed -i 's/^DATA_API_MYSQL_HOST=.*/DATA_API_MYSQL_HOST=127.0.0.1/' .env
sed -i 's/^DATA_API_REDIS_HOST=.*/DATA_API_REDIS_HOST=127.0.0.1/' .env
# storage/logs is not tracked (only the cache dirs carry .gitignore
# placeholders), so a fresh checkout has nowhere to write the log.
mkdir -p storage/logs storage/cache/data storage/cache/metadata tests/_output

- name: Init Database
- name: "Migrate"
run: composer migrate

# The api suite talks to this over the network. .htrouter.php is a router
# script: it serves anything that exists under public/ and hands the rest
# to public/index.php.
- name: "Start the application"
run: |
mysql -uroot -h127.0.0.1 -psecret -e 'CREATE DATABASE IF NOT EXISTS `phalcon_api`;'
php -S 127.0.0.1:8080 .htrouter.php > storage/logs/server.log 2>&1 &
# Any HTTP status means the application answered - /sommething is
# expected to 404. curl reports "000" when it could not connect at
# all, which is the only case worth waiting on.
for i in $(seq 1 30); do
code=$(curl -sS -o /dev/null -w '%{http_code}' "${TALON_REST_URL}/sommething" 2>/dev/null || true)
if [ "$code" != "000" ]; then
echo "application answered with HTTP ${code} after ${i}s"
exit 0
fi
sleep 1
done
echo "the application did not answer within 30s"
cat storage/logs/server.log || true
exit 1

- name: Validate composer.json and composer.lock
run: composer validate
- name: "Tests"
run: vendor/bin/talon run all

- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: "Server log"
if: failure()
run: cat storage/logs/server.log || true

- name: Cache dependencies
uses: actions/cache@v2
coverage:
name: "Coverage"
permissions:
contents: read
pull-requests: write
actions: read
runs-on: ubuntu-latest
needs:
- quality
services:
mysql:
image: mysql:8.0
ports:
- "3306:3306"
env:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: phalcon_api
options: >-
--health-cmd "mysqladmin ping --silent"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:alpine
ports:
- "6379:6379"
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ matrix.php }}-composer-
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# SonarQube needs full history for blame/new-code detection;
# octocov uses it for the base-branch diff.
fetch-depth: 0

- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: "Setup PHP"
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.3'
extensions: ${{ env.EXTENSIONS }}
coverage: pcov
tools: composer:v2

- name: Run PHPCS
if: always()
run: vendor/bin/phpcs
# pecl's phalcon release metadata caps at PHP 8.4; PIE builds it from source.
- name: "Install Phalcon (v5) via PIE"
run: |
curl -fsSL https://github.com/php/pie/releases/latest/download/pie.phar -o pie.phar
sudo "$(which php)" pie.phar install --no-interaction "phalcon/cphalcon:${{ env.PHALCON_CONSTRAINT }}"
php -m | grep -i phalcon

- name: Env file
if: always()
run: cp -v ./storage/config/.env.ci ./.env
- name: "Install dependencies"
uses: ramsey/composer-install@26d8a556604053a9612623447203a691f406fbe6 # v4
with:
composer-options: "--prefer-dist"

- name: Run migrations
if: always()
- name: "Prepare environment"
run: |
vendor/bin/phinx migrate
cp resources/.env.example .env
sed -i 's/^DATA_API_MYSQL_HOST=.*/DATA_API_MYSQL_HOST=127.0.0.1/' .env
sed -i 's/^DATA_API_REDIS_HOST=.*/DATA_API_REDIS_HOST=127.0.0.1/' .env
mkdir -p storage/logs storage/cache/data storage/cache/metadata tests/_output

- name: Run tests
if: always()
- name: "Migrate"
run: composer migrate

- name: "Start the application"
run: |
sudo php -S 0.0.0.0 -t ./.htrouter.php &
vendor/bin/codecept build
vendor/bin/codecept run unit --coverage-xml=unit-coverage.xml
vendor/bin/codecept run integration --coverage-xml=integration-coverage.xml
vendor/bin/codecept run cli --coverage-xml=cli-coverage.xml
# vendor/bin/codecept run api --coverage-xml=api-coverage.xml

- name: Upload to codecov
uses: codecov/codecov-action@v3
php -S 127.0.0.1:8080 .htrouter.php > storage/logs/server.log 2>&1 &
for i in $(seq 1 30); do
code=$(curl -sS -o /dev/null -w '%{http_code}' "${TALON_REST_URL}/sommething" 2>/dev/null || true)
if [ "$code" != "000" ]; then
echo "application answered with HTTP ${code} after ${i}s"
exit 0
fi
sleep 1
done
echo "the application did not answer within 30s"
cat storage/logs/server.log || true
exit 1

# Runs phpunit over resources/phpunit.xml.dist directly rather than
# through talon: `talon run all` starts one process per suite, and each
# would overwrite the others' coverage.xml. One process, all four suites,
# one merged report.
#
# NOTE: the api suite exercises the application in the php -S process
# above, which pcov does not instrument - so the controllers read as 0%
# covered here despite being tested. See resources/octocov.yml.
- name: "Tests with coverage"
run: composer test-coverage

# SonarQube first (push only) so a failing octocov gate can't skip the upload.
- name: "SonarQube Scan"
if: github.event_name == 'push'
uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.organization=phalcon
-Dsonar.projectKey=phalcon_rest-api
-Dsonar.sources=src
-Dsonar.tests=tests
-Dsonar.exclusions=resources/**
-Dsonar.php.coverage.reportPaths=tests/_output/coverage.xml

# octocov last: PR comment + coverage gate (strict on PRs via
# octocov.pr.yml, floor only on pushes via octocov.yml), stores the
# baseline report on the default branch, and writes the badge SVG.
- name: "octocov"
uses: k1LoW/octocov-action@b3b6ee60482a667950f87553abf1df63217235d9 # v1.5.1
with:
config: ${{ github.event_name == 'pull_request' && 'resources/octocov.pr.yml' || 'resources/octocov.yml' }}

- name: "Upload coverage badge"
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
directory: ./tests/_output/
files: unit-coverage.xml,integration-coverage.xml,cli-coverage.xml
name: codecov-umbrella # optional
fail_ci_if_error: false
verbose: true # optional (default = false)
name: coverage-badge
path: tests/_output/coverage.svg
if-no-files-found: ignore
4 changes: 2 additions & 2 deletions .htrouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);

if ($uri !== '/' && file_exists(__DIR__ . '/public' . $uri)) {
if ($uri !== '/' && file_exists(dirname(__FILE__) . '/public' . $uri)) {
return false;
}

$_GET['_url'] = $_SERVER['REQUEST_URI'];

require_once __DIR__ . '/api/public/index.php';
require_once dirname(__FILE__) . '/public/index.php';
Loading