# Run from your project root
vendor/lts/php-qa-ci/scripts/install-github-actions.bashOr manually:
mkdir -p .github/workflows
cp vendor/lts/php-qa-ci/templates/github-actions/php-qa-ci.yml .github/workflows/qa.ymlgit add .github/workflows/qa.yml
git commit -m "Add PHP QA pipeline"
git pushThe workflow will run automatically on your next push or pull request.
The workflow automatically detects the PHP version from your composer.json file's require.php constraint. No manual configuration needed.
- Shallow clone by default: Faster CI runs
- Full clone when needed: Only when
AUTO_COMMIT_FIXESis enabled - Protected branch safety: Never auto-commits to main/master or default branch
Manually trigger specific tools from GitHub UI:
- Actions -> PHP QA Pipeline -> Run workflow -> Select tool
- Test results saved for 7 days
- Coverage reports available for download
- Useful for debugging failed builds
Add these to the env: section of your workflow:
env:
CI: true # Required
AUTO_COMMIT_FIXES: 'false' # Enable auto-commit of fixes
phpUnitCoverage: 0 # Disable coverage
phpqaQuickTests: 1 # Quick test mode
useInfection: 0 # Skip mutation testing
phpqaMemoryLimit: 4G # Memory limit for all QA toolsTo enable automatic committing of Rector/CS Fixer changes:
env:
AUTO_COMMIT_FIXES: 'true' # Only on feature branchesSafety Features:
- Never commits to main/master branches
- Never commits to repository's default branch
- Requires write permissions in repository settings
- Adds
[skip ci]to prevent infinite loops
- Go to Settings -> Actions -> General
- Under "Workflow permissions":
- Select "Read and write permissions"
- Check "Allow GitHub Actions to create and approve pull requests" (if using auto-commits)
- Go to Settings -> Branches
- Add rule for main/master branch
- Check "Require status checks to pass before merging"
- Select the required check-run names — these are the job names, not the workflow name.
For the
php-qa-ci.ymltemplate they are "Detect PHP Version", "PHP QA ()" (the version is filled in dynamically, e.g. "PHP QA (8.4)"), and "Coverage Report". ("PHP QA Pipeline" is the workflow name shown in the Actions tab, not a selectable status check.)
Template: templates/github-actions/qa-autofix.yml
Two jobs daisy-chained with needs:. The autofix job writes and commits fixes back on PRs;
the gate job validates the fixed tree read-only in the same run:
autofix (PR only: WRITE fixers + commit back) ──needs──► gate (READ-ONLY qa)
- autofix runs only on
pull_request. It checks out the PR branch tip, applies the fixers in WRITE mode (QA_READONLY=0→ Rector + PHP CS Fixer rewrite), and if anything changed commits + pushes it back to the PR branch. It is skipped on push to the default branch (already clean, arriving from an auto-fixed PR). - gate (
needs: autofix) checks out the same (possibly auto-fixed) tip and runs the pipeline read-only. On GitHub Actions php-qa-ci auto-enables read-only (detectReadOnly), so the fixers run as--dry-runand fail on any pending change; PHPStan etc. run as normal.
The gate validates the fixed tip in the same run. The autofix push uses the default
GITHUB_TOKEN, which deliberately starts no new workflow run — so there is no loop, no
cancellation, and nothing to wait for. (A re-trigger-based design would need a PAT; this one
does not.)
Contrast with the
php-qa-ci.ymltemplate'sAUTO_COMMIT_FIXES, which commits fixes at the end of a single run (so the validating tools ran against unfixed code). Here the fixers run before the gate, and the gate re-validates the fixed tree read-only.
on: filters cannot use expressions, so the template triggers on all pushes and guards
the gate to run — for push events — only on the default branch via
github.ref_name == github.event.repository.default_branch. PRs always run. So: PR →
autofix+gate; push to default (post-merge) → gate; push to a feature branch → skipped (its PR
covers it). No double-runs and no branch name baked into the file, which is what makes it safe
to copy verbatim into any repo. (A feature-branch push with no open PR produces an empty
skipped run — cosmetic.)
- The autofix write-back runs only on PRs, never on push to the default branch — automated commits to production branches are never made.
- Requires
permissions: contents: writeon the autofix job (the workflow iscontents: readby default) and Settings → Actions → General → "Read and write permissions". - A custom composer
bin-dir(e.g.bin) changes the qa path fromvendor/bin/qatobin/qa— replace everyvendor/bin/qarun:line accordingly. This is the one edit the template cannot make for you. - The gate runs
qa -t allCS+qa -t allStatic(tests deferred — a PHPUnit suite usually needs a DB / services not on the runner). When your suite runs without external services, add a tests step plus agate.servicesblock. - Private first-party deps: the template ships a guarded "Install SSH deploy keys" step
that is a no-op unless the
CI_SSH_DEPLOY_BUNDLEsecret is set. Ifcomposer.lockpins any dep to agithub_deploy_*SSH alias (CI'sGITHUB_TOKENcan't clone private sibling repos), provision the existing read-only deploy keys once, from a dev container that holds them:vendor/lts/php-qa-ci/scripts/ci-push-ssh-deploy-bundle.bash(auto-discovers the aliases fromcomposer.lock, bundles the keys, sets the secret).lts/php-qa-ciitself is public — no key. - Pre-wired:
PHP_QA_CI_DISABLE_CONFIG_PUSH: 'true'(workflow env, stops the composer plugins failing the read-only gate on config-push drift) andcomposer install --no-scripts.
mkdir -p .github/workflows
cp vendor/lts/php-qa-ci/templates/github-actions/qa-autofix.yml .github/workflows/qa.ymlPHP-QA-CI includes an update-deps.yml workflow that runs weekly to automatically update all dependencies:
- Composer dependencies (
composer update) - PHARs via PHIVE (
phive update) -- PHPStan, PHP CS Fixer, Infection, Composer Require Checker, PHPArkitect - Rector PHAR rebuild (
composer update --working-dir=build/rector-pharthenscripts/build-rector-phar.bash --force)
If changes are detected, it runs the full QA pipeline. If QA passes, it creates a pull request with auto-merge enabled.
To add this to your project:
cp vendor/lts/php-qa-ci/.github/workflows/update-deps.yml .github/workflows/update-deps.ymlThe workflow checks out your repository's own default branch (it pins no
explicit ref:), so the copied file works as-is — no branch edit is required.
See Continuous Integration for more details on the available workflows.
Place configuration files in your project's qaConfig/ directory:
qaConfig/qaConfig.inc.bash- Override pipeline settingsqaConfig/phpstan.neon- PHPStan configurationqaConfig/phpunit.xml- PHPUnit configurationqaConfig/php_cs.php- PHP CS Fixer rulesqaConfig/rector.php- Rector rulesqaConfig/infection.json- Infection configuration
- Go to Actions tab
- Select "PHP QA Pipeline"
- Click "Run workflow"
- Select tool from dropdown
- name: Run specific tool
run: vendor/bin/qa -t phpstanThe php-qa-ci.yml template uses two caches:
- Composer dependencies — keyed on
${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}(this key does not include the PHP version). - QA tool PHARs + tool cache (
vendor-phar/andvar/qa/cache) — keyed on the OS, the detected PHP version, andhashFiles('**/phive.xml', '**/composer.lock').
There is no separate "PHPStan cache" block. Only the QA-tools cache key includes the PHP version.
The workflow includes:
- Dynamic PHP version detection from composer.json
- Smart git cloning (shallow by default, full when auto-commit enabled)
- PHIVE integration for PHAR tool management
- Comprehensive caching for speed
- Auto-commit capability with safety checks
- Manual tool selection via workflow_dispatch
- Artifact storage for test results and coverage
- Extensive inline documentation
"php-qa-ci not installed" error
composer require --dev lts/php-qa-ci:dev-php8.4@devPermission denied for auto-commits
- Check repository Settings -> Actions -> Workflow permissions
- Ensure "Read and write permissions" is selected
Out of memory (default is now 4G for all tools)
env:
phpqaMemoryLimit: 8GInfection timeout
env:
useInfection: 0 # Disable mutation testing in CI- Download artifacts from failed run
- Check
var/qa/directory contents - Reproduce the CI gate locally. On GitHub Actions php-qa-ci auto-enables read-only mode, so a
pending Rector / PHP CS Fixer change fails the run instead of being applied.
CI=trueonly disables prompts — it does not make the run read-only. To reproduce the gate, setQA_READONLY=1:
QA_READONLY=1 vendor/bin/qa- Use branch protection - Require QA checks to pass
- Cache aggressively - Speeds up builds significantly
- Skip infection in CI - Run locally for faster feedback
- Auto-commit carefully - Only on feature branches, never on main
- Review workflow documentation - The template has extensive inline comments
- Enable update-deps.yml - Keep dependencies current automatically
- Replace
.travis.ymlwith.github/workflows/qa.yml - Environment variables work similarly
- Artifacts replace Travis's build artifacts
- GitHub Actions is commit-triggered by default
- No need for webhooks or polling
- Use workflow_dispatch for manual triggers
- name: Upload to Codecov
uses: codecov/codecov-action@v3
with:
file: var/qa/phpunit_logs/coverage.clover- name: Slack Notification
if: failure()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}