diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..59c6420 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,77 @@ +pipeline { + agent any + + environment { + SSH_CONFIG_NAME = 'SEJONG-NAS' + DEPLOY_DIR = '/volume1/docker/docker-registry-aio' + } + + options { + timestamps() + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '20')) + } + + stages { + stage('Validate') { + steps { + sh ''' + set -eu + # No .env in the workspace: compose falls back to defaults, enough to check syntax. + docker compose config -q + sh -n scripts/deploy.sh + ''' + } + } + + stage('Build') { + steps { + // Same Dockerfile the NAS builds; the agent shares the host daemon, + // so this also warms the build cache for the Deploy stage. + sh ''' + set -eu + tag="registry-aio-app-ci:$(printf '%s' "$BUILD_TAG" | tr -c 'A-Za-z0-9_.-' '-' | cut -c1-128)" + trap 'docker image rm "$tag" >/dev/null 2>&1 || true' EXIT + docker build -t "$tag" app + ''' + } + } + + stage('Deploy') { + when { + allOf { + not { changeRequest() } + anyOf { + branch 'main' + expression { + def branch = env.GIT_BRANCH ?: env.BRANCH_NAME ?: '' + branch == 'main' || branch == 'origin/main' || branch == 'refs/heads/main' + } + } + } + } + steps { + // Keep the existing flow: the NAS checkout is the deployment. + // Fast-forward it to the built commit, then rebuild app and `compose up -d` there. + sshPublisher( + failOnError: true, + publishers: [ + sshPublisherDesc( + configName: env.SSH_CONFIG_NAME, + verbose: true, + transfers: [ + sshTransfer( + execTimeout: 1800000, + usePty: true, + execCommand: """ + sudo -n su -c 'set -eu; export PATH=/usr/local/bin:/usr/bin:/bin; cd "${env.DEPLOY_DIR}"; git fetch -q origin main; git checkout -q main; git merge --ff-only "${env.GIT_COMMIT}"; sh scripts/deploy.sh' + """.stripIndent().trim() + ) + ] + ) + ] + ) + } + } + } +} diff --git a/README.md b/README.md index 6124bfb..3192e85 100644 --- a/README.md +++ b/README.md @@ -158,3 +158,20 @@ config/registry/config.yml.template certs/ # compose certs 서비스가 자동 생성 (gitignore) app/ # Nuxt + Prisma ``` + +## Jenkins 배포 + +`Jenkinsfile`은 Gitea 멀티브랜치 파이프라인용입니다. + +1. **Validate** — `docker compose config -q`, 배포 스크립트 문법 검사 +2. **Build** — `app` 이미지를 빌드해 확인만 하고 지움 (모든 브랜치·PR) +3. **Deploy** — `main`만. SSH(`SEJONG-NAS`)로 배포 디렉터리의 체크아웃을 빌드한 커밋까지 fast-forward한 뒤 `scripts/deploy.sh` 실행 + +`scripts/deploy.sh`는 `app`을 다시 빌드하고 `docker compose up -d` 후 `app`이 healthy가 될 때까지 기다립니다. +수동 배포도 같은 스크립트를 씁니다. + +```bash +git pull --ff-only && sh scripts/deploy.sh +``` + +배포 디렉터리에 커밋되지 않은 수정이 있어 fast-forward가 안 되면 배포는 실패하고 아무것도 바뀌지 않습니다. diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..6407793 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Rebuild the app image from the current checkout and bring the stack up. +# Run from anywhere; used by the Jenkins Deploy stage and for manual deploys. +set -eu + +cd "$(dirname "$0")/.." + +HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-180}" + +echo "deploy: $(git log -1 --format='%h %s' 2>/dev/null || echo unknown)" + +docker compose config -q +docker compose build app +docker compose up -d + +# registry's healthcheck always passes, so the app health is what tells us the deploy worked. +cid="$(docker compose ps -q app)" +elapsed=0 +while :; do + status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$cid")" + case "$status" in + healthy) break ;; + unhealthy|exited|dead) + echo "deploy: app is $status" >&2 + docker compose logs --tail 50 app >&2 + exit 1 ;; + esac + if [ "$elapsed" -ge "$HEALTH_TIMEOUT" ]; then + echo "deploy: app not healthy after ${HEALTH_TIMEOUT}s (status: $status)" >&2 + docker compose logs --tail 50 app >&2 + exit 1 + fi + sleep 5 + elapsed=$((elapsed + 5)) +done + +docker compose ps +echo "deploy: done"