Test, build and release a Shopware 6 Plugin with GitLab CI - Part 1 - release

Insider Blog

There are many ways to install Shopware 6 plugins. You can download them directly in the admin panel or install them with composer.

There is a detailed comparison in the official documentation.

As a developer and maintainer of themes, customizations, third part API's, etc., I'm focused on Static Plugins.

The workflow is simple:

  1. Create a plugin with bin/console plugin:create --static
  2. Require it with composer
  3. Build the project with shopware-cli

When we need the same plugin in more than one shop, we could create the same plugin more than ones, but this wouldn't be great for maintenance.

Extracting the plugin

Let's move the source code of our plugin to a separate repository. To make things easier for now, we make the repository public.

Download with git

We just need to tell composer where to find our plugin

<project-root>/composer.json
{
  "name": "shopware/production",
  "license": "MIT",
  "type": "project",
  "require": {
    "composer-runtime-api": "^2.0",
    "acme/sample-plugin": "^1.0",
    "shopware/administration": "*",
    "shopware/core": "6.6.10.2",
    "shopware/elasticsearch": "*",
    "shopware/storefront": "*",
    "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
      }
    },
    {
      "type": "git",
      "url": "https://<DOMAIN-NAME>/<group>/<repo>.git"
    }
  ],
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "prefer-stable": true,
  "config": {
    "allow-plugins": {
      "symfony/flex": true,
      "symfony/runtime": true
    },
    "optimize-autoloader": true,
    "sort-packages": true
  },
  "scripts": {
    "auto-scripts": {
      "assets:install": "symfony-cmd"
    },
    "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"
      ]
    }
  }
}

and require it with

composer req acme/sample-plugin

Yes... this is the downside. We need to use dev-master as a version

composer req acme/sample-plugin:dev-master
./composer.json has been updated
Running composer update acme/sample-plugin
Loading composer repositories with package information                                                                
Updating dependencies                                 
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking acme/sample-plugin (dev-master 294414d)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Syncing acme/sample-plugin (dev-master 294414d) into cache
  - Installing acme/sample-plugin (dev-master 294414d): Cloning 294414deb2 from cache
Generating optimized autoload files

Run composer recipes at any time to see the status of your Symfony recipes.

Executing script assets:install [OK]

Composer will use git to clone our repo and use the default branch and the commit hash to track the release.

This works, but we can do better.

Git tags

Let's tag our plugin with v1.0.0.

Make sure to set the version in composer.json.

<plugin-root>/composer.json
{
    "name": "acme/sample-plugin",
    "description": "acme/sample-plugin",
    "type": "shopware-platform-plugin",
    "version": "1.0.0",
    "license": "MIT",
    "require": {
        "shopware/core": "~6.6.0"
    },
    "extra": {
        "shopware-plugin-class": "Acme\\SamplePlugin",
        "label": {
            "de-DE": "Skeleton plugin",
            "en-GB": "Skeleton plugin"
        }
    },
    "autoload": {
        "psr-4": {
            "Acme\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Acme\\Tests\\": "tests/"
        }
    }
}
git tag v1.0.0
git push --tags

Now this will work:

composer req acme/sample-plugin
./composer.json has been updated                                                                                                            
Running composer update acme/sample-plugin
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking acme/sample-plugin (1.0.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Syncing acme/sample-plugin (1.0.0) into cache
  - Installing acme/sample-plugin (1.0.0): Cloning 294414deb2 from cache
Generating optimized autoload files

Run composer recipes at any time to see the status of your Symfony recipes.

Executing script assets:install [OK]

Using version ^1.0 for acme/sample-plugin

This is better, but we are still using git to fetch the plugin. We can do better.

GitLab Package registry

Here is where the GitLab part starts. For more details refer to the official documentation.

At this point, it doesn't matter if our project in public or not, because we will need to authenticate with the package registry anyway.

Let's release our v1.0.0 tag as a composer package.

curl --fail-with-body --data tag=v1.0.0 "https://__token__:<personal-access-token>@<DOMAIN-NAME>/api/v4/projects/<project_id>/packages/composer"

Now we need to update the repository information:

<project-root>/composer.json
{
  "name": "shopware/production",
  "license": "MIT",
  "type": "project",
  "require": {
    "composer-runtime-api": "^2.0",
    "acme/sample-plugin": "^1.0",
    "shopware/administration": "*",
    "shopware/core": "6.6.10.2",
    "shopware/elasticsearch": "*",
    "shopware/storefront": "*",
    "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
      }
    },
    {
      "type": "composer",
      "url": "https://<DOMAIN-NAME>/api/v4/api/v4/group/<group_id>/-/packages/composer/packages.json"
    }
  ],
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "prefer-stable": true,
  "config": {
    "allow-plugins": {
      "symfony/flex": true,
      "symfony/runtime": true
    },
    "optimize-autoloader": true,
    "sort-packages": true
  },
  "scripts": {
    "auto-scripts": {
      "assets:install": "symfony-cmd"
    },
    "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"
      ]
    }
  }
}

or by using the CLI:

composer config repositories.<group_id> composer https://<DOMAIN-NAME>/api/v4/group/<group_id>/-/packages/composer/packages.json

And setup GitLab credentials:

composer config gitlab-token.<DOMAIN-NAME> <personal_access_token>

You can read more about this process in the official documentation.

Now we require our package us usual:

composer req acme/sample-plugin
./composer.json has been updated
Running composer update acme/sample-plugin
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking acme/sample-plugin (1.0.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Downloading acme/sample-plugin (1.0.0)
  - Installing acme/sample-plugin (1.0.0): Extracting archive
Generating optimized autoload files

Run composer recipes at any time to see the status of your Symfony recipes.

Executing script assets:install [OK]

Using version ^1.0 for acme/sample-plugin

Awesome. Direct package download!

Why bother?

This is an excellent question. The main reason is package caching. When running in a CI/CD environment or docker build, caching packages can give you a massive performance boost.



Release pipeline

With manual tagging

This is a simple pipeline when you need to manually create and push a git tag.

Make sure to always update the version in composer.json

git tag <version>
git push --tags
.gitlab-ci.yml
stages:
  - release

deploy:
  image: alpine/curl
  stage: release
  script:
    - 'curl --fail-with-body --header "Job-Token: $CI_JOB_TOKEN" --data tag=$CI_COMMIT_TAG "${CI_API_V4_URL}/projects/$CI_PROJECT_ID/packages/composer"'
  environment: production
  rules:
    if: $CI_COMMIT_TAG

With semantic-release

It would be a lot easier if we could just push our changes and don't care about versioning and tagging.

semantic-release automates the whole package release workflow.

Please follow the GitLab authentication instruction before you continue to read.

<plugin-root>/.gitlab-ci.yml
stages:
  - release

release:
  stage: release
  image:
    name: ghcr.io/voxpupuli/semantic-release:25.0.0-latest
    entrypoint: [""]
  interruptible: true
  script:
    - /container-entrypoint.sh
  rules:
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: never
    - if: $CI_COMMIT_BRANCH
<plugin-root>/.releaserc.json
{
  "plugins": [
    "@semantic-release/commit-analyzer",
    [
      "semantic-release-replace-plugin",
      {
        "replacements": [
          {
            "files": ["composer.json"],
            "from": "version\": \".*\"",
            "to": "version\": \"${nextRelease.version}\""
          }
        ]
      }
    ],
    [
      "@semantic-release/git",
      {
        "assets": ["composer.json"],
        "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
      }
    ],
    [
      "@semantic-release/exec",
      {
        "publishCmd": "curl --fail-with-body --header \"Job-Token: ${process.env.CI_JOB_TOKEN}\" --data tag=${nextRelease.gitTag} ${process.env.CI_API_V4_URL}/projects/${process.env.CI_PROJECT_ID}/packages/composer"
      }
    ]
  ]
}

This will:

  1. Analise the commits from the last release to decide if a new version should be released
  2. Update the version in composer.json
  3. Commit the composer.json back into the repo
  4. Create a tag
  5. Release a composer package from this tag

Author

Robert Juzak , B.Sc.

Characteristics

released:

March 26, 2026

categories:

What moves us, DevOps

Tags:

DevOps, Open Source, Shopware
previous article