iru-setup-php-gitignore
Create or update the root `.gitignore` file for a PHP/Composer project — `vendor/`, Composer credentials (`auth.json`), PHPUnit/Pest caches (`.phpunit.cache/`, `.phpunit.result.cache`), report output
SKILL.md.claude/skills/iru-setup-php-gitignore/SKILL.md
GitHubSetup PHP Gitignore
Create or refresh the root .gitignore for a PHP/Composer project. Never overwrite an existing .gitignore without
showing the user exactly what would change and getting explicit approval first — a .gitignore can carry
hand-added, project-specific entries that must not be silently dropped.
Step 0 — Parse args
This skill takes a single optional key, as a key: value line in args: on-existing (ask/keep/update/
replace, default ask) — how to treat an existing .gitignore in scripted runs (an orchestrator forwards it only
if its own args received it). Ignore any other key (orchestrators may pass shared keys such as mode or flavor;
the flavor is detected in Step 3 from the files themselves, and mode: existing alone doesn't skip Step 5's
question). If args is absent or unparseable, treat on-existing as ask.
Step 1 — Check whether .gitignore already exists at the repository root
- Not found: tell the user "Creating
.gitignorefrom scratch — none found." Continue to Step 2. Because there's nothing to lose, the final write happens without needing approval — go straight from composing the content to writing it, then report in Step 6. - Found: tell the user "Updating existing
.gitignore" and warn that one already exists, so this skill will propose changes for them to review rather than overwrite it silently. Read its current content now — it matters later, both to avoid proposing duplicate entries and because Step 4 must preserve any hand-written, project-specific lines this skill doesn't know how to regenerate. Ifon-existing: keep, stop here: leave the file untouched and go straight to Step 6's report. Otherwise continue to Step 2.
Step 2 — The universal base template
Every PHP/Composer project gets this baseline, regardless of flavor:
# Composer
/vendor/
composer.phar
# Composer credentials for private repositories / tokens — never commit
/auth.json
# Test / coverage / report output (Clover + JUnit under build/logs/, coverage HTML)
/build/
/coverage/
# PHPUnit / Pest caches (PHPUnit 10+ cacheDirectory, and the PHPUnit 9 result cache)
.phpunit.cache/
.phpunit.result.cache
# Code-style caches (PHP-CS-Fixer, incl. the pre-v3 name)
.php-cs-fixer.cache
.php_cs.cache
# Environment files (never commit real secrets; the example file documents the shape)
.env
.env.backup
.env.production
.env.local
.env.*.local
# Logs
*.log
# Repository-wide lock the coding skills use to serialize container-backed test runs
.integration-test.lock/
# UI test screenshots (regenerated on every UI test run; the docs copy the ones they use into images/ui/)
.ui-screenshots/
# OS noise
.DS_Store
Thumbs.db
# IDE noise
.idea/
.fleet/
.zed/
.nova/
.phpactor.json
# VS Code — ignore the directory but keep the shared, non-machine-specific config files
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/tasks.json
This section is unconditional — include it verbatim regardless of flavor, with two adjustments when an existing
.gitignore already decides otherwise:
.vscode: if the existing file ignores the whole directory (/.vscodeor.vscode/, as the Laravel skeleton does), keep that line and leave out the template's.vscode/*+!block — git can't re-include files under an excluded directory, so the two would contradict each other.- Env files:
.env.example(and Laravel's.env.testing, which holds test-only settings) stay tracked — that's why the template lists explicit names instead of.env*. If the existing file uses.env*with!.env.example, keep it and don't add the explicit lines.
composer.lock is never ignored — and if an existing .gitignore ignores it, propose removing that line (call
it out explicitly in Step 5's question). Rationale, per what this catalog scaffolds:
- Laravel application: the lock is what makes
composer installreproduce the exact tested dependency set in CI, in the container image, and in production — the Composer documentation's standard advice for applications. - WordPress plugin/theme, PrestaShop module: they're deployed artifacts, not libraries resolved by a consumer's
Composer —
vendor/is built into the release zip withcomposer install --no-dev, so the lock is what makes two builds of the same tag ship the same code, and it's whatcomposer audit, Dependabot, and OSV-Scanner read. - Plain Composer library (not scaffolded here): the lock doesn't affect consumers (their own resolution wins), but committing it keeps the library's own CI and contributors on a known-good tool/test set; this catalog commits it there too. A project that deliberately doesn't is respected — keep its existing ignore line if the user says so in Step 5.
Note on /build/: report tools write build/logs/clover.xml, build/logs/junit.xml, and coverage HTML there,
and @wordpress/scripts writes compiled block assets to build/. Some WordPress plugins deliberately commit
their compiled build/ (the @wordpress/create-block template does not ignore it). If .gitignore doesn't exist
yet, check git ls-files -- build coverage before assuming; when files are already tracked there, ask via
AskUserQuestion whether to keep ignoring the directory (and untrack it — the release job then has to run
npm run build before packaging) or narrow the entry to /build/logs/ + /build/coverage/ — never ignore a
directory out from under tracked files without asking.
Step 3 — Detect the flavor and project-specific additions
Don't guess; only add a section below when the thing it targets is actually present. Use the same markers
iru-explore and iru-setup-php-repository rely on: composer.json, artisan, a root PHP file with a Plugin Name: header, a root style.css with Theme Name: / theme.json, a root <module>.php whose class extends Module (or PaymentModule/CarrierModule) / config.xml, package.json, phpcs.xml.dist/.phpcs.xml.dist/
phpcs.xml, phpstan.neon/phpstan.neon.dist, and docs/antora.yml.
Laravel application (
composer.jsonrequireslaravel/frameworkand anartisanfile exists): add the entries the Laravel skeleton's own root.gitignorecarries that the base doesn't already cover:/node_modules /public/build /public/hot /public/storage /public/fonts-manifest.dev.json /storage/*.key /storage/pail /.codex /.cursor/ _ide_helper.php _ide_helper_models.php .phpstorm.meta.php Homestead.json Homestead.yaml(
public/build+public/hotare Vite's output and dev-server marker,public/storageis thestorage:linksymlink,storage/*.keyare the Passport/encryption keys,_ide_helper*.php/.phpstorm.meta.phpare barryvdh/laravel-ide-helper output.) The skeleton also ships nested.gitignorefiles —storage/app/,storage/app/{public,private}/,storage/framework/and itscache/,cache/data/,cache/locks/,sessions/,testing/,views/,storage/logs/,bootstrap/cache/, anddatabase/(*.sqlite*). They're part of the scaffold, not this skill's file: never delete or fold them into the root file. Only if one of them is missing in an existing app, add the root-level equivalent so runtime files can't be committed:/storage/app/* !/storage/app/public/ !/storage/app/private/ /storage/framework/cache/* /storage/framework/sessions/* /storage/framework/testing/* /storage/framework/views/* /storage/logs/* /bootstrap/cache/*.php /database/*.sqlite*and mention in Step 6 which nested files were missing.
WordPress plugin or theme (a root plugin header or
style.cssTheme Name:/theme.json): add# WordPress # Release archives (wp-scripts plugin-zip, wp dist-archive, the release job) *.zip # Per-developer @wordpress/env overrides (the shared .wp-env.json stays tracked) .wp-env.override.jsonIf a
package.jsonexists (asset tooling,@wordpress/env,@wordpress/scripts), also add the Node section below. When@wordpress/scriptsis inpackage.jsondevDependencies, its compiled output lands inbuild/— already covered by the base/build/entry, subject to the tracked-files check in Step 2. Nothing else from WordPress core belongs here:@wordpress/envkeeps its WordPress install outside the repository (~/.wp-env), andbin/install-wp-tests.shinstalls the test library under the system temp directory.PrestaShop module (a root class that
extends Module/PaymentModule/CarrierModule, usually withconfig.xml): add, matching PrestaShop's own native modules (e.g.ps_emailsubscription,ps_facetedsearch):# PrestaShop # Per-language module-list caches PrestaShop regenerates; config.xml itself stays tracked /config_*.xml # Release archive of the module *.zipconfig.xmlstays tracked: PrestaShop regenerates it when missing, but committing it keeps the module's<version>in sync with the class (whichiru-php-bump-versionrelies on when the file is tracked) and makes the module list show correct metadata before the first install. If the module builds front-end assets (apackage.jsonwith a build script, typically intoviews/dist/), add the Node section and that output directory.Node tooling (
package.jsonat the root, any flavor not already covered by the Laravel section's/node_modules): addnode_modules/ npm-debug.log* .eslintcachePlaywright browser UI tests (
@playwright/testor@wordpress/scriptsinpackage.jsondevDependencies, or aplaywright.config.*at the root — whatiru-setup-php-laravel,iru-setup-php-wordpressandiru-setup-php-prestashopscaffold): add# Playwright (browser UI tests) — run output, HTML/blob reports, local browser cache /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/and, for a WordPress plugin/theme whose tests run through
wp-scripts test-playwright(its config writes traces/screenshots toartifacts/):/artifacts/Screenshots the UI contract asks for go to
.ui-screenshots/(base template), not to these directories.PHPCS cache (
phpcs.xml.dist/.phpcs.xml.dist/phpcs.xmlsets<arg name="cache" .../>): ignore the exact path itsvaluenames (e.g..phpcs-cache); if thecachearg has no value, PHPCS uses the system temp directory and nothing is added.PHPStan
tmpDirinside the project (phpstan.neon/phpstan.neon.dist/tests/phpstan/phpstan.neonsetsparameters.tmpDirto a relative path such asvar/phpstanor.phpstan-cache): ignore that path, unless it's underbuild/(already covered). By default PHPStan uses the system temp directory and nothing is added. Also ignore a project-localphpstan.neonwhen the tracked config isphpstan.neon.dist(PHPStan's convention for a per-developer override).Antora documentation build output (
docs/antora.ymlordocs/antora-playbook.ymlexists — seeiru-setup-antora): adddocs/build/ docs/node_modules/Don't invent additional entries beyond what's actually detected — a project with no Node tooling, no Antora docs, or no PHPCS cache gets no section for them.
Step 4 — Compose the proposed content
Concatenate Step 2's base template with whichever Step 3 sections matched, each under its own comment header for
readability (e.g. # Laravel, # WordPress, # PrestaShop, # Node, # Playwright, # Antora). Drop any line
that duplicates one already present (including path-equivalent forms such as /vendor vs /vendor/, and
/node_modules vs node_modules/). If Step 1 found an existing .gitignore, compose two drafts:
- Merged (the Update draft): keep every existing line that isn't already covered by the composed template
(hand-added project-specific ignores must survive), add only the missing template lines, and remove a
composer.lockignore line (Step 2) — the one deliberate deletion, listed in Step 5's question. - Template-only (the Replace draft): exactly the composed template. Note which existing lines it would drop.
Step 5 — If .gitignore already existed, get approval before writing
Skip this step entirely if Step 1 found no existing file — proceed straight to Step 6's write.
- Show the user the merged draft as a diff against the current file (writing the draft to a temp path and running
git diff --no-index <current> <draft>gives a clean unified diff). If the merged draft is identical to the current file, say so, treat the outcome as "kept as is", and skip the question. - If
on-existingisupdateorreplace, apply it without asking (still show the diff of what is being written). Otherwise ask viaAskUserQuestion(header.gitignore), with these consequences in the option descriptions:- Update (Recommended) — "Write the merged draft: add the missing entries from this skill's template and keep
every line you added by hand. You've seen the diff; nothing you customized is dropped." — plus, when it applies,
"…except the
composer.lockignore line, which is removed so the lock is committed (see rationale)". - Keep as is — "Leave
.gitignoreuntouched. Your entries stay exactly as they are, but vendor/test/tooling output the template would ignore (listed in the diff) may show up as untracked files." - Replace — "Write the template-only draft. Matches this house template exactly, but the hand-added lines
listed below are lost (recoverable only from git history)." — list those lines in the question text.
There is no partial-apply option — if the user wants only some of the proposed lines (e.g. keep ignoring
composer.lockfor a library), let them say so in free text and revise the draft before writing.
- Update (Recommended) — "Write the merged draft: add the missing entries from this skill's template and keep
every line you added by hand. You've seen the diff; nothing you customized is dropped." — plus, when it applies,
"…except the
- Only proceed to Step 6's write on Update or Replace; on Keep as is, write nothing.
Step 6 — Write .gitignore and report
Write the chosen draft to .gitignore at the repository root — or nothing, if it was kept as is. Then summarize:
whether this run was from scratch or an update, the action taken (created / updated / replaced / kept as is, and
whether that came from the user's answer or from on-existing), the detected flavor, which conditional sections from
Step 3 were included versus skipped and why (e.g. "no Node section — no package.json", "no PHPCS cache entry —
phpcs.xml.dist sets no cache path", "no Playwright section — no playwright.config.* or Playwright dependency"),
any Laravel nested .gitignore files found missing, whether a composer.lock ignore line was removed, and — if
.gitignore already existed — which hand-added lines were preserved (Update) or dropped (Replace).
Warn explicitly:
- Review the generated
.gitignorebefore committing — detection is heuristic (manifest/config-file signals), not a guarantee that every relevant path was found. - If the repository already had files tracked under a path this skill now ignores (a committed
vendor/,build/, orauth.json),git rm -r --cachedit manually after reviewing the diff — writing.gitignorealone does not untrack already-committed files. A committedauth.jsonor.envmay contain credentials: rotate them, since they remain in git history. - Commit
composer.lock; for WordPress/PrestaShop, the release zip is built from it withcomposer install --no-dev, sovendor/is never committed. - Which values were verified versus inferred: the Laravel section was checked against the root and nested
.gitignorefiles of a freshcomposer create-project laravel/laravel(Laravel 13 skeleton) when this skill was authored, and the PrestaShop section against PrestaShop's own native modules'.gitignorefiles; the WordPress section follows the@wordpress/create-blocktemplate and the@wordpress/envdocumentation; every other Step 3 entry is inferred from local config signals and should be spot-checked against the project's actual generated output.