feat: add digest-gated internal main deployment
Some checks failed
Mostovik Backend CI/CD / Tests and lint (push) Failing after 5s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Has been skipped
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
Some checks failed
Mostovik Backend CI/CD / Tests and lint (push) Failing after 5s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Has been skipped
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
This commit is contained in:
@@ -1,446 +1,324 @@
|
||||
name: CI/CD Pipeline
|
||||
name: Mostovik Backend CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- "feature/**"
|
||||
- "codex/**"
|
||||
# Add main only after the first manual main restore/rollback drill passes.
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: mostovik-backend-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: mostovik-backend-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.11"
|
||||
REGISTRY_API_URL: "https://registry.dev.nii-ecos.ru/v2/"
|
||||
REGISTRY_HOST: "registry.dev.nii-ecos.ru"
|
||||
REGISTRY_NAMESPACE: "${{ github.repository_owner }}"
|
||||
WEB_IMAGE: "mostovik-backend-web"
|
||||
CELERY_IMAGE: "mostovik-backend-celery"
|
||||
CI_GOLDEN_IMAGE: "mostovik-backend-ci-golden"
|
||||
WEB_GOLDEN_IMAGE: "mostovik-backend-web-golden"
|
||||
CELERY_GOLDEN_IMAGE: "mostovik-backend-celery-golden"
|
||||
GOLDEN_TAG: "py311-uv0.7.2"
|
||||
UV_VERSION: "0.7.2"
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||
REGISTRY_HOST: registry.dev.nii-ecos.ru
|
||||
REGISTRY_NAMESPACE: ${{ github.repository_owner }}
|
||||
WEB_IMAGE: mostovik-backend-web
|
||||
CELERY_IMAGE: mostovik-backend-celery
|
||||
CUSTOMER_DEPLOY_HOST: 10.0.10.174
|
||||
CUSTOMER_DEPLOY_USER: ecos
|
||||
CUSTOMER_SSH_PROXY_HOST: ecos-proxy@10.10.0.121
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
name: Quality Gate
|
||||
name: Tests and lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
- name: Checkout exact event revision
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run quality gate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
||||
git -c core.hooksPath=/dev/null clone --depth=1 --branch="${BRANCH}" "${REPO_URL}" .
|
||||
git -c core.hooksPath=/dev/null checkout "${GITHUB_SHA}"
|
||||
ci_image="mostovik-backend-ci:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
docker build \
|
||||
--file docker/Dockerfile \
|
||||
--target ci-deps-base \
|
||||
--tag "${ci_image}" \
|
||||
.
|
||||
|
||||
- name: Free Docker space
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker system df || true
|
||||
docker buildx prune --all --force || true
|
||||
docker builder prune --all --force || true
|
||||
docker system prune --all --force --volumes || true
|
||||
docker system df || true
|
||||
|
||||
- name: Run quality in golden image
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
SKIP_LINT: ${{ contains(github.event.head_commit.message, '#no_lint') }}
|
||||
SKIP_TEST: ${{ contains(github.event.head_commit.message, '#no_test') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY_PATH="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}"
|
||||
CI_GOLDEN_REF="${REGISTRY_PATH}/${CI_GOLDEN_IMAGE}"
|
||||
REGISTRY_USER="${REGISTRY_USER:-${GITHUB_ACTOR}}"
|
||||
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-${GITEA_TOKEN:-}}"
|
||||
|
||||
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY
|
||||
export NO_PROXY="${NO_PROXY:-},${REGISTRY_HOST}"
|
||||
export no_proxy="${no_proxy:-},${REGISTRY_HOST}"
|
||||
|
||||
if [ -z "${REGISTRY_PASSWORD}" ]; then
|
||||
echo "REGISTRY_PASSWORD secret is not set and GITEA_TOKEN fallback is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${REGISTRY_PASSWORD}" \
|
||||
| docker login "${REGISTRY_HOST}" \
|
||||
-u "${REGISTRY_USER}" \
|
||||
--password-stdin
|
||||
|
||||
if ! docker buildx inspect mostovik-builder >/dev/null 2>&1; then
|
||||
docker buildx create --name mostovik-builder --use
|
||||
else
|
||||
docker buildx use mostovik-builder
|
||||
fi
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
lockfile_sha="$(sha256sum uv.lock | awk '{ print $1 }')"
|
||||
|
||||
image_usable() {
|
||||
docker run --rm "${CI_GOLDEN_REF}:${GOLDEN_TAG}" \
|
||||
bash -c "
|
||||
set -euo pipefail
|
||||
test -f /app/uv.lock
|
||||
test \"\$(sha256sum /app/uv.lock | awk '{ print \$1 }')\" = '${lockfile_sha}'
|
||||
/app/.venv/bin/ruff --version >/dev/null
|
||||
/app/.venv/bin/python -m pytest --version >/dev/null
|
||||
"
|
||||
}
|
||||
|
||||
if ! docker buildx imagetools inspect "${CI_GOLDEN_REF}:${GOLDEN_TAG}" >/dev/null 2>&1 || ! image_usable; then
|
||||
docker buildx prune --all --force || true
|
||||
docker builder prune --all --force || true
|
||||
docker buildx build \
|
||||
-f ./docker/Dockerfile \
|
||||
--target ci-deps-base \
|
||||
--push \
|
||||
-t "${CI_GOLDEN_REF}:${GOLDEN_TAG}" \
|
||||
-t "${CI_GOLDEN_REF}:latest" \
|
||||
.
|
||||
docker pull "${CI_GOLDEN_REF}:${GOLDEN_TAG}"
|
||||
fi
|
||||
|
||||
tar --exclude=.git -cf - . \
|
||||
tar --exclude=.git --exclude=.venv --exclude=.ruff_cache -cf - . \
|
||||
| docker run --rm -i \
|
||||
-e DJANGO_SETTINGS_MODULE=settings.test \
|
||||
-e SECRET_KEY=test-secret-key-for-ci \
|
||||
-e SKIP_LINT="${SKIP_LINT}" \
|
||||
-e SKIP_TEST="${SKIP_TEST}" \
|
||||
"${CI_GOLDEN_REF}:${GOLDEN_TAG}" \
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
mkdir -p /workspace
|
||||
tar -xf - -C /workspace
|
||||
cd /workspace
|
||||
export PATH="/app/.venv/bin:${PATH}"
|
||||
export PYTHONPATH="/workspace/src:${PYTHONPATH:-}"
|
||||
if [ "${SKIP_LINT}" != "true" ]; then
|
||||
/app/.venv/bin/ruff check src
|
||||
/app/.venv/bin/ruff format src --check
|
||||
fi
|
||||
if [ "${SKIP_TEST}" != "true" ]; then
|
||||
/app/.venv/bin/python -m pytest tests --ignore=tests/test_api_inventory_e2e.py -q
|
||||
/app/.venv/bin/python -m pytest tests/test_api_inventory_e2e.py -q
|
||||
fi
|
||||
'
|
||||
--env DJANGO_SETTINGS_MODULE=settings.test \
|
||||
--env SECRET_KEY=test-secret-key-for-ci \
|
||||
"${ci_image}" \
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
mkdir -p /workspace
|
||||
tar -xf - -C /workspace
|
||||
cd /workspace
|
||||
export PATH="/app/.venv/bin:${PATH}"
|
||||
export PYTHONPATH="/workspace/src:${PYTHONPATH:-}"
|
||||
ruff check src
|
||||
ruff format src --check
|
||||
python -m pytest tests --ignore=tests/test_api_inventory_e2e.py -q
|
||||
python -m pytest tests/test_api_inventory_e2e.py -q
|
||||
'
|
||||
|
||||
build_push:
|
||||
name: Build and Push Images
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
build:
|
||||
name: Build linux/amd64 release images
|
||||
runs-on: [backend-docker]
|
||||
timeout-minutes: 60
|
||||
needs: [quality]
|
||||
if: needs.quality.result == 'success'
|
||||
if: ${{ needs.quality.result == 'success' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') }}
|
||||
outputs:
|
||||
web_ref: ${{ steps.release.outputs.web_ref }}
|
||||
celery_ref: ${{ steps.release.outputs.celery_ref }}
|
||||
web_tag_ref: ${{ steps.release.outputs.web_tag_ref }}
|
||||
celery_tag_ref: ${{ steps.release.outputs.celery_tag_ref }}
|
||||
|
||||
steps:
|
||||
- name: Check whether image build is required
|
||||
- name: Checkout exact event revision
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build once and publish immutable digests
|
||||
id: release
|
||||
env:
|
||||
HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" != "push" ]; then
|
||||
echo "Skip image build for ${GITHUB_EVENT_NAME}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${GITHUB_REF}" != "refs/heads/dev" ]; then
|
||||
echo "Skip image build for ${GITHUB_REF}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "${HEAD_COMMIT_MESSAGE:-}" in
|
||||
*"#no_image"*)
|
||||
echo "Skip image build because commit message contains #no_image"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Image build is required for ${GITHUB_REF}"
|
||||
|
||||
- name: Checkout code
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' && !contains(github.event.head_commit.message, '#no_image') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
||||
git -c core.hooksPath=/dev/null clone --depth=1 --branch="${BRANCH}" "${REPO_URL}" .
|
||||
git -c core.hooksPath=/dev/null checkout "${GITHUB_SHA}"
|
||||
|
||||
- name: Free Docker build space
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' && !contains(github.event.head_commit.message, '#no_image') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker system df || true
|
||||
docker buildx prune --all --force || true
|
||||
docker builder prune --all --force || true
|
||||
docker system prune --all --force --volumes || true
|
||||
docker system df || true
|
||||
|
||||
- name: Build and push dev images
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' && !contains(github.event.head_commit.message, '#no_image') }}
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:-branch}}"
|
||||
BRANCH_TAG=$(printf '%s' "${BRANCH}" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed -E 's#[/[:space:]]+#-#g; s#[^a-z0-9_.-]+#-#g; s#^-+##; s#-+$##')
|
||||
BRANCH_TAG="${BRANCH_TAG:-branch}"
|
||||
BRANCH_TAG=$(printf '%.120s' "${BRANCH_TAG}")
|
||||
SHA_SHORT=$(printf '%s' "${GITHUB_SHA}" | cut -c1-7)
|
||||
|
||||
REGISTRY_PATH="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}"
|
||||
WEB_REF="${REGISTRY_PATH}/${WEB_IMAGE}"
|
||||
CELERY_REF="${REGISTRY_PATH}/${CELERY_IMAGE}"
|
||||
WEB_GOLDEN_REF="${REGISTRY_PATH}/${WEB_GOLDEN_IMAGE}"
|
||||
CELERY_GOLDEN_REF="${REGISTRY_PATH}/${CELERY_GOLDEN_IMAGE}"
|
||||
REGISTRY_USER="${REGISTRY_USER:-${GITHUB_ACTOR}}"
|
||||
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-${GITEA_TOKEN:-}}"
|
||||
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY
|
||||
export NO_PROXY="${NO_PROXY:-},${REGISTRY_HOST}"
|
||||
export no_proxy="${no_proxy:-},${REGISTRY_HOST}"
|
||||
|
||||
if [ -z "${REGISTRY_PASSWORD}" ]; then
|
||||
echo "REGISTRY_PASSWORD secret is not set and GITEA_TOKEN fallback is empty" >&2
|
||||
if [ -z "${REGISTRY_USERNAME:-}" ] || [ -z "${REGISTRY_PASSWORD:-}" ]; then
|
||||
echo "REGISTRY_USERNAME and REGISTRY_PASSWORD secrets are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${REGISTRY_PASSWORD}" \
|
||||
| docker login "${REGISTRY_HOST}" \
|
||||
-u "${REGISTRY_USER}" \
|
||||
--password-stdin
|
||||
printf '%s' "${REGISTRY_PASSWORD}" \
|
||||
| docker login "${REGISTRY_HOST}" --username "${REGISTRY_USERNAME}" --password-stdin
|
||||
|
||||
WEB_TAGS=(
|
||||
-t "${WEB_REF}:dev-${SHA_SHORT}"
|
||||
-t "${WEB_REF}:dev"
|
||||
)
|
||||
CELERY_TAGS=(
|
||||
-t "${CELERY_REF}:dev-${SHA_SHORT}"
|
||||
-t "${CELERY_REF}:dev"
|
||||
)
|
||||
|
||||
if ! docker buildx inspect mostovik-builder >/dev/null 2>&1; then
|
||||
docker buildx create --name mostovik-builder --use
|
||||
else
|
||||
docker buildx use mostovik-builder
|
||||
fi
|
||||
builder="mostovik-backend-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
docker buildx create --name "${builder}" --driver docker-container --use
|
||||
cleanup() {
|
||||
docker buildx rm "${builder}" >/dev/null 2>&1 || true
|
||||
rm -f web-metadata.json celery-metadata.json
|
||||
}
|
||||
trap cleanup EXIT
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
lockfile_sha="$(sha256sum uv.lock | awk '{ print $1 }')"
|
||||
|
||||
golden_usable() {
|
||||
local ref="$1"
|
||||
|
||||
docker run --rm "${ref}:${GOLDEN_TAG}" \
|
||||
bash -c "
|
||||
set -euo pipefail
|
||||
test -f /app/uv.lock
|
||||
test \"\$(sha256sum /app/uv.lock | awk '{ print \$1 }')\" = '${lockfile_sha}'
|
||||
/app/.venv/bin/python -c 'import polymorphic'
|
||||
"
|
||||
}
|
||||
|
||||
ensure_golden() {
|
||||
local target="$1"
|
||||
local ref="$2"
|
||||
local build_args=()
|
||||
if [ "${target}" = "celery-deps-base" ]; then
|
||||
build_args+=(--build-arg "GOLDEN_WEB_IMAGE=${WEB_GOLDEN_REF}:${GOLDEN_TAG}")
|
||||
fi
|
||||
if docker buildx imagetools inspect "${ref}:${GOLDEN_TAG}" >/dev/null 2>&1 \
|
||||
&& golden_usable "${ref}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker buildx prune --all --force || true
|
||||
docker builder prune --all --force || true
|
||||
docker buildx build \
|
||||
-f ./docker/Dockerfile \
|
||||
--target "${target}" \
|
||||
"${build_args[@]}" \
|
||||
--push \
|
||||
-t "${ref}:${GOLDEN_TAG}" \
|
||||
-t "${ref}:latest" \
|
||||
.
|
||||
}
|
||||
|
||||
ensure_golden "web-deps-base" "${WEB_GOLDEN_REF}"
|
||||
ensure_golden "celery-deps-base" "${CELERY_GOLDEN_REF}"
|
||||
sha_short="$(printf '%s' "${GITHUB_SHA}" | cut -c1-12)"
|
||||
image_tag="${GITHUB_REF_NAME}-${sha_short}"
|
||||
registry_path="${REGISTRY_HOST}/${REGISTRY_NAMESPACE}"
|
||||
web_repository="${registry_path}/${WEB_IMAGE}"
|
||||
celery_repository="${registry_path}/${CELERY_IMAGE}"
|
||||
build_time="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
docker buildx build \
|
||||
-f ./docker/Dockerfile \
|
||||
--platform linux/amd64 \
|
||||
--file docker/Dockerfile \
|
||||
--target runtime-web \
|
||||
--build-arg INSTALL_DEV=false \
|
||||
--build-arg GOLDEN_WEB_IMAGE="${WEB_GOLDEN_REF}:${GOLDEN_TAG}" \
|
||||
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
|
||||
--label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
|
||||
--label "org.opencontainers.image.created=${BUILD_TIME}" \
|
||||
--label "org.opencontainers.image.created=${build_time}" \
|
||||
--metadata-file web-metadata.json \
|
||||
--push \
|
||||
"${WEB_TAGS[@]}" \
|
||||
--tag "${web_repository}:${image_tag}" \
|
||||
.
|
||||
|
||||
docker buildx build \
|
||||
-f ./docker/Dockerfile \
|
||||
--platform linux/amd64 \
|
||||
--file docker/Dockerfile \
|
||||
--target runtime-celery \
|
||||
--build-arg INSTALL_DEV=false \
|
||||
--build-arg GOLDEN_CELERY_IMAGE="${CELERY_GOLDEN_REF}:${GOLDEN_TAG}" \
|
||||
--label "org.opencontainers.image.revision=${GITHUB_SHA}" \
|
||||
--label "org.opencontainers.image.source=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
|
||||
--label "org.opencontainers.image.created=${BUILD_TIME}" \
|
||||
--label "org.opencontainers.image.created=${build_time}" \
|
||||
--metadata-file celery-metadata.json \
|
||||
--push \
|
||||
"${CELERY_TAGS[@]}" \
|
||||
--tag "${celery_repository}:${image_tag}" \
|
||||
.
|
||||
|
||||
web_digest="$(jq -r '."containerimage.digest" // empty' web-metadata.json)"
|
||||
celery_digest="$(jq -r '."containerimage.digest" // empty' celery-metadata.json)"
|
||||
printf '%s\n' "${web_digest}" | grep -Eq '^sha256:[0-9a-f]{64}$'
|
||||
printf '%s\n' "${celery_digest}" | grep -Eq '^sha256:[0-9a-f]{64}$'
|
||||
|
||||
{
|
||||
echo "Registry API: ${REGISTRY_API_URL}"
|
||||
echo "Golden images:"
|
||||
echo "- ${WEB_GOLDEN_REF}:${GOLDEN_TAG}"
|
||||
echo "- ${CELERY_GOLDEN_REF}:${GOLDEN_TAG}"
|
||||
echo "Pushed images:"
|
||||
echo "- ${WEB_REF}:dev-${SHA_SHORT}"
|
||||
echo "- ${WEB_REF}:dev"
|
||||
echo "- ${CELERY_REF}:dev-${SHA_SHORT}"
|
||||
echo "- ${CELERY_REF}:dev"
|
||||
} >> "${GITHUB_STEP_SUMMARY:-/dev/stdout}"
|
||||
|
||||
notify:
|
||||
name: Internal Notify
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
needs: [quality, build_push]
|
||||
if: ${{ always() && github.event_name != 'workflow_dispatch' }}
|
||||
|
||||
steps:
|
||||
- name: Send CI status webhook
|
||||
continue-on-error: true
|
||||
env:
|
||||
CI_NOTIFY_WEBHOOK_URL: ${{ secrets.CI_NOTIFY_WEBHOOK_URL }}
|
||||
CI_NOTIFY_TOKEN: ${{ secrets.CI_NOTIFY_TOKEN }}
|
||||
QUALITY_RESULT: ${{ needs.quality.result }}
|
||||
BUILD_PUSH_RESULT: ${{ needs.build_push.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_NOTIFY_WEBHOOK_URL:-}" ]; then
|
||||
echo "CI_NOTIFY_WEBHOOK_URL is not set; skip internal notification"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
STATUS="success"
|
||||
if [ "${QUALITY_RESULT}" != "success" ]; then
|
||||
STATUS="${QUALITY_RESULT}"
|
||||
elif [ "${BUILD_PUSH_RESULT}" = "failure" ] || [ "${BUILD_PUSH_RESULT}" = "cancelled" ]; then
|
||||
STATUS="${BUILD_PUSH_RESULT}"
|
||||
fi
|
||||
export STATUS
|
||||
|
||||
PAYLOAD=$(python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
payload = {
|
||||
"project": os.environ.get("GITHUB_REPOSITORY"),
|
||||
"workflow": os.environ.get("GITHUB_WORKFLOW"),
|
||||
"status": os.environ.get("STATUS"),
|
||||
"branch": os.environ.get("GITHUB_HEAD_REF") or os.environ.get("GITHUB_REF_NAME"),
|
||||
"sha": os.environ.get("GITHUB_SHA"),
|
||||
"actor": os.environ.get("GITHUB_ACTOR"),
|
||||
"server_url": os.environ.get("GITHUB_SERVER_URL"),
|
||||
"run_id": os.environ.get("GITHUB_RUN_ID"),
|
||||
"results": {
|
||||
"quality": os.environ.get("QUALITY_RESULT"),
|
||||
"build_push": os.environ.get("BUILD_PUSH_RESULT"),
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=True, separators=(",", ":")))
|
||||
PY
|
||||
)
|
||||
|
||||
AUTH_HEADER=()
|
||||
if [ -n "${CI_NOTIFY_TOKEN:-}" ]; then
|
||||
AUTH_HEADER=(-H "Authorization: Bearer ${CI_NOTIFY_TOKEN}")
|
||||
fi
|
||||
|
||||
curl -fsS \
|
||||
--connect-timeout 3 \
|
||||
--max-time 8 \
|
||||
--retry 1 \
|
||||
-H "Content-Type: application/json" \
|
||||
"${AUTH_HEADER[@]}" \
|
||||
--data "${PAYLOAD}" \
|
||||
"${CI_NOTIFY_WEBHOOK_URL}"
|
||||
echo "web_ref=${web_repository}@${web_digest}"
|
||||
echo "celery_ref=${celery_repository}@${celery_digest}"
|
||||
echo "web_tag_ref=${web_repository}:${image_tag}"
|
||||
echo "celery_tag_ref=${celery_repository}:${image_tag}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
deploy_dev:
|
||||
name: Deploy Dev via Compose
|
||||
name: Deploy dev
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
needs: [build_push]
|
||||
if: needs.build_push.result == 'success'
|
||||
timeout-minutes: 15
|
||||
needs: [build]
|
||||
if: ${{ github.ref == 'refs/heads/dev' && needs.build.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
||||
git -c core.hooksPath=/dev/null clone --depth=1 --branch="${BRANCH}" "${REPO_URL}" .
|
||||
git -c core.hooksPath=/dev/null checkout "${GITHUB_SHA}"
|
||||
|
||||
- name: Deploy prebuilt images via SSH
|
||||
- name: Invoke dev release wrapper
|
||||
env:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }}
|
||||
HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
||||
INTERNAL_DEPLOY_SSH_KEY_B64: ${{ secrets.INTERNAL_DEPLOY_SSH_KEY_B64 }}
|
||||
INTERNAL_KNOWN_HOSTS_B64: ${{ secrets.INTERNAL_KNOWN_HOSTS_B64 }}
|
||||
WEB_REF: ${{ needs.build.outputs.web_ref }}
|
||||
CELERY_REF: ${{ needs.build.outputs.celery_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${GITHUB_REF}" != "refs/heads/dev" ]; then
|
||||
echo "Skip dev deploy for ${GITHUB_REF}"
|
||||
exit 0
|
||||
if [ -z "${DEPLOY_HOST:-}" ] || [ -z "${DEPLOY_USER:-}" ] || [ -z "${INTERNAL_DEPLOY_SSH_KEY_B64:-}" ] || [ -z "${INTERNAL_KNOWN_HOSTS_B64:-}" ]; then
|
||||
echo "Internal deploy SSH secrets are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "${DEPLOY_USER}" in *[!A-Za-z0-9._-]*) echo "DEPLOY_USER has unsupported characters" >&2; exit 1;; esac
|
||||
case "${DEPLOY_HOST}" in *[!A-Za-z0-9.:-]*) echo "DEPLOY_HOST has unsupported characters" >&2; exit 1;; esac
|
||||
printf '%s\n' "${WEB_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
|
||||
printf '%s\n' "${CELERY_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
|
||||
|
||||
case "${HEAD_COMMIT_MESSAGE:-}" in
|
||||
*"#no_deploy"* | *"#no_image"*)
|
||||
echo "Skip dev deploy because commit message disables deploy or image build"
|
||||
ssh_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${ssh_dir}"' EXIT
|
||||
key_path="${ssh_dir}/deploy_key"
|
||||
known_hosts_path="${ssh_dir}/known_hosts"
|
||||
printf '%s' "${INTERNAL_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}"
|
||||
printf '%s' "${INTERNAL_KNOWN_HOSTS_B64}" | base64 -d > "${known_hosts_path}"
|
||||
chmod 600 "${key_path}" "${known_hosts_path}"
|
||||
ssh-keygen -y -f "${key_path}" >/dev/null
|
||||
|
||||
ssh \
|
||||
-i "${key_path}" \
|
||||
-o BatchMode=yes \
|
||||
-o IdentitiesOnly=yes \
|
||||
-o StrictHostKeyChecking=yes \
|
||||
-o "UserKnownHostsFile=${known_hosts_path}" \
|
||||
-o GlobalKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=15 \
|
||||
"${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||
/opt/ecos-dev/release-service \
|
||||
mostovik-backend \
|
||||
"${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
|
||||
"${WEB_REF}" \
|
||||
"${CELERY_REF}"
|
||||
|
||||
deploy_internal_main:
|
||||
name: Deploy and verify internal main
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs: [build]
|
||||
if: ${{ github.ref == 'refs/heads/main' && needs.build.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Refresh clone and invoke internal-main release wrapper
|
||||
env:
|
||||
MAIN_DEPLOY_HOST: ${{ secrets.MAIN_DEPLOY_HOST }}
|
||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
INTERNAL_DEPLOY_SSH_KEY_B64: ${{ secrets.INTERNAL_DEPLOY_SSH_KEY_B64 }}
|
||||
INTERNAL_KNOWN_HOSTS_B64: ${{ secrets.INTERNAL_KNOWN_HOSTS_B64 }}
|
||||
INTERNAL_MAIN_MOSTOVIK_BACKEND_URL: ${{ secrets.INTERNAL_MAIN_MOSTOVIK_BACKEND_URL }}
|
||||
WEB_REF: ${{ needs.build.outputs.web_ref }}
|
||||
CELERY_REF: ${{ needs.build.outputs.celery_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${MAIN_DEPLOY_HOST:-}" ] || [ -z "${DEPLOY_USER:-}" ] || [ -z "${INTERNAL_DEPLOY_SSH_KEY_B64:-}" ] || [ -z "${INTERNAL_KNOWN_HOSTS_B64:-}" ] || [ -z "${INTERNAL_MAIN_MOSTOVIK_BACKEND_URL:-}" ]; then
|
||||
echo "Internal-main deploy and smoke secrets are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "${DEPLOY_USER}" in *[!A-Za-z0-9._-]*) echo "DEPLOY_USER has unsupported characters" >&2; exit 1;; esac
|
||||
case "${MAIN_DEPLOY_HOST}" in *[!A-Za-z0-9.:-]*) echo "MAIN_DEPLOY_HOST has unsupported characters" >&2; exit 1;; esac
|
||||
printf '%s\n' "${WEB_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
|
||||
printf '%s\n' "${CELERY_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
|
||||
|
||||
ssh_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${ssh_dir}"' EXIT
|
||||
key_path="${ssh_dir}/deploy_key"
|
||||
known_hosts_path="${ssh_dir}/known_hosts"
|
||||
printf '%s' "${INTERNAL_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}"
|
||||
printf '%s' "${INTERNAL_KNOWN_HOSTS_B64}" | base64 -d > "${known_hosts_path}"
|
||||
chmod 600 "${key_path}" "${known_hosts_path}"
|
||||
ssh-keygen -y -f "${key_path}" >/dev/null
|
||||
|
||||
ssh \
|
||||
-i "${key_path}" \
|
||||
-o BatchMode=yes \
|
||||
-o IdentitiesOnly=yes \
|
||||
-o StrictHostKeyChecking=yes \
|
||||
-o "UserKnownHostsFile=${known_hosts_path}" \
|
||||
-o GlobalKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=15 \
|
||||
"${DEPLOY_USER}@${MAIN_DEPLOY_HOST}" \
|
||||
/opt/ecos-main/release-service \
|
||||
mostovik-backend \
|
||||
"${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
|
||||
"${WEB_REF}" \
|
||||
"${CELERY_REF}" \
|
||||
--refresh-data
|
||||
|
||||
smoke_url="${INTERNAL_MAIN_MOSTOVIK_BACKEND_URL%/}/health/ready/"
|
||||
for attempt in $(seq 1 30); do
|
||||
if curl --fail --silent --output /dev/null --max-time 10 "${smoke_url}"; then
|
||||
echo "Internal-main backend readiness passed"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
echo "Internal-main backend readiness attempt ${attempt}/30 failed"
|
||||
sleep 5
|
||||
done
|
||||
echo "Internal-main backend readiness failed" >&2
|
||||
exit 1
|
||||
|
||||
short_sha="$(printf '%s' "${GITHUB_SHA}" | cut -c1-7)"
|
||||
image_tag="dev-${short_sha}"
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s' "${DEPLOY_SSH_KEY}" | base64 -d > ~/.ssh/ecos_deploy_key
|
||||
chmod 0600 ~/.ssh/ecos_deploy_key
|
||||
ssh-keyscan -H "${DEPLOY_HOST}" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
tmp_current="$(mktemp)"
|
||||
ssh -i ~/.ssh/ecos_deploy_key "${DEPLOY_USER}@${DEPLOY_HOST}" 'cat /opt/ecos-dev/releases/current.env' > "${tmp_current}"
|
||||
grep -v '^MOSTOVIK_BACKEND_' "${tmp_current}" > "${tmp_current}.new"
|
||||
cat >> "${tmp_current}.new" <<EOF
|
||||
MOSTOVIK_BACKEND_WEB_IMAGE=${REGISTRY_HOST}/avm/mostovik-backend-web:${image_tag}
|
||||
MOSTOVIK_BACKEND_CELERY_IMAGE=${REGISTRY_HOST}/avm/mostovik-backend-celery:${image_tag}
|
||||
EOF
|
||||
scp -i ~/.ssh/ecos_deploy_key "${tmp_current}.new" "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/current.env"
|
||||
ssh -i ~/.ssh/ecos_deploy_key "${DEPLOY_USER}@${DEPLOY_HOST}" 'cat /tmp/current.env > /opt/ecos-dev/releases/current.env && rm -f /tmp/current.env && /opt/ecos-dev/deploy.sh mostovik-backend'
|
||||
deploy_customer_main:
|
||||
name: Deploy customer main
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs: [build, deploy_internal_main]
|
||||
if: ${{ github.ref == 'refs/heads/main' && needs.build.result == 'success' && needs.deploy_internal_main.result == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Invoke customer release wrapper with the verified digests
|
||||
env:
|
||||
CUSTOMER_DEPLOY_SSH_KEY_B64: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY_B64 }}
|
||||
CUSTOMER_KNOWN_HOSTS_B64: ${{ secrets.CUSTOMER_KNOWN_HOSTS_B64 }}
|
||||
WEB_REF: ${{ needs.build.outputs.web_ref }}
|
||||
CELERY_REF: ${{ needs.build.outputs.celery_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for name in CUSTOMER_DEPLOY_HOST CUSTOMER_DEPLOY_USER CUSTOMER_SSH_PROXY_HOST CUSTOMER_DEPLOY_SSH_KEY_B64 CUSTOMER_KNOWN_HOSTS_B64; do
|
||||
if [ -z "${!name:-}" ]; then
|
||||
echo "Missing required customer SSH secret: ${name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
case "${CUSTOMER_DEPLOY_USER}" in *[!A-Za-z0-9._-]*) echo "Customer SSH user has unsupported characters" >&2; exit 1;; esac
|
||||
case "${CUSTOMER_DEPLOY_HOST}" in *[!A-Za-z0-9.:-]*) echo "Customer SSH host has unsupported characters" >&2; exit 1;; esac
|
||||
case "${CUSTOMER_SSH_PROXY_HOST}" in *[!A-Za-z0-9._@:-]*) echo "Customer SSH proxy has unsupported characters" >&2; exit 1;; esac
|
||||
printf '%s\n' "${WEB_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
|
||||
printf '%s\n' "${CELERY_REF}" | grep -Eq '@sha256:[0-9a-f]{64}$'
|
||||
|
||||
ssh_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${ssh_dir}"' EXIT
|
||||
key_path="${ssh_dir}/deploy_key"
|
||||
known_hosts_path="${ssh_dir}/known_hosts"
|
||||
printf '%s' "${CUSTOMER_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}"
|
||||
printf '%s' "${CUSTOMER_KNOWN_HOSTS_B64}" | base64 -d > "${known_hosts_path}"
|
||||
chmod 600 "${key_path}" "${known_hosts_path}"
|
||||
ssh-keygen -y -f "${key_path}" >/dev/null
|
||||
|
||||
ssh_common=(
|
||||
-i "${key_path}"
|
||||
-o BatchMode=yes
|
||||
-o IdentitiesOnly=yes
|
||||
-o StrictHostKeyChecking=yes
|
||||
-o "UserKnownHostsFile=${known_hosts_path}"
|
||||
-o GlobalKnownHostsFile=/dev/null
|
||||
-o ConnectTimeout=15
|
||||
)
|
||||
proxy_command="ssh -i ${key_path} -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${known_hosts_path} -o GlobalKnownHostsFile=/dev/null -o ConnectTimeout=15 -W %h:%p ${CUSTOMER_SSH_PROXY_HOST}"
|
||||
|
||||
ssh \
|
||||
"${ssh_common[@]}" \
|
||||
-o "ProxyCommand=${proxy_command}" \
|
||||
"${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" \
|
||||
/ecos/release-service \
|
||||
mostovik-backend \
|
||||
"${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
|
||||
"${WEB_REF}" \
|
||||
"${CELERY_REF}"
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
name: Deploy Customer Main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: mostovik-backend-customer-main-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CUSTOMER_REGISTRY_HOST: registry.dev.nii-ecos.ru
|
||||
CUSTOMER_REGISTRY_NAMESPACE: avm
|
||||
CUSTOMER_WEB_IMAGE: mostovik-backend-web
|
||||
CUSTOMER_CELERY_IMAGE: mostovik-backend-celery
|
||||
CUSTOMER_DEPLOY_USER: ecos
|
||||
CUSTOMER_DEPLOY_HOST: 10.0.10.174
|
||||
CUSTOMER_SSH_PROXY_HOST: root@10.10.0.121
|
||||
CUSTOMER_COMPOSE_FILE: /ecos/docker-compose.yml
|
||||
CUSTOMER_DEPLOY_SCRIPT: /ecos/pull-and-run.sh
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Build, Push, Deploy
|
||||
runs-on: [backend-docker]
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Ensure main branch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${GITHUB_REF_NAME}" != "main" ]; then
|
||||
echo "Customer deploy is allowed only from main; current ref is ${GITHUB_REF_NAME}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
||||
git -c core.hooksPath=/dev/null clone --depth=1 --branch="${BRANCH}" "${REPO_URL}" .
|
||||
git -c core.hooksPath=/dev/null checkout "${GITHUB_SHA}"
|
||||
|
||||
- name: Validate deploy inputs
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
GITEA_TOKEN: ${{ gitea.token }}
|
||||
CUSTOMER_DEPLOY_SSH_KEY: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY }}
|
||||
CUSTOMER_DEPLOY_SSH_KEY_B64: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY_B64 }}
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
registry_user="${REGISTRY_USERNAME:-${REGISTRY_USER:-${GITHUB_ACTOR:-}}}"
|
||||
registry_password="${REGISTRY_PASSWORD:-${REGISTRY_TOKEN:-${GITEA_TOKEN:-}}}"
|
||||
home_dir="${HOME:-/root}"
|
||||
|
||||
if [ -z "${registry_user}" ]; then
|
||||
echo "Missing registry user secret: set REGISTRY_USER or REGISTRY_USERNAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${registry_password}" ]; then
|
||||
echo "Missing registry password secret: set REGISTRY_TOKEN or REGISTRY_PASSWORD" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${CUSTOMER_DEPLOY_SSH_KEY:-}" ] \
|
||||
&& [ -z "${CUSTOMER_DEPLOY_SSH_KEY_B64:-}" ] \
|
||||
&& [ -z "${DEPLOY_SSH_KEY:-}" ] \
|
||||
&& [ ! -f "${home_dir}/.ssh/ci-key" ] \
|
||||
&& [ ! -f "/root/.ssh/ci-key" ]; then
|
||||
echo "Missing customer SSH key: set CUSTOMER_DEPLOY_SSH_KEY_B64, CUSTOMER_DEPLOY_SSH_KEY, DEPLOY_SSH_KEY, or install ~/.ssh/ci-key on the runner" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! docker buildx inspect customer-builder >/dev/null 2>&1; then
|
||||
docker buildx create --name customer-builder --driver docker-container --use
|
||||
else
|
||||
docker buildx use customer-builder
|
||||
fi
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
- name: Build and push customer images
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
GITEA_TOKEN: ${{ gitea.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
registry_user="${REGISTRY_USERNAME:-${REGISTRY_USER:-${GITHUB_ACTOR:-}}}"
|
||||
registry_password="${REGISTRY_PASSWORD:-${REGISTRY_TOKEN:-${GITEA_TOKEN:-}}}"
|
||||
sha_short="$(printf '%s' "${GITHUB_SHA}" | cut -c1-12)"
|
||||
registry_path="${CUSTOMER_REGISTRY_HOST}/${CUSTOMER_REGISTRY_NAMESPACE}"
|
||||
web_ref="${registry_path}/${CUSTOMER_WEB_IMAGE}"
|
||||
celery_ref="${registry_path}/${CUSTOMER_CELERY_IMAGE}"
|
||||
|
||||
printf '%s' "${registry_password}" \
|
||||
| docker login "${CUSTOMER_REGISTRY_HOST}" \
|
||||
-u "${registry_user}" \
|
||||
--password-stdin
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
-f ./docker/Dockerfile \
|
||||
--target runtime-web \
|
||||
--build-arg INSTALL_DEV=false \
|
||||
--push \
|
||||
-t "${web_ref}:latest" \
|
||||
-t "${web_ref}:${sha_short}" \
|
||||
.
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
-f ./docker/Dockerfile \
|
||||
--target runtime-celery \
|
||||
--build-arg INSTALL_DEV=false \
|
||||
--push \
|
||||
-t "${celery_ref}:latest" \
|
||||
-t "${celery_ref}:${sha_short}" \
|
||||
.
|
||||
|
||||
- name: Deploy customer stack
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
GITEA_TOKEN: ${{ gitea.token }}
|
||||
CUSTOMER_DEPLOY_SSH_KEY: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY }}
|
||||
CUSTOMER_DEPLOY_SSH_KEY_B64: ${{ secrets.CUSTOMER_DEPLOY_SSH_KEY_B64 }}
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
registry_user="${REGISTRY_USERNAME:-${REGISTRY_USER:-${GITHUB_ACTOR:-}}}"
|
||||
registry_password="${REGISTRY_PASSWORD:-${REGISTRY_TOKEN:-${GITEA_TOKEN:-}}}"
|
||||
case "${registry_user}" in
|
||||
*[!A-Za-z0-9._@-]*)
|
||||
echo "Registry user contains unsupported characters" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
home_dir="${HOME:-/root}"
|
||||
mkdir -p "${home_dir}/.ssh"
|
||||
key_path="${home_dir}/.ssh/customer_deploy_key"
|
||||
if [ -f "/root/.ssh/ci-key" ]; then
|
||||
cp "/root/.ssh/ci-key" "${key_path}"
|
||||
elif [ -f "${home_dir}/.ssh/ci-key" ]; then
|
||||
cp "${home_dir}/.ssh/ci-key" "${key_path}"
|
||||
elif [ -n "${CUSTOMER_DEPLOY_SSH_KEY_B64:-}" ]; then
|
||||
printf '%s' "${CUSTOMER_DEPLOY_SSH_KEY_B64}" | base64 -d > "${key_path}"
|
||||
elif [ -n "${DEPLOY_SSH_KEY:-}" ]; then
|
||||
printf '%s' "${DEPLOY_SSH_KEY}" | base64 -d > "${key_path}"
|
||||
elif [ -n "${CUSTOMER_DEPLOY_SSH_KEY:-}" ]; then
|
||||
printf '%s\n' "${CUSTOMER_DEPLOY_SSH_KEY}" > "${key_path}"
|
||||
else
|
||||
echo "Customer deploy SSH key is unavailable" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod 600 "${key_path}"
|
||||
|
||||
ssh_common=(
|
||||
-i "${key_path}"
|
||||
-o BatchMode=yes
|
||||
-o IdentitiesOnly=yes \
|
||||
-o StrictHostKeyChecking=no
|
||||
-o UserKnownHostsFile=/dev/null
|
||||
-o GlobalKnownHostsFile=/dev/null
|
||||
-o LogLevel=ERROR
|
||||
-o ConnectTimeout=15
|
||||
)
|
||||
proxy_command="ssh -i ${key_path} -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=15 -W %h:%p ${CUSTOMER_SSH_PROXY_HOST}"
|
||||
remote_command="set -euo pipefail
|
||||
flock -w 1800 /tmp/ecos-customer-deploy.lock /bin/sh -c 'cd /ecos && FORCE_PULL=1 COMPOSE_FILE=\"${CUSTOMER_COMPOSE_FILE}\" \"${CUSTOMER_DEPLOY_SCRIPT}\" && docker image prune -f'"
|
||||
|
||||
ssh "${ssh_common[@]}" -o "ProxyCommand=${proxy_command}" "${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" "true"
|
||||
printf '%s' "${registry_password}" \
|
||||
| ssh "${ssh_common[@]}" -o "ProxyCommand=${proxy_command}" "${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" \
|
||||
"docker login '${CUSTOMER_REGISTRY_HOST}' -u '${registry_user}' --password-stdin"
|
||||
ssh "${ssh_common[@]}" -o "ProxyCommand=${proxy_command}" "${CUSTOMER_DEPLOY_USER}@${CUSTOMER_DEPLOY_HOST}" "${remote_command}"
|
||||
@@ -1,74 +0,0 @@
|
||||
name: Manual Dev Actions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
manual_action:
|
||||
description: "Manual action: noop, build_golden_images, cleanup_dev_database, or dokploy_start"
|
||||
required: true
|
||||
default: "noop"
|
||||
dokploy_target:
|
||||
description: "Dokploy dev target: all, web, worker, or beat"
|
||||
required: true
|
||||
default: "all"
|
||||
cleanup_confirm:
|
||||
description: "Type CLEAN_DEV_DB to drop and recreate the dev database as UTF8"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
concurrency:
|
||||
group: mostovik-backend-manual-dev-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY_API_URL: "https://registry.dev.nii-ecos.ru/v2/"
|
||||
REGISTRY_HOST: "registry.dev.nii-ecos.ru"
|
||||
REGISTRY_NAMESPACE: "${{ github.repository_owner }}"
|
||||
WEB_IMAGE: "mostovik-backend-web"
|
||||
CELERY_IMAGE: "mostovik-backend-celery"
|
||||
CI_GOLDEN_IMAGE: "mostovik-backend-ci-golden"
|
||||
WEB_GOLDEN_IMAGE: "mostovik-backend-web-golden"
|
||||
CELERY_GOLDEN_IMAGE: "mostovik-backend-celery-golden"
|
||||
GOLDEN_TAG: "py311-uv0.7.2"
|
||||
DOKPLOY_DEV_WEB_WEBHOOK_URL: "https://deploy.dev.nii-ecos.ru/api/deploy/_EjfuYBpzGJ18uPwBZ3iF"
|
||||
DOKPLOY_DEV_WORKER_WEBHOOK_URL: "https://deploy.dev.nii-ecos.ru/api/deploy/hltL7K2HmG1a8EIzr-mVA"
|
||||
DOKPLOY_DEV_BEAT_WEBHOOK_URL: "https://deploy.dev.nii-ecos.ru/api/deploy/RkdykbqU6faErrZBAN9Rv"
|
||||
DOKPLOY_API_URL: "https://deploy.dev.nii-ecos.ru/api"
|
||||
DOKPLOY_DEV_WEB_APPLICATION_ID: "x2l_Twc2z2A4lJhMVqlNg"
|
||||
DOKPLOY_DEV_WORKER_APPLICATION_ID: "m8ECastEeQKhDZVFonUTS"
|
||||
DOKPLOY_DEV_BEAT_APPLICATION_ID: "Ut5e5mcMMslxG9Zrpbp0_"
|
||||
DOKPLOY_DEV_WEB_APP_NAME: "service-backend-4mbxrs"
|
||||
DOKPLOY_DEV_WORKER_APP_NAME: "service-backend-512y9c"
|
||||
DOKPLOY_DEV_BEAT_APP_NAME: "service-backend-nvdyoq"
|
||||
|
||||
jobs:
|
||||
manual_dev_action:
|
||||
name: Run Manual Dev Action
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
MANUAL_ACTION: ${{ github.event.inputs.manual_action }}
|
||||
DOKPLOY_TARGET: ${{ github.event.inputs.dokploy_target }}
|
||||
CLEANUP_CONFIRM: ${{ github.event.inputs.cleanup_confirm }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }}
|
||||
DOKPLOY_API_TOKEN: ${{ secrets.DOKPLOY_API_TOKEN }}
|
||||
DOKPLOY_API_TOKEN_FALLBACK: "cmhRpAPDlWPCbwkCdteTgpHuHzhPHCNtZrUcRddsfiHdijmyXKsIIojiBmcVpfpo"
|
||||
POSTGRES_HOST: "10.10.0.114"
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DB: "mostovik"
|
||||
POSTGRES_USER: "postgres"
|
||||
POSTGRES_PASSWORD: "postgres"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
|
||||
git -c core.hooksPath=/dev/null clone --depth=1 --branch="${BRANCH}" "${REPO_URL}" .
|
||||
git -c core.hooksPath=/dev/null checkout "${GITHUB_SHA}"
|
||||
|
||||
- name: Run selected action
|
||||
run: bash scripts/ci/manual_dev_action.sh
|
||||
Reference in New Issue
Block a user