drupal-scaffold
Skill0 starsChanged 24 days agoMITView on GitHub
Hand a Drupal project's scaffold files (web/.htaccess, index.php, robots.txt, default.settings.php, README/example files…) back to drupal/core-composer-scaffold on a Dockerized, Makefile-driven project: untrack and gitignore every generated file, move project customizations into scaffold/*-prepend.txt / *-append.txt (or a full replacement), make branch-specific .htaccess edits (prod redirects, firewall, force-www, preprod Basic auth) safe on every environment, and add a preprod-auth GitLab CI job when the preprod needs a password. Use when the user mentions drupal-scaffold, file-mapping, "[web-root]/.htaccess": false, a hand-edited .htaccess that conflicts or silently changes between develop / release / master, prod redirects in .htaccess, a committed .htpasswd, or preprod Basic auth. IMPORTANT: all commands MUST go through Makefile targets; check what the CI and deploy read before untracking anything.
In this repository17 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- d11Skill
- drupal-build-foundationsSkill
- drupal-build-intakeSkill
- drupal-buildSkill
- drupal-build-unitSkill
- drupal-rgaaSkill
- gulp-to-viteSkill
- jquery-4-migrationSkill
- lando-to-dockerSkill
- phpcbfSkill
- phpcsSkill
- php-deprecations-auditSkill
- php-docker-upgradeSkill
- php-standardsSkill
- phpstanSkill
- sass-migratorSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 2 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 67
Read the root .gitignore too.
- Very specific: line 99
Do not port default.settings.php changes.
- Distinctive: line 123
Verify each generated file is exactly the parts in order (the plugin adds a newline between the parts):
- Very specific: line 144
Keep the root .editorconfig / .gitattributes tracked if the project lists them under initial: the plugin writes those once and never …
- Very specific: line 154
Split CI jobs: if the composer job's artifacts:paths lists only vendor/ and web/core/, later jobs see no scaffold output.
- Very specific: line 161
The server: when the deploy recipe runs composer install there, generation happens on the server too.
- Distinctive: line 170
When the preprod must be password-protected:
- Very specific: line 172
Add the preprod-auth job from references/htaccess.md § Preprod auth.
- Very specific: line 184
Before resolving a develop ← release / master merge, list the files that merge without conflict but change:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Drupal scaffold files under composer control
drupal/core-composer-scaffold writes ~20 files from core
(web/core/composer.json → extra.drupal-scaffold.file-mapping). Projects
usually freeze some of them with false and hand-edit them per branch. Symptoms:
.htaccessdiffers on every long-lived branch (prod redirects onmaster, Basic auth onrelease). A merge between them often does not conflict: when only one side changed the file since the merge base, git silently takes that side. Preprod auth lands indevelop, then in prod.- Frozen files stay on the previous core's version after a major bump.
- A
.htpasswdis committed with the preprod credentials.
Target state: every scaffold output is generated and untracked. Project-specific
content lives in scaffold/ and is identical on every branch. Environment-only
behaviour sits behind a host condition, or in CI.
Golden rule: everything goes through the Makefile
| Need | Make target |
| Regenerate the scaffold files | make composer c='drupal:scaffold' |
Refresh content-hash after editing extra | make composer c='update --lock' |
| Validate | make composer c='validate --no-check-publish' |
| Smoke tests | make cr, make drush c='…' |
0. Ask once
- The long-lived branches and the environment each one deploys (e.g.
develop→ none,release→ preprod,master→ prod). - Whether the preprod must be password-protected (→ step 6).
- The ticket, and whether
gm:merge-requestis available for the MR.
1. Inventory
# Every file core scaffolds, and where to.
python3 -c "import json;m=json.load(open('web/core/composer.json'))['extra']['drupal-scaffold']['file-mapping'];print('\n'.join(m))"
# Project overrides: locations, file-mapping, gitignore, initial.
python3 -c "import json;print(json.dumps(json.load(open('composer.json'))['extra']['drupal-scaffold'],indent=2))"
git ls-files web/.htaccess web/index.php web/update.php web/robots.txt web/.ht.router.php \
web/sites/default/default.settings.php web/sites/default/default.services.yml \
web/sites/*.php web/sites/*.yml web/sites/README.txt web/*.txt web/*.md \
web/.csslintrc web/.eslint* web/example.gitignore web/*/README.txt recipes/README.txt \
.editorconfig .gitattributes
git ls-files | grep -E '(^|/)\.htpasswd$'Read the root .gitignore too. Template-era entries such as /.htaccess or
/index.php with no web/ prefix ignore nothing when the web root is web/.
2. Classify every tracked scaffold file, on every long-lived branch
Diff each branch's copy against the stock file of that branch's core version. Otherwise the core diff (D10 → D11 stock) hides the project's changes:
S=<session scratchpad dir>
for b in origin/develop origin/release origin/master; do
v=$(git show "$b:composer.lock" | python3 -c "import json,sys;print(next(p['version'] for p in json.load(sys.stdin)['packages'] if p['name']=='drupal/core'))")
curl -s "https://git.drupalcode.org/project/drupal/-/raw/$v/core/assets/scaffold/files/htaccess" > "$S/htaccess.$v"
git show "$b:web/.htaccess" | diff "$S/htaccess.$v" - > "$S/htaccess.${b#origin/}.diff"
done(Repeat for each tracked file; the stock
name is the right-hand side of core's file-mapping.)
| Delta vs stock | Mapping | Git |
| none | remove the override | untrack |
| additions at the top or bottom, or whose position does not matter | {"prepend": "scaffold/<name>-prepend.txt"} / "append" | untrack |
the file is entirely the project's (custom robots.txt) | "scaffold/robots.txt" (full replacement) | untrack |
| edits in the middle of a stock file that must stay where they are | keep false, last resort | keep tracked |
| present on one branch only (env-specific) | make it safe everywhere (host condition) or move it to CI (step 6) | — |
.htaccess customizations almost always fit a prepend once you know which
directives are position-independent: see references/htaccess.md. Drop the
lines that only duplicate stock rules; the reference lists the dangerous ones.
Do not port default.settings.php changes. Projects put real settings in
settings.php or an included file; stock comment blocks are core's job.
3. composer.json
"drupal-scaffold": {
"locations": { "web-root": "web/" },
"gitignore": false,
"file-mapping": {
"[web-root]/.htaccess": { "prepend": "scaffold/htaccess-prepend.txt" },
"[web-root]/robots.txt": "scaffold/robots.txt"
}
}scaffold/sits at the project root, outside the web root, so it is never served. It must be shipped with the code: check the deploy rsync excludes."gitignore": false: otherwise the plugin, oncevendoris ignored, writes a.gitignoreinto each destination directory. One explicit root block is easier to review.extrais part ofcontent-hash: runmake composer c='update --lock', thenmake composer c='drupal:scaffold'.
Verify each generated file is exactly the parts in order (the plugin adds a newline between the parts):
{ cat scaffold/htaccess-prepend.txt; echo; cat web/core/assets/scaffold/files/htaccess; } | diff - web/.htaccess && echo OK4. Untrack and ignore
git rm --cached <every scaffold output> # local files stay# Drupal scaffold output
/web/.htaccess
/web/index.php
…one line per destination from step 1…
/web/.htpasswdKeep the root .editorconfig / .gitattributes tracked if the project lists
them under initial: the plugin writes those once and never overwrites them.
5. Prove the build chain regenerates them
Untracked outputs must be rebuilt wherever code is packaged:
composer install --no-scriptsstill scaffolds. The scaffold is a plugin listener, and Composer only skips script listeners (EventDispatcher::getListeners:$this->runScripts ? getScriptListeners() : []).- Split CI jobs: if the composer job's
artifacts:pathslists onlyvendor/andweb/core/, later jobs see no scaffold output. Add the destinations, or the deploy uploads a tree with noindex.php. - Grep CI and deploy for scaffold paths:
grep -nE 'default\.settings|htaccess|index\.php|robots' .gitlab-ci.yml deploy*.php. A classic case is asettingsjob that runscp default.settings.php settings.phpand depends on the composer job's artifacts. - The server: when the deploy recipe runs
composer installthere, generation happens on the server too. When it only rsyncs, the CI artifacts are all it gets.
Local proof: rm web/.htaccess web/index.php && make composer c='install', and
both files come back.
6. Preprod Basic auth: a CI job, not a branch-only .htaccess
When the preprod must be password-protected:
- Add the
preprod-authjob fromreferences/htaccess.md§ Preprod auth. It runs in thedeploystage, on the preprod branch only, withneeds: [deploy]. It writes.htpasswdfrom a masked CI variable and appends the auth block to the deployed.htaccess. - Untrack any committed
.htpasswdand ignore it. Rotate the password: the hash stays in the history. - MR prerequisite: create the CI variable (e.g.
PREPROD_HTPASSWD) before the next preprod deploy. Without it the job writes an empty.htpasswdand locks everyone out.
7. Merging long-lived branches
Before resolving a develop ← release / master merge, list the files that
merge without conflict but change:
git merge --no-commit --no-ff origin/release
git diff --cached --stat HEAD # an .htaccess here = env-specific content arrivinggit merge-tree --name-only only lists conflicts, so it misses these.
8. Verify
On the local stack (Apache reads the generated web/.htaccess):
B=https://<project>.dev.localhost
for u in / /<a-redirected-path> /<path>/ "/?q=union+select+(1)" /.git/config /composer.json; do
curl -sk -o /dev/null -w "$u %{http_code} %{redirect_url}\n" "$B$u"; doneExpected: front and back office 200, each 301 to its target, trailing slash
301, firewall 403, /.git/config and /composer.json 403.
Host-specific rules (force-www on the prod domain) cannot be tested through
Traefik, which routes by Host and answers 404 for an unknown host. Hit Apache
directly:
docker compose … exec -T apache sh -c 'printf "GET /foo HTTP/1.0\r\nHost: example.com\r\n\r\n" | nc -w3 localhost 80 | grep -E "^HTTP|^Location"'A 400 for any other host is Drupal's trusted_host_patterns, not a redirect.
9. Close
One commit for the mapping + scaffold/ + untracking, one for the preprod
auth if any, on the ticket's branch, and a Draft MR. The MR body states:
- Prerequisites: the CI variable, the password rotation.
- Behaviour changes in prod: dropped duplicate rules, the force-www host restriction.
- Production actions: none to run (the deploy's
composer installregenerates). After delivery, check the www redirect, a few 301s and the back office.
Any later hand edit of web/.htaccess (e.g. the node_modules deny rule from
gulp-to-vite) goes into scaffold/htaccess-prepend.txt. The next scaffold run
overwrites the generated file.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

