Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to MageBox will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.2.0] - 2026-09-28

### Changed

- **`MAGE_RUN_CODE` Is Only Sent When a Domain Names a Store** - MageBox injected `MAGE_RUN_CODE` and `MAGE_RUN_TYPE` into every vhost, so Magento looked for a store with that exact code even in single-store projects whose default code is not `default`, instead of resolving the store itself. A domain now contributes those variables only when it sets `store_code`, and one project can mix store and website codes. Thanks to Louis de Looze. ([#109](https://github.com/qoliber/magebox/pull/109), [#107](https://github.com/qoliber/magebox/issues/107))
- **`mage_run_code` and `mage_run_type` Renamed to `store_code` and `store_type`** - The old names still work and print a deprecation warning on `magebox start`, because the config loader ignores unknown keys and a project written for an older release would otherwise lose its store codes silently and serve the wrong store.

### Added

- **`--store-type` on `magebox domain add`** - Declares whether a code names a store view or a website.

### Fixed

- **A Second Multistore Project Would Have Stopped nginx Entirely** - The store map was written per project into the shared vhosts directory, and nginx rejects a second `map` for the same variable with `duplicate "MAGE_RUN_CODE" variable`, which stops it serving every project on the machine. All projects now share one map file, rebuilt from every project on `magebox start`, and the per-project files an earlier build left behind are removed.
- **Store Codes Leaked into Subdomains** - The map matched with a leading dot in nginx's `hostnames` mode, so a deliberately code-less `admin.mystore.test` inherited the code of `mystore.test`. Hosts are now matched exactly.
- **An Invalid `store_type` Passed Silently** - Anything other than `store` or `website` was sent to Magento as `MAGE_RUN_TYPE`. A typo now fails validation.

## [2.1.2] - 2026-09-24

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.1.2
2.2.0
19 changes: 13 additions & 6 deletions cmd/magebox/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ var domainListCmd = &cobra.Command{

var (
domainStoreCode string
domainStoreType string
domainRoot string
domainSSL bool
)

func init() {
domainAddCmd.Flags().StringVar(&domainStoreCode, "store-code", "", "Magento store code (default: \"default\")")
domainAddCmd.Flags().StringVar(&domainStoreCode, "store-code", "", "Magento store/website code for multi-store setup")
domainAddCmd.Flags().StringVar(&domainStoreType, "store-type", "", "Magento run type: \"store\" or \"website\" (default: \"store\")")
domainAddCmd.Flags().StringVar(&domainRoot, "root", "", "Document root relative to project (default: \"pub\" for Magento, \"public\" for Laravel)")
domainAddCmd.Flags().BoolVar(&domainSSL, "ssl", true, "Enable SSL for the domain")

Expand Down Expand Up @@ -92,9 +94,10 @@ func runDomainAdd(cmd *cobra.Command, args []string) error {

// Create new domain
newDomain := config.Domain{
Host: host,
Root: domainRoot,
MageRunCode: domainStoreCode,
Host: host,
Root: domainRoot,
StoreCode: domainStoreCode,
StoreType: domainStoreType,
}

// Only set SSL if explicitly changed from default (true)
Expand Down Expand Up @@ -163,7 +166,9 @@ func runDomainAdd(cmd *cobra.Command, args []string) error {
}

fmt.Println()
cli.PrintInfo("Domain %s configured with store code: %s", host, newDomain.GetStoreCode())
if newDomain.StoreCode != "" {
cli.PrintInfo("Domain %s configured with store code: %s (%s)", host, newDomain.StoreCode, newDomain.GetStoreType())
}

return nil
}
Expand Down Expand Up @@ -284,7 +289,9 @@ func runDomainList(cmd *cobra.Command, args []string) error {
fmt.Printf(" %s\n", cli.Highlight(d.Host))
fmt.Printf(" URL: %s://%s\n", protocol, d.Host)
fmt.Printf(" Root: %s\n", d.GetRoot())
fmt.Printf(" Store Code: %s\n", d.GetStoreCode())
if d.StoreCode != "" {
fmt.Printf(" Store Code: %s (%s)\n", d.StoreCode, d.GetStoreType())
}
fmt.Printf(" SSL: %s\n", sslStatus)
fmt.Println()
}
Expand Down
4 changes: 4 additions & 0 deletions internal/config/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func (l *Loader) loadFileWithIncludes(path string, visited map[string]bool) (*Co
return nil, &ParseError{Path: path, Err: err}
}

// Settings renamed in 2.3.0 are migrated here rather than at each use, so a
// project written for an older release cannot silently lose them.
config.DeprecationWarnings = config.MigrateDeprecatedKeys()

if len(config.IncludeConfig) == 0 {
return &config, nil
}
Expand Down
65 changes: 65 additions & 0 deletions internal/config/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package config

import "testing"

// A store type other than store or website silently produces a MAGE_RUN_TYPE
// Magento does not understand, so a typo must fail validation.
func TestConfigValidatesStoreType(t *testing.T) {
base := func(storeType string) *Config {
return &Config{
Name: "mystore",
PHP: "8.3",
Domains: []Domain{{Host: "mystore.test", StoreCode: "nl", StoreType: storeType}},
}
}

tests := []struct {
name string
storeType string
wantError bool
}{
{name: "unset defaults to store", storeType: "", wantError: false},
{name: "store", storeType: "store", wantError: false},
{name: "website", storeType: "website", wantError: false},
{name: "typo", storeType: "websites", wantError: true},
{name: "store view", storeType: "store_view", wantError: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := base(tt.storeType).Validate()
if (err != nil) != tt.wantError {
t.Errorf("Validate() error = %v, wantError %v", err, tt.wantError)
}
})
}
}

// Projects written for earlier MageBox releases use mage_run_code and
// mage_run_type. The loader ignores unknown keys, so without a migration those
// projects would quietly lose their store codes and serve the wrong store.
func TestMigrateDeprecatedStoreKeys(t *testing.T) {
cfg := &Config{
Name: "mystore",
Domains: []Domain{
{Host: "mystore.test", MageRunCode: "nl", MageRunType: "website"},
{Host: "de.mystore.test", StoreCode: "de", MageRunCode: "ignored"},
{Host: "plain.test"},
},
}

warnings := cfg.MigrateDeprecatedKeys()

if cfg.Domains[0].StoreCode != "nl" || cfg.Domains[0].StoreType != "website" {
t.Errorf("legacy keys were not migrated: %+v", cfg.Domains[0])
}
if cfg.Domains[1].StoreCode != "de" {
t.Errorf("an explicit store_code must win over the legacy key: %+v", cfg.Domains[1])
}
if len(warnings) == 0 {
t.Error("migrating a legacy key should report a deprecation warning")
}
if cfg.Domains[2].StoreCode != "" {
t.Errorf("a domain without either key must stay empty: %+v", cfg.Domains[2])
}
}
74 changes: 59 additions & 15 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type Config struct {
ComposeFile string `yaml:"compose_file,omitempty"` // Path to project-specific docker-compose.yml
Sandbox *SandboxConfig `yaml:"sandbox,omitempty"`
IncludeConfig []string `yaml:"include_config,omitempty"` // Paths to additional config files or directories to merge

// DeprecationWarnings are filled by the loader for settings that still work
// under an old name. Not part of the file itself.
DeprecationWarnings []string `yaml:"-"`
}

// GetType returns the project type, defaulting to "magento"
Expand Down Expand Up @@ -174,11 +178,18 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error {

// Domain represents a domain configuration
type Domain struct {
Host string `yaml:"host"`
Root string `yaml:"root,omitempty"`
SSL *bool `yaml:"ssl,omitempty"`
MageRunCode string `yaml:"mage_run_code,omitempty"` // Magento store/website code for multi-store setup
MageRunType string `yaml:"mage_run_type,omitempty"` // "store" or "website" (default: "store")
Host string `yaml:"host"`
Root string `yaml:"root,omitempty"`
SSL *bool `yaml:"ssl,omitempty"`
StoreCode string `yaml:"store_code,omitempty"` // Magento store/website code for multi-store setup
StoreType string `yaml:"store_type,omitempty"` // "store" or "website" (default: "store")

// MageRunCode and MageRunType are the names these settings had before 2.3.0.
// They are still read, because the loader ignores unknown keys and a project
// written for an older release would otherwise lose its store code without
// any warning and serve the wrong store.
MageRunCode string `yaml:"mage_run_code,omitempty"`
MageRunType string `yaml:"mage_run_type,omitempty"`
}

// Services represents the services configuration
Expand Down Expand Up @@ -321,20 +332,22 @@ func (d *Domain) IsSSLEnabled() bool {
return *d.SSL
}

// GetStoreCode returns the Magento store code, defaulting to "default"
func (d *Domain) GetStoreCode() string {
if d.MageRunCode == "" {
return "default"
// GetStoreType returns the Magento run type, defaulting to "store"
func (d *Domain) GetStoreType() string {
if d.StoreType == "" {
return "store"
}
return d.MageRunCode
return d.StoreType
}

// GetMageRunType returns the Magento run type, defaulting to "store"
func (d *Domain) GetMageRunType() string {
if d.MageRunType == "" {
return "store"
// HasMultiStore returns true if any domain has a store code configured
func (c *Config) HasMultiStore() bool {
for _, d := range c.Domains {
if d.StoreCode != "" {
return true
}
}
return d.MageRunType
return false
}

// Validate checks if the configuration is valid
Expand All @@ -353,6 +366,15 @@ func (c *Config) Validate() error {
if c.PHP == "" {
return &ValidationError{Field: "php", Message: "php version is required"}
}
for i, d := range c.Domains {
if !validStoreTypes[d.StoreType] {
return &ValidationError{
Field: "domains",
Message: fmt.Sprintf("store_type %q is not valid, expected \"store\" or \"website\"", d.StoreType),
Index: i,
}
}
}
return nil
}

Expand Down Expand Up @@ -468,3 +490,25 @@ func (s *Services) GetSearchService() *ServiceConfig {
}
return nil
}

// validStoreTypes are the values Magento understands for MAGE_RUN_TYPE.
var validStoreTypes = map[string]bool{"": true, "store": true, "website": true}

// MigrateDeprecatedKeys copies settings from the names they had before 2.3.0 and
// returns a warning for each one found, so a project keeps working while its
// owner is told what to rename.
func (c *Config) MigrateDeprecatedKeys() []string {
var warnings []string
for i := range c.Domains {
domain := &c.Domains[i]
if domain.MageRunCode != "" && domain.StoreCode == "" {
domain.StoreCode = domain.MageRunCode
warnings = append(warnings, fmt.Sprintf("domain %s: 'mage_run_code' is deprecated, rename it to 'store_code'", domain.Host))
}
if domain.MageRunType != "" && domain.StoreType == "" {
domain.StoreType = domain.MageRunType
warnings = append(warnings, fmt.Sprintf("domain %s: 'mage_run_type' is deprecated, rename it to 'store_type'", domain.Host))
}
}
return warnings
}
1 change: 1 addition & 0 deletions internal/lib/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ var TemplateNames = map[string][]string{
"vhost.conf.tmpl",
"proxy.conf.tmpl",
"upstream.conf.tmpl",
"map.conf.tmpl",
},
TemplatePHP: {
"pool.conf.tmpl",
Expand Down
Loading
Loading