diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4695df..9a8044a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,10 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Validate Composer metadata strictly - run: docker run --rm -v "$GITHUB_WORKSPACE:/app" -w /app composer:2.10 validate --strict --no-check-publish + run: docker run --rm -v "$GITHUB_WORKSPACE:/app" -w /app composer:2.10 validate --strict + + - name: Validate VCS imports before and after the package rename + run: docker run --rm --network none -v "$GITHUB_WORKSPACE:/app:ro" -w /app composer:2.10 php tests/php/check-composer-import.php /usr/bin/composer - name: Check optimized production classmap run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 758871e..b78575c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -Notable changes to the Basicrum Analytics module are recorded here. +Notable changes to Basicrum Analytics are recorded here. ## [Unreleased] @@ -28,6 +28,15 @@ Notable changes to the Basicrum Analytics module are recorded here. ### Changed +- **Breaking packaging change:** the Composer name is now + `basicrum/basicrum-magento-2`, retaining the public title "Basicrum Analytics" + and distribution filename `basicrum-magento-2.zip`. No compatibility + replacement is declared. The old-name conflict is omitted during the rename + because VCS imports under the old default-branch name reject it as a self-conflict. + Remove the old package explicitly; Composer does not prevent co-installation. + Magento module/namespace/ACL identifiers and configuration paths are unchanged + by this packaging change. Packagist registration and release publication are + separate, pending maintainer steps; historical tags are not rewritten. - **Breaking:** Magento 2 `p_type` now uses Magento 1's exact 27 named labels for equivalent native pages, including `Checkout Success`, account/address, wishlist, guest-order, and PayPal billing-agreement pages. HTTP 404 takes @@ -65,6 +74,12 @@ Notable changes to the Basicrum Analytics module are recorded here. ### Fixed +- The Admin integration test opens Magento's native Basicrum navigation group + before selecting the exact settings link, which is hidden when collapsed. +- Validate rename branches through Composer's validating VCS importer in CI, + including the pre-merge old-name default branch that root validation misses. +- Rebuild the disposable Magento catalog search index before native browser + checks, including when retained application/database volumes outlive OpenSearch data. - Fresh disposable Magento provisioning no longer writes the Admin Usage setting after disabling its owning module. Configuration failures still stop the job. - Release ZIPs and expected hashes now come from committed files; ignored local diff --git a/README.md b/README.md index f68cfde..c7f0fe7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Basicrum Analytics for Magento 2 +# Basicrum Analytics Basicrum adds Boomerang real user monitoring (RUM) to a Magento 2 storefront. Monitoring is fail-closed: no Basicrum storefront scripts are emitted unless @@ -24,12 +24,21 @@ verification scope, skips, and remaining release requirements. ## Installation -Install a published package with Composer: +The Composer package is being renamed to `basicrum/basicrum-magento-2`. +This source change does not register the new Packagist listing or publish a +release. After the new package's `0.1.0` release is published, install it with: ```sh -composer require basicrum/basicrum-analytics +composer require 'basicrum/basicrum-magento-2:^0.1' ``` +The version constraint deliberately excludes the historical `0.0.x` releases, +which may also appear under the new name when Packagist imports the repository. +Do not drop the constraint to make an unpublished release install. Until +`0.1.0` is available, use a reviewed source checkout for development only. +See the [package migration checklist](https://github.com/basicrum/basicrum-magento-2/blob/main/docs/PACKAGE-NAMING-MIGRATION.md) +for the separate maintainer steps and existing-installation considerations. + For a manual source installation, place this module at the exact path below. The casing is required on case-sensitive filesystems: @@ -53,7 +62,7 @@ static content using the store's normal deployment process as well. ## Configuration -Open **Stores > Configuration > Basicrum Analytics**. Every setting supports +Open **Stores > Configuration > Basicrum > Basicrum Analytics**. Every setting supports Magento default, website, and store inheritance. Display-only status, version, and callback instructions have no inheritance controls or stored values. Visitor Consent and Privacy open expanded each time you visit the page. You @@ -155,6 +164,14 @@ Automatic consent-provider adapters are not part of Phase 1. No data migration renames, deletes, or heuristically rewrites stored settings. Review the following before enabling the upgraded module: +- The Composer package name changes from `basicrum/basicrum-analytics` to + `basicrum/basicrum-magento-2`. The two packages must not be installed together; + Composer does not prevent that combination during the naming transition. + Remove the old requirement and check the resolved lock file for old-name + transitive dependencies when the new release is available; this is not an + automatic or backward-compatible replacement. A manual `app/code` copy must not coexist + with a Composer installation either. No stored configuration is deleted. + - Magento 2 `p_type` labels now match Magento 1, including capitalization and spaces. This intentionally changes existing report groupings; historical beacons are not migrated and no legacy-label mode is provided. Update any @@ -240,8 +257,18 @@ blocked unnonced inline negative control. The fixture is never packaged with the extension. This is separate from the report-only homepage FPC checks; it does not certify nonce handling on cacheable pages or custom strict-dynamic policies. See the integration README for fixture installation and scope. -The regular CI workflow runs strict Composer 2.10 validation and optimized -production classmap checks plus the fast PHP and Chromium checks. The Chromium +The regular CI workflow runs strict Composer 2.10 validation, a validating VCS +import regression before and after the default-branch rename, and optimized +production classmap checks plus the fast PHP and Chromium checks. The VCS test +uses the real Composer importer on a temporary local Git repository containing +the candidate metadata and a synthetic historical tag; it needs no network or +Packagist account. Run it with: + +```sh +docker run --rm --network none -v "$PWD:/app:ro" -w /app composer:2.10 php tests/php/check-composer-import.php /usr/bin/composer +``` + +This is not a live Packagist update or a Magento Composer installation. The Chromium job uploads an HTML report with traces from failed test attempts, retained for seven days. A flaky pass still fails the job; retries do not hide failures. Additional CI checks run Magento-aware PHPStan level 8 and Magento coding diff --git a/composer.json b/composer.json index 2b2ba1c..9af7f45 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,12 @@ { - "name": "basicrum/basicrum-analytics", - "description": "Basicrum real user monitoring for Magento 2", + "name": "basicrum/basicrum-magento-2", + "description": "Basicrum real user monitoring (RUM) for Magento 2", "type": "magento2-module", + "homepage": "https://github.com/basicrum/basicrum-magento-2", + "support": { + "issues": "https://github.com/basicrum/basicrum-magento-2/issues", + "docs": "https://github.com/basicrum/basicrum-magento-2#readme" + }, "authors": [ { "name": "Tsvetan Stoychev", diff --git a/docs/PACKAGE-NAMING-MIGRATION.md b/docs/PACKAGE-NAMING-MIGRATION.md new file mode 100644 index 0000000..2359e7f --- /dev/null +++ b/docs/PACKAGE-NAMING-MIGRATION.md @@ -0,0 +1,119 @@ +# Composer and Packagist naming migration + +## Scope and identity + +This repository prepares the rename; it does not register, abandon, publish or +delete any Packagist package, create a release tag, or approve a module license. + +| Surface | Canonical value | +| --- | --- | +| Composer / Packagist | `basicrum/basicrum-magento-2` | +| Public title and Admin section | Basicrum Analytics | +| Description | Basicrum real user monitoring (RUM) for Magento 2 | +| Distribution ZIP | `basicrum-magento-2.zip` | +| Magento module (unchanged) | `Basicrum_Analytics` | +| PHP namespace (unchanged) | `Basicrum\Analytics` | +| Configuration paths (unchanged) | `basicrum/*` | + +The ACL resource, layout aliases, JavaScript identifiers, Beacon Endpoint and +Brum Site ID names also stay unchanged. Historical tags, the existing MIT +declaration and third-party notices are preserved. + +## Composer decision + +The rename does not declare a conflict with `basicrum/basicrum-analytics`. +While `main` still uses that old name, Composer's VCS importer assigns it to +the rename branch too. An old-name conflict then becomes a self-conflict and +Packagist rejects the branch, even though standalone `composer validate` passes. +The initial PR declared that conflict; the Packagist update failure exposed this +import-stage gap. CI now exercises Composer's validating VCS importer with both +old-name and new-name default branches, the real candidate metadata, and a +synthetic historical tag in a disposable local Git repository. + +Both packages must not be installed together, but **Composer does not enforce +that restriction during this transition**. Remove the old requirement explicitly +and inspect the resolved lock file for transitive old-name dependencies. +Any later conflict declaration requires a separately verified migration step, +including successful imports for the affected Packagist listings; it is not +automatically safe merely because this PR was merged. There is no `replace`, `provide`, +compatibility metapackage, class alias or automatic upgrade: this is an +intentional package-name break, not a promise to satisfy old dependencies. +Composer cannot detect a duplicate manually installed `app/code` copy. + +Package names come from the default branch during VCS import. Composer can +therefore expose historical tags under the new name without changing those +tags. An unversioned `composer require` could select the old `0.0.2` code before +the new release exists. The README uses `basicrum/basicrum-magento-2:^0.1` +and explicitly makes installation conditional on publication of `0.1.0`. +Do not present `dev-main` or an imported `0.0.x` tag as the new stable release. + +## Maintainer checklist — not executed by this change + +1. Merge the naming change into the repository's default branch, `main`. + A feature branch alone does not establish the new Packagist identity. + After the corrected branch is pushed, verify that the old listing's next + update no longer rejects it for a self-conflict. If the hook has not retried, + an authorized maintainer can trigger an update. Repeat the import check after + merging; a local test does not certify the hosted updater's state. +2. Before publishing `0.1.0`, obtain the owner's approval for the missing root + LICENSE text and run the release gate against the exact clean commit to be + tagged. CI success is not license approval. Keep the Composer `version` + field absent. Do not move, delete or reuse `0.0.1` or `0.0.2`. +3. Publish the approved new `0.1.0` tag as a separate release action, **before + submitting the new Packagist listing**. Packagist's generated install command + has no version constraint and could otherwise select old `0.0.2` code. +4. As an authorized `basicrum` maintainer, submit the same repository URL to + Packagist under `basicrum/basicrum-magento-2`. Configure/verify its GitHub + update hook and trigger an update if needed. Do not assume updating the old + listing renames it. Verify `0.1.0` is the newest stable version, with the + intended source commit, description, support links and README. + Inspect imported historical versions; their appearance is not evidence that + `0.0.x` contains the new implementation. Verify a clean disposable Magento + Composer install using `^0.1`, including `Basicrum_Analytics` registration. +5. Once the new stable package is available and verified, mark + `basicrum/basicrum-analytics` abandoned in the **old listing's Packagist UI**, + with `basicrum/basicrum-magento-2` as the suggested alternative. Do not add an + `abandoned` field to the renamed package's `composer.json`. Keep the old + listing and historical versions; do not delete them or expect download + statistics to transfer. Abandonment is a notice, not a lock-file migration. + +## Existing development installations + +After the new release is available, review the project's dependency graph and +change the old requirement explicitly. For a project that directly requires +the old package, the intended Composer transaction is: + +```sh +composer remove basicrum/basicrum-analytics --no-update +composer require 'basicrum/basicrum-magento-2:^0.1' +``` + +Back up and review the resulting `composer.json` / `composer.lock` diff. If a +different package still requires the old name, stop and update that dependency +deliberately. Do not deploy a lock file containing both names; there is no +solver-level conflict guard in this rename. This transaction only changes Composer +packages. It does not migrate the historical module-name capitalization, +Magento's enabled-module configuration, or third-party customizations. Review +the README's upgrade notes and Magento deployment steps. Remove any duplicate +manual installation through the project's normal deployment process, preserve +stored settings, and rebuild compiled DI/static assets and caches as documented. + +## Review and sources + +CLI consultations used `grok-4.7` at high reasoning effort (reported +`grok-4.7-build`) and `claude-opus-5-5` at maximum effort. Both initially recommended an explicit +conflict without `replace` / `provide`. We removed that conflict +after the Packagist failure and a local reproduction using Composer's validating +VCS importer; the no-alias decision remains. Opus also identified the old-tag install trap +and recommended publishing the new stable tag before submitting the listing. +An initial Grok claim that old tags stay confined to the old name was corrected +against Composer source. A fixed `support.source` URL is deliberately omitted: +Composer's GitHub driver supplies a version-specific source link instead. +Tests guard package identity and dependency semantics; the native Admin test +opens Magento's native collapsible navigation before checking the exact rendered +section name and logo caption rather than adding a repository-wide branding scan. + +- [Packagist maintainer rename procedure](https://github.com/composer/packagist/issues/47) +- [Composer conflict semantics](https://getcomposer.org/doc/04-schema.md#conflict) +- [Composer VCS name normalization, including historical tags](https://github.com/composer/composer/blob/2.10.3/src/Composer/Repository/VcsRepository.php#L432-L438) +- [Packagist publication and update hooks](https://packagist.org/about) diff --git a/tests/integration/README.md b/tests/integration/README.md index 6fc9d47..42f7887 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -32,6 +32,9 @@ For subsequent runs after `down`, first recreate the containers with `docker compose -f tests/integration/docker/compose.yaml up -d`. Application/database volumes are retained; do not reprovision them. Git's `safe.directory` is baked into the PHP image and survives container recreation. +OpenSearch data is not persisted by this test stack. The release gate rebuilds +`catalogsearch_fulltext` through Magento's standard indexer before browser checks, +so a reused installation does not depend on an old search index or background cron. After harness Dockerfile changes, run `docker compose -f tests/integration/docker/compose.yaml build php` before `up -d`. First-time `start.sh` installs npm dependencies using the caller's UID/GID so the bind-mounted checkout does not acquire root-owned `node_modules`. @@ -306,7 +309,7 @@ CAPTCHA must be disabled for this isolated test account. The gate requires Git and a clean, committed module checkout, including no untracked files. First build the Git-commit ZIP using `build-artifact.sh` and install its contents. `BASICRUM_ARTIFACT` can select a ZIP path; the default is -`.test-results/package/basicrum-analytics.zip`. The gate hashes every ZIP entry +`.test-results/package/basicrum-magento-2.zip`. The gate hashes every ZIP entry against the candidate's production manifest. Before any upgrade/configuration write it also resolves `Basicrum_Analytics` through Magento's actual `ComponentRegistrar` and compares SHA-256 hashes of package diff --git a/tests/integration/admin.spec.js b/tests/integration/admin.spec.js index fbd5cd5..58d65d7 100644 --- a/tests/integration/admin.spec.js +++ b/tests/integration/admin.spec.js @@ -34,12 +34,25 @@ test("Basicrum configuration renders in Magento Admin", async ({ page }) => { expect(configurationUrl).toBeTruthy(); await page.goto(configurationUrl, { waitUntil: "domcontentloaded" }); - const basicrumSection = page - .locator("#system_config_tabs a") - .filter({ hasText: "Basicrum Analytics" }); - const basicrumUrl = await basicrumSection.getAttribute("href"); - expect(basicrumUrl).toBeTruthy(); - const response = await page.goto(basicrumUrl, { waitUntil: "networkidle" }); + const configTabs = page.locator("#system_config_tabs"); + // Magento initializes this group as a collapsible. Hidden links are not + // accessible by role until the group is opened through its native control. + // The tab's accessible name also contains Magento's expand/collapse icon. + const basicrumTab = configTabs.getByRole("tab").filter({ + has: page.getByText("Basicrum", { exact: true }) + }); + await expect(basicrumTab).toBeVisible(); + if ((await basicrumTab.getAttribute("aria-expanded")) !== "true") { + await basicrumTab.click(); + } + await expect(basicrumTab).toHaveAttribute("aria-expanded", "true"); + const basicrumSection = configTabs.getByRole("link", { name: "Basicrum Analytics", exact: true }); + await expect(basicrumSection).toBeVisible(); + const [response] = await Promise.all([ + page.waitForNavigation({ waitUntil: "networkidle" }), + basicrumSection.click() + ]); + expect(response).toBeTruthy(); expectAdminCsp(response.headers()); const generalSettings = page.locator('a[href="#basicrum_general-link"]'); @@ -52,6 +65,7 @@ test("Basicrum configuration renders in Magento Admin", async ({ page }) => { return logo.isVisible(); }) .toBe(true); + await expect(logo.locator("span")).toHaveText("Basicrum Analytics"); await expect(page.getByText("Monitoring Status", { exact: true })).toBeVisible(); await expect(page.getByText("Beacon Endpoint", { exact: true })).toBeVisible(); await expect(page.getByText("Brum Site ID", { exact: true })).toBeVisible(); diff --git a/tests/integration/build-artifact.sh b/tests/integration/build-artifact.sh index ab5acf5..c3a6536 100644 --- a/tests/integration/build-artifact.sh +++ b/tests/integration/build-artifact.sh @@ -4,5 +4,5 @@ module_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) cd "$module_root" mkdir -p .test-results/package candidate_commit=$(sh tests/integration/check-candidate.sh) -git archive --format=zip --output=.test-results/package/basicrum-analytics.zip "$candidate_commit" -php tests/integration/check-artifact.php .test-results/package/basicrum-analytics.zip +git archive --format=zip --output=.test-results/package/basicrum-magento-2.zip "$candidate_commit" +php tests/integration/check-artifact.php .test-results/package/basicrum-magento-2.zip diff --git a/tests/integration/docker/install-artifact.sh b/tests/integration/docker/install-artifact.sh index ac134e4..d97b3a9 100644 --- a/tests/integration/docker/install-artifact.sh +++ b/tests/integration/docker/install-artifact.sh @@ -3,7 +3,7 @@ set -eu test "${BASICRUM_DISPOSABLE_MAGENTO:-}" = 1 test "${MAGENTO_ROOT:-}" = /var/www/html module_root=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd) -artifact="$module_root/.test-results/package/basicrum-analytics.zip" +artifact="$module_root/.test-results/package/basicrum-magento-2.zip" php "$module_root/tests/integration/check-artifact.php" "$artifact" cd "$MAGENTO_ROOT" # Exact destination inside this disposable stack. Do not delete stale files: diff --git a/tests/integration/release-gate.sh b/tests/integration/release-gate.sh index e91403e..9a5a20f 100755 --- a/tests/integration/release-gate.sh +++ b/tests/integration/release-gate.sh @@ -37,7 +37,7 @@ module_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) magento="${MAGENTO_ROOT}/bin/magento" candidate_commit=$(sh "$module_root/tests/integration/check-candidate.sh") echo "Checking Basicrum candidate ${candidate_commit} for ${BASICRUM_RELEASE_TAG}." -artifact=${BASICRUM_ARTIFACT:-$module_root/.test-results/package/basicrum-analytics.zip} +artifact=${BASICRUM_ARTIFACT:-$module_root/.test-results/package/basicrum-magento-2.zip} php "$module_root/tests/integration/check-artifact.php" "$artifact" php "$module_root/tests/integration/check-installed-candidate.php" @@ -47,6 +47,9 @@ sh "$module_root/tests/integration/install-csp-fixture.sh" "$magento" setup:upgrade "$magento" setup:di:compile "$magento" setup:static-content:deploy -f en_US +# Reused disposable stacks may retain the database but lose OpenSearch data. +# No cron runs here, and setup:upgrade can invalidate scheduled indexers. +"$magento" indexer:reindex catalogsearch_fulltext php "$module_root/tests/integration/config-save.php" cd "$module_root" diff --git a/tests/integration/test-artifact.php b/tests/integration/test-artifact.php index 0f363f6..999d8be 100644 --- a/tests/integration/test-artifact.php +++ b/tests/integration/test-artifact.php @@ -2,7 +2,7 @@ declare(strict_types=1); // Run after build-artifact.sh; mutate temporary copies, never the candidate ZIP. -$artifact = $argv[1] ?? dirname(__DIR__, 2) . '/.test-results/package/basicrum-analytics.zip'; +$artifact = $argv[1] ?? dirname(__DIR__, 2) . '/.test-results/package/basicrum-magento-2.zip'; if (!is_file($artifact)) { throw new RuntimeException('Build the candidate ZIP before running archive regression checks.'); } diff --git a/tests/php/check-composer-import.php b/tests/php/check-composer-import.php new file mode 100644 index 0000000..4ec03fb --- /dev/null +++ b/tests/php/check-composer-import.php @@ -0,0 +1,93 @@ +execute($command, $output) !== 0) { + throw new RuntimeException('Fixture Git command failed: ' . $process->getErrorOutput()); + } +}; +$writeManifest = static function (array $data) use ($fixture): void { + file_put_contents($fixture . '/composer.json', json_encode($data, JSON_THROW_ON_ERROR)); +}; +$checkImport = static function (string $expectedName) use ($fixture, $process): void { + $io = new BufferIO(); + $config = new Config(false); + $config->merge(['config' => ['home' => $fixture, 'cache-dir' => $fixture . '/cache']]); + $repository = new VcsRepository( + ['type' => 'git', 'url' => $fixture], + $io, + $config, + new HttpDownloader($io, $config), + null, + $process + ); + $repository->setLoader(new ValidatingArrayLoader(new ArrayLoader())); + $packages = $repository->getPackages(); + if ($repository->hadInvalidBranches()) { + throw new RuntimeException('Invalid VCS branch under ' . $expectedName . ":\n" . $io->getOutput()); + } + foreach ($packages as $package) { + if ($package->getName() !== $expectedName) { + throw new RuntimeException('VCS import did not use the default branch package name.'); + } + } + foreach (['dev-main', 'dev-rename', '0.0.2'] as $version) { + if ($repository->findPackage($expectedName, $version) === null) { + throw new RuntimeException('VCS import omitted ' . $expectedName . ' ' . $version); + } + } + echo 'PASS: validated default branch, rename branch and historical tag under ' . $expectedName . ".\n"; +}; + +try { + $git(['init', '--initial-branch=main', '--template=']); + $git(['config', 'user.name', 'Basicrum test']); + $git(['config', 'user.email', 'test@example.test']); + $git(['config', 'commit.gpgsign', 'false']); + // A tiny synthetic historical release; the rename branch uses real metadata. + $writeManifest(['name' => $oldName, 'description' => 'VCS import fixture', 'license' => 'MIT']); + $git(['add', 'composer.json']); + $git(['commit', '-m', 'Historical package']); + $git(['tag', '0.0.2']); + $git(['checkout', '-b', 'rename']); + $writeManifest($candidate); + $git(['commit', '-am', 'Candidate package metadata']); + $git(['checkout', 'main']); + $checkImport($oldName); + + // Model the same repository after the naming change reaches its default branch. + $writeManifest($candidate); + $git(['commit', '-am', 'Rename on default branch']); + $checkImport($newName); +} finally { + // This random directory was created above and contains only this test's fixture. + (new Filesystem())->removeDirectory($fixture); +} diff --git a/tests/php/run.php b/tests/php/run.php index 6ce9d09..dc0aa39 100644 --- a/tests/php/run.php +++ b/tests/php/run.php @@ -223,6 +223,11 @@ 512, JSON_THROW_ON_ERROR ); + basicrum_assert_same('basicrum/basicrum-magento-2', $composer['name'], 'canonical Composer package name'); + basicrum_assert_same('magento2-module', $composer['type'], 'Magento package type'); + foreach (['replace', 'provide'] as $alias) { + basicrum_assert_false(isset($composer[$alias]), 'no backward-compatible Composer alias: ' . $alias); + } basicrum_assert_same('^102.0', $composer['require']['magento/module-backend'], 'Backend dependency'); basicrum_assert_same('^101.2', $composer['require']['magento/module-config'], 'Config dependency'); basicrum_assert_same('^100.4', $composer['require']['magento/module-csp'], 'CSP dependency');