mirror of
https://github.com/symfony/recipes-contrib.git
synced 2026-09-16 09:36:26 +03:00
3461 lines
208 KiB
JSON
3461 lines
208 KiB
JSON
{
|
|
"manifests": {
|
|
"fyrst/shopware-cd": {
|
|
"manifest": {
|
|
"copy-from-recipe": {
|
|
"root/": ""
|
|
},
|
|
"env": {
|
|
"SHOPWARE_SHOP_ID": "",
|
|
"SHOPWARE_DEPLOY_ENV": "live",
|
|
"SHOPWARE_DATA_BASE": "/var/lib/shopware/data"
|
|
},
|
|
"post-install-output": [
|
|
" * <fg=blue>fyrst/shopware-cd</>",
|
|
" Flex copied CI, <comment>.env.example</comment>, <comment>.dockerignore</comment>,",
|
|
" and <comment>deploy/</comment> (CD Compose + VPS helpers) into the shop root.",
|
|
"",
|
|
" This recipe requires <comment>shopware/docker</comment>, which copies",
|
|
" <comment>docker/Dockerfile</comment>. CI and Compose default to that file",
|
|
" (<comment>DOCKERFILE=docker/Dockerfile</comment>).",
|
|
"",
|
|
" It does not copy shop-root <comment>compose.yaml</comment>, <comment>.gitignore</comment>,",
|
|
" or <comment>.shopware-project.yml</comment> (create\u2019s default;",
|
|
" <comment>.shopware-project.yaml</comment> is also accepted \u2014 do not rename)",
|
|
" \u2014 those come from <comment>shopware-cli project create</comment>.",
|
|
"",
|
|
" CD Compose files live under <comment>deploy/</comment>:",
|
|
" <comment>deploy/compose.yaml</comment>, <comment>deploy/compose.prod.yaml</comment>,",
|
|
" <comment>deploy/compose.vps.yaml</comment>. From the shop root:",
|
|
" <comment>docker compose --project-directory . -f deploy/compose.yaml -f deploy/compose.prod.yaml -f deploy/compose.vps.yaml</comment>",
|
|
"",
|
|
" VPS runtime copy (no S3): <comment>deploy/sync-runtime.sh</comment> plus",
|
|
" <comment>deploy/sync.env.example</comment> (see <comment>deploy/sync-runtime.md</comment>).",
|
|
" Bind-mounted host dirs default to",
|
|
" <comment>${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/{files,media,thumbnail,theme,sitemap}</comment>",
|
|
" (<comment>SHOPWARE_SHOP_ID</comment> and <comment>SHOPWARE_DEPLOY_ENV</comment> in <comment>.env</comment>;",
|
|
" optional <comment>SHOPWARE_DATA_BASE</comment>). Compose does not require",
|
|
" <comment>COMPOSE_PROJECT_NAME</comment> or <comment>SHOPWARE_DATA_ROOT</comment>.",
|
|
" WARNING: <comment>shopware-cli project create</comment> writes",
|
|
" <comment>COMPOSE_PROJECT_NAME=sw-shop-\u2026</comment> into shop-root",
|
|
" <comment>.env</comment> for local <comment>project dev</comment>. That env var",
|
|
" overrides Compose <comment>name:</comment>",
|
|
" (<comment>${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}</comment>).",
|
|
" On the VPS, comment out that line",
|
|
" (<comment>bash deploy/init-env.sh --vps</comment> or by hand).",
|
|
" Flex does not delete it on <comment>composer require</comment>",
|
|
" (create owns the local flow).",
|
|
" Same-host tag-and-load / air-gap: <comment>PULL_POLICY=never</comment>",
|
|
" and <comment>SKIP_PULL=1</comment> (or",
|
|
" <comment>bash deploy/vps-release.sh --skip-pull</comment>). Default",
|
|
" <comment>pull_policy</comment> is <comment>always</comment> for CI/VPS.",
|
|
" On staging, copy to",
|
|
" <comment>deploy/sync.env</comment> and pull live \u2192 lower.",
|
|
"",
|
|
" Local <comment>project dev</comment> (laptop): rsync those VPS trees into the",
|
|
" project with <comment>deploy/sync-runtime-local.sh --from live --data all</comment>",
|
|
" \u2014 not <comment>deploy/sync-runtime.sh</comment>. Laptop <comment>.env</comment> needs",
|
|
" <comment>SHOPWARE_SHOP_ID</comment> (same as live).",
|
|
"",
|
|
" Flex may append a <comment>###> fyrst/shopware-cd ###</comment>",
|
|
" SoT block to <comment>.env</comment> (empty shop id; no secrets).",
|
|
" It does not overwrite create\u2019s whole <comment>.env</comment>.",
|
|
" Then run <comment>bash deploy/init-env.sh --shop-id \u2026</comment>",
|
|
" (VPS: add <comment>--vps</comment>; see <comment>deploy/README</comment>).",
|
|
" Commit the copied files; <comment>vendor/</comment> is gitignored.",
|
|
"",
|
|
" GitHub Actions picks up",
|
|
" <comment>.github/workflows/cd.yaml</comment>. GitLab still defaults to",
|
|
" <comment>.gitlab-ci.yml</comment>: set Settings \u2192 CI/CD \u2192 CI/CD configuration file",
|
|
" to <comment>.gitlab-ci.yaml</comment> (or copy/symlink that name).",
|
|
"",
|
|
" shopware-cli project create writes <comment>.shopware-project.yml</comment>",
|
|
" (that extension is fine). shopware-cli accepts both",
|
|
" <comment>.yml</comment> and <comment>.yaml</comment>; do not rename.",
|
|
" Refresh later with <comment>composer recipes:update fyrst/shopware-cd</comment>.",
|
|
" 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 CLI-owned .shopware-project.yml / .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.",
|
|
"#",
|
|
"# After `composer require`, Flex may append a ###> fyrst/shopware-cd ### block",
|
|
"# (SHOPWARE_SHOP_ID empty, SHOPWARE_DEPLOY_ENV=live, SHOPWARE_DATA_BASE default).",
|
|
"# Finish shop-specific values with:",
|
|
"# bash deploy/init-env.sh --shop-id <slug>",
|
|
"# VPS (comment out create's COMPOSE_PROJECT_NAME=sw-shop-\u2026):",
|
|
"# bash deploy/init-env.sh --shop-id <slug> --vps",
|
|
"# Optional: --env live|staging|playground|dev --image <registry/repo>",
|
|
"# --generate-app-secret --dry-run",
|
|
"# init-env copies this file \u2192 .env when .env is missing, then merges **missing**",
|
|
"# keys without clobbering existing non-empty values. It does not overwrite the",
|
|
"# whole .env and does not invent MYSQL passwords or APP_URL.",
|
|
"#",
|
|
"# Generate APP_SECRET with: openssl rand -hex 32",
|
|
"# (or: bash deploy/init-env.sh --generate-app-secret)",
|
|
"# First-install admin vars are consumed by shopware-deployment-helper only on a fresh DB.",
|
|
"",
|
|
"# --- Image (Compose + CD) ----------------------------------------------------",
|
|
"# SHOPWARE_PACKAGES_TOKEN is a CI secret, not a Compose variable. Set it only",
|
|
"# if the shop uses packages.shopware.com (empty is fine \u2014 not required).",
|
|
"# 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",
|
|
"",
|
|
"# --- Identity (multi-shop + live/staging on one VPS) -------------------------",
|
|
"# Same SHOPWARE_SHOP_ID on live, staging, and the laptop. Deploy env differs",
|
|
"# per stack. Compose interpolates these directly (no COMPOSE_PROJECT_NAME or",
|
|
"# SHOPWARE_DATA_ROOT required):",
|
|
"#",
|
|
"# name: ${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV} # e.g. acme-live",
|
|
"# bind mounts:",
|
|
"# ${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/...",
|
|
"#",
|
|
"# SHOPWARE_SHOP_ID=acme",
|
|
"# SHOPWARE_DEPLOY_ENV=live # live | staging | playground | \u2026",
|
|
"#",
|
|
"SHOPWARE_SHOP_ID=",
|
|
"SHOPWARE_DEPLOY_ENV=",
|
|
"# Optional bind-mount prefix (Compose default /var/lib/shopware/data):",
|
|
"# SHOPWARE_DATA_BASE=/var/lib/shopware/data",
|
|
"# Create once on the VPS (uid 82 = www-data):",
|
|
"# mkdir -p \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/$SHOPWARE_SHOP_ID/$SHOPWARE_DEPLOY_ENV\"/{files,media,thumbnail,theme,sitemap}",
|
|
"# chown -R 82:82 \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/$SHOPWARE_SHOP_ID/$SHOPWARE_DEPLOY_ENV\"",
|
|
"# mysql/redis stay named volumes (mysql_data, redis_data).",
|
|
"# Optional script helpers (Compose does not require these).",
|
|
"# Leave unset: vps-release / sync derive the same formulas. If set, scripts prefer",
|
|
"# these values. Do not assign them empty (COMPOSE_PROJECT_NAME= would override",
|
|
"# Compose's derived project name).",
|
|
"#",
|
|
"# WARNING: shopware-cli project create writes COMPOSE_PROJECT_NAME=sw-shop-\u2026",
|
|
"# into the shop-root `.env` for local `project dev`. That env var **overrides**",
|
|
"# Compose `name:` (`${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}`). On the VPS,",
|
|
"# comment out that line (`bash deploy/init-env.sh --vps` or by hand) and rely",
|
|
"# on shop id + deploy env. Flex does **not** delete it on composer require",
|
|
"# (create owns the local flow).",
|
|
"# COMPOSE_PROJECT_NAME=acme-live",
|
|
"# SHOPWARE_DATA_ROOT=/var/lib/shopware/data/acme/live",
|
|
"",
|
|
"# --- 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",
|
|
"",
|
|
"# --- VPS image pull (deploy/compose.vps.yaml) --------------------------------",
|
|
"# Default pull_policy is always (CI/VPS after CI pushed IMAGE:IMAGE_TAG).",
|
|
"# Same-host tag-and-load / air-gap never pushed that tag, so pull fails.",
|
|
"# PULL_POLICY=never",
|
|
"# SKIP_PULL=1 # also skip `docker compose pull` in vps-release.sh",
|
|
"# # same as: bash deploy/vps-release.sh --skip-pull",
|
|
"# Leave unset on a real VPS that should pull from the registry.",
|
|
"# PULL_POLICY=never",
|
|
"",
|
|
"# --- Optional Compose profiles on the VPS (comma-separated) ------------------",
|
|
"# COMPOSE_PROFILES=redis,worker,scheduler",
|
|
"",
|
|
"# --- 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.",
|
|
"#",
|
|
"# ---------------------------------------------------------------------------",
|
|
"# CI secrets (Settings \u2192 Secrets and variables \u2192 Actions)",
|
|
"# ---------------------------------------------------------------------------",
|
|
"# Optional build-time (a shop can create/build/deploy without these):",
|
|
"# SHOPWARE_PACKAGES_TOKEN set only if the shop uses packages.shopware.com",
|
|
"# (BuildKit secret packages_token; empty is fine)",
|
|
"# COMPOSER_AUTH 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/<owner>/<repo>",
|
|
"# 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 }}",
|
|
" # Empty SHOPWARE_PACKAGES_TOKEN is fine (optional packages.shopware.com).",
|
|
" 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.",
|
|
"# ---------------------------------------------------------------------------",
|
|
"# Optional build-time (a shop can create/build/deploy without these):",
|
|
"# SHOPWARE_PACKAGES_TOKEN set only if the shop uses packages.shopware.com",
|
|
"# (BuildKit secret packages_token; empty is fine)",
|
|
"# COMPOSER_AUTH 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}\"",
|
|
" # Optional; empty if the shop does not use packages.shopware.com.",
|
|
" 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",
|
|
"- **identity** \u2014 Compose `name: ${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}` and bind mounts under `${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}` (no `COMPOSE_PROJECT_NAME` / `SHOPWARE_DATA_ROOT` required, no hardcoded `name: shopware`)",
|
|
"- **WARNING:** `shopware-cli project create` writes `COMPOSE_PROJECT_NAME=sw-shop-\u2026` into shop-root `.env` for local `project dev`. That env var **overrides** Compose `name:`. On the VPS, **comment out** that line with `bash deploy/init-env.sh --vps` (or by hand). Flex does not delete it on `composer require` (create owns the local flow). `deploy/vps-release.sh` warns when the value does not match shop id + deploy env.",
|
|
"",
|
|
"## Shop-root `.env` after create (Flex + `deploy/init-env.sh`)",
|
|
"",
|
|
"`shopware-cli project create` writes `.env`. Flex may append a marked block",
|
|
"(safe defaults only \u2014 empty shop id, no secrets):",
|
|
"",
|
|
"```bash",
|
|
"###> fyrst/shopware-cd ###",
|
|
"SHOPWARE_SHOP_ID=",
|
|
"SHOPWARE_DEPLOY_ENV=live",
|
|
"SHOPWARE_DATA_BASE=/var/lib/shopware/data",
|
|
"###< fyrst/shopware-cd ###",
|
|
"```",
|
|
"",
|
|
"```bash",
|
|
"bash deploy/init-env.sh --shop-id acme",
|
|
"# VPS: bash deploy/init-env.sh --shop-id acme --env live --vps --image ghcr.io/example/acme",
|
|
"bash deploy/init-env.sh --shop-id acme --vps --dry-run",
|
|
"```",
|
|
"",
|
|
"`--shop-id` is required unless already non-empty. The script copies",
|
|
"`.env.example` \u2192 `.env` when `.env` is missing, merges missing keys, and does",
|
|
"not invent MYSQL passwords or `APP_URL`.",
|
|
"",
|
|
"## 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/<shop>`.",
|
|
" That path is `VPS_PATH` in CI.",
|
|
"3. Finish `.env` with `bash deploy/init-env.sh --shop-id <slug> --vps` (Flex may already have appended SoT keys). `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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}`.",
|
|
" Set `SHOPWARE_SHOP_ID` (same on live + staging) and `SHOPWARE_DEPLOY_ENV` in shop-root `.env`.",
|
|
" Optional `SHOPWARE_DATA_BASE` (default `/var/lib/shopware/data`). Compose does not require",
|
|
" `COMPOSE_PROJECT_NAME` or `SHOPWARE_DATA_ROOT`.",
|
|
" **Do not copy create\u2019s `COMPOSE_PROJECT_NAME=sw-shop-\u2026` onto the VPS.**",
|
|
" That line overrides Compose `name:`. Comment it out with",
|
|
" `bash deploy/init-env.sh --vps` (or by hand). Local `shopware-cli project dev`",
|
|
" can keep it; Flex does not delete it on `composer require` (create owns",
|
|
" the local flow).",
|
|
" Docker creates `{files,media,thumbnail,theme,sitemap}` on first up; `init-perm` chowns those dirs to uid 82.",
|
|
" Do not hardcode Compose `name: shopware`.",
|
|
"",
|
|
"## 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` (skip with `SKIP_PULL=1` / `PULL_POLICY=never` / `--skip-pull`)",
|
|
"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 --pull never 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=<full-git-sha>",
|
|
"",
|
|
"cd /opt/shopware/<shop> # 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` \u2014 `pull_policy: ${PULL_POLICY:-always}` (CI/VPS default). Same-host tag-and-load / air-gap: `PULL_POLICY=never` and `SKIP_PULL=1` (or `bash deploy/vps-release.sh --skip-pull`) so Compose does not pull a tag that was never pushed.",
|
|
"- `deploy/sync-runtime.sh` / `deploy/sync.env.example` / `deploy/sync-runtime.md` \u2014 live \u2192 lower VPS runtime copy (no S3)",
|
|
"- `deploy/sync-runtime-local.sh` \u2014 live `SHOPWARE_DATA_ROOT` \u2192 local `project dev` paths (rsync)",
|
|
"",
|
|
"shopware-cli project create owns shop-root `compose.yaml` (local). Do not point CD at that file. Create writes `.shopware-project.yml` (fine as-is; shopware-cli also accepts `.yaml` \u2014 do not rename). `compose run` uses `--pull never` (Compose v5 dropped `--no-build` from the run subcommand). `up` uses `--no-build`.",
|
|
"",
|
|
"## 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**:",
|
|
"",
|
|
"```text",
|
|
"/var/lib/shopware/data/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/{files,media,thumbnail,theme,sitemap}",
|
|
"```",
|
|
"",
|
|
"Set `SHOPWARE_SHOP_ID` + `SHOPWARE_DEPLOY_ENV` in shop-root `.env`. Compose interpolates the project name and bind-mount paths from those two (plus optional `SHOPWARE_DATA_BASE`). Sync scripts still derive `COMPOSE_PROJECT_NAME` / `SHOPWARE_DATA_ROOT` when those helpers are unset. `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 <id>` | 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/acme-staging",
|
|
"# .env: SHOPWARE_SHOP_ID=acme SHOPWARE_DEPLOY_ENV=staging",
|
|
"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/acme-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 <id> --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.",
|
|
"",
|
|
"## Local laptop (`shopware-cli project dev`)",
|
|
"",
|
|
"VPS bind mounts are **not** the local CLI paths (the whole project is mounted). Pull live trees into the checkout:",
|
|
"",
|
|
"```bash",
|
|
"bash deploy/sync-runtime-local.sh --from live --data all",
|
|
"# optional: --delete --dry-run",
|
|
"shopware-cli project console cache:clear",
|
|
"```",
|
|
"",
|
|
"`--from live` is an SSH host (often `Host live` in `~/.ssh/config`). Map: `media` \u2192 `public/media/`, `files` \u2192 `files/`, plus thumbnail/theme/sitemap. Laptop `.env` needs `SHOPWARE_SHOP_ID` (same as live); remote default is `/var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live`. This is **not** `deploy/sync-runtime.sh`.",
|
|
"",
|
|
"## CI secrets (Compose path)",
|
|
"",
|
|
"See comments at the top of `.github/workflows/cd.yaml` and `.gitlab-ci.yaml`.",
|
|
"",
|
|
"Typical for deploy: `SSH_PRIVATE_KEY`, `VPS_HOST`, `VPS_USER`, `VPS_PATH`, `SSH_KNOWN_HOSTS`.",
|
|
"",
|
|
"`SHOPWARE_PACKAGES_TOKEN` is optional \u2014 set it only if the shop uses packages.shopware.com. Empty is fine.",
|
|
""
|
|
],
|
|
"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",
|
|
"#",
|
|
"# pull_policy default is always \u2014 correct after CI pushes IMAGE:IMAGE_TAG to a",
|
|
"# registry. Same-host tag-and-load (local E2E / air-gap) never pushed that tag,",
|
|
"# so pull fails. Override without a second compose file:",
|
|
"# PULL_POLICY=never",
|
|
"# SKIP_PULL=1 # also skip `docker compose pull` in vps-release.sh",
|
|
"# Leave unset on a real VPS that should pull from the registry.",
|
|
"",
|
|
"services:",
|
|
" web:",
|
|
" image: ${IMAGE:?Set IMAGE}:${IMAGE_TAG:?Set IMAGE_TAG to the git SHA}",
|
|
" pull_policy: ${PULL_POLICY:-always}",
|
|
"",
|
|
" setup:",
|
|
" image: ${IMAGE}:${IMAGE_TAG}",
|
|
" pull_policy: ${PULL_POLICY:-always}",
|
|
"",
|
|
" init-perm:",
|
|
" image: ${IMAGE}:${IMAGE_TAG}",
|
|
" pull_policy: ${PULL_POLICY:-always}",
|
|
"",
|
|
" worker:",
|
|
" image: ${IMAGE}:${IMAGE_TAG}",
|
|
" pull_policy: ${PULL_POLICY:-always}",
|
|
"",
|
|
" scheduler:",
|
|
" image: ${IMAGE}:${IMAGE_TAG}",
|
|
" pull_policy: ${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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/{files,media,thumbnail,theme,sitemap}.",
|
|
"# Compose interpolates SHOPWARE_SHOP_ID and SHOPWARE_DEPLOY_ENV (required via :?).",
|
|
"# SHOPWARE_DATA_BASE defaults to /var/lib/shopware/data.",
|
|
"# COMPOSE_PROJECT_NAME and SHOPWARE_DATA_ROOT are not required for this file.",
|
|
"# Do not set COMPOSE_PROJECT_NAME= (empty) in .env \u2014 that env var overrides name:.",
|
|
"# shopware-cli project create writes COMPOSE_PROJECT_NAME=sw-shop-\u2026 into .env",
|
|
"# for local project dev. On the VPS, remove or comment out that line.",
|
|
"# mysql_data / redis_data stay named volumes, scoped by the project name.",
|
|
"#",
|
|
"# 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.",
|
|
"# Project name is ${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV} (e.g. acme-live).",
|
|
"# Do not hardcode `name: shopware` \u2014 several shops and live+staging share a host.",
|
|
"",
|
|
"name: \"${SHOPWARE_SHOP_ID:?Set SHOPWARE_SHOP_ID in .env}-${SHOPWARE_DEPLOY_ENV:?Set SHOPWARE_DEPLOY_ENV in .env}\"",
|
|
"",
|
|
"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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/files:/var/www/html/files\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/theme:/var/www/html/public/theme\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/media:/var/www/html/public/media\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/thumbnail:/var/www/html/public/thumbnail\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/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 --pull never 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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/files:/var/www/html/files\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/theme:/var/www/html/public/theme\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/media:/var/www/html/public/media\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/thumbnail:/var/www/html/public/thumbnail\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/files:/var/www/html/files\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/theme:/var/www/html/public/theme\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/media:/var/www/html/public/media\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/thumbnail:/var/www/html/public/thumbnail\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/files:/var/www/html/files\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/theme:/var/www/html/public/theme\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/media:/var/www/html/public/media\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/thumbnail:/var/www/html/public/thumbnail\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/files:/var/www/html/files\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/theme:/var/www/html/public/theme\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/media:/var/www/html/public/media\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/thumbnail:/var/www/html/public/thumbnail\"",
|
|
" - \"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/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/init-env.sh": {
|
|
"contents": [
|
|
"#!/usr/bin/env bash",
|
|
"# Finish shop-root .env after shopware-cli project create + Flex.",
|
|
"#",
|
|
"# Flex may append a ###> fyrst/shopware-cd ### block with safe SoT defaults",
|
|
"# (empty SHOPWARE_SHOP_ID, SHOPWARE_DEPLOY_ENV=live, SHOPWARE_DATA_BASE).",
|
|
"# This script fills shop-specific values. It does not overwrite the whole",
|
|
"# file, does not invent MYSQL passwords / APP_URL, and does not put secrets",
|
|
"# in the Flex env block.",
|
|
"#",
|
|
"# Usage (from shop root, or COMPOSE_DIR=shop-root):",
|
|
"# bash deploy/init-env.sh --shop-id <slug> [--env live] [--vps] \u2026",
|
|
"#",
|
|
"# See deploy/README.md.",
|
|
"",
|
|
"set -euo pipefail",
|
|
"",
|
|
"case \"$-\" in",
|
|
" *x*)",
|
|
" printf 'ERROR: refusing to run with xtrace (credentials may be in .env)\\n' >&2",
|
|
" exit 1",
|
|
" ;;",
|
|
"esac",
|
|
"",
|
|
"umask 077",
|
|
"",
|
|
"SCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"",
|
|
"COMPOSE_DIR=\"${COMPOSE_DIR:-$(cd \"${SCRIPT_DIR}/..\" && pwd)}\"",
|
|
"",
|
|
"log() { printf '==> %s\\n' \"$*\"; }",
|
|
"err() { printf 'ERROR: %s\\n' \"$*\" >&2; }",
|
|
"die() { err \"$@\"; exit 1; }",
|
|
"",
|
|
"usage() {",
|
|
" cat <<'EOF'",
|
|
"Usage: bash deploy/init-env.sh [options]",
|
|
"",
|
|
"Finish shop-root .env after create + Flex. Merges missing keys from",
|
|
".env.example, sets shop identity, and can strip create's",
|
|
"COMPOSE_PROJECT_NAME=sw-shop-\u2026 line on a VPS.",
|
|
"",
|
|
"Does not overwrite the whole .env. Does not invent MYSQL passwords or",
|
|
"APP_URL. Flex env only ships safe defaults (empty shop id).",
|
|
"",
|
|
"Options:",
|
|
" --shop-id <slug> Required unless SHOPWARE_SHOP_ID is already non-empty",
|
|
" --env <name> live | staging | playground | dev (default: live",
|
|
" when unset/empty; existing non-empty value is kept)",
|
|
" --image <repo> Set IMAGE (registry/repo). Unset leaves IMAGE as-is",
|
|
" --vps Comment out COMPOSE_PROJECT_NAME=\u2026 lines (create",
|
|
" footgun). Does not leave an empty COMPOSE_PROJECT_NAME=",
|
|
" --generate-app-secret",
|
|
" Set APP_SECRET with openssl rand -hex 32 if empty",
|
|
" --dry-run Print the summary; do not write .env",
|
|
" -h, --help Show this help",
|
|
"",
|
|
"Environment:",
|
|
" COMPOSE_DIR Shop checkout (default: parent of deploy/)",
|
|
"",
|
|
"Examples:",
|
|
" bash deploy/init-env.sh --shop-id acme",
|
|
" bash deploy/init-env.sh --shop-id acme --env live --vps --image ghcr.io/example/acme",
|
|
" bash deploy/init-env.sh --shop-id acme --generate-app-secret",
|
|
" bash deploy/init-env.sh --shop-id acme --vps --dry-run",
|
|
"EOF",
|
|
"}",
|
|
"",
|
|
"need_value() {",
|
|
" local flag=$1",
|
|
" local value=${2:-}",
|
|
" if [[ -z \"$value\" || \"$value\" == --* ]]; then",
|
|
" die \"${flag} requires a value\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"SHOP_ID_FLAG=\"\"",
|
|
"ENV_FLAG=\"\"",
|
|
"IMAGE_FLAG=\"\"",
|
|
"VPS=0",
|
|
"GENERATE_SECRET=0",
|
|
"DRY_RUN=0",
|
|
"WORK=\"\"",
|
|
"ORIG=\"\"",
|
|
"",
|
|
"cleanup() {",
|
|
" rm -f \"${WORK:-}\" \"${ORIG:-}\"",
|
|
"}",
|
|
"trap cleanup EXIT",
|
|
"",
|
|
"while [[ $# -gt 0 ]]; do",
|
|
" case \"$1\" in",
|
|
" --shop-id)",
|
|
" need_value \"$1\" \"${2:-}\"",
|
|
" SHOP_ID_FLAG=$2",
|
|
" shift 2",
|
|
" ;;",
|
|
" --shop-id=*)",
|
|
" SHOP_ID_FLAG=\"${1#*=}\"",
|
|
" shift",
|
|
" ;;",
|
|
" --env)",
|
|
" need_value \"$1\" \"${2:-}\"",
|
|
" ENV_FLAG=$2",
|
|
" shift 2",
|
|
" ;;",
|
|
" --env=*)",
|
|
" ENV_FLAG=\"${1#*=}\"",
|
|
" shift",
|
|
" ;;",
|
|
" --image)",
|
|
" need_value \"$1\" \"${2:-}\"",
|
|
" IMAGE_FLAG=$2",
|
|
" shift 2",
|
|
" ;;",
|
|
" --image=*)",
|
|
" IMAGE_FLAG=\"${1#*=}\"",
|
|
" shift",
|
|
" ;;",
|
|
" --vps)",
|
|
" VPS=1",
|
|
" shift",
|
|
" ;;",
|
|
" --generate-app-secret)",
|
|
" GENERATE_SECRET=1",
|
|
" shift",
|
|
" ;;",
|
|
" --dry-run)",
|
|
" DRY_RUN=1",
|
|
" shift",
|
|
" ;;",
|
|
" -h | --help)",
|
|
" usage",
|
|
" exit 0",
|
|
" ;;",
|
|
" -*)",
|
|
" die \"Unknown option: $1 (try --help)\"",
|
|
" ;;",
|
|
" *)",
|
|
" die \"Unexpected argument: $1 (try --help)\"",
|
|
" ;;",
|
|
" esac",
|
|
"done",
|
|
"",
|
|
"cd \"$COMPOSE_DIR\" || die \"Cannot cd to COMPOSE_DIR=${COMPOSE_DIR}\"",
|
|
"",
|
|
"ENV_FILE=\"${COMPOSE_DIR}/.env\"",
|
|
"EXAMPLE_FILE=\"${COMPOSE_DIR}/.env.example\"",
|
|
"",
|
|
"is_comment_or_blank() {",
|
|
" [[ \"$1\" =~ ^[[:space:]]*# ]] || [[ \"$1\" =~ ^[[:space:]]*$ ]]",
|
|
"}",
|
|
"",
|
|
"# Uncommented KEY= assignment (optional export). Prints 1 if present.",
|
|
"env_has_key() {",
|
|
" local file=$1 key=$2 line",
|
|
" [[ -f \"$file\" ]] || return 1",
|
|
" while IFS= read -r line || [[ -n \"$line\" ]]; do",
|
|
" is_comment_or_blank \"$line\" && continue",
|
|
" if [[ \"$line\" =~ ^[[:space:]]*(export[[:space:]]+)?${key}= ]]; then",
|
|
" return 0",
|
|
" fi",
|
|
" done <\"$file\"",
|
|
" return 1",
|
|
"}",
|
|
"",
|
|
"env_unquote() {",
|
|
" local v=$1",
|
|
" if [[ ${#v} -ge 2 && \"$v\" == \\\"*\\\" ]]; then",
|
|
" v=\"${v#\\\"}\"",
|
|
" v=\"${v%\\\"}\"",
|
|
" elif [[ ${#v} -ge 2 && \"$v\" == \\'*\\' ]]; then",
|
|
" v=\"${v#\\'}\"",
|
|
" v=\"${v%\\'}\"",
|
|
" fi",
|
|
" printf '%s' \"$v\"",
|
|
"}",
|
|
"",
|
|
"# Last uncommented assignment wins (Compose / Docker dotenv).",
|
|
"env_get() {",
|
|
" local file=$1 key=$2 val=\"\" line",
|
|
" [[ -f \"$file\" ]] || return 0",
|
|
" while IFS= read -r line || [[ -n \"$line\" ]]; do",
|
|
" is_comment_or_blank \"$line\" && continue",
|
|
" if [[ \"$line\" =~ ^[[:space:]]*(export[[:space:]]+)?${key}=(.*)$ ]]; then",
|
|
" val=\"$(env_unquote \"${BASH_REMATCH[2]}\")\"",
|
|
" fi",
|
|
" done <\"$file\"",
|
|
" printf '%s' \"$val\"",
|
|
"}",
|
|
"",
|
|
"env_set_key() {",
|
|
" local file=$1 key=$2 value=$3",
|
|
" local tmp line found=0",
|
|
" tmp=\"$(mktemp)\"",
|
|
" while IFS= read -r line || [[ -n \"$line\" ]]; do",
|
|
" if is_comment_or_blank \"$line\"; then",
|
|
" printf '%s\\n' \"$line\"",
|
|
" continue",
|
|
" fi",
|
|
" if [[ \"$line\" =~ ^[[:space:]]*(export[[:space:]]+)?${key}= ]]; then",
|
|
" if [[ \"$line\" =~ ^[[:space:]]*export[[:space:]]+ ]]; then",
|
|
" printf 'export %s=%s\\n' \"$key\" \"$value\"",
|
|
" else",
|
|
" printf '%s=%s\\n' \"$key\" \"$value\"",
|
|
" fi",
|
|
" found=1",
|
|
" continue",
|
|
" fi",
|
|
" printf '%s\\n' \"$line\"",
|
|
" done <\"$file\" >\"$tmp\"",
|
|
" if [[ \"$found\" -eq 0 ]]; then",
|
|
" if [[ -s \"$tmp\" ]] && [[ \"$(tail -c 1 \"$tmp\" 2>/dev/null || true)\" != $'\\n' ]]; then",
|
|
" printf '\\n' >>\"$tmp\"",
|
|
" fi",
|
|
" printf '%s=%s\\n' \"$key\" \"$value\" >>\"$tmp\"",
|
|
" fi",
|
|
" mv \"$tmp\" \"$file\"",
|
|
"}",
|
|
"",
|
|
"validate_shop_id() {",
|
|
" local slug=$1",
|
|
" if [[ ! \"$slug\" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then",
|
|
" die \"Invalid --shop-id '${slug}'. Use a lowercase slug (letters, digits, hyphens), e.g. acme.\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"validate_deploy_env() {",
|
|
" local name=$1",
|
|
" case \"$name\" in",
|
|
" live | staging | playground | dev) ;;",
|
|
" *)",
|
|
" die \"Invalid --env '${name}'. Use live, staging, playground, or dev.\"",
|
|
" ;;",
|
|
" esac",
|
|
"}",
|
|
"",
|
|
"comment_compose_project_name() {",
|
|
" local file=$1",
|
|
" local tmp line n=0",
|
|
" tmp=\"$(mktemp)\"",
|
|
" while IFS= read -r line || [[ -n \"$line\" ]]; do",
|
|
" if [[ \"$line\" =~ ^[[:space:]]*(export[[:space:]]+)?COMPOSE_PROJECT_NAME= ]]; then",
|
|
" printf '# %s # commented by deploy/init-env.sh --vps (restore for local project dev)\\n' \"$line\"",
|
|
" n=$((n + 1))",
|
|
" continue",
|
|
" fi",
|
|
" printf '%s\\n' \"$line\"",
|
|
" done <\"$file\" >\"$tmp\"",
|
|
" mv \"$tmp\" \"$file\"",
|
|
" printf '%s' \"$n\"",
|
|
"}",
|
|
"",
|
|
"merge_missing_from_example() {",
|
|
" local example=$1 dest=$2",
|
|
" local line key header=0",
|
|
" local -a added=()",
|
|
" while IFS= read -r line || [[ -n \"$line\" ]]; do",
|
|
" is_comment_or_blank \"$line\" && continue",
|
|
" if [[ \"$line\" =~ ^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)= ]]; then",
|
|
" key=\"${BASH_REMATCH[2]}\"",
|
|
" if ! env_has_key \"$dest\" \"$key\"; then",
|
|
" if [[ \"$header\" -eq 0 ]]; then",
|
|
" printf '\\n# --- missing keys merged from .env.example by deploy/init-env.sh ---\\n' >>\"$dest\"",
|
|
" header=1",
|
|
" fi",
|
|
" printf '%s\\n' \"$line\" >>\"$dest\"",
|
|
" added+=(\"$key\")",
|
|
" fi",
|
|
" fi",
|
|
" done <\"$example\"",
|
|
" if [[ ${#added[@]} -gt 0 ]]; then",
|
|
" local IFS=,",
|
|
" printf '%s' \"${added[*]}\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"if [[ ! -f \"$ENV_FILE\" && ! -f \"$EXAMPLE_FILE\" ]]; then",
|
|
" die \"Missing ${ENV_FILE} and ${EXAMPLE_FILE}. Run this from the shop root after Flex copied .env.example (composer require fyrst/shopware-cd), or copy a shop .env into COMPOSE_DIR=${COMPOSE_DIR}.\"",
|
|
"fi",
|
|
"",
|
|
"COPIED=0",
|
|
"if [[ ! -f \"$ENV_FILE\" ]]; then",
|
|
" COPIED=1",
|
|
"fi",
|
|
"",
|
|
"WORK=\"$(mktemp)\"",
|
|
"ORIG=\"$(mktemp)\"",
|
|
"if [[ -f \"$ENV_FILE\" ]]; then",
|
|
" cp \"$ENV_FILE\" \"$WORK\"",
|
|
" cp \"$ENV_FILE\" \"$ORIG\"",
|
|
"else",
|
|
" cp \"$EXAMPLE_FILE\" \"$WORK\"",
|
|
" : >\"$ORIG\"",
|
|
"fi",
|
|
"",
|
|
"MERGED_KEYS=\"\"",
|
|
"if [[ -f \"$EXAMPLE_FILE\" ]]; then",
|
|
" MERGED_KEYS=\"$(merge_missing_from_example \"$EXAMPLE_FILE\" \"$WORK\")\"",
|
|
"fi",
|
|
"",
|
|
"EXISTING_SHOP_ID=\"$(env_get \"$WORK\" SHOPWARE_SHOP_ID)\"",
|
|
"EXISTING_DEPLOY_ENV=\"$(env_get \"$WORK\" SHOPWARE_DEPLOY_ENV)\"",
|
|
"EXISTING_APP_SECRET=\"$(env_get \"$WORK\" APP_SECRET)\"",
|
|
"EXISTING_IMAGE=\"$(env_get \"$WORK\" IMAGE)\"",
|
|
"",
|
|
"SHOP_ID=\"${SHOP_ID_FLAG:-$EXISTING_SHOP_ID}\"",
|
|
"if [[ -z \"$SHOP_ID\" ]]; then",
|
|
" die \"SHOPWARE_SHOP_ID is empty. Pass --shop-id <slug> (same slug on live, staging, and laptop).\"",
|
|
"fi",
|
|
"validate_shop_id \"$SHOP_ID\"",
|
|
"",
|
|
"DEPLOY_ENV=\"${ENV_FLAG:-$EXISTING_DEPLOY_ENV}\"",
|
|
"if [[ -z \"$DEPLOY_ENV\" ]]; then",
|
|
" DEPLOY_ENV=live",
|
|
"fi",
|
|
"validate_deploy_env \"$DEPLOY_ENV\"",
|
|
"",
|
|
"if [[ -n \"$IMAGE_FLAG\" ]]; then",
|
|
" if [[ \"$IMAGE_FLAG\" =~ [[:space:]] ]]; then",
|
|
" die \"Invalid --image '${IMAGE_FLAG}' (no whitespace).\"",
|
|
" fi",
|
|
"fi",
|
|
"",
|
|
"if [[ \"$GENERATE_SECRET\" -eq 1 && -z \"$EXISTING_APP_SECRET\" && \"$DRY_RUN\" -eq 0 ]]; then",
|
|
" if ! command -v openssl >/dev/null 2>&1; then",
|
|
" die \"--generate-app-secret needs openssl (openssl rand -hex 32).\"",
|
|
" fi",
|
|
" NEW_SECRET=\"$(openssl rand -hex 32)\"",
|
|
" if [[ ${#NEW_SECRET} -ne 64 ]]; then",
|
|
" die \"openssl rand -hex 32 did not return 32 bytes.\"",
|
|
" fi",
|
|
" env_set_key \"$WORK\" APP_SECRET \"$NEW_SECRET\"",
|
|
" GENERATED_SECRET=1",
|
|
"else",
|
|
" GENERATED_SECRET=0",
|
|
" if [[ \"$GENERATE_SECRET\" -eq 1 && -z \"$EXISTING_APP_SECRET\" && \"$DRY_RUN\" -eq 1 ]]; then",
|
|
" GENERATED_SECRET=1",
|
|
" fi",
|
|
"fi",
|
|
"",
|
|
"env_set_key \"$WORK\" SHOPWARE_SHOP_ID \"$SHOP_ID\"",
|
|
"env_set_key \"$WORK\" SHOPWARE_DEPLOY_ENV \"$DEPLOY_ENV\"",
|
|
"if [[ -n \"$IMAGE_FLAG\" ]]; then",
|
|
" env_set_key \"$WORK\" IMAGE \"$IMAGE_FLAG\"",
|
|
"fi",
|
|
"",
|
|
"VPS_COMMENTED=0",
|
|
"if [[ \"$VPS\" -eq 1 ]]; then",
|
|
" VPS_COMMENTED=\"$(comment_compose_project_name \"$WORK\")\"",
|
|
"fi",
|
|
"",
|
|
"if [[ \"$DRY_RUN\" -eq 0 ]]; then",
|
|
" cp \"$WORK\" \"$ENV_FILE\"",
|
|
" chmod 600 \"$ENV_FILE\"",
|
|
"fi",
|
|
"",
|
|
"# --- summary (never print secret values) -----------------------------------",
|
|
"if [[ \"$DRY_RUN\" -eq 1 ]]; then",
|
|
" log \"DRY-RUN (no write) COMPOSE_DIR=${COMPOSE_DIR}\"",
|
|
"else",
|
|
" log \"Updated ${ENV_FILE}\"",
|
|
"fi",
|
|
"",
|
|
"CHANGES=0",
|
|
"if [[ \"$COPIED\" -eq 1 ]]; then",
|
|
" printf ' copy .env.example \u2192 .env\\n'",
|
|
" CHANGES=1",
|
|
"fi",
|
|
"if [[ -n \"$MERGED_KEYS\" ]]; then",
|
|
" printf ' merge missing keys from .env.example: %s\\n' \"${MERGED_KEYS//,/, }\"",
|
|
" CHANGES=1",
|
|
"fi",
|
|
"if [[ \"$EXISTING_SHOP_ID\" != \"$SHOP_ID\" ]]; then",
|
|
" printf ' set SHOPWARE_SHOP_ID=%s\\n' \"$SHOP_ID\"",
|
|
" CHANGES=1",
|
|
"fi",
|
|
"if [[ \"$EXISTING_DEPLOY_ENV\" != \"$DEPLOY_ENV\" ]]; then",
|
|
" printf ' set SHOPWARE_DEPLOY_ENV=%s\\n' \"$DEPLOY_ENV\"",
|
|
" CHANGES=1",
|
|
"fi",
|
|
"if [[ -n \"$IMAGE_FLAG\" && \"$EXISTING_IMAGE\" != \"$IMAGE_FLAG\" ]]; then",
|
|
" printf ' set IMAGE=%s\\n' \"$IMAGE_FLAG\"",
|
|
" CHANGES=1",
|
|
"fi",
|
|
"if [[ \"$GENERATE_SECRET\" -eq 1 ]]; then",
|
|
" if [[ -n \"$EXISTING_APP_SECRET\" ]]; then",
|
|
" printf ' APP_SECRET already set; skipped --generate-app-secret\\n'",
|
|
" elif [[ \"$GENERATED_SECRET\" -eq 1 ]]; then",
|
|
" printf ' set APP_SECRET (openssl rand -hex 32; value not printed)\\n'",
|
|
" CHANGES=1",
|
|
" fi",
|
|
"fi",
|
|
"if [[ \"$VPS\" -eq 1 ]]; then",
|
|
" if [[ \"$VPS_COMMENTED\" -gt 0 ]]; then",
|
|
" printf ' commented %s COMPOSE_PROJECT_NAME=\u2026 line(s) (--vps)\\n' \"$VPS_COMMENTED\"",
|
|
" CHANGES=1",
|
|
" else",
|
|
" printf ' --vps: no uncommented COMPOSE_PROJECT_NAME=\u2026 lines\\n'",
|
|
" fi",
|
|
"fi",
|
|
"",
|
|
"if [[ \"$CHANGES\" -eq 0 ]]; then",
|
|
" printf ' no changes (already up to date)\\n'",
|
|
"fi",
|
|
"",
|
|
"printf ' left unchanged: MYSQL passwords, APP_URL (fill those by hand)\\n'",
|
|
"if [[ \"$DRY_RUN\" -eq 0 ]]; then",
|
|
" log \"chmod 600 .env\"",
|
|
"fi",
|
|
""
|
|
],
|
|
"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.yml` (create\u2019s default; `.yaml` also accepted \u2014 owned by `shopware-cli project create` / the CLI, 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)",
|
|
"- Optional build-time secrets (`SHOPWARE_PACKAGES_TOKEN` only if the shop uses packages.shopware.com; Composer auth). Empty token is fine.",
|
|
""
|
|
],
|
|
"executable": false
|
|
},
|
|
"root/deploy/sync-runtime-local.sh": {
|
|
"contents": [
|
|
"#!/usr/bin/env bash",
|
|
"# Pull live VPS SHOPWARE_DATA_ROOT trees into local `shopware-cli project dev` paths.",
|
|
"#",
|
|
"# Local CLI compose bind-mounts the whole project, so destinations are the shop",
|
|
"# tree (files/, public/media/, \u2026) \u2014 not VPS /var/lib/shopware/data/<shop>/<env>.",
|
|
"# VPS \u2192 VPS copy stays deploy/sync-runtime.sh on the consumer.",
|
|
"#",
|
|
"# Remote default (when unset / unprobed):",
|
|
"# /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/${--from env}",
|
|
"# Laptop .env needs at least SHOPWARE_SHOP_ID (same as live).",
|
|
"#",
|
|
"# bash deploy/sync-runtime-local.sh --from live --data all",
|
|
"# bash deploy/sync-runtime-local.sh --from live --data all --delete --dry-run",
|
|
"#",
|
|
"# Do not run with `bash -x` \u2014 SSH identities / env may be sensitive.",
|
|
"",
|
|
"set -euo pipefail",
|
|
"",
|
|
"case \"$-\" in",
|
|
" *x*)",
|
|
" printf 'ERROR: refusing to run with xtrace (credentials may be in the environment)\\n' >&2",
|
|
" exit 1",
|
|
" ;;",
|
|
"esac",
|
|
"",
|
|
"umask 077",
|
|
"",
|
|
"SCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"",
|
|
"COMPOSE_DIR=\"${COMPOSE_DIR:-$(cd \"${SCRIPT_DIR}/..\" && pwd)}\"",
|
|
"",
|
|
"DEFAULT_DATA=\"files,media,thumbnail,theme,sitemap\"",
|
|
"SHOPWARE_DATA_BASE=\"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}\"",
|
|
"",
|
|
"FROM=\"\"",
|
|
"DATA_SPEC=\"all\"",
|
|
"DRY_RUN=0",
|
|
"DELETE=0",
|
|
"",
|
|
"SSH_TARGET=\"\"",
|
|
"REMOTE_PATH=\"\"",
|
|
"REMOTE_DATA_ROOT=\"\"",
|
|
"WANT_VOLUMES=()",
|
|
"",
|
|
"log() { printf '==> %s\\n' \"$*\"; }",
|
|
"err() { printf 'ERROR: %s\\n' \"$*\" >&2; }",
|
|
"die() { err \"$@\"; exit 1; }",
|
|
"",
|
|
"usage() {",
|
|
" cat <<'EOF'",
|
|
"Usage: deploy/sync-runtime-local.sh --from <alias> [options]",
|
|
"",
|
|
"Copy bind-mounted Shopware runtime files from a VPS (typically live) into",
|
|
"this shop checkout for `shopware-cli project dev`.",
|
|
"",
|
|
" live $SHOPWARE_DATA_ROOT/files \u2192 ./files/",
|
|
" live $SHOPWARE_DATA_ROOT/media \u2192 ./public/media/",
|
|
" live $SHOPWARE_DATA_ROOT/thumbnail \u2192 ./public/thumbnail/",
|
|
" live $SHOPWARE_DATA_ROOT/theme \u2192 ./public/theme/",
|
|
" live $SHOPWARE_DATA_ROOT/sitemap \u2192 ./public/sitemap/",
|
|
"",
|
|
"Remote SHOPWARE_DATA_ROOT defaults to",
|
|
"/var/lib/shopware/data/${SHOPWARE_SHOP_ID}/<from-env> (usually live).",
|
|
"Set SHOPWARE_SHOP_ID in laptop .env (same slug as the VPS).",
|
|
"",
|
|
"This is not deploy/sync-runtime.sh (that script targets VPS SHOPWARE_DATA_ROOT",
|
|
"+ deploy Compose). Database dumps are out of scope here.",
|
|
"",
|
|
"Options:",
|
|
" --from <alias> SSH source (required). Default host is the alias",
|
|
" itself (e.g. Host live in ~/.ssh/config).",
|
|
" --data <list>|all Comma-separated subset, or \"all\" (default).",
|
|
" Items: files, media, thumbnail, theme, sitemap",
|
|
" --delete Pass rsync --delete (drops local-only files)",
|
|
" --dry-run Print planned rsyncs; do not connect or copy",
|
|
" -h, --help",
|
|
"",
|
|
"Environment (optional; deploy/sync.env is sourced when present):",
|
|
" SYNC_SSH_HOST Source hostname (default: the --from alias)",
|
|
" SYNC_SSH_USER SSH user (empty = ssh config / current user)",
|
|
" SYNC_SSH_PORT SSH port (default 22)",
|
|
" SYNC_SSH_KEY Identity file (SYNC_SSH_IDENTITY also accepted)",
|
|
" SYNC_REMOTE_PATH Shop checkout on the source (optional; used to",
|
|
" probe SHOPWARE_DATA_ROOT from the remote .env).",
|
|
" SYNC_SSH_PATH / SYNC_LIVE_PATH also accepted.",
|
|
" SYNC_REMOTE_DATA_ROOT Bind-mount root on the source (overrides derivation)",
|
|
" SHOPWARE_SHOP_ID Same slug as live; used to derive the remote root",
|
|
" SHOPWARE_DATA_ROOT Unused as a local destination (project-dev paths)",
|
|
"",
|
|
"Per-alias overrides (example --from live): SYNC_LIVE_SSH_HOST,",
|
|
"SYNC_LIVE_SSH_USER, SYNC_LIVE_SSH_PORT, SYNC_LIVE_SSH_KEY,",
|
|
"SYNC_LIVE_REMOTE_PATH, SYNC_LIVE_DATA_ROOT.",
|
|
"",
|
|
"Examples:",
|
|
" bash deploy/sync-runtime-local.sh --from live --data all",
|
|
" bash deploy/sync-runtime-local.sh --from live --data media,files --delete",
|
|
" bash deploy/sync-runtime-local.sh --from live --data all --dry-run",
|
|
"",
|
|
"After copy:",
|
|
" shopware-cli project console cache:clear",
|
|
"EOF",
|
|
"}",
|
|
"",
|
|
"need_value() {",
|
|
" local flag=$1",
|
|
" local value=${2:-}",
|
|
" if [[ -z \"$value\" || \"$value\" == --* ]]; then",
|
|
" die \"${flag} requires a value\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"while [[ $# -gt 0 ]]; do",
|
|
" case \"$1\" in",
|
|
" sync)",
|
|
" # Muscle memory from deploy/sync-runtime.sh; this script only pulls locally.",
|
|
" shift",
|
|
" ;;",
|
|
" snapshot | restore | export)",
|
|
" die \"Use deploy/sync-runtime.sh ${1} on a VPS. This script only rsyncs into local project-dev paths.\"",
|
|
" ;;",
|
|
" help | -h | --help)",
|
|
" usage",
|
|
" exit 0",
|
|
" ;;",
|
|
" --from)",
|
|
" need_value \"$1\" \"${2:-}\"",
|
|
" FROM=$2",
|
|
" shift 2",
|
|
" ;;",
|
|
" --from=*)",
|
|
" FROM=\"${1#*=}\"",
|
|
" shift",
|
|
" ;;",
|
|
" --data)",
|
|
" need_value \"$1\" \"${2:-}\"",
|
|
" DATA_SPEC=$2",
|
|
" shift 2",
|
|
" ;;",
|
|
" --data=*)",
|
|
" DATA_SPEC=\"${1#*=}\"",
|
|
" shift",
|
|
" ;;",
|
|
" --delete)",
|
|
" DELETE=1",
|
|
" shift",
|
|
" ;;",
|
|
" --dry-run)",
|
|
" DRY_RUN=1",
|
|
" shift",
|
|
" ;;",
|
|
" -*)",
|
|
" die \"Unknown option: $1 (try --help)\"",
|
|
" ;;",
|
|
" *)",
|
|
" die \"Unexpected argument: $1 (try --help)\"",
|
|
" ;;",
|
|
" esac",
|
|
"done",
|
|
"",
|
|
"if [[ -z \"$FROM\" ]]; then",
|
|
" usage",
|
|
" die \"Missing --from <alias> (e.g. --from live)\"",
|
|
"fi",
|
|
"",
|
|
"cd \"$COMPOSE_DIR\"",
|
|
"",
|
|
"load_env_file() {",
|
|
" local f=$1",
|
|
" if [[ -f \"$f\" ]]; then",
|
|
" set -a",
|
|
" # shellcheck disable=SC1090",
|
|
" source \"$f\"",
|
|
" set +a",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"load_env_file .env",
|
|
"load_env_file deploy/sync.env",
|
|
"SHOPWARE_DATA_BASE=\"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}\"",
|
|
"",
|
|
"derived_remote_data_root() {",
|
|
" local shop=\"${SHOPWARE_SHOP_ID:-}\"",
|
|
" local envn=\"${1:-${FROM_LC:-live}}\"",
|
|
" if [[ -n \"$shop\" ]]; then",
|
|
" printf '%s' \"${SHOPWARE_DATA_BASE}/${shop}/${envn}\"",
|
|
" else",
|
|
" printf '%s' \"$SHOPWARE_DATA_BASE\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"split_csv() {",
|
|
" local csv=$1",
|
|
" local IFS=','",
|
|
" # shellcheck disable=SC2086",
|
|
" set -- $csv",
|
|
" local item",
|
|
" for item in \"$@\"; do",
|
|
" item=\"${item// /}\"",
|
|
" [[ -n \"$item\" ]] && printf '%s\\n' \"$item\"",
|
|
" done",
|
|
"}",
|
|
"",
|
|
"normalize_data() {",
|
|
" local spec=$1",
|
|
" local item lower",
|
|
" local -a raw=()",
|
|
"",
|
|
" if [[ \"$spec\" == \"all\" ]]; then",
|
|
" spec=$DEFAULT_DATA",
|
|
" fi",
|
|
"",
|
|
" while IFS= read -r item; do",
|
|
" [[ -z \"$item\" ]] && continue",
|
|
" raw+=(\"$item\")",
|
|
" done < <(split_csv \"$spec\")",
|
|
"",
|
|
" if [[ ${#raw[@]} -eq 0 ]]; then",
|
|
" die \"--data is empty\"",
|
|
" fi",
|
|
"",
|
|
" WANT_VOLUMES=()",
|
|
" for item in \"${raw[@]}\"; do",
|
|
" lower=$(printf '%s' \"$item\" | tr '[:upper:]' '[:lower:]')",
|
|
" case \"$lower\" in",
|
|
" db | database | mysql)",
|
|
" die \"Database dumps are out of scope for sync-runtime-local.sh. Copy SQL separately into the CLI database service, or use deploy/sync-runtime.sh on a VPS.\"",
|
|
" ;;",
|
|
" files | media | thumbnail | theme | sitemap)",
|
|
" WANT_VOLUMES+=(\"$lower\")",
|
|
" ;;",
|
|
" mysql_data | redis_data | volumes)",
|
|
" die \"Unknown --data item '${item}'. Use files, media, thumbnail, theme, sitemap, or all.\"",
|
|
" ;;",
|
|
" *)",
|
|
" die \"Unknown --data item '${item}'. Use files, media, thumbnail, theme, sitemap, or all.\"",
|
|
" ;;",
|
|
" esac",
|
|
" done",
|
|
"",
|
|
" if [[ ${#WANT_VOLUMES[@]} -eq 0 ]]; then",
|
|
" die \"Nothing to copy\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"normalize_data \"$DATA_SPEC\"",
|
|
"",
|
|
"lower_s() { printf '%s' \"$1\" | tr '[:upper:]' '[:lower:]'; }",
|
|
"",
|
|
"FROM_LC=\"$(lower_s \"$FROM\")\"",
|
|
"",
|
|
"if [[ \"$FROM_LC\" == \"local\" || \"$FROM_LC\" == \"this\" ]]; then",
|
|
" die \"--from local is invalid here. Source must be a VPS alias (e.g. --from live).\"",
|
|
"fi",
|
|
"",
|
|
"alias_key() {",
|
|
" printf '%s' \"$1\" | tr '[:lower:]-' '[:upper:]_'",
|
|
"}",
|
|
"",
|
|
"pick_alias_env() {",
|
|
" local key=$1",
|
|
" local suffix=$2",
|
|
" local specific=\"SYNC_${key}_${suffix}\"",
|
|
" local general=\"SYNC_${suffix}\"",
|
|
" if [[ -n \"${!specific:-}\" ]]; then",
|
|
" printf '%s' \"${!specific}\"",
|
|
" else",
|
|
" printf '%s' \"${!general:-}\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"key=\"$(alias_key \"$FROM\")\"",
|
|
"host=\"$(pick_alias_env \"$key\" SSH_HOST)\"",
|
|
"user=\"$(pick_alias_env \"$key\" SSH_USER)\"",
|
|
"port=\"$(pick_alias_env \"$key\" SSH_PORT)\"",
|
|
"keyfile=\"$(pick_alias_env \"$key\" SSH_KEY)\"",
|
|
"if [[ -z \"$keyfile\" ]]; then",
|
|
" keyfile=\"$(pick_alias_env \"$key\" SSH_IDENTITY)\"",
|
|
"fi",
|
|
"REMOTE_PATH=\"$(pick_alias_env \"$key\" REMOTE_PATH)\"",
|
|
"if [[ -z \"$REMOTE_PATH\" ]]; then",
|
|
" REMOTE_PATH=\"$(pick_alias_env \"$key\" SSH_PATH)\"",
|
|
"fi",
|
|
"if [[ -z \"$REMOTE_PATH\" ]]; then",
|
|
" REMOTE_PATH=\"$(pick_alias_env \"$key\" PATH)\"",
|
|
"fi",
|
|
"specific_dr=\"SYNC_${key}_DATA_ROOT\"",
|
|
"REMOTE_DATA_ROOT=\"${!specific_dr:-${SYNC_REMOTE_DATA_ROOT:-}}\"",
|
|
"",
|
|
"if [[ -z \"$host\" ]]; then",
|
|
" host=$FROM",
|
|
"fi",
|
|
"SYNC_SSH_HOST=$host",
|
|
"SYNC_SSH_USER=$user",
|
|
"SYNC_SSH_PORT=\"${port:-22}\"",
|
|
"SYNC_SSH_KEY=$keyfile",
|
|
"",
|
|
"if [[ -n \"$SYNC_SSH_USER\" ]]; then",
|
|
" SSH_TARGET=\"${SYNC_SSH_USER}@${host}\"",
|
|
"else",
|
|
" SSH_TARGET=$host",
|
|
"fi",
|
|
"",
|
|
"SSH_CMD=(ssh -o BatchMode=yes -p \"${SYNC_SSH_PORT:-22}\")",
|
|
"if [[ -n \"${SYNC_SSH_KEY:-}\" ]]; then",
|
|
" SSH_CMD+=(-o IdentitiesOnly=yes -i \"${SYNC_SSH_KEY}\")",
|
|
"fi",
|
|
"",
|
|
"local_item_dir() {",
|
|
" local logical=$1",
|
|
" case \"$logical\" in",
|
|
" files) printf '%s/files\\n' \"$COMPOSE_DIR\" ;;",
|
|
" media) printf '%s/public/media\\n' \"$COMPOSE_DIR\" ;;",
|
|
" thumbnail) printf '%s/public/thumbnail\\n' \"$COMPOSE_DIR\" ;;",
|
|
" theme) printf '%s/public/theme\\n' \"$COMPOSE_DIR\" ;;",
|
|
" sitemap) printf '%s/public/sitemap\\n' \"$COMPOSE_DIR\" ;;",
|
|
" *) die \"No local project-dev path for ${logical}\" ;;",
|
|
" esac",
|
|
"}",
|
|
"",
|
|
"bind_item_dir() {",
|
|
" local root=$1",
|
|
" local logical=$2",
|
|
" printf '%s/%s\\n' \"$root\" \"$logical\"",
|
|
"}",
|
|
"",
|
|
"require_cmd() {",
|
|
" local c=$1",
|
|
" if ! command -v \"$c\" >/dev/null 2>&1; then",
|
|
" die \"Missing command '${c}'. Install rsync and OpenSSH client on this machine.\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"if [[ -n \"${SYNC_SSH_KEY:-}\" && ! -f \"${SYNC_SSH_KEY}\" ]]; then",
|
|
" die \"SYNC_SSH_KEY not found: ${SYNC_SSH_KEY}\"",
|
|
"fi",
|
|
"",
|
|
"require_cmd bash",
|
|
"if [[ \"$DRY_RUN\" -eq 0 ]]; then",
|
|
" require_cmd ssh",
|
|
" require_cmd rsync",
|
|
"fi",
|
|
"",
|
|
"remote_bash() {",
|
|
" local cmd=$1",
|
|
" local remote_cmd",
|
|
" if [[ -n \"${REMOTE_PATH}\" ]]; then",
|
|
" remote_cmd=\"set -euo pipefail; cd $(printf '%q' \"$REMOTE_PATH\"); if [ -f .env ]; then set -a; . ./.env; set +a; fi; ${cmd}\"",
|
|
" else",
|
|
" remote_cmd=\"set -euo pipefail; ${cmd}\"",
|
|
" fi",
|
|
" \"${SSH_CMD[@]}\" \"$SSH_TARGET\" \"$remote_cmd\"",
|
|
"}",
|
|
"",
|
|
"resolve_remote_data_root() {",
|
|
" if [[ -n \"${REMOTE_DATA_ROOT}\" ]]; then",
|
|
" return",
|
|
" fi",
|
|
" local fallback",
|
|
" fallback=\"$(derived_remote_data_root \"$FROM_LC\")\"",
|
|
" if [[ -z \"${SHOPWARE_SHOP_ID:-}\" && -z \"${REMOTE_DATA_ROOT}\" ]]; then",
|
|
" log \"SHOPWARE_SHOP_ID unset; remote root fallback ${fallback}. Set SHOPWARE_SHOP_ID in .env (same as live).\"",
|
|
" fi",
|
|
" if [[ \"$DRY_RUN\" -eq 1 ]]; then",
|
|
" REMOTE_DATA_ROOT=$fallback",
|
|
" log \"DRY-RUN remote SHOPWARE_DATA_ROOT default ${REMOTE_DATA_ROOT} (probe skipped)\"",
|
|
" return",
|
|
" fi",
|
|
" local probed=\"\" remote_printf",
|
|
" if [[ -n \"${REMOTE_PATH}\" ]]; then",
|
|
" # shellcheck disable=SC2016",
|
|
" remote_printf='printf %s \"${SYNC_DATA_ROOT:-${SHOPWARE_DATA_ROOT:-}}\"'",
|
|
" probed=\"$(remote_bash \"$remote_printf\" || true)\"",
|
|
" probed=\"$(printf '%s' \"$probed\" | tr -d '\\r' | tail -n 1)\"",
|
|
" fi",
|
|
" if [[ -n \"$probed\" ]]; then",
|
|
" REMOTE_DATA_ROOT=$probed",
|
|
" else",
|
|
" REMOTE_DATA_ROOT=$fallback",
|
|
" fi",
|
|
" log \"Remote bind-mount root: ${REMOTE_DATA_ROOT}\"",
|
|
"}",
|
|
"",
|
|
"rsync_from_remote_tree() {",
|
|
" local remote_dir=$1",
|
|
" local dest=$2",
|
|
" local -a args",
|
|
" args=(-az --no-owner --no-group -e \"${SSH_CMD[*]}\")",
|
|
" if [[ \"$DELETE\" -eq 1 ]]; then",
|
|
" args+=(--delete)",
|
|
" fi",
|
|
" mkdir -p \"$dest\"",
|
|
" rsync \"${args[@]}\" \"${SSH_TARGET}:${remote_dir%/}/\" \"${dest%/}/\"",
|
|
"}",
|
|
"",
|
|
"sync_item() {",
|
|
" local logical=$1",
|
|
" local src dest",
|
|
" src=\"$(bind_item_dir \"$REMOTE_DATA_ROOT\" \"$logical\")\"",
|
|
" dest=\"$(local_item_dir \"$logical\")\"",
|
|
" log \"Rsync ${SSH_TARGET}:${src}/ \u2192 ${dest}/\"",
|
|
" if [[ \"$DRY_RUN\" -eq 1 ]]; then",
|
|
" if [[ \"$DELETE\" -eq 1 ]]; then",
|
|
" log \"DRY-RUN rsync -az --delete ${SSH_TARGET}:${src}/ ${dest}/\"",
|
|
" else",
|
|
" log \"DRY-RUN rsync -az ${SSH_TARGET}:${src}/ ${dest}/\"",
|
|
" fi",
|
|
" return",
|
|
" fi",
|
|
" rsync_from_remote_tree \"$src\" \"$dest\"",
|
|
"}",
|
|
"",
|
|
"resolve_remote_data_root",
|
|
"",
|
|
"log \"Local project-dev pull from=${FROM} data=$(IFS=,; printf '%s' \"${WANT_VOLUMES[*]}\") delete=${DELETE} dry-run=${DRY_RUN}\"",
|
|
"",
|
|
"item=\"\"",
|
|
"for item in \"${WANT_VOLUMES[@]}\"; do",
|
|
" sync_item \"$item\"",
|
|
"done",
|
|
"",
|
|
"if [[ \"$DRY_RUN\" -eq 1 ]]; then",
|
|
" log \"Dry-run finished (no files copied)\"",
|
|
" exit 0",
|
|
"fi",
|
|
"",
|
|
"log \"Copy finished ${FROM} \u2192 ${COMPOSE_DIR} (project-dev paths)\"",
|
|
"log \"Next: shopware-cli project console cache:clear\"",
|
|
"log \"These dirs stay gitignored \u2014 never commit them.\"",
|
|
""
|
|
],
|
|
"executable": true
|
|
},
|
|
"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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/<name>` |",
|
|
"",
|
|
"Default host root (when `SHOPWARE_DATA_ROOT` is unset, derived by this script):",
|
|
"",
|
|
"```text",
|
|
"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}",
|
|
"```",
|
|
"",
|
|
"Set `SHOPWARE_SHOP_ID` (same slug on live + staging) and `SHOPWARE_DEPLOY_ENV` in shop-root `.env`. Compose interpolates those two plus optional `SHOPWARE_DATA_BASE`; it does not require `COMPOSE_PROJECT_NAME` or `SHOPWARE_DATA_ROOT`. If the **source** uses a non-derived root, set `SYNC_REMOTE_DATA_ROOT` or `SYNC_LIVE_DATA_ROOT` in `deploy/sync.env`. Otherwise the source path is `${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/<from-env>`.",
|
|
"",
|
|
"Not copied: `mysql_data` / `redis_data` named volumes (use `db` for SQL; Redis is ephemeral for this recipe). Do not put dumps in git.",
|
|
"",
|
|
"## 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`, `SHOPWARE_SHOP_ID`, and `SHOPWARE_DEPLOY_ENV`. Compose interpolates project name and bind-mount paths from those. Sync derives `COMPOSE_PROJECT_NAME` / `SHOPWARE_DATA_ROOT` when they are empty. 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 <id> --data all",
|
|
"```",
|
|
"",
|
|
"| Flag | Meaning |",
|
|
"| --- | --- |",
|
|
"| `--from <env>` | 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 <name>` | Single dir for `export --data volumes` (`files`, `media`, \u2026) |",
|
|
"| `--snapshot <id>` | Snapshot directory name under `SYNC_SNAPSHOT_DIR` |",
|
|
"| `--yes` | Skip the overwrite prompt |",
|
|
"",
|
|
"### Cron (consumer)",
|
|
"",
|
|
"```cron",
|
|
"15 2 * * * cd /opt/shopware/acme-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 `<project>_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`. Compose project name is `${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}`, not a hardcoded `shopware`.",
|
|
"",
|
|
"## Local `project dev` (laptop)",
|
|
"",
|
|
"Do **not** run `deploy/sync-runtime.sh` on a laptop. Local CLI compose bind-mounts the shop tree, not `SHOPWARE_DATA_ROOT`.",
|
|
"",
|
|
"```bash",
|
|
"bash deploy/sync-runtime-local.sh --from live --data all",
|
|
"# optional: --delete --dry-run",
|
|
"shopware-cli project console cache:clear",
|
|
"```",
|
|
"",
|
|
"| Live VPS (`SHOPWARE_DATA_ROOT`, default `/var/lib/shopware/data/${SHOPWARE_SHOP_ID}/live`) | Local project |",
|
|
"| --- | --- |",
|
|
"| `.../files` | `files/` |",
|
|
"| `.../media` | `public/media/` |",
|
|
"| `.../thumbnail` | `public/thumbnail/` |",
|
|
"| `.../theme` | `public/theme/` |",
|
|
"| `.../sitemap` | `public/sitemap/` |",
|
|
"",
|
|
"`--from` defaults the SSH host to that alias (`Host live` in `~/.ssh/config`). Laptop `.env` needs `SHOPWARE_SHOP_ID` (same as live). Optional `deploy/sync.env` / `SYNC_LIVE_*` / `SYNC_REMOTE_DATA_ROOT` match `sync-runtime.sh`. `--delete` is off unless passed (keeps local-only uploads). Database copy is out of scope here.",
|
|
""
|
|
],
|
|
"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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/{files,media,thumbnail,theme,sitemap}",
|
|
"# (compose bind mounts). Optional SHOPWARE_DATA_ROOT is derived when unset.",
|
|
"# 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 <id> --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 <command> [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 <env> Source env for sync (e.g. live)",
|
|
" --data <what> all | db | volumes (default: all)",
|
|
" --volume <name> Single bind-mount dir (files|media|thumbnail|theme|sitemap)",
|
|
" --snapshot <id> 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 ]]; }",
|
|
"",
|
|
"ensure_data_root() {",
|
|
" if want_volumes && [[ -z \"${SHOPWARE_DATA_ROOT:-}\" ]]; then",
|
|
" die \"Set SHOPWARE_DATA_ROOT or SHOPWARE_SHOP_ID+SHOPWARE_DEPLOY_ENV in .env\"",
|
|
" fi",
|
|
"}",
|
|
"",
|
|
"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",
|
|
"",
|
|
"SHOPWARE_DATA_BASE=\"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}\"",
|
|
"",
|
|
"# SHOPWARE_SHOP_ID + SHOPWARE_DEPLOY_ENV (or SYNC_ENV) \u2192 project name / data root.",
|
|
"# Compose interpolates those two (+ optional SHOPWARE_DATA_BASE) itself.",
|
|
"# Scripts fill COMPOSE_PROJECT_NAME / SHOPWARE_DATA_ROOT when they are empty.",
|
|
"if [[ -z \"${SHOPWARE_DEPLOY_ENV:-}\" && -n \"${SYNC_ENV:-}\" ]]; then",
|
|
" SHOPWARE_DEPLOY_ENV=\"${SYNC_ENV}\"",
|
|
"fi",
|
|
"if [[ -z \"${SYNC_ENV:-}\" && -n \"${SHOPWARE_DEPLOY_ENV:-}\" ]]; then",
|
|
" SYNC_ENV=\"${SHOPWARE_DEPLOY_ENV}\"",
|
|
"fi",
|
|
"if [[ -z \"${COMPOSE_PROJECT_NAME:-}\" && -n \"${SHOPWARE_SHOP_ID:-}\" && -n \"${SHOPWARE_DEPLOY_ENV:-}\" ]]; then",
|
|
" COMPOSE_PROJECT_NAME=\"${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}\"",
|
|
"elif [[ -n \"${COMPOSE_PROJECT_NAME:-}\" && -n \"${SHOPWARE_SHOP_ID:-}\" && -n \"${SHOPWARE_DEPLOY_ENV:-}\" ]]; then",
|
|
" derived_project=\"${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}\"",
|
|
" if [[ \"$COMPOSE_PROJECT_NAME\" != \"$derived_project\" ]]; then",
|
|
" log \"WARNING: COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME} is set and overrides Compose name: (${derived_project}). shopware-cli project create writes COMPOSE_PROJECT_NAME=sw-shop-\u2026 into .env for local project dev. On the VPS, remove or comment out that line. This script does not delete it.\"",
|
|
" fi",
|
|
"fi",
|
|
"if [[ -z \"${SHOPWARE_DATA_ROOT:-}\" && -n \"${SHOPWARE_SHOP_ID:-}\" && -n \"${SHOPWARE_DEPLOY_ENV:-}\" ]]; then",
|
|
" SHOPWARE_DATA_ROOT=\"${SHOPWARE_DATA_BASE}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}\"",
|
|
"fi",
|
|
"if [[ -n \"${COMPOSE_PROJECT_NAME:-}\" ]]; then",
|
|
" export COMPOSE_PROJECT_NAME",
|
|
"fi",
|
|
"if [[ -n \"${SHOPWARE_DATA_ROOT:-}\" ]]; then",
|
|
" export SHOPWARE_DATA_ROOT",
|
|
"fi",
|
|
"",
|
|
"SYNC_COMPOSE_PROJECT=\"${SYNC_COMPOSE_PROJECT:-${COMPOSE_PROJECT_NAME:-}}\"",
|
|
"SYNC_VOLUMES=\"${SYNC_VOLUMES:-files,media,thumbnail,theme,sitemap}\"",
|
|
"SYNC_SNAPSHOT_DIR=\"${SYNC_SNAPSHOT_DIR:-$COMPOSE_DIR/.runtime-snapshots}\"",
|
|
"SYNC_KEEP_SNAPSHOTS=\"${SYNC_KEEP_SNAPSHOTS:-5}\"",
|
|
"SYNC_SSH_PORT=\"${SYNC_SSH_PORT:-22}\"",
|
|
"",
|
|
"if [[ \"$COMMAND\" != \"export\" && -n \"${COMPOSE_PROJECT_NAME:-}${SHOPWARE_DATA_ROOT:-}\" ]]; then",
|
|
" log \"Identity shop=${SHOPWARE_SHOP_ID:-?} env=${SHOPWARE_DEPLOY_ENV:-?} project=${COMPOSE_PROJECT_NAME:-?} data_root=${SHOPWARE_DATA_ROOT:-?}\"",
|
|
"fi",
|
|
"",
|
|
"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}\"",
|
|
" n=\"${n:-$COMPOSE_PROJECT_NAME}\"",
|
|
" [[ -n \"$n\" ]] || die \"Set COMPOSE_PROJECT_NAME in .env (e.g. \\${SHOPWARE_SHOP_ID}-\\${SHOPWARE_DEPLOY_ENV})\"",
|
|
" printf '%s\\n' \"$n\"",
|
|
"}",
|
|
"",
|
|
"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-${COMPOSE_PROJECT_NAME:-default}.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",
|
|
" var=\"SYNC_$(env_upper \"$1\")_DATA_ROOT\"",
|
|
" if [[ -n \"${!var:-}\" ]]; then",
|
|
" printf '%s\\n' \"${!var}\"",
|
|
" return",
|
|
" fi",
|
|
" if [[ -n \"${SYNC_REMOTE_DATA_ROOT:-}\" ]]; then",
|
|
" printf '%s\\n' \"$SYNC_REMOTE_DATA_ROOT\"",
|
|
" return",
|
|
" fi",
|
|
" if [[ -n \"${SHOPWARE_SHOP_ID:-}\" ]]; then",
|
|
" printf '%s\\n' \"${SHOPWARE_DATA_BASE}/${SHOPWARE_SHOP_ID}/$1\"",
|
|
" return",
|
|
" fi",
|
|
" printf '%s\\n' \"$SHOPWARE_DATA_BASE\"",
|
|
"}",
|
|
"",
|
|
"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() {",
|
|
" ensure_data_root",
|
|
" case \"$DATA\" in",
|
|
" db)",
|
|
" dump_sql",
|
|
" ;;",
|
|
" volumes)",
|
|
" [[ -n \"$VOLUME_KEY\" ]] || die \"export --data volumes requires --volume <key>\"",
|
|
" 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",
|
|
" ensure_data_root",
|
|
" 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",
|
|
" ensure_data_root",
|
|
" if [[ -z \"$SNAPSHOT_ID\" ]]; then",
|
|
" log \"Available snapshots in $SYNC_SNAPSHOT_DIR:\"",
|
|
" snapshot_ids || true",
|
|
" die \"restore requires --snapshot <id>\"",
|
|
" 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",
|
|
" ensure_data_root",
|
|
" [[ -n \"$FROM_ENV\" ]] || die \"sync requires --from <env> (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": false
|
|
},
|
|
"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/acme/live",
|
|
"# 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_BASE:-/var/lib/shopware/data}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}/<name>",
|
|
"# Optional SHOPWARE_DATA_ROOT (scripts derive that formula when unset).",
|
|
"SYNC_VOLUMES=files,media,thumbnail,theme,sitemap",
|
|
"",
|
|
"# Remote bind-mount root when it cannot be derived from SHOPWARE_SHOP_ID + --from.",
|
|
"# Default: /var/lib/shopware/data/${SHOPWARE_SHOP_ID}/<from-env>",
|
|
"# SYNC_REMOTE_DATA_ROOT=/var/lib/shopware/data/acme/live",
|
|
"",
|
|
"# 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)",
|
|
"# SHOPWARE_SHOP_ID stable shop slug (same on live + staging)",
|
|
"# SHOPWARE_DEPLOY_ENV this stack: live | staging | playground | \u2026",
|
|
"# 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)",
|
|
"# COMPOSE_PROJECT_NAME scripts/docs only; unset \u2192 ${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}",
|
|
"# create writes COMPOSE_PROJECT_NAME=sw-shop-\u2026 into .env; that",
|
|
"# overrides Compose name:. On the VPS, remove or comment it out.",
|
|
"# This script does not delete it (create owns the local flow).",
|
|
"# SHOPWARE_DATA_BASE bind-mount prefix (default: /var/lib/shopware/data)",
|
|
"# SHOPWARE_DATA_ROOT bind-mount root (default: ${SHOPWARE_DATA_BASE}/<shop>/<env>)",
|
|
"# PULL_POLICY always (default, CI/VPS) | never (same-host tag-and-load / air-gap).",
|
|
"# SKIP_PULL 1/true \u2192 skip `docker compose pull`, set PULL_POLICY=never.",
|
|
"#",
|
|
"# CI-exported IMAGE / IMAGE_TAG / SKIP_PULL / PULL_POLICY always win over .env.",
|
|
"#",
|
|
"# Compose files (shop root as --project-directory):",
|
|
"# deploy/compose.yaml, deploy/compose.prod.yaml, deploy/compose.vps.yaml",
|
|
"#",
|
|
"# compose run uses --pull never (Compose v5 dropped --no-build from the run",
|
|
"# subcommand; do not pass --build). up uses --no-build.",
|
|
"",
|
|
"set -euo pipefail",
|
|
"",
|
|
"usage() {",
|
|
" cat <<'EOF'",
|
|
"Usage: bash deploy/vps-release.sh [--skip-pull]",
|
|
"",
|
|
" --skip-pull Same-host / air-gap: skip registry pull, PULL_POLICY=never",
|
|
"EOF",
|
|
"}",
|
|
"",
|
|
"SKIP_PULL=\"${SKIP_PULL:-}\"",
|
|
"for arg in \"$@\"; do",
|
|
" case \"$arg\" in",
|
|
" --skip-pull) SKIP_PULL=1 ;;",
|
|
" -h|--help)",
|
|
" usage",
|
|
" exit 0",
|
|
" ;;",
|
|
" *)",
|
|
" echo \"Unknown argument: $arg\" >&2",
|
|
" usage",
|
|
" exit 1",
|
|
" ;;",
|
|
" esac",
|
|
"done",
|
|
"",
|
|
"COMPOSE_DIR=\"${COMPOSE_DIR:-$(cd \"$(dirname \"$0\")/..\" && pwd)}\"",
|
|
"cd \"$COMPOSE_DIR\"",
|
|
"",
|
|
"env_truthy() {",
|
|
" local v",
|
|
" v=\"$(printf '%s' \"${1:-}\" | tr '[:upper:]' '[:lower:]')\"",
|
|
" [[ \"$v\" == \"1\" || \"$v\" == \"true\" || \"$v\" == \"yes\" || \"$v\" == \"on\" ]]",
|
|
"}",
|
|
"",
|
|
"CI_IMAGE=\"${IMAGE:-}\"",
|
|
"CI_IMAGE_TAG=\"${IMAGE_TAG:-}\"",
|
|
"CI_SMOKE_URL=\"${SMOKE_URL:-}\"",
|
|
"CI_PROFILES=\"${COMPOSE_PROFILES:-}\"",
|
|
"CI_SKIP_PULL=\"${SKIP_PULL:-}\"",
|
|
"CI_PULL_POLICY=\"${PULL_POLICY:-}\"",
|
|
"",
|
|
"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:-}}\"",
|
|
"if [[ -n \"${CI_SKIP_PULL:-}\" ]]; then",
|
|
" SKIP_PULL=\"${CI_SKIP_PULL}\"",
|
|
"fi",
|
|
"if [[ -n \"${CI_PULL_POLICY:-}\" ]]; then",
|
|
" PULL_POLICY=\"${CI_PULL_POLICY}\"",
|
|
"fi",
|
|
"",
|
|
"# Compose interpolates SHOPWARE_SHOP_ID + SHOPWARE_DEPLOY_ENV from .env",
|
|
"# (optional SHOPWARE_DATA_BASE). Derive COMPOSE_PROJECT_NAME / SHOPWARE_DATA_ROOT",
|
|
"# for scripts when only shop id + deploy env are set.",
|
|
": \"${SHOPWARE_SHOP_ID:?Set SHOPWARE_SHOP_ID in .env (stable shop slug, same on live/staging/laptop)}\"",
|
|
": \"${SHOPWARE_DEPLOY_ENV:?Set SHOPWARE_DEPLOY_ENV in .env (live|staging|playground|dev)}\"",
|
|
"SHOPWARE_DATA_BASE=\"${SHOPWARE_DATA_BASE:-/var/lib/shopware/data}\"",
|
|
"derived_project=\"${SHOPWARE_SHOP_ID}-${SHOPWARE_DEPLOY_ENV}\"",
|
|
"if [[ -z \"${COMPOSE_PROJECT_NAME:-}\" ]]; then",
|
|
" COMPOSE_PROJECT_NAME=\"${derived_project}\"",
|
|
"elif [[ \"$COMPOSE_PROJECT_NAME\" != \"$derived_project\" ]]; then",
|
|
" echo \"WARNING: COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME} is set and overrides Compose name: (${derived_project}).\" >&2",
|
|
" echo \"WARNING: shopware-cli project create writes COMPOSE_PROJECT_NAME=sw-shop-\u2026 into .env for local project dev.\" >&2",
|
|
" echo \"WARNING: On the VPS, remove or comment out that line so the project name is ${derived_project}.\" >&2",
|
|
" echo \"WARNING: This script does not delete it (create owns the local flow).\" >&2",
|
|
"fi",
|
|
"if [[ -z \"${SHOPWARE_DATA_ROOT:-}\" ]]; then",
|
|
" SHOPWARE_DATA_ROOT=\"${SHOPWARE_DATA_BASE}/${SHOPWARE_SHOP_ID}/${SHOPWARE_DEPLOY_ENV}\"",
|
|
"fi",
|
|
"",
|
|
"if env_truthy \"${SKIP_PULL:-}\"; then",
|
|
" PULL_POLICY=never",
|
|
" SKIP_PULL=1",
|
|
"else",
|
|
" PULL_POLICY=\"$(printf '%s' \"${PULL_POLICY:-always}\" | tr '[:upper:]' '[:lower:]')\"",
|
|
" if [[ \"$PULL_POLICY\" == \"never\" ]]; then",
|
|
" SKIP_PULL=1",
|
|
" else",
|
|
" SKIP_PULL=0",
|
|
" fi",
|
|
"fi",
|
|
"",
|
|
": \"${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 SHOPWARE_SHOP_ID SHOPWARE_DEPLOY_ENV SHOPWARE_DATA_BASE PULL_POLICY SKIP_PULL",
|
|
"if [[ -n \"${COMPOSE_PROJECT_NAME:-}\" ]]; then",
|
|
" export COMPOSE_PROJECT_NAME",
|
|
"fi",
|
|
"if [[ -n \"${SHOPWARE_DATA_ROOT:-}\" ]]; then",
|
|
" export SHOPWARE_DATA_ROOT",
|
|
"fi",
|
|
"",
|
|
"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\"",
|
|
"}",
|
|
"",
|
|
"up_pull=()",
|
|
"if [[ \"${SKIP_PULL}\" == \"1\" ]]; then",
|
|
" up_pull=(--pull never)",
|
|
"fi",
|
|
"",
|
|
"echo \"==> Deploying ${IMAGE}:${IMAGE_TAG} from ${COMPOSE_DIR} (compose run --pull never, up --no-build)\"",
|
|
"",
|
|
"if [[ -f .deployed-tag ]]; then",
|
|
" cp .deployed-tag .previous-tag",
|
|
" echo \"==> Previous tag: $(cat .previous-tag)\"",
|
|
"fi",
|
|
"",
|
|
"if [[ \"${SKIP_PULL}\" == \"1\" ]]; then",
|
|
" echo \"==> Skipping registry pull (SKIP_PULL=1 / PULL_POLICY=${PULL_POLICY}); using images already on this host\"",
|
|
"else",
|
|
" echo \"==> Pulling images\"",
|
|
" \"${COMPOSE[@]}\" \"${PROFILE_ARGS[@]}\" pull",
|
|
"fi",
|
|
"",
|
|
"if has_service mysql; then",
|
|
" echo \"==> Starting mysql\"",
|
|
" \"${COMPOSE[@]}\" up -d --no-build ${up_pull[@]+\"${up_pull[@]}\"} mysql",
|
|
"fi",
|
|
"",
|
|
"if has_service redis; then",
|
|
" echo \"==> Starting redis\"",
|
|
" \"${COMPOSE[@]}\" --profile redis up -d --no-build ${up_pull[@]+\"${up_pull[@]}\"} redis",
|
|
"fi",
|
|
"",
|
|
"echo \"==> One-shot setup (shopware-deployment-helper, skip theme/assets)\"",
|
|
"\"${COMPOSE[@]}\" --profile setup run --rm --pull never setup",
|
|
"",
|
|
"echo \"==> Recreating web (no build)\"",
|
|
"\"${COMPOSE[@]}\" up -d --no-build ${up_pull[@]+\"${up_pull[@]}\"} --remove-orphans web",
|
|
"",
|
|
"if [[ ${#PROFILE_ARGS[@]} -gt 0 ]]; then",
|
|
" echo \"==> Starting extra profiles: ${COMPOSE_PROFILES}\"",
|
|
" \"${COMPOSE[@]}\" \"${PROFILE_ARGS[@]}\" up -d --no-build ${up_pull[@]+\"${up_pull[@]}\"}",
|
|
"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": false
|
|
}
|
|
},
|
|
"ref": "eb919b0761534ef249ede74cd996b31671f325de"
|
|
}
|
|
}
|
|
}
|