Skip to content
Draft
21 changes: 21 additions & 0 deletions cmd/project/project_clear_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,27 @@ var projectClearCacheCmd = &cobra.Command{
return err
}

// with a selected environment the executor decides how and where
// cache:clear runs (local, docker, ssh, ...)
if environmentName != "" {
// the project root is only needed by local executors, remote
// environments work from anywhere
projectRoot, _ := findClosestShopwareProject()

cmdExecutor, err := resolveExecutor(cmd, projectRoot)
if err != nil {
return err
}

logging.FromContext(cmd.Context()).Infof("Clearing cache on environment %s", environmentName)

p := cmdExecutor.ConsoleCommand(cmd.Context(), "cache:clear")
p.Cmd.Stdout = cmd.OutOrStdout()
p.Cmd.Stderr = cmd.ErrOrStderr()

return p.Run()
}

if cfg.AdminApi == nil {
logging.FromContext(cmd.Context()).Infof("Clearing cache localy")

Expand Down
62 changes: 60 additions & 2 deletions cmd/project/project_console.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package project

import (
"context"
"fmt"
"os/exec"
"slices"
"strings"

"github.com/spf13/cobra"

Expand All @@ -16,12 +18,63 @@ var (
appCommands = []string{"app:install", "app:update", "app:activate", "app:deactivate"}
)

// stripConsoleFlags extracts shopware-cli's own flags (--env/-e and
// --project-config) from the arguments before the first positional argument
// and applies them. Everything from the first positional argument on — the
// console command name — is passed to bin/console untouched, so Symfony's own
// --env/-e flag keeps working there. A leading "--" ends flag handling early.
func stripConsoleFlags(args []string) []string {
rest := make([]string, 0, len(args))

for i := 0; i < len(args); i++ {
arg := args[i]

if arg == "--" {
return append(rest, args[i+1:]...)
}

if !strings.HasPrefix(arg, "-") {
return append(rest, args[i:]...)
}

switch {
case arg == "--env" || arg == "-e":
if i+1 < len(args) {
environmentName = args[i+1]
i++
}
case strings.HasPrefix(arg, "--env="):
environmentName = strings.TrimPrefix(arg, "--env=")
case strings.HasPrefix(arg, "-e="):
environmentName = strings.TrimPrefix(arg, "-e=")
case arg == "--project-config":
if i+1 < len(args) {
projectConfigPath = args[i+1]
i++
}
case strings.HasPrefix(arg, "--project-config="):
projectConfigPath = strings.TrimPrefix(arg, "--project-config=")
default:
rest = append(rest, arg)
}
}

return rest
}

var projectConsoleCmd = &cobra.Command{
Use: "console",
Short: "Runs the Symfony Console (bin/console) for current project",
Use: "console [flags] -- <command> [console-args]",
Short: "Runs the Symfony Console (bin/console) for current project",
Long: `Runs the Symfony Console (bin/console) for the current project.

Flags for shopware-cli (--env/-e to pick an environment, --project-config) must
be placed before the console command name; everything after it is passed to
bin/console unchanged, including Symfony's own --env flag.`,
Args: cobra.MinimumNArgs(1),
DisableFlagParsing: true,
ValidArgsFunction: func(cmd *cobra.Command, input []string, _ string) ([]string, cobra.ShellCompDirective) {
input = stripConsoleFlags(input)

projectRoot, err := findClosestShopwareProject()
if err != nil {
return nil, cobra.ShellCompDirectiveDefault
Expand Down Expand Up @@ -82,6 +135,11 @@ var projectConsoleCmd = &cobra.Command{
return completions, cobra.ShellCompDirectiveDefault
},
RunE: func(cmd *cobra.Command, args []string) error {
args = stripConsoleFlags(args)
if len(args) == 0 {
return fmt.Errorf("no console command given")
}

projectRoot, err := findClosestShopwareProject()
if err != nil {
return err
Expand Down
73 changes: 73 additions & 0 deletions cmd/project/project_console_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package project

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestStripConsoleFlags(t *testing.T) {
cases := []struct {
name string
args []string
expected []string
expectedEnv string
}{
{
name: "no flags",
args: []string{"cache:clear"},
expected: []string{"cache:clear"},
},
{
name: "env before command",
args: []string{"--env", "production", "cache:clear"},
expected: []string{"cache:clear"},
expectedEnv: "production",
},
{
name: "env equals form",
args: []string{"--env=production", "cache:clear"},
expected: []string{"cache:clear"},
expectedEnv: "production",
},
{
name: "shorthand",
args: []string{"-e", "production", "cache:clear"},
expected: []string{"cache:clear"},
expectedEnv: "production",
},
{
name: "symfony env after command stays untouched",
args: []string{"cache:clear", "--env=prod"},
expected: []string{"cache:clear", "--env=prod"},
},
{
name: "cli env before, symfony env after",
args: []string{"--env", "production", "cache:clear", "--env=prod", "-e", "dev"},
expected: []string{"cache:clear", "--env=prod", "-e", "dev"},
expectedEnv: "production",
},
{
name: "double dash passes everything through",
args: []string{"--", "--env=prod", "cache:clear"},
expected: []string{"--env=prod", "cache:clear"},
},
{
name: "unknown flags before command are forwarded",
args: []string{"-v", "cache:clear"},
expected: []string{"-v", "cache:clear"},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
environmentName = ""
t.Cleanup(func() { environmentName = "" })

rest := stripConsoleFlags(tc.args)

assert.Equal(t, tc.expected, rest)
assert.Equal(t, tc.expectedEnv, environmentName)
})
}
}
66 changes: 66 additions & 0 deletions cmd/project/project_deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package project

import (
"fmt"

"github.com/spf13/cobra"

"github.com/shopware/shopware-cli/internal/deployment"
"github.com/shopware/shopware-cli/internal/shop"
)

var projectDeployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy the project to a remote environment",
Long: `Deploys the project to the environment selected with --env.

The project is uploaded as a new release into <deployment.path>/releases, shared
files and directories are linked from <deployment.path>/shared and the
<deployment.path>/current symlink is switched atomically to the new release.
Use "shopware-cli project deploy rollback" to switch back to a previous release.`,
RunE: func(cmd *cobra.Command, _ []string) error {
skipBuildHooks, _ := cmd.Flags().GetBool("skip-build-hooks")

deployer, cleanup, err := resolveDeployer(cmd)
if err != nil {
return err
}
defer cleanup()

return deployer.Deploy(cmd.Context(), deployment.Options{SkipBuildHooks: skipBuildHooks})
},
}

// resolveDeployer creates the Deployer for the selected environment.
func resolveDeployer(cmd *cobra.Command) (deployment.Deployer, func(), error) {
projectRoot, err := findClosestShopwareProject()
if err != nil {
return nil, nil, err
}

cfg, err := shop.ReadConfig(cmd.Context(), projectConfigPath, false)
if err != nil {
return nil, nil, err
}

if environmentName == "" {
return nil, nil, fmt.Errorf("no environment selected, use --env to pick one of the environments defined in %s", projectConfigPath)
}

envCfg, err := cfg.ResolveEnvironment(environmentName)
if err != nil {
return nil, nil, err
}

deployer, err := deployment.NewDeployer(projectRoot, envCfg, cfg)
if err != nil {
return nil, nil, err
}

return deployer, func() { _ = deployer.Close() }, nil
}

func init() {
projectDeployCmd.Flags().Bool("skip-build-hooks", false, "Skip the local build hooks before the upload")
projectRootCmd.AddCommand(projectDeployCmd)
}
57 changes: 57 additions & 0 deletions cmd/project/project_deploy_releases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package project

import (
"fmt"

"github.com/spf13/cobra"
)

var projectDeployReleasesCmd = &cobra.Command{
Use: "releases",
Short: "List the releases on the deployment target",
RunE: func(cmd *cobra.Command, _ []string) error {
deployer, cleanup, err := resolveDeployer(cmd)
if err != nil {
return err
}
defer cleanup()

hostReleases, err := deployer.Releases(cmd.Context())
if err != nil {
return err
}

for i, hr := range hostReleases {
if len(hostReleases) > 1 {
if i > 0 {
_, _ = fmt.Fprintln(cmd.OutOrStdout())
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Host %s:\n", hr.Host)
}

if len(hr.Releases) == 0 {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No releases found")
continue
}

for _, release := range hr.Releases {
marker := ""
if release.Active {
marker = " (active)"
}
if release.Bad {
marker += " (bad)"
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s%s\n", release.Name, marker)
}
}

return nil
},
}

func init() {
projectDeployCmd.AddCommand(projectDeployReleasesCmd)
}
33 changes: 33 additions & 0 deletions cmd/project/project_deploy_rollback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package project

import (
"github.com/spf13/cobra"
)

var projectDeployRollbackCmd = &cobra.Command{
Use: "rollback [release]",
Short: "Roll back to a previous release",
Long: `Switches the current symlink back to a previous release.

Without an argument the release deployed before the currently active one is used.
The release that was rolled back from is marked as bad and skipped by later rollbacks.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var release string
if len(args) > 0 {
release = args[0]
}

deployer, cleanup, err := resolveDeployer(cmd)
if err != nil {
return err
}
defer cleanup()

return deployer.Rollback(cmd.Context(), release)
},
}

func init() {
projectDeployCmd.AddCommand(projectDeployRollbackCmd)
}
Loading
Loading