From c08d27c8e10790ab4930ad9e90cc93981535c04e Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 00:49:53 +0200 Subject: [PATCH 01/10] fix(release): repack the zip root-level for the Composer registry; make release steps idempotent Gitea's registry answered 400 'composer.json file is missing' for the shopware-cli zip (single top-level folder). Repack the same prebuilt content with composer.json at the root. Drop the tag-exists skip guard so a half-finished release can be re-run; reuse an existing release/asset. Bump to 0.1.1. Co-Authored-By: Claude Sonnet 5.5 --- .gitea/workflows/release.yml | 73 +++++++++++++++++++++++------------- CHANGELOG.md | 7 ++++ composer.json | 2 +- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 3a91d7e..af04609 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -3,6 +3,10 @@ name: release # Plugin-agnostic: owner, repo, host, package name and version come from the `github` context # and composer.json. shopware-cli runs from its official Docker image, intentionally unpinned (latest). # RELEASE_TOKEN is the single token used for releases, assets and the Composer registry. +# +# Every step is idempotent (release/asset reuse, registry 409 = already published), so a failed run can +# simply be re-run and picks up where it stopped. There is deliberately no "tag exists -> skip" guard: +# it made a half-finished release (tag + release but no registry package) impossible to heal. on: push: @@ -23,17 +27,17 @@ jobs: container: image: ghcr.io/shopware/shopware-cli:latest steps: - - name: Ensure helper tools (jq, curl, unzip, git) + - name: Ensure helper tools (jq, curl, unzip, zip, git) run: | - command -v jq >/dev/null && command -v curl >/dev/null && command -v unzip >/dev/null && command -v git >/dev/null && exit 0 - (apt-get update -q && apt-get install -y -q --no-install-recommends jq curl unzip git) || apk add --no-cache jq curl unzip git + command -v jq >/dev/null && command -v curl >/dev/null && command -v unzip >/dev/null && command -v zip >/dev/null && command -v git >/dev/null && exit 0 + (apt-get update -q && apt-get install -y -q --no-install-recommends jq curl unzip zip git) || apk add --no-cache jq curl unzip zip git - uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.RELEASE_TOKEN }} - - name: Determine version and guard against re-release + - name: Determine version, tag and file names id: meta run: | set -e @@ -47,21 +51,13 @@ jobs: echo "tag=${TAG}" echo "zip=${REPO_NAME}-${TAG}.zip" } >> "$GITHUB_OUTPUT" - if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q "${TAG}"; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "Tag ${TAG} already exists, nothing to release." - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi # Reuse the zip the PR workflow already built and tested for this exact tree. - name: Tree hash of this commit - if: steps.meta.outputs.skip == 'false' id: tree run: echo "hash=$(git rev-parse "${GITHUB_SHA}^{tree}")" >> "$GITHUB_OUTPUT" - name: Restore the tested zip from the PR run - if: steps.meta.outputs.skip == 'false' id: prebuilt uses: actions/cache/restore@v4 with: @@ -69,18 +65,16 @@ jobs: key: plugin-zip-${{ steps.tree.outputs.hash }} - name: Name the prebuilt zip - if: steps.meta.outputs.skip == 'false' && steps.prebuilt.outputs.cache-hit == 'true' + if: steps.prebuilt.outputs.cache-hit == 'true' run: mv .build/package.zip ".build/${{ steps.meta.outputs.zip }}" # Packages only on a cache miss; always validates and asserts what ships (guards a bad cache entry). - uses: ./.gitea/actions/package - if: steps.meta.outputs.skip == 'false' with: filename: ${{ steps.meta.outputs.zip }} build: ${{ steps.prebuilt.outputs.cache-hit != 'true' }} - name: Extract changelog section - if: steps.meta.outputs.skip == 'false' run: | awk -v ver="${{ steps.meta.outputs.version }}" ' BEGIN { found=0 } @@ -93,26 +87,54 @@ jobs: cat release-notes.md - name: Create Gitea release (creates the tag) and upload the zip - if: steps.meta.outputs.skip == 'false' env: TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | set -e API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" - BODY=$(jq -n --arg tag "${{ steps.meta.outputs.tag }}" --arg sha "$GITHUB_SHA" --rawfile notes release-notes.md \ - '{tag_name:$tag, target_commitish:$sha, name:$tag, body:$notes}') - RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "$BODY" "${API}/releases" | jq -r .id) + TAG="${{ steps.meta.outputs.tag }}" + ZIP="${{ steps.meta.outputs.zip }}" + CODE=$(curl -s -o /tmp/release.json -w "%{http_code}" -H "Authorization: token ${TOKEN}" "${API}/releases/tags/${TAG}") + if [ "$CODE" = "200" ]; then + echo "Release ${TAG} already exists, reusing it." + else + BODY=$(jq -n --arg tag "$TAG" --arg sha "$GITHUB_SHA" --rawfile notes release-notes.md \ + '{tag_name:$tag, target_commitish:$sha, name:$tag, body:$notes}') + curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "$BODY" "${API}/releases" > /tmp/release.json + fi + RELEASE_ID=$(jq -r .id /tmp/release.json) echo "release id: ${RELEASE_ID}" - curl -sf -H "Authorization: token ${TOKEN}" \ - -F "attachment=@.build/${{ steps.meta.outputs.zip }}" \ - "${API}/releases/${RELEASE_ID}/assets?name=${{ steps.meta.outputs.zip }}" > /dev/null + if jq -e --arg n "$ZIP" '.assets[]? | select(.name == $n)' /tmp/release.json > /dev/null; then + echo "Asset ${ZIP} already uploaded." + else + curl -sf -H "Authorization: token ${TOKEN}" -F "attachment=@.build/${ZIP}" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP}" > /dev/null + fi - - name: Publish the same zip to the Composer registry - if: steps.meta.outputs.skip == 'false' + # Gitea's Composer registry needs composer.json at the zip root, but shopware-cli zips wrap + # everything in one top-level folder (the registry answers 400 "composer.json file is missing"). + # Same prebuilt content, repacked root-level. + - name: Repack the zip for the Composer registry + run: | + set -e + rm -rf /tmp/repack && mkdir /tmp/repack + unzip -q ".build/${{ steps.meta.outputs.zip }}" -d /tmp/repack + if [ -f /tmp/repack/composer.json ]; then + ROOT=/tmp/repack + else + ROOT=$(find /tmp/repack -mindepth 1 -maxdepth 1 -type d | head -1) + fi + test -f "$ROOT/composer.json" || { echo "::error::no composer.json found in the packaged zip"; exit 1; } + OUT="$PWD/.build/composer-${{ steps.meta.outputs.zip }}" + (cd "$ROOT" && zip -qr "$OUT" .) + unzip -l "$OUT" | grep -E ' composer\.json$' > /dev/null || { echo "::error::repacked zip has no root-level composer.json"; exit 1; } + echo "repacked: $OUT" + + - name: Publish to the Composer registry run: | STATUS=$(curl -s -o /tmp/composer-publish.log -w "%{http_code}" \ -u "${GITHUB_REPOSITORY_OWNER}:${{ secrets.RELEASE_TOKEN }}" \ - -T ".build/${{ steps.meta.outputs.zip }}" \ + -T ".build/composer-${{ steps.meta.outputs.zip }}" \ "${GITHUB_SERVER_URL}/api/packages/${GITHUB_REPOSITORY_OWNER}/composer") cat /tmp/composer-publish.log if [ "$STATUS" != "201" ] && [ "$STATUS" != "409" ]; then @@ -121,7 +143,6 @@ jobs: fi - name: Link Composer package to this repository - if: steps.meta.outputs.skip == 'false' run: | ENCODED_NAME=$(printf '%s' "${{ steps.meta.outputs.package_name }}" | sed 's#/#%2F#g') STATUS=$(curl -s -o /dev/null -w "%{http_code}" -u "${GITHUB_REPOSITORY_OWNER}:${{ secrets.RELEASE_TOKEN }}" -X POST \ diff --git a/CHANGELOG.md b/CHANGELOG.md index dde9226..71e4443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.1] - 2026-10-04 + +### Fixed + +- Release pipeline: the Composer registry rejected the packaged zip (`composer.json file is missing`) because `shopware-cli` zips wrap everything in a top-level folder. The same prebuilt content is now repacked root-level for the registry upload. +- Release steps are idempotent (existing release and asset are reused, registry 409 counts as published) and the "tag exists -> skip" guard is gone, so a failed release can be re-run to completion. + ## [0.1.0] - 2026-10-03 ### Changed diff --git a/composer.json b/composer.json index bd55f73..2bab846 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Export database dumps for Shopware 6 environments.", "type": "shopware-platform-plugin", "license": "MIT", - "version": "0.1.0", + "version": "0.1.1", "keywords": [ "shopware", "shopware6", From 5f8cc9bdadcc50e4c29b2efb4a5cd72c882a250a Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 00:49:42 +0200 Subject: [PATCH 02/10] docs: refresh CLAUDE.md and compliance spec after guideline PR Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 29 ++++++++++++++++++++-- docs/spec/shopware-guideline-compliance.md | 28 ++++++++++++--------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 634eee2..c7e7ce8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,9 @@ the full build spec live under `.scratch/admin-dump-manager/` and `.scratch/dump - Plugin store-config UI fields (e.g. `maxDumps`, `retantionDays`) are defined in `src/Resources/config/config.xml` and read at runtime via Shopware's SystemConfigService. - `src/Resources/app/administration/` holds the admin UI module (Vue-based Shopware Administration - extension), currently just the `swag-example` scaffold module. + extension). The `aeon-dump-manager` module (`src/module/aeon-dump-manager/`) provides the list page + (`page/aeon-dump-manager-list`), en-GB/de-DE snippets and ACL privilege roles (`acl/`: `viewer`, + `creator`, `deleter`); `src/service/dump-api.service.ts` is the client for the dump API routes. ## Running the plugin @@ -51,6 +53,10 @@ activates this plugin inside a real Shopware kernel for the test run (`KERNEL_CL `Shopware\Core\Kernel` in `phpunit.xml`). This means tests require a working Shopware installation/DB to bootstrap against — they are not runnable as an isolated PHP package. +In CI (`phpunit` job) a Shopware 6.6 project is prepared once (`composer create-project` + phpunit, +cached monthly), the plugin is copied into `custom/plugins`, and `system:install` runs once into +`shopware_test` (MariaDB 10.11 service), so the bootstrapper only activates the plugin. + ```bash vendor/bin/phpunit # full suite vendor/bin/phpunit --filter TestClassName # single test class @@ -59,10 +65,29 @@ vendor/bin/phpunit tests/SomeTest.php # single file ## Dependencies -- PHP `^8.1`, `shopware/core` `~6.6.0` (from `composer.json`); `phpunit/phpunit` `^10.0` as the only +- PHP `^8.2`, `shopware/core` `~6.6.0` (from `composer.json`); `phpunit/phpunit` `^10.0` as the only dev dependency. - PSR-4 autoload: `AeonDumpManager\` → `src/`, `AeonDumpManager\Tests\` → `tests/`. +## CI / release pipeline + +Gitea workflows in `.gitea/`, plugin-agnostic (name/version come from `composer.json`). `shopware-cli` +runs from its official Docker image, intentionally unpinned (`latest`): a red build after a CLI release +means fix the config. + +- `workflows/plugin-checks.yml` (PRs into `main`) has three jobs: `version-changelog` (every PR into + `main` must bump `composer.json` version and add a matching `## [x.y.z]` CHANGELOG heading), `package` + (shared composite action `actions/package`: `shopware-cli extension package`, `extension validate` on + the zip, `scripts/ci/assert-built-assets.sh`), and `phpunit`. The package job caches the tested zip + keyed by the commit's tree hash. +- `workflows/release.yml` (push to `main`, serialized, skipped if tag `v` exists): reuses the + cached zip when the tree hash matches (else packages again, always re-validates), creates the Gitea + release + tag via API with the zip as asset, publishes to the Composer registry, then links the + package to the repo. Uses `RELEASE_TOKEN`. +- Required checks on `main` (`plugin-checks / version-changelog|package|phpunit (pull_request)`) are set + once with `scripts/protect-main.sh` (`tea api`). Build excludes/validation live in + `.config/shopware-extension.yml`. Background: `docs/spec/shopware-guideline-compliance.md`. + ## Adding functionality - New console commands go in `src/Command/`, new scheduled tasks in `src/ScheduledTask/` — both must diff --git a/docs/spec/shopware-guideline-compliance.md b/docs/spec/shopware-guideline-compliance.md index 3603f38..78674a2 100644 --- a/docs/spec/shopware-guideline-compliance.md +++ b/docs/spec/shopware-guideline-compliance.md @@ -10,8 +10,9 @@ on branches `research/shopware-guideline-checklist`, `research/shopware-cli-ci-b - Workflows are **plugin-agnostic**: no plugin name, package name, owner, repo or host is hard-coded; all derive from `composer.json` and the `gitea` context. The same files must work unchanged for a second plugin and be extractable to Gitea's shared-workflow repository later. -- One artifact: the `shopware-cli extension package` zip (prebuilt admin/storefront JS) is used for - both the Gitea Release asset and the Composer registry. +- One artifact: the `shopware-cli extension package` zip (prebuilt admin/storefront JS) is the Gitea + Release asset; the Composer registry gets the same content repacked root-level (the registry needs + `composer.json` at the zip root, see section 4). - `shopware-cli` is intentionally **unpinned** (`latest`). Known tradeoff: a CLI release can turn PRs red without a repo change; treat that as a signal to fix config. - `RELEASE_TOKEN` is the only token (release, assets, registry publish, link). Workflows still declare @@ -34,7 +35,9 @@ on branches `research/shopware-guideline-checklist`, `research/shopware-cli-ci-b ### PR workflows Jobs (all required checks on `main`): -`pr-version-check.yml` (job `version-changelog`) is unchanged apart from `permissions:`; its name keeps the existing required context. New `plugin-checks.yml`: +`plugin-checks.yml` (the former `pr-version-check.yml` was deleted; `version-changelog` is now a job in it): + +0. `version-changelog` - version bump vs `main` and matching CHANGELOG heading. 1. `package` - `extension package` with `--git-commit "$GITHUB_SHA"`, `extension validate` on the resulting zip (hard gate; replaces a separate directory validate, which only repeats those checks), then `scripts/ci/assert-built-assets.sh` (built admin/storefront JS present when sources exist). All shopware-cli jobs run in `ghcr.io/shopware/shopware-cli:latest`; npm and shopware-cli caches via `actions/cache`. 2. `phpunit` - `composer create-project shopware/production` (6.6.x) + MariaDB service, copy the plugin into `custom/plugins`, install/activate, run phpunit. Runs in the same `shopware-cli` image (PHP and Composer included; `shivammathur/setup-php` hangs on this runner and apt installs cost ~4.5 min). MariaDB 10.11, because 11.4 broke Shopware's own migrations. @@ -51,17 +54,18 @@ before the tag exists, so a failed build leaves no orphan tag. ### Branch protection (one-time, manual) `scripts/protect-main.sh` using `tea api PATCH repos/{owner}/{repo}/branch_protections/main` to set the -required status-check contexts (current: `version-changelog / version-changelog (pull_request)`; add -the new PR jobs). `tea branches protect` cannot set status checks. Not run by the pipeline. +required status-check contexts (now `plugin-checks / version-changelog|package|phpunit (pull_request)`, +set on `main`). `tea branches protect` cannot set status checks. Not run by the pipeline. -## 4. Open items to verify while implementing +## 4. Open items (status after the first pipeline run) -- Release POST creates a missing tag (fallback: tag step after packaging). -- Meaning of HTTP 400 on the package-link endpoint. -- Whether bare `validate` runs PHPStan/ESLint (needs PHP/Node on the runner); Node availability on the runner image. -- Accepted size for `metadata.icon.size`; error-vs-warning rule list of validate. -- Whether `routes.php` is flagged by validate; `shopware-cli extension fix` correctness for the YAML migration. -- Exact status-check context names of the new jobs (set after the first run). +- Release POST creates a missing tag: **resolved** - the POST created the tag and release for v0.1.0 (release id 55). +- Meaning of HTTP 400 on the package-link endpoint: **open** (not evidenced yet). +- Whether bare `validate` runs PHPStan/ESLint; Node availability on the runner image: **open** (not evidenced; the zip validate passes in the `shopware-cli` image). +- Accepted size for `metadata.icon.size`; error-vs-warning rule list of validate: **resolved** - `validate` on the zip reports "No problems found", so the icon size is accepted. +- Whether `routes.php` is flagged by validate: **resolved** - not flagged ("No problems found"). `shopware-cli extension fix` correctness for the YAML migration: **open**. +- Exact status-check context names: **resolved** - final: `plugin-checks / version-changelog (pull_request)`, `plugin-checks / package (pull_request)`, `plugin-checks / phpunit (pull_request)`, set on `main`. +- **Finding:** the Composer registry rejected the `shopware-cli` zip with "composer.json file is missing"; Gitea needs `composer.json` at the zip root, while the `shopware-cli` zip nests everything in a top-level folder. Fix (implemented separately): repack the zip with the top-level folder stripped for the registry upload; the Release asset stays the `shopware-cli` zip. ## 5. Out of scope / later From 8afe30b4762993a6455f662f0c76f2d2536b8066 Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 00:49:52 +0200 Subject: [PATCH 03/10] test: assert plugin logs to injected PSR logger Co-Authored-By: Claude Sonnet 5.5 --- .../Handler/CreateDumpHandlerTest.php | 46 +++++++++++++ tests/Service/DumpServiceLoggingTest.php | 66 +++++++++++++++++++ tests/Support/RecordingLogger.php | 16 +++++ 3 files changed, 128 insertions(+) create mode 100644 tests/MessageQueue/Handler/CreateDumpHandlerTest.php create mode 100644 tests/Service/DumpServiceLoggingTest.php create mode 100644 tests/Support/RecordingLogger.php diff --git a/tests/MessageQueue/Handler/CreateDumpHandlerTest.php b/tests/MessageQueue/Handler/CreateDumpHandlerTest.php new file mode 100644 index 0000000..45a296a --- /dev/null +++ b/tests/MessageQueue/Handler/CreateDumpHandlerTest.php @@ -0,0 +1,46 @@ +createMock(DumpJobStatusService::class); + $status->expects(self::once())->method('markRunning')->with('job-1'); + $status->expects(self::never())->method('markFailed'); + $logger = new RecordingLogger(); + + (new CreateDumpHandler($this->createMock(DumpService::class), $status, $logger))(new CreateDumpMessage('job-1')); + + self::assertCount(1, $logger->records); + self::assertSame('info', $logger->records[0]['level']); + self::assertSame('Dump job started.', $logger->records[0]['message']); + self::assertSame(['jobId' => 'job-1'], $logger->records[0]['context']); + } + + public function testLogsErrorAndMarksFailedWhenCreateThrows(): void + { + $exception = new \RuntimeException('boom'); + $service = $this->createMock(DumpService::class); + $service->method('createAsync')->willThrowException($exception); + $status = $this->createMock(DumpJobStatusService::class); + $status->expects(self::once())->method('markFailed')->with('job-2', 'boom'); + $logger = new RecordingLogger(); + + (new CreateDumpHandler($service, $status, $logger))(new CreateDumpMessage('job-2')); + + self::assertCount(2, $logger->records); + self::assertSame('error', $logger->records[1]['level']); + self::assertSame('Dump job failed.', $logger->records[1]['message']); + self::assertSame('job-2', $logger->records[1]['context']['jobId']); + self::assertSame($exception, $logger->records[1]['context']['exception']); + } +} diff --git a/tests/Service/DumpServiceLoggingTest.php b/tests/Service/DumpServiceLoggingTest.php new file mode 100644 index 0000000..cffddbe --- /dev/null +++ b/tests/Service/DumpServiceLoggingTest.php @@ -0,0 +1,66 @@ +filesystem = $this->createMock(FilesystemOperator::class); + $this->logger = new RecordingLogger(); + $this->service = new DumpService( + $this->createMock(DumpLister::class), + $this->filesystem, + $this->createMock(DumpBinaryLocator::class), + $this->createMock(Connection::class), + $this->createMock(SystemConfigService::class), + new DumpFilenameParser(), + $this->logger, + ); + } + + public function testDeleteLogsInfo(): void + { + $this->filesystem->method('fileExists')->willReturn(true); + $this->filesystem->expects(self::once())->method('delete')->with(DumpLister::path(self::FILENAME)); + + $this->service->delete(self::FILENAME); + + self::assertSame( + [['level' => 'info', 'message' => 'Dump deleted.', 'context' => ['filename' => self::FILENAME]]], + $this->logger->records + ); + } + + public function testDeleteMissingDumpDoesNotLog(): void + { + $this->filesystem->method('fileExists')->willReturn(false); + + try { + $this->service->delete(self::FILENAME); + self::fail('Expected DumpNotFoundException.'); + } catch (DumpNotFoundException) { + self::assertSame([], $this->logger->records); + } + } +} diff --git a/tests/Support/RecordingLogger.php b/tests/Support/RecordingLogger.php new file mode 100644 index 0000000..933bf67 --- /dev/null +++ b/tests/Support/RecordingLogger.php @@ -0,0 +1,16 @@ +}> */ + public array $records = []; + + public function log($level, string|\Stringable $message, array $context = []): void + { + $this->records[] = ['level' => (string) $level, 'message' => (string) $message, 'context' => $context]; + } +} From 391db5b621677eb524f1ceec59f3bd9a04df10c4 Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 00:50:20 +0200 Subject: [PATCH 04/10] docs: match CLAUDE.md to the idempotent release and registry repack Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c7e7ce8..b3e146a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,9 +80,10 @@ means fix the config. (shared composite action `actions/package`: `shopware-cli extension package`, `extension validate` on the zip, `scripts/ci/assert-built-assets.sh`), and `phpunit`. The package job caches the tested zip keyed by the commit's tree hash. -- `workflows/release.yml` (push to `main`, serialized, skipped if tag `v` exists): reuses the +- `workflows/release.yml` (push to `main`, serialized; every step is idempotent so a failed run can simply be re-run): reuses the cached zip when the tree hash matches (else packages again, always re-validates), creates the Gitea - release + tag via API with the zip as asset, publishes to the Composer registry, then links the + release + tag via API with the zip as asset, publishes a root-level repack of the zip to the Composer registry + (Gitea requires `composer.json` at the zip root), then links the package to the repo. Uses `RELEASE_TOKEN`. - Required checks on `main` (`plugin-checks / version-changelog|package|phpunit (pull_request)`) are set once with `scripts/protect-main.sh` (`tea api`). Build excludes/validation live in From 953f4ee5796937f26cec076420c72b21f95f6209 Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 00:53:13 +0200 Subject: [PATCH 05/10] docs: research shopware-cli changelog handling and PHP floor Co-Authored-By: Claude Sonnet 5.5 --- docs/research/changelog-and-php-floor.md | 167 +++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/research/changelog-and-php-floor.md diff --git a/docs/research/changelog-and-php-floor.md b/docs/research/changelog-and-php-floor.md new file mode 100644 index 0000000..b89f1d7 --- /dev/null +++ b/docs/research/changelog-and-php-floor.md @@ -0,0 +1,167 @@ +# 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: + +```go +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`): + +```go +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 ``. +- 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`: + +```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`: + +```go +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 `# ` 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: + +```markdown +# 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 `# ` 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: + +```json +"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 `` per a comment in `internal/markdown`. From eb2a3172e542d17c718f2d7f0fa450e2ab06092f Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 00:58:42 +0200 Subject: [PATCH 06/10] fix(package): exclude .config and .gitea from the zip Co-Authored-By: Claude Sonnet 5.5 --- .config/shopware-extension.yml | 2 ++ CHANGELOG.md | 1 + 2 files changed, 3 insertions(+) diff --git a/.config/shopware-extension.yml b/.config/shopware-extension.yml index fa16e4b..a262c31 100644 --- a/.config/shopware-extension.yml +++ b/.config/shopware-extension.yml @@ -14,6 +14,8 @@ build: - AGENTS.md - CLAUDE.md - QWEN.md + - .config + - .gitea - .claude - .scratch - .wayfinder diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e4443..9261827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The packaged zip no longer contains `.gitea/` and `.config/`. - Release pipeline: the Composer registry rejected the packaged zip (`composer.json file is missing`) because `shopware-cli` zips wrap everything in a top-level folder. The same prebuilt content is now repacked root-level for the registry upload. - Release steps are idempotent (existing release and asset are reused, registry 409 counts as published) and the "tag exists -> skip" guard is gone, so a failed release can be re-run to completion. From 18844718d26d58accbc490ff01ab1690e25dd934 Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 01:01:21 +0200 Subject: [PATCH 07/10] ci(phpunit): cache the installed database, key the cache on the Shopware version - resolve the latest 6.6.x from Packagist; cache key shopware--installed-v1 - cache the project plus a SQL dump of the installed shopware_test database; on a hit restore the dump instead of running system:install (~4.6 min) - rename the step 'Install Shopware (once)' to 'Install Shopware' Co-Authored-By: Claude Sonnet 5.5 --- .gitea/workflows/plugin-checks.yml | 84 ++++++++++++++++++++++-------- CLAUDE.md | 8 +-- 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/.gitea/workflows/plugin-checks.yml b/.gitea/workflows/plugin-checks.yml index e437554..e495819 100644 --- a/.gitea/workflows/plugin-checks.yml +++ b/.gitea/workflows/plugin-checks.yml @@ -98,28 +98,41 @@ jobs: - uses: actions/checkout@v4 with: path: plugin-src - - name: Month stamp (refreshes the cached project monthly) - id: stamp - run: echo "month=$(date +%Y-%m)" >> "$GITHUB_OUTPUT" - # The project (incl. vendor/ and phpunit) does not depend on the plugin, so it is cached on its own. - - name: Restore prepared Shopware project - id: project + # The cache key follows the Shopware version: a new 6.6.x release changes the key, so the old + # project + database are never reused. Bump -v1 to force a refresh by hand. + - name: Resolve the latest Shopware 6.6 version + id: sw + run: | + VERSION=$(php -r ' + $all = json_decode(file_get_contents("https://repo.packagist.org/p2/shopware/production.json"), true)["packages"]["shopware/production"]; + $v = []; + foreach ($all as $p) { if (preg_match("/^6\.6\.\d+(\.\d+)*$/", $p["version"])) { $v[] = $p["version"]; } } + usort($v, "version_compare"); + echo end($v);') + test -n "$VERSION" || { echo "::error::could not resolve the Shopware 6.6 version"; exit 1; } + echo "Shopware ${VERSION}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + # Project (vendor/, phpunit, config) and the installed database as a SQL dump. Neither depends on the plugin. + - name: Restore Shopware project and database + id: shopware uses: actions/cache/restore@v4 with: - path: shopware - key: shopware-6.6-prepared-${{ steps.stamp.outputs.month }} - - name: Create Shopware 6.6 project - if: steps.project.outputs.cache-hit != 'true' + path: | + shopware + !shopware/var/cache + !shopware/var/log + shopware-db.sql.gz + key: shopware-${{ steps.sw.outputs.version }}-installed-v1 + - name: Create Shopware project + if: steps.shopware.outputs.cache-hit != 'true' run: | set -e - composer create-project shopware/production:6.6.* shopware --no-interaction --no-scripts + composer create-project "shopware/production:${{ steps.sw.outputs.version }}" shopware --no-interaction --no-scripts composer require --dev phpunit/phpunit:^10 --no-interaction --no-scripts -d shopware - - name: Save prepared Shopware project - if: steps.project.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: shopware - key: shopware-6.6-prepared-${{ steps.stamp.outputs.month }} + - name: Ensure the MariaDB client + run: | + command -v mariadb-dump >/dev/null || command -v mysqldump >/dev/null && exit 0 + (apt-get update -q && apt-get install -y -q --no-install-recommends mariadb-client) || apk add --no-cache mariadb-client - name: Wait for the database run: | for i in $(seq 60); do @@ -127,9 +140,39 @@ jobs: sleep 2 done echo "::error::database service did not come up"; exit 1 - - name: Relax database durability (throwaway CI database, speeds up the migrations) + - name: Relax database durability (throwaway CI database, speeds up the install and the restore) run: | php -r '$p = new PDO("mysql:host=database;port=3306", "root", "root"); $p->exec("SET GLOBAL innodb_flush_log_at_trx_commit=0"); $p->exec("SET GLOBAL sync_binlog=0");' + # One install into shopware_test; the bootstrapper then sees an installed DB, skips its own install and + # only activates the plugin (tests/TestBootstrap.php forces plugin installation). + - name: Install Shopware + if: steps.shopware.outputs.cache-hit != 'true' + working-directory: shopware + run: bin/console system:install --basic-setup --create-database --force --no-interaction + - name: Dump the installed database + if: steps.shopware.outputs.cache-hit != 'true' + run: | + set -e + DUMP=$(command -v mariadb-dump || command -v mysqldump) + "$DUMP" -h database -P 3306 -u root -proot --skip-ssl --single-transaction --routines --triggers \ + --databases shopware_test | gzip > shopware-db.sql.gz + ls -l shopware-db.sql.gz + - name: Save Shopware project and database + if: steps.shopware.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: | + shopware + !shopware/var/cache + !shopware/var/log + shopware-db.sql.gz + key: shopware-${{ steps.sw.outputs.version }}-installed-v1 + - name: Restore the database from the cache + if: steps.shopware.outputs.cache-hit == 'true' + run: | + set -e + CLIENT=$(command -v mariadb || command -v mysql) + gunzip -c shopware-db.sql.gz | "$CLIENT" -h database -P 3306 -u root -proot --skip-ssl - name: Resolve plugin name and install the plugin sources run: | # plugin directory name = short class name of extra.shopware-plugin-class @@ -138,11 +181,6 @@ jobs: echo "PLUGIN_NAME=${PLUGIN_NAME}" >> "$GITHUB_ENV" rm -rf "shopware/custom/plugins/${PLUGIN_NAME}" cp -r plugin-src "shopware/custom/plugins/${PLUGIN_NAME}" - # One install into shopware_test; the bootstrapper sees an installed DB, skips its own install and - # only activates the plugin (tests/TestBootstrap.php forces plugin installation). - - name: Install Shopware (once) - working-directory: shopware - run: bin/console system:install --basic-setup --create-database --force --no-interaction - name: Run phpunit working-directory: shopware run: vendor/bin/phpunit -c "custom/plugins/${PLUGIN_NAME}/phpunit.xml" diff --git a/CLAUDE.md b/CLAUDE.md index b3e146a..7ffab58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,9 +53,11 @@ activates this plugin inside a real Shopware kernel for the test run (`KERNEL_CL `Shopware\Core\Kernel` in `phpunit.xml`). This means tests require a working Shopware installation/DB to bootstrap against — they are not runnable as an isolated PHP package. -In CI (`phpunit` job) a Shopware 6.6 project is prepared once (`composer create-project` + phpunit, -cached monthly), the plugin is copied into `custom/plugins`, and `system:install` runs once into -`shopware_test` (MariaDB 10.11 service), so the bootstrapper only activates the plugin. +In CI (`phpunit` job) the latest Shopware 6.6 version is resolved from Packagist and the prepared project +(`composer create-project` + phpunit, then `system:install` into `shopware_test` on a MariaDB 10.11 service) +plus a SQL dump of the installed database are cached under a key that contains that version, so a new +Shopware release invalidates the cache. On a hit the dump is restored instead of reinstalling; the plugin +is then copied into `custom/plugins` and the bootstrapper only activates it. ```bash vendor/bin/phpunit # full suite From 8b2c2278d26950dd96fa4e979710a74d6bc0ca68 Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 01:05:25 +0200 Subject: [PATCH 08/10] ci: resolve the Shopware version with Composer from the plugin constraint; harden the registry repack - phpunit: 'Resolve the latest Shopware Version' uses composer show against require.shopware/core from the plugin's composer.json (the Packagist scrape returned nothing on the first run) - release: build the registry zip from an explicit file list without a leading ./ so the composer.json entry is stored at the zip root Co-Authored-By: Claude Sonnet 5.5 --- .gitea/workflows/plugin-checks.yml | 22 +++++++++++----------- .gitea/workflows/release.yml | 3 ++- CLAUDE.md | 8 ++++---- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/plugin-checks.yml b/.gitea/workflows/plugin-checks.yml index e495819..840c925 100644 --- a/.gitea/workflows/plugin-checks.yml +++ b/.gitea/workflows/plugin-checks.yml @@ -98,19 +98,19 @@ jobs: - uses: actions/checkout@v4 with: path: plugin-src - # The cache key follows the Shopware version: a new 6.6.x release changes the key, so the old - # project + database are never reused. Bump -v1 to force a refresh by hand. - - name: Resolve the latest Shopware 6.6 version + # The cache key follows the Shopware version: a new release inside the plugin's supported range changes + # the key, so the old project + database are never reused. Bump -v1 to force a refresh by hand. + - name: Resolve the latest Shopware Version id: sw run: | - VERSION=$(php -r ' - $all = json_decode(file_get_contents("https://repo.packagist.org/p2/shopware/production.json"), true)["packages"]["shopware/production"]; - $v = []; - foreach ($all as $p) { if (preg_match("/^6\.6\.\d+(\.\d+)*$/", $p["version"])) { $v[] = $p["version"]; } } - usort($v, "version_compare"); - echo end($v);') - test -n "$VERSION" || { echo "::error::could not resolve the Shopware 6.6 version"; exit 1; } - echo "Shopware ${VERSION}" + set -e + # The plugin's own constraint (require.shopware/core, e.g. ~6.6.0) decides which versions are acceptable; + # Composer lists the matching ones newest first, the first stable one is the latest. + CONSTRAINT=$(php -r 'echo json_decode(file_get_contents("plugin-src/composer.json"), true)["require"]["shopware/core"];') + VERSION=$(composer show shopware/core "$CONSTRAINT" --all --format=json --no-interaction 2>/dev/null \ + | php -r '$d = json_decode(stream_get_contents(STDIN), true); foreach ($d["versions"] as $v) { if (preg_match("/^v?\d+(\.\d+)+$/", $v)) { echo ltrim($v, "v"); exit; } }') + test -n "$VERSION" || { echo "::error::could not resolve a Shopware version for ${CONSTRAINT}"; exit 1; } + echo "Shopware ${VERSION} (constraint ${CONSTRAINT})" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" # Project (vendor/, phpunit, config) and the installed database as a SQL dump. Neither depends on the plugin. - name: Restore Shopware project and database diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index af04609..7d8bcda 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -126,7 +126,8 @@ jobs: fi test -f "$ROOT/composer.json" || { echo "::error::no composer.json found in the packaged zip"; exit 1; } OUT="$PWD/.build/composer-${{ steps.meta.outputs.zip }}" - (cd "$ROOT" && zip -qr "$OUT" .) + # explicit file list with the leading "./" stripped, so entries are stored as "composer.json", "src/..." + (cd "$ROOT" && find . -mindepth 1 | sed 's|^\./||' | zip -q "$OUT" -@) unzip -l "$OUT" | grep -E ' composer\.json$' > /dev/null || { echo "::error::repacked zip has no root-level composer.json"; exit 1; } echo "repacked: $OUT" diff --git a/CLAUDE.md b/CLAUDE.md index 7ffab58..65af51a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,10 +53,10 @@ activates this plugin inside a real Shopware kernel for the test run (`KERNEL_CL `Shopware\Core\Kernel` in `phpunit.xml`). This means tests require a working Shopware installation/DB to bootstrap against — they are not runnable as an isolated PHP package. -In CI (`phpunit` job) the latest Shopware 6.6 version is resolved from Packagist and the prepared project -(`composer create-project` + phpunit, then `system:install` into `shopware_test` on a MariaDB 10.11 service) -plus a SQL dump of the installed database are cached under a key that contains that version, so a new -Shopware release invalidates the cache. On a hit the dump is restored instead of reinstalling; the plugin +In CI (`phpunit` job) the latest Shopware version allowed by the plugin's `require.shopware/core` constraint +is resolved with Composer. The prepared project (`composer create-project` + phpunit, then `system:install` +into `shopware_test` on a MariaDB 10.11 service) plus a SQL dump of the installed database are cached under +a key that contains that version, so a new Shopware release invalidates the cache. On a hit the dump is restored instead of reinstalling; the plugin is then copied into `custom/plugins` and the bootstrapper only activates it. ```bash From 7f3e77a3e9fddee58376cb35a2f716dad674036d Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 01:12:21 +0200 Subject: [PATCH 09/10] ci: retrigger to exercise the cache-hit path Co-Authored-By: Claude Sonnet 5.5 From bfb88a25b8571f3de402014271594d723b35cf19 Mon Sep 17 00:00:00 2001 From: ArthurErlich Date: Sun, 4 Oct 2026 01:18:29 +0200 Subject: [PATCH 10/10] chore: release as 0.0.6; compare the version check against published releases - composer.json 0.0.6, CHANGELOG entries of the withdrawn 0.1.0/0.1.1 merged into [0.0.6] - version-changelog: version must exceed the latest published release (main may carry a version whose release was withdrawn), instead of exceeding main's version Co-Authored-By: Claude Sonnet 5.5 --- .gitea/workflows/plugin-checks.yml | 28 +++++++++++++--------------- CHANGELOG.md | 20 +++++++++----------- CLAUDE.md | 5 +++-- composer.json | 2 +- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/.gitea/workflows/plugin-checks.yml b/.gitea/workflows/plugin-checks.yml index 840c925..71c3f8b 100644 --- a/.gitea/workflows/plugin-checks.yml +++ b/.gitea/workflows/plugin-checks.yml @@ -22,26 +22,24 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Check version bump and changelog entry + # The version must be higher than the latest *published release* (not just higher than main), so main can + # carry a version whose release failed or was withdrawn without blocking the next PR. + - name: Check version is unreleased and changelog entry exists + env: + TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | set -e PR_VERSION=$(grep -m1 '"version"' composer.json | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/') - MAIN_VERSION=$(git show origin/main:composer.json | grep -m1 '"version"' | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/') + LATEST=$(curl -sf -H "Authorization: token ${TOKEN}" "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases?limit=100" \ + | jq -r '.[].tag_name' | sed 's/^v//' | sort -V | tail -n1) + LATEST="${LATEST:-0.0.0}" - echo "main version: $MAIN_VERSION" - echo "PR version: $PR_VERSION" + echo "latest release: $LATEST" + echo "PR version: $PR_VERSION" - if [ "$MAIN_VERSION" = "$PR_VERSION" ]; then - echo "::error::composer.json version ($PR_VERSION) was not bumped from main ($MAIN_VERSION)" - exit 1 - fi - - HIGHEST=$(printf '%s\n%s\n' "$MAIN_VERSION" "$PR_VERSION" | sort -V | tail -n1) - if [ "$HIGHEST" != "$PR_VERSION" ]; then - echo "::error::composer.json version ($PR_VERSION) is not greater than main's ($MAIN_VERSION)" + if [ "$PR_VERSION" = "$LATEST" ] || [ "$(printf '%s\n%s\n' "$LATEST" "$PR_VERSION" | sort -V | tail -n1)" != "$PR_VERSION" ]; then + echo "::error::composer.json version ($PR_VERSION) is not greater than the latest release ($LATEST)" exit 1 fi @@ -50,7 +48,7 @@ jobs: exit 1 fi - echo "OK: version bumped to $PR_VERSION and documented in CHANGELOG.md" + echo "OK: $PR_VERSION is unreleased and documented in CHANGELOG.md" # Packages the extension (builds admin/storefront assets) and validates the resulting zip, # which is the artifact that ships. A separate directory validate would only repeat these checks. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9261827..dbe3c51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,27 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.1.1] - 2026-10-04 - -### Fixed - -- The packaged zip no longer contains `.gitea/` and `.config/`. -- Release pipeline: the Composer registry rejected the packaged zip (`composer.json file is missing`) because `shopware-cli` zips wrap everything in a top-level folder. The same prebuilt content is now repacked root-level for the registry upload. -- Release steps are idempotent (existing release and asset are reused, registry 409 counts as published) and the "tag exists -> skip" guard is gone, so a failed release can be re-run to completion. - -## [0.1.0] - 2026-10-03 +## [0.0.6] - 2026-10-04 ### Changed - Services and routes are now configured in YAML; the plugin requires PHP `^8.2`. -- Release pipeline: the packaged `shopware-cli` zip (with prebuilt assets) is validated and used for both the Gitea Release and the Composer registry; the release is created through the Gitea API after packaging, so a failed build no longer leaves an orphan tag. -- New PR checks: `shopware-cli` validate, package build and phpunit. +- Release pipeline: the packaged `shopware-cli` zip (with prebuilt assets) is validated and used for the Gitea Release; the Composer registry gets the same content repacked root-level (Gitea requires `composer.json` at the zip root). The release is created through the Gitea API after packaging, so a failed build no longer leaves an orphan tag. +- New PR checks: `shopware-cli` validate, package build and phpunit (with a cached Shopware project and database). +- The PR version check compares against the latest published release instead of the version on `main`. ### Added - Dedicated `aeon_dump_manager` monolog channel; dump jobs, deletions and purges are logged. - Extension icon and `.config/shopware-extension.yml`. +### Fixed + +- The packaged zip no longer contains `.gitea/` and `.config/`. +- Release steps are idempotent (existing release and asset are reused, registry 409 counts as published), so a failed release can be re-run to completion. + ### Removed - Empty plugin lifecycle overrides. diff --git a/CLAUDE.md b/CLAUDE.md index 65af51a..4923555 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,8 +77,9 @@ Gitea workflows in `.gitea/`, plugin-agnostic (name/version come from `composer. runs from its official Docker image, intentionally unpinned (`latest`): a red build after a CLI release means fix the config. -- `workflows/plugin-checks.yml` (PRs into `main`) has three jobs: `version-changelog` (every PR into - `main` must bump `composer.json` version and add a matching `## [x.y.z]` CHANGELOG heading), `package` +- `workflows/plugin-checks.yml` (PRs into `main`) has three jobs: `version-changelog` (the + `composer.json` version must be greater than the latest published Gitea release and have a matching + `## [x.y.z]` CHANGELOG heading), `package` (shared composite action `actions/package`: `shopware-cli extension package`, `extension validate` on the zip, `scripts/ci/assert-built-assets.sh`), and `phpunit`. The package job caches the tested zip keyed by the commit's tree hash. diff --git a/composer.json b/composer.json index 2bab846..01f6008 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Export database dumps for Shopware 6 environments.", "type": "shopware-platform-plugin", "license": "MIT", - "version": "0.1.1", + "version": "0.0.6", "keywords": [ "shopware", "shopware6",