Files
AeonDumpManager/docs/research/changelog-and-php-floor.md
haylanandClaude Bot 953f4ee579
plugin-checks / version-changelog (pull_request) Successful in 22s
plugin-checks / package (pull_request) Successful in 1m1s
plugin-checks / phpunit (pull_request) Canceled after 5m12s
docs: research shopware-cli changelog handling and PHP floor
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 00:53:26 +02:00

11 KiB

Research: shopware-cli changelog handling and the PHP floor for a Shopware 6.6 plugin

Sources are primary only: shopware/shopware-cli source (main branch, fetched 2026-10-04), shopware/docs (the repo behind developer.shopware.com, main), and shopware/shopware tags. Paths below are relative to those repos. Refs are branch heads or tags as noted; no commit SHAs were pinned.

Summary

  • extension package --release does not parse CHANGELOG.md. It only generates one, from git commits, and only if changelog.enabled: true is set in .config/shopware-extension.yml. Our pipeline does not pass --release.
  • Parsing happens elsewhere: in account producer extension upload (Store push) and in extension get-changelog. The parser treats every line starting with # as a version key.
  • Our Keep-a-Changelog file is not compatible with that parser. The key becomes # [0.1.0] - 2026-10-03, never 0.1.0, so the Store upload would fail with "english changelog is missing". This only matters if we later use account producer extension upload; today we publish to Gitea and a Composer registry.
  • Minimal fix if we ever upload to the Store: add CHANGELOG_en-GB.md with # x.y.z headings and flat bullets, with no ### sub-headings. Keep CHANGELOG.md for humans; the localized file wins for en-GB.
  • PHP: Shopware 6.6.x requires ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 (6.6.0.0 was ~8.2.0 || ~8.3.0). Our php: ^8.2 is consistent but broader than core; it is harmless because shopware/core ~6.6.0 already pins the effective range.
  • Recommendation: keep ^8.2 or use >=8.2. Do not copy core's tilde list. Shopware's own docs skeleton declares no php at all, only shopware/core.

Q1. Changelog handling

1.1 What the parser does (verified in source)

File: internal/extension/changelog.go (shopware-cli, main).

File discovery and locale:

files, err := filepath.Glob(path + "/CHANGELOG*.md")
...
language := strings.Trim(strings.ReplaceAll(strings.ReplaceAll(filepath.Base(file), "CHANGELOG", ""), ".md", ""), "_")
if len(language) == 0 { language = "en-GB" }
  • CHANGELOG.md maps to en-GB.
  • CHANGELOG_en-GB.md maps to en-GB, and CHANGELOG_de-DE.md maps to de-DE.
  • If both CHANGELOG.md and CHANGELOG_en-GB.md exist, both land in the same map slot. filepath.Glob returns lexically sorted names and . sorts before _, so CHANGELOG_en-GB.md is processed last and overwrites. This ordering is inferred from Go's documented sorted-directory behaviour; I did not run it.

Section splitting (parseMarkdownChangelog):

for _, line := range strings.Split(content, "\n") {
    if strings.HasPrefix(line, "#") {
        ...
        currentVersion = strings.Trim(strings.TrimPrefix(line, "#"), " ")
  • Every line that starts with #, at any heading depth, starts a new "version" section.
  • The key is the line minus one leading #, with spaces trimmed. Only a single # is removed.
  • The body is converted to HTML with goldmark via internal/markdown. Headings inside bodies are rendered as <span class="hN">.
  • Upstream's own tests use this exact shape: "# 1.0.0\n\n- Test\n- Test2\n# 2.0.0\n- Test3\n- Test4\n" (internal/extension/changelog_test.go).

Version lookup (parseExtensionMarkdownChangelog):

  • The key looked up is ext.GetVersion().String(), which is the plain version from composer.json, for example 0.1.0.
  • changelogs["en-GB"][version] must exist, or it returns an error ("english changelog is missing").
  • de-DE is optional and falls back to the English file. If a de-DE file exists but lacks the version, it errors.
  • The docs agree: "The zip file contains a CHANGELOG*.md file with a changelog entry for the new version. Having a German changelog is optional." (products/tools/cli/shopware-account-commands/releasing-extension-to-shopware-store.md).

1.2 Where it is used

GetChangelog() is called in only two non-test places:

  • internal/account-api/producer_store_upload.go: account producer extension upload sends de_DE and en_GB texts as the Store changelog.
  • cmd/extension/extension_changelog.go: extension get-changelog.

Not called from cmd/extension/extension_package.go, nor from validation: no changelog reference was found under internal/validation.

1.3 What --release does

cmd/extension/extension_package.go:

extensionPackageCmd.Flags().BoolVar(&extensionReleaseMode, "release", false, "Prepare a release build: generate the changelog if enabled and, for apps, remove the secret from manifest.xml")

internal/extension/zip.go, PrepareExtensionForRelease:

if ext.GetExtensionConfig().Changelog.Enabled {
    ...
    changelogFile := fmt.Sprintf("# %s\n%s", v.String(), content)
    os.WriteFile(path.Join(extensionRoot, "CHANGELOG_en-GB.md"), ...)
}
if ext.GetType() == "plugin" { return nil }
// apps only: remove setup/secret from manifest.xml
  • Added: a generated CHANGELOG_en-GB.md, containing # <version> plus one bullet per git commit between the last tag and the current commit (template internal/changelog/changelog.tpl). It only happens if changelog.enabled: true. The default is false (internal/changelog/changelog.go, Enabled bool).
  • Stripped: the app secret in manifest.xml. This does not apply to plugins, which return early.
  • Not stripped or rewritten: an existing CHANGELOG.md is left untouched. When enabled, the generated file overwrites any existing CHANGELOG_en-GB.md.
  • This confirms # x.y.z is the format the tool itself writes.
  • Docs on release mode and changelog.enabled: products/tools/cli/extension-commands/build.md, "Release mode" section.

1.4 Our repo

  • .gitea/actions/package/action.yml:29 runs shopware-cli extension package . --output-directory .build --filename ... --git-commit "$GITHUB_SHA". There is no --release, and no changelog: key was found in .config or .gitea.
  • The release notes come from our own awk in .gitea/workflows/release.yml, which matches ## [x.y.z]. That is independent of shopware-cli.
  • CHANGELOG.md uses ## [0.1.0] - 2026-10-03 and ### Added/Changed/....

1.5 Compatibility verdict

Walking CHANGELOG.md through parseMarkdownChangelog by hand (not executed):

  • ## [0.1.0] - 2026-10-03 gives key # [0.1.0] - 2026-10-03, which never equals 0.1.0.
  • Each ### Added and similar heading becomes its own bogus "version", so the entries under it are split off.

Result: incompatible. account producer extension upload and extension get-changelog would fail with "english changelog is missing". extension package, with or without --release, would not fail, because it does not call the parser. extension validate does not parse it either, as far as I could find.

Minimal change, only needed if we adopt Store upload:

# 0.1.0

- Services and routes are now configured in YAML; the plugin requires PHP `^8.2`.
- Release pipeline: ...
  • Put it in CHANGELOG_en-GB.md at the plugin root, with one # <exact composer version> section per release and flat bullets. Prefix bullets with Added/Changed/Fixed if the categories matter.
  • No ### sub-headings, and no brackets or dates in the heading.
  • CHANGELOG_de-DE.md is optional.
  • The alternative is to enable changelog.enabled: true plus --release and let the CLI generate it from commits. That would not use our hand-written entries.
  • The packaged zip must contain the file. I did not verify whether .config/shopware-extension.yml pack excludes would remove it.

Not verified: the Store's own server-side handling of the changelog HTML. The CLI source only shows what is sent (de_DE/en_GB text), not what the Store renders. The Store API and backend are not public.

Q2. PHP constraint

2.1 What Shopware 6.6 requires

require.php in shopware/shopware src/Core/composer.json and the root composer.json:

Ref require.php
v6.6.0.0 "php": "~8.2.0 || ~8.3.0"
v6.6.10.28 (latest 6.6 tag seen) "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
  • The v6.6.10.28 root composer.json has the same line as core.
  • A fetch of the 6.6.x branch-head composer.json returned no usable content, so only the two tags above are verified.
  • Official docs, guides/hosting/index.md ("Recommended stack and supported versions"): PHP minimum 8.2+, recommended 8.4.
  • guides/development/dev-environment.md: "PHP Version | 8.2, 8.3, 8.4, 8.5".
  • Fetching the rendered developer.shopware.com requirements page returned no PHP row. It defers to the hosting guide, which is the source above.
  • The repo's own CLAUDE.md says PHP ^8.1. That is stale for 6.6, since core requires 8.2+. It is outside this task's scope.

2.2 Is php: ^8.2 right?

composer.json today:

"require": { "php": "^8.2", "shopware/core": "~6.6.0" }
  • ^8.2 means >=8.2.0 <9.0.0. It is consistent with core's floor.
  • It is wider than core's ceiling (<8.6). The plugin's PHP range is the intersection of its own constraint and core's. Because shopware/core ~6.6.0 is also required, Composer resolves to core's tilde list anyway. The plugin constraint adds no extra restriction in practice.
  • Copying core's ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 would only make sense if the plugin itself breaks on a specific PHP minor. That would also force a release every time Shopware adds a PHP minor.
  • Practical conclusion: ^8.2 is fine and not too strict. >=8.2 behaves the same in practice. The only real argument for changing it is wanting the plugin's PHP floor to stay visible in its own metadata, which ^8.2 already does.

2.3 What Shopware's skeletons recommend

  • guides/plugins/plugins/creating-plugins.md (docs repo): the example plugin composer.json has only "require": { "shopware/core": "~6.7.0" }. There is no php key, and "Depending on other plugins" repeats the same shape.
  • shopware-cli's scaffolding (internal/extension/create.go) does not set a php constraint; the only "php" hit in that package is a test fixture, internal/extension/root_test.go:233, with ">=8.1".
  • So the official guidance is to constrain shopware/core and leave PHP to core. A plugin php constraint is optional. If you want one, mirroring the supported floor (>=8.2 or ^8.2) is reasonable.
  • I did not find a SwagPluginSkeleton repo (fetching shopware/SwagPluginSkeleton returned 404).

Could not verify

  • That the Store's server-side processing matches what the CLI sends. The Store backend is not public.
  • That .config/shopware-extension.yml pack excludes would keep or remove a CHANGELOG_en-GB.md in the zip.
  • The exact ordering behaviour of filepath.Glob for the two en-GB files was inferred from Go's documented sorted results, not run.
  • No source evidence of whether the Store accepts HTML headings; the CLI renders headings as <span class="hN"> per a comment in internal/markdown.