Repository navigation
5.0.0-alpha1
What's new since 4.19.0
Highlights
5.0 is the first release under the Eddy name, and most of it is about where things live: the template has a new home, the build tooling became a Composer package, and GitHub Actions is the only CI left. Drupal 12 joins the CI matrix, and deployments no longer rewind branches. A project created from 4.x needs a one-off upgrade, which MIGRATION.md walks through.
The Drupal Extension Scaffold is now Eddy
The template moved from AlexSkrypnyk/drupal_extension_scaffold to drevops/eddy, next to Vortex, and the README now spells out how the 2 differ: Eddy maintains a contributed Drupal module or theme on GitHub, Vortex builds websites, and a repository uses one or the other.
| 4.x | 5.x | |
|---|---|---|
| Repository | AlexSkrypnyk/drupal_extension_scaffold |
drevops/eddy |
| Default branch | 1.x |
main |
| Template maintenance files | .scaffold/ |
.eddy/ |
| Update skill | update-consumer-drupal-extension-scaffold |
update-consumer-eddy |
| Installer | php init.php, answers from DEX_* |
php install.php, answers from INSTALL_* |
The branch rename only applies to the Eddy repository itself. New projects still build and deploy from 1.x, and when the installer runs during an update, it keeps whatever branches the project's committed test.yml lists.
The build tooling is a Composer package
In 4.x, every project carried its own copy of the build, run and deploy scripts in .devtools/, so a fix reached a project only when its maintainer merged the next release by hand. In 5.x, those scripts ship as the drevops/eddy-tooling package, and every project installs the same one:
4.x: each project carries its own copy 5.x: each project installs the same package
ahoy assemble, make assemble or CI ahoy assemble, make assemble or CI
│ │
▼ ▼
.devtools/assemble scripts/eddy-tooling
your copy, fixed only when you merge installs drevops/eddy-tooling ~1.0.0
the next release by hand into vendor/, and skips Composer
while nothing has changed
│
▼
vendor/bin/eddy-assemble
fixes arrive as patch releases,
your changes as patches or hooks
Each .devtools/<script> is now vendor/bin/eddy-<script>, and .devtools/browser start and stop are eddy-browser-start and eddy-browser-stop, while the ahoy and make commands keep their names. CI picks up tooling fixes on its own, and a local checkout picks them up after ahoy reset or make reset, which now also remove vendor/. A project-specific step goes into a custom lifecycle script, and a change to what a command does becomes a patch for the package in composer.dev.json.
The Cloudflare quick tunnel moved into the package as well: eddy-start, eddy-provision and eddy-stop run it whenever CLOUDFLARE_TUNNEL=1 is set, so projects no longer carry the scripts/*-cloudflared.sh hooks. The default Drupal major for local builds lives in extra.eddy.drupal-version in composer.dev.json, now that there's no script copy to rewrite.
Drupal 12 in CI
Drupal 12 support is opt-in: select 12 when install.php asks for the Drupal versions, or ask Claude to include it when it runs the update skill, and an upgrade keeps the majors your CI tests today. Opting in adds a README badge and 5 CI jobs, all on PHP 8.5, which Drupal 12 requires:
| Job | DRUPAL_VERSION |
Builds |
|---|---|---|
lint-d12, test-php-min-d12-stable, test-php-max-d12-stable |
12 |
Newest stable 12.x, or the newest pre-release until 12.0.0 ships |
test-php-min-d12-legacy |
12.0.0 |
Newest 12.0.x, the floor a ^12 requirement promises |
test-php-max-d12-canary |
12@beta |
Newest beta, release candidate or stable, so the next minor shows up early |
The jobs follow Drupal releases with no edits: until 12.0.0 ships they all build the newest Drupal 12 pre-release, and after that canary moves on to the 12.1 pre-releases while legacy stays on 12.0.x. Drupal 12 builds use Drush's 14.x development branch until Drush 14.0.0 is tagged, and they install drupal/core from source, since its packages leave out the tests directories your tests need.
Drupal 12 also needs 3 changes in your own code, which no template merge makes:
-
Add
^12tocore_version_requirementin your*.info.yml. -
Declare
string|falseas the return type ofgetMinkDriverArgs()if your FunctionalJavascript tests extend the base class the template generated. -
Keep both forms of test metadata: doc-comment annotations for PHPUnit 9.6 on Drupal 10, and PHP attributes for PHPUnit 12 on Drupal 12.
Builds pin Drupal core and apply your patches
Every build now pins Drupal core to 1 exact release and lets each other dependency take its most stable release that fits, so composer.dev.json and your composer.json only say which versions are acceptable:
DRUPAL_VERSION=11
│
▼
drupal/core* pinned to 1 exact release, the newest stable 11.x,
and printed in the build log
│
▼
other dependencies minimum-stability: dev, prefer-stable: true
each takes its most stable release that fits, and falls back
to a pre-release or development branch only when nothing
stable supports that core; the build log lists every branch
│
▼
extra.patches applied by cweagans/composer-patches 2.x
a patch that no longer applies fails the build
DRUPAL_VERSION takes values such as 11, 11.1.0, 11@beta or 12, but no longer @dev, since a development branch isn't a release.
The build also installs cweagans/composer-patches 2.x. From 4.14.0 to 4.19.0 it didn't, so unless your project required the plugin itself, Composer skipped every patch you declared without a word. Those patches apply now, and a patch that went stale while it was skipped fails the build until you fix or drop it.
Deployments that never rewind a branch
In 4.19.0, pushing a release tag force-pushed the tagged commit over a branch of your Drupal.org repository, so a tag behind that branch's tip rewound it. A late or re-run test run could do the same, the first deployment of a new branch failed, and the Deploy workflow failed on every run until deployment was set up. 5.x fixes all 4:
| Event | 4.19.0 | 5.x |
|---|---|---|
| Release tag pushed | Force-pushes the tagged commit over a branch | Pushes the tag only and leaves branches alone |
| Older commit deployed late or re-run | Force-pushes it over the newer branch tip | Skipped with a notice |
| First deployment of a branch | Fails | Creates the branch |
No DEPLOY_REMOTE secret |
Every Deploy run fails |
Skipped with a notice |
DEPLOY_BRANCH now applies to branch pushes only. If your release process relied on a tag push to update a branch, push the branch as well.
GitHub Actions is the only CI
CircleCI support is gone, along with the installer's CI provider question, so every project gets the same GitHub Actions workflows: test.yml, deploy.yml, draft-release-notes.yml and assign-author.yml. A project already on GitHub Actions has nothing to do. A project on CircleCI moves its settings to GitHub Actions, under Settings -> Secrets and variables -> Actions:
-
The private deployment key becomes the
DEPLOY_SSH_KEYsecret, and its fingerprint isn't needed anymore. -
DEPLOY_REMOTE,DEPLOY_USER_NAME,DEPLOY_USER_EMAILandDEPLOY_PROCEEDbecome secrets, andDEPLOY_BRANCHbecomes a repository variable. -
CODECOV_TOKENbecomes a secret, and eachCI_*_IGNORE_FAILUREtoggle becomes a repository variable. -
Each
ci/circleci: <job>check in branch protection becomes the GitHub Actions job of the same name.
Then stop building the project in CircleCI, or every push gets a failing "CircleCI Pipeline" check. CircleCI is gone has the details, including which branches deploy from GitHub Actions.
Upgrading a 4.x project
A 4.x project can't update itself to 5.x: its AGENTS.md fetches the update skill from the old .scaffold/ path, which returns 404, and curl -sSL saves that error page as the skill. Fetch the new skill once by hand, from the project root:
rm -rf .claude/skills/update-consumer-drupal-extension-scaffold
mkdir -p .claude/skills/update-consumer-eddy
curl -fsSL -o .claude/skills/update-consumer-eddy/SKILL.md \
https://raw.githubusercontent.com/drevops/eddy/main/.eddy/skills/update-consumer-eddy/SKILL.mdCommit your work in progress first, since the update replaces most of the project root. Then start Claude Code in the project and run /update-consumer-eddy. The skill opens a pull request with the 5.x files, drops .devtools/ and the Cloudflare hooks, and rewrites AGENTS.md, so every later update works as usual. It leaves 3 things to you: moving your .devtools/ edits, moving CircleCI settings, and adding Drupal 12 if you want it.
MIGRATION.md covers the same upgrade by hand, what changed in each area, and a checklist for after the upgrade.
Breaking changes
-
Renamed 'init.php' to 'install.php' and its 'INIT_' variables to 'INSTALL_'. @AlexSkrypnyk (#570)
The installer is nowphp install.php, and it reads pre-filled answers fromINSTALL_*variables such asINSTALL_NAMEandINSTALL_TYPE. In 4.19.0 it wasphp init.phpreadingDEX_*variables (theINIT_*prefix never shipped), so switch any script that runs it to the new name and prefix, and runphp install.php --helpfor the full list. The CI provider and Cloudflare questions are gone, so their variables have no replacement. -
[#553] Moved the Cloudflare tunnel hooks from 'scripts/' into the 'drevops/eddy-tooling' commands. @AlexSkrypnyk (#557)
eddy-start,eddy-provisionandeddy-stopnow run the Cloudflare quick tunnel themselves wheneverCLOUDFLARE_TUNNEL=1is set, in the shell or now also in.env, so the 3scripts/*-cloudflared.shhooks are gone and the installer no longer asks about them. A project created from 4.18.0 or 4.19.0 should delete its copies of the hooks, which would otherwise act on the same tunnel, and carry any edit to them as a patch fordrevops/eddy-tooling. The built-in tunnel also keepslocalhostamong the trusted hosts, so a tunnel provision no longer leaves the site answering 400 onhttp://localhost:<port>. -
[#540] Moved '.devtools' to the 'drevops/eddy-tooling' package that 'scripts/eddy-tooling' installs into 'vendor/'. @AlexSkrypnyk (#549)
The scripts in.devtools/now ship as thedrevops/eddy-toolingpackage, so every.devtools/<script>call becomesvendor/bin/eddy-<script>, while theahoyandmakecommand names stay the same. To upgrade, delete.devtools/and addscripts/eddy-tooling, merge the newcomposer.dev.jsonentries, and replace any.devtools/call in your own scripts, docs and CI steps. Move each edit you made to.devtools/into a custom lifecycle script or a patch for the package, as.devtools/is a package now describes. -
[#503] Dropped CircleCI support and the 'init.php' CI provider prompt. @AlexSkrypnyk (#542)
.circleci/config.ymlis gone and GitHub Actions is the only CI, so the installer no longer asks for a provider. A project on CircleCI should delete.circleci/and move its deployment settings to GitHub Actions secrets and variables, with the private key as theDEPLOY_SSH_KEYsecret, then swap theci/circleci: <job>checks in its branch protection for the GitHub Actions jobs of the same name. Stop building the project in CircleCI too, or every push gets a failing "CircleCI Pipeline" check. -
[#539] Renamed the project to Eddy and moved '.scaffold' to '.eddy'. @AlexSkrypnyk (#541)
The template now lives atdrevops/eddywith its update skill renamed toupdate-consumer-eddy, so a 4.x project can't update itself anymore: itsAGENTS.mdfetches the old skill from a.scaffold/path that returns 404, andcurl -sSLsaves the error page as the skill. Delete.claude/skills/update-consumer-drupal-extension-scaffold/, which 5.x no longer git-ignores, then fetchupdate-consumer-eddyonce by hand and run it, as Upgrade with Claude Code describes. The upgrade rewritesAGENTS.md, so every update after it works as usual.
Other changes
-
[#575] Added a 'MIGRATION.md' guide for upgrading a 4.x project to 5.x and restored 'css/' and 'js/' in the update skill. @AlexSkrypnyk (#579)
MIGRATION.mdwalks a 4.x project through the upgrade to 5.x, with Claude Code or by hand, and says for each change what it was, what it is now and what to do. The update skill now also restores a project's owncss/andjs/with the rest of its code, where it used to commit the template's example files over tracked ones. The 4.x skill had the same gap, so if your project tracks its owncss/orjs/, check that your last scaffold update kept them. -
Re-recorded the README demos against 'drevops/eddy-tooling' 1.0.0 and dropped the release notes skill's first-release path. @AlexSkrypnyk (#578)
The README'sbuild,lintandtestdemos now show the releasedvendor/bin/eddy-*commands instead of the.devtools/scripts they replaced, recorded againstdrevops/eddy-tooling1.0.0 from Packagist. The maintainer-onlycreate-eddy-tooling-release-notesskill always starts its range at a published release or a named bound, now that the package has a release. -
[#576] Switched the scaffold to 'main' and kept generated projects on '1.x' or their committed branches. @AlexSkrypnyk (#577)
The Eddy repository now develops onmain, and the update instructions inAGENTS.mdfetch the skill from it. Generated projects still build, deploy and draft release notes from1.x, and when the installer runs during an update, it now keeps the branches listed in the project's committedtest.ymlinstead of leaving1.xthere, so a project that builds from2.xno longer restores its branch filters by hand. -
Kept the git-ignored root '.chromedriver', '.env' and 'composer.lock' out of the repository copy that the functional tests run against. @AlexSkrypnyk (#574)
Eddy's own installer snapshot tests no longer fail locally afterahoy testormake testleaves.chromedriver/and.envin the repository root. This only affects work on Eddy itself, and generated projects don't change. -
Renamed the leftover 'scaffold' and 'devtools' names to 'eddy' and 'tooling'. @AlexSkrypnyk (#573)
Eddy's own maintenance workflows are noweddy-test.ymlandeddy-publish-tooling.yml, which the installer strips from new projects by theireddy-prefix, and the tooling commands live in theDrevOps\Eddy\Toolingnamespace. Generated projects only run thevendor/bin/eddy-*commands, so nothing in them changes. -
Stubbed 'MockTrait' functions through php-mock instead of the deprecated 'any()' matcher and made risky tests fail the '.eddy/tests' run. @AlexSkrypnyk (#572)
Eddy's own test suite no longer uses PHPUnit's deprecatedany()matcher, and a risky test now fails the run with exit code 1 instead of ending it early with a passing exit code. Generated projects don't change. -
Updated the '.eddy/tests' dependencies to their latest PHP 8.3 releases and adapted the suite to PHPCS 4, Coder 9 and Rector 2.7. @AlexSkrypnyk (#571)
Eddy's own test suite now runs on PHPCS 4, Coder 9, PHPStan 2.3, Rector 2.7 and PHPUnit 12.5, and PHPCS now checks the 10 extensionless tooling scripts it used to skip. Generated projects don't change, and neither does what theeddy-*commands do. -
Added the 'create-eddy-tooling-release-notes' skill and bumped 'shivammathur/setup-php' to 2.40.0. @AlexSkrypnyk (#569)
Generated projects'test.ymlmoves toshivammathur/setup-php2.40.0, which officially supports theubuntu-26.04runners its jobs use, and Eddy's own macOS CI legs now restore a prebuilt PHP instead of building it against the latest Homebrew. The newcreate-eddy-tooling-release-notesskill helps maintainers write release notes fordrevops/eddy-tooling, and generated projects never receive it. -
[#563] Pointed the 'test.yml' Composer cache at the directory Composer writes to and keyed it per matrix leg and month. @AlexSkrypnyk (#568)
The Composer cache intest.ymlnever stored anything, because it pointed at/tmp/composer-cache, a directory Composer never wrote to. It now caches the directorycomposer config cache-files-dirreports, keyed by job, UTC month and a hash ofcomposer.jsonandcomposer.dev.json, so each CI job restores its package downloads instead of fetching about 150 archives every run. The gain is seconds per job, not minutes, and the README explains how to clear a job's cache. -
[#562] Started the browser in 'make test' and 'ahoy test' before they run the FunctionalJavascript suite. @AlexSkrypnyk (#567)
ahoy testandmake testrun every PHPUnit suite, but onlytest-functional-javascriptstarted the WebDriver browser, so the README's build-then-test flow failed on the template's own FunctionalJavascript test. In a project that keeps FunctionalJavascript tests, both now start the browser first and leave it running untilbrowser-stop, sotestneeds Chrome, or Docker withWEBDRIVER_BACKEND=selenium, whiletest-unit,test-kernelandtest-functionalstill run without one. -
[#564] Corrected the 'init.php' marker comments, documented the template markers and added 'TemplateMarkersTest'. @AlexSkrypnyk (#566)
A broken template marker block, such as a misspelled token or a missing closing marker, now fails Eddy's own test suite instead of shipping into generated projects. The maintainer guide in.eddy/CLAUDE.mdgains the marker rules and checklists for adding a Drupal major or a development tool. -
Corrected doc drift around 'test', documented the CI toggles and coverage upload, flattened 'eddy-deploy' and trimmed test comments. @AlexSkrypnyk (#565)
The README now documents the 8CI_*_IGNORE_FAILURErepository variables that let a CI step report problems without failing its job, and theCODECOV_TOKENsecret for optional Codecov uploads. The rest is cleanup that leaves behavior alone:eddy-deployandeddy-startread more simply, and the generatedAGENTS.mdlists every fieldeddy-infoprints. -
Split 'eddy-browser' into 'eddy-browser-start' and 'eddy-browser-stop' and aligned the banners and logs of the tooling commands. @AlexSkrypnyk (#561)
The browser commands areeddy-browser-startandeddy-browser-stop, replacing.devtools/browser startandstopfrom 4.19.0, and each ends with a completion banner like the other tooling commands, whileahoy browser-start,make browser-startand theirstopcounterparts keep their names. The PHP webserver now logs to.logs/php.loginstead of the/tmp/php.logevery project on the machine shared, and chromedriver logs to.logs/chromedriver.log. -
[#555] Removed project prefixes from environment and CI variable names. @AlexSkrypnyk (#560)
Eddy's own CI toggles are nowCI_YAMLLINT_IGNORE_FAILUREandCI_ACTIONLINT_IGNORE_FAILURE, matchingCI_ZIZMOR_IGNORE_FAILURE, and a test stops project prefixes from coming back. Nothing a generated project reads was renamed: theDEPLOY_*secrets, theCI_*toggles intest.ymland the tooling's variables keep their names, and the installer's move toINSTALL_*is listed under Breaking changes. -
[#556] Scoped the Zizmor audit to '.github' so the init fixtures no longer report as unpinned actions. @AlexSkrypnyk (#559)
Eddy's own Zizmor audit only reads.github/, so its test fixtures stop producing false "unpinned action" alerts in code scanning. Generated projects don't run this workflow. -
Switched actionlint to the 'kjanat/actionlint' fork and renamed its ignore-failure toggle to 'EDDY_CI_ACTIONLINT_IGNORE_FAILURE'. @AlexSkrypnyk (#558)
Eddy's own workflow lint runskjanat/actionlint1.17.0, which recognizes theubuntu-26.04runner label that norhysd/actionlintrelease knows, so the check passes without being told to ignore failures. The toggle ended up asCI_ACTIONLINT_IGNORE_FAILUREafter #560, and generated projects don't run this workflow. -
[#525] Made regenerating the README terminal recordings produce byte-identical SVGs. @AlexSkrypnyk (#552)
Regenerating the README's terminal demos now produces byte-identical SVGs when nothing changed, so a diff in them is a real change. This only affects work on Eddy itself. -
[#518] Fixed deployments that overwrote or rewound the remote branch, failed on a new branch, or failed without configuration. @AlexSkrypnyk (#551)
A release tag now deploys only the tag and leaves the remote's branches alone, a branch deploy is skipped with a notice once its commit is no longer the branch tip, and the first deployment of a new branch creates it instead of failing. Until theDEPLOY_REMOTEsecret is set, theDeployworkflow reports a notice instead of failing,DEPLOY_BRANCHapplies to branch pushes only, and the SSH key step runs on Node.js 24. If your release process relied on a tag push to update a branch, push the branch as well. -
Replaced the README logo with an adaptive 'logo.svg', added a social preview card and regenerated the README recordings. @AlexSkrypnyk (#550)
The README shows the new Eddy logo, which switches its lettering for dark mode, and.eddy/assets/gains a social preview card for the repository. The installer removeslogo.svgfrom new projects, so they still start with the placeholderlogo.png. -
[#544] Installed 'cweagans/composer-patches' in the build so declared dependency patches apply. @AlexSkrypnyk (#548)
From 4.14.0 to 4.19.0, the build didn't installcweagans/composer-patches, so unless a project required the plugin itself, Composer silently skipped every declared patch. The build now installs composer-patches 2.x, the patches incomposer.jsonandcomposer.dev.jsonapply, and a patch that no longer applies fails the build withNo available patcher was able to apply patch. -
[#545] Fixed generated 'CONTRIBUTING.md' documenting both 'make' and 'ahoy' commands whatever wrapper 'init.php' keeps. @AlexSkrypnyk (#547)
A generatedCONTRIBUTING.mdnow documents only the command wrapper the project keeps:ahoycommands for Ahoy,makecommands for the Makefile, both when both are kept, and thevendor/bin/eddy-*commands when neither is. Before, every project got both sets, even the ones with only 1 wrapper or none. -
Rewrote the README introduction to separate Eddy from Vortex, and fixed documentation, comments and code that disagreed with each other. @AlexSkrypnyk (#546)
The README now explains that Eddy maintains a contributed module or theme on GitHub, while Vortex builds websites, and a pass of the docs against the code fixed examples that didn't work, such as running a single test, which needs a path relative tobuild/. 2 tooling helpers changed along the way:drush()prints a failed command's output whenDEBUG=1is set, andkill_port()stays quiet whenlsofis missing. -
Update release-drafter/release-drafter digest to 72967cd @renovate[bot] (#537)
-
[#418] Added Drupal 12 stable, legacy and canary CI jobs, with exact core release pinning and a shared dependency stability policy. @AlexSkrypnyk (#538)
A project that selects Drupal 12 in the installer gets 5 Drupal 12 CI jobs on PHP 8.5 and a README badge, the example extension declarescore_version_requirement: ^10 || ^11 || ^12, and Drush 14 joins the allowed range. Every build now pins Drupal core to 1 exact release and resolves other dependencies withminimum-stability: devandprefer-stable: true, listing any package installed from a development branch, andDRUPAL_VERSIONno longer accepts@dev.
Full Changelog: 4.19.0...5.0.0-alpha1