Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
21 changes: 21 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.git
.github
vendor
node_modules
.env
.env.local
secrets
*.env
*.md
.idea
.phpunit.cache
.phpunit.result.cache
.phpstan-cache
srv/tmp/**/*
srv/profile/*
logs/*
!**/.gitignore

# A restore source is a copy of a database, and the php stage does `COPY . /var/www/app`.
# Keep backups out of the build context entirely — the local stack bind-mounts them.
db/backup/*
49 changes: 49 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copy this to `.env` — not `.env.local`.
#
# cp .env.example .env
# docker compose run --rm php vendor/bin/gf env --init # appends the app's own variables
#
# One file, two halves. This file holds what DOCKER COMPOSE needs; `gf env --init`
# appends what CONFIG.JSON needs (APP_TYPE, MONGO_URI, …) to the same file, deriving
# them from config.json itself so the two cannot drift. --init only appends variables
# the file does not already declare, so it is safe to re-run after adding a config
# reference.
#
# Why one file rather than .env + .env.local: docker compose interpolates ${HTTP_PORT}
# and the CORS origins below from `.env` and from nothing else. Put them in .env.local
# and compose silently uses its defaults instead — no warning, no error. (The framework
# does read .env.local, and it wins over .env, so it remains a fine place for a personal
# override of an application variable.)
#
# Never commit .env.

# ---- Ports published on the host by the dev stack ----
HTTP_PORT=8080
MONGO_PORT=27017

# ---- PHP-FPM upstream, as nginx reaches it ----
PHP_FPM_HOST=php:9000

# ---- CORS allowlist. Keep all three distinct — nginx will not start with a
# duplicate map key, and only these exact origins are allowed. ----
CORS_ORIGIN_APP=http://localhost:8080
CORS_ORIGIN_FRONTEND=http://localhost:5173
CORS_ORIGIN_SWAGGER=http://localhost:8081

# ---- MONGO_URI, for the host ----
# gf env --init will append MONGO_URI blank. The value below is the one the HOST gf
# CLI needs; the php container gets its own from docker-compose.yml, because one
# variable cannot name both `localhost` and the compose service.
#
# directConnection=true is required from the host: the replica set advertises its
# member as "mongodb:27017", a name only the compose network resolves, so without it
# the driver discovers that member and then cannot reach it.
#
# MONGO_URI=mongodb://localhost:27017/?directConnection=true

# ---- Restoring the development database from db/backup/ (optional) ----
# `docker compose run --rm mongo-restore` restores db/backup/${MONGO_DATABASE} into the
# development database. Set this when the backup directory is named for a different
# database — the dump is restored into MONGO_DATABASE either way.
#
# MONGO_RESTORE_FROM=SomeOtherDatabase
159 changes: 84 additions & 75 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,98 +8,107 @@ on:
permissions:
contents: read

env:
# The prod image is php:8.4-fpm; composer.lock is resolved for that platform
# (see config.platform in composer.json). Keep these in step.
PHP_VERSION: '8.4'

jobs:
phpstan:
name: PHPStan (PHP ${{ matrix.php }})
name: PHPStan
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.3']
steps:
- uses: actions/checkout@v4
- name: Check out sibling repositories required by composer-ci.json
uses: actions/checkout@v4
with:
repository: gcgov/framework
ref: main
path: ../framework
- uses: actions/checkout@v4
with:
repository: gcgov/framework-service-gcgov-cron-monitor
ref: main
path: ../framework-service-gcgov-cron-monitor
- uses: actions/checkout@v4
with:
repository: gcgov/framework-service-documentation
ref: main
path: ../framework-service-documentation
- uses: actions/checkout@v4
with:
repository: gcgov/framework-service-auth-oauth-server
ref: main
path: ../framework-service-auth-oauth-server
- uses: actions/checkout@v4
with:
repository: gcgov/framework-service-user-crud
ref: main
path: ../framework-service-user-crud
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
php-version: ${{ env.PHP_VERSION }}
extensions: mongodb, sodium, fileinfo, pdo, imagick
tools: composer:v2
coverage: none
- name: Swap composer.json for CI variant
run: cp composer-ci.json composer.json
- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Run PHPStan
run: composer phpstan
# composer install treats a lock that disagrees with composer.json as a warning and
# carries on from the stale lock — so a drifted lock installs the wrong framework and
# surfaces later as confusing class-not-found errors. validate exits non-zero, here,
# naming the problem.
- run: composer validate --no-check-publish --no-check-all
# Installs from the committed composer.lock, which pins gcgov/framework to a
# published release.
- run: composer install --no-interaction --no-progress --prefer-dist
- run: composer phpstan

phpunit:
name: PHPUnit (PHP ${{ matrix.php }})
name: PHPUnit
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.3']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: gcgov/framework
ref: main
path: ../framework
- uses: actions/checkout@v4
with:
repository: gcgov/framework-service-gcgov-cron-monitor
ref: main
path: ../framework-service-gcgov-cron-monitor
- uses: actions/checkout@v4
with:
repository: gcgov/framework-service-documentation
ref: main
path: ../framework-service-documentation
- uses: actions/checkout@v4
with:
repository: gcgov/framework-service-auth-oauth-server
ref: main
path: ../framework-service-auth-oauth-server
- uses: actions/checkout@v4
with:
repository: gcgov/framework-service-user-crud
ref: main
path: ../framework-service-user-crud
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
php-version: ${{ env.PHP_VERSION }}
extensions: mongodb, sodium, fileinfo, pdo, imagick
tools: composer:v2
coverage: none
- name: Swap composer.json for CI variant
run: cp composer-ci.json composer.json
- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Run PHPUnit
run: composer test
- run: composer validate --no-check-publish --no-check-all
- run: composer install --no-interaction --no-progress --prefer-dist
- run: composer test

nginx:
name: Nginx config lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Render the template and validate nginx config
run: |
docker run --rm \
-e PHP_FPM_HOST=php:9000 \
-e CORS_ORIGIN_APP=http://localhost:8080 \
-e CORS_ORIGIN_FRONTEND=http://localhost:5173 \
-e CORS_ORIGIN_SWAGGER=http://localhost:8081 \
-e NGINX_ENVSUBST_FILTER='^(CORS_ORIGIN_|PHP_FPM_HOST)' \
-v "$PWD/docker/nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro" \
nginx:1.27-alpine \
sh -c 'set -e; /docker-entrypoint.sh nginx -t'

docker-build:
name: Docker build (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [php, nginx]
steps:
- uses: actions/checkout@v4
- name: Build
run: docker build --target ${{ matrix.target }} --build-arg APP_VERSION=ci -t app:${{ matrix.target }} .

- name: php — the committed config.json is in the image, secrets are not
if: matrix.target == 'php'
run: |
set -eux
docker run --rm --entrypoint test app:php -f config.json
# JWT keys and .env are gitignored, so they must not have reached the build
# context. If either appears, authentication material is baked into a layer.
! docker run --rm --entrypoint test app:php -e .env
! docker run --rm --entrypoint test app:php -d srv/jwtCertificates

- name: php — runs as a non-root user with a valid FPM config
if: matrix.target == 'php'
run: |
set -eux
test "$(docker run --rm --entrypoint id app:php -un)" = www-data
# -t exits non-zero on a bad pool config, and prints the warning we removed
# www.conf to avoid; fail if it comes back.
docker run --rm --entrypoint php-fpm app:php -t 2>&1 | tee /tmp/fpm.log
! grep -qi "when FPM is not running as root" /tmp/fpm.log

- name: php — opcache and the upload limits the nginx config assumes
if: matrix.target == 'php'
run: |
set -eux
docker run --rm --entrypoint php app:php -r 'exit(ini_get("opcache.enable") ? 0 : 1);'
docker run --rm --entrypoint php app:php -r 'exit(ini_get("display_errors") ? 1 : 0);'
# Must be >= nginx client_max_body_size (1024m), or a large upload is accepted
# by nginx and then rejected by PHP after crossing the wire.
docker run --rm --entrypoint php app:php -r 'exit(ini_get("post_max_size") === "1024M" ? 0 : 1);'

- name: nginx — serves the application's static assets
if: matrix.target == 'nginx'
run: docker run --rm --entrypoint test app:nginx -f /var/www/app/www/index.php
38 changes: 38 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Build a Release and ask gcgov/deploy to deploy it.
#
# push to main build and publish only
# push of v* build, publish, and request deployment
#
# This file is deliberately thin. The real logic lives in gcgov/deploy, so changing how
# deployment works is one pull request rather than one per application.
#
# What it calls only ever runs on GitHub-hosted runners. Deployment itself happens in
# gcgov/deploy's own context, reached by repository_dispatch — a called workflow executes
# in the CALLER's context, so if the deploy step lived here, every contributor to this
# repository would have code execution inside a network Zone.

name: Release

on:
push:
branches: [main]
tags: ['v*']

permissions:
contents: read
packages: write

jobs:
release:
uses: gcgov/deploy/.github/workflows/build.yml@main
with:
# Everything derives from this: image names, ops repo paths, database users.
app: framework-app-template
# Which Zone this application runs in: internal | bridge | isolated.
zone: bridge
# Only a tag deploys. A push to main publishes an image and stops there.
deploy: ${{ startsWith(github.ref, 'refs/tags/v') }}
secrets:
# Fine-grained token with contents:write on gcgov/deploy and nothing else. Used
# solely to fire the deploy dispatch; without it, images publish and nothing ships.
OPS_DISPATCH_TOKEN: ${{ secrets.OPS_DISPATCH_TOKEN }}
10 changes: 6 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
composer.phar
/vendor/
composer.lock
composer.json
/.idea
www/web.config
version.json
app/config/environment.json

# Local secrets — never commit real values (see DOCKER.md)
.env
.env.local
/secrets/

.phpunit.cache/
.phpunit.result.cache
.phpstan-cache/
Loading
Loading