skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
spree/agent-skills106 installs

spree-upgrade-5-to-6

Use when upgrading a Spree 5.x application to Spree 6.0, or when fixing code that broke after that upgrade. Common phrasings include "upgrade to Spree 6", "migrate from 5.6 to 6.0", "Spree 6 breaking changes", "checkout_flow / state machine is gone", "Spree::Adjustment not found", "master variant removed", "PermissionSets NameError", "staff locked out after upgrade", "what replaced Shipment / Taxon / ReturnAuthorization / metafields", "spree_admin gem not found". Covers preconditions (5.6 first, Rails 8.1, Gemfile swaps, roles as data), the ordered 5.6 → 6.0 data backfills, grep recipes for code that must change, and the behavioral review checklist.

How do I install this agent skill?

npx skills add https://github.com/spree/agent-skills --skill spree-upgrade-5-to-6
view source ↗

Is this agent skill safe to install?

  • Gen Agent Trust Hubpass

    This skill is a developer guide for upgrading the Spree e-commerce framework from version 5.x to 6.0. It provides detailed checklists, configuration examples for data encryption, and command-line recipes for identifying deprecated code patterns. No malicious behavior or security risks were identified.

  • Socketpass

    No alerts

  • Snykpass

    Risk: LOW · No issues

What does this agent skill do?

Upgrading Spree 5.x → 6.0

Spree 6.0 is the breaking-change release. Cart and Order are separate models, state machines are gone (statuses + workflows), adjustments became typed rows (TaxLine / Discount / Fee), the master variant is gone, shipping became fulfillment/delivery, taxons became categories/collections, users became Spree::Customer, staff permissions became data, and the Rails admin, Rails storefront and API v2 were removed in favor of the React dashboard (spree_dashboard) and API v3.

The upgrade is: preconditions → gems → migrations → backfills → code fixes → behavioral review. Budget real time for the last two; the rake part is the easy part.

Generic mechanics (spree upgrade flags, STEP=, production release phase) are in the spree-upgrade skill. This skill is the 6.0-specific playbook.

1. Preconditions — do these before touching the Gemfile

  • Be on 5.6.x, fully upgraded. The 6.0 manifest assumes the 5.6 schema. Land on 5.6 first (spree upgrade --to 5.6, or bump to ~> 5.6 and run bundle exec rake spree:upgrade). The 6.0 run also replays the 5.4→5.5 and 5.5→5.6 backfills, but that doesn't replace being on 5.6.
  • Rails 8.1. spree_core 6.0 requires rails >= 8.1, < 8.2; Ruby ≥ 3.2. Do the Rails bump (and its own deprecations) as a separate commit if you can.
  • Back up the database and rehearse on a restored copy. Several backfills rewrite money, stock and customer rows; measure how long they take.
  • Gemfile swaps:
    • remove spree_admin and spree_storefront (gone — replaced by the React dashboard and the Next.js storefront);
    • add spree_dashboard (serves the admin at /dashboard) — skip it for API-only deployments;
    • add spree_meilisearch if Spree.search_provider points at Meilisearch;
    • every other spree_* extension (payments, i18n, your own) needs a 6.0-compatible release — check before you start, or bundle update won't resolve. spree_stripe, spree_easypost and spree_meilisearch now ship from the Spree monorepo in lock-step versions.
  • Action Text / Action Cable. spree_core no longer requires them. If your code uses has_rich_text, Action Text helpers, or Action Cable, add require 'action_text/engine' / require 'action_cable/engine' to config/application.rb.
  • Widen the rich-text allowlist first, if needed. Descriptions/notes are re-sanitized to what the dashboard editor emits (paragraphs, headings, inline formatting, code, blockquote, lists, links, images). Tables, div/span, inline style and arbitrary classes are stripped. If you need them, set Spree::RichTextSanitizer.allowed_tags += %w[…] / allowed_attributes += %w[…] in an initializer before migrate_rich_text_to_columns runs and before anything is saved under 6.0.
  • Recreate staff roles as data. Permission sets are removed with no bridge. A pre-existing role row comes up with no permissions — staff holding it are locked out (fail closed). Seed them before deploying:
    store.roles.find_or_create_by!(name: 'support').update!(permissions: %w[read_orders read_customers])  # roles belong to a store
    
    Keys are read_<resource> / write_<resource> from the catalog (Spree::ApiKey.known_scopes lists them).
  • Delete PermissionSets initializers. Spree::PermissionSets::* raises NameError and Spree.permissions.assign raises at boot. Extensions register resources with Spree.permissions.register_scope(:reviews, group: :catalog, resources: -> { [SpreeReviews::Review] }).
  • Customer migration check (Devise apps). spree:upgrade:migrate_users_to_customers copies spree_users into spree_customers and maps Devise's encrypted_password to password_digest. It only works if no Devise pepper was configured; with Devise no longer loaded it aborts until you pass CONFIRM_NO_PEPPER=true. Rows with blank/duplicate emails abort it too (SKIP_INVALID_ROWS=true to skip). Peppered/SSO/non-bcrypt installs keep a custom Spree.customer_class instead.
  • Initializer settings that moved to the store (track_inventory_levels, auto_capture, company, …) are copied by store_settings_backfill_from_config — leave them in the initializer until that step has run, then remove them.
  • Enable Active Record encryption. Spree 6 encrypts webhook signing secrets, gateway customer IDs and OAuth identity tokens — only when keys are configured. Make config/application.rb read them (inside the Application class):
    %i[primary_key deterministic_key key_derivation_salt].each do |key|
      value = ENV["ACTIVE_RECORD_ENCRYPTION_#{key.upcase}"].presence ||
        credentials.dig(:active_record_encryption, key).presence
      config.active_record.encryption[key] = value if value
    end
    
    Keys only in credentials, without this, leave webhook secrets and gateway customer IDs plaintext (their encrypts checks config.active_record.encryption). Generate a separate set per environment (spree encryption init for dev .env, spree encryption init --print / bin/rails db:encryption:init for production), set them on the host before deploying, back them up, never change them. Existing plaintext webhook secrets and gateway customer IDs become unreadable once encryption is on: deploy first with config.active_record.encryption.support_unencrypted_data = true and extend_queries = true, run [Spree::WebhookEndpoint, Spree::GatewayCustomer, Spree::UserIdentity].each { |m| m.find_each(&:encrypt) }, then remove both settings. See spree-security.
  • Secret API keys minted with settings lose /admin_users, /invitations, /roles — they now need read_staff / write_staff. Re-mint integration keys that manage staff.

2. Run the upgrade

# create-spree-app (ejected dev stack; Rails app in server/, older projects backend/)
spree upgrade
spree upgrade --plan              # after the gem bump: list every step

# classic Rails app / no CLI
bundle update
bundle exec rake spree:install:migrations && bin/rails db:migrate
DRY_RUN=1 bundle exec rake spree:upgrade
CONFIRM_NO_PEPPER=true bundle exec rake spree:upgrade

Production: deploy runs db:migrate, then the release phase runs bundle exec rake spree:upgrade (pass CONFIRM_NO_PEPPER=true there too if applicable). Take a maintenance window — in-flight checkouts are converted into carts by the backfills.

3. What the 5.6 → 6.0 manifest runs (in order)

From spree_core/lib/spree/upgrades/5_6_to_6_0/manifest.yml. All idempotent; re-run one with spree upgrade --step <id>.

#Step idWhat it does
1consolidate_metadataSafety net: merges any leftover public_metadata/private_metadata pairs into the single metadata column
2migrate_media_class_namesRewrites stored Spree::Asset/Spree::Image class names to Spree::Media
3migrate_master_images_to_product_mediaMoves variant-pinned images onto the product gallery (enqueues jobs — let them drain)
4backfill_media_store_idsGives every media row a store_id
5remove_master_variantBackfills default_variant_id, retires master variants
6migrate_taxons_to_categories_and_collectionsAutomatic taxons → Spree::Collection; the rest stay Spree::Category; rewrites Spree::Taxon* strings
7backfill_library_media_placementsPuts category/collection images into the media library
8migrate_users_to_customersspree_users → spree_customers, repoints user_type columns (see Devise pepper precondition)
9migrate_rich_text_to_columnsAction Text bodies → text columns, sanitized
10migrate_shipping_to_deliveryShipment → fulfillment strings/statuses, fulfillment providers, display_on → storefront_visible
11migrate_delivery_profilesShipping categories → store-owned delivery profiles
12migrate_fulfillment_statusesRemaps to unfulfilled / fulfilled / delivered
13migrate_deliveriesTracking numbers → spree_deliveries rows
14migrate_zones_to_delivery_zonesDelivery-referenced zones → Spree::DeliveryZone
15migrate_country_state_codesFills ISO country_code/state_code columns (countries/states are reference data now)
16migrate_promotion_option_value_rulesOption-value promo rules → stable option value ids
17migrate_adjustments_to_typed_rowsspree_adjustments → TaxLine / Discount / Fee; flags non-reconciling orders
18backfill_reason_store_idsStore-owns return/refund reasons
19migrate_returnsReturnAuthorization → Spree::Return / Spree::Exchange (aborts on failed rows; SKIP_FAILED_ROWS=true)
20migrate_stock_movements_to_typed_rowsTypes stock movements; re-allocates open fulfillments (stock now leaves at dispatch)
21backfill_delivery_and_stock_store_idsStore-owns delivery methods and stock locations
22migrate_tax_zonesTax rate zones → country/state on the rate
23backfill_tax_store_idsStore-owns tax rates/categories
24backfill_order_coupon_codesFills the new spree_orders.coupon_code on historical placed orders
25backfill_order_marketsAssigns default market to orders
26migrate_incomplete_orders_to_cartsIncomplete orders → Spree::Cart (same token)
27migrate_calculator_bounds_to_delivery_method_rulesFlatRate min/max bounds → DeliveryMethodRule
28store_settings_backfill_from_configCopies moved Spree::Config settings onto every store
29migrate_capture_methodsauto_capture → capture_method on payment methods
30product_types_backfillStore-owns product types
31search_reindexRebuilds indexing search providers (Meilisearch); no-op for the DB provider
32fold_store_credit_categoriesStore credit category → memo
33migrate_stripe_webhook_keys(optional, spree_stripe only) webhook secrets onto the gateway
34backfill_import_export_tenancyGives imports a store_id (+ seller_id)
35backfill_custom_field_definition_storesStore-owns custom field definitions
36package_typesStore default box → package type row
37migrate_external_receives_to_purchase_orders5.x supplier receives → purchase orders; gives transfers a store/status
38backfill_actor_typesFills the new actor *_type columns (canceler, approver, created_by, …) with the admin user class; until it runs, old rows resolve with a deprecation warning
39recount_stock_levelsFills reserved_count / incoming_count

Always trust spree upgrade --plan (run after the gem bump) over this table — the gem you install is the source of truth.

Post-check: frozen orders

migrate_adjustments_to_typed_rows never changes money. Orders whose typed sums don't reconcile with stored totals are left untouched and flagged in metadata (the guide says private_metadata — that column is now metadata). Review each:

# spree console
Spree::Order.where.not(completed_at: nil).find_each.select { |o| o.metadata['typed_adjustments_frozen'].present? }
  .map { |o| [o.number, o.metadata['typed_adjustments_frozen']] }

Reasons include totals_do_not_reconcile and return-authorization freezes (handled by migrate_returns).

4. Fix your code — grep recipes

Run these over server/app server/config server/lib server/spec (classic apps: app config lib spec), plus apps/storefront, apps/dashboard and any integration code. The full pattern → replacement table is in references/grep-recipes.md. The hits that break boot or checkout first:

cd server   # or the app root
grep -rnE "checkout_flow|go_to_state|insert_checkout_step|remove_checkout_step|state_machine|before_transition|after_transition" app config lib
grep -rnE "PermittedAttributes|permitted_[[:alnum:]_]+_attributes" app config lib
grep -rnE "PermissionSets|permissions\.assign|ApiKey::SCOPES" app config lib
grep -rnE "Spree::Adjustment|\.adjustments\b|promo_total|ship_total|item_count" app config lib
grep -rnE "\.master\b|is_master|default_price|DefaultPrice|variant\.price\b|\.price =" app config lib
grep -rnE "finalize!|order\.completed|OrderUpdater|register_update_hook|\bresume!?\b" app config lib
grep -rnE "ReturnAuthorization|CustomerReturn|Reimbursement|ReturnItem" app config lib
grep -rnE "_service\s*=" config

Deprecated-but-working names (Shipment, ShippingMethod, Taxon, metafields, firstname, Spree.user_class, …) log Spree::Deprecation warnings — fix them now; they're removed in 6.1. Run the suite with deprecations visible and treat each warning as a to-do.

5. Behavioral review checklist

These don't raise — they change what happens. Walk through each.

  • Completed carts are read-only. Post-checkout edits belong to the Spree::Order. Anything that mutated an order after completion through cart APIs must move to order-side admin services.
  • line_item.order may be nil. LineItem, Fulfillment, Payment, TaxLine, Discount, Fee and StockReservation have cart_id + order_id (exactly one set). Use #owner.
  • Statuses are derived. payment_status / fulfillment_status are written only by Spree::Orders::UpdateStatuses; direct assignment is overwritten. payment_state/shipment_state/state are read aliases until 6.1. Order filter: q[status_eq]=placed.
  • Placed orders are money-frozen. Recalculation re-sums typed rows; it never regenerates them. Post-placement money edits go through Spree::Orders::Discounts::* / Orders::Fees::*.
  • 5.x extensions call Order names on carts. Gateway gems written when a cart was an incomplete order call promo_total, item_count, shipments, user, available_payment_methods … on Spree::Cart. Those deprecated names now warn and forward on Cart as well as Order until 6.1 (see the grep recipes). Watch the deprecation log and update the gem.
  • Free gifts don't count toward thresholds. A create_line_items gift no longer counts toward item_total rules or the base of a percentage-off order discount, including its own promotion's threshold. "Spend over $100, get a gift" takes the gift back once the paid items fall to $100.
  • Cancel is final. No resume for orders or fulfillments; order.resumed / fulfillment.resumed events are gone. A fully recalled order reads fulfillment_status: unfulfilled.
  • Spree::Store.default can be nil. Set Spree::Current.store in jobs, rake tasks, seeds and tests that run outside a request; ensure a default store exists before creating store-scoped records.
  • belongs_to is required by default in anything inheriting Spree::Base — including your own models. Add optional: true where blank is legitimate. The error reads "must exist", not "can't be blank" (fix assertions and clients parsing 422 details).
  • Storefront contract. requirements[] entries carry { step, field, code, message }; the delivery requirement is field: 'delivery_method', code: 'delivery_method_required'. Rich-text reads return plain text plus *_html (description_html, internal_note_html) — hydrate editors from *_html. Cart number mirrors the prefixed cart_ ID for one release.
  • Webhooks/events. Subscribe to order.placed (order.completed is a deprecated alias until 6.1). report.*, order.resumed, fulfillment.resumed are removed. New: return.*, exchange.*, claim.*, digital_asset.*, digital_link.downloaded.
  • Rich-text allowlist narrowed (see preconditions). Content is re-sanitized on next save.
  • Digital downloads answer 302 to a short-lived signed URL (≤ 1 hour, digital_asset_link_expire_time). Clients must follow redirects. Customers now get a download-links email from spree_emails — suppress your own if you sent one.
  • Custom field definitions are store-scoped. Multi-store installs end up with all definitions on the default store; recreate them per store. Product CSV columns use the custom_field. prefix (was metafield.) and product_type (was shipping_category).
  • Staff API permissions. JWT staff pass the same per-controller gate as secret keys (403 with details.required_permission). Staff endpoints need read_staff / write_staff.
  • Store API authorization no longer consults CanCanCan — customer access is ownership scoping plus Spree::Storefront::AccessPolicy. Move storefront can rules into an access-policy subclass or a workflow validate hook.
  • Countries and states are reference data (plain objects from the countries gem, no .where/.find). Records name them by ISO country_code / state_code.
  • Stock leaves at dispatch, not at placement (open fulfillments hold an allocation). Inventory reports built on count_on_hand alone will read differently.
  • Jobs. New projects run Solid Queue inside Puma (Mission Control at /jobs). An upgraded app keeps its own config.active_job.queue_adapter — Sidekiq keeps working; just move any Spree recurring jobs into whatever scheduler you use.
  • SDK majors. Bump @spree/sdk to 2.x in the storefront and @spree/admin-sdk to 1.x in integrations; replace the Rails admin customizations with apps/dashboard plugins (spree add dashboard).

Gotchas

  • Don't reorder backfills. Several steps depend on earlier ones (media before master-variant removal; taxons and customers before rich text; adjustments before carts and returns; returns before stock movements). The manifest order is load-bearing.
  • migrate_users_to_customers aborts by design without CONFIRM_NO_PEPPER=true once Devise is gone. Confirm no pepper was used rather than forcing it.
  • Overriding a *_service DI key does nothing now. Legacy writes like Spree::Dependencies.carts_complete_service = 'MyComplete' are stashed, not applied. Move to the *_workflow key (subclass the shipped workflow) or, better, a Spree.hooks.register handler — see spree-dependencies and spree-workflows.
  • Spree::Product.additional_permitted_attributes << :x raises FrozenError. Use += [:x].
  • Legacy tables stay until 6.1 (spree_adjustments, return-authorization tables, spree_state_changes, spree_log_entries, spree_users, Action Text rows) as rollback sources. Export anything you want to keep before 6.1.

Where to read further

  • node_modules/@spree/docs/dist/developer/upgrades/5.6-to-6.0.md — the official guide (https://spreecommerce.org/docs/developer/upgrades/5.6-to-6.0)
  • references/grep-recipes.md — full pattern → replacement table
  • Related skills: spree-upgrade, spree-workflows, spree-checkout, spree-order-totals, spree-fulfillment, spree-returns, spree-auth-permissions, spree-catalog, spree-pricing, spree-dashboard-plugins, spree-typescript-sdk

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/spree/agent-skills/spree-upgrade-5-to-6">View spree-upgrade-5-to-6 on skillZs</a>