diff --git a/.github/workflows/php-ci.md b/.github/workflows/php-ci.md new file mode 100644 index 0000000..d9dbfd9 --- /dev/null +++ b/.github/workflows/php-ci.md @@ -0,0 +1,129 @@ +# php-ci + +Reusable workflow that wires together the full PHP / Composer CI pipeline in a single call. The consuming repo controls **when** it runs; this workflow controls **how**. + +> Replace `` with the current SHA from the [root README](../../README.md#current-sha). + +## Pipeline shape + +``` +push / pull_request / workflow_dispatch + │ + ▼ + ┌──────────┐ + │ security │ gitleaks + composer-audit + guarddog (parallel) + └─────┬────┘ + │ (gates everything below) + ├──────────────┐ + ▼ ▼ + ┌──────┐ ┌──────┐ + │ lint │ │ test │ + └──┬───┘ └──┬───┘ + └──────────────┘ + │ (both must pass) + ▼ + ┌──────────────────┐ + │vulnerability-scan│ Syft + Grype (code + Docker) + └──────────────────┘ +``` + +## Usage + +### Minimal — PHP service, no Dockerfile + +```yaml +# .github/workflows/ci.yml (in your consuming repo) +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + ci: + uses: orangitfi/platform-tooling/.github/workflows/php-ci.yml@ + with: + run-docker-scan: false +``` + +### PHP service with a Dockerfile + +```yaml +jobs: + ci: + uses: orangitfi/platform-tooling/.github/workflows/php-ci.yml@ + with: + image-name: my-app +``` + +### Custom PHPStan level and test suite + +```yaml +jobs: + ci: + uses: orangitfi/platform-tooling/.github/workflows/php-ci.yml@ + with: + phpstan-level: "6" + test-command: "vendor/bin/phpunit --testsuite=Unit" +``` + +### Full example with all options explicit + +```yaml +jobs: + ci: + uses: orangitfi/platform-tooling/.github/workflows/php-ci.yml@ + with: + working-directory: . + run-phpcs: true + phpcs-standard: PSR12 + run-phpstan: true + phpstan-level: "5" + paths: src + dockerfile-path: Dockerfile + test-command: vendor/bin/phpunit + composer-args: "" + fail-on-severity: high + run-docker-scan: true + image-name: my-app +``` + +## Parameters + +| Input | Default | Description | +|-------|---------|-------------| +| `working-directory` | `.` | Directory containing `composer.json` | +| `run-phpcs` | `true` | Run PHP_CodeSniffer in the lint job | +| `phpcs-standard` | `PSR12` | Coding standard when no `phpcs.xml` is present | +| `run-phpstan` | `true` | Run PHPStan in the lint job | +| `phpstan-level` | `5` | Analysis level 1–9 when no `phpstan.neon` is present | +| `paths` | `src` | Space-separated paths both lint tools scan (when no config file is present) | +| `dockerfile-path` | `Dockerfile` | Path to the Dockerfile (used by lint and vulnerability scan) | +| `test-command` | `vendor/bin/phpunit` | Full command to run tests | +| `composer-args` | `""` | Extra arguments passed to `composer install` in the test job | +| `fail-on-severity` | `high` | Minimum Grype severity for the vulnerability scan | +| `run-docker-scan` | `true` | Build and scan the Docker image in the vulnerability scan | +| `image-name` | *(repo name)* | Docker image name for the vulnerability scan | + +## Jobs reference + +| Job | Calls | Condition | +|-----|-------|-----------| +| `security` | `php-security-scan` | always | +| `lint` | `php-lint` | always (after security) | +| `test` | `php-test` | always (after security) | +| `vulnerability-scan` | `php-vulnerability-scan` | after lint and test pass | + +## When it has value + +- **Single line of CI**: one `uses:` line gives you secret scanning, dependency auditing, static analysis, unit tests, and vulnerability scanning — in the right order, with the right gates. +- **No Dockerfile?** Set `run-docker-scan: false` — the pipeline still runs all PHP-specific quality checks. +- **Scan before install**: composer-audit and guarddog run against `composer.lock` and `composer.json` before any package is installed. A compromised dependency is blocked before it executes. +- **Project config files respected**: if `phpcs.xml` or `phpstan.neon` are committed, the lint job uses them as-is. Workflow inputs only apply as fallbacks. + +## Tips + +- If phpcs or phpstan are not yet dev dependencies in the project, set `run-phpcs: false` and/or `run-phpstan: false` to skip them until the tools are added. +- Add this workflow as the sole required status check in branch protection rules: one check covers the entire pipeline. +- For nightly DAST scanning of the deployed service, use [`php-daily`](php-daily.md) in a separate scheduled workflow. diff --git a/.github/workflows/php-ci.yml b/.github/workflows/php-ci.yml new file mode 100644 index 0000000..e709990 --- /dev/null +++ b/.github/workflows/php-ci.yml @@ -0,0 +1,149 @@ +# PHP CI +# +# Reusable workflow that wires together the full PHP / Composer CI pipeline: +# +# security — gitleaks + composer-audit + guarddog (gates everything below) +# lint — phpcs + phpstan + hadolint ┐ +# test — composer install + phpunit ├─ parallel after security +# vulnerability-scan — Syft + Grype (after all above pass or are skipped) +# +# The consuming repo controls WHEN this runs. This workflow controls HOW. +# +# Usage in a consuming repository: +# +# on: [push, pull_request, workflow_dispatch] +# +# jobs: +# ci: +# uses: orangitfi/platform-tooling/.github/workflows/php-ci.yml@ +# with: +# run-docker-scan: false +# +# See php-ci.md for full documentation and all parameter options. +# +# ─── SHA pinning ───────────────────────────────────────────────────────────── +# The `uses:` lines below reference platform-tooling workflows at a specific +# SHA. After committing this file, run the SHA bump process documented in the +# root README to replace the SHA with the new commit SHA. +# ───────────────────────────────────────────────────────────────────────────── + +name: CI + +on: + workflow_call: + inputs: + working-directory: + description: "Directory containing composer.json" + required: false + default: "." + type: string + # ── Lint inputs ────────────────────────────────────────────────────── + run-phpcs: + description: "Run PHP_CodeSniffer in the lint job" + required: false + default: true + type: boolean + phpcs-standard: + description: "phpcs coding standard when no phpcs.xml is present (e.g. PSR12)" + required: false + default: "PSR12" + type: string + run-phpstan: + description: "Run PHPStan in the lint job" + required: false + default: true + type: boolean + phpstan-level: + description: "PHPStan analysis level (1-9) when no phpstan.neon is present" + required: false + default: "5" + type: string + paths: + description: "Space-separated paths both lint tools scan when no tool config file is present" + required: false + default: "src" + type: string + dockerfile-path: + description: "Path to the Dockerfile relative to the repo root (used by lint and vulnerability scan)" + required: false + default: "Dockerfile" + type: string + # ── Test inputs ────────────────────────────────────────────────────── + test-command: + description: "Command to run tests (e.g. vendor/bin/phpunit --testsuite=Unit)" + required: false + default: "vendor/bin/phpunit" + type: string + composer-args: + description: "Extra arguments passed to composer install in the test job" + required: false + default: "" + type: string + # ── Vulnerability scan inputs ──────────────────────────────────────── + fail-on-severity: + description: "Minimum Grype severity for the vulnerability scan: critical, high, medium, low, negligible" + required: false + default: "high" + type: string + run-docker-scan: + description: "Build and scan the Docker image in the vulnerability scan (set false for repos without a Dockerfile)" + required: false + default: true + type: boolean + image-name: + description: "Docker image name — used by the vulnerability scan" + required: false + default: "" + type: string + +permissions: + contents: read + +jobs: + # ─────────────────────────────────────────────────────────────────────────── + # 1. Security audit — runs first, gates everything else + # ─────────────────────────────────────────────────────────────────────────── + security: + name: Security Scan + uses: orangitfi/platform-tooling/.github/workflows/php-security-scan.yml@f877740fc9a519be6b06492ffd69017337b8ca86 # pt-sha + with: + working-directory: ${{ inputs.working-directory }} + + # ─────────────────────────────────────────────────────────────────────────── + # 2. Lint + Test — parallel after security passes + # ─────────────────────────────────────────────────────────────────────────── + lint: + name: Lint + needs: security + uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@f877740fc9a519be6b06492ffd69017337b8ca86 # pt-sha + with: + working-directory: ${{ inputs.working-directory }} + run-phpcs: ${{ inputs.run-phpcs }} + phpcs-standard: ${{ inputs.phpcs-standard }} + run-phpstan: ${{ inputs.run-phpstan }} + phpstan-level: ${{ inputs.phpstan-level }} + paths: ${{ inputs.paths }} + dockerfile-path: ${{ inputs.dockerfile-path }} + + test: + name: Test + needs: security + uses: orangitfi/platform-tooling/.github/workflows/php-test.yml@f877740fc9a519be6b06492ffd69017337b8ca86 # pt-sha + with: + working-directory: ${{ inputs.working-directory }} + test-command: ${{ inputs.test-command }} + composer-args: ${{ inputs.composer-args }} + + # ─────────────────────────────────────────────────────────────────────────── + # 3. Vulnerability scan — runs after all quality gates pass or are skipped + # ─────────────────────────────────────────────────────────────────────────── + vulnerability-scan: + name: Vulnerability Scan + needs: [lint, test] + uses: orangitfi/platform-tooling/.github/workflows/php-vulnerability-scan.yml@f877740fc9a519be6b06492ffd69017337b8ca86 # pt-sha + with: + working-directory: ${{ inputs.working-directory }} + fail-on-severity: ${{ inputs.fail-on-severity }} + run-docker-scan: ${{ inputs.run-docker-scan }} + dockerfile-path: ${{ inputs.dockerfile-path }} + image-name: ${{ inputs.image-name }} diff --git a/.github/workflows/php-daily.md b/.github/workflows/php-daily.md new file mode 100644 index 0000000..efdccd5 --- /dev/null +++ b/.github/workflows/php-daily.md @@ -0,0 +1,140 @@ +# php-daily + +Reusable workflow for a daily security status run against a PHP / Symfony project. Combines the full CI pipeline with an OWASP ZAP DAST scan running in parallel — together they give a complete picture of the current security posture of both the codebase and the running application. + +> Replace `` with the current SHA from the [root README](../../README.md#current-sha). + +## Pipeline shape + +``` +schedule (03:00 UTC) / workflow_dispatch + │ + ┌─────────┴──────────┐ + ▼ ▼ +┌────────┐ ┌──────────┐ +│ ci │ │ dast │ +│ │ │ │ +│security│ │ OWASP ZAP│ +│ lint │ │ baseline │ +│ test │ │ (or full │ +│vuln │ │ or api) │ +│ scan │ │ │ +└────────┘ │ skipped │ + │ if no │ + │ target │ + └──────────┘ +``` + +Both jobs run in **parallel**. `ci` checks the code and dependencies; `dast` checks the live deployed environment. A test or lint failure does not block the DAST scan — they are independent signals. + +The `dast` job is **skipped cleanly** when `target-url` is empty. + +## Usage + +### Minimal nightly run — code checks only, no live URL yet + +```yaml +# .github/workflows/daily.yml (in your consuming repo) +name: Daily Security + +on: + schedule: + - cron: "0 3 * * *" # 03:00 UTC every night + workflow_dispatch: + +jobs: + daily: + uses: orangitfi/platform-tooling/.github/workflows/php-daily.yml@ + with: + run-docker-scan: false + # target-url not set → DAST job is skipped +``` + +### Full daily run — CI + DAST baseline scan + +```yaml +jobs: + daily: + uses: orangitfi/platform-tooling/.github/workflows/php-daily.yml@ + with: + image-name: my-app + target-url: https://staging.myapp.com +``` + +### Symfony API with OpenAPI spec + +```yaml +jobs: + daily: + uses: orangitfi/platform-tooling/.github/workflows/php-daily.yml@ + with: + image-name: my-app + target-url: https://staging.myapp.com + scan-type: api + openapi-spec: https://staging.myapp.com/api/doc.json +``` + +### Use a repo variable for the staging URL + +```yaml +jobs: + daily: + uses: orangitfi/platform-tooling/.github/workflows/php-daily.yml@ + with: + image-name: my-app + target-url: ${{ vars.STAGING_URL }} # empty → DAST skipped, CI still runs +``` + +## Parameters + +### CI parameters + +| Input | Default | Description | +|-------|---------|-------------| +| `working-directory` | `.` | Directory containing `composer.json` | +| `run-phpcs` | `true` | Run PHP_CodeSniffer in the lint job | +| `phpcs-standard` | `PSR12` | Coding standard when no `phpcs.xml` is present | +| `run-phpstan` | `true` | Run PHPStan in the lint job | +| `phpstan-level` | `5` | Analysis level 1–9 when no `phpstan.neon` is present | +| `paths` | `src` | Space-separated paths both lint tools scan (when no config file is present) | +| `dockerfile-path` | `Dockerfile` | Path to the Dockerfile | +| `test-command` | `vendor/bin/phpunit` | Full command to run tests | +| `composer-args` | `""` | Extra arguments passed to `composer install` | +| `run-docker-scan` | `true` | Scan the Docker image in the vulnerability scan | +| `image-name` | *(repo name)* | Docker image name for the vulnerability scan | +| `fail-on-severity` | `high` | Minimum Grype severity for the vulnerability scan | + +### DAST parameters + +| Input | Default | Description | +|-------|---------|-------------| +| `target-url` | `""` | URL to scan. Empty = DAST job skipped | +| `scan-type` | `baseline` | `baseline` (passive), `full` (active), `api` (OpenAPI-aware) | +| `openapi-spec` | `""` | OpenAPI spec URL — used only for `api` scan type | +| `dast-fail-on-findings` | `true` | Fail the DAST job if ZAP finds alerts | + +## What it covers + +| Layer | Tool | What it checks | +|-------|------|----------------| +| Secrets in code | gitleaks | Committed credentials and tokens | +| PHP dependencies | composer audit | Known CVEs in `composer.lock` | +| Supply chain | guarddog | Malicious Packagist packages | +| Code style | PHP_CodeSniffer | PSR-12 violations and formatting | +| Static analysis | PHPStan | Type errors, undefined variables, dead code | +| Unit tests | PHPUnit | Regressions in logic | +| OS + container CVEs | Syft + Grype | Vulnerabilities in image layers | +| Live app behaviour | OWASP ZAP | Runtime security headers, cookies, API misconfigs | + +## When it has value + +- **Complete daily picture**: static analysis tells you about the code; DAST tells you about what's actually running. Both together mean you don't miss a class of vulnerability in either direction. +- **CVE early warning**: dependency and OS vulnerability databases are updated daily. Running nightly ensures new CVEs are caught within 24 hours even with no code changes. +- **Symfony APIs**: the `api` scan type is valuable for Symfony backend projects — ZAP uses the OpenAPI spec to exercise every declared endpoint, catching misconfigurations a passive scan would miss. + +## Tips + +- Use `${{ vars.STAGING_URL }}` (a repository variable) for `target-url`. When the variable is empty the DAST job is skipped automatically — no workflow changes needed for repos without a staging environment yet. +- Start with `dast-fail-on-findings: false` for the first week to understand the baseline noise before enabling hard failures. +- The ZAP report is uploaded as an artifact named `zap-daily-report` on every scan run. +- Pair with `slack-notify`: add it as a final job with `needs: [ci, dast]` and `if: always()` in the consuming repo's caller workflow to get a daily status message in your security channel. diff --git a/.github/workflows/php-daily.yml b/.github/workflows/php-daily.yml new file mode 100644 index 0000000..d649880 --- /dev/null +++ b/.github/workflows/php-daily.yml @@ -0,0 +1,161 @@ +# PHP Daily +# +# Reusable workflow for a daily security status run against a PHP / Symfony project. +# Combines the full CI pipeline with an OWASP ZAP DAST scan in parallel: +# +# ci — full php-ci pipeline (security + lint + test + vuln scan) +# dast — OWASP ZAP scan against the live staging/production URL +# +# Both jobs run in parallel. CI checks the current code and dependencies; +# ZAP checks the running application. They are independent — a build failure +# does not prevent the DAST scan from running, and vice versa. +# +# If target-url is not provided the dast job is skipped cleanly. +# +# The consuming repo controls WHEN this runs (typically a nightly schedule). +# This workflow controls HOW. +# +# Usage in a consuming repository: +# +# on: +# schedule: +# - cron: "0 3 * * *" # 03:00 UTC every night +# workflow_dispatch: # allow manual trigger +# +# jobs: +# daily: +# uses: orangitfi/platform-tooling/.github/workflows/php-daily.yml@ +# with: +# target-url: https://staging.myapp.com +# +# See php-daily.md for full documentation and all parameter options. +# +# ─── SHA pinning ───────────────────────────────────────────────────────────── +# After committing this file, replace the SHA with the new commit SHA using +# the bump process documented in the root README. +# ───────────────────────────────────────────────────────────────────────────── + +name: Daily Security + +on: + workflow_call: + inputs: + # ── CI inputs ──────────────────────────────────────────────────────── + working-directory: + description: "Directory containing composer.json" + required: false + default: "." + type: string + run-phpcs: + description: "Run PHP_CodeSniffer in the lint job" + required: false + default: true + type: boolean + phpcs-standard: + description: "phpcs coding standard when no phpcs.xml is present" + required: false + default: "PSR12" + type: string + run-phpstan: + description: "Run PHPStan in the lint job" + required: false + default: true + type: boolean + phpstan-level: + description: "PHPStan analysis level (1-9) when no phpstan.neon is present" + required: false + default: "5" + type: string + paths: + description: "Space-separated paths both lint tools scan when no tool config file is present" + required: false + default: "src" + type: string + dockerfile-path: + description: "Path to the Dockerfile relative to the repo root" + required: false + default: "Dockerfile" + type: string + test-command: + description: "Command to run tests (e.g. vendor/bin/phpunit --testsuite=Unit)" + required: false + default: "vendor/bin/phpunit" + type: string + composer-args: + description: "Extra arguments passed to composer install in the test job" + required: false + default: "" + type: string + run-docker-scan: + description: "Build and scan the Docker image in the vulnerability scan" + required: false + default: true + type: boolean + image-name: + description: "Docker image name for the vulnerability scan" + required: false + default: "" + type: string + fail-on-severity: + description: "Minimum Grype severity for the vulnerability scan" + required: false + default: "high" + type: string + # ── DAST inputs ────────────────────────────────────────────────────── + target-url: + description: "URL of the running application to scan with ZAP. Leave empty to skip DAST." + required: false + default: "" + type: string + scan-type: + description: "ZAP scan type: baseline (passive, ~2 min) | full (active, 10-30 min) | api" + required: false + default: "baseline" + type: string + openapi-spec: + description: "OpenAPI spec URL or path — used only when scan-type is api" + required: false + default: "" + type: string + dast-fail-on-findings: + description: "Fail the DAST job if ZAP reports any alerts" + required: false + default: true + type: boolean + +permissions: + contents: read + +jobs: + # ─────────────────────────────────────────────────────────────────────────── + # Full CI pipeline — static security, lint, test, vulnerability scan + # ─────────────────────────────────────────────────────────────────────────── + ci: + name: CI + uses: orangitfi/platform-tooling/.github/workflows/php-ci.yml@f877740fc9a519be6b06492ffd69017337b8ca86 # pt-sha + with: + working-directory: ${{ inputs.working-directory }} + run-phpcs: ${{ inputs.run-phpcs }} + phpcs-standard: ${{ inputs.phpcs-standard }} + run-phpstan: ${{ inputs.run-phpstan }} + phpstan-level: ${{ inputs.phpstan-level }} + paths: ${{ inputs.paths }} + dockerfile-path: ${{ inputs.dockerfile-path }} + test-command: ${{ inputs.test-command }} + composer-args: ${{ inputs.composer-args }} + run-docker-scan: ${{ inputs.run-docker-scan }} + image-name: ${{ inputs.image-name }} + fail-on-severity: ${{ inputs.fail-on-severity }} + + # ─────────────────────────────────────────────────────────────────────────── + # DAST — dynamic scan against the live URL (skipped if target-url is empty) + # ─────────────────────────────────────────────────────────────────────────── + dast: + name: DAST (OWASP ZAP) + uses: orangitfi/platform-tooling/.github/workflows/dast-scan.yml@f877740fc9a519be6b06492ffd69017337b8ca86 # pt-sha + with: + target-url: ${{ inputs.target-url }} + scan-type: ${{ inputs.scan-type }} + openapi-spec: ${{ inputs.openapi-spec }} + fail-on-findings: ${{ inputs.dast-fail-on-findings }} + artifact-name: zap-daily-report diff --git a/.github/workflows/php-lint.md b/.github/workflows/php-lint.md new file mode 100644 index 0000000..5793a4c --- /dev/null +++ b/.github/workflows/php-lint.md @@ -0,0 +1,124 @@ +# php-lint + +Reusable workflow that runs PHP code quality checks **in parallel** against PHP / Symfony repositories. + +> Replace `` with the current SHA from the [root README](../../README.md#current-sha). + +## Jobs + +| Job | Tool | What it checks | +|-----|------|----------------| +| `phpcs` | PHP_CodeSniffer | Code style violations and formatting (PSR-12 by default) | +| `phpstan` | PHPStan | Type errors, undefined variables, unreachable code, and other static analysis findings | +| `docker-lint` | hadolint | Dockerfile best-practice violations — skipped cleanly if no Dockerfile is found | + +## Usage + +### Minimal + +```yaml +on: [push, pull_request] + +jobs: + lint: + uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@ +``` + +### Backend in a subdirectory + +```yaml +jobs: + lint: + uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@ + with: + working-directory: ./backend + dockerfile-path: ./backend/Dockerfile +``` + +### phpcs only (disable PHPStan) + +```yaml +jobs: + lint: + uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@ + with: + run-phpstan: false +``` + +### PHPStan only (disable phpcs) + +```yaml +jobs: + lint: + uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@ + with: + run-phpcs: false +``` + +### Custom PHPStan level and paths (no phpstan.neon in the project) + +```yaml +jobs: + lint: + uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@ + with: + phpstan-level: "6" + paths: "src tests" +``` + +### Repos without a Dockerfile (docker-lint skips automatically) + +```yaml +jobs: + lint: + uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@ + # No dockerfile-path needed — if Dockerfile is not found the job is skipped, not failed +``` + +## Parameters + +| Input | Default | Description | +|-------|---------|-------------| +| `working-directory` | `.` | Directory containing `composer.json` | +| `run-phpcs` | `true` | Run PHP_CodeSniffer | +| `phpcs-standard` | `PSR12` | Coding standard (used only when no `phpcs.xml` is present) | +| `run-phpstan` | `true` | Run PHPStan | +| `phpstan-level` | `5` | Analysis level 1–9 (used only when no `phpstan.neon` is present) | +| `paths` | `src` | Space-separated paths both tools scan (used only when no tool config file is present) | +| `dockerfile-path` | `Dockerfile` | Path to the Dockerfile relative to the repo root | + +## Prerequisites + +Both phpcs and phpstan must be installed as Composer dev dependencies in the project. The workflow runs `composer install` before each job. + +```bash +composer require --dev squizlabs/php_codesniffer phpstan/phpstan +``` + +If either binary is missing from `vendor/bin/`, the job fails immediately with a clear error message pointing to the missing dependency. + +## Project config files take precedence + +When a project-level config file is found, it is used as-is and the workflow inputs for paths/standard/level are ignored: + +| Tool | Config files | +|------|-------------| +| phpcs | `phpcs.xml`, `phpcs.xml.dist` | +| phpstan | `phpstan.neon`, `phpstan.neon.dist` | + +This means teams that already have these config files committed get exactly the same behaviour locally and in CI — no duplication. + +## When it has value + +- Enforces a consistent style gate on every PR without depending on developer tooling or IDE settings +- PHPStan catches bugs that unit tests often miss: wrong method signatures, missing return types, null-safety violations +- Running phpcs and phpstan in parallel keeps the job fast — neither blocks the other +- Dockerfile quality is checked in the same pipeline at no extra cost + +## Tips + +- Start with PHPStan level 1 or 2 on an existing codebase and increase the level gradually. Level 5 is a reasonable starting point for new projects. +- Commit a `phpstan.neon` to the project root to lock the level and paths so developers get the same analysis locally (`vendor/bin/phpstan analyse`) as in CI. +- phpcs auto-fixes are not available via this workflow — run `vendor/bin/phpcbf` locally to fix style violations. +- hadolint rules can be suppressed inline with `# hadolint ignore=DL3008` or globally via `.hadolint.yaml` in the repo root. +- Add this workflow as a required status check in branch protection rules so PRs cannot be merged with lint failures. diff --git a/.github/workflows/php-lint.yml b/.github/workflows/php-lint.yml new file mode 100644 index 0000000..e10c3a6 --- /dev/null +++ b/.github/workflows/php-lint.yml @@ -0,0 +1,161 @@ +# PHP Lint +# +# Reusable workflow that runs three jobs in parallel: +# +# 1. phpcs — PHP_CodeSniffer: code style and formatting (PSR-12 by default) +# uses phpcs.xml / phpcs.xml.dist if found in the project root +# 2. phpstan — static analysis: type errors, undefined variables, dead code +# uses phpstan.neon / phpstan.neon.dist if found in the project root +# 3. docker-lint — hadolint against the Dockerfile +# skipped cleanly if no Dockerfile is found +# +# Both phpcs and phpstan require the tools to be installed as Composer dev +# dependencies (vendor/bin/phpcs and vendor/bin/phpstan). The workflow runs +# composer install before each job so phpstan can resolve types from +# project dependencies. +# +# Usage in a consuming repository: +# +# jobs: +# lint: +# uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@ +# with: +# working-directory: . + +name: PHP Lint + +on: + workflow_call: + inputs: + working-directory: + description: "Directory containing composer.json" + required: false + default: "." + type: string + run-phpcs: + description: "Run PHP_CodeSniffer (code style check)" + required: false + default: true + type: boolean + paths: + description: "Space-separated paths to lint when no tool config file is present (e.g. 'src tests')" + required: false + default: "src" + type: string + phpcs-standard: + description: "Coding standard for phpcs when no phpcs.xml is present (e.g. PSR12, PSR2)" + required: false + default: "PSR12" + type: string + run-phpstan: + description: "Run PHPStan (static analysis)" + required: false + default: true + type: boolean + phpstan-level: + description: "PHPStan analysis level (1-9) when no phpstan.neon is present" + required: false + default: "5" + type: string + dockerfile-path: + description: "Path to the Dockerfile relative to the repository root" + required: false + default: "Dockerfile" + type: string + +permissions: + contents: read + +jobs: + phpcs: + name: PHP_CodeSniffer + if: ${{ inputs.run-phpcs }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install dependencies + shell: bash + working-directory: ${{ inputs.working-directory }} + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: Run phpcs + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + PATHS: ${{ inputs.paths }} + PHPCS_STANDARD: ${{ inputs.phpcs-standard }} + run: | + set -euo pipefail + + if [ ! -f "vendor/bin/phpcs" ]; then + echo "::error::vendor/bin/phpcs not found. Add PHP_CodeSniffer as a dev dependency: composer require --dev squizlabs/php_codesniffer" + exit 1 + fi + + if [ -f "phpcs.xml" ] || [ -f "phpcs.xml.dist" ]; then + vendor/bin/phpcs + else + # shellcheck disable=SC2086 + vendor/bin/phpcs --standard="${PHPCS_STANDARD}" ${PATHS} + fi + + phpstan: + name: PHPStan + if: ${{ inputs.run-phpstan }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install dependencies + shell: bash + working-directory: ${{ inputs.working-directory }} + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: Run phpstan + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + PATHS: ${{ inputs.paths }} + PHPSTAN_LEVEL: ${{ inputs.phpstan-level }} + run: | + set -euo pipefail + + if [ ! -f "vendor/bin/phpstan" ]; then + echo "::error::vendor/bin/phpstan not found. Add PHPStan as a dev dependency: composer require --dev phpstan/phpstan" + exit 1 + fi + + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + vendor/bin/phpstan analyse --no-progress + else + # shellcheck disable=SC2086 + vendor/bin/phpstan analyse --level="${PHPSTAN_LEVEL}" --no-progress ${PATHS} + fi + + docker-lint: + name: Docker Lint (hadolint) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check for Dockerfile + id: check + shell: bash + run: | + if [ -f "${{ inputs.dockerfile-path }}" ]; then + echo "exists=true" >> "${GITHUB_OUTPUT}" + else + echo "exists=false" >> "${GITHUB_OUTPUT}" + echo "No Dockerfile found at '${{ inputs.dockerfile-path }}', skipping docker lint." + fi + + - name: Lint Dockerfile + if: steps.check.outputs.exists == 'true' + uses: orangitfi/platform-tooling/.github/actions/docker-lint@f877740fc9a519be6b06492ffd69017337b8ca86 # pt-sha + with: + dockerfile: ${{ inputs.dockerfile-path }} + working-directory: ${{ inputs.working-directory }} diff --git a/.github/workflows/php-test.md b/.github/workflows/php-test.md new file mode 100644 index 0000000..987ff46 --- /dev/null +++ b/.github/workflows/php-test.md @@ -0,0 +1,100 @@ +# php-test + +Reusable workflow that installs Composer dependencies and runs PHPUnit unit tests. + +This covers **unit tests only** — tests that run without external services (no database, no Redis, no message queue). Integration and end-to-end tests that require a live environment or `docker-compose` are out of scope for this baseline workflow. + +> Replace `` with the current SHA from the [root README](../../README.md#current-sha). + +## Usage + +### Minimal — run PHPUnit from the repo root + +```yaml +on: [push, pull_request] + +jobs: + test: + uses: orangitfi/platform-tooling/.github/workflows/php-test.yml@ +``` + +### Run a specific test suite + +```yaml +jobs: + test: + uses: orangitfi/platform-tooling/.github/workflows/php-test.yml@ + with: + test-command: "vendor/bin/phpunit --testsuite=Unit" +``` + +### Verbose output with short backtrace + +```yaml +jobs: + test: + uses: orangitfi/platform-tooling/.github/workflows/php-test.yml@ + with: + test-command: "vendor/bin/phpunit -v" +``` + +### Backend in a subdirectory + +```yaml +jobs: + test: + uses: orangitfi/platform-tooling/.github/workflows/php-test.yml@ + with: + working-directory: ./backend +``` + +### Recommended position in a pipeline + +```yaml +jobs: + security: + uses: orangitfi/platform-tooling/.github/workflows/php-security-scan.yml@ + + lint: + uses: orangitfi/platform-tooling/.github/workflows/php-lint.yml@ + + test: + needs: security + uses: orangitfi/platform-tooling/.github/workflows/php-test.yml@ +``` + +## Parameters + +| Input | Default | Description | +|-------|---------|-------------| +| `working-directory` | `.` | Directory containing `composer.json` | +| `test-command` | `vendor/bin/phpunit` | Full command to run tests | +| `composer-args` | `""` | Extra arguments passed to `composer install` | + +## What it does + +1. Checks out the repository +2. Runs `composer install --no-interaction --prefer-dist --optimize-autoloader [composer-args]` — installs all dependencies including dev +3. Runs `` in the working directory + +## Prerequisites + +PHPUnit must be installed as a Composer dev dependency: + +```bash +composer require --dev phpunit/phpunit +``` + +PHPUnit configuration (`phpunit.xml` or `phpunit.xml.dist`) in the project root is used automatically when running `vendor/bin/phpunit` without extra arguments. + +## When it has value + +- Catches regressions in pure logic (domain objects, services, utilities) without spinning up a full environment +- Fast feedback on every PR — unit tests typically complete in seconds +- A useful baseline even for projects where integration tests require `docker-compose`: run unit tests in CI and integration tests separately or on a schedule + +## Tips + +- Use `--testsuite=Unit` to target only tests that have no environment dependencies, especially in projects that mix unit and integration tests in the same PHPUnit config. +- Pass environment variables the tests need via `env:` on the calling job — reusable workflows inherit the caller's environment. +- To generate a code coverage report, set `test-command` to `vendor/bin/phpunit --coverage-clover coverage.xml` and upload `coverage.xml` as a workflow artifact in a subsequent step. diff --git a/.github/workflows/php-test.yml b/.github/workflows/php-test.yml new file mode 100644 index 0000000..e328b6a --- /dev/null +++ b/.github/workflows/php-test.yml @@ -0,0 +1,63 @@ +# PHP Test +# +# Reusable workflow that installs Composer dependencies and runs PHPUnit. +# +# This covers unit tests only — tests that run without external services +# (no database, no Redis, no message queue). Integration and e2e tests +# that require a live environment are out of scope for this workflow. +# +# Usage in a consuming repository: +# +# # Minimal: runs PHPUnit from the repo root +# jobs: +# test: +# uses: orangitfi/platform-tooling/.github/workflows/php-test.yml@ +# +# # Run a specific test suite +# jobs: +# test: +# uses: orangitfi/platform-tooling/.github/workflows/php-test.yml@ +# with: +# test-command: "vendor/bin/phpunit --testsuite=Unit" + +name: PHP Test + +on: + workflow_call: + inputs: + working-directory: + description: "Directory containing composer.json" + required: false + default: "." + type: string + test-command: + description: "Command to run tests (e.g. vendor/bin/phpunit, vendor/bin/phpunit --testsuite=Unit -v)" + required: false + default: "vendor/bin/phpunit" + type: string + composer-args: + description: "Extra arguments passed to composer install (e.g. --no-dev)" + required: false + default: "" + type: string + +permissions: + contents: read + +jobs: + phpunit: + name: PHPUnit + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install dependencies + shell: bash + working-directory: ${{ inputs.working-directory }} + run: composer install --no-interaction --prefer-dist --optimize-autoloader ${{ inputs.composer-args }} + + - name: Run tests + shell: bash + working-directory: ${{ inputs.working-directory }} + run: ${{ inputs.test-command }} diff --git a/README.md b/README.md index e044702..b465e25 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,17 @@ Located in `.github/workflows/`. All use `on: workflow_call` and never define th | [`python-vulnerability-scan.yml`](.github/workflows/python-vulnerability-scan.yml) | [📄](.github/workflows/python-vulnerability-scan.md) | Runs Syft + Grype vulnerability scans against the source code and the Docker image. Docker scan is optional. | | [`python-build-and-publish-docker.yml`](.github/workflows/python-build-and-publish-docker.yml) | [📄](.github/workflows/python-build-and-publish-docker.md) | Builds a Docker image and optionally pushes it to GCP Artifact Registry. Credentials are read from 1Password at runtime. | +### PHP / Symfony / Composer + +| Workflow | Doc | Description | +|----------|-----|-------------| +| [`php-ci.yml`](.github/workflows/php-ci.yml) | [📄](.github/workflows/php-ci.md) | Full CI pipeline — security → lint + test (parallel) → vulnerability scan. Single `uses:` line replaces an entire CI file. | +| [`php-daily.yml`](.github/workflows/php-daily.yml) | [📄](.github/workflows/php-daily.md) | Nightly pipeline — runs the full CI pipeline and an OWASP ZAP DAST scan in parallel. | +| [`php-security-scan.yml`](.github/workflows/php-security-scan.yml) | [📄](.github/workflows/php-security-scan.md) | Runs gitleaks, composer-audit, and guarddog in parallel. Intended as the first gate in any PHP pipeline. | +| [`php-lint.yml`](.github/workflows/php-lint.yml) | [📄](.github/workflows/php-lint.md) | Runs PHP_CodeSniffer and PHPStan in parallel. Docker lint is skipped cleanly when no Dockerfile is found. | +| [`php-test.yml`](.github/workflows/php-test.yml) | [📄](.github/workflows/php-test.md) | Installs dependencies with `composer install` and runs PHPUnit. Covers unit tests only. | +| [`php-vulnerability-scan.yml`](.github/workflows/php-vulnerability-scan.yml) | [📄](.github/workflows/php-vulnerability-scan.md) | Runs Syft + Grype vulnerability scans against the source code and the Docker image. Docker scan is optional. | + ### Shared | Workflow | Doc | Description |