From 491e6ba96ee8b06a4da7f61724950bc515f9a9f7 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Fri, 9 Jan 2026 10:08:41 +0100 Subject: [PATCH 01/12] feat: start with project creator --- cmd/project/project_create.go | 473 ++++++++++++++---- cmd/project/static/deploy.php | 98 ++++ cmd/project/static/github-ci.yml | 32 ++ cmd/project/static/github-deploy.yml | 29 ++ cmd/project/static/gitlab-ci.yml.tmpl | 56 +++ cmd/root.go | 3 +- go.mod | 2 +- internal/git/git.go | 5 + internal/packagist/project_composer_json.go | 274 ++++++---- .../packagist/project_composer_json_test.go | 67 ++- internal/system/symfony.go | 8 + internal/validation/reporter.go | 2 +- 12 files changed, 841 insertions(+), 208 deletions(-) create mode 100644 cmd/project/static/deploy.php create mode 100644 cmd/project/static/github-ci.yml create mode 100644 cmd/project/static/github-deploy.yml create mode 100644 cmd/project/static/gitlab-ci.yml.tmpl create mode 100644 internal/system/symfony.go diff --git a/cmd/project/project_create.go b/cmd/project/project_create.go index c970d87c..4d1e8e48 100644 --- a/cmd/project/project_create.go +++ b/cmd/project/project_create.go @@ -1,7 +1,9 @@ package project import ( + "bytes" "context" + _ "embed" "encoding/json" "fmt" "io" @@ -11,124 +13,273 @@ import ( "path/filepath" "sort" "strings" + "text/template" "github.com/charmbracelet/huh" "github.com/shyim/go-version" "github.com/spf13/cobra" + "github.com/shopware/shopware-cli/internal/git" "github.com/shopware/shopware-cli/internal/packagist" "github.com/shopware/shopware-cli/internal/system" "github.com/shopware/shopware-cli/logging" ) +//go:embed static/deploy.php +var deployerTemplate string + +//go:embed static/github-ci.yml +var githubCITemplate string + +//go:embed static/github-deploy.yml +var githubDeployTemplate string + +//go:embed static/gitlab-ci.yml.tmpl +var gitlabCITemplate string + +const versionLatest = "latest" + var projectCreateCmd = &cobra.Command{ Use: "create [name] [version]", Short: "Create a new Shopware 6 project", - Args: cobra.MinimumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + Args: cobra.MaximumNArgs(2), + ValidArgsFunction: func(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return []string{}, cobra.ShellCompDirectiveFilterDirs + } + if len(args) == 1 { filteredVersions, err := getFilteredInstallVersions(cmd.Context()) if err != nil { return []string{}, cobra.ShellCompDirectiveNoFileComp } - versions := make([]string, 0) - - for i, v := range filteredVersions { - versions[i] = v.String() + versions := make([]string, 0, len(filteredVersions)+1) + versions = append(versions, versionLatest) + for _, v := range filteredVersions { + versions = append(versions, v.String()) } - - versions = append(versions, "latest") - return versions, cobra.ShellCompDirectiveNoFileComp } - return []string{}, cobra.ShellCompDirectiveFilterDirs + return []string{}, cobra.ShellCompDirectiveNoFileComp }, RunE: func(cmd *cobra.Command, args []string) error { - projectFolder := args[0] - useDocker, _ := cmd.PersistentFlags().GetBool("docker") + withElasticsearch, _ := cmd.PersistentFlags().GetBool("with-elasticsearch") withoutElasticsearch, _ := cmd.PersistentFlags().GetBool("without-elasticsearch") noAudit, _ := cmd.PersistentFlags().GetBool("no-audit") + initGit, _ := cmd.PersistentFlags().GetBool("git") + versionFlag, _ := cmd.PersistentFlags().GetString("version") + deploymentMethod, _ := cmd.PersistentFlags().GetString("deployment") + ciSystem, _ := cmd.PersistentFlags().GetString("ci") + + if cmd.PersistentFlags().Changed("without-elasticsearch") { + logging.FromContext(cmd.Context()).Warnf("Flag --without-elasticsearch is deprecated, use --with-elasticsearch instead") + withElasticsearch = !withoutElasticsearch + } - if _, err := os.Stat(projectFolder); err == nil { - empty, err := system.IsDirEmpty(projectFolder) - if err != nil { - return err - } + interactive := system.IsInteractionEnabled(cmd.Context()) - if !empty { - return fmt.Errorf("the folder %s exists already and is not empty", projectFolder) - } - } + const ( + optionDocker = "docker" + optionGit = "git" + optionElasticsearch = "elasticsearch" - logging.FromContext(cmd.Context()).Infof("Using Symfony Flex to create a new Shopware 6 project") + ciNone = "none" + ciGitHub = "github" + ciGitLab = "gitlab" + ) filteredVersions, err := getFilteredInstallVersions(cmd.Context()) if err != nil { return err } - var result string + versionOptions := make([]huh.Option[string], 0, len(filteredVersions)+1) + versionOptions = append(versionOptions, huh.NewOption(versionLatest, versionLatest)) + for _, v := range filteredVersions { + versionStr := v.String() + versionOptions = append(versionOptions, huh.NewOption(versionStr, versionStr)) + } + + deploymentOptions := []huh.Option[string]{ + huh.NewOption("None", packagist.DeploymentNone), + huh.NewOption("DeployerPHP", packagist.DeploymentDeployer), + huh.NewOption("PaaS powered by Platform.sh", packagist.DeploymentPlatformSH), + huh.NewOption("PaaS powered by Shopware", packagist.DeploymentShopwarePaaS), + } + + ciOptions := []huh.Option[string]{ + huh.NewOption("None", ciNone), + huh.NewOption("GitHub Actions", ciGitHub), + huh.NewOption("GitLab CI", ciGitLab), + } + + var projectFolder string + selectedVersion := versionFlag + selectedDeployment := deploymentMethod + selectedCI := ciSystem + var selectedOptions []string - if len(args) == 2 { - result = args[1] - } else if !system.IsInteractionEnabled(cmd.Context()) { - result = "latest" + if len(args) > 0 { + projectFolder = args[0] + } + + if len(args) > 1 && selectedVersion == "" { + selectedVersion = args[1] + } + + if !interactive { + if projectFolder == "" { + return fmt.Errorf("project name is required in non-interactive mode") + } + if selectedVersion == "" { + selectedVersion = versionLatest + } + if selectedDeployment == "" { + selectedDeployment = packagist.DeploymentNone + } + if selectedCI == "" { + selectedCI = ciNone + } } else { - options := make([]huh.Option[string], 0) - for _, v := range filteredVersions { - versionStr := v.String() - options = append(options, huh.NewOption(versionStr, versionStr)) + var formFields []huh.Field + + if projectFolder == "" { + formFields = append(formFields, + huh.NewInput(). + Title("Project Name"). + Description("The name of the project directory to create"). + Placeholder("my-shopware-project"). + Value(&projectFolder). + Validate(func(s string) error { + if s == "" { + return fmt.Errorf("project name is required") + } + return nil + }), + ) } - // Add "latest" option - options = append(options, huh.NewOption("latest", "latest")) - - // Create and run the select form - form := huh.NewForm( - huh.NewGroup( + if selectedVersion == "" { + selectedVersion = versionLatest + formFields = append(formFields, huh.NewSelect[string](). + Title("Shopware Version"). + Description("Select the Shopware version to install"). + Options(versionOptions...). Height(10). - Title("Select Version"). - Options(options...). - Value(&result), - ), - ) + Value(&selectedVersion), + ) + } - if err := form.Run(); err != nil { - return err + if selectedDeployment == "" { + selectedDeployment = packagist.DeploymentNone + formFields = append(formFields, + huh.NewSelect[string](). + Title("Deployment Method"). + Description("Select how you want to deploy your project"). + Options(deploymentOptions...). + Value(&selectedDeployment), + ) } - } - chooseVersion := "" + if selectedCI == "" { + selectedCI = ciNone + formFields = append(formFields, + huh.NewSelect[string](). + Title("CI/CD System"). + Description("Select your CI/CD platform for automated testing and deployment"). + Options(ciOptions...). + Value(&selectedCI), + ) + } - if result == "latest" { - // pick the most recent stable (non-RC) version - for _, v := range filteredVersions { - vs := v.String() - if !strings.Contains(strings.ToLower(vs), "rc") { - chooseVersion = vs - break - } + var optionalOptions []huh.Option[string] + if !cmd.PersistentFlags().Changed("git") { + optionalOptions = append(optionalOptions, huh.NewOption("Initialize Git Repository", optionGit)) } - // if no stable found, fall back to top entry - if chooseVersion == "" && len(filteredVersions) > 0 { - chooseVersion = filteredVersions[0].String() + if !cmd.PersistentFlags().Changed("docker") { + optionalOptions = append(optionalOptions, huh.NewOption("Local Docker Setup", optionDocker)) } - } else if strings.HasPrefix(result, "dev-") { - chooseVersion = result - } else { - for _, release := range filteredVersions { - if release.String() == result { - chooseVersion = release.String() - break + if !cmd.PersistentFlags().Changed("with-elasticsearch") { + optionalOptions = append(optionalOptions, huh.NewOption("Setup Elasticsearch/OpenSearch support", optionElasticsearch)) + } + + if len(optionalOptions) > 0 { + formFields = append(formFields, + huh.NewMultiSelect[string](). + Title("Optional"). + Description("Select additional features to enable"). + Options(optionalOptions...). + Value(&selectedOptions), + ) + } + + if len(formFields) > 0 { + form := huh.NewForm(huh.NewGroup(formFields...)) + if err := form.Run(); err != nil { + return err + } + } + + for _, opt := range selectedOptions { + switch opt { + case optionDocker: + useDocker = true + case optionGit: + initGit = true + case optionElasticsearch: + withElasticsearch = true } } } + if !useDocker { + phpOk, err := system.IsPHPVersionAtLeast(cmd.Context(), "8.2") + if err != nil { + return fmt.Errorf("PHP 8.2 or higher is required: %w", err) + } + if !phpOk { + return fmt.Errorf("PHP 8.2 or higher is required for Shopware 6") + } + } + + validDeployments := map[string]bool{ + packagist.DeploymentNone: true, + packagist.DeploymentDeployer: true, + packagist.DeploymentPlatformSH: true, + packagist.DeploymentShopwarePaaS: true, + } + if !validDeployments[selectedDeployment] { + return fmt.Errorf("invalid deployment method: %s. Valid options: none, deployer, platformsh, shopware-paas", selectedDeployment) + } + + validCISystems := map[string]bool{ + ciNone: true, + ciGitHub: true, + ciGitLab: true, + } + if !validCISystems[selectedCI] { + return fmt.Errorf("invalid CI system: %s. Valid options: none, github, gitlab", selectedCI) + } + + if _, err := os.Stat(projectFolder); err == nil { + empty, err := system.IsDirEmpty(projectFolder) + if err != nil { + return err + } + + if !empty { + return fmt.Errorf("the folder %s exists already and is not empty", projectFolder) + } + } + + logging.FromContext(cmd.Context()).Infof("Using Symfony Flex to create a new Shopware 6 project") + + chooseVersion := resolveVersion(selectedVersion, filteredVersions) if chooseVersion == "" { - return fmt.Errorf("cannot find version %s", result) + return fmt.Errorf("cannot find version %s", selectedVersion) } if err := os.MkdirAll(projectFolder, os.ModePerm); err != nil { @@ -137,7 +288,14 @@ var projectCreateCmd = &cobra.Command{ logging.FromContext(cmd.Context()).Infof("Setting up Shopware %s", chooseVersion) - composerJson, err := packagist.GenerateComposerJson(cmd.Context(), chooseVersion, strings.Contains(chooseVersion, "rc"), useDocker, withoutElasticsearch, noAudit) + composerJson, err := packagist.GenerateComposerJson(cmd.Context(), packagist.ComposerJsonOptions{ + Version: chooseVersion, + RC: strings.Contains(chooseVersion, "rc"), + UseDocker: useDocker, + UseElasticsearch: withElasticsearch, + NoAudit: noAudit, + DeploymentMethod: selectedDeployment, + }) if err != nil { return err } @@ -166,55 +324,167 @@ var projectCreateCmd = &cobra.Command{ return err } - if err := os.WriteFile(filepath.Join(projectFolder, "php.ini"), []byte("memory_limit=512M"), os.ModePerm); err != nil { + if !useDocker && system.IsSymfonyCliInstalled() { + if err := os.WriteFile(filepath.Join(projectFolder, "php.ini"), []byte("memory_limit=512M"), os.ModePerm); err != nil { + return err + } + } + + if err := setupDeployment(projectFolder, selectedDeployment); err != nil { + return err + } + + if err := setupCI(projectFolder, selectedCI, selectedDeployment); err != nil { return err } logging.FromContext(cmd.Context()).Infof("Installing dependencies") - var cmdInstall *exec.Cmd + if err := runComposerInstall(cmd.Context(), projectFolder, useDocker); err != nil { + return err + } - if useDocker { - // Use Docker to run composer - absProjectFolder, err := filepath.Abs(projectFolder) - if err != nil { - return err + if initGit { + logging.FromContext(cmd.Context()).Infof("Initializing Git repository") + if err := git.Init(cmd.Context(), projectFolder); err != nil { + return fmt.Errorf("failed to initialize git repository: %w", err) } + } - dockerArgs := []string{"run", "--rm", - "-v", fmt.Sprintf("%s:/app", absProjectFolder), - "-w", "/app", - "ghcr.io/shopware/docker-dev:php8.3-node22-caddy", - "composer", "install", "--no-interaction"} + logging.FromContext(cmd.Context()).Infof("Project created successfully in %s", projectFolder) - cmdInstall = exec.CommandContext(cmd.Context(), "docker", dockerArgs...) - cmdInstall.Stdout = os.Stdout - cmdInstall.Stderr = os.Stderr + return nil + }, +} - return cmdInstall.Run() - } else { - // Use local composer - composerBinary, err := exec.LookPath("composer") - if err != nil { - return err +func resolveVersion(selectedVersion string, filteredVersions []*version.Version) string { + if selectedVersion == versionLatest { + // pick the most recent stable (non-RC) version + for _, v := range filteredVersions { + vs := v.String() + if !strings.Contains(strings.ToLower(vs), "rc") { + return vs } + } + // if no stable found, fall back to top entry + if len(filteredVersions) > 0 { + return filteredVersions[0].String() + } + return "" + } + + if strings.HasPrefix(selectedVersion, "dev-") { + return selectedVersion + } + + for _, release := range filteredVersions { + if release.String() == selectedVersion { + return release.String() + } + } + + return "" +} + +func setupDeployment(projectFolder, deploymentMethod string) error { + switch deploymentMethod { + case packagist.DeploymentDeployer: + if err := os.WriteFile(filepath.Join(projectFolder, "deploy.php"), []byte(deployerTemplate), os.ModePerm); err != nil { + return err + } + + case packagist.DeploymentShopwarePaaS: + shopwarePaasApp := `app: + php: + version: "8.4" +services: + mysql: + version: "8.0" +` + + if err := os.WriteFile(filepath.Join(projectFolder, "application.yaml"), []byte(shopwarePaasApp), os.ModePerm); err != nil { + return err + } + } - phpBinary := os.Getenv("PHP_BINARY") + return nil +} - if phpBinary != "" { - cmdInstall = exec.CommandContext(cmd.Context(), phpBinary, composerBinary, "install", "--no-interaction") - } else { - cmdInstall = exec.CommandContext(cmd.Context(), "composer", "install", "--no-interaction") +func setupCI(projectFolder, ciSystem, deploymentMethod string) error { + switch ciSystem { + case "github": + if err := os.MkdirAll(filepath.Join(projectFolder, ".github", "workflows"), os.ModePerm); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(projectFolder, ".github", "workflows", "ci.yml"), []byte(githubCITemplate), os.ModePerm); err != nil { + return err + } + if deploymentMethod == packagist.DeploymentDeployer { + if err := os.WriteFile(filepath.Join(projectFolder, ".github", "workflows", "deploy.yml"), []byte(githubDeployTemplate), os.ModePerm); err != nil { + return err } + } - cmdInstall.Dir = projectFolder - cmdInstall.Stdin = os.Stdin - cmdInstall.Stdout = os.Stdout - cmdInstall.Stderr = os.Stderr + case "gitlab": + tmpl, err := template.New("gitlab-ci").Parse(gitlabCITemplate) + if err != nil { + return err + } - return cmdInstall.Run() + var buf bytes.Buffer + if err := tmpl.Execute(&buf, struct{ Deployer bool }{Deployer: deploymentMethod == packagist.DeploymentDeployer}); err != nil { + return err } - }, + + if err := os.WriteFile(filepath.Join(projectFolder, ".gitlab-ci.yml"), buf.Bytes(), os.ModePerm); err != nil { + return err + } + } + + return nil +} + +func runComposerInstall(ctx context.Context, projectFolder string, useDocker bool) error { + var cmdInstall *exec.Cmd + + if useDocker { + absProjectFolder, err := filepath.Abs(projectFolder) + if err != nil { + return err + } + + dockerArgs := []string{"run", "--rm", + "-v", fmt.Sprintf("%s:/app", absProjectFolder), + "-w", "/app", + "ghcr.io/shopware/docker-dev:php8.3-node22-caddy", + "composer", "install", "--no-interaction"} + + cmdInstall = exec.CommandContext(ctx, "docker", dockerArgs...) + cmdInstall.Stdout = os.Stdout + cmdInstall.Stderr = os.Stderr + + return cmdInstall.Run() + } + + composerBinary, err := exec.LookPath("composer") + if err != nil { + return err + } + + phpBinary := os.Getenv("PHP_BINARY") + + if phpBinary != "" { + cmdInstall = exec.CommandContext(ctx, phpBinary, composerBinary, "install", "--no-interaction") + } else { + cmdInstall = exec.CommandContext(ctx, "composer", "install", "--no-interaction") + } + + cmdInstall.Dir = projectFolder + cmdInstall.Stdin = os.Stdin + cmdInstall.Stdout = os.Stdout + cmdInstall.Stderr = os.Stderr + + return cmdInstall.Run() } func getFilteredInstallVersions(ctx context.Context) ([]*version.Version, error) { @@ -242,8 +512,13 @@ func getFilteredInstallVersions(ctx context.Context) ([]*version.Version, error) func init() { projectRootCmd.AddCommand(projectCreateCmd) projectCreateCmd.PersistentFlags().Bool("docker", false, "Use Docker to run Composer instead of local installation") - projectCreateCmd.PersistentFlags().Bool("without-elasticsearch", false, "Remove Elasticsearch from the installation") + projectCreateCmd.PersistentFlags().Bool("with-elasticsearch", false, "Include Elasticsearch/OpenSearch support") + projectCreateCmd.PersistentFlags().Bool("without-elasticsearch", false, "Remove Elasticsearch from the installation (deprecated: use --with-elasticsearch)") projectCreateCmd.PersistentFlags().Bool("no-audit", false, "Disable composer audit blocking insecure packages") + projectCreateCmd.PersistentFlags().Bool("git", false, "Initialize a Git repository") + projectCreateCmd.PersistentFlags().String("version", "", "Shopware version to install (e.g., 6.6.0.0, latest)") + projectCreateCmd.PersistentFlags().String("deployment", "", "Deployment method: none, deployer, platformsh, shopware-paas") + projectCreateCmd.PersistentFlags().String("ci", "", "CI/CD system: none, github, gitlab") } func fetchAvailableShopwareVersions(ctx context.Context) ([]string, error) { diff --git a/cmd/project/static/deploy.php b/cmd/project/static/deploy.php new file mode 100644 index 00000000..3ee8cdfc --- /dev/null +++ b/cmd/project/static/deploy.php @@ -0,0 +1,98 @@ +setLabels([ + 'type' => 'web', + 'env' => 'production', + ]) + ->setRemoteUser('www-data') + ->set('deploy_path', '/var/www/shopware') + ->set('http_user', 'www-data') // Not needed, if the `user` is the same, the webserver is running with + ->set('writable_mode', 'chmod') + ->set('keep_releases', 3); // Keeps 3 old releases for rollbacks (if no DB migrations were executed) + +// These files are shared among all releases. +set('shared_files', [ + '.env.local', + 'install.lock', + 'public/.htaccess', + 'public/.user.ini', +]); + +// These directories are shared among all releases. +set('shared_dirs', [ + 'config/jwt', + 'files', + 'var/log', + 'public/media', + 'public/plugins', + 'public/thumbnail', + 'public/sitemap', +]); + +// These directories are made writable (the definition of "writable" requires attention). +// Please note that the files in `config/jwt/*` receive special attention in the `sw:writable:jwt` task. +set('writable_dirs', [ + 'config/jwt', + 'custom/plugins', + 'files', + 'public/bundles', + 'public/css', + 'public/fonts', + 'public/js', + 'public/media', + 'public/sitemap', + 'public/theme', + 'public/thumbnail', + 'var', +]); + +task('sw:deployment:helper', static function() { + run('cd {{release_path}} && vendor/bin/shopware-deployment-helper run'); +}); + +task('sw:touch_install_lock', static function () { + run('cd {{release_path}} && touch install.lock'); +}); + +task('sw:health_checks', static function () { + run('cd {{release_path}} && bin/console system:check --context=pre_rollout'); +}); + +desc('Deploys your project'); +task('deploy', [ + 'deploy:prepare', + 'deploy:clear_paths', + 'sw:deployment:helper', + "sw:touch_install_lock", + 'sw:health_checks', + 'deploy:publish', +]); + +task('deploy:update_code')->setCallback(static function () { + upload('.', '{{release_path}}', [ + 'options' => [ + '--exclude=.git', + '--exclude=deploy.php', + '--exclude=node_modules', + ], + ]); +}); + +// Hooks +after('deploy:failed', 'deploy:unlock'); +after('deploy:symlink', 'cachetool:clear:opcache'); diff --git a/cmd/project/static/github-ci.yml b/cmd/project/static/github-ci.yml new file mode 100644 index 00000000..e89d6001 --- /dev/null +++ b/cmd/project/static/github-ci.yml @@ -0,0 +1,32 @@ +name: Check + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + + - name: Install Shopware CLI + uses: shopware/shopware-cli-action@v3 + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Validate Project + run: shopware-cli project validate . + + - name: Check Code Style + run: shopware-cli project format --dry-run . diff --git a/cmd/project/static/github-deploy.yml b/cmd/project/static/github-deploy.yml new file mode 100644 index 00000000..79deac8c --- /dev/null +++ b/cmd/project/static/github-deploy.yml @@ -0,0 +1,29 @@ +name: Deployment + +on: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + + - name: Install Shopware CLI + uses: shopware/shopware-cli-action@v3 + + - name: Build + run: shopware-cli project ci . + + - name: Deploy + uses: deployphp/action@v1 + with: + dep: deploy + private-key: ${{ secrets.SSH_PRIVATE_KEY }} diff --git a/cmd/project/static/gitlab-ci.yml.tmpl b/cmd/project/static/gitlab-ci.yml.tmpl new file mode 100644 index 00000000..05c2baf6 --- /dev/null +++ b/cmd/project/static/gitlab-ci.yml.tmpl @@ -0,0 +1,56 @@ +stages: + - check +{{- if .Deployer }} + - deploy +{{- end }} + +variables: + COMPOSER_HOME: ${CI_PROJECT_DIR}/.composer +{{- if .Deployer }} + GIT_STRATEGY: clone +{{- end }} + +cache: + paths: + - .composer/ + +validate: + stage: check + image: + name: ghcr.io/shopware/shopware-cli:latest-php-8.4 + entrypoint: ["/bin/sh", "-c"] + before_script: + - composer install + script: + - shopware-cli project validate . > validate.json + - shopware-cli project format --dry-run . + artifacts: + reports: + codequality: validate.json +{{- if .Deployer }} + +.configureSSHAgent: &configureSSHAgent |- + eval $(ssh-agent -s) + echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + mkdir -p ~/.ssh + if ! printf '%s\n' "$DEPLOYMENT_SERVER" | grep -Eq '^[a-zA-Z0-9.-]+$'; then + echo "Invalid DEPLOYMENT_SERVER value: $DEPLOYMENT_SERVER" >&2 + exit 1 + fi + ssh-keyscan -H -- "$DEPLOYMENT_SERVER" >> ~/.ssh/known_hosts + chmod 700 ~/.ssh + +deploy: + stage: deploy + image: + name: ghcr.io/shopware/shopware-cli:latest-php-8.4 + entrypoint: ["/bin/sh", "-c"] + before_script: + - *configureSSHAgent + script: + - shopware-cli project ci . + - vendor/bin/dep deploy + only: + - main + when: manual +{{- end }} diff --git a/cmd/root.go b/cmd/root.go index 01989969..836dc0c8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "os" "slices" + "github.com/mattn/go-isatty" "github.com/spf13/cobra" "github.com/shopware/shopware-cli/cmd/account" @@ -30,7 +31,7 @@ var rootCmd = &cobra.Command{ func Execute(ctx context.Context) { ctx = logging.WithLogger(ctx, logging.NewLogger(slices.Contains(os.Args, "--verbose"))) - ctx = system.WithInteraction(ctx, !slices.Contains(os.Args, "--no-interaction") && !slices.Contains(os.Args, "-n")) + ctx = system.WithInteraction(ctx, !slices.Contains(os.Args, "--no-interaction") && !slices.Contains(os.Args, "-n") && isatty.IsTerminal(os.Stdin.Fd())) accountApi.SetUserAgent("shopware-cli/" + version) if err := rootCmd.ExecuteContext(ctx); err != nil { diff --git a/go.mod b/go.mod index 8221bcc5..59cfc2da 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/invopop/jsonschema v0.13.0 github.com/jaswdr/faker/v2 v2.9.1 github.com/joho/godotenv v1.5.1 + github.com/mattn/go-isatty v0.0.20 github.com/microcosm-cc/bluemonday v1.0.27 github.com/otiai10/copy v1.14.1 github.com/shyim/go-version v0.0.0-20250828113848-97ec77491b32 @@ -72,7 +73,6 @@ require ( github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect diff --git a/internal/git/git.go b/internal/git/git.go index 56c45053..d6e96883 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -195,3 +195,8 @@ func unshallowRepository(ctx context.Context, repo string) error { return err } + +func Init(ctx context.Context, repo string) error { + _, err := runGit(ctx, repo, "init") + return err +} diff --git a/internal/packagist/project_composer_json.go b/internal/packagist/project_composer_json.go index acf8639c..2e2632a4 100644 --- a/internal/packagist/project_composer_json.go +++ b/internal/packagist/project_composer_json.go @@ -1,135 +1,203 @@ package packagist import ( - "bytes" "context" + "encoding/json" "fmt" "io" "net/http" "regexp" "strings" - "text/template" "github.com/shopware/shopware-cli/logging" ) -func GenerateComposerJson(ctx context.Context, version string, rc bool, useDocker bool, withoutElasticsearch bool, noAudit bool) (string, error) { - tplContent, err := template.New("composer.json").Parse(`{ - "name": "shopware/production", - "license": "MIT", - "type": "project", - "require": { - "composer-runtime-api": "^2.0", - "shopware/administration": "{{ .DependingVersions }}", - "shopware/core": "{{ .Version }}", - {{if .UseElasticsearch}} - "shopware/elasticsearch": "{{ .DependingVersions }}", - {{end}} - "shopware/storefront": "{{ .DependingVersions }}", - {{if .UseDocker}} - "shopware/docker-dev": "*", - {{end}} - "symfony/flex": "~2" - }, - "repositories": [ - { - "type": "path", - "url": "custom/plugins/*", - "options": { - "symlink": true - } - }, - { - "type": "path", - "url": "custom/plugins/*/packages/*", - "options": { - "symlink": true - } - }, - { - "type": "path", - "url": "custom/static-plugins/*", - "options": { - "symlink": true - } - } - ], - "autoload": { - "psr-4": { - "App\\": "src/" - } - }, - {{if .RC}} - "minimum-stability": "RC", - {{end}} - "prefer-stable": true, - "config": { - "allow-plugins": { - "symfony/flex": true, - "symfony/runtime": true - }, - "optimize-autoloader": true, - "sort-packages": true{{if .NoAudit}}, - "audit": { - "block-insecure": false - } - {{end}} - }, - "scripts": { - "auto-scripts": [ - ], - "post-install-cmd": [ - "@auto-scripts" - ], - "post-update-cmd": [ - "@auto-scripts" - ] - }, - "extra": { - "symfony": { - "allow-contrib": true, - "endpoint": [ - "https://raw.githubusercontent.com/shopware/recipes/flex/main/index.json", - "flex://defaults" - ] - } - } -}`) - if err != nil { - return "", err - } +const ( + DeploymentNone = "none" + DeploymentDeployer = "deployer" + DeploymentPlatformSH = "platformsh" + DeploymentShopwarePaaS = "shopware-paas" +) + +type ComposerJsonOptions struct { + Version string + DependingVersion string + RC bool + UseDocker bool + UseElasticsearch bool + NoAudit bool + DeploymentMethod string +} - buf := new(bytes.Buffer) +func (o ComposerJsonOptions) IsShopwarePaaS() bool { + return o.DeploymentMethod == DeploymentShopwarePaaS +} - dependingVersions := "*" +func (o ComposerJsonOptions) IsPlatformSH() bool { + return o.DeploymentMethod == DeploymentPlatformSH +} + +func (o ComposerJsonOptions) IsDeployer() bool { + return o.DeploymentMethod == DeploymentDeployer +} - if strings.HasPrefix(version, "dev-") { - fallbackVersion, err := getLatestFallbackVersion(ctx, strings.TrimPrefix(version, "dev-")) +func GenerateComposerJson(ctx context.Context, opts ComposerJsonOptions) (string, error) { + opts.DependingVersion = "*" + + if strings.HasPrefix(opts.Version, "dev-") { + fallbackVersion, err := getLatestFallbackVersion(ctx, strings.TrimPrefix(opts.Version, "dev-")) if err != nil { return "", err } - if strings.HasPrefix(version, "dev-6") { - version = strings.TrimPrefix(version, "dev-") + "-dev" + if strings.HasPrefix(opts.Version, "dev-6") { + opts.Version = strings.TrimPrefix(opts.Version, "dev-") + "-dev" } - version = fmt.Sprintf("%s as %s.9999999-dev", version, fallbackVersion) - dependingVersions = version + opts.Version = fmt.Sprintf("%s as %s.9999999-dev", opts.Version, fallbackVersion) + opts.DependingVersion = opts.Version + } + + require := newOrderedMap() + require.set("composer-runtime-api", "^2.0") + require.set("shopware/administration", opts.DependingVersion) + require.set("shopware/core", opts.Version) + if opts.UseElasticsearch { + require.set("shopware/elasticsearch", opts.DependingVersion) + } + require.set("shopware/storefront", opts.DependingVersion) + if opts.UseDocker { + require.set("shopware/docker-dev", "*") + } + if opts.IsDeployer() { + require.set("deployer/deployer", "*") + } + if opts.IsPlatformSH() { + require.set("shopware/paas-meta", "*") + } + if opts.IsShopwarePaaS() { + require.set("shopware/k8s-meta", "*") + } + require.set("symfony/flex", "~2") + + allowPlugins := newOrderedMap() + allowPlugins.set("symfony/flex", true) + allowPlugins.set("symfony/runtime", true) + + config := newOrderedMap() + config.set("allow-plugins", allowPlugins) + config.set("optimize-autoloader", true) + config.set("sort-packages", true) + if opts.NoAudit { + audit := newOrderedMap() + audit.set("block-insecure", false) + config.set("audit", audit) + } + if opts.IsShopwarePaaS() { + platform := newOrderedMap() + platform.set("ext-grpc", "1.44.0") + platform.set("ext-opentelemetry", "3.21.0") + config.set("platform", platform) } - err = tplContent.Execute(buf, map[string]interface{}{ - "Version": version, - "DependingVersions": dependingVersions, - "RC": rc, - "UseDocker": useDocker, - "UseElasticsearch": !withoutElasticsearch, - "NoAudit": noAudit, + composer := newOrderedMap() + composer.set("name", "shopware/production") + composer.set("license", "MIT") + composer.set("type", "project") + composer.set("require", require) + symlinkOptions := newOrderedMap() + symlinkOptions.set("symlink", true) + + repo1 := newOrderedMap() + repo1.set("type", "path") + repo1.set("url", "custom/plugins/*") + repo1.set("options", symlinkOptions) + + repo2 := newOrderedMap() + repo2.set("type", "path") + repo2.set("url", "custom/plugins/*/packages/*") + repo2.set("options", symlinkOptions) + + repo3 := newOrderedMap() + repo3.set("type", "path") + repo3.set("url", "custom/static-plugins/*") + repo3.set("options", symlinkOptions) + + composer.set("repositories", []*orderedMap{repo1, repo2, repo3}) + psr4 := newOrderedMap() + psr4.set("App\\", "src/") + autoload := newOrderedMap() + autoload.set("psr-4", psr4) + composer.set("autoload", autoload) + if opts.RC { + composer.set("minimum-stability", "RC") + } + composer.set("prefer-stable", true) + composer.set("config", config) + scripts := newOrderedMap() + scripts.set("auto-scripts", []string{}) + scripts.set("post-install-cmd", []string{"@auto-scripts"}) + scripts.set("post-update-cmd", []string{"@auto-scripts"}) + composer.set("scripts", scripts) + symfony := newOrderedMap() + symfony.set("allow-contrib", true) + symfony.set("endpoint", []string{ + "https://raw.githubusercontent.com/shopware/recipes/flex/main/index.json", + "flex://defaults", }) + extra := newOrderedMap() + extra.set("symfony", symfony) + composer.set("extra", extra) + + result, err := json.MarshalIndent(composer, "", " ") if err != nil { return "", err } - return buf.String(), nil + return string(result) + "\n", nil +} + +// orderedMap preserves insertion order for JSON marshaling. +type orderedMap struct { + keys []string + values map[string]any +} + +func newOrderedMap() *orderedMap { + return &orderedMap{ + keys: []string{}, + values: make(map[string]any), + } +} + +func (o *orderedMap) set(key string, value any) { + if _, exists := o.values[key]; !exists { + o.keys = append(o.keys, key) + } + o.values[key] = value +} + +func (o *orderedMap) MarshalJSON() ([]byte, error) { + var buf strings.Builder + buf.WriteString("{") + for i, key := range o.keys { + if i > 0 { + buf.WriteString(",") + } + keyJSON, err := json.Marshal(key) + if err != nil { + return nil, err + } + valueJSON, err := json.Marshal(o.values[key]) + if err != nil { + return nil, err + } + buf.Write(keyJSON) + buf.WriteString(":") + buf.Write(valueJSON) + } + buf.WriteString("}") + return []byte(buf.String()), nil } var kernelFallbackRegExp = regexp.MustCompile(`(?m)SHOPWARE_FALLBACK_VERSION\s*=\s*'?"?(\d+\.\d+)`) diff --git a/internal/packagist/project_composer_json_test.go b/internal/packagist/project_composer_json_test.go index c010c33a..9ec6a679 100644 --- a/internal/packagist/project_composer_json_test.go +++ b/internal/packagist/project_composer_json_test.go @@ -12,7 +12,7 @@ func TestGenerateComposerJson(t *testing.T) { ctx := context.Background() t.Run("without audit", func(t *testing.T) { - jsonStr, err := GenerateComposerJson(ctx, "6.4.18.0", false, false, false, false) + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0"}) assert.NoError(t, err) assert.Contains(t, jsonStr, `"sort-packages": true`) assert.NotContains(t, jsonStr, `"audit": {`) @@ -22,8 +22,8 @@ func TestGenerateComposerJson(t *testing.T) { assert.NoError(t, err, "Generated JSON should be valid") }) - t.Run("with audit", func(t *testing.T) { - jsonStr, err := GenerateComposerJson(ctx, "6.4.18.0", false, false, false, true) + t.Run("with audit disabled", func(t *testing.T) { + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", NoAudit: true}) assert.NoError(t, err) assert.Contains(t, jsonStr, `"sort-packages": true`) assert.Contains(t, jsonStr, `"audit": {`) @@ -32,4 +32,65 @@ func TestGenerateComposerJson(t *testing.T) { err = json.Unmarshal([]byte(jsonStr), &data) assert.NoError(t, err, "Generated JSON should be valid") }) + + t.Run("with elasticsearch", func(t *testing.T) { + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", UseElasticsearch: true}) + assert.NoError(t, err) + assert.Contains(t, jsonStr, `"shopware/elasticsearch"`) + + var data map[string]interface{} + err = json.Unmarshal([]byte(jsonStr), &data) + assert.NoError(t, err, "Generated JSON should be valid") + }) + + t.Run("without elasticsearch", func(t *testing.T) { + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", UseElasticsearch: false}) + assert.NoError(t, err) + assert.NotContains(t, jsonStr, `"shopware/elasticsearch"`) + + var data map[string]interface{} + err = json.Unmarshal([]byte(jsonStr), &data) + assert.NoError(t, err, "Generated JSON should be valid") + }) + + t.Run("with shopware paas deployment", func(t *testing.T) { + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", DeploymentMethod: DeploymentShopwarePaaS}) + assert.NoError(t, err) + assert.Contains(t, jsonStr, `"shopware/k8s-meta": "*"`) + assert.NotContains(t, jsonStr, `"shopware/paas-meta"`) + assert.Contains(t, jsonStr, `"platform": {`) + assert.Contains(t, jsonStr, `"ext-grpc": "1.44.0"`) + assert.Contains(t, jsonStr, `"ext-opentelemetry": "3.21.0"`) + + var data map[string]interface{} + err = json.Unmarshal([]byte(jsonStr), &data) + assert.NoError(t, err, "Generated JSON should be valid") + }) + + t.Run("with platformsh deployment", func(t *testing.T) { + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", DeploymentMethod: DeploymentPlatformSH}) + assert.NoError(t, err) + assert.Contains(t, jsonStr, `"shopware/paas-meta": "*"`) + assert.NotContains(t, jsonStr, `"shopware/k8s-meta"`) + assert.NotContains(t, jsonStr, `"ext-grpc"`) + assert.NotContains(t, jsonStr, `"ext-opentelemetry"`) + + var data map[string]interface{} + err = json.Unmarshal([]byte(jsonStr), &data) + assert.NoError(t, err, "Generated JSON should be valid") + }) + + t.Run("with deployer deployment", func(t *testing.T) { + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", DeploymentMethod: DeploymentDeployer}) + assert.NoError(t, err) + assert.Contains(t, jsonStr, `"deployer/deployer": "*"`) + assert.NotContains(t, jsonStr, `"shopware/paas-meta"`) + assert.NotContains(t, jsonStr, `"shopware/k8s-meta"`) + assert.NotContains(t, jsonStr, `"ext-grpc"`) + assert.NotContains(t, jsonStr, `"ext-opentelemetry"`) + + var data map[string]interface{} + err = json.Unmarshal([]byte(jsonStr), &data) + assert.NoError(t, err, "Generated JSON should be valid") + }) } diff --git a/internal/system/symfony.go b/internal/system/symfony.go new file mode 100644 index 00000000..f8b8a304 --- /dev/null +++ b/internal/system/symfony.go @@ -0,0 +1,8 @@ +package system + +import "os/exec" + +func IsSymfonyCliInstalled() bool { + _, err := exec.LookPath("symfony") + return err == nil +} diff --git a/internal/validation/reporter.go b/internal/validation/reporter.go index 10513388..fc18688f 100644 --- a/internal/validation/reporter.go +++ b/internal/validation/reporter.go @@ -184,7 +184,7 @@ type GitLabCodeQualityLines struct { } func doGitLabReport(result Check) error { - var issues []GitLabCodeQualityIssue + issues := make([]GitLabCodeQualityIssue, 0) // Sort results for deterministic output results := result.GetResults() From ab5e234c6fb1945fedf5ffa391bd0f66c91fb99c Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 28 Jan 2026 08:44:02 +0300 Subject: [PATCH 02/12] feat: use reuseable ci --- cmd/project/static/github-ci.yml | 21 ++-------- cmd/project/static/github-deploy.yml | 19 +-------- cmd/project/static/gitlab-ci.yml.tmpl | 55 ++++----------------------- 3 files changed, 12 insertions(+), 83 deletions(-) diff --git a/cmd/project/static/github-ci.yml b/cmd/project/static/github-ci.yml index e89d6001..12e4d002 100644 --- a/cmd/project/static/github-ci.yml +++ b/cmd/project/static/github-ci.yml @@ -11,22 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 + - name: Project Validate + uses: shopware/github-actions/project-validate@main with: - php-version: '8.4' - - - name: Install Shopware CLI - uses: shopware/shopware-cli-action@v3 - - - name: Install dependencies - uses: ramsey/composer-install@v3 - - - name: Validate Project - run: shopware-cli project validate . - - - name: Check Code Style - run: shopware-cli project format --dry-run . + phpVersion: '8.4' diff --git a/cmd/project/static/github-deploy.yml b/cmd/project/static/github-deploy.yml index 79deac8c..6f98fe70 100644 --- a/cmd/project/static/github-deploy.yml +++ b/cmd/project/static/github-deploy.yml @@ -8,22 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - - - name: Install Shopware CLI - uses: shopware/shopware-cli-action@v3 - - - name: Build - run: shopware-cli project ci . - - name: Deploy - uses: deployphp/action@v1 + uses: shopware/github-actions/project-deployer@main with: - dep: deploy - private-key: ${{ secrets.SSH_PRIVATE_KEY }} + sshPrivateKey: ${{ secrets.SSH_PRIVATE_KEY }} diff --git a/cmd/project/static/gitlab-ci.yml.tmpl b/cmd/project/static/gitlab-ci.yml.tmpl index 05c2baf6..a6f47061 100644 --- a/cmd/project/static/gitlab-ci.yml.tmpl +++ b/cmd/project/static/gitlab-ci.yml.tmpl @@ -4,53 +4,12 @@ stages: - deploy {{- end }} -variables: - COMPOSER_HOME: ${CI_PROJECT_DIR}/.composer +include: + - component: gitlab.com/shopware/ci-components/project-validate@main + inputs: + php_version: "8.4" {{- if .Deployer }} - GIT_STRATEGY: clone -{{- end }} - -cache: - paths: - - .composer/ - -validate: - stage: check - image: - name: ghcr.io/shopware/shopware-cli:latest-php-8.4 - entrypoint: ["/bin/sh", "-c"] - before_script: - - composer install - script: - - shopware-cli project validate . > validate.json - - shopware-cli project format --dry-run . - artifacts: - reports: - codequality: validate.json -{{- if .Deployer }} - -.configureSSHAgent: &configureSSHAgent |- - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh - if ! printf '%s\n' "$DEPLOYMENT_SERVER" | grep -Eq '^[a-zA-Z0-9.-]+$'; then - echo "Invalid DEPLOYMENT_SERVER value: $DEPLOYMENT_SERVER" >&2 - exit 1 - fi - ssh-keyscan -H -- "$DEPLOYMENT_SERVER" >> ~/.ssh/known_hosts - chmod 700 ~/.ssh - -deploy: - stage: deploy - image: - name: ghcr.io/shopware/shopware-cli:latest-php-8.4 - entrypoint: ["/bin/sh", "-c"] - before_script: - - *configureSSHAgent - script: - - shopware-cli project ci . - - vendor/bin/dep deploy - only: - - main - when: manual + - component: gitlab.com/shopware/ci-components/project-deploy@main + inputs: + php_version: "8.4" {{- end }} From a6c887c308e9ec68126e5ec99545ddc2f57c2635 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 2 Feb 2026 08:03:59 +0100 Subject: [PATCH 03/12] fix: show recommanded path --- cmd/project/project_create.go | 16 +++++++++++++--- internal/color/color.go | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/project/project_create.go b/cmd/project/project_create.go index 4d1e8e48..728da122 100644 --- a/cmd/project/project_create.go +++ b/cmd/project/project_create.go @@ -19,6 +19,7 @@ import ( "github.com/shyim/go-version" "github.com/spf13/cobra" + "github.com/shopware/shopware-cli/internal/color" "github.com/shopware/shopware-cli/internal/git" "github.com/shopware/shopware-cli/internal/packagist" "github.com/shopware/shopware-cli/internal/system" @@ -106,12 +107,12 @@ var projectCreateCmd = &cobra.Command{ huh.NewOption("None", packagist.DeploymentNone), huh.NewOption("DeployerPHP", packagist.DeploymentDeployer), huh.NewOption("PaaS powered by Platform.sh", packagist.DeploymentPlatformSH), - huh.NewOption("PaaS powered by Shopware", packagist.DeploymentShopwarePaaS), + huh.NewOption(color.RecommendedText.Render("PaaS powered by Shopware (Recommended)"), packagist.DeploymentShopwarePaaS), } ciOptions := []huh.Option[string]{ huh.NewOption("None", ciNone), - huh.NewOption("GitHub Actions", ciGitHub), + huh.NewOption(color.RecommendedText.Render("GitHub Actions (Recommended)"), ciGitHub), huh.NewOption("GitLab CI", ciGitLab), } @@ -156,6 +157,15 @@ var projectCreateCmd = &cobra.Command{ if s == "" { return fmt.Errorf("project name is required") } + if info, err := os.Stat(s); err == nil && info.IsDir() { + empty, err := system.IsDirEmpty(s) + if err != nil { + return err + } + if !empty { + return fmt.Errorf("folder already exists and is not empty") + } + } return nil }), ) @@ -200,7 +210,7 @@ var projectCreateCmd = &cobra.Command{ optionalOptions = append(optionalOptions, huh.NewOption("Initialize Git Repository", optionGit)) } if !cmd.PersistentFlags().Changed("docker") { - optionalOptions = append(optionalOptions, huh.NewOption("Local Docker Setup", optionDocker)) + optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("Local Docker Setup (Recommended)"), optionDocker)) } if !cmd.PersistentFlags().Changed("with-elasticsearch") { optionalOptions = append(optionalOptions, huh.NewOption("Setup Elasticsearch/OpenSearch support", optionElasticsearch)) diff --git a/internal/color/color.go b/internal/color/color.go index b525b1cf..f5880829 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -3,3 +3,4 @@ package color import "github.com/charmbracelet/lipgloss" var GreenText = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) +var RecommendedText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD700")).Bold(true) From 5acb0481239098dac694ad1c2364492db6b8e8a6 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Tue, 3 Feb 2026 06:10:33 +0100 Subject: [PATCH 04/12] fix: adjust coloring --- cmd/project/project_create.go | 18 +++++++++--------- internal/color/color.go | 4 +++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cmd/project/project_create.go b/cmd/project/project_create.go index 728da122..18505e1e 100644 --- a/cmd/project/project_create.go +++ b/cmd/project/project_create.go @@ -97,23 +97,23 @@ var projectCreateCmd = &cobra.Command{ } versionOptions := make([]huh.Option[string], 0, len(filteredVersions)+1) - versionOptions = append(versionOptions, huh.NewOption(versionLatest, versionLatest)) + versionOptions = append(versionOptions, huh.NewOption(color.NeutralText.Render(versionLatest), versionLatest)) for _, v := range filteredVersions { versionStr := v.String() - versionOptions = append(versionOptions, huh.NewOption(versionStr, versionStr)) + versionOptions = append(versionOptions, huh.NewOption(color.NeutralText.Render(versionStr), versionStr)) } deploymentOptions := []huh.Option[string]{ - huh.NewOption("None", packagist.DeploymentNone), - huh.NewOption("DeployerPHP", packagist.DeploymentDeployer), - huh.NewOption("PaaS powered by Platform.sh", packagist.DeploymentPlatformSH), + huh.NewOption(color.NeutralText.Render("None"), packagist.DeploymentNone), + huh.NewOption(color.SecondaryText.Render("DeployerPHP"), packagist.DeploymentDeployer), + huh.NewOption(color.SecondaryText.Render("PaaS powered by Platform.sh"), packagist.DeploymentPlatformSH), huh.NewOption(color.RecommendedText.Render("PaaS powered by Shopware (Recommended)"), packagist.DeploymentShopwarePaaS), } ciOptions := []huh.Option[string]{ - huh.NewOption("None", ciNone), + huh.NewOption(color.NeutralText.Render("None"), ciNone), huh.NewOption(color.RecommendedText.Render("GitHub Actions (Recommended)"), ciGitHub), - huh.NewOption("GitLab CI", ciGitLab), + huh.NewOption(color.NeutralText.Render("GitLab CI"), ciGitLab), } var projectFolder string @@ -207,13 +207,13 @@ var projectCreateCmd = &cobra.Command{ var optionalOptions []huh.Option[string] if !cmd.PersistentFlags().Changed("git") { - optionalOptions = append(optionalOptions, huh.NewOption("Initialize Git Repository", optionGit)) + optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("Initialize Git Repository (Recommended)"), optionGit)) } if !cmd.PersistentFlags().Changed("docker") { optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("Local Docker Setup (Recommended)"), optionDocker)) } if !cmd.PersistentFlags().Changed("with-elasticsearch") { - optionalOptions = append(optionalOptions, huh.NewOption("Setup Elasticsearch/OpenSearch support", optionElasticsearch)) + optionalOptions = append(optionalOptions, huh.NewOption(color.NeutralText.Render("Setup Elasticsearch/OpenSearch support"), optionElasticsearch)) } if len(optionalOptions) > 0 { diff --git a/internal/color/color.go b/internal/color/color.go index f5880829..e759bfc5 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -3,4 +3,6 @@ package color import "github.com/charmbracelet/lipgloss" var GreenText = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) -var RecommendedText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD700")).Bold(true) +var RecommendedText = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")).Bold(true) +var SecondaryText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD700")).Bold(true) +var NeutralText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) From a59f721a6ba762f45f1ff68f116d47d34f78a54b Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 4 Feb 2026 11:38:39 +0100 Subject: [PATCH 05/12] fix: restrict network connection for tests --- .github/workflows/go_test.yml | 12 +- cmd/project/project_create.go | 11 +- cmd/project/project_create_test.go | 203 ++++++++++++++++++++++++++++ internal/color/color.go | 24 +++- internal/extension/platform.go | 5 +- internal/extension/platform_test.go | 23 ++++ sandbox-no-network.sb | 5 + 7 files changed, 272 insertions(+), 11 deletions(-) create mode 100644 cmd/project/project_create_test.go create mode 100644 sandbox-no-network.sb diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index bf1de30f..04532503 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -34,5 +34,13 @@ jobs: - name: Build run: go build -v ./... - - name: Test - run: go test ./... + - name: Test (with network blocked) + env: + NIX_CC: 1 + run: | + # Block outbound HTTP/HTTPS to detect tests making real network calls + sudo iptables -A OUTPUT -p tcp --dport 80 -j REJECT + sudo iptables -A OUTPUT -p tcp --dport 443 -j REJECT + go test ./... + sudo iptables -D OUTPUT -p tcp --dport 80 -j REJECT + sudo iptables -D OUTPUT -p tcp --dport 443 -j REJECT diff --git a/cmd/project/project_create.go b/cmd/project/project_create.go index 18505e1e..b9cb01c5 100644 --- a/cmd/project/project_create.go +++ b/cmd/project/project_create.go @@ -104,15 +104,15 @@ var projectCreateCmd = &cobra.Command{ } deploymentOptions := []huh.Option[string]{ + huh.NewOption(color.RecommendedText.Render("PaaS powered by Shopware (Recommended)"), packagist.DeploymentShopwarePaaS), huh.NewOption(color.NeutralText.Render("None"), packagist.DeploymentNone), huh.NewOption(color.SecondaryText.Render("DeployerPHP"), packagist.DeploymentDeployer), huh.NewOption(color.SecondaryText.Render("PaaS powered by Platform.sh"), packagist.DeploymentPlatformSH), - huh.NewOption(color.RecommendedText.Render("PaaS powered by Shopware (Recommended)"), packagist.DeploymentShopwarePaaS), } ciOptions := []huh.Option[string]{ - huh.NewOption(color.NeutralText.Render("None"), ciNone), huh.NewOption(color.RecommendedText.Render("GitHub Actions (Recommended)"), ciGitHub), + huh.NewOption(color.NeutralText.Render("None"), ciNone), huh.NewOption(color.NeutralText.Render("GitLab CI"), ciGitLab), } @@ -143,6 +143,9 @@ var projectCreateCmd = &cobra.Command{ if selectedCI == "" { selectedCI = ciNone } + if !cmd.PersistentFlags().Changed("with-elasticsearch") { + withElasticsearch = true + } } else { var formFields []huh.Field @@ -207,10 +210,10 @@ var projectCreateCmd = &cobra.Command{ var optionalOptions []huh.Option[string] if !cmd.PersistentFlags().Changed("git") { - optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("Initialize Git Repository (Recommended)"), optionGit)) + optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("Initialize Git Repository (Recommended)"), optionGit).Selected(true)) } if !cmd.PersistentFlags().Changed("docker") { - optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("Local Docker Setup (Recommended)"), optionDocker)) + optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("Local Docker Setup (Recommended)"), optionDocker).Selected(true)) } if !cmd.PersistentFlags().Changed("with-elasticsearch") { optionalOptions = append(optionalOptions, huh.NewOption(color.NeutralText.Render("Setup Elasticsearch/OpenSearch support"), optionElasticsearch)) diff --git a/cmd/project/project_create_test.go b/cmd/project/project_create_test.go new file mode 100644 index 00000000..1db355f0 --- /dev/null +++ b/cmd/project/project_create_test.go @@ -0,0 +1,203 @@ +package project + +import ( + "os" + "path/filepath" + "testing" + + "github.com/shyim/go-version" + "github.com/stretchr/testify/assert" + + "github.com/shopware/shopware-cli/internal/packagist" +) + +func TestResolveVersion(t *testing.T) { + versions := []*version.Version{ + version.Must(version.NewVersion("6.6.1.0-rc1")), + version.Must(version.NewVersion("6.6.0.0")), + version.Must(version.NewVersion("6.5.8.0")), + version.Must(version.NewVersion("6.5.7.0")), + } + + t.Run("latest selects most recent stable version", func(t *testing.T) { + result := resolveVersion(versionLatest, versions) + assert.Equal(t, "6.6.0.0", result) + }) + + t.Run("latest falls back to RC if no stable", func(t *testing.T) { + rcOnly := []*version.Version{ + version.Must(version.NewVersion("6.7.0.0-rc2")), + version.Must(version.NewVersion("6.7.0.0-rc1")), + } + result := resolveVersion(versionLatest, rcOnly) + assert.Equal(t, "6.7.0.0-rc2", result) + }) + + t.Run("latest returns empty for empty list", func(t *testing.T) { + result := resolveVersion(versionLatest, []*version.Version{}) + assert.Equal(t, "", result) + }) + + t.Run("exact version match", func(t *testing.T) { + result := resolveVersion("6.5.8.0", versions) + assert.Equal(t, "6.5.8.0", result) + }) + + t.Run("version not found returns empty", func(t *testing.T) { + result := resolveVersion("6.4.0.0", versions) + assert.Equal(t, "", result) + }) + + t.Run("dev version passes through", func(t *testing.T) { + result := resolveVersion("dev-trunk", versions) + assert.Equal(t, "dev-trunk", result) + }) + + t.Run("dev version with branch name", func(t *testing.T) { + result := resolveVersion("dev-6.6", versions) + assert.Equal(t, "dev-6.6", result) + }) +} + +func TestSetupDeployment(t *testing.T) { + t.Run("none creates no files", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupDeployment(tmpDir, packagist.DeploymentNone) + assert.NoError(t, err) + + entries, err := os.ReadDir(tmpDir) + assert.NoError(t, err) + assert.Empty(t, entries) + }) + + t.Run("deployer creates deploy.php", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupDeployment(tmpDir, packagist.DeploymentDeployer) + assert.NoError(t, err) + + assert.FileExists(t, filepath.Join(tmpDir, "deploy.php")) + content, err := os.ReadFile(filepath.Join(tmpDir, "deploy.php")) + assert.NoError(t, err) + assert.Equal(t, deployerTemplate, string(content)) + }) + + t.Run("shopware-paas creates application.yaml", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupDeployment(tmpDir, packagist.DeploymentShopwarePaaS) + assert.NoError(t, err) + + assert.FileExists(t, filepath.Join(tmpDir, "application.yaml")) + content, err := os.ReadFile(filepath.Join(tmpDir, "application.yaml")) + assert.NoError(t, err) + assert.Contains(t, string(content), "php:") + assert.Contains(t, string(content), "mysql:") + }) + + t.Run("platformsh creates no files", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupDeployment(tmpDir, packagist.DeploymentPlatformSH) + assert.NoError(t, err) + + entries, err := os.ReadDir(tmpDir) + assert.NoError(t, err) + assert.Empty(t, entries) + }) +} + +func TestSetupCI(t *testing.T) { + t.Run("none creates no files", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupCI(tmpDir, "none", packagist.DeploymentNone) + assert.NoError(t, err) + + entries, err := os.ReadDir(tmpDir) + assert.NoError(t, err) + assert.Empty(t, entries) + }) + + t.Run("github creates workflow directory and ci.yml", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupCI(tmpDir, "github", packagist.DeploymentNone) + assert.NoError(t, err) + + assert.DirExists(t, filepath.Join(tmpDir, ".github", "workflows")) + assert.FileExists(t, filepath.Join(tmpDir, ".github", "workflows", "ci.yml")) + assert.NoFileExists(t, filepath.Join(tmpDir, ".github", "workflows", "deploy.yml")) + }) + + t.Run("github with deployer creates deploy.yml", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupCI(tmpDir, "github", packagist.DeploymentDeployer) + assert.NoError(t, err) + + assert.FileExists(t, filepath.Join(tmpDir, ".github", "workflows", "ci.yml")) + assert.FileExists(t, filepath.Join(tmpDir, ".github", "workflows", "deploy.yml")) + }) + + t.Run("gitlab creates .gitlab-ci.yml", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupCI(tmpDir, "gitlab", packagist.DeploymentNone) + assert.NoError(t, err) + + assert.FileExists(t, filepath.Join(tmpDir, ".gitlab-ci.yml")) + }) + + t.Run("gitlab with deployer includes deploy config", func(t *testing.T) { + tmpDir := t.TempDir() + + err := setupCI(tmpDir, "gitlab", packagist.DeploymentDeployer) + assert.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(tmpDir, ".gitlab-ci.yml")) + assert.NoError(t, err) + assert.Contains(t, string(content), "deploy") + }) +} + +func TestValidDeploymentMethods(t *testing.T) { + validDeployments := map[string]bool{ + packagist.DeploymentNone: true, + packagist.DeploymentDeployer: true, + packagist.DeploymentPlatformSH: true, + packagist.DeploymentShopwarePaaS: true, + } + + t.Run("all deployment constants are valid", func(t *testing.T) { + assert.True(t, validDeployments[packagist.DeploymentNone]) + assert.True(t, validDeployments[packagist.DeploymentDeployer]) + assert.True(t, validDeployments[packagist.DeploymentPlatformSH]) + assert.True(t, validDeployments[packagist.DeploymentShopwarePaaS]) + }) + + t.Run("invalid deployment is rejected", func(t *testing.T) { + assert.False(t, validDeployments["invalid"]) + assert.False(t, validDeployments[""]) + }) +} + +func TestValidCISystems(t *testing.T) { + validCISystems := map[string]bool{ + "none": true, + "github": true, + "gitlab": true, + } + + t.Run("all CI constants are valid", func(t *testing.T) { + assert.True(t, validCISystems["none"]) + assert.True(t, validCISystems["github"]) + assert.True(t, validCISystems["gitlab"]) + }) + + t.Run("invalid CI system is rejected", func(t *testing.T) { + assert.False(t, validCISystems["jenkins"]) + assert.False(t, validCISystems[""]) + }) +} diff --git a/internal/color/color.go b/internal/color/color.go index e759bfc5..18a44dc6 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -2,7 +2,23 @@ package color import "github.com/charmbracelet/lipgloss" -var GreenText = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) -var RecommendedText = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")).Bold(true) -var SecondaryText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD700")).Bold(true) -var NeutralText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) +// AdaptiveColor picks light/dark variants automatically based on terminal background +var GreenText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#047857", // darker green for light backgrounds + Dark: "#04B575", // lighter green for dark backgrounds +}) + +var RecommendedText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#047857", + Dark: "#04B575", +}).Bold(true) + +var SecondaryText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#B8860B", // darker gold for light backgrounds + Dark: "#FFD700", +}).Bold(true) + +var NeutralText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#1F2937", // dark gray for light backgrounds + Dark: "#FFFFFF", +}) diff --git a/internal/extension/platform.go b/internal/extension/platform.go index ebd39f21..98246068 100644 --- a/internal/extension/platform.go +++ b/internal/extension/platform.go @@ -402,8 +402,11 @@ func validatePHPFiles(c context.Context, ext Extension, check validation.Check) } } +// phpVersionURL can be overridden in tests to use a mock server +var phpVersionURL = "https://raw.githubusercontent.com/FriendsOfShopware/shopware-static-data/main/data/php-version.json" + func GetPhpVersion(ctx context.Context, constraint *version.Constraints) (string, error) { - r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://raw.githubusercontent.com/FriendsOfShopware/shopware-static-data/main/data/php-version.json", http.NoBody) + r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, phpVersionURL, http.NoBody) resp, err := http.DefaultClient.Do(r) if err != nil { diff --git a/internal/extension/platform_test.go b/internal/extension/platform_test.go index d980bde9..a4cfd95f 100644 --- a/internal/extension/platform_test.go +++ b/internal/extension/platform_test.go @@ -1,6 +1,8 @@ package extension import ( + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -8,6 +10,21 @@ import ( "github.com/stretchr/testify/assert" ) +func setupMockPHPVersionServer(t *testing.T) { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"6.4.0.0": "7.4", "6.5.0.0": "8.1", "6.6.0.0": "8.2"}`)) + })) + t.Cleanup(server.Close) + + original := phpVersionURL + phpVersionURL = server.URL + t.Cleanup(func() { + phpVersionURL = original + }) +} + func getTestPlugin(tempDir string) PlatformPlugin { return PlatformPlugin{ path: tempDir, @@ -55,6 +72,7 @@ func getTestPlugin(tempDir string) PlatformPlugin { } func TestPluginIconNotExists(t *testing.T) { + setupMockPHPVersionServer(t) dir := t.TempDir() plugin := getTestPlugin(dir) @@ -68,6 +86,7 @@ func TestPluginIconNotExists(t *testing.T) { } func TestPluginIconExists(t *testing.T) { + setupMockPHPVersionServer(t) dir := t.TempDir() plugin := getTestPlugin(dir) @@ -83,6 +102,7 @@ func TestPluginIconExists(t *testing.T) { } func TestPluginIconDifferntPathExists(t *testing.T) { + setupMockPHPVersionServer(t) dir := t.TempDir() plugin := getTestPlugin(dir) @@ -98,6 +118,7 @@ func TestPluginIconDifferntPathExists(t *testing.T) { } func TestPluginIconIsTooBig(t *testing.T) { + setupMockPHPVersionServer(t) dir := t.TempDir() plugin := getTestPlugin(dir) @@ -114,6 +135,7 @@ func TestPluginIconIsTooBig(t *testing.T) { } func TestPluginGermanDescriptionMissing(t *testing.T) { + setupMockPHPVersionServer(t) dir := t.TempDir() plugin := getTestPlugin(dir) @@ -132,6 +154,7 @@ func TestPluginGermanDescriptionMissing(t *testing.T) { } func TestPluginGermanDescriptionMissingOnlyEnglishMarket(t *testing.T) { + setupMockPHPVersionServer(t) dir := t.TempDir() plugin := getTestPlugin(dir) diff --git a/sandbox-no-network.sb b/sandbox-no-network.sb new file mode 100644 index 00000000..b4f5bc44 --- /dev/null +++ b/sandbox-no-network.sb @@ -0,0 +1,5 @@ +(version 1) +(allow default) +(allow network*) +(deny network-outbound (remote tcp "*:80")) +(deny network-outbound (remote tcp "*:443")) From d66519617780050fd706ffd6e1aea0c8c27e34ac Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 4 Feb 2026 14:31:47 +0100 Subject: [PATCH 06/12] fix: parallelize installs --- .github/workflows/go_test.yml | 3 +++ cmd/project/project_create_test.go | 25 +++++++++++++++++++ internal/changelog/changelog_test.go | 3 +++ internal/curl/curl_wrapper_test.go | 13 ++++++++++ internal/esbuild/util_test.go | 2 ++ internal/flexmigrator/cleanup_test.go | 6 +++++ internal/flexmigrator/composer_test.go | 4 +++ internal/flexmigrator/env_test.go | 5 ++++ internal/git/git_test.go | 3 +++ internal/html/parser_test.go | 3 +++ internal/spdx/license_test.go | 3 +++ internal/system/cache_test.go | 7 ++++++ internal/system/fs_test.go | 2 ++ internal/twigparser/twig_test.go | 7 ++++++ internal/verifier/result_test.go | 8 ++++++ internal/verifier/tool_test.go | 6 +++++ .../admintwiglinter/fix_alert_test.go | 1 + .../admintwiglinter/fix_button_test.go | 1 + .../admintwiglinter/fix_card_test.go | 1 + .../fix_checkbox_field_test.go | 1 + .../admintwiglinter/fix_colorpicker_test.go | 1 + .../admintwiglinter/fix_datepicker_test.go | 1 + .../admintwiglinter/fix_email_field_test.go | 1 + .../admintwiglinter/fix_external_link_test.go | 1 + .../admintwiglinter/fix_icon_test.go | 1 + .../admintwiglinter/fix_loader_test.go | 1 + .../admintwiglinter/fix_number_field_test.go | 1 + .../admintwiglinter/fix_passwordfield_test.go | 1 + .../admintwiglinter/fix_progress_bar_test.go | 1 + .../admintwiglinter/fix_select_field_test.go | 1 + .../admintwiglinter/fix_skeleton_bar_test.go | 1 + .../admintwiglinter/fix_switch_test.go | 1 + .../admintwiglinter/fix_text_field_test.go | 1 + .../admintwiglinter/fix_textareafield_test.go | 1 + .../admintwiglinter/fix_url_field_test.go | 1 + .../admintwiglinter/fixer_popover_test.go | 1 + .../storefronttwiglinter/image_test.go | 3 +++ .../storefronttwiglinter/link_test.go | 2 ++ .../storefronttwiglinter/style_test.go | 2 ++ 39 files changed, 127 insertions(+) diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index b747550d..5b892e4d 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -31,6 +31,9 @@ jobs: check-latest: true cache: true + - name: Download modules + run: go mod download + - name: Build run: go build -v ./... diff --git a/cmd/project/project_create_test.go b/cmd/project/project_create_test.go index 1db355f0..a9d12932 100644 --- a/cmd/project/project_create_test.go +++ b/cmd/project/project_create_test.go @@ -12,6 +12,7 @@ import ( ) func TestResolveVersion(t *testing.T) { + t.Parallel() versions := []*version.Version{ version.Must(version.NewVersion("6.6.1.0-rc1")), version.Must(version.NewVersion("6.6.0.0")), @@ -20,11 +21,13 @@ func TestResolveVersion(t *testing.T) { } t.Run("latest selects most recent stable version", func(t *testing.T) { + t.Parallel() result := resolveVersion(versionLatest, versions) assert.Equal(t, "6.6.0.0", result) }) t.Run("latest falls back to RC if no stable", func(t *testing.T) { + t.Parallel() rcOnly := []*version.Version{ version.Must(version.NewVersion("6.7.0.0-rc2")), version.Must(version.NewVersion("6.7.0.0-rc1")), @@ -34,33 +37,40 @@ func TestResolveVersion(t *testing.T) { }) t.Run("latest returns empty for empty list", func(t *testing.T) { + t.Parallel() result := resolveVersion(versionLatest, []*version.Version{}) assert.Equal(t, "", result) }) t.Run("exact version match", func(t *testing.T) { + t.Parallel() result := resolveVersion("6.5.8.0", versions) assert.Equal(t, "6.5.8.0", result) }) t.Run("version not found returns empty", func(t *testing.T) { + t.Parallel() result := resolveVersion("6.4.0.0", versions) assert.Equal(t, "", result) }) t.Run("dev version passes through", func(t *testing.T) { + t.Parallel() result := resolveVersion("dev-trunk", versions) assert.Equal(t, "dev-trunk", result) }) t.Run("dev version with branch name", func(t *testing.T) { + t.Parallel() result := resolveVersion("dev-6.6", versions) assert.Equal(t, "dev-6.6", result) }) } func TestSetupDeployment(t *testing.T) { + t.Parallel() t.Run("none creates no files", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupDeployment(tmpDir, packagist.DeploymentNone) @@ -72,6 +82,7 @@ func TestSetupDeployment(t *testing.T) { }) t.Run("deployer creates deploy.php", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupDeployment(tmpDir, packagist.DeploymentDeployer) @@ -84,6 +95,7 @@ func TestSetupDeployment(t *testing.T) { }) t.Run("shopware-paas creates application.yaml", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupDeployment(tmpDir, packagist.DeploymentShopwarePaaS) @@ -97,6 +109,7 @@ func TestSetupDeployment(t *testing.T) { }) t.Run("platformsh creates no files", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupDeployment(tmpDir, packagist.DeploymentPlatformSH) @@ -109,7 +122,9 @@ func TestSetupDeployment(t *testing.T) { } func TestSetupCI(t *testing.T) { + t.Parallel() t.Run("none creates no files", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupCI(tmpDir, "none", packagist.DeploymentNone) @@ -121,6 +136,7 @@ func TestSetupCI(t *testing.T) { }) t.Run("github creates workflow directory and ci.yml", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupCI(tmpDir, "github", packagist.DeploymentNone) @@ -132,6 +148,7 @@ func TestSetupCI(t *testing.T) { }) t.Run("github with deployer creates deploy.yml", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupCI(tmpDir, "github", packagist.DeploymentDeployer) @@ -142,6 +159,7 @@ func TestSetupCI(t *testing.T) { }) t.Run("gitlab creates .gitlab-ci.yml", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupCI(tmpDir, "gitlab", packagist.DeploymentNone) @@ -151,6 +169,7 @@ func TestSetupCI(t *testing.T) { }) t.Run("gitlab with deployer includes deploy config", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() err := setupCI(tmpDir, "gitlab", packagist.DeploymentDeployer) @@ -163,6 +182,7 @@ func TestSetupCI(t *testing.T) { } func TestValidDeploymentMethods(t *testing.T) { + t.Parallel() validDeployments := map[string]bool{ packagist.DeploymentNone: true, packagist.DeploymentDeployer: true, @@ -171,6 +191,7 @@ func TestValidDeploymentMethods(t *testing.T) { } t.Run("all deployment constants are valid", func(t *testing.T) { + t.Parallel() assert.True(t, validDeployments[packagist.DeploymentNone]) assert.True(t, validDeployments[packagist.DeploymentDeployer]) assert.True(t, validDeployments[packagist.DeploymentPlatformSH]) @@ -178,12 +199,14 @@ func TestValidDeploymentMethods(t *testing.T) { }) t.Run("invalid deployment is rejected", func(t *testing.T) { + t.Parallel() assert.False(t, validDeployments["invalid"]) assert.False(t, validDeployments[""]) }) } func TestValidCISystems(t *testing.T) { + t.Parallel() validCISystems := map[string]bool{ "none": true, "github": true, @@ -191,12 +214,14 @@ func TestValidCISystems(t *testing.T) { } t.Run("all CI constants are valid", func(t *testing.T) { + t.Parallel() assert.True(t, validCISystems["none"]) assert.True(t, validCISystems["github"]) assert.True(t, validCISystems["gitlab"]) }) t.Run("invalid CI system is rejected", func(t *testing.T) { + t.Parallel() assert.False(t, validCISystems["jenkins"]) assert.False(t, validCISystems[""]) }) diff --git a/internal/changelog/changelog_test.go b/internal/changelog/changelog_test.go index f45e08c8..0c2bfd66 100644 --- a/internal/changelog/changelog_test.go +++ b/internal/changelog/changelog_test.go @@ -9,6 +9,7 @@ import ( ) func TestGenerateWithoutConfig(t *testing.T) { + t.Parallel() commits := []git.GitCommit{ { Message: "feat: add new feature", @@ -27,6 +28,7 @@ func TestGenerateWithoutConfig(t *testing.T) { } func TestTicketParsing(t *testing.T) { + t.Parallel() commits := []git.GitCommit{ { Message: "NEXT-1234 - Fooo", @@ -48,6 +50,7 @@ func TestTicketParsing(t *testing.T) { } func TestIncludeFilters(t *testing.T) { + t.Parallel() commits := []git.GitCommit{ { Message: "NEXT-1234 - Fooo", diff --git a/internal/curl/curl_wrapper_test.go b/internal/curl/curl_wrapper_test.go index 7f5a6481..5bc555cd 100644 --- a/internal/curl/curl_wrapper_test.go +++ b/internal/curl/curl_wrapper_test.go @@ -8,13 +8,16 @@ import ( ) func TestInitCurlCommand(t *testing.T) { + t.Parallel() t.Run("empty command", func(t *testing.T) { + t.Parallel() cmd := InitCurlCommand() assert.Empty(t, cmd.options) assert.Empty(t, cmd.args) }) t.Run("with method", func(t *testing.T) { + t.Parallel() cmd := InitCurlCommand(Method("POST")) assert.Len(t, cmd.options, 1) assert.Equal(t, "-X", cmd.options[0].flag) @@ -22,6 +25,7 @@ func TestInitCurlCommand(t *testing.T) { }) t.Run("with bearer token", func(t *testing.T) { + t.Parallel() cmd := InitCurlCommand(BearerToken("test-token")) assert.Len(t, cmd.options, 1) assert.Equal(t, "--header", cmd.options[0].flag) @@ -29,12 +33,14 @@ func TestInitCurlCommand(t *testing.T) { }) t.Run("with custom args", func(t *testing.T) { + t.Parallel() cmd := InitCurlCommand(Args([]string{"-v"})) assert.Empty(t, cmd.options) assert.Equal(t, []string{"-v"}, cmd.args) }) t.Run("with URL", func(t *testing.T) { + t.Parallel() u, _ := url.Parse("https://example.com") cmd := InitCurlCommand(Url(u)) assert.Empty(t, cmd.options) @@ -42,6 +48,7 @@ func TestInitCurlCommand(t *testing.T) { }) t.Run("with header", func(t *testing.T) { + t.Parallel() cmd := InitCurlCommand(Header("Content-Type", "application/json")) assert.Len(t, cmd.options, 1) assert.Equal(t, "--header", cmd.options[0].flag) @@ -49,6 +56,7 @@ func TestInitCurlCommand(t *testing.T) { }) t.Run("with multiple options", func(t *testing.T) { + t.Parallel() u, _ := url.Parse("https://example.com") cmd := InitCurlCommand( Method("POST"), @@ -72,12 +80,15 @@ func TestInitCurlCommand(t *testing.T) { } func TestCommand_getCmdOptions(t *testing.T) { + t.Parallel() t.Run("empty command", func(t *testing.T) { + t.Parallel() cmd := &Command{} assert.Empty(t, cmd.getCmdOptions()) }) t.Run("with options and args", func(t *testing.T) { + t.Parallel() cmd := &Command{ options: []curlOption{ {flag: "-X", value: "POST"}, @@ -96,12 +107,14 @@ func TestCommand_getCmdOptions(t *testing.T) { } func TestArgs_WithExistingArgs(t *testing.T) { + t.Parallel() cmd := &Command{args: []string{"existing"}} Args([]string{"new"})(cmd) assert.Equal(t, []string{"existing", "new"}, cmd.args) } func TestUrl_WithExistingArgs(t *testing.T) { + t.Parallel() cmd := &Command{args: []string{"existing"}} u, _ := url.Parse("https://example.com") Url(u)(cmd) diff --git a/internal/esbuild/util_test.go b/internal/esbuild/util_test.go index 92462050..dc0c7fdf 100644 --- a/internal/esbuild/util_test.go +++ b/internal/esbuild/util_test.go @@ -7,6 +7,7 @@ import ( ) func TestKebabCase(t *testing.T) { + t.Parallel() assert.Equal(t, "foo-bar", ToKebabCase("FooBar")) assert.Equal(t, "f-o-o-bar-baz", ToKebabCase("FOOBarBaz")) assert.Equal(t, "frosh-tools", ToKebabCase("FroshTools")) @@ -18,6 +19,7 @@ func TestKebabCase(t *testing.T) { } func TestBundleFolderName(t *testing.T) { + t.Parallel() assert.Equal(t, "myplugin", toBundleFolderName("MyPluginBundle")) assert.Equal(t, "anotherplugin", toBundleFolderName("AnotherPluginBundle")) assert.Equal(t, "simpleplugin", toBundleFolderName("SimplePlugin")) diff --git a/internal/flexmigrator/cleanup_test.go b/internal/flexmigrator/cleanup_test.go index 3a0c8367..4746af9e 100644 --- a/internal/flexmigrator/cleanup_test.go +++ b/internal/flexmigrator/cleanup_test.go @@ -12,7 +12,9 @@ import ( ) func TestCleanup(t *testing.T) { + t.Parallel() t.Run("remove existing files", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() @@ -52,6 +54,7 @@ func TestCleanup(t *testing.T) { }) t.Run("remove existing directories", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() @@ -95,6 +98,7 @@ func TestCleanup(t *testing.T) { }) t.Run("remove files by MD5", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() @@ -152,6 +156,7 @@ func TestCleanup(t *testing.T) { }) t.Run("handle non-existent files and directories", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() @@ -161,6 +166,7 @@ func TestCleanup(t *testing.T) { }) t.Run("handle files with non-matching MD5", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() diff --git a/internal/flexmigrator/composer_test.go b/internal/flexmigrator/composer_test.go index 89f0ea4d..e6bdf38d 100644 --- a/internal/flexmigrator/composer_test.go +++ b/internal/flexmigrator/composer_test.go @@ -13,7 +13,9 @@ import ( ) func TestMigrateComposerJson(t *testing.T) { + t.Parallel() t.Run("successful migration", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() @@ -106,12 +108,14 @@ func TestMigrateComposerJson(t *testing.T) { }) t.Run("non-existent composer.json", func(t *testing.T) { + t.Parallel() tempDir := t.TempDir() err := MigrateComposerJson(tempDir) assert.Error(t, err) }) t.Run("invalid composer.json", func(t *testing.T) { + t.Parallel() tempDir := t.TempDir() composerFile := filepath.Join(tempDir, "composer.json") err := os.WriteFile(composerFile, []byte("invalid json"), 0o644) diff --git a/internal/flexmigrator/env_test.go b/internal/flexmigrator/env_test.go index e7674f72..4af4d6b4 100644 --- a/internal/flexmigrator/env_test.go +++ b/internal/flexmigrator/env_test.go @@ -10,7 +10,9 @@ import ( ) func TestMigrateEnv(t *testing.T) { + t.Parallel() t.Run("successful migration with only .env", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() @@ -35,6 +37,7 @@ func TestMigrateEnv(t *testing.T) { }) t.Run("no migration needed when .env.local exists", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() @@ -62,6 +65,7 @@ func TestMigrateEnv(t *testing.T) { }) t.Run("no migration needed when no files exist", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() @@ -77,6 +81,7 @@ func TestMigrateEnv(t *testing.T) { }) t.Run("no migration needed when only .env.local exists", func(t *testing.T) { + t.Parallel() // Create a temporary directory for the test tempDir := t.TempDir() diff --git a/internal/git/git_test.go b/internal/git/git_test.go index 4476c1d4..379c7548 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -10,6 +10,7 @@ import ( ) func TestInvalidGitRepository(t *testing.T) { + t.Parallel() repo := "invalid" ctx := t.Context() @@ -19,6 +20,7 @@ func TestInvalidGitRepository(t *testing.T) { } func TestNoTags(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() prepareRepository(t, tmpDir) _ = os.WriteFile(filepath.Join(tmpDir, "a"), []byte(""), os.ModePerm) @@ -39,6 +41,7 @@ func TestNoTags(t *testing.T) { } func TestWithOneTagAndCommit(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() prepareRepository(t, tmpDir) _ = os.WriteFile(filepath.Join(tmpDir, "a"), []byte(""), os.ModePerm) diff --git a/internal/html/parser_test.go b/internal/html/parser_test.go index fec0adaa..d643e432 100644 --- a/internal/html/parser_test.go +++ b/internal/html/parser_test.go @@ -10,6 +10,7 @@ import ( ) func TestFormattingOfHTML(t *testing.T) { + t.Parallel() swBlock := &ElementNode{ Tag: "sw-button", Attributes: NodeList{ @@ -110,6 +111,7 @@ func TestFormatting(t *testing.T) { } func TestChangeElement(t *testing.T) { + t.Parallel() node, err := NewParser(``) assert.NoError(t, err) TraverseNode(node, func(n *ElementNode) { @@ -131,6 +133,7 @@ func TestChangeElement(t *testing.T) { } func TestBlockParsing(t *testing.T) { + t.Parallel() input := `{% block name %}{% endblock %}` node, err := NewParser(input) diff --git a/internal/spdx/license_test.go b/internal/spdx/license_test.go index 29f331e8..2da24125 100644 --- a/internal/spdx/license_test.go +++ b/internal/spdx/license_test.go @@ -7,11 +7,13 @@ import ( ) func TestNewSpdxLicenses(t *testing.T) { + t.Parallel() _, err := NewSpdxLicenses() assert.NoError(t, err) } func TestSpdxLicenses_Validate(t *testing.T) { + t.Parallel() s, _ := NewSpdxLicenses() tests := []struct { @@ -26,6 +28,7 @@ func TestSpdxLicenses_Validate(t *testing.T) { for _, tt := range tests { t.Run(tt.identifier, func(t *testing.T) { + t.Parallel() bl, err := s.Validate(tt.identifier) assert.NoError(t, err) diff --git a/internal/system/cache_test.go b/internal/system/cache_test.go index 8eedd8cb..e49eec70 100644 --- a/internal/system/cache_test.go +++ b/internal/system/cache_test.go @@ -16,6 +16,7 @@ import ( ) func TestDiskCache(t *testing.T) { + t.Parallel() // Create temporary directory for testing tmpDir := t.TempDir() @@ -69,6 +70,7 @@ func TestDiskCache(t *testing.T) { } func TestDiskCacheFilePath(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() cache := NewDiskCache(tmpDir) @@ -103,6 +105,7 @@ func TestDiskCacheFilePath(t *testing.T) { } func TestCacheFactory(t *testing.T) { + t.Parallel() factory := NewCacheFactory() // Test default cache creation @@ -148,6 +151,7 @@ func TestIsGitHubActions(t *testing.T) { } func TestCacheInterfaceCompliance(t *testing.T) { + t.Parallel() // Test that both implementations satisfy the Cache interface tmpDir := t.TempDir() @@ -179,6 +183,7 @@ func TestCacheInterfaceCompliance(t *testing.T) { } func TestDiskCacheFolderOperations(t *testing.T) { + t.Parallel() // Create temporary directories for testing tmpDir := t.TempDir() sourceDir := t.TempDir() @@ -244,6 +249,7 @@ func TestDiskCacheFolderOperations(t *testing.T) { } func TestDiskCacheStoreFolderCreatesParentDirectory(t *testing.T) { + t.Parallel() // Create temporary directory for testing tmpDir := t.TempDir() @@ -279,6 +285,7 @@ func TestDiskCacheStoreFolderCreatesParentDirectory(t *testing.T) { } func TestGitHubActionsCacheSymlinksAndPermissions(t *testing.T) { + t.Parallel() // This test would only work in GitHub Actions, but we can test the tar creation logic sourceDir := t.TempDir() diff --git a/internal/system/fs_test.go b/internal/system/fs_test.go index 6e73def4..037d1449 100644 --- a/internal/system/fs_test.go +++ b/internal/system/fs_test.go @@ -9,6 +9,7 @@ import ( ) func TestCopyFiles(t *testing.T) { + t.Parallel() // Create a temporary directory for testing tempDir := t.TempDir() defer func() { @@ -79,6 +80,7 @@ func TestCopyFiles(t *testing.T) { } func TestIsDirEmpty(t *testing.T) { + t.Parallel() // Test empty directory tmpDir := t.TempDir() empty, err := IsDirEmpty(tmpDir) diff --git a/internal/twigparser/twig_test.go b/internal/twigparser/twig_test.go index 6b79572a..ac3c80b4 100644 --- a/internal/twigparser/twig_test.go +++ b/internal/twigparser/twig_test.go @@ -8,6 +8,7 @@ import ( ) func TestBlockParsing(t *testing.T) { + t.Parallel() template := `{% block content %} {% block page_account_address_form_create_personal %} {{ parent() }} @@ -28,6 +29,7 @@ func TestBlockParsing(t *testing.T) { } func TestTraversing(t *testing.T) { + t.Parallel() template := `{% block content %} {{ parent() }} {% endblock %}` @@ -52,6 +54,7 @@ func TestTraversing(t *testing.T) { } func TestSwExtendsParsing(t *testing.T) { + t.Parallel() testcases := []struct { template string path string @@ -87,6 +90,7 @@ func TestSwExtendsParsing(t *testing.T) { } func TestPrintNodeParsing(t *testing.T) { + t.Parallel() template := `{{ a_variable }}` nodes, err := ParseTemplate(template) assert.NoError(t, err) @@ -104,6 +108,7 @@ func TestPrintNodeParsing(t *testing.T) { } func TestDeprecatedNodeParsing(t *testing.T) { + t.Parallel() template := `{% deprecated 'The "base.html.twig" template is deprecated, use "layout.html.twig" instead.' %}` nodes, err := ParseTemplate(template) assert.NoError(t, err) @@ -122,6 +127,7 @@ func TestDeprecatedNodeParsing(t *testing.T) { } func TestSetNodeParsing(t *testing.T) { + t.Parallel() // Inline set assignment. inlineTemplate := `{% set name = 'Fabien' %}` nodes, err := ParseTemplate(inlineTemplate) @@ -162,6 +168,7 @@ func TestSetNodeParsing(t *testing.T) { } func TestAutoescapeNodeParsing(t *testing.T) { + t.Parallel() template := `{% autoescape %} Everything will be automatically escaped. {% endautoescape %}` diff --git a/internal/verifier/result_test.go b/internal/verifier/result_test.go index 9978514e..29e5637c 100644 --- a/internal/verifier/result_test.go +++ b/internal/verifier/result_test.go @@ -9,12 +9,14 @@ import ( ) func TestNewCheck(t *testing.T) { + t.Parallel() check := NewCheck() assert.NotNil(t, check) assert.Empty(t, check.Results) } func TestAddResult(t *testing.T) { + t.Parallel() check := NewCheck() result := validation.CheckResult{ Path: "test.go", @@ -30,6 +32,7 @@ func TestAddResult(t *testing.T) { } func TestHasErrors(t *testing.T) { + t.Parallel() tests := []struct { name string results []validation.CheckResult @@ -61,6 +64,7 @@ func TestHasErrors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() check := NewCheck() for _, result := range tt.results { check.AddResult(result) @@ -71,6 +75,7 @@ func TestHasErrors(t *testing.T) { } func TestRemoveByIdentifier(t *testing.T) { + t.Parallel() tests := []struct { name string initialResults []validation.CheckResult @@ -162,6 +167,7 @@ func TestRemoveByIdentifier(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() check := NewCheck() for _, result := range tt.initialResults { check.AddResult(result) @@ -174,6 +180,7 @@ func TestRemoveByIdentifier(t *testing.T) { } func TestRemoveByMessage(t *testing.T) { + t.Parallel() tests := []struct { name string initialResults []validation.CheckResult @@ -248,6 +255,7 @@ func TestRemoveByMessage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() check := NewCheck() for _, result := range tt.initialResults { check.AddResult(result) diff --git a/internal/verifier/tool_test.go b/internal/verifier/tool_test.go index dd93fc5a..36973ab8 100644 --- a/internal/verifier/tool_test.go +++ b/internal/verifier/tool_test.go @@ -23,6 +23,7 @@ func toolNames(list ToolList) []string { } func TestExclude_EmptyString_NoChange(t *testing.T) { + t.Parallel() base := ToolList{testTool{"phpstan"}, testTool{"eslint"}, testTool{"sw-cli"}} res, err := base.Exclude("") assert.NoError(t, err) @@ -30,6 +31,7 @@ func TestExclude_EmptyString_NoChange(t *testing.T) { } func TestExclude_SingleTool(t *testing.T) { + t.Parallel() base := ToolList{testTool{"phpstan"}, testTool{"eslint"}, testTool{"sw-cli"}} res, err := base.Exclude("eslint") assert.NoError(t, err) @@ -37,6 +39,7 @@ func TestExclude_SingleTool(t *testing.T) { } func TestExclude_MultipleTools(t *testing.T) { + t.Parallel() base := ToolList{testTool{"phpstan"}, testTool{"eslint"}, testTool{"sw-cli"}, testTool{"stylelint"}} res, err := base.Exclude("eslint, stylelint") assert.NoError(t, err) @@ -44,6 +47,7 @@ func TestExclude_MultipleTools(t *testing.T) { } func TestExclude_AllTools_ReturnsEmpty(t *testing.T) { + t.Parallel() base := ToolList{testTool{"phpstan"}, testTool{"eslint"}} res, err := base.Exclude("phpstan,eslint") assert.NoError(t, err) @@ -51,6 +55,7 @@ func TestExclude_AllTools_ReturnsEmpty(t *testing.T) { } func TestExclude_UnknownTool_Error(t *testing.T) { + t.Parallel() base := ToolList{testTool{"phpstan"}, testTool{"eslint"}} res, err := base.Exclude("rector") assert.Error(t, err) @@ -58,6 +63,7 @@ func TestExclude_UnknownTool_Error(t *testing.T) { } func TestExclude_TrimsAndIgnoresDuplicates(t *testing.T) { + t.Parallel() base := ToolList{testTool{"phpstan"}, testTool{"eslint"}, testTool{"sw-cli"}} res, err := base.Exclude(" eslint , eslint , \teslint\t ") assert.NoError(t, err) diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_alert_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_alert_test.go index b1b47243..9d994a89 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_alert_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_alert_test.go @@ -9,6 +9,7 @@ import ( ) func TestAlertFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_button_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_button_test.go index f0959242..ea9ff230 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_button_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_button_test.go @@ -9,6 +9,7 @@ import ( ) func TestButtonFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_card_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_card_test.go index 49017809..3abc9002 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_card_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_card_test.go @@ -9,6 +9,7 @@ import ( ) func TestCardFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_checkbox_field_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_checkbox_field_test.go index 501544d0..c757a0de 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_checkbox_field_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_checkbox_field_test.go @@ -9,6 +9,7 @@ import ( ) func TestCheckboxFieldFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_colorpicker_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_colorpicker_test.go index 0ff28c59..50b8b3b3 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_colorpicker_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_colorpicker_test.go @@ -9,6 +9,7 @@ import ( ) func TestColorpickerFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_datepicker_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_datepicker_test.go index 9218e46d..b16cdd66 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_datepicker_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_datepicker_test.go @@ -9,6 +9,7 @@ import ( ) func TestDatepickerFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_email_field_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_email_field_test.go index 423b2713..ee55717e 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_email_field_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_email_field_test.go @@ -9,6 +9,7 @@ import ( ) func TestEmailFieldFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_external_link_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_external_link_test.go index 423162ec..6fcd4f58 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_external_link_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_external_link_test.go @@ -9,6 +9,7 @@ import ( ) func TestExternalLinkFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_icon_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_icon_test.go index 44f5ede9..3b179b87 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_icon_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_icon_test.go @@ -9,6 +9,7 @@ import ( ) func TestIconFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_loader_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_loader_test.go index d71838c7..0c4f0291 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_loader_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_loader_test.go @@ -9,6 +9,7 @@ import ( ) func TestLoaderFixer(t *testing.T) { + t.Parallel() cases := []struct { before string after string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_number_field_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_number_field_test.go index 3a79c84f..329d62d1 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_number_field_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_number_field_test.go @@ -9,6 +9,7 @@ import ( ) func TestNumberFieldFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_passwordfield_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_passwordfield_test.go index 95806bf1..dd7bb8d9 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_passwordfield_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_passwordfield_test.go @@ -9,6 +9,7 @@ import ( ) func TestPasswordFieldFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_progress_bar_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_progress_bar_test.go index 6cf5d736..9a7bdfaf 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_progress_bar_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_progress_bar_test.go @@ -9,6 +9,7 @@ import ( ) func TestProgressBarFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_select_field_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_select_field_test.go index cc100993..2c230b91 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_select_field_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_select_field_test.go @@ -9,6 +9,7 @@ import ( ) func TestSelectFieldFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_skeleton_bar_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_skeleton_bar_test.go index 200aee07..f4d82695 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_skeleton_bar_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_skeleton_bar_test.go @@ -9,6 +9,7 @@ import ( ) func TestSkeletonBarFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_switch_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_switch_test.go index 7106aa11..84e40244 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_switch_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_switch_test.go @@ -9,6 +9,7 @@ import ( ) func TestSwitchFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_text_field_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_text_field_test.go index abf77d75..567c69db 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_text_field_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_text_field_test.go @@ -9,6 +9,7 @@ import ( ) func TestTextFieldFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_textareafield_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_textareafield_test.go index 991d79cf..b6b2e441 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_textareafield_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_textareafield_test.go @@ -9,6 +9,7 @@ import ( ) func TestTextarea(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fix_url_field_test.go b/internal/verifier/twiglinter/admintwiglinter/fix_url_field_test.go index 6baa0720..020c90d6 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fix_url_field_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fix_url_field_test.go @@ -9,6 +9,7 @@ import ( ) func TestUrlFieldFixer(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/admintwiglinter/fixer_popover_test.go b/internal/verifier/twiglinter/admintwiglinter/fixer_popover_test.go index d8308aaf..b4215a0a 100644 --- a/internal/verifier/twiglinter/admintwiglinter/fixer_popover_test.go +++ b/internal/verifier/twiglinter/admintwiglinter/fixer_popover_test.go @@ -9,6 +9,7 @@ import ( ) func TestPopover(t *testing.T) { + t.Parallel() cases := []struct { description string before string diff --git a/internal/verifier/twiglinter/storefronttwiglinter/image_test.go b/internal/verifier/twiglinter/storefronttwiglinter/image_test.go index ad4434f5..ce56b612 100644 --- a/internal/verifier/twiglinter/storefronttwiglinter/image_test.go +++ b/internal/verifier/twiglinter/storefronttwiglinter/image_test.go @@ -9,6 +9,7 @@ import ( ) func TestImageAltDetection(t *testing.T) { + t.Parallel() cases := []struct { name string content string @@ -97,6 +98,7 @@ func TestImageAltDetection(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + t.Parallel() checks, err := twiglinter.RunCheckerOnString(ImageAltCheck{}, tc.content) assert.NoError(t, err) assert.Len(t, checks, tc.expectedCount, "Expected %d validation errors but got %d", tc.expectedCount, len(checks)) @@ -105,6 +107,7 @@ func TestImageAltDetection(t *testing.T) { } func TestImageAltCheckIdentifiers(t *testing.T) { + t.Parallel() // Test that correct identifiers are used for different types of errors missingAltChecks, err := twiglinter.RunCheckerOnString(ImageAltCheck{}, ``) assert.NoError(t, err) diff --git a/internal/verifier/twiglinter/storefronttwiglinter/link_test.go b/internal/verifier/twiglinter/storefronttwiglinter/link_test.go index 3871ba30..3ee34c60 100644 --- a/internal/verifier/twiglinter/storefronttwiglinter/link_test.go +++ b/internal/verifier/twiglinter/storefronttwiglinter/link_test.go @@ -9,6 +9,7 @@ import ( ) func TestLinkDetection(t *testing.T) { + t.Parallel() cases := []struct { name string content string @@ -58,6 +59,7 @@ func TestLinkDetection(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + t.Parallel() checks, err := twiglinter.RunCheckerOnString(LinkCheck{}, tc.content) assert.NoError(t, err) assert.Len(t, checks, tc.expectedCount) diff --git a/internal/verifier/twiglinter/storefronttwiglinter/style_test.go b/internal/verifier/twiglinter/storefronttwiglinter/style_test.go index eee2828a..2db8d6c2 100644 --- a/internal/verifier/twiglinter/storefronttwiglinter/style_test.go +++ b/internal/verifier/twiglinter/storefronttwiglinter/style_test.go @@ -9,6 +9,7 @@ import ( ) func TestStyleDetection(t *testing.T) { + t.Parallel() cases := []struct { name string content string @@ -43,6 +44,7 @@ func TestStyleDetection(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + t.Parallel() checks, err := twiglinter.RunCheckerOnString(StyleFixer{}, tc.content) assert.NoError(t, err) if tc.expected { From e011c411135a9bbf0a20d05ababaf451aef3fabf Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 4 Feb 2026 14:35:51 +0100 Subject: [PATCH 07/12] refactor: simplify test step by removing network blocking --- .github/workflows/go_test.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index 5b892e4d..f7069bb0 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -37,13 +37,5 @@ jobs: - name: Build run: go build -v ./... - - name: Test (with network blocked) - env: - NIX_CC: 1 - run: | - # Block outbound HTTP/HTTPS to detect tests making real network calls - sudo iptables -A OUTPUT -p tcp --dport 80 -j REJECT - sudo iptables -A OUTPUT -p tcp --dport 443 -j REJECT - go test -v ./... - sudo iptables -D OUTPUT -p tcp --dport 80 -j REJECT - sudo iptables -D OUTPUT -p tcp --dport 443 -j REJECT + - name: Test + run: go test -v ./... From 250d7788bf76a137818f0b0fe2129e38a76ef262 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 4 Feb 2026 14:47:08 +0100 Subject: [PATCH 08/12] feat: add hardening step to CI workflow --- .github/workflows/go_test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index f7069bb0..74c0f7bf 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -21,6 +21,11 @@ jobs: runs-on: ubuntu-latest steps: + - name: Harden Runner + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # ratchet:step-security/harden-runner@v2.14.1 + with: + egress-policy: audit + - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v4 From a1dc20d00b8c911dd07a5d09fef849899a035723 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 4 Feb 2026 14:50:13 +0100 Subject: [PATCH 09/12] fix: update egress policy and allowed endpoints in CI workflow --- .github/workflows/go_test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index 74c0f7bf..f0dfed44 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -24,7 +24,12 @@ jobs: - name: Harden Runner uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # ratchet:step-security/harden-runner@v2.14.1 with: - egress-policy: audit + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + proxy.golang.org:443 + release-assets.githubusercontent.com:443 - name: Checkout Repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v4 From 91684cd9b39aa945a541dd21ccf64367e57b33d2 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 4 Feb 2026 16:21:03 +0100 Subject: [PATCH 10/12] feat: add amqp option --- cmd/project/project_create.go | 11 ++++- internal/packagist/project_composer_json.go | 4 ++ .../packagist/project_composer_json_test.go | 41 ++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/cmd/project/project_create.go b/cmd/project/project_create.go index b9cb01c5..e7bd9068 100644 --- a/cmd/project/project_create.go +++ b/cmd/project/project_create.go @@ -68,6 +68,7 @@ var projectCreateCmd = &cobra.Command{ useDocker, _ := cmd.PersistentFlags().GetBool("docker") withElasticsearch, _ := cmd.PersistentFlags().GetBool("with-elasticsearch") withoutElasticsearch, _ := cmd.PersistentFlags().GetBool("without-elasticsearch") + withAMQP, _ := cmd.PersistentFlags().GetBool("with-amqp") noAudit, _ := cmd.PersistentFlags().GetBool("no-audit") initGit, _ := cmd.PersistentFlags().GetBool("git") versionFlag, _ := cmd.PersistentFlags().GetString("version") @@ -85,6 +86,7 @@ var projectCreateCmd = &cobra.Command{ optionDocker = "docker" optionGit = "git" optionElasticsearch = "elasticsearch" + optionAMQP = "amqp" ciNone = "none" ciGitHub = "github" @@ -215,6 +217,9 @@ var projectCreateCmd = &cobra.Command{ if !cmd.PersistentFlags().Changed("docker") { optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("Local Docker Setup (Recommended)"), optionDocker).Selected(true)) } + if !cmd.PersistentFlags().Changed("with-amqp") { + optionalOptions = append(optionalOptions, huh.NewOption(color.RecommendedText.Render("AMQP Queue Support (Recommended)"), optionAMQP).Selected(true)) + } if !cmd.PersistentFlags().Changed("with-elasticsearch") { optionalOptions = append(optionalOptions, huh.NewOption(color.NeutralText.Render("Setup Elasticsearch/OpenSearch support"), optionElasticsearch)) } @@ -244,6 +249,8 @@ var projectCreateCmd = &cobra.Command{ initGit = true case optionElasticsearch: withElasticsearch = true + case optionAMQP: + withAMQP = true } } } @@ -306,6 +313,7 @@ var projectCreateCmd = &cobra.Command{ RC: strings.Contains(chooseVersion, "rc"), UseDocker: useDocker, UseElasticsearch: withElasticsearch, + UseAMQP: withAMQP, NoAudit: noAudit, DeploymentMethod: selectedDeployment, }) @@ -466,7 +474,7 @@ func runComposerInstall(ctx context.Context, projectFolder string, useDocker boo return err } - dockerArgs := []string{"run", "--rm", + dockerArgs := []string{"run", "--rm", "--pull=always", "-v", fmt.Sprintf("%s:/app", absProjectFolder), "-w", "/app", "ghcr.io/shopware/docker-dev:php8.3-node22-caddy", @@ -527,6 +535,7 @@ func init() { projectCreateCmd.PersistentFlags().Bool("docker", false, "Use Docker to run Composer instead of local installation") projectCreateCmd.PersistentFlags().Bool("with-elasticsearch", false, "Include Elasticsearch/OpenSearch support") projectCreateCmd.PersistentFlags().Bool("without-elasticsearch", false, "Remove Elasticsearch from the installation (deprecated: use --with-elasticsearch)") + projectCreateCmd.PersistentFlags().Bool("with-amqp", false, "Include AMQP queue support (symfony/amqp-messenger)") projectCreateCmd.PersistentFlags().Bool("no-audit", false, "Disable composer audit blocking insecure packages") projectCreateCmd.PersistentFlags().Bool("git", false, "Initialize a Git repository") projectCreateCmd.PersistentFlags().String("version", "", "Shopware version to install (e.g., 6.6.0.0, latest)") diff --git a/internal/packagist/project_composer_json.go b/internal/packagist/project_composer_json.go index 2e2632a4..705c8cae 100644 --- a/internal/packagist/project_composer_json.go +++ b/internal/packagist/project_composer_json.go @@ -25,6 +25,7 @@ type ComposerJsonOptions struct { RC bool UseDocker bool UseElasticsearch bool + UseAMQP bool NoAudit bool DeploymentMethod string } @@ -66,6 +67,9 @@ func GenerateComposerJson(ctx context.Context, opts ComposerJsonOptions) (string require.set("shopware/elasticsearch", opts.DependingVersion) } require.set("shopware/storefront", opts.DependingVersion) + if opts.UseAMQP { + require.set("symfony/amqp-messenger", "*") + } if opts.UseDocker { require.set("shopware/docker-dev", "*") } diff --git a/internal/packagist/project_composer_json_test.go b/internal/packagist/project_composer_json_test.go index 9ec6a679..ca89d90d 100644 --- a/internal/packagist/project_composer_json_test.go +++ b/internal/packagist/project_composer_json_test.go @@ -1,7 +1,6 @@ package packagist import ( - "context" "encoding/json" "testing" @@ -9,9 +8,11 @@ import ( ) func TestGenerateComposerJson(t *testing.T) { - ctx := context.Background() + t.Parallel() t.Run("without audit", func(t *testing.T) { + t.Parallel() + ctx := t.Context() jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0"}) assert.NoError(t, err) assert.Contains(t, jsonStr, `"sort-packages": true`) @@ -23,6 +24,8 @@ func TestGenerateComposerJson(t *testing.T) { }) t.Run("with audit disabled", func(t *testing.T) { + t.Parallel() + ctx := t.Context() jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", NoAudit: true}) assert.NoError(t, err) assert.Contains(t, jsonStr, `"sort-packages": true`) @@ -34,6 +37,8 @@ func TestGenerateComposerJson(t *testing.T) { }) t.Run("with elasticsearch", func(t *testing.T) { + t.Parallel() + ctx := t.Context() jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", UseElasticsearch: true}) assert.NoError(t, err) assert.Contains(t, jsonStr, `"shopware/elasticsearch"`) @@ -44,6 +49,8 @@ func TestGenerateComposerJson(t *testing.T) { }) t.Run("without elasticsearch", func(t *testing.T) { + t.Parallel() + ctx := t.Context() jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", UseElasticsearch: false}) assert.NoError(t, err) assert.NotContains(t, jsonStr, `"shopware/elasticsearch"`) @@ -54,6 +61,8 @@ func TestGenerateComposerJson(t *testing.T) { }) t.Run("with shopware paas deployment", func(t *testing.T) { + t.Parallel() + ctx := t.Context() jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", DeploymentMethod: DeploymentShopwarePaaS}) assert.NoError(t, err) assert.Contains(t, jsonStr, `"shopware/k8s-meta": "*"`) @@ -68,6 +77,8 @@ func TestGenerateComposerJson(t *testing.T) { }) t.Run("with platformsh deployment", func(t *testing.T) { + t.Parallel() + ctx := t.Context() jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", DeploymentMethod: DeploymentPlatformSH}) assert.NoError(t, err) assert.Contains(t, jsonStr, `"shopware/paas-meta": "*"`) @@ -81,6 +92,8 @@ func TestGenerateComposerJson(t *testing.T) { }) t.Run("with deployer deployment", func(t *testing.T) { + t.Parallel() + ctx := t.Context() jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", DeploymentMethod: DeploymentDeployer}) assert.NoError(t, err) assert.Contains(t, jsonStr, `"deployer/deployer": "*"`) @@ -93,4 +106,28 @@ func TestGenerateComposerJson(t *testing.T) { err = json.Unmarshal([]byte(jsonStr), &data) assert.NoError(t, err, "Generated JSON should be valid") }) + + t.Run("with amqp", func(t *testing.T) { + t.Parallel() + ctx := t.Context() + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", UseAMQP: true}) + assert.NoError(t, err) + assert.Contains(t, jsonStr, `"symfony/amqp-messenger": "*"`) + + var data map[string]interface{} + err = json.Unmarshal([]byte(jsonStr), &data) + assert.NoError(t, err, "Generated JSON should be valid") + }) + + t.Run("without amqp", func(t *testing.T) { + t.Parallel() + ctx := t.Context() + jsonStr, err := GenerateComposerJson(ctx, ComposerJsonOptions{Version: "6.4.18.0", UseAMQP: false}) + assert.NoError(t, err) + assert.NotContains(t, jsonStr, `"symfony/amqp-messenger"`) + + var data map[string]interface{} + err = json.Unmarshal([]byte(jsonStr), &data) + assert.NoError(t, err, "Generated JSON should be valid") + }) } From 213a6832cc25aa2a6989207db4c011f26f5d3c3a Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Wed, 11 Feb 2026 04:39:51 +0100 Subject: [PATCH 11/12] fix: check for Composer installation when not using Docker --- cmd/project/project_create.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/project/project_create.go b/cmd/project/project_create.go index e7bd9068..3e65c323 100644 --- a/cmd/project/project_create.go +++ b/cmd/project/project_create.go @@ -284,6 +284,12 @@ var projectCreateCmd = &cobra.Command{ return fmt.Errorf("invalid CI system: %s. Valid options: none, github, gitlab", selectedCI) } + if !useDocker { + if _, err := exec.LookPath("composer"); err != nil { + return fmt.Errorf("composer is not installed. Please install Composer (https://getcomposer.org/) or use the --docker flag") + } + } + if _, err := os.Stat(projectFolder); err == nil { empty, err := system.IsDirEmpty(projectFolder) if err != nil { From c39616766548590f1ff70d617396d256c044e635 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Thu, 12 Feb 2026 14:05:14 +0100 Subject: [PATCH 12/12] feat: add IsInsideContainer function and update runComposerInstall condition --- cmd/project/project_create.go | 2 +- internal/color/color.go | 9 ++++----- internal/system/container.go | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 internal/system/container.go diff --git a/cmd/project/project_create.go b/cmd/project/project_create.go index 3e65c323..181dcb36 100644 --- a/cmd/project/project_create.go +++ b/cmd/project/project_create.go @@ -474,7 +474,7 @@ func setupCI(projectFolder, ciSystem, deploymentMethod string) error { func runComposerInstall(ctx context.Context, projectFolder string, useDocker bool) error { var cmdInstall *exec.Cmd - if useDocker { + if useDocker && !system.IsInsideContainer() { absProjectFolder, err := filepath.Abs(projectFolder) if err != nil { return err diff --git a/internal/color/color.go b/internal/color/color.go index 18a44dc6..0e7b727e 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -2,10 +2,9 @@ package color import "github.com/charmbracelet/lipgloss" -// AdaptiveColor picks light/dark variants automatically based on terminal background var GreenText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ - Light: "#047857", // darker green for light backgrounds - Dark: "#04B575", // lighter green for dark backgrounds + Light: "#047857", + Dark: "#04B575", }) var RecommendedText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ @@ -14,11 +13,11 @@ var RecommendedText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ }).Bold(true) var SecondaryText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ - Light: "#B8860B", // darker gold for light backgrounds + Light: "#B8860B", Dark: "#FFD700", }).Bold(true) var NeutralText = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ - Light: "#1F2937", // dark gray for light backgrounds + Light: "#1F2937", Dark: "#FFFFFF", }) diff --git a/internal/system/container.go b/internal/system/container.go new file mode 100644 index 00000000..0eb05c56 --- /dev/null +++ b/internal/system/container.go @@ -0,0 +1,15 @@ +package system + +import "os" + +func IsInsideContainer() bool { + if _, err := os.Stat("/.dockerenv"); err == nil { + return true + } + + if os.Getenv("container") != "" { + return true + } + + return false +}