Imported from k-kinzal/php-ai-toolkit (
skills/setup-toolkit-github-actions/SKILL.md). Install upstream withnpx skills add k-kinzal/php-ai-toolkit --skill setup-toolkit-github-actions. Copyright stays with the author.
Setup GitHub Actions CI
This skill configures GitHub Actions so every php-ai-toolkit gate is visible in CI and the workflow follows Actions security and maintainability practices.
Discover Project Constraints
Read these files before editing CI:
composer.json:require.php,config.platform.php, and Composer scripts.- Existing
.github/workflows/*.ymlor.yaml. - Toolkit configs that imply CI gates:
.php-cs-fixer.dist.php,phpstan.neon,phpcs.xml.dist,loc.yaml,tree.yaml,deptrac.yaml,phpunit.xml.dist. - Project docs that declare supported PHP versions.
- Composer lock policy: one normal
composer.lock, no committed lock, or PHP-versioned locks such ascomposer.lock.php-<minor>. - Composer platform requirements and each command's runtime needs. Derive the
setup-phpextension list fromcomposer check-platform-reqsplus the config-driven needs below; do not rely on whatubuntu-latesthappens to preinstall, and do not copy another project's list.
Extensions that a project's configuration demands even though no require
declares them:
| Extension | Needed by | Failure without it |
|---|---|---|
mbstring |
phpunit/phpunit and infection/infection require it directly |
composer install fails the platform check |
pcntl |
phpunit/php-invoker, which implements PHPUnit's enforceTimeLimit="true" |
Time limits are not enforced; PHPUnit 10+ raises a runner warning that failOnAllIssues="true" turns into exit 1 |
pcov or Xdebug |
Coverage and mutation testing | No coverage data |
| GD with FreeType | DocGen social preview images | Image generation fails |
ParaTest does not need pcntl: it starts workers through symfony/process, and
its own ext-pcntl entry is a require-dev for its own test suite.
Apply the list to every job that installs the dev graph or runs tests, including a
mutation-testing job — it executes the same phpunit.xml.dist through Infection
and hits the same requirements as the test job.
If the declared PHP floor, Composer constraint, docs, and CI matrix disagree, surface the conflict and make CI match the declared support policy.
Template
Read the template from
vendor/k-kinzal/php-ai-toolkit/skills/setup-toolkit-github-actions/ci.yml
and apply it to the project root as .github/workflows/ci.yml.
The template contains REPLACE_WITH_* sentinels instead of this repository's PHP
matrix and lock policy. Replace every sentinel from target-project evidence before
installing the workflow; a remaining sentinel is a failed setup, not a default.
If a workflow already exists, merge rather than blindly replacing it.
Required CI Gates
The workflow must make the toolkit checks observable as separate named steps.
Do not hide all lint gates behind a single unnamed composer lint step.
Required gates when the corresponding script/config exists:
composer format:checkfor PHP-CS-Fixer.composer phpstanfor PHPStan and toolkit PHPStan rules.composer compatfor PHPCompatibility.composer loc-guardfor LocGuard.composer tree-guardfor TreeGuard.composer deptracfor Deptrac.composer testfor the PHPUnit suite when ParaTest is installed, otherwisecomposer test:unit.
Property-based tests configured by /setup-toolkit-pbt run through
composer test:pbt in their separate pbt.yml workflow. Verify that workflow is
present and that ordinary PHPUnit and ParaTest commands exclude the pbt group;
do not add the slower group to the normal test matrix as well.
Every gate the project has configured belongs in CI. A tool that is installed,
configured, and wired into composer lint but never runs on the default branch
is a gate the project believes it has: it passes locally for whoever last ran it
and drifts from then on. When a config file exists and its Composer script does
not run anywhere in CI, that is the finding to report, not a detail to leave.
Keep compat inside the lint job alongside formatting, PHPStan, LocGuard,
TreeGuard, and Deptrac. It may be a separate step for visibility, but it should
not be a separate CI job unless the project has an explicit reason. Namespace
visibility is enforced by the toolkit's PHPStan rules and needs no separate CI
step.
If a script is missing but the config exists, add the Composer script using the corresponding setup skill before wiring CI. If neither script nor config exists, do not invent the gate in CI; set up that tool first.
The Parallel Test Runner
When ParaTest is installed, make composer test select it and run that command in
the normal PHP matrix. Do not add a second sequential PHPUnit job: ParaTest already
executes the PHPUnit suite, and the matrix simultaneously verifies runtime support
and isolation across worker processes.
A real multi-major matrix must use the version-selecting tests/run.php runner from
/setup-toolkit-phpunit, so ParaTest receives the configuration matching the
installed PHPUnit major. Do not rely on the newest phpunit.xml.dist under older
dependency graphs. Keep composer test:unit available for local debugging and for
tools that specifically require PHPUnit, but CI does not need to repeat it.
Separate Decision: Documentation Publishing
Do not add documentation generation or GitHub Pages publishing to ci.yml.
Those live in their own workflows, and their templates and the questions to ask
before installing them belong to the /setup-toolkit-docgen skill.
Do not silently finish when the project has a docgen script but no DocGen
workflow. Ask the user to choose one of the three supported outcomes: local only,
publish the default branch, or publish the default branch plus pull-request diff
previews. Then use /setup-toolkit-docgen for the chosen workflows. The workflows
remain separate from ci.yml so publishing permissions do not leak into CI.
Separate Decision: Context7 Documentation Refresh
Context7 (https://context7.com) indexes a repository's documentation and serves
it to AI coding agents. It re-indexes public libraries on its own schedule, so a
project that wants agents to read current documentation asks for a refresh from
a workflow. Keep that workflow separate from ci.yml: the API key is a secret,
and ci.yml runs untrusted pull-request code.
Do not install it unasked. When the target publishes documentation that agents are expected to consume, ask the user whether the library is registered on Context7 and which event should refresh it. Two things must exist before the workflow can succeed, and neither can be created from the repository:
- The library page at
https://context7.com/<owner>/<repository>. If it does not exist, the user adds the repository at https://context7.com/add-library. - The
CONTEXT7_API_KEYrepository secret, copied from https://context7.com/dashboard into Settings > Secrets and variables > Actions.
Read the template from
vendor/k-kinzal/php-ai-toolkit/skills/setup-toolkit-github-actions/context7.yml
and apply it as .github/workflows/context7.yml. It sends one request to
POST https://context7.com/api/v1/refresh and needs no checkout, PHP, or
Composer step; do not add them, and do not replace the request with a
third-party action.
Choose the trigger from the lifecycle the documentation follows. The template
refreshes on every push to the default branch; replace
REPLACE_WITH_DEFAULT_BRANCH for that case and swap the on block for the
others. Keep workflow_dispatch in every variant so a refresh can be requested
by hand.
| Consumers read | Trigger |
|---|---|
| The default branch: trunk-based development, VCS installs, or a documentation site built from it | push to the default branch, as in the template |
| Tagged versions | push on tags matching the project's version pattern |
| GitHub releases | release with type published |
on:
push:
tags:
- 'v*'
workflow_dispatch:
on:
release:
types:
- published
workflow_dispatch:
The request re-indexes the branch Context7 tracks, which is the default branch
unless the library was registered otherwise. A tag or release trigger therefore
publishes whatever that branch holds when the event fires; it does not index the
tag itself. Add branch to the request body only when Context7 already tracks
that branch, because the API answers branch_not_found otherwise.
libraryName is /<owner>/<repository>, which the template derives from
github.repository. Replace it with a literal only when the Context7 library id
differs from the GitHub name, such as after a repository rename. Private
repositories need a paid Context7 plan and a gitToken in the request; report
that requirement instead of adding a second secret unprompted.
Context7 indexes markdown-family files from the tracked branch and falls back to
source files when a repository has no documentation. If generated output,
fixtures, or vendored documentation would pollute the index, a context7.json
at the repository root narrows the scope; see
https://context7.com/docs/adding-libraries for the current fields.
Context7 accepts a refresh of a library at most once per minimum interval, ten
days at the time of writing, and answers 400 with "error":"too-early" in
between. The template treats that answer as a skipped refresh: it annotates the
run and exits successfully, because the next push after the interval refreshes
the library and a red badge would report a non-defect. Every other error still
fails the job. If pushes are rare enough that a declined refresh may not be
followed by another push for weeks, a schedule trigger at the interval's
cadence catches up; add it only when the user confirms the tracked branch should
be re-indexed without a push.
After installing the workflow, validate it with actionlint. Once the secret
exists, run it through workflow_dispatch and read the job log:
{"message":"Refresh started successfully"}: the refresh is queued.400withtoo-early: the interval since the last update has not passed; the job exits 0 with a notice, and the answer proves the secret and the library id work.401: the secret is missing or wrong.404: the library is not registered, orlibraryNamediffers from its id.429: refreshes are rate limited; do not add retries to work around it.
Out of Scope: Mutation Testing
Do not write a mutation testing job from scratch. /setup-toolkit-infection ships
the job together with the configuration it depends on, because the job needs a
coverage driver, fetch-depth: 0, and different direct Infection arguments per
event. Use that skill when an Infection job is in scope.
Out of Scope: Performance Benchmarks
Do not add PHPBench to the normal test or lint matrix. /setup-toolkit-phpbench
owns the benchmark contract, stable runner settings, paired merge-target and
candidate measurements, and any requested comparison gate. Preserve an existing
standalone benchmark job when applying CI conventions; do not replace it with a
new comparison workflow unless that capability is requested. A single execution
is not evidence of performance relative to the base revision.
Out of Scope: Fuzzing
Do not write a generic fuzz job in ci.yml. /setup-toolkit-fuzzing derives the
input generator and oracle from a specific contract and ships a separate scheduled
workflow with corpus caching and crash artifacts. A random-input loop without that
contract design is not a CI gate.
PHP Version Coverage
CI must match the project's declared support range. Do not compensate for a bad Composer constraint by narrowing the workflow matrix.
- Determine the supported PHP range from
composer.json require.phpand the project docs. - List every supported minor version in the
testsmatrix. - Prefer the project's existing lock policy. A normal single
composer.lockis fine for most applications. Use PHP-versioned lock files only when a supported older PHP minor needs a different dependency graph and the project has a supply-chain requirement to pin those graphs, which is common for libraries and developer tools. - If the project uses PHP-versioned lock files, make sure every matrix minor
has a matching
composer.lock.php-<minor>file. - Check whether dev dependencies can install on each minor:
cp "composer.lock.php-<minor>" composer.lock composer validate --strict --no-check-publish - If dependencies do not install on a supported PHP minor, fix the Composer constraints first. Select the newest compatible tool line for that leg and add an older line only when Composer proves the target matrix needs it.
- Run the lint gates as named steps in one
lintjob on the highest supported runtime compatible with the tooling. PHPCompatibility checks the declared PHP range; repeating formatting, PHPStan, LocGuard, TreeGuard, and Deptrac on every runtime adds no distinct gate. - Use the highest matrix minor for one-off mutation, documentation, and benchmark jobs unless the tool cannot run there. Record that limitation rather than copying the template's PHP literal.
Never use --ignore-platform-reqs to make a lower PHP job pass. That hides a
real compatibility problem.
Check upper PHP bounds in the locked dev graph as well as minimum versions.
composer check-platform-reqs --no-dev verifies product requirements only; it
cannot establish that ParaTest or another development tool supports that runtime.
If resolving the mismatch requires changing an intentional dependency policy
outside the CI request, report that limitation explicitly instead of claiming the
green test job proves full platform compatibility.
The workflow template is deliberately incomplete until its PHP values, test command, extensions, branches, and lock steps are derived from the target. Never retain a literal merely because it matches php-ai-toolkit's own CI. In particular, do not copy this repository's full matrix or versioned-lock topology into a single-runtime application.
When generating PHP-versioned lock files, do not write config.platform.php
into the root composer.json. Repeat this process for each minor in the target's
actual support matrix, using a temporary directory and Composer home:
target_php='<supported-minor>'
lock_tmp_dir="$(mktemp -d)"
composer_home_dir="$lock_tmp_dir/composer-home"
mkdir -p "$composer_home_dir"
cp composer.json "$lock_tmp_dir/composer.json"
COMPOSER_HOME="$composer_home_dir" composer --working-dir="$lock_tmp_dir" config -g platform.php "$target_php.0"
COMPOSER_HOME="$composer_home_dir" composer --working-dir="$lock_tmp_dir" update --no-install --no-audit --no-interaction --no-progress
cp "$lock_tmp_dir/composer.lock" "composer.lock.php-$target_php"
Actions Best Practices
Apply these rules to every workflow created by this skill:
- Pin every external action with the full 40-character commit SHA.
- Keep workflow YAML free of explanatory comments. Use clear job and step names.
- Verify each SHA from the action's original repository:
gh release view --repo actions/checkout --json tagName,publishedAt,url git ls-remote --tags https://github.com/actions/checkout.git 'refs/tags/<tag>' - Use top-level
permissions: contents: readand only grant additional permissions for a step that truly needs them. - Use
pull_request, notpull_request_target, for untrusted PR code. - Use
concurrencywith PR number or ref andcancel-in-progress: true. Include the event name so a manual whole-tree mutation run cannot cancel the PR checks on the same branch. - Set
fail-fast: falsefor version matrices so all supported minors report. - Add
timeout-minutesto jobs. - Give every job and every step a clear
name. - Do not use
continue-on-errorfor required lint or test gates. - When versioned lock files are used, copy the matching lock to
composer.lockbefore validation and installation, then install with locked dependencies andrequire-lock-file: true. Runcomposer check-platform-reqsafter installation. - When no lock file is committed, do not claim
dependency-versions: lockedand do not setrequire-lock-file: true; deliberately install the dependency mode the project chose. - Let
ramsey/composer-installhandle Composer caching; do not add a second Composer cache step unless the project has a measured reason.
Job timeouts, PHP memory limits, and worker counts are operational starting points. Measure sustained runtime, peak memory, and runner capacity, then adjust them with headroom. Never use those settings to narrow a gate, skip targets, or turn a quality failure into a pass. The template's values are not target-project facts.
Updating Action Pins
When refreshing the template, resolve current releases and SHAs before editing:
gh release view --repo actions/checkout --json tagName,publishedAt,url
git ls-remote --tags https://github.com/actions/checkout.git 'refs/tags/<tag>'
gh release view --repo shivammathur/setup-php --json tagName,publishedAt,url
git ls-remote --tags https://github.com/shivammathur/setup-php.git 'refs/tags/<tag>'
gh release view --repo ramsey/composer-install --json tagName,publishedAt,url
git ls-remote --tags https://github.com/ramsey/composer-install.git 'refs/tags/<tag>'
Use the SHA returned for the exact tag. Do not use a moving branch, a major tag, or an abbreviated SHA.
Verification
After editing the workflow:
git diff --check
go run github.com/rhysd/actionlint/cmd/actionlint@latest .github/workflows/ci.yml
Include .github/workflows/context7.yml in that command when the Context7
workflow was installed.
Run local Composer checks that are reasonably available:
composer validate --strict --no-check-publish
# Repeat validation with each target-project lock when versioned locks exist.
composer format:check
composer phpstan
composer compat
composer loc-guard
composer tree-guard
composer deptrac
composer test:unit
composer test
Then check that nothing configured was left out of the workflow. Every Composer
script the project treats as a gate should appear in ci.yml:
composer run-script --list
grep -o 'composer [a-z:-]*' .github/workflows/ci.yml | sort -u
If a local check cannot run because of the local PHP version or missing tools, state that clearly and rely on the CI job that covers it.