Skip to content

Add interactive project upgrade wizard#1063

Draft
Soner (shyim) wants to merge 17 commits into
nextfrom
claude/great-carson-fvQki
Draft

Add interactive project upgrade wizard#1063
Soner (shyim) wants to merge 17 commits into
nextfrom
claude/great-carson-fvQki

Conversation

@shyim

Copy link
Copy Markdown
Member

Summary

Implements an interactive upgrade wizard for Shopware projects that guides users through the upgrade process with a TUI interface. This mirrors the behavior of the shopware/web-installer but runs from the command line.

Key Changes

New Packages

  • internal/projectupgrade/ - Core upgrade logic package containing:

    • wizard.go - Interactive TUI-based upgrade wizard using Bubble Tea
    • composer.go - Composer.json rewriting for target versions
    • plugins.go - Plugin compatibility resolution and constraint bumping
    • registry.go - Package registry abstraction for version lookups
    • releases.go - Version filtering and selection logic
    • Comprehensive test coverage for all modules
  • cmd/project/project_upgrade.go - New project upgrade command with:

    • Interactive wizard mode (default when TTY available)
    • Headless mode for CI/CD pipelines
    • Git working tree validation
    • Composer-managed plugin verification

Upgrade Workflow

The wizard guides users through:

  1. Welcome screen - Confirm upgrade intent
  2. Version selection - Choose target Shopware version
  3. Compatibility check - Query Shopware Account API for extension compatibility
  4. Review - Confirm selected version and changes
  5. Execution - Run upgrade tasks with live progress:
    • Back up composer.json
    • Clean up stale recipe files
    • Resolve incompatible custom plugins
    • Rewrite composer.json
    • Run composer update --with-all-dependencies
    • Execute bin/console system:update:prepare
    • Execute bin/console system:update:finish

Plugin Resolution

  • Detects incompatible custom plugins by checking Shopware package constraints
  • Attempts to find compatible versions via registry (Packagist or Shopware Store)
  • Bumps constraints to compatible versions when available
  • Removes plugins with no compatible release
  • Restores composer.json on failure

Registry System

  • CombinedRegistry routes lookups to appropriate backend (Store vs Packagist)
  • PackagistRegistry queries repo.packagist.org with minified format support
  • Extensible interface for custom registry implementations

Supporting Changes

  • Enhanced internal/git/ with IsRepository() and WorkingTreeStatus() functions
  • Extended internal/flexmigrator/ with CleanupByHash() for recipe file cleanup
  • Version filtering logic matching web-installer behavior

Implementation Details

  • Uses Bubble Tea for interactive TUI with spinner animations
  • Streams subprocess output (composer, console commands) to live log view
  • Graceful error handling with composer.json restoration on failure
  • Context-based cancellation support for long-running operations
  • Comprehensive test coverage including wizard state machine and plugin resolution

https://claude.ai/code/session_01F4pXpGEuJGsxpfTudpnoLo

Mirrors the shopware/web-installer Update flow so projects can be
upgraded from the command line:

- Reads the current Shopware version from composer.lock
- Filters available releases the same way as ReleaseInfoProvider (next
  major + remaining patches of the current major, no RCs)
- Prompts for the target version (or `--to` flag), then runs the
  existing extension compatibility check before continuing
- Backs up composer.json, cleans up recipe-managed stale files by MD5,
  removes incompatible symlinked custom plugins, rewrites composer.json
  (shopware/core + administration/storefront/elasticsearch when
  required, minimum-stability for RC targets, symfony/runtime
  constraint relax)
- Runs `composer update --with-all-dependencies --no-scripts` and
  restores the backup on failure
- Runs `bin/console system:update:prepare` and `system:update:finish`
- Tracks the outcome

Extracts the MD5-based cleanup map from `flexmigrator.Cleanup` into a
new `flexmigrator.CleanupByHash` helper so the upgrade flow can reuse
it without also deleting flex-migration-specific files.
The interactive flow is now a small bubbletea Program that mirrors
the install-wizard / setup-guide visual idiom:

- Welcome card (cowsay mascot) with current version + project root
- Step 1: select target version (RenderSelectList)
- Step 2 (when extensions are installed): compatibility lookup with
  spinner, then per-extension checkmark/blocker icons
- Step 3: review card with from/to/executor and the full task list
- Step 4: running phase with per-task spinner / checkmark / failure
  icons and a live tail of the composer/console output
- Done card summarising success or failure, restored composer.json on
  failure, and listing any plugins that were dropped

Non-interactive mode (`-n`) and `--to <version>` continue to use the
existing headless flow so CI runs are unchanged.
The upgrade rewrites composer.json, deletes recipe-managed files, and
drops incompatible plugins. Mixing those rewrites with unrelated
uncommitted changes makes it hard to review the diff or roll back, so
the command now refuses to run with a dirty working tree.

- Adds `git.IsRepository` and `git.WorkingTreeStatus` helpers so other
  commands can reuse the same checks.
- When the project directory is not inside a git working tree the
  check is skipped (greenfield projects, tarball-installed copies).
- The error message lists up to ten changed paths and points at
  `--allow-dirty` as the explicit override.
…gins

Before doing anything destructive, `project upgrade` now requires every
directory under custom/plugins/ to be tracked by composer (i.e. appear
in vendor/composer/installed.json). When plain file-drop plugins are
detected the upgrade aborts with a pointer at
`project autofix composer-plugins`. The `--allow-non-composer` flag
opts out for projects that have not migrated yet.

When a composer-managed plugin's declared shopware/core constraint is
not satisfied by the upgrade target, the resolver now queries a package
registry (repo.packagist.org for plain composer packages,
packages.shopware.com for store.shopware.com/* packages) for the newest
release whose require.shopware/core does satisfy the target and rewrites
the composer.json constraint to "^<that-version>". Only when no
compatible release is found does the plugin fall back to being dropped,
matching the old behaviour.

The Shopware Packages token is read from SHOPWARE_PACKAGES_TOKEN or the
project's auth.json. When neither is present and the project has store
plugins the interactive flow prompts for the token (and skips store
lookups gracefully if the prompt is left empty).

The wizard's "Done" card now lists bumped constraints (old → new) in
addition to the removed plugins, so users can see exactly what shifted.

Tests: 9 new tests covering the resolver (bump, remove, registry error,
no installed.json), FindNonComposerPlugins, and the
ensureAllPluginsAreComposerManaged pre-flight check. All packages pass.
- Drop trailing punctuation from the dirty-git-tree and
  non-composer-plugin error strings (ST1005).
- Make the phase / task-status switches exhaustive (exhaustive).
- Rewrite the compat-result if/else chain as a tagless switch
  (gocritic).
- Rename the `max` parameter in `truncate` to `maxRunes` so it stops
  shadowing the predeclared builtin (predeclared).
- Wrap `resp.Body.Close()` in a small `closeBody` helper so we don't
  ignore its error inline (errcheck).
- Rename the test-only `stringErr` type to `testError` to match the
  `xxxError` naming convention (errname).
- Add `t.Parallel()` to the render-smoke subtest (tparallel).
- Drop the unused `upgradeDoneMsg` type (unused).
- Drop the unused `projectRoot` parameter from `runCompatibilityCheck`
  (unparam).
The registry duplicated the packagist HTTP client, the composer v2
minified-metadata unminifier, the response-body closer, and the
packages.json fetch. Move the generic lookups into the packagist
package instead:

- Add packagist.GetComposerPackageVersions(ctx, name) for any composer
  package and rebuild GetShopwarePackageVersions on top of it.
- Add a Require field to packagist.PackageVersion so store-package
  metadata carries its shopware/core constraint.

PackagistRegistry and ShopwareStoreRegistry now delegate to the
packagist package, dropping ~120 lines of duplicated logic.
@lasomethingsomething

Copy link
Copy Markdown
Contributor

Next step: prep this for team demo

The upgrade flow parsed vendor/composer/installed.json by hand, resolved
install paths, and re-implemented composer version-constraint checks.
Move that composer logic into the packagist package where the rest of
the composer model (composer.json, composer.lock, auth.json) already
lives:

- packagist.InstalledJson / InstalledPackage / ReadInstalledJson model
  and read vendor/composer/installed.json.
- InstalledPackage.InstallDirName resolves a package's install location
  (symlinks included) to its directory name under a given base dir.
- packagist.ConstraintsSatisfiedBy reports whether a require map's
  constraints for a set of packages are satisfied by a target version.
- packagist.BumpConstraint turns a concrete version into a caret
  constraint.

projectupgrade now consumes these helpers and keeps only the upgrade
policy (which Shopware packages matter, registry resolution). Its
duplicate ShopwarePackages list is reused in place of the former
pluginShopwarePackages. plugins.go drops ~145 lines.
@shyim Soner (shyim) force-pushed the claude/great-carson-fvQki branch 2 times, most recently from 742a657 to 256390f Compare May 29, 2026 07:15
…teps

Several fixes to the project upgrade flow surfaced by real upgrades:

- Treat store "with new Shopware version" status as resolvable, not a
  blocker. Classification now keys on the semantic status name
  (notCompatible) instead of the display color, matching the platform's
  ExtensionCompatibility constants.
- Resolve plugin constraints for vendor-installed plugins, not just those
  under custom/plugins/. Scope candidates by the root composer.json require
  so store plugins (swag/*, frosh/*) installed into vendor/ get bumped too.
- Look up store-owned, vendor-named plugins via the store registry first
  (falling back to Packagist) instead of routing only by name prefix.
- Run system:update:prepare before composer update so it executes on the
  still-installed Shopware; restore composer.json if prepare fails.
- Center the wizard in the terminal and replay the full failed-step log
  after the alt-screen tears down.

Adds tests for status classification, vendor-installed resolution, registry
routing, and upgrade step ordering.
@shyim Soner (shyim) force-pushed the claude/great-carson-fvQki branch from 39ef87d to 951109a Compare May 29, 2026 07:23
Replace the wizard's hand-rolled version-list cursor and rendering with the
reusable tui.SelectList component: it owns the cursor, windowing, paging
(PgUp/PgDn, Home/End) and the navigation shortcuts, so the wizard only
forwards keys and renders.
Claude (claude) and others added 4 commits June 1, 2026 12:17
The upgrade used to drive the lifecycle by hand: stash a composer.json
backup, run system:update:prepare on the old code, swap vendor via
composer update, run system:update:finish on the new code, and rewind
composer.json on any failure. shopware-deployment-helper already owns
that lifecycle (prepare, migrations, finish, theme compile, plugin
refresh) and is what production deployments run, so the wizard now
delegates to it instead of orchestrating each step itself.

- UpdateComposerJson additionally calls EnsureRequire to add
  shopware/deployment-helper to composer.json when the project does
  not require it yet. The subsequent composer update pulls it in.
- The wizard's task list drops the dedicated "Back up composer.json",
  "system:update:prepare" and "system:update:finish" tasks and gains
  a single "vendor/bin/shopware-deployment-helper run" task that runs
  after composer update. Five tasks total instead of seven.
- All composer.json backup / restore plumbing is removed from both
  the wizard and the headless flow. The pre-flight clean-git-tree
  check already guarantees `git checkout composer.json composer.lock`
  is a one-liner if the user wants to revert.
- packagist.ComposerJson.EnsureRequire is the new shared helper for
  "add this require entry if missing"; the devtui setup guide reuses
  it in place of its private branch.
The compatibility phase used to call the Shopware account API to ask
"which extensions are blocked by this Shopware version?". The same
question is already answered by the composer-managed plugin metadata
the resolver consults right after: for each platform plugin, its
require.shopware/* tells us whether it satisfies the target, and the
registry (Packagist + Shopware store) tells us whether a newer
release would. Use that consistent source instead and drop the
account API dependency from the upgrade flow:

- projectupgrade.CheckPluginCompatibility is a dry-run of the
  resolver: for every shopware-platform-plugin in composer.json's
  require it reports CompatCompatible / CompatUpdatable (with the
  target version) / CompatBlocker (no compatible release) /
  CompatUnknown (registry unreachable - e.g. store token missing).
- The wizard's loadCompatibility now calls that instead of
  account_api.GetFutureExtensionUpdates, and renders rows as
  `name — current → new` (Updatable) or `name — current — status`.
- The headless runCompatibilityCheck mirrors the wizard's logic and
  renders a Plugin / Current / Status table.
- packagist.InstalledPackage gains a Version field so the report can
  show the installed version.
- WizardOptions.Extensions and the "skip compat phase when no
  extensions" branch are removed - the phase always runs against the
  composer data, and the step counter is always 4.

The account-api package itself stays in the codebase; it is still
used by `project upgrade-check` and other commands that genuinely
need the store's extension catalog.
…vQki

Resolve conflicts in cmd/project (ci.go imports, ci_test.go helpers,
project_create.go decomposition). Revert internal/git additions
(IsRepository/WorkingTreeStatus) in favor of origin/main's
IsWorkingTreeDirty; rewire project upgrade clean-tree check accordingly.
Replace the homegrown registry + constraint resolver with composer itself.
The upgrade now runs `composer require --no-install -W shopware/core:<target>
<plugins…>` and reads composer's verdict: success means the upgrade resolves,
a non-zero exit with "could not be resolved" means it doesn't, and the
blocking plugin(s) are dropped and retried.

- new composer_resolve.go: DryRunRequire (compat preview, --dry-run, nothing
  written) and ApplyRequire (real require, drops unresolvable plugins and
  retries). Store plugins resolve via the project's own composer config /
  auth.json - no token plumbing.
- delete registry.go, compatibility.go and the version-finding half of
  plugins.go; keep FindNonComposerPlugins and the ResolveResult reporting type.
- wizard + command rewired off Registry; compat phase shows composer's own
  resolution and the plugins it cannot resolve.
- drop now-unused packagist.ConstraintsSatisfiedBy / BumpConstraint.
@Weltraumakustik

Copy link
Copy Markdown
Member

Arnold Stoba (@arnoldstoba) would you please do a quick review from UX Engineering perspective on this? Maybe there's additional ideas or optimizations

@arnoldstoba

Arnold Stoba (arnoldstoba) commented Jun 25, 2026

Copy link
Copy Markdown
Screenshot 2026-06-25 at 13 54 36

General

  • There is a general height problem in this wizard that imo we have to address. Maybe we can work on a smaller version of the elephant header part, just display it on the first step or just remove it completely for these kind of larger wizard/config steps, but parts of the TUI are cut off even on a 1920x1080 size terminal, would be even worse on a smaller laptop screen.
  • This height problem is also especially visible in the last step, the actual upgrade. Due to the nature of the TUI the logs are cut off / pushing up the height of the TUI content. This leads to not being able to read some of the logs at all. Idk what exactly is possible here but potentially we could display them in some sort of scroll container? I'm not sure what the best option would be here, @shopware/product-dx.

Visuals

  • The horizontal dividers could all be full width for consistency, this seems cleaner to me but it's an optional visual-only change.
  • We could also improve the general width of the container in order to avoid all of these content wrapping issues. Might lessen the readability slightly and could clash with reusable components used in other parts of the CLI.
  • The terminal bullet points are always wrapping onto the beginning of the next line whenever the content is too long. I think this should be pretty easily solvable and would improve readability and visual cleanliness. Basic example of how this could be abstracted into a go bullet.go TUI component which then could be reused everywhere (same could be done for the key-value pair UI e.g. "Version: 6.5.0.0" etc.):
package tui

import "charm.land/lipgloss/v2"

// bulletMarker is the standard bullet prefix: 2 spaces + bullet + space = 4
// display columns. The leading spaces live inside the marker so the hanging
// indent column is derived directly from the marker width.
const bulletMarker = "  • "

// HangingIndent renders body to the right of marker so that when body wraps to
// the given total width, continuation lines align under the body text instead
// of the marker. marker appears as-is on the first line; continuation lines are
// indented by marker's display width. body may carry its own ANSI styling and
// explicit newlines — only its Width is set, so the styling is preserved.
func HangingIndent(marker, body string, width int) string {
	markerW := lipgloss.Width(marker)
	bodyW := width - markerW
	if bodyW < 1 {
		// No room to wrap; join unwrapped rather than pass a negative Width.
		return lipgloss.JoinHorizontal(lipgloss.Top, marker, body)
	}
	wrapped := lipgloss.NewStyle().Width(bodyW).Render(body)
	return lipgloss.JoinHorizontal(lipgloss.Top, marker, wrapped)
}

// Bullet renders "  • text" (dim marker, LabelStyle text) with a hanging indent
// so wrapped text aligns under the first line instead of the bullet.
func Bullet(text string, width int) string {
	return HangingIndent(DimStyle.Render(bulletMarker), LabelStyle.Render(text), width)
}

// BulletRaw is like Bullet but leaves text unstyled, so callers can pass an
// already-styled or mixed-style body (e.g. a label followed by a dim detail).
func BulletRaw(text string, width int) string {
	return HangingIndent(DimStyle.Render(bulletMarker), text, width)
}
Screenshot 2026-06-25 at 14 03 25
  • For better separation we could also display the keyboard shortcuts below the "card". This would make it a bit more visually clear what the most important actions of the current step are, since then they would be the last element placed at the bottom of the card.
Screenshot 2026-06-25 at 14 08 12
  • In general the steps/flow make sense to me and are clear to understand. It's easy to select all the options and all necessary information is provided. Good work @shopware/product-dx.

I'm happy to help out with details or additional feedback, just let me know 👍🏻

Soner (shyim) and others added 3 commits July 7, 2026 10:57
Implements the local upgrade wizard flow from #1167:

- Preflight checklist panel with visible, re-runnable safety checks
  (composer.lock, clean git tree, composer-managed plugins, running web
  environment, Packagist reachability) that block the flow until fixed
- Version selection with release-notes links
- Prepare panel with BLOCKED/READY state, system preparation checks and a
  risk-ranked extension compatibility queue (State | Name | Current ->
  Target | Result) derived from composer's dry-run verdict, including
  deprecated/replaced detection from composer abandoned warnings
- Per-extension detail overlays with contextual user actions; blocked
  extensions must be updated or explicitly marked for removal before the
  upgrade can start
- Exportable Markdown support report (OK / needed review / was blocked
  blocks, PHP requirement of the target, composer.json, raw composer
  output on blockers) from the review and done panels
- Full-terminal-width live log during the upgrade and a post-upgrade
  validation checklist on completion
- Consistent header bar: current version | environment | target + docs

Headless (--to / non-interactive) behaviour is unchanged; its hard safety
checks now run only on that path since the wizard owns them interactively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFmjkiNERLCC1fE45wS7cj
…vQki

Conflict resolution: cmd/project/ci_test.go had TestProjectCISafetyCheck
on both sides at different positions; kept next's placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFmjkiNERLCC1fE45wS7cj
Adopt the wizard UI polish that landed on next for the devtui migration
wizard: drop the 'Step X/Y' badges in favor of clear per-panel titles
(#1153) and keep the terminal window title in sync with the current step
or view (#1159).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFmjkiNERLCC1fE45wS7cj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants