Silverstripe Module SS6 Upgrade
Repeatable workflow for upgrading a single Silverstripe module (a silverstripe-vendormodule composer package with its own repo, CI, and Packagist listing) from SS5 to SS6. This is the loop you run once per module across a suite of repos such as the dynamic/* open-source modules.
Scope: This skill covers the module/package upgrade loop. It is one of three related skills:
- silverstripe-version-upgrade: full project upgrades (a website root: recipes, DDEV, data migration, deployment). Its SS6 sections are the canonical reference for namespace renames and breaking changes; this skill points there instead of restating them.
- ss6-data-migration: DB content migration after a project upgrade.
- This skill: the module loop (constraints, code sweep, CI, tag, default branch).
Branch-naming and default-branch conventions come from ss-branch-strategy (from
jsirish/workflow-skills); this skill applies that convention, it does not redefine it.
Reference implementation for Phases 1-4 and 7: dynamic/silverstripe-calendar#112 (branch 2 to 3, tagged 3.0.0). Its branch 3 does NOT yet demonstrate Phases 3.4/6 (recipe-testing still ^3, PHPUnit 9 config, no CI run recorded); treat those phases as prescriptive, not descriptive.
Upgrade Phases (Summary)
| Phase | Key Actions |
|---|---|
| 1. Ordering & Assessment | Place the module in the dependency graph; upgrade leaf modules first |
| 2. Branch Setup | New integer branch off the current SS5 default, per ss-branch-strategy |
| 3. Composer Constraints | php ^8.3, framework/cms ^6, per-dependency bumps, branch-alias |
| 4. Code Sweep | SS6 namespace renames, typed signatures, BuildTask, BaseElement changes |
| 5. Rector, PHPStan, PHPCS | Automated rules first, then static analysis and style |
| 6. CI & PHPUnit Config | silverstripe/gha-ci matrix regeneration, PHPUnit 11 config |
| 7. PR, Tag, Default Branch | Merge to the integer branch, tag, flip default, verify Packagist |
| 8. Testbed Verification | Install into an SS6 project, dev/build flush=1, exercise the module |
Do not rationalize
Every phase gate requires evidence: actual command output, not a recollection or an inference. The shortcuts agents talk themselves into on module upgrades, and the required counter-behavior:
| Rationalization | Required behavior |
|---|---|
| "The constraints look right" | Run composer validate and a real composer update (testbed or CI) and paste the resolved versions. A constraint that never resolved is a guess. |
| "Rector handled the renames" | Run the grep sweep after Rector and paste the zero-hit output. Rector does not rewrite YAML config, Injector strings, or docblocks. |
| "CI is green, so the module works" | CI installs the module against a bare scaffold. Install it into an SS6 testbed, run dev/build flush=1, and paste the output. Phase 8 is not optional. |
| "This module has no BuildTasks / elements / validate() overrides" | Paste the grep output proving it: `rg "extends BuildTask |
| "I tagged it, so Packagist has it" | Paste the version list from https://packagist.org/packages/<vendor>/<module> (or its API). Webhooks fail silently. |
| "The default branch is flipped" | Paste gh api repos/<vendor>/<module> --jq .default_branch. |
| "Tests probably still pass on PHPUnit 11" | Run vendor/bin/phpunit and paste the summary line. PHPUnit 10+ changed config schema and assertions; no run, no claim. |
Phase 1: Ordering & Assessment
1.1 The dependency ordering problem
Modules form a dependency graph, and a module cannot resolve silverstripe/framework ^6 while one of its own requirements still caps at SS5. Upgrade leaf modules first, then modules that depend on them, then recipes last:
- Leaf modules (no dependencies on other in-house modules): e.g.
dynamic/silverstripe-elemental-baseobject,dynamic/flexslider,dynamic/silverstripe-calendar. - Mid-tier modules (depend on leaf modules): e.g. the
dynamic/silverstripe-elemental-*content blocks, which requiresilverstripe-elemental-baseobject. - Recipes (aggregate everything): e.g.
dynamic/recipe-silverstripe-base-site,dynamic/recipe-silverstripe-essentials-website. A recipe is upgraded by bumping every member constraint, so it goes last.
While a dependency is upgraded on its integer branch but not yet tagged, depend on it with a dev constraint (^6@dev); minimum-stability: dev + prefer-stable: true in the module's composer.json makes this resolvable. Replace with the tagged constraint once the dependency releases.
1.2 Assess the module
gh repo view <vendor>/<module> --json defaultBranchRef --jq .defaultBranchRef.name
gh api repos/<vendor>/<module>/branches --jq '.[].name'
# What does it require, and are SS6 releases available for each?
cat composer.json | python3 -c "import json,sys; print(json.load(sys.stdin)['require'])"
For each third-party requirement, confirm an SS6-compatible release exists on Packagist. If one does not, follow the fork-and-upstream workflow in ss-branch-strategy or find a maintained replacement (see 3.3).
Evidence gate (Phase 1): a written ordering position ("leaf", "mid-tier after X", "recipe") and, for every requirement, either the SS6-compatible version number or the replacement/fork decision. Paste the requirement list with the target version beside each entry.
Phase 2: Branch Setup
Integer branch naming per ss-branch-strategy: the new branch integer is the module's next major version (or the recipe version, if the repo versions against a shared recipe). Example: dynamic/silverstripe-calendar was on 2 (SS5), so SS6 work went to 3.
# Create the new integer branch from the current SS5 default
git fetch origin
git checkout -b 3 origin/2
git push -u origin 3
# Do the work on a feature branch targeting the new integer branch
git checkout -b feature/ss6-upgrade
Do not flip the default branch yet; that happens after merge and tag (Phase 7). The old integer branch (2) stays for SS5 backports.
Phase 3: Composer Constraints
3.1 Core bumps
Every module gets, at minimum:
{
"require": {
"php": "^8.3",
"silverstripe/cms": "^6.0"
},
"extra": {
"branch-alias": {
"dev-3": "3.x-dev"
}
}
}
- Modules that only need the framework require
silverstripe/framework: ^6instead ofsilverstripe/cms. - The
branch-aliaskey must match the new integer branch (dev-3for branch3) and the new major (3.x-dev). Without it,^3@devconstraints in downstream consumers will not resolve to the branch. - Elemental content blocks bump
dnadesign/silverstripe-elementalto^6.
3.2 Common third-party bumps
Observed across the dynamic/* SS6 sweep (verify each on Packagist rather than trusting this table blindly):
| Package | SS6 constraint |
|---|---|
silverstripe/lumberjack |
^4 |
symbiote/silverstripe-gridfieldextensions |
^5 |
symbiote/silverstripe-queuedjobs |
^6 |
unclecheese/display-logic |
^4 |
silverstripe/linkfield |
^5 (the 4.x line is CMS 5 only and cannot resolve against cms ^6) |
silverstripe/vendor-plugin (if pinned) |
^3 |
3.3 Replacements for packages with no SS6 release
| Package | Replacement |
|---|---|
ryanpotter/silverstripe-color-field |
tractorcow/silverstripe-colorpicker (no tagged SS6 release yet; require "dev-master as 5.0" inline alias) |
nathancox/embedfield |
fromholdio/silverstripe-embedfield ^5.1 |
sheadawson/silverstripe-linkable |
silverstripe/linkfield (^5 on SS6; run the linkable data migration on SS5 with linkfield ^4 BEFORE the SS6 bump) |
The project-level removal list (this table is a module-focused superset) lives in silverstripe-version-upgrade references/version-map-ss6.md.
3.4 Dev dependencies
"require-dev": {
"cambis/silverstan": "^2.1",
"phpstan/extension-installer": "^1.3",
"silverstripe/recipe-testing": "^4",
"squizlabs/php_codesniffer": "^3.7"
}
[!WARNING] A stale
silverstripe/recipe-testing: ^3blocks resolution: recipe-testing 3.x requiressilverstripe/framework ^5. SS6 needs^4, which also moves the test suite to PHPUnit^11.3(see Phase 6.2).
Also drop unused requirements while you are in the file; the reference implementation removed dft/silverstripe-frontend-multiselectfield after confirming zero usages by grep.
Evidence gate (Phase 3): paste the output of composer validate plus a successful dependency resolution (a composer update in a throwaway checkout, the Phase 8 testbed, or the first green CI install). The edited JSON alone does not clear this gate.
Phase 4: Code Sweep
The canonical SS6 rename and breaking-change tables live in silverstripe-version-upgrade (Phase 4 and the "SS6 Breaking Changes" section) and references/version-map-ss6.md. Do not restate them; run them against the module's src/ and tests/ instead of app/src/:
# Namespace renames (each old FQCN is a runtime fatal in SS6)
rg "SilverStripe\\\\View\\\\ViewableData" src/ tests/
rg "SilverStripe\\\\View\\\\ArrayData" src/ tests/
rg "SilverStripe\\\\ORM\\\\ArrayList" src/ tests/
rg "SilverStripe\\\\ORM\\\\ValidationResult" src/ tests/
rg "SilverStripe\\\\ORM\\\\ValidationException" src/ tests/
Module-specific checklist (the items that recur across dynamic/* modules):
ArrayListmoved toSilverStripe\Model\List\ArrayList;ArrayDatatoSilverStripe\Model\ArrayData;ViewableDatais nowSilverStripe\Model\ModelData.ValidationResultmoved toSilverStripe\Core\Validation\ValidationResult, and everyvalidate()override must declare the return type:public function validate(): ValidationResult.- BuildTask signature:
run($request)becomesexecute(InputInterface $input, PolyOutput $output): intreturningCommand::SUCCESS, withprotected static string $commandName. The full old-vs-new table and a task template are in silverstripe-version-upgrade Phase 4 and its references/data-migration-tasks.md. BaseElement::getType()/getDescription()are replaced by config: useprivate static string $class_description(and keep$singular_name/$plural_nameconfig for the type label). Applies to every elemental content block module.ModelDatasubclass overrides of__get,__set,__isset,hasField,getField,setField, etc. must add the typed parameters and return types the SS6 parent declares, or PHP throws declaration-compatibility fatals.forTemplate()must declare: stringand never returnfalse; return''instead.CMSPageAddControllerwas removed. Any extension bound to it (commonly via anupdatePageOptions(FieldList $fields)hook) is dead code with no error - the extended class simply no longer exists, so the hook never fires. Rebind toCMSMainAddForm::updateFieldsandCMSMain::updateDoAdd. Full detail (theRecordTypefield rename, how to read submitted values, the live-CMS verification note) is in silverstripe-version-upgrade's "SS6 Breaking Changes" section - this is the canonical source for SS6 breaking changes per Phase 4 above; do not restate it here.
# Prove presence or absence of each pattern
rg "extends BuildTask" src/
rg "extends BaseElement" src/
rg "function getType\(|function getDescription\(" src/
rg "public function validate\(\)(?!\s*:)" src/ --pcre2
rg "function forTemplate\(\)(?!\s*:)" src/ --pcre2
rg "updatePageOptions" src/
Evidence gate (Phase 4): paste the grep sweep output showing zero remaining hits for every old FQCN and untyped signature, run after the Rector pass in Phase 5 (Rector misses YAML, string class references, and docblocks).
Phase 5: Rector, PHPStan, PHPCS
Tooling, configs, and the SS6 Rector level set (SilverstripeLevelSetList::UP_TO_SS_6_0) are documented in silverstripe-version-upgrade references/code-quality.md. For a module the paths are src/ and tests/, not app/src/:
vendor/bin/rector --dry-run # review, then apply
vendor/bin/rector
vendor/bin/phpstan analyse src/ tests/
vendor/bin/phpcs src/ tests/
phpstan.neon.dist in a module scans the module's own src/ (there is no vendor-path problem here; that warning in code-quality.md applies to project roots).
Evidence gate (Phase 5): paste the summary line of each tool run (Rector applied-rules count, PHPStan error count, PHPCS error count). Then re-run the Phase 4 greps and paste the zero-hit output.
Phase 6: CI & PHPUnit Config
6.1 gha-ci matrix
Modules use the shared Silverstripe CI workflow:
# .github/workflows/ci.yml
name: CI
on:
push:
pull_request:
workflow_dispatch:
permissions: {}
jobs:
ci:
name: CI
permissions:
pull-requests: read
contents: read
actions: write
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1
with:
phpcoverage: false
js: false
gha-ci derives the PHP and database matrix from composer.json constraints (via silverstripe/gha-generate-matrix), so once php ^8.3 and cms ^6 land on the branch, the next run generates the SS6 matrix automatically. Manual matrix edits are only needed for non-standard setups. Verify:
- The workflow references
@v1(the floating major tag), not a pinned old minor that predates SS6 support. - The workflow triggers on
pushandpull_requestso the new integer branch gets runs. - No branch filters still name the old branch only.
6.2 PHPUnit config for SS6
silverstripe/recipe-testing ^4 moves modules to PHPUnit ^11.3. Update phpunit.xml.dist: the schema version bumps, and PHPUnit 10+ moved coverage includes from <coverage> into <source>:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/silverstripe/cms/tests/bootstrap.php"
colors="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.3/phpunit.xsd">
<testsuites>
<testsuite name="default">
<directory>tests/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src/</directory>
</include>
</source>
</phpunit>
The bootstrap path (vendor/silverstripe/cms/tests/bootstrap.php, or vendor/silverstripe/framework/tests/bootstrap.php for framework-only modules) is unchanged.
Evidence gate (Phase 6): a green CI run on the PR, with the matrix jobs visibly running PHP 8.3+ against CMS 6. Paste the run URL and the job list (gh run view <id> --json jobs --jq '.jobs[].name').
Phase 7: PR, Tag, Default Branch, Packagist
Open the PR from
feature/ss6-upgradeinto the new integer branch (3), referencing the module's SS6 issue.Merge once CI is green and review passes.
Tag the release on the integer branch:
git checkout 3 && git pull git tag 3.0.0 git push origin 3.0.0During a coordinated multi-module sweep it is normal to hold tagging until downstream consumers have verified against
^3@dev; tag when the module is proven in the testbed (Phase 8).Flip the default branch to the new integer branch, keeping the old one for backports:
gh api -X PATCH repos/<vendor>/<module> -f default_branch=3Verify Packagist picked up the tag and the new branch alias (webhook-driven, but webhooks fail silently):
curl -s https://repo.packagist.org/p2/<vendor>/<module>.json \ | python3 -c "import json,sys; print([v['version'] for v in list(json.load(sys.stdin)['packages'].values())[0]][:5])"
Evidence gate (Phase 7): paste the merged PR URL, the tag from git tag -l '3.*', the default branch from gh api repos/<vendor>/<module> --jq .default_branch, and the Packagist version list showing the new tag.
Phase 8: Testbed Verification
CI proves the module installs and its unit tests pass against a scaffold. It does not prove the module works inside a real SS6 site. Install it into an SS6 testbed project (any DDEV project already on SS6, or a scratch silverstripe/installer ^6 checkout):
# In the testbed project
ddev composer require <vendor>/<module>:^3@dev # or ^3.0 once tagged
ddev sake dev/build flush=1
Then exercise the module:
dev/buildoutput shows the module's tables and fields, with no errors and no obsolete-type warnings introduced by the module.- The CMS section or elemental block the module provides loads and saves.
- For modules with BuildTasks:
ddev sake taskslists them (proves thecommandNameconversion worked), and each task runs toSUCCESS. - The front-end template output renders (no
forTemplate()fatals).
Evidence gate (Phase 8): paste the dev/build output and, for each BuildTask, the ddev sake tasks listing plus one task run. "CI was green" does not clear this gate.
Per-module checklist
- Ordering position established; all dependencies have an SS6 path (Phase 1)
- Integer branch created off the SS5 default; feature branch targets it (Phase 2)
-
php ^8.3, framework/cms^6, third-party bumps,branch-alias(Phase 3) - Namespace renames, typed signatures, BuildTask,
$class_description,forTemplate(): string(Phase 4) - Rector SS6 level set applied; PHPStan and PHPCS clean; greps re-run (Phase 5)
- gha-ci on
@v1, matrix regenerated; PHPUnit 11 config (Phase 6) - PR merged, tagged, default branch flipped, Packagist verified (Phase 7)
- Installed in an SS6 testbed,
dev/build flush=1clean, functionality exercised (Phase 8)
Related skills
- silverstripe-version-upgrade: the project-level SS6 upgrade this skill feeds into; canonical home of the SS6 rename tables, breaking changes, and version maps.
- ss6-data-migration: DB content migration after the project upgrade.
- ss-branch-strategy (from
jsirish/workflow-skills): the integer-branch, default-branch, and fork-and-upstream conventions applied in Phases 2 and 7.