From da0bec75c6126fa3c736056bbbb9e26ce1baffd4 Mon Sep 17 00:00:00 2001 From: "github-action[bot]" <> Date: Fri, 11 Sep 2026 11:51:13 +0000 Subject: [PATCH] Create Flex endpoint --- fyrst.shopware-cd.1.0.json | 2268 ++++++++++++++++++++++++++++++++++++ index.json | 20 + 2 files changed, 2288 insertions(+) create mode 100644 fyrst.shopware-cd.1.0.json create mode 100644 index.json diff --git a/fyrst.shopware-cd.1.0.json b/fyrst.shopware-cd.1.0.json new file mode 100644 index 00000000..d4c3901a --- /dev/null +++ b/fyrst.shopware-cd.1.0.json @@ -0,0 +1,2268 @@ +{ + "manifests": { + "fyrst/shopware-cd": { + "manifest": { + "copy-from-recipe": { + "root/": "" + }, + "post-install-output": [ + " * fyrst/shopware-cd", + " Flex copied CI, .env.example, .dockerignore,", + " and deploy/ (CD Compose + VPS helpers) into the shop root.", + "", + " This recipe requires shopware/docker, which copies", + " docker/Dockerfile. CI and Compose default to that file", + " (DOCKERFILE=docker/Dockerfile).", + "", + " It does not copy shop-root compose.yaml, .gitignore,", + " or .shopware-project.yaml \u2014 those come from", + " shopware-cli project create.", + "", + " CD Compose files live under deploy/:", + " deploy/compose.yaml, deploy/compose.prod.yaml,", + " deploy/compose.vps.yaml. From the shop root:", + " docker compose --project-directory . -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml", + "", + " VPS runtime copy (no S3): deploy/sync-runtime.sh plus", + " deploy/sync.env.example (see deploy/sync-runtime.md).", + " Bind-mounted host dirs default to", + " /var/lib/shopware/data/{files,media,thumbnail,theme,sitemap}", + " (SHOPWARE_DATA_ROOT in .env). On staging, copy to", + " deploy/sync.env and pull live \u2192 lower.", + "", + " Copy .env.example \u2192 .env yourself (Flex never writes .env).", + " Commit the copied files; vendor/ is gitignored.", + "", + " GitHub Actions picks up", + " .github/workflows/cd.yaml. GitLab still defaults to", + " .gitlab-ci.yml: set Settings \u2192 CI/CD \u2192 CI/CD configuration file", + " to .gitlab-ci.yaml (or copy/symlink that name).", + "", + " Refresh later with composer recipes:update fyrst/shopware-cd.", + " See https://github.com/fyrst-dev/shopware-cd" + ] + }, + "files": { + "root/.dockerignore": { + "contents": [ + "# Overlay .dockerignore \u2014 keep the Shopware project, drop secrets and", + "# paths that shopware-cli project ci rebuilds or that must never enter the image.", + "", + ".git", + ".gitattributes", + ".github", + ".gitlab-ci.yml", + ".gitlab-ci.yaml", + ".gitignore", + "", + "# Local / prod env and Composer auth (passed as BuildKit secrets instead)", + ".env", + ".env.*", + "!.env.example", + "auth.json", + "**/*.pem", + "**/*.key", + "", + "# Rebuilt during `shopware-cli project ci`", + "/vendor", + "/node_modules", + "", + "# Generated / state (runtime bind mounts on the host, not the image)", + "/var", + "/files", + "/public/bundles", + "/public/theme", + "/public/media", + "/public/thumbnail", + "/public/sitemap", + "/config/jwt", + "", + "# Editor / OS", + ".idea", + ".vscode", + ".DS_Store", + "Thumbs.db", + "*.swp", + "*.bak", + "", + "# Overlay docs and VPS helpers are not needed inside the image", + "# (CD Compose lives under /deploy; shop-root compose.yaml is from project create)", + "*.md", + "/deploy", + "compose.yaml", + "compose.prod.yaml", + "compose.override.yaml", + "compose.override.yml", + "docker-compose*.yml", + "docker-compose*.yaml", + "Makefile", + "", + "# Tests (also list extra cleanup in shop-root .shopware-project.yaml if needed)", + "/tests", + ".phpunit.result.cache", + ".php-cs-fixer.cache", + ".phpstan.cache", + "" + ], + "executable": false + }, + "root/.env.example": { + "contents": [ + "# Copy to `.env` (local) and to the VPS `.env` (mode 0600). Never commit filled values.", + "# Compose interpolates ${VAR} from this file. Shopware reads the same names at runtime.", + "#", + "# Generate APP_SECRET with: openssl rand -hex 32", + "# First-install admin vars are consumed by shopware-deployment-helper only on a fresh DB.", + "", + "# --- Image (Compose + CD) ----------------------------------------------------", + "# TODO: registry/repo for this shop, e.g. ghcr.io/fyrst-dev/shop-name", + "IMAGE=", + "IMAGE_TAG=latest", + "# From shopware/docker (Flex copies docker/Dockerfile)", + "DOCKERFILE=docker/Dockerfile", + "PHP_VERSION=8.3", + "", + "# --- Shopware runtime -----------------------------------------------------------", + "APP_ENV=prod", + "APP_URL=", + "APP_SECRET=", + "# Optional: INSTANCE_ID=", + "", + "# mysql://USER:PASSWORD@HOST:3306/DATABASE", + "# Bundled service hostname is `mysql`. For DBaaS, use the provider host and", + "# remove/disable the `mysql` service in deploy/compose.yaml.", + "DATABASE_URL=", + "# DATABASE_SSL_CA=", + "# DATABASE_SSL_CERT=", + "# DATABASE_SSL_KEY=", + "# DATABASE_SSL_DONT_VERIFY_SERVER_CERT=", + "", + "# --- Bundled MySQL service (compose service `mysql`) -----------------------", + "# Unused when DATABASE_URL points at an external database.", + "MYSQL_DATABASE=shopware", + "MYSQL_USER=", + "MYSQL_PASSWORD=", + "MYSQL_ROOT_PASSWORD=", + "", + "# --- First install (deployment helper; ignored on updates) --------------------", + "INSTALL_LOCALE=en-GB", + "INSTALL_CURRENCY=EUR", + "INSTALL_ADMIN_USERNAME=", + "INSTALL_ADMIN_PASSWORD=", + "INSTALL_ADMIN_EMAIL=", + "# Defaults to APP_URL when empty", + "SALES_CHANNEL_URL=", + "", + "# --- Optional Redis (compose profile `redis`) ------------------------------", + "# REDIS_URL=redis://redis:6379", + "# PHP_SESSION_HANDLER=redis", + "# PHP_SESSION_SAVE_PATH=tcp://redis:6379", + "", + "# --- Optional HTTP publish (local / VPS behind a reverse proxy) ---------------", + "HTTP_PORT=8000", + "", + "# --- Optional VPS smoke check (deploy/vps-release.sh) ------------------------", + "# SMOKE_URL=http://127.0.0.1:8000", + "", + "# --- Optional Compose profiles on the VPS (comma-separated) ------------------", + "# COMPOSE_PROFILES=redis,worker,scheduler", + "", + "# --- VPS runtime files (bind mounts; first-class default) --------------------", + "# Host dirs mapped into the container. Per-shop if several stacks share a VPS.", + "# Compose mounts {files,media,thumbnail,theme,sitemap} from this root.", + "SHOPWARE_DATA_ROOT=/var/lib/shopware/data", + "", + "# --- Optional VPS runtime data sync (deploy/sync-runtime.sh) -----------------", + "# Copy deploy/sync.env.example \u2192 deploy/sync.env (mode 0600). SSH and SYNC_ENV", + "# live there, not here. Object storage is out of scope for this VPS path.", + "# SYNC_ENV=staging", + "" + ], + "executable": false + }, + "root/.github/workflows/cd.yaml": { + "contents": [ + "# fyrst.dev Shopware CD \u2014 GitHub Actions", + "#", + "# Stages (same as .gitlab-ci.yaml): build \u2192 push extra tags \u2192 deploy.", + "# shopware-cli runs only inside docker/Dockerfile (shopware-cli image), not on the runner.", + "#", + "# Copied into shops by the Symfony Flex recipe (`copy-from-recipe` \u2192 shop root).", + "# Preflight runs CD only for a real Shopware shop (bin/console / shopware/*),", + "# never for the fyrst/shopware-cd Packagist library itself.", + "#", + "# ---------------------------------------------------------------------------", + "# Required secrets (Settings \u2192 Secrets and variables \u2192 Actions)", + "# ---------------------------------------------------------------------------", + "# SHOPWARE_PACKAGES_TOKEN packages.shopware.com (BuildKit secret packages_token)", + "# COMPOSER_AUTH optional JSON object for private Composer repos", + "#", + "# Registry \u2014 GHCR default (GITHUB_TOKEN). For another registry set:", + "# REGISTRY_USERNAME", + "# REGISTRY_PASSWORD", + "# Optional variables (not secrets):", + "# REGISTRY default ghcr.io", + "# REGISTRY_IMAGE default ghcr.io//", + "# DOCKERFILE default docker/Dockerfile (from shopware/docker)", + "#", + "# Compose / VPS deploy (DEPLOY_TARGET empty or \"compose\"):", + "# SSH_PRIVATE_KEY deploy key (no passphrase)", + "# VPS_HOST hostname or IP \u2014 set per shop, do not hardcode here", + "# VPS_USER", + "# VPS_PATH absolute checkout path on the VPS", + "# SSH_KNOWN_HOSTS recommended (ssh-keyscan output). If unset, keyscan is used.", + "# VPS_SSH_PORT optional, default 22 (repository variable)", + "# Compose files (via deploy/vps-release.sh, --project-directory = shop root):", + "# deploy/compose.yaml, deploy/compose.prod.yaml, deploy/compose.vps.yaml", + "#", + "# Optional:", + "# DEPLOY_TARGET repository variable: \"managed\" skips SSH Compose", + "# (see deploy/managed/README.md)", + "# SMOKE_URL public URL CI can GET after deploy", + "#", + "# Never put real tokens, keys, or hostnames in this file.", + "", + "name: CD", + "", + "on:", + " push:", + " branches: [main]", + " tags: [\"v*\"]", + " pull_request:", + " branches: [main]", + " workflow_dispatch:", + " inputs:", + " image_tag:", + " description: \"Override IMAGE_TAG (default: github.sha)\"", + " required: false", + "", + "concurrency:", + " group: cd-${{ github.workflow }}-${{ github.ref }}", + " cancel-in-progress: ${{ github.ref_name != 'main' }}", + "", + "permissions:", + " contents: read", + " packages: write", + "", + "env:", + " REGISTRY: ${{ vars.REGISTRY != '' && vars.REGISTRY || 'ghcr.io' }}", + " IMAGE: ${{ vars.REGISTRY_IMAGE != '' && vars.REGISTRY_IMAGE || format('ghcr.io/{0}', github.repository) }}", + " DOCKERFILE: ${{ vars.DOCKERFILE != '' && vars.DOCKERFILE || 'docker/Dockerfile' }}", + " PHP_VERSION: \"8.3\"", + "", + "jobs:", + " preflight:", + " name: Preflight", + " runs-on: ubuntu-latest", + " outputs:", + " is_shop: ${{ steps.check.outputs.is_shop }}", + " steps:", + " - uses: actions/checkout@v4", + " - id: check", + " run: |", + " set -euo pipefail", + " is_shop=false", + " if [ -f composer.json ] && grep -q '\"name\": *\"fyrst/shopware-cd\"' composer.json; then", + " echo \"fyrst/shopware-cd package \u2014 skip shop CD.\"", + " elif [ -f bin/console ]; then", + " is_shop=true", + " elif [ -f composer.json ] && grep -Eq '\"shopware/' composer.json; then", + " is_shop=true", + " else", + " echo \"Not a Shopware shop \u2014 skip CD.\"", + " fi", + " echo \"is_shop=${is_shop}\" >> \"$GITHUB_OUTPUT\"", + "", + " build:", + " name: Build", + " needs: [preflight]", + " if: needs.preflight.outputs.is_shop == 'true'", + " runs-on: ubuntu-latest", + " timeout-minutes: 60", + " steps:", + " - name: Checkout", + " uses: actions/checkout@v4", + "", + " - name: Set up Docker Buildx", + " uses: docker/setup-buildx-action@v3", + "", + " - name: Log in to registry", + " if: github.event_name != 'pull_request'", + " uses: docker/login-action@v3", + " with:", + " registry: ${{ env.REGISTRY }}", + " username: ${{ secrets.REGISTRY_USERNAME || github.actor }}", + " password: ${{ secrets.REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}", + "", + " - name: Write Composer auth.json", + " env:", + " COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}", + " run: |", + " if [ -n \"$COMPOSER_AUTH\" ]; then", + " printf '%s\\n' \"$COMPOSER_AUTH\" > auth.json", + " else", + " printf '%s\\n' '{}' > auth.json", + " fi", + "", + " - name: Build and push :sha", + " uses: docker/build-push-action@v6", + " with:", + " context: .", + " file: ${{ env.DOCKERFILE }}", + " push: ${{ github.event_name != 'pull_request' }}", + " tags: ${{ env.IMAGE }}:${{ github.sha }}", + " platforms: linux/amd64", + " provenance: false", + " build-args: |", + " PHP_VERSION=${{ env.PHP_VERSION }}", + " secrets: |", + " packages_token=${{ secrets.SHOPWARE_PACKAGES_TOKEN }}", + " secret-files: |", + " composer_auth=auth.json", + " cache-from: type=gha", + " cache-to: type=gha,mode=max", + "", + " push:", + " name: Push", + " needs: [preflight, build]", + " if: needs.preflight.outputs.is_shop == 'true' && github.event_name != 'pull_request'", + " runs-on: ubuntu-latest", + " timeout-minutes: 15", + " steps:", + " - name: Log in to registry", + " uses: docker/login-action@v3", + " with:", + " registry: ${{ env.REGISTRY }}", + " username: ${{ secrets.REGISTRY_USERNAME || github.actor }}", + " password: ${{ secrets.REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}", + "", + " - name: Set up Docker Buildx", + " uses: docker/setup-buildx-action@v3", + "", + " - name: Tag :latest on main", + " if: github.ref == 'refs/heads/main'", + " run: docker buildx imagetools create -t \"${IMAGE}:latest\" \"${IMAGE}:${GITHUB_SHA}\"", + "", + " - name: Tag :semver on v* tags", + " if: startsWith(github.ref, 'refs/tags/v')", + " run: |", + " set -euo pipefail", + " VERSION=\"${GITHUB_REF_NAME#v}\"", + " docker buildx imagetools create -t \"${IMAGE}:${VERSION}\" \"${IMAGE}:${GITHUB_SHA}\"", + " MAJOR_MINOR=\"${VERSION%.*}\"", + " docker buildx imagetools create -t \"${IMAGE}:${MAJOR_MINOR}\" \"${IMAGE}:${GITHUB_SHA}\"", + "", + " deploy:", + " name: Deploy (Compose / VPS)", + " needs: [preflight, push]", + " if: >", + " needs.preflight.outputs.is_shop == 'true' &&", + " github.event_name != 'pull_request' &&", + " (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) &&", + " vars.DEPLOY_TARGET != 'managed'", + " runs-on: ubuntu-latest", + " timeout-minutes: 30", + " # TODO: rename or remove if the shop does not use GitHub Environments", + " environment: production", + " env:", + " IMAGE_TAG: ${{ github.event.inputs.image_tag || github.sha }}", + " VPS_HOST: ${{ secrets.VPS_HOST }}", + " VPS_USER: ${{ secrets.VPS_USER }}", + " VPS_PATH: ${{ secrets.VPS_PATH }}", + " VPS_SSH_PORT: ${{ vars.VPS_SSH_PORT != '' && vars.VPS_SSH_PORT || '22' }}", + " SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}", + " SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}", + " SMOKE_URL: ${{ secrets.SMOKE_URL }}", + " steps:", + " - name: Deploy over SSH", + " run: |", + " set -euo pipefail", + " : \"${VPS_HOST:?Set secret VPS_HOST}\"", + " : \"${VPS_USER:?Set secret VPS_USER}\"", + " : \"${VPS_PATH:?Set secret VPS_PATH}\"", + " : \"${SSH_PRIVATE_KEY:?Set secret SSH_PRIVATE_KEY}\"", + "", + " mkdir -p ~/.ssh", + " chmod 700 ~/.ssh", + " printf '%s\\n' \"$SSH_PRIVATE_KEY\" > ~/.ssh/id_deploy", + " chmod 600 ~/.ssh/id_deploy", + "", + " if [ -n \"${SSH_KNOWN_HOSTS:-}\" ]; then", + " printf '%s\\n' \"$SSH_KNOWN_HOSTS\" > ~/.ssh/known_hosts", + " chmod 600 ~/.ssh/known_hosts", + " else", + " echo \"SSH_KNOWN_HOSTS unset; using ssh-keyscan (TODO: pin known_hosts)\" >&2", + " ssh-keyscan -p \"$VPS_SSH_PORT\" -H \"$VPS_HOST\" >> ~/.ssh/known_hosts", + " fi", + "", + " ssh -i ~/.ssh/id_deploy -p \"$VPS_SSH_PORT\" -o IdentitiesOnly=yes \\", + " \"${VPS_USER}@${VPS_HOST}\" \\", + " \"set -euo pipefail", + " cd '${VPS_PATH}'", + " git fetch --quiet origin", + " git checkout --detach '${IMAGE_TAG}'", + " export IMAGE='${IMAGE}' IMAGE_TAG='${IMAGE_TAG}' COMPOSE_DIR='${VPS_PATH}' SMOKE_URL='${SMOKE_URL:-}'", + " bash ./deploy/vps-release.sh\"", + "", + " - name: Optional CI-side smoke", + " if: env.SMOKE_URL != ''", + " run: |", + " set -euo pipefail", + " for i in $(seq 1 20); do", + " if curl -fsS \"$SMOKE_URL\" >/dev/null; then", + " echo \"Smoke OK\"", + " exit 0", + " fi", + " sleep 3", + " done", + " echo \"Smoke check failed for ${SMOKE_URL}\" >&2", + " exit 1", + "", + " deploy_managed:", + " name: Deploy (managed host)", + " needs: [preflight, push]", + " if: >", + " needs.preflight.outputs.is_shop == 'true' &&", + " github.event_name != 'pull_request' &&", + " (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) &&", + " vars.DEPLOY_TARGET == 'managed'", + " runs-on: ubuntu-latest", + " timeout-minutes: 15", + " environment: production", + " steps:", + " - name: Managed deploy stub", + " env:", + " IMAGE_TAG: ${{ github.event.inputs.image_tag || github.sha }}", + " run: |", + " echo \"DEPLOY_TARGET=managed \u2014 same image, different last mile.\"", + " echo \"Image: ${IMAGE}:${IMAGE_TAG}\"", + " echo \"Replace this job with the host CLI/API. See deploy/managed/README.md.\"", + " echo \"Setup command stays:\"", + " echo \" vendor/bin/shopware-deployment-helper run --skip-theme-compile --skip-assets-install\"", + " exit 1", + "" + ], + "executable": false + }, + "root/.gitlab-ci.yaml": { + "contents": [ + "# fyrst.dev Shopware CD \u2014 GitLab CI", + "#", + "# Stages (same as .github/workflows/cd.yaml): build \u2192 push extra tags \u2192 deploy.", + "# shopware-cli runs only inside docker/Dockerfile (shopware-cli image), not on the runner.", + "#", + "# Copied into shops by the Symfony Flex recipe (`copy-from-recipe` \u2192 shop root).", + "# Workflow rules require `bin/console` so this Packagist library never runs", + "# shop CD (it has composer.json but no Shopware console).", + "#", + "# GitLab still looks for `.gitlab-ci.yml` by default. After Flex copies this file,", + "# set Settings \u2192 CI/CD \u2192 CI/CD configuration file to `.gitlab-ci.yaml`.", + "#", + "# ---------------------------------------------------------------------------", + "# CI/CD variables (Settings \u2192 CI/CD \u2192 Variables). Mask tokens. Never commit them.", + "# ---------------------------------------------------------------------------", + "# SHOPWARE_PACKAGES_TOKEN packages.shopware.com (BuildKit secret packages_token)", + "# COMPOSER_AUTH optional JSON for private Composer repos", + "#", + "# Registry \u2014 GitLab Container Registry is the default (CI_REGISTRY_*).", + "# For GHCR or another registry, set:", + "# REGISTRY e.g. ghcr.io", + "# REGISTRY_IMAGE e.g. ghcr.io/fyrst-dev/shop-name", + "# REGISTRY_USERNAME", + "# REGISTRY_PASSWORD", + "# DOCKERFILE default docker/Dockerfile (from shopware/docker)", + "#", + "# Compose / VPS (DEPLOY_TARGET empty or \"compose\"):", + "# SSH_PRIVATE_KEY file or masked variable (PEM)", + "# VPS_HOST", + "# VPS_USER", + "# VPS_PATH", + "# SSH_KNOWN_HOSTS recommended", + "# VPS_SSH_PORT optional, default 22", + "# Compose files (via deploy/vps-release.sh, --project-directory = shop root):", + "# deploy/compose.yaml, deploy/compose.prod.yaml, deploy/compose.vps.yaml", + "#", + "# Optional:", + "# DEPLOY_TARGET \"managed\" skips SSH Compose (see deploy/managed/README.md)", + "# SMOKE_URL", + "#", + "# Runner: build uses Docker-in-Docker (privileged). Swap to kaniko/buildah", + "# if the runner cannot run dind \u2014 keep the same docker/Dockerfile and tags.", + "", + "workflow:", + " rules:", + " - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"", + " exists:", + " - bin/console", + " - if: $CI_COMMIT_TAG", + " exists:", + " - bin/console", + " - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS", + " when: never", + " - if: $CI_COMMIT_BRANCH", + " exists:", + " - bin/console", + "", + "stages:", + " - build", + " - push", + " - deploy", + "", + "default:", + " interruptible: true", + "", + "variables:", + " DOCKER_TLS_CERTDIR: \"/certs\"", + " DOCKER_BUILDKIT: \"1\"", + " PHP_VERSION: \"8.3\"", + " DOCKERFILE: \"docker/Dockerfile\"", + "", + ".registry_env:", + " before_script:", + " - |", + " set -eu", + " export REGISTRY=\"${REGISTRY:-${CI_REGISTRY:-}}\"", + " export IMAGE=\"${REGISTRY_IMAGE:-${CI_REGISTRY_IMAGE}}\"", + " export REGISTRY_USERNAME=\"${REGISTRY_USERNAME:-${CI_REGISTRY_USER:-}}\"", + " export REGISTRY_PASSWORD=\"${REGISTRY_PASSWORD:-${CI_REGISTRY_PASSWORD:-}}\"", + " export DOCKERFILE=\"${DOCKERFILE:-docker/Dockerfile}\"", + " export SHOPWARE_PACKAGES_TOKEN=\"${SHOPWARE_PACKAGES_TOKEN:-}\"", + " : \"${IMAGE:?Set REGISTRY_IMAGE or use GitLab Container Registry}\"", + "", + "build:", + " stage: build", + " image: docker:27", + " services:", + " - docker:27-dind", + " timeout: 60m", + " needs: []", + " rules:", + " - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"", + " - if: $CI_COMMIT_BRANCH", + " - if: $CI_COMMIT_TAG", + " before_script:", + " - !reference [.registry_env, before_script]", + " - docker info", + " - |", + " if [ -n \"${REGISTRY:-}\" ] && [ \"${CI_PIPELINE_SOURCE}\" != \"merge_request_event\" ]; then", + " echo \"$REGISTRY_PASSWORD\" | docker login -u \"$REGISTRY_USERNAME\" --password-stdin \"$REGISTRY\"", + " fi", + " - docker buildx create --use --name fyrst-ci --driver docker-container || docker buildx use fyrst-ci", + " - docker buildx inspect --bootstrap", + " - |", + " if [ -n \"${COMPOSER_AUTH:-}\" ]; then", + " printf '%s\\n' \"$COMPOSER_AUTH\" > auth.json", + " else", + " printf '%s\\n' '{}' > auth.json", + " fi", + " script:", + " - |", + " set -eu", + " PUSH_FLAG=\"\"", + " CACHE_TO=\"\"", + " if [ \"$CI_PIPELINE_SOURCE\" != \"merge_request_event\" ]; then", + " PUSH_FLAG=\"--push\"", + " CACHE_TO=\"--cache-to type=registry,ref=${IMAGE}:buildcache,mode=max\"", + " fi", + " docker buildx build \\", + " --platform linux/amd64 \\", + " --file \"$DOCKERFILE\" \\", + " --build-arg PHP_VERSION=\"$PHP_VERSION\" \\", + " --secret id=packages_token,env=SHOPWARE_PACKAGES_TOKEN \\", + " --secret id=composer_auth,src=auth.json \\", + " --tag \"${IMAGE}:${CI_COMMIT_SHA}\" \\", + " --cache-from \"type=registry,ref=${IMAGE}:buildcache\" \\", + " $CACHE_TO \\", + " $PUSH_FLAG \\", + " .", + " after_script:", + " - rm -f auth.json", + "", + "push:", + " stage: push", + " image: docker:27", + " services:", + " - docker:27-dind", + " timeout: 15m", + " needs: [\"build\"]", + " rules:", + " - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"", + " when: never", + " - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH", + " - if: $CI_COMMIT_TAG", + " before_script:", + " - !reference [.registry_env, before_script]", + " - echo \"$REGISTRY_PASSWORD\" | docker login -u \"$REGISTRY_USERNAME\" --password-stdin \"$REGISTRY\"", + " - docker buildx create --use --name fyrst-ci --driver docker-container || docker buildx use fyrst-ci", + " script:", + " - |", + " set -eu", + " if [ \"${CI_COMMIT_BRANCH:-}\" = \"$CI_DEFAULT_BRANCH\" ]; then", + " docker buildx imagetools create -t \"${IMAGE}:latest\" \"${IMAGE}:${CI_COMMIT_SHA}\"", + " fi", + " if [ -n \"${CI_COMMIT_TAG:-}\" ]; then", + " VERSION=\"${CI_COMMIT_TAG#v}\"", + " docker buildx imagetools create -t \"${IMAGE}:${VERSION}\" \"${IMAGE}:${CI_COMMIT_SHA}\"", + " MAJOR_MINOR=\"${VERSION%.*}\"", + " docker buildx imagetools create -t \"${IMAGE}:${MAJOR_MINOR}\" \"${IMAGE}:${CI_COMMIT_SHA}\"", + " fi", + "", + "deploy:", + " stage: deploy", + " image: alpine:3.21", + " interruptible: false", + " timeout: 30m", + " needs: [\"push\"]", + " resource_group: production", + " rules:", + " - if: $DEPLOY_TARGET == \"managed\"", + " when: never", + " - if: $CI_COMMIT_TAG", + " - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH", + " before_script:", + " - !reference [.registry_env, before_script]", + " - apk add --no-cache openssh-client bash", + " - |", + " set -eu", + " : \"${VPS_HOST:?Set CI variable VPS_HOST}\"", + " : \"${VPS_USER:?Set CI variable VPS_USER}\"", + " : \"${VPS_PATH:?Set CI variable VPS_PATH}\"", + " : \"${SSH_PRIVATE_KEY:?Set CI variable SSH_PRIVATE_KEY}\"", + " mkdir -p ~/.ssh", + " chmod 700 ~/.ssh", + " printf '%s\\n' \"$SSH_PRIVATE_KEY\" > ~/.ssh/id_deploy", + " chmod 600 ~/.ssh/id_deploy", + " PORT=\"${VPS_SSH_PORT:-22}\"", + " if [ -n \"${SSH_KNOWN_HOSTS:-}\" ]; then", + " printf '%s\\n' \"$SSH_KNOWN_HOSTS\" > ~/.ssh/known_hosts", + " chmod 600 ~/.ssh/known_hosts", + " else", + " echo \"SSH_KNOWN_HOSTS unset; using ssh-keyscan (TODO: pin known_hosts)\" >&2", + " ssh-keyscan -p \"$PORT\" -H \"$VPS_HOST\" >> ~/.ssh/known_hosts", + " fi", + " script:", + " - |", + " set -euo pipefail", + " PORT=\"${VPS_SSH_PORT:-22}\"", + " IMAGE_TAG=\"${CI_COMMIT_SHA}\"", + " ssh -i ~/.ssh/id_deploy -p \"$PORT\" -o IdentitiesOnly=yes \\", + " \"${VPS_USER}@${VPS_HOST}\" \\", + " \"set -euo pipefail", + " cd '${VPS_PATH}'", + " git fetch --quiet origin", + " git checkout --detach '${CI_COMMIT_SHA}'", + " export IMAGE='${IMAGE}' IMAGE_TAG='${IMAGE_TAG}' COMPOSE_DIR='${VPS_PATH}' SMOKE_URL='${SMOKE_URL:-}'", + " bash ./deploy/vps-release.sh\"", + " - |", + " if [ -n \"${SMOKE_URL:-}\" ]; then", + " apk add --no-cache curl", + " for i in $(seq 1 20); do", + " if curl -fsS \"$SMOKE_URL\" >/dev/null; then", + " echo \"Smoke OK\"", + " exit 0", + " fi", + " sleep 3", + " done", + " echo \"Smoke check failed for ${SMOKE_URL}\" >&2", + " exit 1", + " fi", + "", + "deploy_managed:", + " stage: deploy", + " image: alpine:3.21", + " interruptible: false", + " timeout: 15m", + " needs: [\"push\"]", + " resource_group: production", + " rules:", + " - if: $DEPLOY_TARGET == \"managed\" && $CI_COMMIT_TAG", + " - if: $DEPLOY_TARGET == \"managed\" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH", + " before_script:", + " - !reference [.registry_env, before_script]", + " script:", + " - |", + " echo \"DEPLOY_TARGET=managed \u2014 same image, different last mile.\"", + " echo \"Image: ${IMAGE}:${CI_COMMIT_SHA}\"", + " echo \"Replace this job with the host CLI/API. See deploy/managed/README.md.\"", + " echo \"Setup command stays:\"", + " echo \" vendor/bin/shopware-deployment-helper run --skip-theme-compile --skip-assets-install\"", + " exit 1", + "" + ], + "executable": false + }, + "root/deploy/README.md": { + "contents": [ + "# fyrst.dev \u2014 primary deploy: Docker Compose on a VPS", + "#", + "# Locked process: https://app.clickup.com/90151931897/docs/2kyqjkzt-915", + "# Image is built in CI (`shopware-cli project ci`). This host only pulls and runs it.", + "", + "## Model", + "", + "- **web** \u2014 Shopware image (`ghcr.io/shopware/docker-base` + project artifact), port 8000", + "- **setup** \u2014 one-shot `shopware-deployment-helper` (profile `setup`)", + "- **mysql** \u2014 bundled in Compose, or delete the service and point `DATABASE_URL` at DBaaS", + "- **redis** / **worker** / **scheduler** \u2014 optional Compose profiles", + "", + "## One-time VPS bootstrap", + "", + "1. Install Docker Engine + Compose plugin. Do not install Shopware or PHP on the host.", + "2. Checkout this shop repo (read-only deploy key) to a path such as `/opt/shopware/`.", + " That path is `VPS_PATH` in CI.", + "3. Copy `.env.example` \u2192 `.env` and fill runtime secrets. `chmod 600 .env`.", + "4. Create `.env.prod` (may be empty) so `deploy/compose.prod.yaml` can mount it.", + "5. Set `IMAGE` to the registry repository CI pushes (example: `ghcr.io/fyrst-dev/shop-name`).", + "6. `docker login` to that registry on the VPS (or use a credential helper / `~/.docker/config.json`).", + "7. Put a reverse proxy in front of `HTTP_PORT` (TLS). Do not expose MySQL.", + "8. Store the previous image tag for rollback (the release script writes `.deployed-tag` / `.previous-tag`).", + "9. Runtime media/files bind-mount from `SHOPWARE_DATA_ROOT` (default `/var/lib/shopware/data`). Docker creates `{files,media,thumbnail,theme,sitemap}` on first up; `init-perm` chowns those dirs to uid 82.", + "", + "## CD sequence (what CI runs)", + "", + "`deploy/vps-release.sh` (from the checkout at `VPS_PATH`):", + "", + "1. Record the currently deployed tag as `.previous-tag`", + "2. `docker compose \u2026 pull` the new `:git-sha`", + "3. Start bundled `mysql` (if present) and optional profiles", + "4. Run setup **once**:", + "", + " ```bash", + " vendor/bin/shopware-deployment-helper run \\", + " --skip-theme-compile \\", + " --skip-assets-install", + " ```", + "", + " (via `docker compose --profile setup run --rm setup`)", + "5. Recreate `web` with `--no-build`", + "6. Optional `SMOKE_URL` check", + "", + "Manual equivalent:", + "", + "```bash", + "export IMAGE=ghcr.io/example-org/shop-name # TODO", + "export IMAGE_TAG=", + "", + "cd /opt/shopware/ # TODO: VPS_PATH", + "git fetch --quiet origin", + "git checkout --quiet \"$IMAGE_TAG\"", + "", + "bash ./deploy/vps-release.sh", + "```", + "", + "Compose files used (from the shop root, with `--project-directory .`):", + "", + "- `deploy/compose.yaml`", + "- `deploy/compose.prod.yaml`", + "- `deploy/compose.vps.yaml`", + "- `deploy/sync-runtime.sh` / `deploy/sync.env.example` / `deploy/sync-runtime.md` \u2014 live \u2192 lower runtime copy (no S3)", + "", + "shopware-cli project create owns shop-root `compose.yaml` (local). Do not point CD at that file.", + "", + "## Why skip theme/assets on deploy", + "", + "`shopware-cli project ci` already compiled them into the image. Rebuilding on the VPS is an anti-pattern (time + drift).", + "", + "## Fresh install vs update", + "", + "The helper detects a fresh database vs an existing shop:", + "", + "- **Fresh:** schema, admin user from `INSTALL_ADMIN_*`, sales channel from `APP_URL` / `SALES_CHANNEL_URL`, extensions", + "- **Update:** migrations when the Shopware version changed, extension sync, hooks", + "", + "## Rollback", + "", + "```bash", + "export IMAGE_TAG=$(cat .previous-tag)", + "bash ./deploy/vps-release.sh", + "```", + "", + "Keep the previous image physically on the host (`docker image prune` with care).", + "", + "## Runtime data sync (VPS, no S3)", + "", + "DB + media/files are **not** in git and **not** in the app image. They live in MySQL and in **bind-mounted host directories** (default `/var/lib/shopware/data/{files,media,thumbnail,theme,sitemap}`). Set `SHOPWARE_DATA_ROOT` in shop-root `.env` if several shops share a VPS. `mysql_data` / `redis_data` stay named volumes (copy the database with mysqldump, not `mysql_data`).", + "", + "`deploy/sync-runtime.sh` copies that runtime data **live \u2192 lower** (staging / playground / dev) with **SSH + mysqldump + rsync of those host dirs**. Volume tars are only a fallback (no rsync, or a leftover named volume). There is no S3/MinIO path in this recipe.", + "", + "See **[sync-runtime.md](sync-runtime.md)** for flags, cron, and safety.", + "", + "| Command | What it does |", + "| --- | --- |", + "| `sync --from live` | Pull dump + bind-mount dirs from a higher env onto **this** host |", + "| `snapshot` | Write a local snapshot under `SYNC_SNAPSHOT_DIR` (rsync of host dirs) |", + "| `restore --snapshot ` | Restore a local snapshot onto this host |", + "", + "**Direction:** run `sync` on the consumer (cron on staging). Never auto-push into live. `SYNC_ENV=live` refuses `sync`.", + "", + "```bash", + "# on staging", + "cd /opt/shopware/", + "cp deploy/sync.env.example deploy/sync.env # set SYNC_SSH_* , SYNC_ENV=staging", + "chmod 600 deploy/sync.env", + "", + "bash deploy/sync-runtime.sh sync --from live --data all", + "```", + "", + "`--data all` (default) is DB + bind-mount dirs; use `db` or `volumes` to limit. Both shops need this script in `deploy/` (Flex-update live as well as staging).", + "", + "Cron (staging):", + "", + "```cron", + "15 2 * * * cd /opt/shopware/staging && bash deploy/sync-runtime.sh sync --from live --data all", + "```", + "", + "Local snapshot / restore (same host, still no S3):", + "", + "```bash", + "bash deploy/sync-runtime.sh snapshot --data all", + "bash deploy/sync-runtime.sh restore --snapshot --data all", + "```", + "", + "After a live DB lands on staging, `.env` is left alone. Optional `SYNC_REWRITE_FROM_URL` / `SYNC_REWRITE_TO_URL` rewrites `sales_channel_domain.url`. Then set staging `APP_URL` as usual.", + "", + "## Required CI secrets (Compose path)", + "", + "See comments at the top of `.github/workflows/cd.yaml` and `.gitlab-ci.yaml`.", + "", + "Typical: `SSH_PRIVATE_KEY`, `VPS_HOST`, `VPS_USER`, `VPS_PATH`, `SSH_KNOWN_HOSTS`.", + "" + ], + "executable": false + }, + "root/deploy/compose.prod.yaml": { + "contents": [ + "# Production / VPS overrides. Used as:", + "# docker compose --project-directory . \\", + "# -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml ...", + "#", + "# Put secrets in `.env` (required) and optional extra keys in `.env.prod`.", + "# Create an empty `.env.prod` on the VPS if you do not need overrides.", + "# Never commit filled env files.", + "", + "services:", + " web:", + " restart: unless-stopped", + " env_file:", + " - .env", + " - .env.prod", + " ports:", + " # TODO: publish 8000 only on localhost and terminate TLS on a reverse proxy.", + " - \"${HTTP_PORT:-8000}:8000\"", + " deploy:", + " resources:", + " limits:", + " cpus: \"2\"", + " memory: 1G", + " reservations:", + " cpus: \"0.50\"", + " memory: 512M", + " logging:", + " driver: json-file", + " options:", + " max-size: \"10m\"", + " max-file: \"5\"", + "", + " mysql:", + " restart: unless-stopped", + " env_file:", + " - .env", + " - .env.prod", + " # Do not publish 3306 on a public interface.", + " ports: []", + " deploy:", + " resources:", + " limits:", + " cpus: \"1\"", + " memory: 1G", + " reservations:", + " memory: 256M", + "", + " redis:", + " restart: unless-stopped", + " deploy:", + " resources:", + " limits:", + " memory: 256M", + "", + " worker:", + " restart: unless-stopped", + " env_file:", + " - .env", + " - .env.prod", + " deploy:", + " replicas: 1", + " resources:", + " limits:", + " memory: 512M", + "", + " scheduler:", + " restart: unless-stopped", + " env_file:", + " - .env", + " - .env.prod", + "", + " setup:", + " env_file:", + " - .env", + " - .env.prod", + " restart: \"no\"", + "", + " init-perm:", + " restart: \"no\"", + "" + ], + "executable": false + }, + "root/deploy/compose.vps.yaml": { + "contents": [ + "# VPS-only Compose overlay. CI / deploy/vps-release.sh adds this file so the", + "# host never rebuilds the image (assets were compiled in CI).", + "#", + "# docker compose --project-directory . \\", + "# -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml pull", + "# docker compose --project-directory . \\", + "# -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml up -d --no-build", + "", + "services:", + " web:", + " image: ${IMAGE:?Set IMAGE}:${IMAGE_TAG:?Set IMAGE_TAG to the git SHA}", + " pull_policy: always", + "", + " setup:", + " image: ${IMAGE}:${IMAGE_TAG}", + " pull_policy: always", + "", + " init-perm:", + " image: ${IMAGE}:${IMAGE_TAG}", + " pull_policy: always", + "", + " worker:", + " image: ${IMAGE}:${IMAGE_TAG}", + " pull_policy: always", + "", + " scheduler:", + " image: ${IMAGE}:${IMAGE_TAG}", + " pull_policy: always", + "" + ], + "executable": false + }, + "root/deploy/compose.yaml": { + "contents": [ + "# CD Compose stack for fyrst.dev Shopware shops (VPS).", + "# shopware-cli project create owns shop-root compose.yaml (local).", + "# Production overrides: deploy/compose.prod.yaml", + "# VPS pull/no-build: deploy/compose.vps.yaml", + "# Local-only tweaks: compose.override.yaml at the shop root (do not commit secrets)", + "#", + "# Copy .env.example \u2192 .env at the shop root and fill values. No real secrets belong here.", + "#", + "# From the shop root:", + "# docker compose --project-directory . \\", + "# -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml ...", + "#", + "# Runtime files (media, theme, \u2026) are bind-mounted from", + "# ${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/{files,media,thumbnail,theme,sitemap}.", + "# mysql_data / redis_data stay named volumes. Set SHOPWARE_DATA_ROOT per shop if", + "# several stacks share a Docker host.", + "#", + "# Bundled MySQL: service name `mysql` (DATABASE_URL host = mysql).", + "# External DB: set DATABASE_URL to the DBaaS DSN, delete or comment out", + "# the `mysql` service, and drop it from depends_on.", + "#", + "# Optional profiles:", + "# redis \u2014 Redis 7", + "# setup \u2014 one-shot shopware-deployment-helper (CD / first install)", + "# worker \u2014 messenger:consume", + "# scheduler \u2014 scheduled-task:run", + "#", + "# Flex recipes cannot use YAML anchors; app services repeat image/build/mounts.", + "# TODO: set `name:` per shop if several stacks share a Docker host.", + "", + "name: shopware", + "", + "services:", + " web:", + " image: ${IMAGE:?Set IMAGE in .env}:${IMAGE_TAG:-latest}", + " build:", + " context: .", + " dockerfile: ${DOCKERFILE:-docker/Dockerfile}", + " args:", + " PHP_VERSION: ${PHP_VERSION:-8.3}", + " env_file:", + " - .env", + " volumes:", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/files:/var/www/html/files\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/theme:/var/www/html/public/theme\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/media:/var/www/html/public/media\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/thumbnail:/var/www/html/public/thumbnail\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/sitemap:/var/www/html/public/sitemap\"", + " ports:", + " - \"${HTTP_PORT:-8000}:8000\"", + " depends_on:", + " mysql:", + " condition: service_healthy", + " required: false", + " healthcheck:", + " test: [\"CMD-SHELL\", \"php -r 'exit(0);'\"]", + " interval: 10s", + " timeout: 5s", + " retries: 12", + " start_period: 30s", + "", + " # One-shot install/update. Never compiles themes/assets (those are in the image).", + " # docker compose --profile setup run --rm setup", + " setup:", + " image: ${IMAGE:?Set IMAGE in .env}:${IMAGE_TAG:-latest}", + " build:", + " context: .", + " dockerfile: ${DOCKERFILE:-docker/Dockerfile}", + " args:", + " PHP_VERSION: ${PHP_VERSION:-8.3}", + " env_file:", + " - .env", + " volumes:", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/files:/var/www/html/files\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/theme:/var/www/html/public/theme\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/media:/var/www/html/public/media\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/thumbnail:/var/www/html/public/thumbnail\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/sitemap:/var/www/html/public/sitemap\"", + " profiles: [\"setup\"]", + " restart: \"no\"", + " depends_on:", + " mysql:", + " condition: service_healthy", + " required: false", + " init-perm:", + " condition: service_completed_successfully", + " required: false", + " entrypoint:", + " [", + " \"php\",", + " \"vendor/bin/shopware-deployment-helper\",", + " \"run\",", + " \"--skip-theme-compile\",", + " \"--skip-assets-install\",", + " ]", + "", + " # Directory ownership for uid 82 (www-data in docker-base). Same profile as setup.", + " init-perm:", + " image: ${IMAGE:?Set IMAGE in .env}:${IMAGE_TAG:-latest}", + " build:", + " context: .", + " dockerfile: ${DOCKERFILE:-docker/Dockerfile}", + " args:", + " PHP_VERSION: ${PHP_VERSION:-8.3}", + " env_file:", + " - .env", + " volumes:", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/files:/var/www/html/files\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/theme:/var/www/html/public/theme\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/media:/var/www/html/public/media\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/thumbnail:/var/www/html/public/thumbnail\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/sitemap:/var/www/html/public/sitemap\"", + " profiles: [\"setup\"]", + " user: \"root\"", + " restart: \"no\"", + " entrypoint:", + " [", + " \"chown\",", + " \"82:82\",", + " \"/var/www/html/files\",", + " \"/var/www/html/public/theme\",", + " \"/var/www/html/public/media\",", + " \"/var/www/html/public/thumbnail\",", + " \"/var/www/html/public/sitemap\",", + " ]", + "", + " mysql:", + " image: mysql:8.4", + " # Shopware docs often use mariadb:11.4 \u2014 swap the image if you prefer MariaDB.", + " command:", + " - --character-set-server=utf8mb4", + " - --collation-server=utf8mb4_unicode_ci", + " env_file:", + " - .env", + " environment:", + " MYSQL_DATABASE: ${MYSQL_DATABASE:-shopware}", + " MYSQL_USER: ${MYSQL_USER:?Set MYSQL_USER in .env}", + " MYSQL_PASSWORD: ${MYSQL_PASSWORD:?Set MYSQL_PASSWORD in .env}", + " MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}", + " volumes:", + " - mysql_data:/var/lib/mysql", + " healthcheck:", + " test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"127.0.0.1\"]", + " interval: 5s", + " timeout: 5s", + " retries: 20", + " start_period: 20s", + "", + " redis:", + " image: redis:7-alpine", + " profiles: [\"redis\"]", + " command: [\"redis-server\", \"--appendonly\", \"yes\"]", + " volumes:", + " - redis_data:/data", + " healthcheck:", + " test: [\"CMD\", \"redis-cli\", \"ping\"]", + " interval: 5s", + " timeout: 3s", + " retries: 10", + "", + " worker:", + " image: ${IMAGE:?Set IMAGE in .env}:${IMAGE_TAG:-latest}", + " build:", + " context: .", + " dockerfile: ${DOCKERFILE:-docker/Dockerfile}", + " args:", + " PHP_VERSION: ${PHP_VERSION:-8.3}", + " env_file:", + " - .env", + " volumes:", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/files:/var/www/html/files\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/theme:/var/www/html/public/theme\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/media:/var/www/html/public/media\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/thumbnail:/var/www/html/public/thumbnail\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/sitemap:/var/www/html/public/sitemap\"", + " profiles: [\"worker\"]", + " depends_on:", + " mysql:", + " condition: service_healthy", + " required: false", + " entrypoint:", + " [", + " \"php\",", + " \"bin/console\",", + " \"messenger:consume\",", + " \"async\",", + " \"low_priority\",", + " \"--time-limit=300\",", + " \"--memory-limit=512M\",", + " ]", + "", + " scheduler:", + " image: ${IMAGE:?Set IMAGE in .env}:${IMAGE_TAG:-latest}", + " build:", + " context: .", + " dockerfile: ${DOCKERFILE:-docker/Dockerfile}", + " args:", + " PHP_VERSION: ${PHP_VERSION:-8.3}", + " env_file:", + " - .env", + " volumes:", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/files:/var/www/html/files\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/theme:/var/www/html/public/theme\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/media:/var/www/html/public/media\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/thumbnail:/var/www/html/public/thumbnail\"", + " - \"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/sitemap:/var/www/html/public/sitemap\"", + " profiles: [\"scheduler\"]", + " depends_on:", + " mysql:", + " condition: service_healthy", + " required: false", + " entrypoint: [\"php\", \"bin/console\", \"scheduled-task:run\"]", + "", + "volumes:", + " mysql_data:", + " redis_data:", + "" + ], + "executable": false + }, + "root/deploy/managed/README.md": { + "contents": [ + "# Optional deploy: managed container host", + "", + "Same Shopware **image** as the Compose/VPS path. Use `docker/Dockerfile` from `shopware/docker`. Do **not** add another Dockerfile.", + "", + "Locked process: [Shopware Create & Continuous Deploy](https://app.clickup.com/90151931897/docs/2kyqjkzt-915)", + "", + "## When to use", + "", + "The platform starts/restarts containers for you (managed Kubernetes-like runtimes, PaaS-style hosts, mittwald-style container hosting, etc.). CI still:", + "", + "1. Builds with `shopware-cli project ci` in the multi-stage `docker/Dockerfile`", + "2. Pushes `:sha` / `:latest` / `:semver`", + "3. Runs Deployment Helper as a one-shot/setup job against that image", + "", + "## What changes", + "", + "Only the **deploy job** (and maybe which registry you push to):", + "", + "- Push to the host\u2019s registry **or** let the host pull from yours", + "- Trigger their deploy API / CLI / UI instead of SSH + Compose", + "- Map runtime env (`APP_URL`, `DATABASE_URL`, `APP_SECRET`, `INSTALL_ADMIN_*`) in the host\u2019s secret store", + "", + "## CI switch", + "", + "Use **`DEPLOY_TARGET`** (repository variable, not a secret):", + "", + "| Value | Deploy job |", + "| --- | --- |", + "| unset / `compose` | Primary: SSH + Compose (`deploy/vps-release.sh`) |", + "| `managed` | Skip Compose SSH; run the managed job instead |", + "", + "(`DEPLY_TARGET` is a typo \u2014 do not use it.)", + "", + "GitHub: Actions variable `DEPLOY_TARGET`. GitLab: CI/CD variable `DEPLOY_TARGET`.", + "", + "## What to fill in per host (TODOs)", + "", + "- [ ] Registry URL the platform pulls from", + "- [ ] Deploy token / kubeconfig / host CLI credentials (CI secret)", + "- [ ] How to run the one-shot setup command with the **same** flags:", + "", + " ```bash", + " vendor/bin/shopware-deployment-helper run \\", + " --skip-theme-compile \\", + " --skip-assets-install", + " ```", + "", + "- [ ] Health/smoke URL after rollout", + "- [ ] Rollback: redeploy the previous `:sha` tag", + "", + "The managed jobs in `.github/workflows/cd.yaml` and `.gitlab-ci.yaml` are **stubs**: they fail with a clear message until you replace the script with the host\u2019s CLI. That is intentional \u2014 do not copy a fake happy-path.", + "", + "## Keep identical across hosts", + "", + "- `docker/Dockerfile` / `PHP_VERSION=8.3`", + "- `.shopware-project.yaml` (from `shopware-cli project create`, not this recipe)", + "- CD Compose under `deploy/` (`compose.yaml`, `compose.prod.yaml`, `compose.vps.yaml`)", + "- Image naming and tags", + "- Setup command (deployment helper + skip flags)", + "- Build-time secrets (`SHOPWARE_PACKAGES_TOKEN`, Composer auth)", + "" + ], + "executable": false + }, + "root/deploy/sync-runtime.md": { + "contents": [ + "# Runtime data sync (VPS, bind mounts, no object storage)", + "", + "Pull **database + Shopware runtime files** from another VPS onto this one. Typical direction: **live \u2192 staging / playground / dev**.", + "", + "This is **not** part of image CD. `deploy/vps-release.sh` is unchanged (pull image, setup helper, recreate `web`). Runtime files stay out of git and out of the Shopware app image (`/.dockerignore` already excludes `/deploy` and `/var`).", + "", + "Object storage (S3 and similar) is **out of scope** for this VPS path. Transfer is SSH + `mysqldump` + **rsync of host bind-mount directories**. Named-volume tars are only a fallback (no `rsync` on the consumer, or a leftover Docker volume from an older stack).", + "", + "## What is copied", + "", + "Default `--data all` (same as omitting `--data`):", + "", + "| Item | Mechanism |", + "| --- | --- |", + "| `db` | Logical SQL dump from the bundled compose `mysql` service (or `DATABASE_URL`) |", + "| `media` `files` `thumbnail` `theme` `sitemap` | Bind mounts under `${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/` |", + "", + "Not copied: `mysql_data` / `redis_data` named volumes (use `db` for SQL; Redis is ephemeral for this recipe). Do not put dumps in git.", + "", + "Set `SHOPWARE_DATA_ROOT` in shop-root `.env` when several shops share a host. If the **source** uses a non-default root, set `SYNC_REMOTE_DATA_ROOT` or `SYNC_LIVE_DATA_ROOT` in `deploy/sync.env`.", + "", + "## Host packages", + "", + "On **every** VPS that snapshots or restores:", + "", + "- Docker Engine + Compose v2 plugin", + "- bash", + "- OpenSSH client", + "- gzip", + "- **rsync** (primary copy path for bind-mount dirs)", + "", + "The SSH user must be able to run `docker` (typically the `docker` group) so restore can `chown` uid 82 via a one-shot Alpine container.", + "", + "## One-time setup (consumer)", + "", + "On staging (or playground/dev), not on live:", + "", + "1. Copy `deploy/sync.env.example` \u2192 `deploy/sync.env` and `chmod 600 deploy/sync.env`.", + "2. Set `SYNC_ENV=staging` (or `playground` / `dev`). **Never** set `SYNC_ENV=live` on a host you restore onto.", + "3. Fill `SYNC_SSH_*` and `SYNC_SSH_PATH` for the source (live checkout, e.g. `/opt/shopware/live`).", + "4. Install an SSH key that can log in to live **without a passphrase** (cron). Pin `known_hosts`.", + "5. Confirm shop-root `.env` has `IMAGE` and `SHOPWARE_DATA_ROOT` (compose interpolation). Sync does not read secrets from the script itself.", + "", + "Do not commit `deploy/sync.env` (add it to the shop `.gitignore`; that file is owned by `shopware-cli project create`).", + "", + "## Commands", + "", + "Run from the **shop root** (the script `cd`s to the parent of `deploy/`):", + "", + "```bash", + "bash deploy/sync-runtime.sh sync --from live --data all", + "bash deploy/sync-runtime.sh snapshot --data all", + "bash deploy/sync-runtime.sh restore --snapshot --data all", + "```", + "", + "| Flag | Meaning |", + "| --- | --- |", + "| `--from ` | Source for `sync` (e.g. `live`). Uses `SYNC_SSH_*` / `SYNC_LIVE_*` |", + "| `--data all\\|db\\|volumes` | Default `all`. `volumes` means the bind-mount dirs, not `mysql_data` |", + "| `--volume ` | Single dir for `export --data volumes` (`files`, `media`, \u2026) |", + "| `--snapshot ` | Snapshot directory name under `SYNC_SNAPSHOT_DIR` |", + "| `--yes` | Skip the overwrite prompt |", + "", + "### Cron (consumer)", + "", + "```cron", + "15 2 * * * cd /opt/shopware/staging && bash deploy/sync-runtime.sh sync --from live --data all", + "```", + "", + "Overlapping runs are blocked with `flock`.", + "", + "## After restore", + "", + "- The script tries `bin/console cache:clear` via compose `web` and **does not fail** if that errors.", + "- Optional `SYNC_REWRITE_FROM_URL` / `SYNC_REWRITE_TO_URL` rewrites `sales_channel_domain.url`.", + "- Bind-mount dirs are `chown -R 82:82` after copy so `www-data` in the Shopware image can write.", + "", + "## Safety", + "", + "- `sync` **refuses** `SYNC_ENV=live` / `prod` / `production`. Convention is pull-only onto the lower env.", + "- `restore` onto live is refused unless `SYNC_ALLOW_LIVE_RESTORE=1` (disaster recovery).", + "- Dumps contain customer data: `umask` is not forced here; keep `SYNC_SNAPSHOT_DIR` mode `700` on the host.", + "", + "## Named-volume leftover", + "", + "If a host still has `shopware_media` (etc.) from an older recipe and the bind-mount directory is missing, snapshot/export will tar that named volume once. New stacks use bind mounts only; do not add `files`/`media`/\u2026 back as named volumes in `deploy/compose.yaml`.", + "" + ], + "executable": false + }, + "root/deploy/sync-runtime.sh": { + "contents": [ + "#!/usr/bin/env bash", + "# VPS runtime data: mysqldump + rsync of bind-mounted host dirs over SSH. No S3.", + "#", + "# Commands: sync | snapshot | restore", + "# Direction for sync: higher env \u2192 this host (live \u2192 staging / playground / dev).", + "# Never auto-pushes into live.", + "#", + "# Shopware files live on the host under", + "# ${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/{files,media,thumbnail,theme,sitemap}", + "# (compose bind mounts). mysql_data / redis_data stay named volumes and are not", + "# copied here (DB is mysqldump).", + "#", + "# Copy deploy/sync.env.example \u2192 deploy/sync.env on the consumer and fill SYNC_SSH_*.", + "#", + "# bash deploy/sync-runtime.sh sync --from live --data all", + "# bash deploy/sync-runtime.sh snapshot --data all", + "# bash deploy/sync-runtime.sh restore --snapshot --data all", + "#", + "# Plumbing (used over SSH on the source; stdout is the payload):", + "# bash deploy/sync-runtime.sh export --data db", + "# bash deploy/sync-runtime.sh export --data volumes --volume media", + "", + "set -euo pipefail", + "", + "usage() {", + " cat <<'EOF'", + "Usage: deploy/sync-runtime.sh [options]", + "", + "Commands:", + " sync Pull DB + bind-mount dirs from a higher env onto this host", + " snapshot Write a local snapshot (mysqldump + rsync of host dirs)", + " restore Restore a local snapshot onto this host", + " export Plumbing: write a dump/tar to stdout (SSH fallback)", + "", + "Options:", + " --from Source env for sync (e.g. live)", + " --data all | db | volumes (default: all)", + " --volume Single bind-mount dir (files|media|thumbnail|theme|sitemap)", + " --snapshot Snapshot id for restore (directory name under SYNC_SNAPSHOT_DIR)", + " --yes Do not prompt", + " -h, --help", + "", + "Examples:", + " bash deploy/sync-runtime.sh sync --from live --data all", + " bash deploy/sync-runtime.sh snapshot --data all", + " bash deploy/sync-runtime.sh restore --snapshot 20260911T021500Z-live --data all", + "", + "No S3. Files stay on the VPS (rsync over SSH, or a local snapshot dir). Live", + "is never the destination of `sync`. Cron on the lower env (see", + "deploy/README.md and deploy/sync-runtime.md).", + "EOF", + "}", + "", + "log() { printf '==> %s\\n' \"$*\" >&2; }", + "die() { printf '%s\\n' \"$*\" >&2; exit 1; }", + "", + "COMMAND=\"\"", + "FROM_ENV=\"\"", + "DATA=\"all\"", + "VOLUME_KEY=\"\"", + "SNAPSHOT_ID=\"\"", + "YES=0", + "", + "while [[ $# -gt 0 ]]; do", + " case \"$1\" in", + " sync|snapshot|restore|export)", + " [[ -z \"$COMMAND\" ]] || die \"Multiple commands: $COMMAND and $1\"", + " COMMAND=\"$1\"", + " shift", + " ;;", + " --from)", + " FROM_ENV=\"${2:-}\"", + " [[ -n \"$FROM_ENV\" ]] || die \"--from requires an env name\"", + " shift 2", + " ;;", + " --data)", + " DATA=\"${2:-}\"", + " [[ -n \"$DATA\" ]] || die \"--data requires all|db|volumes\"", + " shift 2", + " ;;", + " --volume)", + " VOLUME_KEY=\"${2:-}\"", + " [[ -n \"$VOLUME_KEY\" ]] || die \"--volume requires a bind-mount dir name (files|media|thumbnail|theme|sitemap)\"", + " shift 2", + " ;;", + " --snapshot)", + " SNAPSHOT_ID=\"${2:-}\"", + " [[ -n \"$SNAPSHOT_ID\" ]] || die \"--snapshot requires an id\"", + " shift 2", + " ;;", + " --yes|-y)", + " YES=1", + " shift", + " ;;", + " -h|--help)", + " usage", + " exit 0", + " ;;", + " *)", + " die \"Unknown argument: $1\"", + " ;;", + " esac", + "done", + "", + "[[ -n \"$COMMAND\" ]] || { usage >&2; exit 1; }", + "", + "case \"$DATA\" in", + " all|db|volumes) ;;", + " *) die \"--data must be all, db, or volumes (got: $DATA)\" ;;", + "esac", + "", + "want_db() { [[ \"$DATA\" == all || \"$DATA\" == db ]]; }", + "want_volumes() { [[ \"$DATA\" == all || \"$DATA\" == volumes ]]; }", + "", + "COMPOSE_DIR=\"${COMPOSE_DIR:-$(cd \"$(dirname \"$0\")/..\" && pwd)}\"", + "cd \"$COMPOSE_DIR\"", + "", + "CI_IMAGE=\"${IMAGE:-}\"", + "CI_IMAGE_TAG=\"${IMAGE_TAG:-}\"", + "CI_PROFILES=\"${COMPOSE_PROFILES:-}\"", + "", + "if [[ -f .env ]]; then", + " set -a", + " # shellcheck disable=SC1091", + " source .env", + " set +a", + "fi", + "if [[ -f .env.prod ]]; then", + " set -a", + " # shellcheck disable=SC1091", + " source .env.prod", + " set +a", + "fi", + "", + "IMAGE=\"${CI_IMAGE:-${IMAGE:-}}\"", + "IMAGE_TAG=\"${CI_IMAGE_TAG:-${IMAGE_TAG:-}}\"", + "COMPOSE_PROFILES=\"${CI_PROFILES:-${COMPOSE_PROFILES:-}}\"", + "export IMAGE IMAGE_TAG", + "", + "SYNC_ENV_FILE=\"${SYNC_ENV_FILE:-$COMPOSE_DIR/deploy/sync.env}\"", + "if [[ -f \"$SYNC_ENV_FILE\" ]]; then", + " set -a", + " # shellcheck disable=SC1091", + " source \"$SYNC_ENV_FILE\"", + " set +a", + "fi", + "", + "SYNC_COMPOSE_PROJECT=\"${SYNC_COMPOSE_PROJECT:-shopware}\"", + "SYNC_VOLUMES=\"${SYNC_VOLUMES:-files,media,thumbnail,theme,sitemap}\"", + "SHOPWARE_DATA_ROOT=\"${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}\"", + "SYNC_REMOTE_DATA_ROOT=\"${SYNC_REMOTE_DATA_ROOT:-/var/lib/shopware/data}\"", + "SYNC_SNAPSHOT_DIR=\"${SYNC_SNAPSHOT_DIR:-$COMPOSE_DIR/.runtime-snapshots}\"", + "SYNC_KEEP_SNAPSHOTS=\"${SYNC_KEEP_SNAPSHOTS:-5}\"", + "SYNC_SSH_PORT=\"${SYNC_SSH_PORT:-22}\"", + "", + "COMPOSE=(", + " docker compose", + " --project-directory \"$COMPOSE_DIR\"", + " -f deploy/compose.yaml", + " -f deploy/compose.prod.yaml", + " -f deploy/compose.vps.yaml", + ")", + "", + "PROFILE_ARGS=()", + "IFS=',' read -ra RAW_PROFILES <<< \"${COMPOSE_PROFILES:-}\"", + "for p in \"${RAW_PROFILES[@]}\"; do", + " p=\"${p// /}\"", + " if [[ -z \"$p\" ]]; then", + " continue", + " fi", + " if [[ \"$p\" == \"setup\" ]]; then", + " continue", + " fi", + " PROFILE_ARGS+=(--profile \"$p\")", + "done", + "", + "has_service() {", + " \"${COMPOSE[@]}\" \"${PROFILE_ARGS[@]}\" config --services 2>/dev/null | grep -qx \"$1\"", + "}", + "", + "project_name() {", + " local n", + " n=\"$(\"${COMPOSE[@]}\" config 2>/dev/null | sed -n 's/^name:[[:space:]]*//p' | head -n1 || true)\"", + " n=\"${n:-$SYNC_COMPOSE_PROJECT}\"", + " printf '%s\\n' \"${n:-shopware}\"", + "}", + "", + "volume_list() {", + " local raw=\"$SYNC_VOLUMES\" item", + " IFS=',' read -ra items <<< \"$raw\"", + " for item in \"${items[@]}\"; do", + " item=\"${item// /}\"", + " [[ -n \"$item\" ]] && printf '%s\\n' \"$item\"", + " done", + "}", + "", + "host_data_dir() {", + " printf '%s/%s\\n' \"$SHOPWARE_DATA_ROOT\" \"$1\"", + "}", + "", + "docker_volume_name() {", + " printf '%s_%s\\n' \"$(project_name)\" \"$1\"", + "}", + "", + "has_named_volume() {", + " docker volume inspect \"$(docker_volume_name \"$1\")\" >/dev/null 2>&1", + "}", + "", + "env_rank() {", + " case \"$1\" in", + " live|prod|production) echo 0 ;;", + " staging|stage) echo 1 ;;", + " playground|preview) echo 2 ;;", + " dev|local|development) echo 3 ;;", + " *) echo 10 ;;", + " esac", + "}", + "", + "is_live_env() {", + " case \"$1\" in", + " live|prod|production) return 0 ;;", + " *) return 1 ;;", + " esac", + "}", + "", + "confirm() {", + " local msg=\"$1\"", + " if [[ \"$YES\" -eq 1 || \"${SYNC_ASSUME_YES:-0}\" == 1 ]]; then", + " return 0", + " fi", + " if [[ ! -t 0 ]]; then", + " log \"$msg (non-interactive; continuing)\"", + " return 0", + " fi", + " local ans", + " read -r -p \"$msg [y/N] \" ans", + " [[ \"$ans\" == y || \"$ans\" == Y || \"$ans\" == yes ]]", + "}", + "", + "acquire_lock() {", + " local lock=\"/tmp/shopware-sync-runtime.lock\"", + " if command -v flock >/dev/null 2>&1; then", + " exec 9>\"$lock\"", + " if ! flock -n 9; then", + " die \"Another sync-runtime.sh is running ($lock)\"", + " fi", + " fi", + "}", + "", + "urldecode() {", + " local s=\"${1//+/ }\"", + " printf '%b' \"${s//%/\\\\x}\"", + "}", + "", + "# Sets DB_USER DB_PASS DB_HOST DB_PORT DB_NAME from DATABASE_URL (mysql://).", + "parse_database_url() {", + " local url=\"${DATABASE_URL:-}\"", + " [[ -n \"$url\" ]] || die \"DATABASE_URL is empty\"", + " url=\"${url#mysql://}\"", + " url=\"${url#mysqli://}\"", + " url=\"${url%%\\?*}\"", + " local cred hostpart", + " cred=\"${url%%@*}\"", + " hostpart=\"${url#*@}\"", + " if [[ \"$url\" == \"$cred\" ]]; then", + " die \"DATABASE_URL must look like mysql://USER:PASSWORD@HOST:3306/DATABASE\"", + " fi", + " DB_USER=\"${cred%%:*}\"", + " if [[ \"$cred\" == *:* ]]; then", + " DB_PASS=\"$(urldecode \"${cred#*:}\")\"", + " else", + " DB_PASS=\"\"", + " fi", + " DB_NAME=\"${hostpart#*/}\"", + " DB_NAME=\"${DB_NAME%%/*}\"", + " hostpart=\"${hostpart%%/*}\"", + " if [[ \"$hostpart\" == \\[* ]]; then", + " DB_HOST=\"${hostpart#\\[}\"", + " DB_HOST=\"${DB_HOST%%]*}\"", + " DB_PORT=\"${hostpart##*]:}\"", + " [[ \"$DB_PORT\" == \"$hostpart\" ]] && DB_PORT=3306", + " elif [[ \"$hostpart\" == *:* ]]; then", + " DB_HOST=\"${hostpart%%:*}\"", + " DB_PORT=\"${hostpart##*:}\"", + " else", + " DB_HOST=\"$hostpart\"", + " DB_PORT=3306", + " fi", + "}", + "", + "wait_mysql() {", + " local i", + " for i in $(seq 1 60); do", + " if \"${COMPOSE[@]}\" exec -T mysql mysqladmin ping -h 127.0.0.1 --silent >/dev/null 2>&1; then", + " return 0", + " fi", + " sleep 2", + " done", + " die \"mysql did not become ready\"", + "}", + "", + "ensure_mysql_up() {", + " if has_service mysql; then", + " log \"Starting mysql\"", + " \"${COMPOSE[@]}\" up -d --no-build mysql", + " wait_mysql", + " fi", + "}", + "", + "# Dump SQL to stdout. Logs go to stderr.", + "dump_sql() {", + " if has_service mysql; then", + " ensure_mysql_up", + " : \"${MYSQL_DATABASE:?Set MYSQL_DATABASE}\"", + " : \"${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD}\"", + " \"${COMPOSE[@]}\" exec -T \\", + " -e MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" \\", + " mysql \\", + " mysqldump \\", + " -uroot \\", + " --single-transaction \\", + " --quick \\", + " --routines \\", + " --triggers \\", + " --no-tablespaces \\", + " --default-character-set=utf8mb4 \\", + " \"$MYSQL_DATABASE\"", + " return", + " fi", + " parse_database_url", + " docker run --rm --network host \\", + " -e MYSQL_PWD=\"$DB_PASS\" \\", + " mysql:8.4 \\", + " mysqldump \\", + " -h\"$DB_HOST\" \\", + " -P\"$DB_PORT\" \\", + " -u\"$DB_USER\" \\", + " --single-transaction \\", + " --quick \\", + " --routines \\", + " --triggers \\", + " --no-tablespaces \\", + " --default-character-set=utf8mb4 \\", + " \"$DB_NAME\"", + "}", + "", + "import_sql() {", + " if has_service mysql; then", + " ensure_mysql_up", + " : \"${MYSQL_DATABASE:?Set MYSQL_DATABASE}\"", + " : \"${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD}\"", + " \"${COMPOSE[@]}\" exec -T \\", + " -e MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" \\", + " mysql \\", + " mysql \\", + " -uroot \\", + " --default-character-set=utf8mb4 \\", + " --max-allowed-packet=512M \\", + " \"$MYSQL_DATABASE\"", + " return", + " fi", + " parse_database_url", + " docker run --rm -i --network host \\", + " -e MYSQL_PWD=\"$DB_PASS\" \\", + " mysql:8.4 \\", + " mysql \\", + " -h\"$DB_HOST\" \\", + " -P\"$DB_PORT\" \\", + " -u\"$DB_USER\" \\", + " --default-character-set=utf8mb4 \\", + " --max-allowed-packet=512M \\", + " \"$DB_NAME\"", + "}", + "", + "rewrite_urls() {", + " local from=\"${SYNC_REWRITE_FROM_URL:-}\" to=\"${SYNC_REWRITE_TO_URL:-}\"", + " [[ -n \"$from\" && -n \"$to\" ]] || return 0", + " log \"Rewriting sales_channel_domain URLs: $from \u2192 $to\"", + " local sql", + " sql=$(printf \"UPDATE sales_channel_domain SET url = REPLACE(url, '%s', '%s');\\n\" \\", + " \"${from//\\'/\\'\\'}\" \"${to//\\'/\\'\\'}\")", + " printf '%s' \"$sql\" | import_sql", + "}", + "", + "# Primary: tar/rsync the bind-mounted host dir. Fallback: named Docker volume.", + "dump_volume_tar() {", + " local key=\"$1\" host vol", + " host=\"$(host_data_dir \"$key\")\"", + " if [[ -d \"$host\" ]]; then", + " tar -C \"$host\" -czf - .", + " return", + " fi", + " vol=\"$(docker_volume_name \"$key\")\"", + " if has_named_volume \"$key\"; then", + " log \"Bind-mount dir $host missing; archiving named volume $vol\"", + " docker run --rm \\", + " -v \"$vol\":/volume:ro \\", + " alpine:3.20 \\", + " tar -C /volume -czf - .", + " return", + " fi", + " die \"No bind-mount dir ($host) and no Docker volume $vol for $key\"", + "}", + "", + "rsync_local_dir() {", + " local src=\"$1\" dest=\"$2\"", + " mkdir -p \"$dest\"", + " if command -v rsync >/dev/null 2>&1; then", + " rsync -a --delete \"$src/\" \"$dest/\"", + " return", + " fi", + " find \"$dest\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +", + " tar -C \"$src\" -cf - . | tar -C \"$dest\" -xf -", + "}", + "", + "restore_host_tar_stdin() {", + " local dest", + " dest=\"$(host_data_dir \"$1\")\"", + " mkdir -p \"$dest\"", + " find \"$dest\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +", + " tar -C \"$dest\" -xzf -", + "}", + "", + "snapshot_runtime_dir() {", + " local key=\"$1\" dest=\"$2/$key\" host", + " host=\"$(host_data_dir \"$key\")\"", + " if [[ -d \"$host\" ]]; then", + " log \"Rsync $host \u2192 $dest\"", + " rsync_local_dir \"$host\" \"$dest\"", + " return", + " fi", + " if has_named_volume \"$key\"; then", + " log \"Archiving named volume $key \u2192 $2/${key}.tar.gz\"", + " dump_volume_tar \"$key\" >\"$2/${key}.tar.gz\"", + " return", + " fi", + " log \"Skip $key (no host dir $host)\"", + "}", + "", + "restore_runtime_dir() {", + " local key=\"$1\" dir=\"$2\" dest", + " dest=\"$(host_data_dir \"$key\")\"", + " mkdir -p \"$dest\"", + " if [[ -d \"$dir/$key\" ]]; then", + " log \"Rsync snapshot $key \u2192 $dest\"", + " rsync_local_dir \"$dir/$key\" \"$dest\"", + " return", + " fi", + " if [[ -f \"$dir/${key}.tar.gz\" ]]; then", + " log \"Extracting $key tar \u2192 $dest\"", + " find \"$dest\" -mindepth 1 -maxdepth 1 -exec rm -rf {} +", + " tar -C \"$dest\" -xzf \"$dir/${key}.tar.gz\"", + " return", + " fi", + " log \"Skip $key (not in snapshot)\"", + "}", + "", + "stop_app() {", + " local svcs=()", + " has_service web && svcs+=(web)", + " has_service worker && svcs+=(worker)", + " has_service scheduler && svcs+=(scheduler)", + " if [[ ${#svcs[@]} -gt 0 ]]; then", + " log \"Stopping ${svcs[*]}\"", + " \"${COMPOSE[@]}\" \"${PROFILE_ARGS[@]}\" stop \"${svcs[@]}\" || true", + " fi", + "}", + "", + "start_app() {", + " log \"Starting web\"", + " \"${COMPOSE[@]}\" up -d --no-build --remove-orphans web", + " if [[ ${#PROFILE_ARGS[@]} -gt 0 ]]; then", + " \"${COMPOSE[@]}\" \"${PROFILE_ARGS[@]}\" up -d --no-build", + " fi", + "}", + "", + "chown_volumes() {", + " local key dest", + " while IFS= read -r key; do", + " dest=\"$(host_data_dir \"$key\")\"", + " mkdir -p \"$dest\"", + " log \"chown 82:82 $dest\"", + " docker run --rm \\", + " -v \"$dest\":/data \\", + " alpine:3.20 \\", + " chown -R 82:82 /data", + " done < <(volume_list)", + "}", + "", + "cache_clear() {", + " if ! has_service web; then", + " return 0", + " fi", + " log \"cache:clear\"", + " \"${COMPOSE[@]}\" exec -T web php bin/console cache:clear || log \"cache:clear failed (continuing)\"", + "}", + "", + "env_upper() {", + " printf '%s' \"$1\" | tr '[:lower:]' '[:upper:]' | tr '-' '_'", + "}", + "", + "snapshot_ids() {", + " [[ -d \"$SYNC_SNAPSHOT_DIR\" ]] || return 0", + " find \"$SYNC_SNAPSHOT_DIR\" -mindepth 1 -maxdepth 1 -type d -printf '%f\\n' | sort", + "}", + "", + "prune_snapshots() {", + " local keep=\"$SYNC_KEEP_SNAPSHOTS\"", + " [[ \"$keep\" =~ ^[0-9]+$ ]] || return 0", + " local -a ids=()", + " mapfile -t ids < <(snapshot_ids)", + " local extra=$(( ${#ids[@]} - keep ))", + " (( extra > 0 )) || return 0", + " local i", + " for ((i = 0; i < extra; i++)); do", + " log \"Pruning snapshot ${ids[i]}\"", + " rm -rf \"${SYNC_SNAPSHOT_DIR:?}/${ids[i]}\"", + " done", + "}", + "", + "from_ssh_host() {", + " local var=\"SYNC_$(env_upper \"$1\")_SSH_HOST\"", + " printf '%s\\n' \"${!var:-${SYNC_SSH_HOST:-}}\"", + "}", + "from_ssh_user() {", + " local var=\"SYNC_$(env_upper \"$1\")_SSH_USER\"", + " printf '%s\\n' \"${!var:-${SYNC_SSH_USER:-}}\"", + "}", + "from_ssh_port() {", + " local var=\"SYNC_$(env_upper \"$1\")_SSH_PORT\"", + " printf '%s\\n' \"${!var:-${SYNC_SSH_PORT:-22}}\"", + "}", + "from_ssh_identity() {", + " local var=\"SYNC_$(env_upper \"$1\")_SSH_IDENTITY\"", + " printf '%s\\n' \"${!var:-${SYNC_SSH_IDENTITY:-}}\"", + "}", + "from_ssh_path() {", + " local var=\"SYNC_$(env_upper \"$1\")_PATH\"", + " printf '%s\\n' \"${!var:-${SYNC_SSH_PATH:-}}\"", + "}", + "from_data_root() {", + " local var=\"SYNC_$(env_upper \"$1\")_DATA_ROOT\"", + " printf '%s\\n' \"${!var:-${SYNC_REMOTE_DATA_ROOT:-/var/lib/shopware/data}}\"", + "}", + "", + "ssh_rsh() {", + " local a out=\"\"", + " ssh_base \"$1\"", + " for a in \"${SSH_CMD[@]}\"; do", + " out+=\"$(printf '%q ' \"$a\")\"", + " done", + " printf '%s' \"${out% }\"", + "}", + "", + "rsync_from_remote() {", + " local from=\"$1\" key=\"$2\"", + " local host user remote dest", + " host=\"$(from_ssh_host \"$from\")\"", + " user=\"$(from_ssh_user \"$from\")\"", + " remote=\"$(from_data_root \"$from\")/$key\"", + " dest=\"$(host_data_dir \"$key\")\"", + " mkdir -p \"$dest\"", + " if command -v rsync >/dev/null 2>&1; then", + " log \"Rsync ${user}@${host}:${remote}/ \u2192 ${dest}/\"", + " rsync -a --delete -e \"$(ssh_rsh \"$from\")\" \\", + " \"${user}@${host}:${remote}/\" \"${dest}/\"", + " return", + " fi", + " log \"rsync not installed; streaming tar of $key from $from\"", + " remote_export \"$from\" \"--data volumes --volume $(printf '%q' \"$key\")\" \\", + " | restore_host_tar_stdin \"$key\"", + "}", + "", + "ssh_base() {", + " local from=\"$1\" port ident known", + " port=\"$(from_ssh_port \"$from\")\"", + " ident=\"$(from_ssh_identity \"$from\")\"", + " known=\"${SYNC_SSH_KNOWN_HOSTS:-}\"", + " SSH_CMD=(ssh -o BatchMode=yes -o IdentitiesOnly=yes)", + " SSH_CMD+=(-o ControlMaster=auto -o \"ControlPath=/tmp/shopware-sync-%C\" -o ControlPersist=30)", + " SSH_CMD+=(-p \"$port\")", + " if [[ -n \"$ident\" ]]; then", + " SSH_CMD+=(-i \"$ident\")", + " fi", + " if [[ -n \"$known\" ]]; then", + " SSH_CMD+=(-o \"UserKnownHostsFile=$known\" -o StrictHostKeyChecking=yes)", + " else", + " SSH_CMD+=(-o StrictHostKeyChecking=accept-new)", + " fi", + "}", + "", + "remote_export() {", + " local from=\"$1\" args=\"$2\" host user path", + " host=\"$(from_ssh_host \"$from\")\"", + " user=\"$(from_ssh_user \"$from\")\"", + " path=\"$(from_ssh_path \"$from\")\"", + " [[ -n \"$host\" ]] || die \"Set SYNC_SSH_HOST (or SYNC_$(env_upper \"$from\")_SSH_HOST) in deploy/sync.env\"", + " [[ -n \"$user\" ]] || die \"Set SYNC_SSH_USER in deploy/sync.env\"", + " [[ -n \"$path\" ]] || die \"Set SYNC_SSH_PATH in deploy/sync.env\"", + " ssh_base \"$from\"", + " \"${SSH_CMD[@]}\" \"${user}@${host}\" \\", + " \"set -euo pipefail; cd $(printf '%q' \"$path\"); bash ./deploy/sync-runtime.sh export $args\"", + "}", + "", + "cmd_export() {", + " case \"$DATA\" in", + " db)", + " dump_sql", + " ;;", + " volumes)", + " [[ -n \"$VOLUME_KEY\" ]] || die \"export --data volumes requires --volume \"", + " dump_volume_tar \"$VOLUME_KEY\"", + " ;;", + " all)", + " die \"export --data all is not streamed as one payload; use db or volumes\"", + " ;;", + " esac", + "}", + "", + "cmd_snapshot() {", + " acquire_lock", + " mkdir -p \"$SYNC_SNAPSHOT_DIR\"", + " local id ts envn", + " ts=\"$(date -u +%Y%m%dT%H%M%SZ)\"", + " envn=\"${SYNC_ENV:-local}\"", + " id=\"${SNAPSHOT_ID:-$ts-$envn}\"", + " local dir=\"$SYNC_SNAPSHOT_DIR/$id\"", + " mkdir -p \"$dir\"", + " log \"Snapshot $id \u2192 $dir\"", + " {", + " printf 'id=%s\\n' \"$id\"", + " printf 'env=%s\\n' \"$envn\"", + " printf 'created_at=%s\\n' \"$ts\"", + " printf 'data=%s\\n' \"$DATA\"", + " printf 'volumes=%s\\n' \"$SYNC_VOLUMES\"", + " printf 'data_root=%s\\n' \"$SHOPWARE_DATA_ROOT\"", + " } >\"$dir/meta.txt\"", + " if want_db; then", + " log \"Dumping database\"", + " dump_sql | gzip -c >\"$dir/db.sql.gz\"", + " fi", + " if want_volumes; then", + " local key", + " while IFS= read -r key; do", + " snapshot_runtime_dir \"$key\" \"$dir\"", + " done < <(volume_list)", + " fi", + " prune_snapshots", + " log \"Snapshot finished $id\"", + " printf '%s\\n' \"$id\"", + "}", + "", + "cmd_restore() {", + " acquire_lock", + " if [[ -z \"$SNAPSHOT_ID\" ]]; then", + " log \"Available snapshots in $SYNC_SNAPSHOT_DIR:\"", + " snapshot_ids || true", + " die \"restore requires --snapshot \"", + " fi", + " local dir=\"$SYNC_SNAPSHOT_DIR/$SNAPSHOT_ID\"", + " [[ -d \"$dir\" ]] || die \"Snapshot not found: $dir\"", + " local this_env=\"${SYNC_ENV:-}\"", + " if [[ -n \"$this_env\" ]] && is_live_env \"$this_env\"; then", + " if [[ \"${SYNC_ALLOW_LIVE_RESTORE:-0}\" != 1 ]]; then", + " die \"Refusing restore onto live/prod (set SYNC_ALLOW_LIVE_RESTORE=1 for disaster recovery)\"", + " fi", + " fi", + " confirm \"Overwrite runtime data on ${this_env:-this host} from snapshot $SNAPSHOT_ID?\" \\", + " || die \"Cancelled\"", + " stop_app", + " if want_db; then", + " [[ -f \"$dir/db.sql.gz\" ]] || die \"Snapshot has no db.sql.gz\"", + " log \"Importing database\"", + " gzip -dc \"$dir/db.sql.gz\" | import_sql", + " rewrite_urls", + " fi", + " if want_volumes; then", + " local key", + " while IFS= read -r key; do", + " restore_runtime_dir \"$key\" \"$dir\"", + " done < <(volume_list)", + " chown_volumes", + " fi", + " start_app", + " cache_clear", + " log \"Restore finished $SNAPSHOT_ID\"", + "}", + "", + "cmd_sync() {", + " acquire_lock", + " [[ -n \"$FROM_ENV\" ]] || die \"sync requires --from (e.g. --from live)\"", + " [[ -n \"${SYNC_ENV:-}\" ]] || die \"Set SYNC_ENV in deploy/sync.env (this host, e.g. staging)\"", + " if is_live_env \"$SYNC_ENV\"; then", + " die \"Refusing sync onto live/prod (pull on the lower env, never push into live)\"", + " fi", + " if [[ \"$FROM_ENV\" == \"$SYNC_ENV\" ]]; then", + " die \"--from ($FROM_ENV) is this host (SYNC_ENV=$SYNC_ENV)\"", + " fi", + " local from_rank to_rank", + " from_rank=\"$(env_rank \"$FROM_ENV\")\"", + " to_rank=\"$(env_rank \"$SYNC_ENV\")\"", + " if [[ \"$to_rank\" -le \"$from_rank\" ]]; then", + " die \"Refusing ${FROM_ENV} \u2192 ${SYNC_ENV} (only higher \u2192 lower, e.g. live \u2192 staging)\"", + " fi", + " # Fail closed before stopping services.", + " [[ -n \"$(from_ssh_host \"$FROM_ENV\")\" ]] || die \"Set SYNC_SSH_HOST (or SYNC_$(env_upper \"$FROM_ENV\")_SSH_HOST) in deploy/sync.env\"", + " [[ -n \"$(from_ssh_user \"$FROM_ENV\")\" ]] || die \"Set SYNC_SSH_USER in deploy/sync.env\"", + " [[ -n \"$(from_ssh_path \"$FROM_ENV\")\" ]] || die \"Set SYNC_SSH_PATH in deploy/sync.env\"", + " confirm \"Overwrite ${SYNC_ENV} runtime data with ${FROM_ENV} (${DATA})?\" || die \"Cancelled\"", + " stop_app", + " if want_db; then", + " log \"Streaming mysqldump from $FROM_ENV\"", + " remote_export \"$FROM_ENV\" \"--data db\" | import_sql", + " rewrite_urls", + " fi", + " if want_volumes; then", + " local key", + " while IFS= read -r key; do", + " log \"Syncing bind-mount dir $key from $FROM_ENV\"", + " rsync_from_remote \"$FROM_ENV\" \"$key\"", + " done < <(volume_list)", + " chown_volumes", + " fi", + " start_app", + " cache_clear", + " log \"Sync finished ${FROM_ENV} \u2192 ${SYNC_ENV} ($DATA)\"", + "}", + "", + "case \"$COMMAND\" in", + " export) cmd_export ;;", + " snapshot) cmd_snapshot ;;", + " restore) cmd_restore ;;", + " sync) cmd_sync ;;", + " *) die \"Unknown command: $COMMAND\" ;;", + "esac", + "" + ], + "executable": true + }, + "root/deploy/sync.env.example": { + "contents": [ + "# Copy to deploy/sync.env on the *consumer* (staging / playground / dev). chmod 600.", + "# Never commit filled values. Live does not need this file for `snapshot`.", + "#", + "# No S3 \u2014 mysqldump + rsync of bind-mounted host dirs over SSH (or a local snapshot).", + "#", + "# Typical: this host is staging, source is live.", + "# cp deploy/sync.env.example deploy/sync.env", + "# bash deploy/sync-runtime.sh sync --from live --data all", + "", + "# This machine's environment. Must be a *lower* env for `sync` (not live / prod).", + "SYNC_ENV=staging", + "", + "# --- SSH to the source (--from live uses these unless SYNC_LIVE_* is set) ------", + "# TODO: live VPS hostname or IP", + "SYNC_SSH_HOST=", + "SYNC_SSH_USER=", + "SYNC_SSH_PORT=22", + "# Optional private key path (mode 0600). Empty = ssh defaults / agent.", + "SYNC_SSH_IDENTITY=", + "# Optional known_hosts file (ssh-keyscan output). Empty = accept-new on first connect.", + "# SYNC_SSH_KNOWN_HOSTS=/etc/ssh/ssh_known_hosts", + "# Checkout on the source (that env's VPS_PATH). Must contain this recipe's deploy/.", + "SYNC_SSH_PATH=/opt/shopware/live", + "", + "# Optional per-source overrides (used when --from NAME matches):", + "# SYNC_LIVE_SSH_HOST=", + "# SYNC_LIVE_SSH_USER=", + "# SYNC_LIVE_SSH_PORT=22", + "# SYNC_LIVE_SSH_IDENTITY=", + "# SYNC_LIVE_PATH=/opt/shopware/live", + "# SYNC_LIVE_DATA_ROOT=/var/lib/shopware/data", + "# SYNC_STAGING_SSH_HOST=", + "# SYNC_STAGING_PATH=/opt/shopware/staging", + "", + "# Bind-mount dirs to copy (not mysql_data / redis_data \u2014 DB is mysqldump).", + "# Host path is ${SHOPWARE_DATA_ROOT:-/var/lib/shopware/data}/", + "SYNC_VOLUMES=files,media,thumbnail,theme,sitemap", + "", + "# Remote SHOPWARE_DATA_ROOT when the source uses a non-default path.", + "# SYNC_REMOTE_DATA_ROOT=/var/lib/shopware/data", + "", + "# Local snapshot directory (snapshot / restore). Not used as an object store.", + "SYNC_SNAPSHOT_DIR=/var/backups/shopware-runtime", + "SYNC_KEEP_SNAPSHOTS=5", + "", + "# Optional: rewrite live storefront URLs in sales_channel_domain after DB restore.", + "# SYNC_REWRITE_FROM_URL=https://www.example.com", + "# SYNC_REWRITE_TO_URL=https://staging.example.com", + "", + "# Disaster recovery only. `restore` onto live is refused unless this is 1.", + "# SYNC_ALLOW_LIVE_RESTORE=0", + "" + ], + "executable": false + }, + "root/deploy/vps-release.sh": { + "contents": [ + "#!/usr/bin/env bash", + "# Run on the VPS (or via SSH from CI) after the image has been pushed.", + "# Never builds the image and never compiles themes/assets.", + "#", + "# Required env:", + "# IMAGE registry/repo (e.g. ghcr.io/fyrst-dev/shop-name) \u2014 no real defaults", + "# IMAGE_TAG full git SHA (or a rollback tag)", + "# Optional:", + "# COMPOSE_DIR shop checkout (default: repository root next to deploy/)", + "# COMPOSE_PROFILES comma-separated: redis,worker,scheduler (never include \"setup\")", + "# SMOKE_URL HTTP URL to probe after up (e.g. http://127.0.0.1:8000)", + "#", + "# CI-exported IMAGE / IMAGE_TAG always win over .env (which often has IMAGE_TAG=latest).", + "#", + "# Compose files (shop root as --project-directory):", + "# deploy/compose.yaml, deploy/compose.prod.yaml, deploy/compose.vps.yaml", + "", + "set -euo pipefail", + "", + "COMPOSE_DIR=\"${COMPOSE_DIR:-$(cd \"$(dirname \"$0\")/..\" && pwd)}\"", + "cd \"$COMPOSE_DIR\"", + "", + "CI_IMAGE=\"${IMAGE:-}\"", + "CI_IMAGE_TAG=\"${IMAGE_TAG:-}\"", + "CI_SMOKE_URL=\"${SMOKE_URL:-}\"", + "CI_PROFILES=\"${COMPOSE_PROFILES:-}\"", + "", + "if [[ -f .env ]]; then", + " set -a", + " # shellcheck disable=SC1091", + " source .env", + " set +a", + "fi", + "if [[ -f .env.prod ]]; then", + " set -a", + " # shellcheck disable=SC1091", + " source .env.prod", + " set +a", + "fi", + "", + "IMAGE=\"${CI_IMAGE:-${IMAGE:-}}\"", + "IMAGE_TAG=\"${CI_IMAGE_TAG:-${IMAGE_TAG:-}}\"", + "SMOKE_URL=\"${CI_SMOKE_URL:-${SMOKE_URL:-}}\"", + "COMPOSE_PROFILES=\"${CI_PROFILES:-${COMPOSE_PROFILES:-}}\"", + "", + ": \"${IMAGE:?Set IMAGE to the registry repository}\"", + ": \"${IMAGE_TAG:?Set IMAGE_TAG to the git SHA (or previous tag for rollback)}\"", + "", + "export IMAGE IMAGE_TAG", + "", + "touch .env.prod", + "", + "COMPOSE=(", + " docker compose", + " --project-directory \"$COMPOSE_DIR\"", + " -f deploy/compose.yaml", + " -f deploy/compose.prod.yaml", + " -f deploy/compose.vps.yaml", + ")", + "", + "PROFILE_ARGS=()", + "IFS=',' read -ra RAW_PROFILES <<< \"${COMPOSE_PROFILES:-}\"", + "for p in \"${RAW_PROFILES[@]}\"; do", + " p=\"${p// /}\"", + " if [[ -z \"$p\" ]]; then", + " continue", + " fi", + " if [[ \"$p\" == \"setup\" ]]; then", + " echo \"COMPOSE_PROFILES must not include setup (the script runs that profile itself)\" >&2", + " exit 1", + " fi", + " PROFILE_ARGS+=(--profile \"$p\")", + "done", + "", + "has_service() {", + " \"${COMPOSE[@]}\" \"${PROFILE_ARGS[@]}\" config --services 2>/dev/null | grep -qx \"$1\"", + "}", + "", + "echo \"==> Deploying ${IMAGE}:${IMAGE_TAG} from ${COMPOSE_DIR}\"", + "", + "if [[ -f .deployed-tag ]]; then", + " cp .deployed-tag .previous-tag", + " echo \"==> Previous tag: $(cat .previous-tag)\"", + "fi", + "", + "echo \"==> Pulling images\"", + "\"${COMPOSE[@]}\" \"${PROFILE_ARGS[@]}\" pull", + "", + "if has_service mysql; then", + " echo \"==> Starting mysql\"", + " \"${COMPOSE[@]}\" up -d --no-build mysql", + "fi", + "", + "if has_service redis; then", + " echo \"==> Starting redis\"", + " \"${COMPOSE[@]}\" --profile redis up -d --no-build redis", + "fi", + "", + "echo \"==> One-shot setup (shopware-deployment-helper, skip theme/assets)\"", + "\"${COMPOSE[@]}\" --profile setup run --rm --no-build setup", + "", + "echo \"==> Recreating web (no build)\"", + "\"${COMPOSE[@]}\" up -d --no-build --remove-orphans web", + "", + "if [[ ${#PROFILE_ARGS[@]} -gt 0 ]]; then", + " echo \"==> Starting extra profiles: ${COMPOSE_PROFILES}\"", + " \"${COMPOSE[@]}\" \"${PROFILE_ARGS[@]}\" up -d --no-build", + "fi", + "", + "printf '%s\\n' \"$IMAGE_TAG\" > .deployed-tag", + "", + "if [[ -n \"${SMOKE_URL:-}\" ]]; then", + " echo \"==> Smoke ${SMOKE_URL}\"", + " ok=0", + " for _ in $(seq 1 30); do", + " if command -v curl >/dev/null 2>&1 && curl -fsS \"$SMOKE_URL\" >/dev/null; then", + " echo \"==> Smoke OK\"", + " ok=1", + " break", + " fi", + " sleep 2", + " done", + " if [[ \"$ok\" -ne 1 ]]; then", + " echo \"Smoke check failed for ${SMOKE_URL}\" >&2", + " exit 1", + " fi", + "fi", + "", + "echo \"==> Deploy finished ${IMAGE}:${IMAGE_TAG}\"", + "" + ], + "executable": true + } + }, + "ref": "ef8f53eb0e3086076a1e9868f1f16712e479d247" + } + } +} diff --git a/index.json b/index.json new file mode 100644 index 00000000..d95d35b2 --- /dev/null +++ b/index.json @@ -0,0 +1,20 @@ +{ + "aliases": [], + "recipes": { + "fyrst/shopware-cd": [ + "1.0" + ] + }, + "recipe-conflicts": [], + "versions": [], + "branch": "main", + "is_contrib": true, + "_links": { + "repository": "github.com/symfony/recipes-contrib", + "origin_template": "{package}:{version}@github.com/symfony/recipes-contrib:main", + "recipe_template": "https://raw.githubusercontent.com/symfony/recipes-contrib/flex/pull-2049/{package_dotted}.{version}.json", + "recipe_template_relative": "{package_dotted}.{version}.json", + "archived_recipes_template": "https://raw.githubusercontent.com/symfony/recipes-contrib/flex/pull-2049/archived/{package_dotted}/{ref}.json", + "archived_recipes_template_relative": "archived/{package_dotted}/{ref}.json" + } +}