From c3381bdd31dae78dfd117b97c911fb97610bcdb9 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Tue, 26 May 2026 22:30:10 +0300 Subject: [PATCH] chore: add image publish and mirror sync scripts --- README.md | 13 +++- docs/administration/maintenance.md | 32 +++++++++ docs/deployment.md | 57 ++++++++++----- scripts/docker-build-push-images.ps1 | 53 ++++++++++++++ scripts/docker-build-push-images.sh | 58 +++++++++++++++ scripts/sync-gitlab-mirror.ps1 | 101 +++++++++++++++++++++++++++ scripts/sync-gitlab-mirror.sh | 99 ++++++++++++++++++++++++++ 7 files changed, 394 insertions(+), 19 deletions(-) create mode 100644 scripts/docker-build-push-images.ps1 create mode 100644 scripts/docker-build-push-images.sh create mode 100644 scripts/sync-gitlab-mirror.ps1 create mode 100644 scripts/sync-gitlab-mirror.sh diff --git a/README.md b/README.md index 8123f2b..580be69 100644 --- a/README.md +++ b/README.md @@ -125,11 +125,22 @@ IMAGE_TAG=3.1.0 docker compose up -d Для продакшен-запуска удобнее брать готовые папки из [`deploy/examples`](deploy/examples), а читать каноничные инструкции в [docs/deployment.md](docs/deployment.md). Предпочтительный вариант для обычного публичного сервера - Caddy: он сам выпускает и продлевает HTTPS-сертификаты. В папках рядом с compose лежат только конфиги и короткие ссылки на документацию. -GHCR image names for releases: +Имена образов для релизов: - `ghcr.io/3252a8/remnawave-minishop-backend` - `ghcr.io/3252a8/remnawave-minishop-worker` - `ghcr.io/3252a8/remnawave-minishop-frontend` +- `docker.io/3252a8/remnawave-minishop-backend` +- `docker.io/3252a8/remnawave-minishop-worker` +- `docker.io/3252a8/remnawave-minishop-frontend` + +Сборка и публикация сразу в GHCR и Docker Hub: + +```bash +docker login ghcr.io +docker login docker.io +IMAGE_TAG=v3.4.3 bash scripts/docker-build-push-images.sh +``` ## Поддержать проект diff --git a/docs/administration/maintenance.md b/docs/administration/maintenance.md index 0340eab..6af4feb 100644 --- a/docs/administration/maintenance.md +++ b/docs/administration/maintenance.md @@ -24,4 +24,36 @@ docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES - вход в Mini App и админку - тестовый платеж или тестовая активация +## Синхронизация GitLab-зеркала + +Основное место работы - GitHub remote `origin`. GitLab remote `gitlab` используется как запасное зеркало. + +После пуша в GitHub синхронизируйте зеркало: + +```bash +bash scripts/sync-gitlab-mirror.sh +``` + +PowerShell-вариант: + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\sync-gitlab-mirror.ps1 +``` + +По умолчанию скрипт делает `git fetch origin --prune --tags`, пушит все ветки из `origin/*` в `gitlab` и пушит теги. Он не удаляет ветки в GitLab и не делает force-push. Для строгого зеркалирования доступны флаги: + +```bash +bash scripts/sync-gitlab-mirror.sh --force --prune +``` + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\sync-gitlab-mirror.ps1 -Force -Prune +``` + +Перед опасными режимами можно посмотреть команды без выполнения: + +```bash +bash scripts/sync-gitlab-mirror.sh --force --prune --dry-run +``` + Подробности: [развертывание](../deployment.md) и [логи](../troubleshooting/logs.md). diff --git a/docs/deployment.md b/docs/deployment.md index 6a551d1..9491ddc 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -231,7 +231,7 @@ docker compose build frontend backend worker docker compose up -d ``` -## Образы GHCR +## Образы GHCR и Docker Hub Образы приложения называются единообразно: @@ -239,53 +239,74 @@ docker compose up -d ghcr.io/3252a8/remnawave-minishop-backend: ghcr.io/3252a8/remnawave-minishop-worker: ghcr.io/3252a8/remnawave-minishop-frontend: +docker.io/3252a8/remnawave-minishop-backend: +docker.io/3252a8/remnawave-minishop-worker: +docker.io/3252a8/remnawave-minishop-frontend: ``` -Сборка образов с конкретным тегом: +Чтобы собрать и сразу опубликовать все три образа в GHCR и Docker Hub, сначала выполните логин в оба registry: ```bash -IMAGE_TAG=3.2.0 scripts/docker-build-images.sh +docker login ghcr.io +docker login docker.io +IMAGE_TAG=v3.4.3 bash scripts/docker-build-push-images.sh ``` -Публикация после `docker login ghcr.io`: +PowerShell-вариант: + +```powershell +$env:IMAGE_TAG = "v3.4.3" +docker login ghcr.io +docker login docker.io +powershell -ExecutionPolicy Bypass -File .\scripts\docker-build-push-images.ps1 +``` + +По умолчанию скрипты используют: + +- `IMAGE_REGISTRIES=ghcr.io docker.io` +- `IMAGE_NAMESPACE=3252a8` +- `IMAGE_PREFIX=remnawave-minishop` +- `TARGETS=backend worker frontend` +- `DOCKERFILE=deploy/docker/Dockerfile` + +Если нужен только один registry или другой namespace, переопределите переменные: ```bash -IMAGE_TAG=3.2.0 scripts/docker-push-images.sh +IMAGE_REGISTRIES=ghcr.io IMAGE_TAG=v3.4.3 bash scripts/docker-build-push-images.sh +IMAGE_REGISTRIES="ghcr.io docker.io" IMAGE_NAMESPACE=other IMAGE_TAG=v3.4.3 bash scripts/docker-build-push-images.sh +``` + +Старые раздельные команды тоже остаются: + +```bash +IMAGE_TAG=v3.4.3 scripts/docker-build-images.sh +IMAGE_TAG=v3.4.3 scripts/docker-push-images.sh ``` Для PowerShell есть варианты `scripts/docker-build-images.ps1` и `scripts/docker-push-images.ps1`. Если публикуете образы в другой registry, namespace или с другим префиксом имени, переопределите `IMAGE_NAMESPACE`, `IMAGE_REGISTRY` или `IMAGE_PREFIX`. -Для Docker Hub аккаунта `3252a8` есть отдельный скрипт, который сразу собирает и публикует все три -образа: +Для совместимости оставлены Docker Hub-only скрипты: ```bash docker login -IMAGE_TAG=3.2.0 bash scripts/dockerhub-build-push-images.sh +IMAGE_TAG=v3.4.3 bash scripts/dockerhub-build-push-images.sh ``` PowerShell-вариант: ```powershell -$env:IMAGE_TAG = "3.2.0" +$env:IMAGE_TAG = "v3.4.3" docker login powershell -ExecutionPolicy Bypass -File .\scripts\dockerhub-build-push-images.ps1 ``` -По умолчанию он публикует: - -```text -docker.io/3252a8/remnawave-minishop-backend: -docker.io/3252a8/remnawave-minishop-worker: -docker.io/3252a8/remnawave-minishop-frontend: -``` - Если PowerShell блокирует локальные скрипты ошибкой `PSSecurityException` / Execution Policy, запустите те же скрипты с обходом политики только для текущего процесса: ```powershell -$env:IMAGE_TAG = "3.2.0" +$env:IMAGE_TAG = "v3.4.3" docker login ghcr.io powershell -ExecutionPolicy Bypass -File .\scripts\docker-build-images.ps1 powershell -ExecutionPolicy Bypass -File .\scripts\docker-push-images.ps1 diff --git a/scripts/docker-build-push-images.ps1 b/scripts/docker-build-push-images.ps1 new file mode 100644 index 0000000..1f4b2e6 --- /dev/null +++ b/scripts/docker-build-push-images.ps1 @@ -0,0 +1,53 @@ +$ErrorActionPreference = "Stop" + +if (-not $env:IMAGE_TAG) { + throw "Set IMAGE_TAG to the release tag you want to build and push" +} + +$imageRegistriesRaw = if ($env:IMAGE_REGISTRIES) { $env:IMAGE_REGISTRIES } else { "ghcr.io,docker.io" } +$imageRegistries = @($imageRegistriesRaw -split "[,;\s]+" | Where-Object { $_ }) +$imageNamespace = if ($env:IMAGE_NAMESPACE) { $env:IMAGE_NAMESPACE } else { "3252a8" } +$imageTag = $env:IMAGE_TAG +$imagePrefix = if ($env:IMAGE_PREFIX) { $env:IMAGE_PREFIX } else { "remnawave-minishop" } +$dockerfile = if ($env:DOCKERFILE) { $env:DOCKERFILE } else { "deploy/docker/Dockerfile" } +$targetsRaw = if ($env:TARGETS) { $env:TARGETS } else { "backend,worker,frontend" } +$targets = @($targetsRaw -split "[,;\s]+" | Where-Object { $_ }) + +function Get-ImageName { + param( + [string]$Registry, + [string]$Target + ) + + return "$Registry/$imageNamespace/$imagePrefix-$Target`:$imageTag" +} + +function Build-Image { + param([string]$Target) + + $tagArgs = @() + foreach ($registry in $imageRegistries) { + $tagArgs += @("-t", (Get-ImageName -Registry $registry -Target $Target)) + } + + Write-Host "Building $Target for: $($imageRegistries -join ', ')" -ForegroundColor Cyan + docker build -f $dockerfile --target $Target @tagArgs . +} + +function Push-Image { + param([string]$Target) + + foreach ($registry in $imageRegistries) { + $image = Get-ImageName -Registry $registry -Target $Target + Write-Host "Pushing $image" -ForegroundColor Cyan + docker push $image + } +} + +foreach ($target in $targets) { + Build-Image -Target $target +} + +foreach ($target in $targets) { + Push-Image -Target $target +} diff --git a/scripts/docker-build-push-images.sh b/scripts/docker-build-push-images.sh new file mode 100644 index 0000000..129a0d5 --- /dev/null +++ b/scripts/docker-build-push-images.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_REGISTRIES="${IMAGE_REGISTRIES:-ghcr.io docker.io}" +IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-3252a8}" +IMAGE_TAG="${IMAGE_TAG:?Set IMAGE_TAG to the release tag you want to build and push}" +IMAGE_PREFIX="${IMAGE_PREFIX:-remnawave-minishop}" +DOCKERFILE="${DOCKERFILE:-deploy/docker/Dockerfile}" +TARGETS="${TARGETS:-backend worker frontend}" + +normalize_list() { + local value="$1" + value="${value//,/ }" + value="${value//;/ }" + echo "$value" +} + +read -r -a registries <<< "$(normalize_list "$IMAGE_REGISTRIES")" +read -r -a targets <<< "$(normalize_list "$TARGETS")" + +image_name() { + local registry="$1" + local target="$2" + echo "$registry/$IMAGE_NAMESPACE/$IMAGE_PREFIX-$target:$IMAGE_TAG" +} + +build_image() { + local target="$1" + local tags=() + local registry + + for registry in "${registries[@]}"; do + tags+=("-t" "$(image_name "$registry" "$target")") + done + + echo "Building $target for: ${registries[*]}" + docker build -f "$DOCKERFILE" --target "$target" "${tags[@]}" . +} + +push_image() { + local target="$1" + local registry + local image + + for registry in "${registries[@]}"; do + image="$(image_name "$registry" "$target")" + echo "Pushing $image" + docker push "$image" + done +} + +for target in "${targets[@]}"; do + build_image "$target" +done + +for target in "${targets[@]}"; do + push_image "$target" +done diff --git a/scripts/sync-gitlab-mirror.ps1 b/scripts/sync-gitlab-mirror.ps1 new file mode 100644 index 0000000..a02b6ea --- /dev/null +++ b/scripts/sync-gitlab-mirror.ps1 @@ -0,0 +1,101 @@ +param( + [string]$PrimaryRemote, + [string]$MirrorRemote, + [string[]]$Branches, + [switch]$Force, + [switch]$Prune, + [switch]$NoTags, + [switch]$DryRun +) + +$ErrorActionPreference = "Stop" + +if (-not $PrimaryRemote) { + $PrimaryRemote = if ($env:PRIMARY_REMOTE) { $env:PRIMARY_REMOTE } else { "origin" } +} +if (-not $MirrorRemote) { + $MirrorRemote = if ($env:MIRROR_REMOTE) { $env:MIRROR_REMOTE } else { "gitlab" } +} +if (-not $Branches -and $env:SYNC_BRANCHES) { + $Branches = @($env:SYNC_BRANCHES -split "[,;\s]+" | Where-Object { $_ }) +} +if ($env:FORCE -eq "true") { + $Force = $true +} +if ($env:PRUNE -eq "true") { + $Prune = $true +} +if ($env:SYNC_TAGS -eq "false") { + $NoTags = $true +} +if ($env:DRY_RUN -eq "true") { + $DryRun = $true +} + +function Invoke-Git { + param([string[]]$Arguments) + + if ($DryRun) { + Write-Host ("git " + ($Arguments -join " ")) + return + } + + & git @Arguments + if ($LASTEXITCODE -ne 0) { + throw "git $($Arguments -join ' ') failed with exit code $LASTEXITCODE" + } +} + +Invoke-Git -Arguments @("fetch", $PrimaryRemote, "--prune", "--tags") + +if ($Force) { + Invoke-Git -Arguments @("fetch", $MirrorRemote, "--prune") +} + +if (-not $Branches) { + $Branches = @( + git for-each-ref "--format=%(refname:strip=3)" "refs/remotes/$PrimaryRemote" | + Where-Object { $_ -and $_ -ne "HEAD" } + ) +} + +foreach ($branch in $Branches) { + if (-not $branch) { + continue + } + + $src = "refs/remotes/$PrimaryRemote/$branch" + $dst = "refs/heads/$branch" + $args = @("push", $MirrorRemote) + if ($Force) { + $args += "--force-with-lease=$dst" + } + $args += "${src}:${dst}" + Invoke-Git -Arguments $args +} + +if (-not $NoTags) { + Invoke-Git -Arguments @("push", $MirrorRemote, "--tags") +} + +if ($Prune) { + $primaryBranchSet = @{} + foreach ($branch in $Branches) { + $primaryBranchSet[$branch] = $true + } + + $mirrorBranches = @( + git ls-remote --heads $MirrorRemote | + ForEach-Object { + if ($_ -match "refs/heads/(.+)$") { + $Matches[1] + } + } + ) + + foreach ($mirrorBranch in $mirrorBranches) { + if (-not $primaryBranchSet.ContainsKey($mirrorBranch)) { + Invoke-Git -Arguments @("push", $MirrorRemote, ":refs/heads/$mirrorBranch") + } + } +} diff --git a/scripts/sync-gitlab-mirror.sh b/scripts/sync-gitlab-mirror.sh new file mode 100644 index 0000000..4d9a292 --- /dev/null +++ b/scripts/sync-gitlab-mirror.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +PRIMARY_REMOTE="${PRIMARY_REMOTE:-origin}" +MIRROR_REMOTE="${MIRROR_REMOTE:-gitlab}" +SYNC_BRANCHES="${SYNC_BRANCHES:-}" +SYNC_TAGS="${SYNC_TAGS:-true}" +FORCE="${FORCE:-false}" +PRUNE="${PRUNE:-false}" +DRY_RUN="${DRY_RUN:-false}" + +usage() { + cat <<'EOF' +Usage: scripts/sync-gitlab-mirror.sh [--force] [--prune] [--no-tags] [--dry-run] + +Mirrors branches and tags from the primary GitHub remote to the GitLab backup remote. + +Environment: + PRIMARY_REMOTE Primary remote name. Default: origin + MIRROR_REMOTE Mirror remote name. Default: gitlab + SYNC_BRANCHES Space/comma separated branch list. Default: all PRIMARY_REMOTE branches + SYNC_TAGS Push tags too. Default: true + FORCE Use --force-with-lease for branches. Default: false + PRUNE Delete GitLab branches missing on GitHub. Default: false + DRY_RUN Print git commands without running them. Default: false +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --force) FORCE="true" ;; + --prune) PRUNE="true" ;; + --no-tags) SYNC_TAGS="false" ;; + --dry-run) DRY_RUN="true" ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage; exit 2 ;; + esac + shift +done + +run_git() { + if [[ "$DRY_RUN" == "true" ]]; then + printf 'git' + printf ' %q' "$@" + printf '\n' + return + fi + + git "$@" +} + +normalize_list() { + local value="$1" + value="${value//,/ }" + value="${value//;/ }" + echo "$value" +} + +run_git fetch "$PRIMARY_REMOTE" --prune --tags + +if [[ "$FORCE" == "true" ]]; then + run_git fetch "$MIRROR_REMOTE" --prune +fi + +if [[ -n "$SYNC_BRANCHES" ]]; then + read -r -a branches <<< "$(normalize_list "$SYNC_BRANCHES")" +else + branches=() + while IFS= read -r branch; do + [[ "$branch" != "HEAD" ]] || continue + branches+=("$branch") + done < <(git for-each-ref --format='%(refname:strip=3)' "refs/remotes/$PRIMARY_REMOTE") +fi + +for branch in "${branches[@]}"; do + [[ -n "$branch" ]] || continue + src="refs/remotes/$PRIMARY_REMOTE/$branch" + dst="refs/heads/$branch" + args=("push" "$MIRROR_REMOTE") + if [[ "$FORCE" == "true" ]]; then + args+=("--force-with-lease=$dst") + fi + args+=("$src:$dst") + run_git "${args[@]}" +done + +if [[ "$SYNC_TAGS" == "true" ]]; then + run_git push "$MIRROR_REMOTE" --tags +fi + +if [[ "$PRUNE" == "true" ]]; then + primary_branches="$(printf '%s\n' "${branches[@]}")" + while read -r mirror_branch; do + [[ -n "$mirror_branch" ]] || continue + if ! grep -Fxq "$mirror_branch" <<< "$primary_branches"; then + run_git push "$MIRROR_REMOTE" ":refs/heads/$mirror_branch" + fi + done < <(git ls-remote --heads "$MIRROR_REMOTE" | sed 's#.*refs/heads/##') +fi