Dive into Deployer Customizations and Github Actions

Keep it simple, stupid as everyone knows but another thing that we also know is that everything starts getting complex with scale. You know the drill. You’ve got 15  installations. Each one has its own deploy.php, its own GitHub Actions workflow, its own little deployment quirks that someone added at 11pm on a Thursday. They all started as copies of each other, back when life was simple and you only had three projects.

Now? They’ve drifted. Project A got that fix for the Redis flush order. Project B didn’t. Project C has a deploy script that nobody’s touched in eight months and everyone’s afraid to look at. Onboarding a new project means finding the “good” one (there’s always a “good” one), copying its deployment files, and praying you remembered to update the host references.

And then Shopware 6.7 drops. Time to update deployment routines across all fifteen projects. Individually. By hand. Fun.

There had to be a better way. Turns out, there was.

Two problems wearing a trench coat

Realizing that what we called “our deployment pipeline” are actually two completely different things pretending to be one.

Thing 1: The CI/CD orchestration. Check out code, validate the project, set up SSH keys, figure out which servers to deploy to. This is boring in the best way – it’s the same across every project and barely changes.

Thing 2: The deployment recipes. What actually happens on the server. Composer install, database migrations, theme compilation, plugin updates, cache clearing, service restarts. This is where the action is. It changes with framework versions, it needs per-project tweaks, and it’s where bugs actually live.

These two things change at different speeds, for different reasons, and they were tangled together in every project’s repo. So we untangled them.

The split: a GitHub Action and a Composer package

We separated the two concerns into two independently versioned pieces:

A reusable GitHub Action handles the CI/CD orchestration. Installation repos call it via workflow_call, and it takes care of everything that’s the same everywhere – setting up PHP, configuring SSH, running pre-deploy validation, and invoking Deployer. It’s versioned via git tags.

A Composer package contains the actual Deployer recipes – tasks, configuration, version-specific overrides. It knows how to deploy Shopware 6.6, 6.7 and eventually 6.8 when it lands, handles plugin management, theme compilation, service restarts, and all the other things that make Shopware deployments Shopware deployments. It’s versioned via semver and distributed through a private Composer repository.

The clever bit: the Action installs the Composer package at deploy time. It’s not a static dependency sitting in your project. The Action pulls it in fresh, at whatever version your project specifies.

How they talk to each other

Here’s what the flow looks like from 30,000 feet:

Installation repo pushes to main
        |
        v
Reusable GitHub Action (v1)
        |
        +---> Validates project (runs once)
        +---> Installs Deployer package (^1.0)
        +---> Reads hosts.yml from installation repo
        |
        v
Deployer package runs on server
        |
        +---> Detects Shopware version from composer.json
        +---> Loads version-specific recipes (6.6 or 6.7)
        +---> Loads project overrides (if any)
        +---> Deploys

The Action is the conductor. It doesn’t know or care what happens on the server – that’s the package’s job. It just makes sure the right version of the package is installed and invokes dep deploy.

The Composer package is the musician. It reads the installation’s composer.json, figures out whether it’s dealing with Shopware 6.6 or 6.7, loads the right set of overrides, and executes the deployment tasks. If the project has a deploy-tasks.php with custom overrides, those get loaded too.

The installation repo just brings three things to the table:

  • hosts.yml – which servers to deploy to
  • A workflow file that calls the Action
  • composer.json – which the package reads to detect the framework version

That’s it. That’s the whole setup for a new project.

Versioning is the whole game

This is where the pattern really pays off. Two independent version numbers give you fine-grained control:

The Action version (@v1) controls the CI/CD layer. We maintain floating major tags, so @v1 always points to the latest v1.x.x. Installation repos get non-breaking CI/CD improvements automatically. Need to pin? Use @v1.3.0.

The Composer package version (deployer-ref: ^1.0) controls the recipes. Each installation repo declares its own constraint. This is critical during framework upgrades – when we shipped Shopware 6.7 recipes, projects on 6.6 weren’t affected. They kept pulling recipes that worked for their version, because the package auto-detects and loads the right overrides.

A single release workflow ties both together. Push a semver tag like v1.3.0, and it creates a GitHub Release that works both as a git ref for the Action and as a Composer version for the package. One tag, two distribution channels.

# Installation workflow - this is the entire deployment config
jobs:
  deploy:
    uses: your-org/deployer/workflows/deploy.yml@v1
    with:
      stages: "production"
      repository: gi*@****ub.com:your-org/your-shop.git
      deployer-ref: ^1.0
    secrets:
      composer-auth: ${{ secrets.COMPOSER_AUTH_JSON }}
      deployer-private-key: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
      known-hosts: ${{ secrets.KNOWN_HOSTS }}

One project can stay on ^1.0 while another tests ^1.3. One project can pin to an exact version before a big launch while the rest float. Nobody has to move in lockstep.

Version-specific recipes: deploy 6.6 and 6.7 from the same package

The Composer package keeps version-specific overrides in a directory structure:

deployer/
  recipe/
    tasks.php           # shared tasks
    overrides/
      6.6/              # Shopware 6.6 specific
      6.7/              # Shopware 6.7 specific
  config/
    version-detection.php

When the package loads, it reads shopware/core or shopware/platform from the project’s composer.json, extracts the major.minor version, and loads the matching overrides. A 6.6 project gets 6.6 recipes. A 6.7 project gets 6.7 recipes. No configuration needed – it just works.

This meant our Shopware 6.7 upgrade was a package-level concern, not a per-project fire drill. We shipped new recipes, tested them on one project, and rolled them out. The 6.6 projects never noticed.

Per-project overrides: the escape hatch

Not every installation is identical. One shop needs a B2B platform build step. Another has custom systemd services. A third needs special file permissions for a PDF plugin.

The package handles this with a simple convention: if a deploy-tasks.php exists in the project root, it gets loaded after all the defaults. You can override tasks, add hooks, or extend the shared configuration without forking the package.

Defaults (from package)  →  Version overrides (6.6/6.7)  →  Project overrides (deploy-tasks.php)

Three layers, last one wins. Most projects don’t need the escape hatch, but when you do, it’s right there.

What this actually gets you

Since moving to this pattern:

Bug fixes ship once. Found a race condition in the cache clearing order? Fix it in the package, tag a release, done. Every project picks it up on their next deploy.

New project onboarding takes minutes, not hours. Drop in a hosts.yml, add a four-line workflow file, push. The Action and package handle the rest.

Framework upgrades are a package concern. Shopware 6.8 changed how themes compile? That’s a recipe change in the package, tested and released centrally. Not fifteen separate PRs across fifteen repos.

Per-project quirks are contained. Custom deployment steps live in deploy-tasks.php, not scattered through a 200-line workflow file that nobody wants to touch.

The pattern is the point

This isn’t a Shopware-specific trick. Any PHP agency running Deployer with GitHub Actions can split their deployment into these two layers. The key insight is simple: CI/CD orchestration and deployment recipes are separate concerns that deserve separate versioning.

The Action handles how deploys are triggered and orchestrated. The Composer package handles what happens when they run. Version them independently, and you get the control of per-project configuration with the maintainability of a single shared codebase.

Your future self – the one who just got a new client and needs to set up deployment by end of day – will thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *