Skip to content

Add SSH deployment support with release management#1170

Draft
Soner (shyim) wants to merge 10 commits into
nextfrom
claude/shopware-cli-deploy-command-umiqxz
Draft

Add SSH deployment support with release management#1170
Soner (shyim) wants to merge 10 commits into
nextfrom
claude/shopware-cli-deploy-command-umiqxz

Conversation

@shyim

Copy link
Copy Markdown
Member

Summary

This PR adds comprehensive SSH-based deployment capabilities to the CLI, enabling users to deploy Shopware projects to remote servers using a releases/shared directory layout with atomic symlink switching, similar to Deployer (deployer.org).

Key Changes

New Deployment Infrastructure

  • internal/deployment/ - New package providing deployment abstraction:
    • deployment.go - Core Deployer interface and types (Release, HostReleases, Options)
    • ssh_deployer.go - SSH-based deployer implementing releases/shared/current symlink pattern
    • ssh_connection.go - SSH connection wrapper using system ssh client
    • connection.go - Connection interface for testability
    • archive.go - Project archiving with configurable exclusions
    • Comprehensive test coverage for all deployment scenarios

SSH Command Building

  • internal/sshcmd/ - New package for SSH client command construction:
    • sshcmd.go - Builds SSH arguments from environment config with ControlMaster multiplexing support
    • Handles authentication (keys, passwords, SSH agent), host key verification, and connection pooling
    • Shares multiplexed connections between deployments and remote command execution

Remote Command Execution

  • internal/executor/ssh.go - New SSH executor for running commands on deployed releases:
    • Executes console, composer, PHP, and npm commands on the remote current symlink
    • Supports TTY allocation for interactive commands
    • Integrates with SSH multiplexing for efficient connection reuse

Configuration Schema

  • internal/shop/config.go - Extended EnvironmentConfig with:
    • EnvironmentSSH - SSH connection settings (host, port, user, authentication, known_hosts, ControlMaster control)
    • EnvironmentDeployment - Deployment configuration (path, keep_releases, shared files/dirs, hooks)
    • EnvironmentDeploymentHooks - Build, pre-switch, and post-switch hooks
    • Multi-host support for deployments across multiple servers

CLI Commands

  • cmd/project/project_deploy.go - Main deploy command
  • cmd/project/project_deploy_releases.go - List releases on target
  • cmd/project/project_deploy_rollback.go - Rollback to previous release
  • cmd/project/project_console.go - Enhanced to support SSH environments with flag stripping

Utilities

  • internal/shell/quote.go - POSIX shell quoting for safe command construction
  • internal/executor/factory.go - Extended to support SSH executor type

Notable Implementation Details

  • Atomic Symlink Switching: Uses ln -sfn + mv -fT for atomic current symlink updates, preventing partial deployments
  • Shared Path Management: Automatically seeds shared directories/files from first release, then maintains symlinks across deployments
  • Multi-Host Deployments: Uploads happen in parallel, but hooks and symlink switching are sequential to prevent concurrent database operations
  • Connection Multiplexing: All SSH operations share one ControlMaster connection per host for efficiency
  • Deployment Helper Integration: Auto-detects and runs shopware-deployment-helper when present and no pre-switch hooks are configured
  • Release Cleanup: Configurable retention (default 5 releases) with automatic cleanup of old releases
  • Bad Release Marking: Rollbacks mark the previous release as "bad" to skip it in future rollbacks
  • Graceful Degradation: Cleanup failures are logged as warnings but don't abort deployments

Configuration Example

environments:
  production:
    type: ssh
    ssh:
      host: shop.example.com
      port: 2222
      user: deploy
      identity_file: ~/.ssh/id_ed25519
    deployment:
      path: /var/www/shopware
      keep_releases: 3
      hooks:
        build:
          - shopware-cli project ci .
        pre_switch:
          - vendor/bin/shopware-deployment-helper run
        post

https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ

Adds "shopware-cli project deploy" building on the existing environments
abstraction: environments get a new "ssh" type with connection settings
and a deployment section (path, keep_releases, shared files/dirs, hooks).

Deployment methods are pluggable through a Deployer interface in
internal/deployment so SFTP/PaaS backends can be added later with the
same CLI surface. The first implementation deploys over SSH using a
Deployer-style layout: releases are uploaded as tarball streams into
releases/<timestamp>, shared files and directories are symlinked from
shared/, the current symlink is switched atomically and old releases are
pruned. When no pre_switch hooks are configured the Shopware Deployment
Helper is run automatically when present.

"project deploy rollback [release]" switches back to a previous release
and marks the rolled-back-from release as bad so later rollbacks skip it,
and "project deploy releases" lists the releases on the target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
Environments can now list additional servers under ssh.hosts; each entry
inherits unset connection settings (port, user, authentication) from the
ssh block. The primary host plus all additional hosts form the deployment
target.

Deploys upload the release to all hosts in parallel, run hooks host after
host so database work like migrations never executes concurrently, and
switch the current symlink everywhere only after every host finished its
pre-switch phase, so all hosts serve the same release. A failure on any
host before the switch aborts the deployment with the running release
untouched everywhere.

Rollback validates that the target release exists on every host before
switching any of them, and "deploy releases" lists the releases grouped
per host. Shared symlink creation now uses ln -sfn and best-effort
seeding, making re-deploys after partial failures idempotent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
Implements the Executor interface for ssh environments so commands like
"project console cache:clear --env production", "project clear-cache" and
"project worker" run on the deployment target with the same CLI surface
as local and docker environments.

The SSHExecutor wraps the system ssh client, so the user's SSH agent and
configuration are honored and interactive console commands get a TTY when
stdin is a terminal. Commands execute inside the currently deployed
release (<deployment.path>/current) on the primary host, with environment
variables inlined and all arguments shell-quoted.

Because "project console" disables cobra flag parsing, shopware-cli's own
flags (--env/-e, --project-config) are now extracted from the arguments
before the console command name; everything after it is passed to
bin/console unchanged, keeping Symfony's own --env flag usable.

"project clear-cache --env <name>" runs bin/console cache:clear on the
remote host for ssh environments instead of falling back to the local
cache directory. The shell quoting helper moved to internal/shell so the
deployment and executor packages share it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
Unifies deployments and remote command execution on the system ssh client
with OpenSSH connection multiplexing (ControlMaster). The deployment
previously used the Go SSH library with its own connection, so it could
not share anything with the executor; now both build their commands
through the new internal/sshcmd package, which adds ControlMaster=auto
with a shared ControlPath socket (~/.ssh/shopware-cli-%C) and
ControlPersist=60. The first command opens a master connection per host
and every following command — within a deploy or a "project console"
right after it — reuses it, skipping the TCP, key exchange and
authentication handshake (about 2.5x faster per command on loopback,
more over real networks).

Using the ssh client everywhere also means deployments now honor the
user's ssh_config, SSH agent and ProxyJump settings. Multiplexing is on
by default (except on Windows, where OpenSSH does not support it) and can
be disabled per environment with ssh.control_master: false to defer to
your own ssh_config.

Password based authentication uses sshpass when available and falls back
to the interactive prompt. The ssh.passphrase setting is removed:
encrypted keys are handled by the SSH agent or an interactive prompt,
which the previous in-process client could not delegate. golang.org/x/crypto
is no longer a direct dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
The deployment section (path, releases, shared paths, hooks) describes
the SSH releases-style strategy specifically. Future environment types
like PaaS will deploy in a completely different way with their own
settings, so the section moves from the environment level into the ssh
block:

  environments:
    production:
      type: ssh
      ssh:
        host: shop.example.com
        deployment:
          path: /var/www/shopware

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
Check fmt.Fprint results in the releases listing, collapse an else-if in
the rollback target validation and mark the intentional nil error in
EnvironmentStatus, where a non-zero exit of test -e means no release is
deployed rather than a failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
The remote branch of project clear-cache duplicated what resolveExecutor
already does (read config, resolve the environment, build the executor).
Branch on the resolved executor's type instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
Drop the ssh type check in project clear-cache: the executor abstraction
exists so callers do not dispatch on the environment type. With --env the
command now runs bin/console cache:clear through whatever executor the
environment resolves to (local, docker, ssh). Without --env the previous
behavior (admin API or removing var/cache) is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
The completion function received --env/-e as positional input because the
console command disables cobra flag parsing, so it completed against the
default environment and treated the flag as the console command name.
Apply the same flag extraction as the run path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ev2G1VYZ3kkAzJ3cCeGZtZ
Extends the executor abstraction with DatabaseConnection, so project dump
asks the environment for its database instead of assuming a locally
reachable server. Local, docker and symfony-cli environments keep the
existing behavior (defaults plus the project's Symfony env files); ssh
environments read the deployed release's env files (.env.dist, .env,
.env.local and the APP_ENV variants) for the DATABASE_URL and route the
connection through the ssh client.

The transport uses stdio forwarding (ssh -W) registered as a custom
go-sql-driver dial function: the configured address stays the database
address as seen from the server and is dialed through an ssh channel at
connect time. No local port is opened, connection flags (--host,
--username, ...) override the remote settings naturally, and every
connection - including parallel dumps - rides the multiplexed
ControlMaster connection. All dump features (anonymization, rewrites,
nodata, where) keep working since the dumper still runs locally.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants