diff --git a/.docker/nginx.conf b/.docker/nginx.conf deleted file mode 100644 index 8fe03dbc7..000000000 --- a/.docker/nginx.conf +++ /dev/null @@ -1,36 +0,0 @@ -worker_processes auto; - -error_log /dev/stderr notice; -pid /tmp/nginx.pid; - -events { - worker_connections 1024; -} - -http { - proxy_temp_path /tmp/proxy_temp; - client_body_temp_path /tmp/client_temp; - fastcgi_temp_path /tmp/fastcgi_temp; - uwsgi_temp_path /tmp/uwsgi_temp; - scgi_temp_path /tmp/scgi_temp; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - set_real_ip_from 172.16.0.0/16; - real_ip_recursive on; - real_ip_header X-Forwarded-For; - - log_format main '$http_x_real_ip - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /dev/stdout main; - - sendfile on; - keepalive_timeout 65; - - gzip on; - - include /etc/nginx/conf.d/*.conf; -} diff --git a/.docker/templates/default.conf.template b/.docker/templates/default.conf.template deleted file mode 100644 index 8f789dca5..000000000 --- a/.docker/templates/default.conf.template +++ /dev/null @@ -1,52 +0,0 @@ -server { - listen ${NGINX_PORT}; - server_name localhost; - - root ${NGINX_WEB_ROOT}; - - client_max_body_size ${NGINX_MAX_BODY_SIZE}; - - # This also needs to be set in the single server tag and not only in http. - set_real_ip_from 172.16.0.0/16; - real_ip_recursive on; - real_ip_header X-Forwarded-For; - - location / { - # try to serve file directly, fallback to index.php - try_files $uri /index.php$is_args$args; - } - - # Screen client online check should just serve static files - location /client/online-check { - index index.html index.htm; - } - - # Protect files and directories from prying eyes. - location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|.tar|.gz|.bz2|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ { - deny all; - return 404; - } - - location ~ ^/index\.php(/|$) { - fastcgi_buffers 16 32k; - fastcgi_buffer_size 64k; - fastcgi_busy_buffers_size 64k; - - fastcgi_pass ${NGINX_FPM_SERVICE}; - fastcgi_split_path_info ^(.+\.php)(/.*)$; - include fastcgi_params; - - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $realpath_root; - - internal; - } - - location ~ \.php$ { - return 404; - } - - # Send log message to files symlinked to stdout/stderr. - error_log /dev/stderr; - access_log /dev/stdout main; -} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..7cd4dd2be --- /dev/null +++ b/.dockerignore @@ -0,0 +1,73 @@ +/.git +!/.git/config + +# Unneeded application files +/.docker +/.github +/docs +/fixtures +/infrastructure +/public/build +/public/fixtures +/public/media/* +!public/media/thumbnail_other.png +!public/media/thumbnail_video.png +/scripts +/tests + +# IDE / editor state +/.idea +/.vscode + +# Compose / task runner / dev tooling configs (not consumed at runtime) +/docker-compose*.yml +/Taskfile.yml +/playwright.config.ts + +# CI / quality-tool configs (not consumed at runtime) +/phpstan.dist.neon +/phpunit.xml.dist +/psalm.xml +/psalm-baseline.xml +/rector.php + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.php +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### + +###> lexik/jwt-authentication-bundle ### +/config/jwt/*.pem +###< lexik/jwt-authentication-bundle ### + +xdebug.ini +launch.json + +###> pentatrion/vite-bundle ### +/node_modules/ +###< pentatrion/vite-bundle ### + +#> Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +#< Playwright + +###> vincentlanglet/twig-cs-fixer ### +/.twig-cs-fixer.cache +###< vincentlanglet/twig-cs-fixer ### diff --git a/.env b/.env index 8b78daee5..b386b81ae 100644 --- a/.env +++ b/.env @@ -1,5 +1,8 @@ +# Docker Compose project name (used as a prefix for container names in local dev). COMPOSE_PROJECT_NAME=display +# Hostname used by the local dev stack's reverse proxy. COMPOSE_DOMAIN=display.local.itkdev.dk +# itkdev-docker template identifier. ITKDEV_TEMPLATE=symfony-6 # In all environments, the following files are loaded if they exist, @@ -18,12 +21,16 @@ ITKDEV_TEMPLATE=symfony-6 # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration ###> symfony/framework-bundle ### +# Application environment (prod, dev, test). Override to 'prod' in .env.local on production hosts. APP_ENV=dev -APP_SECRET=22d8a60c047b96413b3337e3ddae3da9 +# Secret key used for encryption and security-related operations (MUST BE CHANGED in .env.local for production). +APP_SECRET=CHANGE_ME +# Comma-separated list of trusted proxy IPs for handling X-Forwarded headers. TRUSTED_PROXIES=127.0.0.1,REMOTE_ADDR ###< symfony/framework-bundle ### ###> doctrine/doctrine-bundle ### +# Database connection string for MariaDB/MySQL. # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml # @@ -33,106 +40,157 @@ DATABASE_URL="mysql://db:db@mariadb:3306/db?serverVersion=10.11.5-MariaDB" ###< doctrine/doctrine-bundle ### ###> nelmio/cors-bundle ### -CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' +# Regular expression defining allowed origins for CORS requests. # Add allowed urls to .env.local as regex. # For development use: # CORS_ALLOW_ORIGIN="^.*" +CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###< nelmio/cors-bundle ### ###> lexik/jwt-authentication-bundle ### +# Path to JWT private key file for signing tokens. JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +# Path to JWT public key file for verifying tokens. JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem -JWT_PASSPHRASE=APP_JWT_PASSPHRASE -# Default: 1 hour +# Passphrase for JWT private key (MUST BE CHANGED in .env.local for production). +JWT_PASSPHRASE=CHANGE_ME +# Time-to-live in seconds for JWT access tokens. Default: 3600 (1 hour). JWT_TOKEN_TTL=3600 -# Default: 15 days +# Time-to-live in seconds for screen JWT tokens. Default: 1296000 (15 days). JWT_SCREEN_TOKEN_TTL=1296000 ###< lexik/jwt-authentication-bundle ### ###> gesdinet/jwt-refresh-token-bundle ### -# Default: 2 hours +# Time-to-live in seconds for JWT refresh tokens. Default: 7200 (2 hours). JWT_REFRESH_TOKEN_TTL=7200 -# Default: 30 days +# Time-to-live in seconds for screen JWT refresh tokens. Default: 2592000 (30 days). JWT_SCREEN_REFRESH_TOKEN_TTL=2592000 ###< gesdinet/jwt-refresh-token-bundle ### ###> redis ### +# Prefix for Redis cache keys to avoid collisions across applications sharing a Redis instance. REDIS_CACHE_PREFIX=DisplayApiService +# Connection string for Redis cache server. REDIS_CACHE_DSN=redis://redis:6379/0 ###< redis ### ###> Http Client ### +# Maximum time in seconds the HTTP client waits for a connection. HTTP_CLIENT_TIMEOUT=5 +# Maximum total duration in seconds for an HTTP request including transfer. HTTP_CLIENT_MAX_DURATION=30 +# Log level for outgoing HTTP requests (debug, info, notice, warning, error). HTTP_CLIENT_LOG_LEVEL=error ###< Http Client ### ###> App ### +# Default date format for API responses (ISO 8601 with milliseconds). DEFAULT_DATE_FORMAT='Y-m-d\TH:i:s.v\Z' +# Time interval for activation code expiration (ISO 8601 duration format, P2D = 2 days). ACTIVATION_CODE_EXPIRE_INTERVAL=P2D +# Source for key vault configuration (ENVIRONMENT, FILE, etc.). KEY_VAULT_SOURCE=ENVIRONMENT +# JSON configuration for key vault when using ENVIRONMENT source. KEY_VAULT_JSON="{}" +# Enable tracking of screen information and statistics. TRACK_SCREEN_INFO=false +# Interval in seconds between screen info updates. TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS=300 +# Toggle the relations checksum optimisation for content updates. RELATIONS_CHECKSUM_ENABLED=true ###< App ### ###> itk-dev/openid-connect-bundle ### # See docs/feed/openid-connect.md for variable explanations. -# internal provider +# internal provider (admin login) +# URL to OIDC provider's metadata/discovery endpoint. INTERNAL_OIDC_METADATA_URL=INTERNAL_OIDC_METADATA_URL +# Client ID registered with the OIDC provider. INTERNAL_OIDC_CLIENT_ID=INTERNAL_OIDC_CLIENT_ID +# Client secret for authentication with the OIDC provider. INTERNAL_OIDC_CLIENT_SECRET=INTERNAL_OIDC_CLIENT_SECRET +# Redirect URI for OIDC callback after authentication. INTERNAL_OIDC_REDIRECT_URI=INTERNAL_OIDC_REDIRECT_URI +# Leeway time in seconds for token validation clock skew tolerance. INTERNAL_OIDC_LEEWAY=30 +# Name of the claim containing user's name. INTERNAL_OIDC_CLAIM_NAME=navn +# Name of the claim containing user's email. INTERNAL_OIDC_CLAIM_EMAIL=email +# Name of the claim containing user's group memberships. INTERNAL_OIDC_CLAIM_GROUPS=groups -# external provider +# external provider (citizen login) +# URL to external OIDC provider's metadata/discovery endpoint. EXTERNAL_OIDC_METADATA_URL=EXTERNAL_OIDC_METADATA_URL +# Client ID registered with the external OIDC provider. EXTERNAL_OIDC_CLIENT_ID=EXTERNAL_OIDC_CLIENT_ID +# Client secret for authentication with the external OIDC provider. EXTERNAL_OIDC_CLIENT_SECRET=EXTERNAL_OIDC_CLIENT_SECRET +# Redirect URI for external OIDC callback after authentication. EXTERNAL_OIDC_REDIRECT_URI=EXTERNAL_OIDC_REDIRECT_URI +# Leeway time in seconds for external token validation clock skew tolerance. EXTERNAL_OIDC_LEEWAY=30 +# Salt used for hashing external OIDC user identifiers. EXTERNAL_OIDC_HASH_SALT= +# Name of the claim containing user's sign-in identifier. EXTERNAL_OIDC_CLAIM_ID=signinname -# cli redirect url +# Redirect URI for CLI-based OIDC authentication flow. OIDC_CLI_REDIRECT=APP_CLI_REDIRECT_URI ###< itk-dev/openid-connect-bundle ### ###> Calendar Api Feed Source ### # See docs/feed/calendar-api-feed.md for variable explanations. +# API endpoint for calendar location data. CALENDAR_API_FEED_SOURCE_LOCATION_ENDPOINT= +# API endpoint for calendar resource data. CALENDAR_API_FEED_SOURCE_RESOURCE_ENDPOINT= +# API endpoint for calendar event data. CALENDAR_API_FEED_SOURCE_EVENT_ENDPOINT= +# JSON object for custom field mappings in calendar feeds. CALENDAR_API_FEED_SOURCE_CUSTOM_MAPPINGS='{}' +# JSON object for modifying calendar event data. CALENDAR_API_FEED_SOURCE_EVENT_MODIFIERS='{}' +# Date format used by the calendar API source. CALENDAR_API_FEED_SOURCE_DATE_FORMAT= +# Timezone used by the calendar API source. CALENDAR_API_FEED_SOURCE_DATE_TIMEZONE= +# Cache expiration time in seconds for calendar feed data. CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_SECONDS=300 ###< Calendar Api Feed Source ### ###> Event Database Api V2 Feed Type ### +# Cache expiration time in seconds for event database API v2 responses. EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS=300 ###< Event Database Api V2 Feed Type ### ###> Admin configuration ### +# API key for Rejseplanen (Danish journey planner) integration. ADMIN_REJSEPLANEN_APIKEY= +# Enable display of screen online/offline status in admin interface. ADMIN_SHOW_SCREEN_STATUS=false +# Enable touch button regions feature in admin interface. ADMIN_TOUCH_BUTTON_REGIONS=false +# JSON array defining available login methods for admin interface. ADMIN_LOGIN_METHODS='[{"type":"username-password","enabled":true,"provider":"username-password","label":""}]' +# Enable enhanced preview mode with additional features. ADMIN_ENHANCED_PREVIEW=false ###< Admin configuration ### ###> Client configuration ### +# Timeout in milliseconds for checking login status. CLIENT_LOGIN_CHECK_TIMEOUT=20000 +# Timeout in milliseconds before attempting to refresh authentication token. 300000 = 5 minutes. CLIENT_REFRESH_TOKEN_TIMEOUT=300000 +# Interval in milliseconds for checking release timestamp updates. 600000 = 10 minutes. CLIENT_RELEASE_TIMESTAMP_INTERVAL_TIMEOUT=600000 +# Interval in milliseconds for running content scheduling checks. 60000 = 1 minute. CLIENT_SCHEDULING_INTERVAL=60000 # Interval in milliseconds for pulling new content from server. 600000 = 10 minutes. CLIENT_PULL_STRATEGY_INTERVAL=600000 +# JSON configuration for client color scheme with location coordinates. CLIENT_COLOR_SCHEME='{"type":"library","lat":56.0,"lng":10.0}' +# Enable debug mode for client-side logging and diagnostics. CLIENT_DEBUG=false ###< Client configuration ### diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..af4e6c73d --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,9 @@ +# Revisions to skip when running `git blame`. +# +# Configure once locally: +# git config blame.ignoreRevsFile .git-blame-ignore-revs +# +# Each line is a full commit SHA followed by a comment. + +# 7329: Applied coding standards (bulk style commit during psalm→phpstan migration) +6a91050b2339fe7c1b5080f9cdb3219fe1adc739 diff --git a/.github/workflows/apispec.yaml b/.github/workflows/apispec.yaml index 970d99d25..94855d683 100644 --- a/.github/workflows/apispec.yaml +++ b/.github/workflows/apispec.yaml @@ -1,50 +1,234 @@ -on: pull_request +### ### Api Spec +### +### Re-exports the OpenAPI spec (`public/api-spec-v2.{yaml,json}`) from the +### API Platform configuration. Posts a PR comment **only** when something +### needs attention — spec drift, non-breaking changes, or breaking changes — +### and stays quiet otherwise. The comment is edited in place across runs +### so a PR carries at most one comment from this workflow. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `bin/console` +### can be run inside the `phpfpm` service. +### 2. The committed `public/api-spec-v2.{yaml,json}` are kept in sync with +### the API Platform configuration via `composer update-api-spec`. + +on: + pull_request: + paths: + - "src/**/*.php" + - "config/**" + - "composer.json" + - "composer.lock" + - "public/api-spec-v2.yaml" + - "public/api-spec-v2.json" + - "docker-compose.yml" name: Api Spec +env: + COMPOSE_USER: runner + jobs: - apispec: + api-spec-export: + name: Ensure API specification is up to date runs-on: ubuntu-latest - name: API Specification validation - strategy: - fail-fast: false - matrix: - php: ["8.4"] + permissions: + contents: read + pull-requests: write steps: - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 2 + uses: actions/checkout@v6 - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 + - name: Cache vendor + uses: actions/cache@v5 with: - php-version: ${{ matrix.php}} - extensions: apcu, ctype, iconv, imagick, json, redis, soap, xmlreader, zip - coverage: none + path: vendor + key: vendor-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-php8.4- + + - name: Setup network + run: docker network create frontend + + - name: Install Dependencies + run: docker compose run --rm phpfpm composer install + + - name: Export specifications + run: | + docker compose run --rm phpfpm bin/console api:openapi:export --yaml --output=public/api-spec-v2.yaml --no-interaction + docker compose run --rm phpfpm bin/console api:openapi:export --output=public/api-spec-v2.json --no-interaction + + - name: Check for uncommitted changes + id: git-diff-spec + continue-on-error: true + run: git diff --diff-filter=ACMRT --exit-code public/api-spec-v2.yaml public/api-spec-v2.json + + - name: Comment PR if spec is outdated + if: steps.git-diff-spec.outcome == 'failure' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr comment ${{ github.event.pull_request.number }} \ + --body "$(cat <<'EOF' + ## API specification not up to date + + The committed API specification files do not match the exported output. + + Please run the following command, then commit and push the changes: - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + ```shell + docker compose exec phpfpm composer update-api-spec + ``` + EOF + )" \ + --create-if-none --edit-last - - name: Cache composer dependencies - uses: actions/cache@v4 + - name: Fail if spec is outdated + if: steps.git-diff-spec.outcome == 'failure' + run: exit 1 + + api-spec-breaking-changes: + name: Detect breaking changes in API specification + runs-on: ubuntu-latest + needs: [api-spec-export] + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Fetch base branch for comparison + run: git fetch --depth=1 origin ${{ github.base_ref }} + + # API Platform 3.4 emits empty header maps as YAML sequences + # (`headers: []`) — out of spec per OpenAPI 3.x, which mandates a map. + # oasdiff is strict and refuses to load either side, so we materialise + # both specs and rewrite to the canonical empty-map form before + # diffing. Files live inside the workspace so the oasdiff-action's + # docker container (which mounts the repo as /github/workspace) can + # read them. + # TODO: drop this sanitisation once we upgrade to API Platform 4.x — + # the exporter there serialises empty maps correctly. Track upgrade + # in https://github.com/os2display/display-api-service/issues (open + # an issue when the upgrade is scheduled). + - name: "Sanitise specs (`headers: []` → `headers: {}`)" + run: | + mkdir -p .api-spec-tmp + git show "origin/${{ github.base_ref }}:public/api-spec-v2.yaml" \ + | sed 's/headers: \[\]/headers: {}/g' > .api-spec-tmp/base.yaml + sed 's/headers: \[\]/headers: {}/g' public/api-spec-v2.yaml \ + > .api-spec-tmp/revision.yaml + + - name: Detect breaking changes + id: breaking + continue-on-error: true + uses: oasdiff/oasdiff-action/breaking@v0.0.44 with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ matrix.php }}-composer- + base: .api-spec-tmp/base.yaml + revision: .api-spec-tmp/revision.yaml + fail-on: ERR - - name: Install Dependencies - run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist + - name: Generate changelog + id: changelog + continue-on-error: true + uses: oasdiff/oasdiff-action/changelog@v0.0.44 + with: + base: .api-spec-tmp/base.yaml + revision: .api-spec-tmp/revision.yaml + format: markdown + output-to-file: changelog.md + + # oasdiff's changelog action writes a "No changes detected" placeholder + # into changelog.md even when there's no diff, so file existence isn't + # a reliable signal — grep the body for the placeholder string instead. + - name: Detect actual diff in changelog + id: changes + run: | + if [ -s changelog.md ] && ! grep -q 'No changes detected' changelog.md; then + echo "has_changes=true" >> "$GITHUB_OUTPUT" + else + echo "has_changes=false" >> "$GITHUB_OUTPUT" + fi + + # Identify any prior comment from this workflow by an invisible HTML + # marker, so subsequent runs update the same comment in place even + # when other GitHub-Actions bot comments exist on the PR. + - name: Find previous workflow comment + id: prev-comment + env: + GH_TOKEN: ${{ github.token }} + run: | + id=$(gh api \ + "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ + --paginate \ + --jq '.[] | select(.body | startswith("")) | .id' \ + | tail -n1) + echo "id=${id:-}" >> "$GITHUB_OUTPUT" - - name: Export specifications (yaml) - run: bin/console api:openapi:export --yaml --output=public/api-spec-v2.yaml --no-interaction + - name: Comment PR - non-breaking changes + if: steps.breaking.outcome == 'success' && steps.changes.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ github.token }} + PREV_ID: ${{ steps.prev-comment.outputs.id }} + run: | + { + echo "" + echo "## API Specification - Non-breaking changes" + echo "" + cat changelog.md + } > comment.md + if [ -n "$PREV_ID" ]; then + jq -Rs '{body: .}' < comment.md \ + | gh api "repos/${{ github.repository }}/issues/comments/$PREV_ID" \ + --method PATCH --input - + else + gh pr comment ${{ github.event.pull_request.number }} --body-file comment.md + fi - - name: Check for changes in specifications (yaml) - run: git diff --diff-filter=ACMRT --exit-code public/api-spec-v2.yaml + - name: Comment PR - breaking changes + if: steps.breaking.outcome == 'failure' + env: + GH_TOKEN: ${{ github.token }} + PREV_ID: ${{ steps.prev-comment.outputs.id }} + run: | + { + echo "" + echo "## API Specification - Breaking changes detected" + echo "" + if [ -s changelog.md ]; then + cat changelog.md + else + echo "The breaking changes action detected incompatible changes. Review the action logs for details." + fi + } > comment.md + if [ -n "$PREV_ID" ]; then + jq -Rs '{body: .}' < comment.md \ + | gh api "repos/${{ github.repository }}/issues/comments/$PREV_ID" \ + --method PATCH --input - + else + gh pr comment ${{ github.event.pull_request.number }} --body-file comment.md + fi - - name: Export specifications (json) - run: bin/console api:openapi:export --output=public/api-spec-v2.json --no-interaction + # If a prior run posted a comment but this run is clean, update the + # existing comment to reflect that. Don't post anything when there + # was no prior comment — that would be the noise we just removed. + - name: Mark previous comment resolved (when no changes) + if: steps.breaking.outcome == 'success' && steps.changes.outputs.has_changes == 'false' && steps.prev-comment.outputs.id != '' + env: + GH_TOKEN: ${{ github.token }} + PREV_ID: ${{ steps.prev-comment.outputs.id }} + run: | + { + echo "" + echo "## API Specification" + echo "" + echo "_No changes detected on this run — previous diff resolved._" + } > comment.md + jq -Rs '{body: .}' < comment.md \ + | gh api "repos/${{ github.repository }}/issues/comments/$PREV_ID" \ + --method PATCH --input - - - name: Check for changes in specifications (json) - run: git diff --diff-filter=ACMRT --exit-code public/api-spec-v2.json + - name: Fail if breaking changes detected + if: steps.breaking.outcome == 'failure' + run: exit 1 diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml new file mode 100644 index 000000000..2560d08d9 --- /dev/null +++ b/.github/workflows/build-images.yml @@ -0,0 +1,131 @@ +### ### Build docker images +### +### Builds and pushes the API and Nginx images to GHCR. Triggers on push to +### `develop` (image tag `develop`) and on git tag pushes (image tag = git +### tag). The Nginx image layers on the API image, so the API build runs +### first in the same job. +### +### #### Assumptions +### +### 1. The `os2display` GHCR namespace exists and `GITHUB_TOKEN` carries +### `packages: write`. +### 2. `infrastructure/build-n-push.sh` mirrors the build for local use. + +--- +name: Build docker images + +on: + push: + branches: + - develop + tags: + - "*" + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set release timestamp + run: | + echo "APP_RELEASE_TIMESTAMP=$(date +%s)" >> "$GITHUB_ENV" + echo "APP_RELEASE_TIME=$(date -u)" >> "$GITHUB_ENV" + + - name: Docker meta (API) + id: meta-api + uses: docker/metadata-action@v6 + with: + images: ghcr.io/os2display/display-api-service + tags: | + # develop branch → 'develop' + type=ref,event=branch + # any tag → tag literal (e.g. '3.0.0', '3.0.0-RC1' for release + # candidates cut from release/X.Y.Z) + type=ref,event=tag + # also emit canonical semver tag for valid semver-shaped tags, + # so '3.0.0' and '3.0.0-RC1' both produce a properly-typed entry. + type=semver,pattern={{version}} + # latest=auto adds ':latest' only for non-prerelease semver tags, + # so '3.0.0' updates ':latest' but '3.0.0-RC1' does not. + flavor: | + latest=auto + + - name: Build and push (API) + uses: docker/build-push-action@v7 + with: + context: ./infrastructure/display-api-service/ + file: ./infrastructure/display-api-service/Dockerfile + build-contexts: | + repository-root=. + # Use the version computed by docker/metadata-action so APP_VERSION + # matches the image tag for every trigger ('develop' / '3.0.0' / + # '3.0.0-RC1') without bespoke string handling per trigger. + build-args: | + APP_VERSION=${{ steps.meta-api.outputs.version }} + APP_RELEASE_TIMESTAMP=${{ env.APP_RELEASE_TIMESTAMP }} + APP_RELEASE_TIME=${{ env.APP_RELEASE_TIME }} + push: true + tags: ${{ steps.meta-api.outputs.tags }} + labels: ${{ steps.meta-api.outputs.labels }} + provenance: mode=max + sbom: true + platforms: linux/amd64,linux/arm64 + # mode=max caches builder stages (client_app_builder, api_app_builder), + # which is what makes multi-arch builds practical to cache at all. + # Scoped per image so API and Nginx caches don't evict each other. + cache-from: type=gha,scope=display-api-service + cache-to: type=gha,mode=max,scope=display-api-service + + - name: Docker meta (Nginx) + id: meta-nginx + uses: docker/metadata-action@v6 + with: + images: ghcr.io/os2display/display-api-service-nginx + tags: | + # develop branch → 'develop' + type=ref,event=branch + # any tag → tag literal (e.g. '3.0.0', '3.0.0-RC1' for release + # candidates cut from release/X.Y.Z) + type=ref,event=tag + # also emit canonical semver tag for valid semver-shaped tags, + # so '3.0.0' and '3.0.0-RC1' both produce a properly-typed entry. + type=semver,pattern={{version}} + flavor: | + latest=auto + + - name: Build and push (Nginx) + uses: docker/build-push-action@v7 + with: + context: ./infrastructure/nginx/ + file: ./infrastructure/nginx/Dockerfile + # Reuse the API image's resolved version so the Nginx FROM + # always tracks the tag that was just pushed in the previous + # step, even if the two metadata-action invocations ever + # diverge. + build-args: | + APP_VERSION=${{ steps.meta-api.outputs.version }} + push: true + tags: ${{ steps.meta-nginx.outputs.tags }} + labels: ${{ steps.meta-nginx.outputs.labels }} + provenance: mode=max + sbom: true + platforms: linux/amd64,linux/arm64 + cache-from: type=gha,scope=display-api-service-nginx + cache-to: type=gha,mode=max,scope=display-api-service-nginx diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml index 483da6e95..327d42953 100644 --- a/.github/workflows/changelog.yaml +++ b/.github/workflows/changelog.yaml @@ -1,7 +1,3 @@ -# Do not edit this file! Make a pull request on changing -# github/workflows/changelog.yaml in -# https://github.com/itk-dev/devops_itkdev-docker if need be. - ### ### Changelog ### ### Checks that changelog has been updated @@ -14,11 +10,9 @@ on: jobs: changelog: runs-on: ubuntu-latest - strategy: - fail-fast: false steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 2 diff --git a/.github/workflows/code-analysis.yaml b/.github/workflows/code-analysis.yaml index e6fe18446..c8a276435 100644 --- a/.github/workflows/code-analysis.yaml +++ b/.github/workflows/code-analysis.yaml @@ -1,39 +1,42 @@ +### ### Code Analysis +### +### Runs PHPStan static analysis against the project. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can +### be run inside the `phpfpm` service. +### 2. [phpstan/phpstan](https://phpstan.org/) is a dev requirement in +### `composer.json`. +### 3. `phpstan.dist.neon` configures the analyser. + on: pull_request name: Code Analysis +env: + COMPOSE_USER: runner + jobs: phpstan: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - php: ["8.4"] - name: PHPStan (PHP ${{ matrix.php }}) + name: PHPStan steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 + - name: Cache vendor + uses: actions/cache@v5 with: - php-version: ${{ matrix.php}} - extensions: apcu, ctype, iconv, imagick, json, redis, soap, xmlreader, zip - coverage: none + path: vendor + key: vendor-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-php8.4- - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Cache composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ matrix.php }}-composer- + - name: Setup network + run: docker network create frontend - name: Install Dependencies - run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + run: docker compose run --rm --no-deps phpfpm composer install - name: PHPStan - run: vendor/bin/phpstan analyse --no-progress + run: docker compose run --rm --no-deps phpfpm vendor/bin/phpstan analyse --no-progress diff --git a/.github/workflows/composer.yaml b/.github/workflows/composer.yaml index b068c235a..c4818f9b5 100644 --- a/.github/workflows/composer.yaml +++ b/.github/workflows/composer.yaml @@ -1,7 +1,3 @@ -# Do not edit this file! Make a pull request on changing -# github/workflows/composer.yaml in -# https://github.com/itk-dev/devops_itkdev-docker if need be. - ### ### Composer ### ### Validates composer.json and checks that it's normalized. @@ -14,13 +10,13 @@ ### is a dev requirement in `composer.json`: ### ### ``` shell -### docker compose run --rm phpfpm composer require --dev ergebnis/composer-normalize +### docker compose run --rm --no-deps phpfpm composer require --dev ergebnis/composer-normalize ### ``` ### ### Normalize `composer.json` by running ### ### ``` shell -### docker compose run --rm phpfpm composer normalize +### docker compose run --rm --no-deps phpfpm composer normalize ### ``` name: Composer @@ -38,21 +34,23 @@ on: jobs: composer-validate: runs-on: ubuntu-latest - strategy: - fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: | docker network create frontend - docker compose run --rm phpfpm composer validate --strict + docker compose run --rm --no-deps phpfpm composer validate --strict composer-normalized: runs-on: ubuntu-latest - strategy: - fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - name: Cache vendor + uses: actions/cache@v5 + with: + path: vendor + key: vendor-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-php8.4- - run: | docker network create frontend - docker compose run --rm phpfpm composer install - docker compose run --rm phpfpm composer normalize --dry-run + docker compose run --rm --no-deps phpfpm composer install + docker compose run --rm --no-deps phpfpm composer normalize --dry-run diff --git a/.github/workflows/composer_install.yaml b/.github/workflows/composer_install.yaml index 296b8f8bb..157e4dd0d 100644 --- a/.github/workflows/composer_install.yaml +++ b/.github/workflows/composer_install.yaml @@ -1,40 +1,30 @@ +### ### Composer install +### +### Verifies that `composer install --no-dev -o` succeeds in prod mode against +### the committed lockfile. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can +### be run inside the `phpfpm` service. + on: pull_request name: Composer install +env: + COMPOSE_USER: runner + jobs: test-composer-install: runs-on: ubuntu-latest - env: - COMPOSER_ALLOW_SUPERUSER: 1 - strategy: - fail-fast: false - matrix: - php: ["8.4"] - name: Composer install in prod mode (PHP ${{ matrix.php}}) + name: Composer install in prod mode steps: - name: Checkout - uses: actions/checkout@v4 - - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php}} - extensions: apcu, ctype, iconv, imagick, json, redis, soap, xmlreader, zip - coverage: none - - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + uses: actions/checkout@v6 - - name: Cache composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ matrix.php }}-composer- + - name: Setup network + run: docker network create frontend - - name: "[prod] Composer install with exported .env variables" - run: | - set -a && source .env && set +a - APP_ENV=prod composer install --no-dev -o + - name: "[prod] Composer install" + run: docker compose run --rm --no-deps -e APP_ENV=prod phpfpm composer install --no-dev -o diff --git a/.github/workflows/doctrine.yaml b/.github/workflows/doctrine.yaml index 3cf09a61b..4a2877110 100644 --- a/.github/workflows/doctrine.yaml +++ b/.github/workflows/doctrine.yaml @@ -1,57 +1,44 @@ +### ### Doctrine +### +### Runs pending Doctrine migrations and validates the resulting schema +### against the entity metadata. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `bin/console` +### can be run inside the `phpfpm` service. +### 2. A `mariadb` service is reachable from `phpfpm`. + on: pull_request name: Doctrine +env: + COMPOSE_USER: runner + jobs: validate-doctrine-shema: runs-on: ubuntu-latest - env: - DATABASE_URL: mysql://db:db@127.0.0.1:3306/db?serverVersion=mariadb-10.5.13 - strategy: - fail-fast: false - matrix: - php: ["8.4"] - name: Validate Schema (PHP ${{ matrix.php}}) - services: - mariadb: - image: mariadb:10.5.13 - env: - MYSQL_USER: db - MYSQL_PASSWORD: db - MYSQL_DATABASE: db - MYSQL_ROOT_PASSWORD: db - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + name: Validate Schema steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 + - name: Cache vendor (prod) + uses: actions/cache@v5 with: - php-version: ${{ matrix.php}} - extensions: apcu, ctype, iconv, imagick, json, redis, soap, xmlreader, zip - coverage: none + path: vendor + key: vendor-prod-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-prod-php8.4- - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Cache composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ matrix.php }}-composer- + - name: Setup network + run: docker network create frontend - - name: "Composer install with exported .env variables" - run: | - set -a && source .env && set +a - APP_ENV=prod composer install --no-dev -o + - name: "[prod] Composer install" + run: docker compose run --rm -e APP_ENV=prod phpfpm composer install --no-dev -o - name: Run Doctrine Migrations - run: APP_ENV=prod php bin/console doctrine:migrations:migrate --no-interaction + run: docker compose run --rm -e APP_ENV=prod phpfpm bin/console doctrine:migrations:migrate --no-interaction - name: Validate Doctrine schema - run: APP_ENV=prod php bin/console doctrine:schema:validate + run: docker compose run --rm -e APP_ENV=prod phpfpm bin/console doctrine:schema:validate diff --git a/.github/workflows/env_coverage.yaml b/.github/workflows/env_coverage.yaml new file mode 100644 index 000000000..8c08be02d --- /dev/null +++ b/.github/workflows/env_coverage.yaml @@ -0,0 +1,33 @@ +### ### Env coverage +### +### Validates that every Symfony env variable read via `%env(...)%` in +### `config/` is documented in `.env`, and that every variable in `.env` +### carries a preceding description comment. +### +### #### Assumptions +### +### 1. `scripts/check-env-coverage.sh` is executable. +### 2. The Taskfile target `coding-standards:env:check` invokes it. +--- +on: + push: + branches: + - develop + pull_request: + +name: Env coverage + +jobs: + env-coverage: + name: Validate env documentation coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install task + uses: go-task/setup-task@v2 + with: + version: 3.x + + - name: Run coding-standards:env:check + run: task coding-standards:env:check diff --git a/.github/workflows/github_build_release.yml b/.github/workflows/github_build_release.yml index 2ca6b44d7..19aecc2fd 100644 --- a/.github/workflows/github_build_release.yml +++ b/.github/workflows/github_build_release.yml @@ -1,3 +1,14 @@ +### ### Create Github Release +### +### On a semver tag push (`*.*.*`): builds the production tarball, writes +### `release.json`, and creates a GitHub release. Tags containing a `-` +### (e.g. `3.0.0-RC1`) are flagged as pre-releases. +### +### #### Assumptions +### +### 1. The `phpfpm` and `node` compose services can run on the runner. +### 2. The tagged commit passes the regular CI suite. + on: push: tags: @@ -8,22 +19,67 @@ name: Create Github Release permissions: contents: write +env: + COMPOSE_USER: runner + jobs: create-release: runs-on: ubuntu-latest - env: - COMPOSER_ALLOW_SUPERUSER: 1 - APP_ENV: prod steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 + + - name: Setup network + run: docker network create frontend + + - name: Composer install (prod) + run: | + docker compose run --rm --env APP_ENV=prod phpfpm \ + composer install --no-dev -o --classmap-authoritative + docker compose run --rm --env APP_ENV=prod phpfpm composer clear-cache + + - name: NPM install and build + run: | + docker compose run --rm node npm install + docker compose run --rm node npm run build + + - name: Cleanup before packaging + run: | + # Mirrors .dockerignore for tarball hygiene, minus vendor/ and + # public/build/ which we just produced and need to ship. + rm -rf \ + infrastructure \ + fixtures \ + tests \ + node_modules \ + docs \ + scripts \ + var \ + test-results \ + playwright-report \ + blob-report \ + playwright \ + public/fixtures + rm -f \ + docker-compose*.yml \ + Taskfile.yml \ + playwright.config.ts \ + phpstan.dist.neon \ + phpstan-baseline.neon \ + phpunit.xml \ + phpunit.xml.dist \ + rector.php \ + xdebug.ini \ + launch.json - - name: Composer install + - name: Write release.json run: | - docker network create frontend - docker compose run --rm --user=root --env APP_ENV=prod phpfpm composer install --no-dev -o --classmap-authoritative - docker compose run --rm --user=root --env APP_ENV=prod phpfpm composer clear-cache - rm -rf infrastructure + printf '{"releaseTimestamp": %s, "releaseTime": "%s", "releaseVersion": "%s"}\n' \ + "$(date +%s)" \ + "$(date -u)" \ + "${{ github.ref_name }}" \ + > public/release.json + cat public/release.json - name: Make assets dir run: | @@ -37,9 +93,22 @@ jobs: - name: Create checksum run: sha256sum ../assets/${{ github.event.repository.name }}-${{ github.ref_name }}.tar.gz > ../assets/checksum.txt + # Mirror docker/metadata-action's 'latest=auto' rule used by build-images.yml: + # any semver tag with a pre-release identifier ('3.0.0-RC1', '3.0.0-beta') + # is published as a GitHub pre-release; final tags ('3.0.0') are not. + - name: Detect pre-release + id: prerelease + shell: bash + run: | + if [[ "$GITHUB_REF_NAME" == *-* ]]; then + echo "flag=--prerelease" >> "$GITHUB_OUTPUT" + else + echo "flag=" >> "$GITHUB_OUTPUT" + fi + - name: Create a release in GitHub and uploads assets run: | - gh release create ${{ github.ref_name }} --verify-tag --generate-notes ../assets/*.* + gh release create ${{ github.ref_name }} --verify-tag --generate-notes ${{ steps.prerelease.outputs.flag }} ../assets/*.* env: GITHUB_TOKEN: ${{ github.TOKEN }} shell: bash diff --git a/.github/workflows/itkdev_docker_build_develop.yml b/.github/workflows/itkdev_docker_build_develop.yml deleted file mode 100644 index aaef33621..000000000 --- a/.github/workflows/itkdev_docker_build_develop.yml +++ /dev/null @@ -1,60 +0,0 @@ ---- -on: - push: - branches: - - "develop" - -# This Action builds to itkdev/* using ./infrastructure/itkdev/* -name: ITK Dev - Build docker image (develop) - -jobs: - docker: - runs-on: ubuntu-latest - env: - APP_VERSION: develop - COMPOSER_ALLOW_SUPERUSER: 1 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Build api - - name: Docker meta (API) - id: meta-api - uses: docker/metadata-action@v5 - with: - images: itkdev/os2display-api-service - - - name: Build and push (API) - uses: docker/build-push-action@v5 - with: - context: ./infrastructure/itkdev/display-api-service/ - file: ./infrastructure/itkdev/display-api-service/Dockerfile - build-args: | - VERSION=${{ env.APP_VERSION }} - push: true - tags: ${{ steps.meta-api.outputs.tags }} - labels: ${{ steps.meta-api.outputs.labels }} - - # Build nginx (depends on api build) - - name: Docker meta (Nginx) - id: meta-nginx - uses: docker/metadata-action@v5 - with: - images: itkdev/os2display-api-service-nginx - - - name: Build and push (Nginx) - uses: docker/build-push-action@v5 - with: - context: ./infrastructure/itkdev/nginx/ - file: ./infrastructure/itkdev/nginx/Dockerfile - build-args: | - APP_VERSION=${{ env.APP_VERSION }} - push: true - tags: ${{ steps.meta-nginx.outputs.tags }} - labels: ${{ steps.meta-nginx.outputs.labels }} diff --git a/.github/workflows/itkdev_docker_build_tag.yml b/.github/workflows/itkdev_docker_build_tag.yml deleted file mode 100644 index f64644a28..000000000 --- a/.github/workflows/itkdev_docker_build_tag.yml +++ /dev/null @@ -1,63 +0,0 @@ ---- -on: - push: - tags: - - "*" - -# This Action builds to itkdev/* using ./infrastructure/itkdev/* -name: ITK Dev - Build docker image (tag) - -jobs: - docker: - runs-on: ubuntu-latest - env: - COMPOSER_ALLOW_SUPERUSER: 1 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Build api - - name: Docker meta (API) - id: meta-api - uses: docker/metadata-action@v5 - with: - images: itkdev/os2display-api-service - - - name: Build and push (API) - uses: docker/build-push-action@v5 - with: - context: ./infrastructure/itkdev/display-api-service/ - file: ./infrastructure/itkdev/display-api-service/Dockerfile - build-args: | - APP_VERSION=${{ github.ref }} - push: true - tags: ${{ steps.meta-api.outputs.tags }} - labels: ${{ steps.meta-api.outputs.labels }} - - # Build nginx (depends on api build) - - name: Docker meta (Nginx) - id: meta-nginx - uses: docker/metadata-action@v5 - with: - images: itkdev/os2display-api-service-nginx - - - name: Get the tag - id: get_tag - run: echo ::set-output name=git_tag::$(echo $GITHUB_REF_NAME) - - - name: Build and push (Nginx) - uses: docker/build-push-action@v5 - with: - context: ./infrastructure/itkdev/nginx/ - file: ./infrastructure/itkdev/nginx/Dockerfile - build-args: | - APP_VERSION=${{ steps.get_tag.outputs.git_tag }} - push: true - tags: ${{ steps.meta-nginx.outputs.tags }} - labels: ${{ steps.meta-nginx.outputs.labels }} diff --git a/.github/workflows/javascript.yaml b/.github/workflows/javascript.yaml index 801ddbe93..8b7ed0a97 100644 --- a/.github/workflows/javascript.yaml +++ b/.github/workflows/javascript.yaml @@ -1,7 +1,3 @@ -# Do not edit this file! Make a pull request on changing -# github/workflows/symfony/javascript.yaml in -# https://github.com/itk-dev/devops_itkdev-docker if need be. - ### ### Symfony JavaScript (and TypeScript) ### ### Validates JavaScript files. @@ -23,11 +19,9 @@ on: jobs: javascript-lint: runs-on: ubuntu-latest - strategy: - fail-fast: false steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - run: | docker network create frontend diff --git a/.github/workflows/markdown.yaml b/.github/workflows/markdown.yaml index 60fc0ee5c..6e4f63e99 100644 --- a/.github/workflows/markdown.yaml +++ b/.github/workflows/markdown.yaml @@ -1,7 +1,3 @@ -# Do not edit this file! Make a pull request on changing -# github/workflows/markdown.yaml in -# https://github.com/itk-dev/devops_itkdev-docker if need be. - ### ### Markdown ### ### Lints Markdown files (`**/*.md`) in the project. @@ -30,11 +26,9 @@ on: jobs: markdown-lint: runs-on: ubuntu-latest - strategy: - fail-fast: false steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - run: | docker network create frontend diff --git a/.github/workflows/os2display_docker_build_develop.yml b/.github/workflows/os2display_docker_build_develop.yml deleted file mode 100644 index bcdf09a6c..000000000 --- a/.github/workflows/os2display_docker_build_develop.yml +++ /dev/null @@ -1,60 +0,0 @@ ---- -on: - push: - branches: - - "develop" - -# This Action builds to os2display/* using ./infrastructure/os2display/* -name: OS2display - Build docker image (develop) - -jobs: - docker: - runs-on: ubuntu-latest - env: - APP_VERSION: develop - COMPOSER_ALLOW_SUPERUSER: 1 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Build api - - name: Docker meta (API) - id: meta-api - uses: docker/metadata-action@v5 - with: - images: os2display/display-api-service - - - name: Build and push (API) - uses: docker/build-push-action@v5 - with: - context: ./infrastructure/os2display/display-api-service/ - file: ./infrastructure/os2display/display-api-service/Dockerfile - build-args: | - VERSION=${{ env.APP_VERSION }} - push: true - tags: ${{ steps.meta-api.outputs.tags }} - labels: ${{ steps.meta-api.outputs.labels }} - - # Build nginx (depends on api build) - - name: Docker meta (Nginx) - id: meta-nginx - uses: docker/metadata-action@v5 - with: - images: os2display/display-api-service-nginx - - - name: Build and push (Nginx) - uses: docker/build-push-action@v5 - with: - context: ./infrastructure/os2display/nginx/ - file: ./infrastructure/os2display/nginx/Dockerfile - build-args: | - APP_VERSION=${{ env.APP_VERSION }} - push: true - tags: ${{ steps.meta-nginx.outputs.tags }} - labels: ${{ steps.meta-nginx.outputs.labels }} diff --git a/.github/workflows/os2display_docker_build_tag.yml b/.github/workflows/os2display_docker_build_tag.yml deleted file mode 100644 index 8bd2b12aa..000000000 --- a/.github/workflows/os2display_docker_build_tag.yml +++ /dev/null @@ -1,63 +0,0 @@ ---- -on: - push: - tags: - - "*" - -# This Action builds to os2display/* using ./infrastructure/os2display/* -name: OS2display - Build docker image (tag) - -jobs: - docker: - runs-on: ubuntu-latest - env: - COMPOSER_ALLOW_SUPERUSER: 1 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Build api - - name: Docker meta (API) - id: meta-api - uses: docker/metadata-action@v5 - with: - images: os2display/display-api-service - - - name: Build and push (API) - uses: docker/build-push-action@v5 - with: - context: ./infrastructure/os2display/display-api-service/ - file: ./infrastructure/os2display/display-api-service/Dockerfile - build-args: | - APP_VERSION=${{ github.ref }} - push: true - tags: ${{ steps.meta-api.outputs.tags }} - labels: ${{ steps.meta-api.outputs.labels }} - - # Build nginx (depends on api build) - - name: Docker meta (Nginx) - id: meta-nginx - uses: docker/metadata-action@v5 - with: - images: os2display/display-api-service-nginx - - - name: Get the tag - id: get_tag - run: echo ::set-output name=git_tag::$(echo $GITHUB_REF_NAME) - - - name: Build and push (Nginx) - uses: docker/build-push-action@v5 - with: - context: ./infrastructure/os2display/nginx/ - file: ./infrastructure/os2display/nginx/Dockerfile - build-args: | - APP_VERSION=${{ steps.get_tag.outputs.git_tag }} - push: true - tags: ${{ steps.meta-nginx.outputs.tags }} - labels: ${{ steps.meta-nginx.outputs.labels }} diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml index ee9679776..bbbfc5c3b 100644 --- a/.github/workflows/php.yaml +++ b/.github/workflows/php.yaml @@ -1,7 +1,3 @@ -# Do not edit this file! Make a pull request on changing -# github/workflows/symfony/php.yaml in -# https://github.com/itk-dev/devops_itkdev-docker if need be. - ### ### Symfony PHP ### ### Checks that PHP code adheres to the [Symfony coding @@ -15,14 +11,14 @@ ### is a dev requirement in `composer.json`: ### ### ``` shell -### docker compose run --rm phpfpm composer require --dev friendsofphp/php-cs-fixer +### docker compose run --rm --no-deps phpfpm composer require --dev friendsofphp/php-cs-fixer ### ``` ### ### Clean up and check code by running ### ### ``` shell -### docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix -### docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff +### docker compose run --rm --no-deps phpfpm vendor/bin/php-cs-fixer fix +### docker compose run --rm --no-deps phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff ### ``` ### ### > [!NOTE] The template adds `.php-cs-fixer.dist.php` as [a configuration @@ -48,9 +44,15 @@ jobs: name: PHP - Check Coding Standards runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - name: Cache vendor + uses: actions/cache@v5 + with: + path: vendor + key: vendor-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-php8.4- - run: | docker network create frontend - docker compose run --rm phpfpm composer install + docker compose run --rm --no-deps phpfpm composer install # https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/doc/usage.rst#the-check-command - docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff + docker compose run --rm --no-deps phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff diff --git a/.github/workflows/phpunit.yaml b/.github/workflows/phpunit.yaml index c2bd9fcc4..87f259d8d 100644 --- a/.github/workflows/phpunit.yaml +++ b/.github/workflows/phpunit.yaml @@ -1,59 +1,44 @@ +### ### Test (PHPUnit) +### +### Runs the API's PHPUnit test suite against a real database. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can +### be run inside the `phpfpm` service. +### 2. A `mariadb` service is reachable from `phpfpm`. +### 3. `composer run test-setup` and `composer run test` are defined. + on: pull_request name: Test +env: + COMPOSE_USER: runner + jobs: phpunit: runs-on: ubuntu-latest - services: - mariadb: - image: mariadb:lts - ports: - - 3306 - env: - MYSQL_USER: db - MYSQL_PASSWORD: db - MYSQL_DATABASE: db_test - MYSQL_ROOT_PASSWORD: password - # https://mariadb.org/mariadb-server-docker-official-images-healthcheck-without-mysqladmin/ - options: >- - --health-cmd="healthcheck.sh --connect --innodb_initialized" - --health-interval=5s - --health-timeout=2s - --health-retries=3 - strategy: - fail-fast: false - matrix: - php: ["8.4"] - name: PHP Unit tests (PHP ${{ matrix.php }}) + name: PHP Unit tests steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 + - name: Cache vendor + uses: actions/cache@v5 with: - php-version: ${{ matrix.php}} - extensions: apcu, ctype, iconv, imagick, json, redis, soap, xmlreader, zip - coverage: none + path: vendor + key: vendor-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-php8.4- - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Cache composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ matrix.php }}-composer- + - name: Setup network + run: docker network create frontend - name: Install Dependencies - run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist + run: docker compose run --rm phpfpm composer install - name: PHP Unit - Test setup - env: - PORT: ${{ job.services.mariadb.ports[3306] }} - run: | - DATABASE_URL="mysql://db:db@127.0.0.1:$PORT/db_test" composer run test-setup - DATABASE_URL="mysql://db:db@127.0.0.1:$PORT/db_test" composer run test + run: docker compose run --rm phpfpm composer run test-setup + + - name: PHP Unit - Run tests + run: docker compose run --rm phpfpm composer run test diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index 5b78012d6..6cc614d80 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -1,3 +1,15 @@ +### ### Playwright +### +### Builds the frontend (`composer install`, `npm install`, `npm run build`) +### and runs the Playwright end-to-end test suite against the local stack. +### +### #### Assumptions +### +### 1. Docker compose services `phpfpm`, `node`, `nginx`, and `playwright` +### are defined. +### 2. The `nginx` service has a healthcheck Playwright can wait on. +### 3. `playwright.config.ts` lives in the repo root. + on: pull_request name: Test @@ -9,11 +21,23 @@ jobs: frontend-build-and-test: name: Playwright runs-on: ubuntu-latest - strategy: - fail-fast: true steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 + + - name: Cache vendor + uses: actions/cache@v5 + with: + path: vendor + key: vendor-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-php8.4- + + - name: Cache node_modules + uses: actions/cache@v5 + with: + path: node_modules + key: node-modules-${{ hashFiles('package-lock.json') }} + restore-keys: node-modules- - name: Setup network run: docker network create frontend @@ -38,7 +62,7 @@ jobs: docker compose run --rm playwright npx playwright install --with-deps docker compose run --rm playwright npx playwright test - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: always() with: name: playwright-report diff --git a/.github/workflows/rector.yaml b/.github/workflows/rector.yaml index b99811134..0e28e9052 100644 --- a/.github/workflows/rector.yaml +++ b/.github/workflows/rector.yaml @@ -1,39 +1,42 @@ +### ### Rector +### +### Runs Rector in `--dry-run` mode to detect missing modernisations. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can +### be run inside the `phpfpm` service. +### 2. [rector/rector](https://github.com/rectorphp/rector) is a dev +### requirement in `composer.json`. +### 3. `rector.php` configures the rule set. + on: pull_request name: Rector +env: + COMPOSE_USER: runner + jobs: rector: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - php: ["8.4"] - name: Rector (PHP ${{ matrix.php }}) + name: Rector steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 + - name: Cache vendor + uses: actions/cache@v5 with: - php-version: ${{ matrix.php}} - extensions: apcu, ctype, iconv, imagick, json, redis, soap, xmlreader, zip - coverage: none + path: vendor + key: vendor-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-php8.4- - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Cache composer dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ matrix.php }}-composer- + - name: Setup network + run: docker network create frontend - name: Install Dependencies - run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist + run: docker compose run --rm --no-deps phpfpm composer install - name: Rector - run: phpdbg -qrr ./vendor/bin/rector --dry-run + run: docker compose run --rm --no-deps phpfpm vendor/bin/rector --dry-run diff --git a/.github/workflows/styles.yaml b/.github/workflows/styles.yaml index edc796020..3b4bf27bb 100644 --- a/.github/workflows/styles.yaml +++ b/.github/workflows/styles.yaml @@ -1,7 +1,3 @@ -# Do not edit this file! Make a pull request on changing -# github/workflows/symfony/styles.yaml in -# https://github.com/itk-dev/devops_itkdev-docker if need be. - ### ### Symfony Styles (CSS and SCSS) ### ### Validates styles files. @@ -23,11 +19,9 @@ on: jobs: styles-lint: runs-on: ubuntu-latest - strategy: - fail-fast: false steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - run: | docker network create frontend diff --git a/.github/workflows/twig.yaml b/.github/workflows/twig.yaml index 9dc424b45..fbf799c89 100644 --- a/.github/workflows/twig.yaml +++ b/.github/workflows/twig.yaml @@ -1,7 +1,3 @@ -# Do not edit this file! Make a pull request on changing -# github/workflows/twig.yaml in -# https://github.com/itk-dev/devops_itkdev-docker if need be. - ### ### Twig ### ### Validates Twig files @@ -14,7 +10,7 @@ ### is a dev requirement in `composer.json`: ### ### ``` shell -### docker compose run --rm phpfpm composer require --dev vincentlanglet/twig-cs-fixer +### docker compose run --rm --no-deps phpfpm composer require --dev vincentlanglet/twig-cs-fixer ### ``` ### ### 3. A [Configuration @@ -36,13 +32,18 @@ on: jobs: twig-lint: runs-on: ubuntu-latest - strategy: - fail-fast: false steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 + + - name: Cache vendor + uses: actions/cache@v5 + with: + path: vendor + key: vendor-php8.4-${{ hashFiles('composer.lock') }} + restore-keys: vendor-php8.4- - run: | docker network create frontend - docker compose run --rm phpfpm composer install - docker compose run --rm phpfpm vendor/bin/twig-cs-fixer lint + docker compose run --rm --no-deps phpfpm composer install + docker compose run --rm --no-deps phpfpm vendor/bin/twig-cs-fixer lint diff --git a/.github/workflows/vitest.yaml b/.github/workflows/vitest.yaml index 7203fe02f..ddb3e1a61 100644 --- a/.github/workflows/vitest.yaml +++ b/.github/workflows/vitest.yaml @@ -1,3 +1,13 @@ +### ### Unit tests (Vitest) +### +### Runs the client-side Vitest unit tests. +### +### #### Assumptions +### +### 1. A docker compose service named `node` can be run and `npm` can be run +### inside the `node` service. +### 2. `npm run test:unit` is defined in `package.json`. + on: pull_request name: Unit tests @@ -11,7 +21,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 + + - name: Cache node_modules + uses: actions/cache@v5 + with: + path: node_modules + key: node-modules-${{ hashFiles('package-lock.json') }} + restore-keys: node-modules- - name: Setup network run: docker network create frontend diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml index 1c0ada3f7..349b55752 100644 --- a/.github/workflows/yaml.yaml +++ b/.github/workflows/yaml.yaml @@ -1,7 +1,3 @@ -# Do not edit this file! Make a pull request on changing -# github/workflows/yaml.yaml in -# https://github.com/itk-dev/devops_itkdev-docker if need be. - ### ### YAML ### ### Validates YAML files. @@ -31,7 +27,7 @@ jobs: yaml-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: | docker network create frontend diff --git a/.gitignore b/.gitignore index ecee3e2fe..40168e85a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,6 @@ /.php-cs-fixer.cache ###< friendsofphp/php-cs-fixer ### -node_modules - public/media/* !public/media/thumbnail_video.png !public/media/thumbnail_other.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eedbdb63..4d3cb3883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- Cleaned up and documented CI workflows; api-spec workflow now reports breaking changes. - Migrated `rector.php` to the `RectorConfig::configure()` builder. - Gathered all repositories in one Symfony application. - Changed to vite 7 and rolldown. @@ -41,6 +42,19 @@ All notable changes to this project will be documented in this file. - Added BRND to feed source admin dropdown. - Upgraded to PHP 8.4. - Changed default CLIENT_PULL_STRATEGY_INTERVAL value to 10 minutes. +- Updated infrastructure and image build for mono-repo. +- Fixed nginx static-file location to fall back to PHP so LiipImagineBundle can generate missing thumbnails (#370). +- Unified nginx config: dev compose now mounts the production template/nginx.conf so local matches deployed behavior (#370). +- Annotated `.env` so it serves as the canonical, self-documenting Symfony + env example, with a CI check that enforces it stays in sync with `config/`. +- Switched image build pipeline to GHCR with multi-arch layer caching. +- Aligned the nginx image env-var contract: split `NGINX_FPM_SERVICE` and + `NGINX_FPM_PORT`, raised upload cap and trusted-proxy CIDR defaults. +- Allowed same-origin iframe embedding so the admin's screen/playlist + preview and fullscreen slide view work (#390). +- Image build now writes `public/release.json` so the client's + release-loader can fetch it. The same file is included in the GitHub + Release tarball. - Changed code analysis tool from psalm to phpstan. - Changed src/Controller/Api/AuthOidcController.php to get session from request. diff --git a/README.md b/README.md index f3b2c9b8e..15f52997d 100644 --- a/README.md +++ b/README.md @@ -9,23 +9,24 @@ 5. [Taskfile](#taskfile) 6. [Development setup](#development-setup) 7. [Production setup](#production-setup) -8. [Coding standards](#coding-standards) -9. [Stateless](#stateless) -10. [OIDC providers](#oidc-providers) -11. [JWT Auth](#jwt-auth) -12. [Test](#test) -13. [API specification and generated code](#api-specification-and-generated-code) -14. [Configuration](#configuration) -15. [Rest API & Relationships](#rest-api--relationships) -16. [Error codes in the Client](#error-codes-in-the-client) -17. [Preview mode in the Client](#preview-mode-in-the-client) -18. [Feeds](#feeds) -19. [Custom Templates](#custom-templates) -20. [Static Analysis](#static-analysis) -21. [Upgrade Guide](#upgrade-guide) -22. [Tenants](#tenants) -23. [Screen layouts](#screen-layouts) -24. [Templates](#templates) +8. [Container images](#container-images) +9. [Coding standards](#coding-standards) +10. [Stateless](#stateless) +11. [OIDC providers](#oidc-providers) +12. [JWT Auth](#jwt-auth) +13. [Test](#test) +14. [API specification and generated code](#api-specification-and-generated-code) +15. [Configuration](#configuration) +16. [Rest API & Relationships](#rest-api--relationships) +17. [Error codes in the Client](#error-codes-in-the-client) +18. [Preview mode in the Client](#preview-mode-in-the-client) +19. [Feeds](#feeds) +20. [Custom Templates](#custom-templates) +21. [Static Analysis](#static-analysis) +22. [Upgrade Guide](#upgrade-guide) +23. [Tenants](#tenants) +24. [Screen layouts](#screen-layouts) +25. [Templates](#templates) ## Description @@ -155,14 +156,34 @@ APP_ENV=prod APP_SECRET= ``` -TODO: Add further production instructions: Build steps, release.json, etc. - Use the `app:update` command to migrate and update templates to latest version: ```shell docker compose exec phpfpm bin/console app:update --no-interaction ``` +## Container images + +Production deployments run two images: + +- `ghcr.io/os2display/display-api-service` — the php-fpm application +- `ghcr.io/os2display/display-api-service-nginx` — the nginx reverse-proxy serving static files and forwarding + PHP requests + +Both are built and published from this repository. See [`infrastructure/Readme.md`](infrastructure/Readme.md) +for the build pipeline (stages, tag scheme, local + CI flows). + +### Changing environment variables for the running images + +Set runtime configuration via your container runtime, not by editing the `.env` files baked into the image: + +- Docker Compose: `env_file:` or `environment:` on the `os2display` service. +- Other orchestrators: equivalent native mechanism (`-e`, env injection, etc.). + +Real environment variables take precedence over the image's compiled `.env.local.php`, so values set this way +override the committed `.env` baselines. Restart the container after changing them — Symfony reads its +configuration once at boot. + ## Coding standards Before a PR can be merged it has to pass the GitHub Actions checks. See `.github/workflows` for workflows that should diff --git a/Taskfile.yml b/Taskfile.yml index c79a46786..7097b346f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -108,6 +108,7 @@ tasks: - task coding-standards:php:check - task coding-standards:markdown:check - task coding-standards:yml:check + - task coding-standards:env:check coding-standards:apply: desc: "Apply coding standards." @@ -128,6 +129,11 @@ tasks: cmds: - docker compose run --rm --volume "$PWD:/md" prettier '**/*.{yml,yaml}' --check + coding-standards:env:check: + desc: Validate that every Symfony env variable in config/ is documented in .env. + cmds: + - sh scripts/check-env-coverage.sh + coding-standards:assets:apply: desc: Apply coding standards for assets cmds: @@ -257,3 +263,14 @@ tasks: desc: "Build the assets." cmds: - task compose -- run --rm node npm run build + + images:build: + desc: "Build API and Nginx images locally (host platform, loaded into local docker daemon)." + cmds: + - sh infrastructure/build.sh + + images:push: + prompt: "This will publish multi-arch images to ghcr.io/os2display. Continue?" + desc: "Build multi-arch and push to GHCR. Requires `docker login ghcr.io` first." + cmds: + - PUSH=1 sh infrastructure/build.sh diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 179834b74..274fc66ba 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -48,7 +48,7 @@ api_platform: openapi: contact: name: ITKDev - email: itkdev@mkb.aarhus.dk + email: itkdev@ba.aarhus.dk license: name: MIT diff --git a/config/packages/pentatrion_vite.yaml b/config/packages/pentatrion_vite.yaml new file mode 100644 index 000000000..3b3ecaacf --- /dev/null +++ b/config/packages/pentatrion_vite.yaml @@ -0,0 +1,4 @@ +# @see https://symfony-vite.pentatrion.com/guide/performance.html#caching-configuration-files-%F0%9F%8F%83 +when@prod: + pentatrion_vite: + cache: true diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 18ab0c07d..ec0c81834 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -41,7 +41,8 @@ services: networks: - app depends_on: - - nginx + nginx: + condition: service_healthy volumes: - .:/app - /tmp/.X11-unix:/tmp/.X11-unix diff --git a/docker-compose.server.yml b/docker-compose.server.yml index bc011cac0..a744072e9 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -29,14 +29,29 @@ services: depends_on: - phpfpm volumes: - - ./.docker/templates:/etc/nginx/templates:ro - - ./.docker/nginx.conf:/etc/nginx/nginx.conf:ro + - ./infrastructure/nginx/etc/templates:/etc/nginx/templates:ro + - ./infrastructure/nginx/etc/nginx.conf:/etc/nginx/nginx.conf:ro - .:/app environment: - NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME}-phpfpm-1:9000 + NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME}-phpfpm-1 + NGINX_FPM_PORT: 9000 NGINX_WEB_ROOT: /app/public NGINX_PORT: 8080 NGINX_MAX_BODY_SIZE: 5M + NGINX_SET_REAL_IP_FROM: 172.16.0.0/12 + healthcheck: + # $$NGINX_PORT escapes compose-time interpolation; the variable is + # expanded by the shell inside the container against the service's + # environment. + test: + [ + "CMD-SHELL", + 'wget --quiet --tries=1 --spider "http://localhost:$$NGINX_PORT/health"', + ] + interval: 5s + timeout: 3s + retries: 6 + start_period: 5s labels: - "traefik.enable=true" - "traefik.docker.network=frontend" diff --git a/docker-compose.yml b/docker-compose.yml index b18ab3814..6981a244a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,13 +57,29 @@ services: ports: - "8080" volumes: - - ./.docker/templates:/etc/nginx/templates:ro + - ./infrastructure/nginx/etc/templates:/etc/nginx/templates:ro + - ./infrastructure/nginx/etc/nginx.conf:/etc/nginx/nginx.conf:ro - .:/app environment: - NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME}-phpfpm-1:9000 + NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME}-phpfpm-1 + NGINX_FPM_PORT: 9000 NGINX_WEB_ROOT: /app/public NGINX_PORT: 8080 NGINX_MAX_BODY_SIZE: 5M + NGINX_SET_REAL_IP_FROM: 172.16.0.0/12 + healthcheck: + # $$NGINX_PORT escapes compose-time interpolation; the variable is + # expanded by the shell inside the container against the service's + # environment. + test: + [ + "CMD-SHELL", + 'wget --quiet --tries=1 --spider "http://localhost:$$NGINX_PORT/health"', + ] + interval: 5s + timeout: 3s + retries: 6 + start_period: 5s labels: - "traefik.enable=true" - "traefik.docker.network=frontend" diff --git a/infrastructure/Readme.md b/infrastructure/Readme.md new file mode 100644 index 000000000..948f80de5 --- /dev/null +++ b/infrastructure/Readme.md @@ -0,0 +1,68 @@ +# OS2display image build + +This folder contains the infrastructure files for building the `os2display/*` images. + +Two images are built: + +- `os2display/display-api-service`: The web (php-fpm) application image +- `os2display/display-api-service-nginx`: The API (nginx) server image + +## Github Actions + +Both images are built automatically on push to the `develop` branch, and on tag creation. They are tagged with the +same version as the application. + +## GitHub Container Registry + +Images are published to . + +## Building images locally + +Two Taskfile entries wrap the build script: + +```sh +task images:build # build + load into the local docker daemon (host platform) +task images:push # build multi-arch + push to GHCR (prompts for confirmation) +``` + +`task images:push` prompts before publishing because the action is non-reversible. The underlying script is +`infrastructure/build.sh`; running it directly defaults to local load. Set `PUSH=1` to push. + +## Build process + +The two images are built sequentially in the same job; the nginx image layers on top of the just-published API +image as a `FROM` stage. The full pipeline: + +1. **Build the assets stage (`assets_builder`).** Runs `npm ci` against the lockfile, then `npm run build`. + Vite emits the bundled JS/CSS to `/app/public/build/`. + *Why:* the API image needs the vite manifest in place before its `composer install` triggers + `cache:clear` — the symfony-vite bundle generates cache config files from the manifest at that point. + +2. **Build the API stage (`api_app_builder`).** Two-pass composer install: + 1. `composer install --no-dev --no-scripts` against `composer.lock` only — populates `vendor/` in a layer + that **stays cached as long as the lockfile is unchanged**, even if all the source code changes. + 2. After the full source tree and the vite manifest are copied in, `composer install` runs again with + scripts so Symfony's auto-scripts (`cache:clear`, `assets:install`) finalise the autoloader and config + cache. + + `release.json` is written into `public/` after the second install, with the `APP_VERSION`, + `APP_RELEASE_TIMESTAMP`, and `APP_RELEASE_TIME` build args. + *Why:* the screen client polls `/release.json` to detect when it should refresh. + +3. **Assemble the production API image.** Copies `vendor/`+source from `api_app_builder`, drops in `composer` + (needed at container start by the entrypoint to `composer dump-env prod`), the Prometheus php-fpm exporter, + and `docker-entrypoint.sh`. `ENTRYPOINT` runs `dump-env` + `cache:warmup` against the operator's runtime env + before exec'ing `CMD ["php-fpm"]`. + +4. **Push the API image.** `task images:push` (or the workflow) pushes the multi-arch manifest to + `ghcr.io/os2display/display-api-service:` *before* the nginx stage runs. + *Why:* the nginx image's `FROM` references this published image — multi-arch manifests resolve per + platform, so the arm64 nginx build copies arm64 contents and amd64 copies amd64 without QEMU re-execution + of npm/composer. + +5. **Build and push the nginx image.** Layers on `ghcr.io/os2display/display-api-service:` as the `app` + stage, `COPY`s `/var/www/html/public/` out of it, drops in the nginx config, and exposes a `/health` + endpoint. Pushed to `ghcr.io/os2display/display-api-service-nginx:`. + +For local `task images:build`, steps 4 and 5 run against the local docker daemon instead of the registry — +BuildKit reuses the locally-loaded API image for nginx's `FROM` without a network pull. diff --git a/infrastructure/build.sh b/infrastructure/build.sh new file mode 100755 index 000000000..73c900b0a --- /dev/null +++ b/infrastructure/build.sh @@ -0,0 +1,52 @@ +#!/bin/sh +set -eux + +APP_VERSION="${APP_VERSION:-develop}" +REGISTRY="${REGISTRY:-ghcr.io/os2display}" +RELEASE_TIMESTAMP="$(date +%s)" +RELEASE_TIME="$(date -u)" + +# Default: build for the host platform and load into the local docker daemon. +# Set PUSH=1 to build multi-arch and push to the registry; opting in keeps a +# bare `sh build.sh` from accidentally publishing. +if [ "${PUSH:-0}" = "1" ]; then + PLATFORMS="--platform=linux/amd64,linux/arm64" + OUTPUT="--push" + NGINX_PULL="--pull" +else + PLATFORMS="" + OUTPUT="--load" + # In load mode the nginx FROM points at the API image we just loaded into + # the local daemon. --pull would force a registry lookup that fails (and + # has no business pulling the just-built image anyway). + NGINX_PULL="" +fi + +# API (php-fpm) image. Context is the API infra dir so the Dockerfile picks +# up docker-entrypoint.sh; repository-root is provided as a named build context +# for the COPY --from=repository-root steps. +# shellcheck disable=SC2086 +docker buildx build \ + ${PLATFORMS} \ + --pull ${OUTPUT} \ + --build-context repository-root=. \ + --build-arg APP_VERSION="${APP_VERSION}" \ + --build-arg APP_RELEASE_TIMESTAMP="${RELEASE_TIMESTAMP}" \ + --build-arg APP_RELEASE_TIME="${RELEASE_TIME}" \ + --tag "${REGISTRY}/display-api-service:${APP_VERSION}" \ + --file infrastructure/display-api-service/Dockerfile \ + infrastructure/display-api-service + +# Nginx image layers on the just-built API image (single source of truth for +# public/), so it has no builder stages of its own. With --push the FROM +# resolves against the registry; with --load the API tag is in the local +# daemon and BuildKit reuses it without a network pull. +# shellcheck disable=SC2086 +docker buildx build \ + ${PLATFORMS} \ + ${NGINX_PULL} ${OUTPUT} \ + --build-arg APP_VERSION="${APP_VERSION}" \ + --build-arg APP_IMAGE="${REGISTRY}/display-api-service" \ + --tag "${REGISTRY}/display-api-service-nginx:${APP_VERSION}" \ + --file infrastructure/nginx/Dockerfile \ + infrastructure/nginx diff --git a/infrastructure/display-api-service/Dockerfile b/infrastructure/display-api-service/Dockerfile new file mode 100644 index 000000000..32632aa55 --- /dev/null +++ b/infrastructure/display-api-service/Dockerfile @@ -0,0 +1,124 @@ +################################ +# Assets build +################################ +FROM node:24-alpine AS assets_builder +LABEL maintainer="ITK Dev " + +ARG APP_VERSION="develop" + +WORKDIR /app + +# Lockfile + vite config first so the npm install layer caches across +# unrelated source edits. +COPY --from=repository-root package.json package-lock.json vite.config.js ./ +RUN npm ci --no-audit --no-fund + +# Source for `npm run build`. Vite emits to /app/public/build, which the +# api_app_builder stage copies out. Files outside of `assets/` (the vite +# entrypoints) are not consumed by the build and don't belong here — +# `public/client/online-check/` and the `public/media/thumbnail_*.png` +# fallbacks reach the final image via the api_app_builder's repo-root +# copy below. +COPY --from=repository-root assets/ ./assets/ +RUN npm run build + + +################################ +# API backend build +################################ +# itkdev/php8.4-fpm publishes two parallel series: `2.x.y` (Ubuntu), +# tracked by `:latest`, and `alpine-1.4.x` (Alpine), tracked by +# `:alpine`. `:alpine` is therefore a moving alias to the newest +# `alpine-1.4.x` — convenient, but not reproducible across time. +# TODO: pin to a specific `alpine-1.4.x` tag (or a digest) once we +# settle on a baseline. Same caveat applies to the final stage below. +FROM itkdev/php8.4-fpm:alpine AS api_app_builder +LABEL maintainer="ITK Dev " + +ARG APP_VERSION="develop" +ARG APP_RELEASE_TIMESTAMP=0 +ARG APP_RELEASE_TIME="" + +USER root + +# Pinned via the official composer image so the build doesn't depend on +# whatever composer happens to ship in the FPM base. +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +WORKDIR /var/www/html + +USER deploy + +# Two-pass composer install: +# 1. Lockfile + composer.json only with --no-scripts. The vendor/ layer +# then survives unrelated source-code edits. +# 2. After the full source COPY, re-run with scripts to finalise the +# autoloader and trigger Symfony's cache:clear hook. +COPY --chown=deploy:deploy --from=repository-root composer.json composer.lock symfony.lock ./ +RUN APP_ENV=prod composer install --no-dev -o --classmap-authoritative --no-scripts + +# Brings in the full source tree, including the static assets the screen +# client needs at runtime (public/client/online-check/, the +# public/media/thumbnail_*.png fallbacks). .dockerignore filters out the +# rest of public/media/ — see the file for the preserved entries. +COPY --chown=deploy:deploy --from=repository-root ./ ./ + +# Vite manifest must be in place before the second composer install +# triggers cache:clear, otherwise the vite bundle can't generate its +# cache config files. +# @see https://symfony-vite.pentatrion.com/guide/performance.html#caching-configuration-files-%F0%9F%8F%83 +COPY --chown=deploy:deploy --from=assets_builder /app/public/build ./public/build + +RUN APP_ENV=prod composer install --no-dev --optimize-autoloader --classmap-authoritative + +# The client polls /release.json to detect when a screen should refresh; +# shape per docs/release-example.json. +RUN printf '{"releaseTimestamp": %s, "releaseTime": "%s", "releaseVersion": "%s"}\n' \ + "${APP_RELEASE_TIMESTAMP}" \ + "${APP_RELEASE_TIME}" \ + "${APP_VERSION}" \ + > public/release.json + + +################################ +# PHP-FPM (API) production image +################################ +FROM itkdev/php8.4-fpm:alpine +LABEL maintainer="ITK Dev " + +ENV APP_ENV=prod \ + PHP_OPCACHE_ENABLED=1 \ + PHP_OPCACHE_VALIDATE_TIMESTAMPS=0 \ + PHP_OPCACHE_MAX_ACCELERATED_FILES=20000 \ + PHP_OPCACHE_MEMORY_CONSUMPTION=256 \ + PHP_PM_TYPE="dynamic" \ + PHP_PM_MAX_CHILDREN="24" \ + PHP_PM_MAX_REQUESTS="0" \ + PHP_PM_START_SERVERS="5" \ + PHP_PM_MIN_SPARE_SERVERS="5" \ + PHP_PM_MAX_SPARE_SERVERS="10" + +USER root + +# Needed at container start so the entrypoint can run +# `composer dump-env prod` against the operator's runtime env. +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +# Prometheus exporter for php-fpm pool stats; scraped by ops monitoring. +COPY --from=hipages/php-fpm_exporter:2.2.0 /php-fpm_exporter /usr/local/bin/php-fpm_exporter + +COPY --chmod=0755 docker-entrypoint.sh /usr/local/bin/ + +USER deploy + +WORKDIR /var/www/html + +COPY --chown=deploy:deploy --from=api_app_builder /var/www/html . + +# Mount point for the Symfony secrets vault. +RUN mkdir -p ./config/secrets + +# Entrypoint compiles env vars to .env.local.php and warms the Symfony +# cache before exec'ing CMD; CMD stays overridable for ad-hoc commands. +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["php-fpm"] diff --git a/infrastructure/display-api-service/docker-entrypoint.sh b/infrastructure/display-api-service/docker-entrypoint.sh new file mode 100644 index 000000000..95309a935 --- /dev/null +++ b/infrastructure/display-api-service/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +set -eu + +## Dump dotenv files into PHP for better performance. +## @see https://symfony.com/doc/6.4/configuration.html#configuring-environment-variables-in-production +composer dump-env prod + +## Warm-up Symfony cache (with the current configuration). +/var/www/html/bin/console --env=prod cache:warmup + +exec "$@" diff --git a/infrastructure/itkdev/Readme.md b/infrastructure/itkdev/Readme.md deleted file mode 100644 index abf2b9d1e..000000000 --- a/infrastructure/itkdev/Readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# ITK-development image build - -This folder contains the infrastructure files for building the `itkdev/*` images diff --git a/infrastructure/itkdev/display-api-service/Dockerfile b/infrastructure/itkdev/display-api-service/Dockerfile deleted file mode 100644 index b30ecff1a..000000000 --- a/infrastructure/itkdev/display-api-service/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -FROM itkdev/php8.3-fpm:alpine AS APP_BUILDER -LABEL maintainer="ITK Dev " - -ARG APP_VERSION="develop" -ENV APP_PATH=/var/www/html - -USER root - -# Move site into the container. -ADD https://github.com/os2display/display-api-service/archive/${APP_VERSION}.tar.gz /tmp/app.tar -RUN tar -zxf /tmp/app.tar --strip-components=1 -C ${APP_PATH} \ - && rm /tmp/app.tar - -# Add composer in from the official composer image (also alpine). -COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer - -WORKDIR ${APP_PATH} - -## Install assets, which requires a HACK as redis is not available (should be removed later on). -RUN COMPOSER_ALLOW_SUPERUSER=1 APP_ENV=prod composer install --no-dev -o --classmap-authoritative - -RUN rm -rf infrastructure - -#### -## Build main application image. -#### -FROM itkdev/php8.3-fpm:alpine -LABEL maintainer="ITK Dev " - -ENV APP_PATH=/var/www/html \ - COMPOSER_VERSION=2 - -USER root - -# Add composer needed to run optimizations after config is loaded. -COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer - -# Install the application. -COPY --from=APP_BUILDER ${APP_PATH} ${APP_PATH} -RUN mkdir -p ${APP_PATH}/config/secrets \ - && chown -R deploy:deploy ${APP_PATH} - -# Download Prometheus php-fpm export. -COPY --from=hipages/php-fpm_exporter:1.1.1 /php-fpm_exporter /usr/local/bin/php-fpm_exporter - -# Copy configuration. -COPY etc /etc/ - -# Install configuration template handler -ADD https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 /usr/local/bin/confd -RUN chmod +x /usr/local/bin/confd - -# Copy confd onfiguration. -COPY etc /etc/ - -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -USER deploy - -WORKDIR ${APP_PATH} - -CMD [ "docker-entrypoint.sh" ] diff --git a/infrastructure/itkdev/display-api-service/docker-entrypoint.sh b/infrastructure/itkdev/display-api-service/docker-entrypoint.sh deleted file mode 100644 index 306e9f13d..000000000 --- a/infrastructure/itkdev/display-api-service/docker-entrypoint.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -set -eux - -## Run templates with configuration. -/usr/local/bin/confd --onetime --backend env --confdir /etc/confd - -## Bump env.local into PHP for better performance. -composer dump-env prod - -## Warm-up Symfony cache (with the current configuration). -/var/www/html/bin/console --env=prod cache:warmup - -## Set selected composer version. Default version 2. -if [ ! -z "${COMPOSER_VERSION}" ]; then - if [ "${COMPOSER_VERSION}" = "1" ]; then - ln -fs /usr/bin/composer1 /home/deploy/bin/composer - else - ln -fs /usr/bin/composer2 /home/deploy/bin/composer - fi -else - ln -fs /usr/bin/composer2 /home/deploy/bin/composer -fi - -exec php-fpm "$@" diff --git a/infrastructure/itkdev/display-api-service/etc/confd/conf.d/env.local.toml b/infrastructure/itkdev/display-api-service/etc/confd/conf.d/env.local.toml deleted file mode 100644 index d614cc69a..000000000 --- a/infrastructure/itkdev/display-api-service/etc/confd/conf.d/env.local.toml +++ /dev/null @@ -1,8 +0,0 @@ -[template] -src = "env.local.tmpl" -dest = "/var/www/html/.env.local" -owner = "deploy" -mode = "0644" -keys = [ - "/app-config" -] diff --git a/infrastructure/itkdev/display-api-service/etc/confd/templates/env.local.tmpl b/infrastructure/itkdev/display-api-service/etc/confd/templates/env.local.tmpl deleted file mode 100644 index 0307f5610..000000000 --- a/infrastructure/itkdev/display-api-service/etc/confd/templates/env.local.tmpl +++ /dev/null @@ -1,76 +0,0 @@ -###> symfony/framework-bundle ### -APP_ENV={{ getenv "APP_ENV" "prod" }} -APP_SECRET={{ getenv "APP_SECRET" "MySuperSecret" }} -TRUSTED_PROXIES={{ getenv "APP_TRUSTED_PROXIES" "127.0.0.1,REMOTE_ADDR" }} -###< symfony/framework-bundle ### - -###> doctrine/doctrine-bundle ### -DATABASE_URL={{ getenv "APP_DATABASE_URL" "mysql://db:db@mariadb:3306/db?serverVersion=mariadb-10.4.0" }} -###< doctrine/doctrine-bundle ### - -###> nelmio/cors-bundle ### -CORS_ALLOW_ORIGIN={{ getenv "APP_CORS_ALLOW_ORIGIN" "'^https?://localhost(:[0-9]+)?$'" }} -###< nelmio/cors-bundle ### - -###> App ### -APP_DEFAULT_DATE_FORMAT='{{ getenv "APP_DEFAULT_DATE_FORMAT" "Y-m-d\\TH:i:s\\Z" }}' -APP_ACTIVATION_CODE_EXPIRE_INTERVAL='{{ getenv "APP_ACTIVATION_CODE_EXPIRE_INTERVAL" "P2D" }}' -###< App ### - -###> lexik/jwt-authentication-bundle ### -JWT_PASSPHRASE={{ getenv "APP_JWT_PASSPHRASE" }} -JWT_TOKEN_TTL={{ getenv "APP_JWT_TOKEN_TTL" "3600" }} -JWT_SCREEN_TOKEN_TTL={{ getenv "APP_JWT_SCREEN_TOKEN_TTL" "3600" }} -###< lexik/jwt-authentication-bundle ### - -###> gesdinet/jwt-refresh-token-bundle ### -JWT_REFRESH_TOKEN_TTL={{ getenv "APP_JWT_REFRESH_TOKEN_TTL" "2592000" }} -JWT_SCREEN_REFRESH_TOKEN_TTL={{ getenv "APP_JWT_SCREEN_REFRESH_TOKEN_TTL" "2592000" }} -###< gesdinet/jwt-refresh-token-bundle ### - -###> itk-dev/openid-connect-bundle ### -# internal provider -INTERNAL_OIDC_METADATA_URL={{ getenv "APP_INTERNAL_OIDC_METADATA_URL" "" }} -INTERNAL_OIDC_CLIENT_ID={{ getenv "APP_INTERNAL_OIDC_CLIENT_ID" "" }} -INTERNAL_OIDC_CLIENT_SECRET={{ getenv "APP_INTERNAL_OIDC_CLIENT_SECRET" "" }} -INTERNAL_OIDC_REDIRECT_URI={{ getenv "APP_INTERNAL_OIDC_REDIRECT_URI" "" }} -INTERNAL_OIDC_LEEWAY={{ getenv "APP_INTERNAL_OIDC_LEEWAY" "30" }} - -# external provider -EXTERNAL_OIDC_METADATA_URL={{ getenv "APP_EXTERNAL_OIDC_METADATA_URL" "" }} -EXTERNAL_OIDC_CLIENT_ID={{ getenv "APP_EXTERNAL_OIDC_CLIENT_ID" "" }} -EXTERNAL_OIDC_CLIENT_SECRET={{ getenv "APP_EXTERNAL_OIDC_CLIENT_SECRET" "" }} -EXTERNAL_OIDC_REDIRECT_URI={{ getenv "APP_EXTERNAL_OIDC_REDIRECT_URI" "" }} -EXTERNAL_OIDC_LEEWAY={{ getenv "APP_EXTERNAL_OIDC_LEEWAY" "30" }} -EXTERNAL_OIDC_HASH_SALT={{ getenv "APP_EXTERNAL_OIDC_HASH_SALT" "" }} - -CLI_REDIRECT={{ getenv "APP_CLI_REDIRECT" "" }} -###< itk-dev/openid-connect-bundle ### - -###> redis ### -REDIS_CACHE_PREFIX={{ getenv "APP_CLI_REDIRECT" "DisplayApiService" }} -REDIS_CACHE_DSN={{ getenv "APP_CLI_REDIRECT" "redis://redis:6379/0" }} -###< redis ### - -###> Calendar Api Feed Source ### -CALENDAR_API_FEED_SOURCE_LOCATION_ENDPOINT={{ getenv "APP_CALENDAR_API_FEED_SOURCE_LOCATION_ENDPOINT" "" }} -CALENDAR_API_FEED_SOURCE_RESOURCE_ENDPOINT={{ getenv "APP_CALENDAR_API_FEED_SOURCE_RESOURCE_ENDPOINT" "" }} -CALENDAR_API_FEED_SOURCE_EVENT_ENDPOINT={{ getenv "APP_CALENDAR_API_FEED_SOURCE_EVENT_ENDPOINT" "" }} -CALENDAR_API_FEED_SOURCE_CUSTOM_MAPPINGS={{ getenv "APP_CALENDAR_API_FEED_SOURCE_CUSTOM_MAPPINGS" "'{}'" }} -CALENDAR_API_FEED_SOURCE_EVENT_MODIFIERS={{ getenv "APP_CALENDAR_API_FEED_SOURCE_EVENT_MODIFIERS" "'{}'" }} -CALENDAR_API_FEED_SOURCE_DATE_FORMAT={{ getenv "APP_CALENDAR_API_FEED_SOURCE_DATE_FORMAT" "" }} -CALENDAR_API_FEED_SOURCE_DATE_TIMEZONE={{ getenv "APP_CALENDAR_API_FEED_SOURCE_DATE_TIMEZONE" "" }} -CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_SECONDS={{ getenv "APP_CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_SECONDS" "300" }} -###< Calendar Api Feed Source ### - -EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS={{ getenv "APP_EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS" "300" }} - -###> Http Client ### -HTTP_CLIENT_TIMEOUT={{ getenv "APP_HTTP_CLIENT_TIMEOUT" "5" }} -HTTP_CLIENT_MAX_DURATION={{ getenv "APP_HTTP_CLIENT_MAX_DURATION" "30" }} -###< Http Client ### - -TRACK_SCREEN_INFO={{ getenv "APP_TRACK_SCREEN_INFO" "false" }} -TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS={{ getenv "APP_TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS" "300" }} - -APP_KEY_VAULT_JSON={{ getenv "APP_KEY_VAULT_JSON" "{}" }} diff --git a/infrastructure/itkdev/nginx/Dockerfile b/infrastructure/itkdev/nginx/Dockerfile deleted file mode 100644 index 7a0334dd9..000000000 --- a/infrastructure/itkdev/nginx/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -ARG APP_VERSION="develop" -FROM itkdev/os2display-api-service:${APP_VERSION} as APPLICATION - -FROM nginxinc/nginx-unprivileged:alpine -LABEL maintainer="ITK Dev " -ARG UID=101 -ARG GID=101 - -ENV APP_PATH=/var/www/html - -USER root - -RUN mkdir -p ${APP_PATH}/public - -COPY --from=APPLICATION ${APP_PATH}/public ${APP_PATH}/public - -WORKDIR ${APP_PATH} - -# Copy configuration. -COPY etc/ /etc/ - -# Install configuration template handler -ADD https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 /usr/local/bin/confd -RUN chmod +x /usr/local/bin/confd - -COPY docker-entrypoint.sh / -RUN chmod +x /docker-entrypoint.sh \ - && chown -R $UID:0 ${APP_PATH} \ - && chmod -R g+w ${APP_PATH} - -USER $UID - -EXPOSE 8080 - -ENTRYPOINT [ "/docker-entrypoint.sh" ] - -CMD ["nginx", "-g", "daemon off;"] diff --git a/infrastructure/itkdev/nginx/docker-entrypoint.sh b/infrastructure/itkdev/nginx/docker-entrypoint.sh deleted file mode 100644 index 978fa9f4e..000000000 --- a/infrastructure/itkdev/nginx/docker-entrypoint.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh - -set -eux - -## Run templates with configuration. -/usr/local/bin/confd --onetime --backend env --confdir /etc/confd - -entrypoint_log() { - if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "$@" - fi -} - -if [ "$1" = "nginx" -o "$1" = "nginx-debug" ]; then - if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then - entrypoint_log "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration" - - entrypoint_log "$0: Looking for shell scripts in /docker-entrypoint.d/" - find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do - case "$f" in - *.envsh) - if [ -x "$f" ]; then - entrypoint_log "$0: Sourcing $f"; - . "$f" - else - # warn on shell scripts without exec bit - entrypoint_log "$0: Ignoring $f, not executable"; - fi - ;; - *.sh) - if [ -x "$f" ]; then - entrypoint_log "$0: Launching $f"; - "$f" - else - # warn on shell scripts without exec bit - entrypoint_log "$0: Ignoring $f, not executable"; - fi - ;; - *) entrypoint_log "$0: Ignoring $f";; - esac - done - - entrypoint_log "$0: Configuration complete; ready for start up" - else - entrypoint_log "$0: No files found in /docker-entrypoint.d/, skipping configuration" - fi -fi - -exec "$@" diff --git a/infrastructure/itkdev/nginx/etc/confd/conf.d/default.conf.toml b/infrastructure/itkdev/nginx/etc/confd/conf.d/default.conf.toml deleted file mode 100644 index 185954f0d..000000000 --- a/infrastructure/itkdev/nginx/etc/confd/conf.d/default.conf.toml +++ /dev/null @@ -1,7 +0,0 @@ -[template] -src = "default.conf.tmpl" -dest = "/etc/nginx/conf.d/default.conf" -mode = "0644" -keys = [ - "/nginx-config" -] diff --git a/infrastructure/itkdev/nginx/etc/confd/conf.d/nginx.conf.toml b/infrastructure/itkdev/nginx/etc/confd/conf.d/nginx.conf.toml deleted file mode 100644 index 7c81a661f..000000000 --- a/infrastructure/itkdev/nginx/etc/confd/conf.d/nginx.conf.toml +++ /dev/null @@ -1,7 +0,0 @@ -[template] -src = "nginx.conf.tmpl" -dest = "/etc/nginx/nginx.conf" -mode = "0644" -keys = [ - "/nginx-config" -] diff --git a/infrastructure/itkdev/nginx/etc/confd/templates/default.conf.tmpl b/infrastructure/itkdev/nginx/etc/confd/templates/default.conf.tmpl deleted file mode 100644 index b6f32099b..000000000 --- a/infrastructure/itkdev/nginx/etc/confd/templates/default.conf.tmpl +++ /dev/null @@ -1,52 +0,0 @@ -# @see https://symfony.com/doc/current/setup/web_server_configuration.html -server { - listen 8080; - server_name localhost; - root /var/www/html/public; - - location / { - add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; - - # try to serve file directly, fallback to index.php - try_files $uri /index.php$is_args$args; - } - - location = /robots.txt { - add_header Content-Type text/plain; - add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; - return 200 "User-agent: *\nDisallow: /\n"; - } - - location ~ ^/index\.php(/|$) { - fastcgi_pass {{ getenv "PHP_FPM_SERVER" "phpfpm" }}:{{ getenv "PHP_FPM_SERVER_PORT" "9000" }}; - fastcgi_split_path_info ^(.+\.php)(/.*)$; - include fastcgi_params; - - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $realpath_root; - - internal; - } - - # return 404 for all other php files not matching the front controller - # this prevents access to other php files you don't want to be accessible. - location ~ \.php$ { - return 404; - } - - error_log /var/log/nginx/error.log; - access_log /var/log/nginx/access.log; -} - -server { - listen 8080; - server_name localhost; - root /var/www/html/public; - - error_log off; - access_log off; - - location /stub_status { - stub_status; - } -} diff --git a/infrastructure/itkdev/nginx/etc/confd/templates/nginx.conf.tmpl b/infrastructure/itkdev/nginx/etc/confd/templates/nginx.conf.tmpl deleted file mode 100644 index 29d5283af..000000000 --- a/infrastructure/itkdev/nginx/etc/confd/templates/nginx.conf.tmpl +++ /dev/null @@ -1,52 +0,0 @@ -worker_processes auto; - -error_log /var/log/nginx/error.log warn; -pid /tmp/nginx.pid; - -events { - worker_connections 2048; - multi_accept on; -} - - -http { - open_file_cache max=10000 inactive=5m; - open_file_cache_valid 5m; - open_file_cache_min_uses 5; - open_file_cache_errors off; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 15 15; - types_hash_max_size 2048; - - server_tokens off; - - client_max_body_size {{ getenv "NGINX_FPM_UPLOAD_MAX" "8M" }}; - - gzip on; - gzip_disable "msie6"; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.0; - gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - set_real_ip_from 172.16.0.0/8; - real_ip_recursive on; - real_ip_header X-Forwarded-For; - - log_format main '$http_x_real_ip - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - error_log /dev/stderr; - access_log /dev/stdout main; - - include /etc/nginx/conf.d/*.conf; -} diff --git a/infrastructure/itkdev/run.sh b/infrastructure/itkdev/run.sh deleted file mode 100755 index b028f5cdc..000000000 --- a/infrastructure/itkdev/run.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -set -eux - -APP_VERSION=develop - -docker pull nginxinc/nginx-unprivileged:alpine - -docker build --pull --no-cache --build-arg APP_VERSION=${APP_VERSION} --tag=itkdev/os2display-api-service:${APP_VERSION} --file="display-api-service/Dockerfile" display-api-service -docker build --no-cache --build-arg VERSION=${APP_VERSION} --tag=itkdev/os2display-api-service-nginx:${APP_VERSION} --file="nginx/Dockerfile" nginx - -# docker push itkdev/os2display-api-service:${APP_VERSION} -# docker push itkdev/os2display-api-service-nginx:${APP_VERSION} diff --git a/infrastructure/nginx/Dockerfile b/infrastructure/nginx/Dockerfile new file mode 100644 index 000000000..e7ba89493 --- /dev/null +++ b/infrastructure/nginx/Dockerfile @@ -0,0 +1,43 @@ +ARG APP_VERSION="develop" +ARG APP_IMAGE="ghcr.io/os2display/display-api-service" + +# Pull the API image that was built and pushed in the preceding step. +# Multi-arch manifests resolve per platform, so the arm64 nginx build +# copies arm64 contents and amd64 copies amd64 — no QEMU re-execution +# of npm/composer for the second image. +FROM ${APP_IMAGE}:${APP_VERSION} AS app + + +######## Nginx production image ######## +FROM nginxinc/nginx-unprivileged:alpine +LABEL maintainer="ITK Dev " + +ARG UID=101 + +# Defaults for the envsubst pass over /etc/nginx/templates/default.conf.template +# at container start; every value below is referenced from that template. +ENV NGINX_PORT=8080 \ + NGINX_FPM_SERVICE=os2display \ + NGINX_FPM_PORT=9000 \ + NGINX_MAX_BODY_SIZE=140m \ + NGINX_SET_REAL_IP_FROM=172.16.0.0/12 \ + NGINX_WEB_ROOT=/var/www/html/public + +WORKDIR /var/www/html + +# Single source of truth: public/ (incl. build/ and release.json) comes +# from the just-published API image, not a duplicated builder stage. +COPY --from=app --chown=$UID:0 --chmod=775 /var/www/html/public ./public + +# Copy nginx configuration +COPY --chown=$UID:0 etc /etc/nginx + +# Add health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:${NGINX_PORT}/health || exit 1 + +USER $UID + +EXPOSE ${NGINX_PORT} + +CMD ["nginx", "-g", "daemon off;"] diff --git a/infrastructure/nginx/etc/nginx.conf b/infrastructure/nginx/etc/nginx.conf new file mode 100644 index 000000000..2fc6396f0 --- /dev/null +++ b/infrastructure/nginx/etc/nginx.conf @@ -0,0 +1,77 @@ +# Note: 'user' directive is not needed in nginx-unprivileged image +worker_processes auto; +worker_rlimit_nofile 65535; + +error_log /dev/stderr notice; +pid /tmp/nginx.pid; + +events { + worker_connections 2048; + use epoll; + multi_accept on; +} + +http { + open_file_cache max=10000 inactive=5m; + open_file_cache_valid 5m; + open_file_cache_min_uses 5; + open_file_cache_errors off; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 15 15; + types_hash_max_size 2048; + + server_tokens off; + + # Compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/x-javascript + application/json + application/xml + application/xml+rss + application/rss+xml + application/atom+xml + image/svg+xml + font/truetype + font/opentype + application/vnd.ms-fontobject + application/font-woff + application/font-woff2; + gzip_disable "msie6"; + gzip_proxied any; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + # Rate limiting zones + limit_req_zone $binary_remote_addr zone=php_limit:10m rate=10r/s; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$http_x_real_ip - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + error_log /dev/stderr; + access_log /dev/stdout main; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/infrastructure/nginx/etc/templates/default.conf.template b/infrastructure/nginx/etc/templates/default.conf.template new file mode 100644 index 000000000..4b574d402 --- /dev/null +++ b/infrastructure/nginx/etc/templates/default.conf.template @@ -0,0 +1,148 @@ +upstream api_backend { + server ${NGINX_FPM_SERVICE}:${NGINX_FPM_PORT}; + keepalive 16; +} + +server { + listen ${NGINX_PORT}; + listen [::]:${NGINX_PORT}; + server_name localhost; + + root ${NGINX_WEB_ROOT}; + index index.php index.html; + + # Health check endpoint + location = /health { + access_log off; + add_header Content-Type text/plain; + return 200 "healthy\n"; + } + + # Upload limits + client_max_body_size ${NGINX_MAX_BODY_SIZE}; + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + # frame-ancestors is the modern primary control; X-Frame-Options is + # defense-in-depth for pre-CSP-2 browsers (#390). + add_header Content-Security-Policy "frame-ancestors 'self'" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Real IP from reverse proxy + set_real_ip_from ${NGINX_SET_REAL_IP_FROM}; + real_ip_recursive on; + real_ip_header X-Forwarded-For; + + location / { + # Prevent indexing of API routes + add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; + + # try to serve file directly, fallback to index.php + try_files $uri /index.php$is_args$args; + } + + # Cache static assets built by Vite under /public/build + location /build/ { + access_log off; + log_not_found off; + + # Serve files directly + try_files $uri =404; + + # Long-term caching for fingerprinted assets + add_header Cache-Control "public, max-age=31536000, immutable" always; + add_header Access-Control-Allow-Origin "*" always; + add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always; + expires 1y; + } + + # Static files with caching + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + try_files $uri /index.php$is_args$args; + } + + # Screen client online check should just serve static files + location /client/online-check { + index index.html index.htm; + } + + location = /robots.txt { + add_header Content-Type text/plain; + add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; + return 200 "User-agent: *\nDisallow: /\n"; + } + + # Protect sensitive file extensions + location ~* \.(engine|inc|install|make|module|profile|po|sh|sql|tar|gz|bz2|theme|twig|tpl|xtmpl|yml)$ { + deny all; + return 404; + } + + # Protect backup and temporary files + location ~* \.(bak|orig|save|sw[op])$ { + deny all; + return 404; + } + + # Protect dotfiles except .well-known + location ~ /\.(?!well-known) { + deny all; + return 404; + } + + # Protect specific files + location ~ ^/(composer\.(json|lock)|web\.config)$ { + deny all; + return 404; + } + + # PHP-FPM handling + location ~ ^/index\.php(/|$) { + fastcgi_pass api_backend; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + include fastcgi_params; + + limit_req zone=php_limit burst=20 nodelay; + + # Increase buffer sizes for large responses + fastcgi_buffers 16 32k; + fastcgi_buffer_size 64k; + fastcgi_busy_buffers_size 64k; + + # Timeout settings + fastcgi_connect_timeout 60s; + fastcgi_send_timeout 60s; + fastcgi_read_timeout 60s; + + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + + internal; + } + + # return 404 for all other php files not matching the front controller + # this prevents access to other php files you don't want to be accessible. + location ~ \.php$ { + return 404; + } + + # Send log message to files symlinked to stdout/stderr. + sendfile on; + keepalive_timeout 65; + + # Client buffer settings + client_body_buffer_size 128k; + client_header_buffer_size 1k; + large_client_header_buffers 4 16k; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; + gzip_disable "msie6"; +} diff --git a/infrastructure/os2display/Readme.md b/infrastructure/os2display/Readme.md deleted file mode 100644 index def6e8a45..000000000 --- a/infrastructure/os2display/Readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# OS2display image build - -This folder contains the infrastructure files for building the `os2display/*` images diff --git a/infrastructure/os2display/display-api-service/Dockerfile b/infrastructure/os2display/display-api-service/Dockerfile deleted file mode 100644 index 2809349f8..000000000 --- a/infrastructure/os2display/display-api-service/Dockerfile +++ /dev/null @@ -1,155 +0,0 @@ -FROM php:8.3-fpm-alpine -LABEL maintainer="ITK Dev " - -############# SETUP CONTAINER ############# - -USER root - -ARG APP_VERSION="develop" - -ENV APP_PATH=/var/www/html \ - # PHP - TZ="Europe/Copenhagen" \ - PHP_TIMEZONE="Europe/Copenhagen" \ - PHP_MAX_EXECUTION_TIME="30" \ - PHP_MEMORY_LIMIT="128M" \ - PHP_POST_MAX_SIZE="8M" \ - PHP_UPLOAD_MAX_FILESIZE="2M" \ - PHP_USER="www-data" \ - PHP_GROUP="www-data" \ - PHP_SENDMAIL_PATH='/usr/sbin/sendmail -S host.docker.internal -t -i' \ - # OpCache - PHP_OPCACHE_ENABLED="1" \ - PHP_OPCACHE_JIT="off" \ - PHP_OPCACHE_REVALIDATE_FREQ=0 \ - PHP_OPCACHE_VALIDATE_TIMESTAMPS="1" \ - PHP_OPCACHE_MAX_ACCELERATED_FILES="20000" \ - PHP_OPCACHE_MEMORY_CONSUMPTION="64" \ - PHP_OPCACHE_MAX_WASTED_PERCENTAGE="10" \ - # APCU - PHP_APCU_ENABLED=0 \ - PHP_APCU_ENABLED_CLI=0 \ - PHP_APCU_MEMORY_SIZE="16M" \ - PHP_APCU_SEGMENTS=1 \ - PHP_APCU_PRELOAD_PATH='' \ - # FPM pool - PHP_PM_TYPE="static" \ - PHP_PM_MAX_CHILDREN="8" \ - PHP_PM_MAX_REQUESTS="0" \ - PHP_PM_START_SERVERS="5" \ - PHP_PM_MIN_SPARE_SERVERS="5" \ - PHP_PM_MAX_SPARE_SERVERS="8" \ - # Other - PHP_REQUEST_SLOWLOG_TIMEOUT="0" \ - PHP_SLOWLOG="/dev/stderr" \ - COMPOSER_ALLOW_SUPERUSER=1 - -RUN apk upgrade --no-cache --ignore curl -RUN apk --update add --no-cache \ - libxslt-dev \ - libzip-dev \ - libpng-dev \ - gettext-dev \ - git \ - unzip \ - icu-dev \ - icu-data-full \ - openldap-dev \ - libmcrypt-dev \ - mysql-client \ - libmemcached-libs \ - zlib \ - patch \ - tzdata \ - freetype-dev \ - libjpeg-turbo-dev \ - libjpeg-turbo \ - libwebp-dev \ - && docker-php-ext-configure gd --with-freetype --with-webp --with-jpeg \ - && docker-php-ext-install -j$(nproc) \ - bcmath \ - calendar \ - gd \ - gettext \ - intl \ - ldap \ - mysqli \ - opcache \ - pdo_mysql \ - sysvsem \ - soap \ - xsl \ - zip - -# Extension that are not available via ext- -RUN apk --update add --no-cache --virtual .build-deps autoconf g++ make zlib-dev libmemcached-dev cyrus-sasl-dev \ - && pecl channel-update pecl.php.net \ - && pecl install redis memcached apcu \ - && docker-php-ext-enable apcu redis memcached \ - && apk del .build-deps - -# Install AMQP support -RUN apk --update add --no-cache rabbitmq-c -RUN apk --update add --no-cache --virtual .build-deps autoconf g++ make rabbitmq-c-dev \ - && pecl install amqp \ - && docker-php-ext-enable amqp memcached \ - && apk del .build-deps - -# Install composer -COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer - -# Use default PHP production configuration. -RUN mv ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini - -# # Copy custom PHP configuration. -COPY php/opcache.ini ${PHP_INI_DIR}/conf.d/docker-php-ext-opcache.ini -COPY php/php.ini ${PHP_INI_DIR}/conf.d/zz-php.ini -COPY php/apcu.ini ${PHP_INI_DIR}/conf.d/docker-php-ext-apcu.ini - -# Custom FPM configuration. -COPY php/fpm.ini ${PHP_INI_DIR}/../php-fpm.d/zz-fpm-docker.conf - -# Add mhsendmail for mailhog -ADD https://github.com/mailhog/mhsendmail/releases/download/v0.2.0/mhsendmail_linux_amd64 /usr/local/bin/mhsendmail -RUN chmod +x /usr/local/bin/mhsendmail - -# Added FPM health check script (https://github.com/renatomefi/php-fpm-healthcheck) -ADD https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/master/php-fpm-healthcheck /usr/local/bin/php-fpm-healthcheck -RUN chmod +x /usr/local/bin/php-fpm-healthcheck - -# Add git global config -COPY gitconfig /root/.gitconfig - -############# SETUP APPLICATION ############# - -# Move site into the container. -ADD https://github.com/os2display/display-api-service/archive/${APP_VERSION}.tar.gz /tmp/app.tar -RUN tar -zxf /tmp/app.tar --strip-components=1 -C ${APP_PATH} \ - && rm /tmp/app.tar - -## Install assets, which requires a HACK as redis is not available (should be removed later on). -RUN APP_ENV=prod composer install --no-dev -o --classmap-authoritative \ - && rm -rf infrastructure \ - && APP_ENV=prod composer clear-cache - -# Install the application. -RUN mkdir -p ${APP_PATH}/config/secrets \ - && chown -R www-data:www-data ${APP_PATH} - -# Copy configuration. -COPY etc /etc/ - -# Install configuration template handler -ADD https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 /usr/local/bin/confd -RUN chmod +x /usr/local/bin/confd - -# Copy confd onfiguration. -COPY etc /etc/ - -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -WORKDIR ${APP_PATH} - -CMD ["php-fpm"] -ENTRYPOINT [ "docker-entrypoint.sh" ] diff --git a/infrastructure/os2display/display-api-service/docker-entrypoint.sh b/infrastructure/os2display/display-api-service/docker-entrypoint.sh deleted file mode 100644 index 8ad622b4d..000000000 --- a/infrastructure/os2display/display-api-service/docker-entrypoint.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -set -eux - -## Run templates with configuration. -/usr/local/bin/confd --onetime --backend env --confdir /etc/confd - -## Bump env.local into PHP for better performance. -composer dump-env prod - -## Warm-up Symfony cache (with the current configuration). -/var/www/html/bin/console --env=prod cache:warmup - -# first arg is `-f` or `--some-option` -if [ "${1#-}" != "$1" ]; then - set -- php-fpm "$@" -fi - -## Start the PHP FPM process. -echo "Starting PHP 8.3 FPM" - -exec "$@" diff --git a/infrastructure/os2display/display-api-service/etc/confd/conf.d/env.local.toml b/infrastructure/os2display/display-api-service/etc/confd/conf.d/env.local.toml deleted file mode 100644 index e7f303a3e..000000000 --- a/infrastructure/os2display/display-api-service/etc/confd/conf.d/env.local.toml +++ /dev/null @@ -1,8 +0,0 @@ -[template] -src = "env.local.tmpl" -dest = "/var/www/html/.env.local" -owner = "www-data" -mode = "0644" -keys = [ - "/app-config" -] diff --git a/infrastructure/os2display/display-api-service/etc/confd/templates/env.local.tmpl b/infrastructure/os2display/display-api-service/etc/confd/templates/env.local.tmpl deleted file mode 100644 index e1d8c43ea..000000000 --- a/infrastructure/os2display/display-api-service/etc/confd/templates/env.local.tmpl +++ /dev/null @@ -1,76 +0,0 @@ -###> symfony/framework-bundle ### -APP_ENV={{ getenv "APP_ENV" "prod" }} -APP_SECRET={{ getenv "APP_SECRET" "MySuperSecret" }} -TRUSTED_PROXIES={{ getenv "APP_TRUSTED_PROXIES" "127.0.0.1,REMOTE_ADDR" }} -###< symfony/framework-bundle ### - -###> doctrine/doctrine-bundle ### -DATABASE_URL={{ getenv "APP_DATABASE_URL" "mysql://db:db@mariadb:3306/db?serverVersion=mariadb-10.4.0" }} -###< doctrine/doctrine-bundle ### - -###> nelmio/cors-bundle ### -CORS_ALLOW_ORIGIN={{ getenv "APP_CORS_ALLOW_ORIGIN" "'^https?://localhost(:[0-9]+)?$'" }} -###< nelmio/cors-bundle ### - -###> App ### -APP_DEFAULT_DATE_FORMAT='{{ getenv "APP_DEFAULT_DATE_FORMAT" "Y-m-d\\TH:i:s\\Z" }}' -APP_ACTIVATION_CODE_EXPIRE_INTERNAL='{{ getenv "APP_ACTIVATION_CODE_EXPIRE_INTERNAL" "P2D" }}' -###< App ### - -###> lexik/jwt-authentication-bundle ### -JWT_PASSPHRASE={{ getenv "APP_JWT_PASSPHRASE" }} -JWT_TOKEN_TTL={{ getenv "APP_JWT_TOKEN_TTL" "3600" }} -JWT_SCREEN_TOKEN_TTL={{ getenv "APP_JWT_SCREEN_TOKEN_TTL" "3600" }} -###< lexik/jwt-authentication-bundle ### - -###> gesdinet/jwt-refresh-token-bundle ### -JWT_REFRESH_TOKEN_TTL={{ getenv "APP_JWT_REFRESH_TOKEN_TTL" "2592000" }} -JWT_SCREEN_REFRESH_TOKEN_TTL={{ getenv "APP_JWT_SCREEN_REFRESH_TOKEN_TTL" "2592000" }} -###< gesdinet/jwt-refresh-token-bundle ### - -###> itk-dev/openid-connect-bundle ### -# internal provider -INTERNAL_OIDC_METADATA_URL={{ getenv "APP_INTERNAL_OIDC_METADATA_URL" "" }} -INTERNAL_OIDC_CLIENT_ID={{ getenv "APP_INTERNAL_OIDC_CLIENT_ID" "" }} -INTERNAL_OIDC_CLIENT_SECRET={{ getenv "APP_INTERNAL_OIDC_CLIENT_SECRET" "" }} -INTERNAL_OIDC_REDIRECT_URI={{ getenv "APP_INTERNAL_OIDC_REDIRECT_URI" "" }} -INTERNAL_OIDC_LEEWAY={{ getenv "APP_INTERNAL_OIDC_LEEWAY" "30" }} - -# external provider -EXTERNAL_OIDC_METADATA_URL={{ getenv "APP_EXTERNAL_OIDC_METADATA_URL" "" }} -EXTERNAL_OIDC_CLIENT_ID={{ getenv "APP_EXTERNAL_OIDC_CLIENT_ID" "" }} -EXTERNAL_OIDC_CLIENT_SECRET={{ getenv "APP_EXTERNAL_OIDC_CLIENT_SECRET" "" }} -EXTERNAL_OIDC_REDIRECT_URI={{ getenv "APP_EXTERNAL_OIDC_REDIRECT_URI" "" }} -EXTERNAL_OIDC_LEEWAY={{ getenv "APP_EXTERNAL_OIDC_LEEWAY" "30" }} -EXTERNAL_OIDC_HASH_SALT={{ getenv "APP_EXTERNAL_OIDC_HASH_SALT" "" }} - -CLI_REDIRECT={{ getenv "APP_CLI_REDIRECT" "" }} -###< itk-dev/openid-connect-bundle ### - -###> redis ### -REDIS_CACHE_PREFIX={{ getenv "APP_CLI_REDIRECT" "DisplayApiService" }} -REDIS_CACHE_DSN={{ getenv "APP_CLI_REDIRECT" "redis://redis:6379/0" }} -###< redis ### - -###> Calendar Api Feed Source ### -CALENDAR_API_FEED_SOURCE_LOCATION_ENDPOINT={{ getenv "APP_CALENDAR_API_FEED_SOURCE_LOCATION_ENDPOINT" "" }} -CALENDAR_API_FEED_SOURCE_RESOURCE_ENDPOINT={{ getenv "APP_CALENDAR_API_FEED_SOURCE_RESOURCE_ENDPOINT" "" }} -CALENDAR_API_FEED_SOURCE_EVENT_ENDPOINT={{ getenv "APP_CALENDAR_API_FEED_SOURCE_EVENT_ENDPOINT" "" }} -CALENDAR_API_FEED_SOURCE_CUSTOM_MAPPINGS={{ getenv "APP_CALENDAR_API_FEED_SOURCE_CUSTOM_MAPPINGS" "'{}'" }} -CALENDAR_API_FEED_SOURCE_EVENT_MODIFIERS={{ getenv "APP_CALENDAR_API_FEED_SOURCE_EVENT_MODIFIERS" "'{}'" }} -CALENDAR_API_FEED_SOURCE_DATE_FORMAT={{ getenv "APP_CALENDAR_API_FEED_SOURCE_DATE_FORMAT" "" }} -CALENDAR_API_FEED_SOURCE_DATE_TIMEZONE={{ getenv "APP_CALENDAR_API_FEED_SOURCE_DATE_TIMEZONE" "" }} -CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_SECONDS={{ getenv "CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_SECONDS" "300" }} -###< Calendar Api Feed Source ### - -EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS={{ getenv "APP_EVENTDATABASE_API_V2_CACHE_EXPIRE_SECONDS" "300" }} - -###> Http Client ### -HTTP_CLIENT_TIMEOUT={{ getenv "APP_HTTP_CLIENT_TIMEOUT" "5" }} -HTTP_CLIENT_MAX_DURATION={{ getenv "APP_HTTP_CLIENT_MAX_DURATION" "30" }} -###< Http Client ### - -TRACK_SCREEN_INFO={{ getenv "APP_TRACK_SCREEN_INFO" "false" }} -TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS={{ getenv "APP_TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS" "300" }} - -APP_KEY_VAULT_JSON={{ getenv "APP_KEY_VAULT_JSON" "{}" }} diff --git a/infrastructure/os2display/display-api-service/gitconfig b/infrastructure/os2display/display-api-service/gitconfig deleted file mode 100644 index 60a3b0202..000000000 --- a/infrastructure/os2display/display-api-service/gitconfig +++ /dev/null @@ -1,9 +0,0 @@ -[color] - ui = true - -[alias] - branch-name = !git for-each-ref --format='%(refname:short)' `git symbolic-ref HEAD` - lg = log --graph --pretty=format:'%Cred%h%Creset %Cgreen(%cr) -%C(yellow)%d%Creset %s %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative - -[safe] - directory = * \ No newline at end of file diff --git a/infrastructure/os2display/display-api-service/php/apcu.ini b/infrastructure/os2display/display-api-service/php/apcu.ini deleted file mode 100644 index 86164be82..000000000 --- a/infrastructure/os2display/display-api-service/php/apcu.ini +++ /dev/null @@ -1,7 +0,0 @@ -extension=apcu -apc.enabled=${PHP_APCU_ENABLED} -apc.shm_segments=${PHP_APCU_SEGMENTS} -apc.shm_size=${PHP_APCU_MEMORY_SIZE} - -apc.enable_cli=${PHP_APCU_ENABLED_CLI} -apc.preload_path=${PHP_APCU_PRELOAD_PATH} \ No newline at end of file diff --git a/infrastructure/os2display/display-api-service/php/fpm.ini b/infrastructure/os2display/display-api-service/php/fpm.ini deleted file mode 100644 index 49e0aeb48..000000000 --- a/infrastructure/os2display/display-api-service/php/fpm.ini +++ /dev/null @@ -1,17 +0,0 @@ -[www] -pm = ${PHP_PM_TYPE} -pm.max_children = ${PHP_PM_MAX_CHILDREN} -pm.start_servers = ${PHP_PM_START_SERVERS} -pm.min_spare_servers = ${PHP_PM_MIN_SPARE_SERVERS} -pm.max_spare_servers = ${PHP_PM_MAX_SPARE_SERVERS} -pm.max_requests = ${PHP_PM_MAX_REQUESTS} - -request_slowlog_timeout = ${PHP_REQUEST_SLOWLOG_TIMEOUT} -slowlog = ${PHP_SLOWLOG} - -; Enable the FPM status page -pm.status_path = /status - -user = ${PHP_USER} -group = ${PHP_GROUP} - diff --git a/infrastructure/os2display/display-api-service/php/opcache.ini b/infrastructure/os2display/display-api-service/php/opcache.ini deleted file mode 100644 index 8ff178634..000000000 --- a/infrastructure/os2display/display-api-service/php/opcache.ini +++ /dev/null @@ -1,15 +0,0 @@ -zend_extension=opcache.so - -[opcache] -opcache.jit=${PHP_OPCACHE_JIT} - -opcache.enable=${PHP_OPCACHE_ENABLED} -opcache.revalidate_freq=${PHP_OPCACHE_REVALIDATE_FREQ} -opcache.validate_timestamps=${PHP_OPCACHE_VALIDATE_TIMESTAMPS} -opcache.max_accelerated_files=${PHP_OPCACHE_MAX_ACCELERATED_FILES} -opcache.memory_consumption=${PHP_OPCACHE_MEMORY_CONSUMPTION} -opcache.max_wasted_percentage=${PHP_OPCACHE_MAX_WASTED_PERCENTAGE} -opcache.interned_strings_buffer=16 -opcache.fast_shutdown=1 - -opcache.optimization_level=0xFFFFFFEF diff --git a/infrastructure/os2display/display-api-service/php/php.ini b/infrastructure/os2display/display-api-service/php/php.ini deleted file mode 100644 index 2bfde2b40..000000000 --- a/infrastructure/os2display/display-api-service/php/php.ini +++ /dev/null @@ -1,13 +0,0 @@ -realpath_cache_size = 4096k -realpath_cache_ttl = 600 - -expose_php = Off -max_execution_time = ${PHP_MAX_EXECUTION_TIME} -memory_limit = ${PHP_MEMORY_LIMIT} - -post_max_size = ${PHP_POST_MAX_SIZE} -upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE} - -date.timezone = ${PHP_TIMEZONE} - -sendmail_path = ${PHP_SENDMAIL_PATH} diff --git a/infrastructure/os2display/nginx/Dockerfile b/infrastructure/os2display/nginx/Dockerfile deleted file mode 100644 index bc13dcd12..000000000 --- a/infrastructure/os2display/nginx/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -ARG APP_VERSION="develop" -FROM os2display/display-api-service:${APP_VERSION} as APPLICATION - -FROM nginxinc/nginx-unprivileged:alpine -LABEL maintainer="ITK Dev " -ARG UID=101 -ARG GID=101 - -ENV APP_PATH=/var/www/html - -USER root - -RUN mkdir -p ${APP_PATH}/public - -COPY --from=APPLICATION ${APP_PATH}/public ${APP_PATH}/public - -WORKDIR ${APP_PATH} - -# Copy configuration. -COPY etc /etc/ - -# Install configuration template handler -ADD https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 /usr/local/bin/confd -RUN chmod +x /usr/local/bin/confd - -COPY docker-entrypoint.sh / -RUN chmod +x /docker-entrypoint.sh \ - && chown -R $UID:0 ${APP_PATH} \ - && chmod -R g+w ${APP_PATH} - -USER $UID - -EXPOSE 8080 - -ENTRYPOINT [ "/docker-entrypoint.sh" ] - -CMD ["nginx", "-g", "daemon off;"] diff --git a/infrastructure/os2display/nginx/docker-entrypoint.sh b/infrastructure/os2display/nginx/docker-entrypoint.sh deleted file mode 100644 index 978fa9f4e..000000000 --- a/infrastructure/os2display/nginx/docker-entrypoint.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh - -set -eux - -## Run templates with configuration. -/usr/local/bin/confd --onetime --backend env --confdir /etc/confd - -entrypoint_log() { - if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "$@" - fi -} - -if [ "$1" = "nginx" -o "$1" = "nginx-debug" ]; then - if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then - entrypoint_log "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration" - - entrypoint_log "$0: Looking for shell scripts in /docker-entrypoint.d/" - find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do - case "$f" in - *.envsh) - if [ -x "$f" ]; then - entrypoint_log "$0: Sourcing $f"; - . "$f" - else - # warn on shell scripts without exec bit - entrypoint_log "$0: Ignoring $f, not executable"; - fi - ;; - *.sh) - if [ -x "$f" ]; then - entrypoint_log "$0: Launching $f"; - "$f" - else - # warn on shell scripts without exec bit - entrypoint_log "$0: Ignoring $f, not executable"; - fi - ;; - *) entrypoint_log "$0: Ignoring $f";; - esac - done - - entrypoint_log "$0: Configuration complete; ready for start up" - else - entrypoint_log "$0: No files found in /docker-entrypoint.d/, skipping configuration" - fi -fi - -exec "$@" diff --git a/infrastructure/os2display/nginx/etc/confd/conf.d/default.conf.toml b/infrastructure/os2display/nginx/etc/confd/conf.d/default.conf.toml deleted file mode 100644 index 185954f0d..000000000 --- a/infrastructure/os2display/nginx/etc/confd/conf.d/default.conf.toml +++ /dev/null @@ -1,7 +0,0 @@ -[template] -src = "default.conf.tmpl" -dest = "/etc/nginx/conf.d/default.conf" -mode = "0644" -keys = [ - "/nginx-config" -] diff --git a/infrastructure/os2display/nginx/etc/confd/conf.d/nginx.conf.toml b/infrastructure/os2display/nginx/etc/confd/conf.d/nginx.conf.toml deleted file mode 100644 index 7c81a661f..000000000 --- a/infrastructure/os2display/nginx/etc/confd/conf.d/nginx.conf.toml +++ /dev/null @@ -1,7 +0,0 @@ -[template] -src = "nginx.conf.tmpl" -dest = "/etc/nginx/nginx.conf" -mode = "0644" -keys = [ - "/nginx-config" -] diff --git a/infrastructure/os2display/nginx/etc/confd/templates/default.conf.tmpl b/infrastructure/os2display/nginx/etc/confd/templates/default.conf.tmpl deleted file mode 100644 index b6f32099b..000000000 --- a/infrastructure/os2display/nginx/etc/confd/templates/default.conf.tmpl +++ /dev/null @@ -1,52 +0,0 @@ -# @see https://symfony.com/doc/current/setup/web_server_configuration.html -server { - listen 8080; - server_name localhost; - root /var/www/html/public; - - location / { - add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; - - # try to serve file directly, fallback to index.php - try_files $uri /index.php$is_args$args; - } - - location = /robots.txt { - add_header Content-Type text/plain; - add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; - return 200 "User-agent: *\nDisallow: /\n"; - } - - location ~ ^/index\.php(/|$) { - fastcgi_pass {{ getenv "PHP_FPM_SERVER" "phpfpm" }}:{{ getenv "PHP_FPM_SERVER_PORT" "9000" }}; - fastcgi_split_path_info ^(.+\.php)(/.*)$; - include fastcgi_params; - - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $realpath_root; - - internal; - } - - # return 404 for all other php files not matching the front controller - # this prevents access to other php files you don't want to be accessible. - location ~ \.php$ { - return 404; - } - - error_log /var/log/nginx/error.log; - access_log /var/log/nginx/access.log; -} - -server { - listen 8080; - server_name localhost; - root /var/www/html/public; - - error_log off; - access_log off; - - location /stub_status { - stub_status; - } -} diff --git a/infrastructure/os2display/nginx/etc/confd/templates/nginx.conf.tmpl b/infrastructure/os2display/nginx/etc/confd/templates/nginx.conf.tmpl deleted file mode 100644 index 254adf3ec..000000000 --- a/infrastructure/os2display/nginx/etc/confd/templates/nginx.conf.tmpl +++ /dev/null @@ -1,51 +0,0 @@ -worker_processes auto; - -error_log /var/log/nginx/error.log warn; -pid /tmp/nginx.pid; - -events { - worker_connections 2048; - multi_accept on; -} - - -http { - open_file_cache max=10000 inactive=5m; - open_file_cache_valid 5m; - open_file_cache_min_uses 5; - open_file_cache_errors off; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 15 15; - types_hash_max_size 2048; - - server_tokens off; - - client_max_body_size {{ getenv "NGINX_FPM_UPLOAD_MAX" "8M" }}; - - gzip on; - gzip_disable "msie6"; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.0; - gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - set_real_ip_from 172.16.0.0/8; - real_ip_recursive on; - real_ip_header X-Forwarded-For; - - log_format main '$http_x_real_ip - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - error_log /dev/stderr; - access_log /dev/stdout main; - - include /etc/nginx/conf.d/*.conf; -} diff --git a/infrastructure/os2display/run.sh b/infrastructure/os2display/run.sh deleted file mode 100755 index ae23d67b4..000000000 --- a/infrastructure/os2display/run.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -set -eux - -APP_VERSION=develop - -docker pull nginxinc/nginx-unprivileged:alpine - -docker build --pull --no-cache --build-arg APP_VERSION=${APP_VERSION} --tag=os2display/display-api-service:${APP_VERSION} --file="display-api-service/Dockerfile" display-api-service -docker build --no-cache --build-arg VERSION=${APP_VERSION} --tag=os2display/display-api-service-nginx:${APP_VERSION} --file="nginx/Dockerfile" nginx - -# docker push os2display/display-api-service:${APP_VERSION} -# docker push os2display/display-api-service-nginx:${APP_VERSION} diff --git a/infrastructure/package-lock.json b/infrastructure/package-lock.json deleted file mode 100644 index 4ca926f5d..000000000 --- a/infrastructure/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "app", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/scripts/check-env-coverage.sh b/scripts/check-env-coverage.sh new file mode 100755 index 000000000..d040e2d1a --- /dev/null +++ b/scripts/check-env-coverage.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# Validate that .env documents every Symfony env variable consumed in config/: +# 1. Every %env(...)% read in config/ is declared in .env. +# 2. Every variable in .env has a preceding description comment. +set -e + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +# Vars consumed via %env() but intentionally not part of the .env contract +# (e.g. injected by tooling, not Symfony-defined defaults). +ALLOW_LIST="VAR_DUMPER_SERVER" + +failed=0 +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +# Check 1 — every %env(...)% read in config/ is declared in .env +grep -rhoE '%env\(([a-z]+:)*[A-Z_][A-Z0-9_]*\)%' config/ \ + | sed -E 's/%env\(([a-z]+:)*([A-Z_][A-Z0-9_]*)\)%/\2/' \ + | sort -u > "$tmp/symfony-reads" +grep -E '^[A-Z_][A-Z0-9_]*=' .env | cut -d= -f1 | sort -u > "$tmp/env-declares" +echo "$ALLOW_LIST" | tr ' ' '\n' | sort -u > "$tmp/allow" + +undocumented=$(comm -23 "$tmp/symfony-reads" "$tmp/env-declares" | comm -23 - "$tmp/allow") +if [ -n "$undocumented" ]; then + echo "FAIL: %env(...)% reads in config/ not declared in .env:" + echo "$undocumented" | sed 's/^/ /' + failed=1 +fi + +# Check 2 — every variable in .env has a preceding description comment +unannotated=$(awk ' + # Order matters: ### must be tested before # so recipe markers are not + # treated as descriptions. + /^###/ { last_was_comment = 0; next } + /^#/ { last_was_comment = 1; next } + /^$/ { last_was_comment = 0; next } + /^[A-Z_][A-Z0-9_]*=/ { + name = $0; sub("=.*", "", name) + if (!last_was_comment) print name + last_was_comment = 0 + } +' .env) +if [ -n "$unannotated" ]; then + echo "FAIL: variables in .env without preceding description:" + echo "$unannotated" | sed 's/^/ /' + failed=1 +fi + +if [ "$failed" -eq 0 ]; then + echo "PASS: env coverage OK" +fi +exit "$failed"