Replace Gitea Actions with Jenkins CI/CD.
office.sejong.dev/pipeline/head There was a failure building this commit
office.sejong.dev/pipeline/head There was a failure building this commit
Build and push images from a Jenkinsfile, then deploy over SSH with compose on main. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b0a6fc326c
commit
f4f98704be
3 files changed
+145
-95
No files matched your search
@@ -1,84 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Type-check & Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
# CAD/OCR/3D viewer assets make Vite exceed the default ~2GB heap
|
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Type-check and build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
image:
|
|
||||||
name: Build & Push image
|
|
||||||
needs: build
|
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Job image often has no docker binary; host sock may already be mounted by act_runner.
|
|
||||||
- name: Install Docker CLI
|
|
||||||
run: |
|
|
||||||
mkdir -p "$HOME/.local/bin"
|
|
||||||
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.5.1.tgz \
|
|
||||||
| tar xz --strip-components=1 -C "$HOME/.local/bin" docker/docker
|
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
docker info
|
|
||||||
docker version
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Host /var/run/docker.sock is not available in this job."
|
|
||||||
echo "On act_runner, set container.docker_host: \"\" (use host Docker)"
|
|
||||||
echo "and/or container.valid_volumes: [/var/run/docker.sock]"
|
|
||||||
ls -la /var/run/docker.sock || true
|
|
||||||
docker info || true
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: docker.sejong.dev
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
docker.sejong.dev/sejong-office:latest
|
|
||||||
docker.sejong.dev/sejong-office:sha-${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Log out of registry
|
|
||||||
if: always()
|
|
||||||
run: docker logout docker.sejong.dev
|
|
||||||
Vendored
+97
@@ -0,0 +1,97 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
NODE_OPTIONS = '--max-old-space-size=8192'
|
||||||
|
NODE_IMAGE = 'node:22-bookworm'
|
||||||
|
REGISTRY = 'docker.sejong.dev'
|
||||||
|
IMAGE = 'docker.sejong.dev/sejong-office'
|
||||||
|
SSH_CONFIG_NAME = 'sejong-office'
|
||||||
|
DEPLOY_DIR = '/volume1/docker/sejong-office'
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timestamps()
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '20'))
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Type-check & Build') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
docker run --rm \
|
||||||
|
-u "$(id -u):$(id -g)" \
|
||||||
|
-e HOME=/tmp \
|
||||||
|
-e NODE_OPTIONS \
|
||||||
|
-v "$PWD":/app \
|
||||||
|
-w /app \
|
||||||
|
"$NODE_IMAGE" \
|
||||||
|
bash -lc 'npm ci && npm run build'
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Release') {
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Build & Push image') {
|
||||||
|
steps {
|
||||||
|
withCredentials([usernamePassword(
|
||||||
|
credentialsId: 'docker-sejong-dev',
|
||||||
|
usernameVariable: 'REGISTRY_USERNAME',
|
||||||
|
passwordVariable: 'REGISTRY_PASSWORD'
|
||||||
|
)]) {
|
||||||
|
sh '''
|
||||||
|
set -eu
|
||||||
|
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin
|
||||||
|
docker build -t "$IMAGE:latest" -t "$IMAGE:sha-${GIT_COMMIT}" .
|
||||||
|
docker push "$IMAGE:latest"
|
||||||
|
docker push "$IMAGE:sha-${GIT_COMMIT}"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
sh 'docker logout "$REGISTRY" || true'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Deploy') {
|
||||||
|
steps {
|
||||||
|
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; cd "${env.DEPLOY_DIR}"; docker compose pull; docker compose up -d'
|
||||||
|
""".stripIndent().trim()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,27 +101,64 @@ volumes:
|
|||||||
documentserver_data:
|
documentserver_data:
|
||||||
```
|
```
|
||||||
|
|
||||||
## CI (Gitea Actions)
|
## CI/CD (Jenkins)
|
||||||
|
|
||||||
[`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) 이 `main` PR/push에서 `npm run build`를 돌리고, **`main` push**에서만 Docker 이미지를 레지스트리에 push합니다.
|
[`Jenkinsfile`](Jenkinsfile) 이 `main` PR/브랜치에서 `npm ci` + `npm run build`를 돌리고, **`main` 브랜치 빌드**(PR 제외)에서만 이미지를 push한 뒤 서버에 SSH로 배포합니다.
|
||||||
|
|
||||||
|
에이전트에 Docker가 있어야 합니다. Node 22는 `node:22-bookworm` 컨테이너로 돌리며, Vite 힙 한도는 `NODE_OPTIONS=--max-old-space-size=8192`입니다.
|
||||||
|
|
||||||
푸시 태그: `docker.sejong.dev/sejong-office:latest`, `docker.sejong.dev/sejong-office:sha-<commit>`
|
푸시 태그: `docker.sejong.dev/sejong-office:latest`, `docker.sejong.dev/sejong-office:sha-<commit>`
|
||||||
|
|
||||||
### 레포 Actions Secrets
|
배포는 [Publish Over SSH](https://plugins.jenkins.io/publish-over-ssh/)로 파일을 올리지 않고, 서버에 이미 있는 `docker-compose.yml` 디렉터리에서 `docker compose pull && docker compose up -d`만 실행합니다.
|
||||||
|
|
||||||
| Secret | 설명 |
|
### Jenkins Credentials
|
||||||
|--------|------|
|
|
||||||
| `REGISTRY_USERNAME` | push 전용 계정 |
|
|
||||||
| `REGISTRY_PASSWORD` | push 전용 토큰 (write) |
|
|
||||||
|
|
||||||
레지스트리 호스트는 `docker.sejong.dev`로 고정입니다. PR job은 이 시크릿을 쓰지 않습니다. 러너에 상시 `docker login` 하지 마세요.
|
Username/password 자격 증명 ID: `docker-sejong-dev`
|
||||||
|
|
||||||
|
| 필드 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| Username | push 전용 계정 |
|
||||||
|
| Password | push 전용 토큰 (write) |
|
||||||
|
|
||||||
|
레지스트리 호스트는 `docker.sejong.dev`로 고정입니다. PR 빌드는 이 자격 증명을 쓰지 않습니다. 에이전트에 상시 `docker login` 하지 마세요.
|
||||||
|
|
||||||
|
Multibranch Pipeline(또는 Gitea/Git 브랜치 소스)로 이 레포를 연결하면 PR과 `main`이 각각 잡으로 뜹니다.
|
||||||
|
|
||||||
|
### Publish Over SSH
|
||||||
|
|
||||||
|
1. Jenkins에 **Publish Over SSH** 플러그인 설치
|
||||||
|
2. **Manage Jenkins → System → Publish over SSH** 에 배포 서버 등록 (호스트, 사용자, 키 또는 비밀번호)
|
||||||
|
3. 등록한 **Name**이 Jenkinsfile의 `SSH_CONFIG_NAME`과 같아야 함 (`sejong-office`)
|
||||||
|
4. 배포 명령은 SSH 접속 후 `sudo -n su -c '...'` 로 root에서 실행함. 대화형 `sudo su`와 비밀번호 입력은 CI에서 불가
|
||||||
|
5. Publish Over SSH에 넣은 **그 계정**으로 서버에 로그인한 뒤, 한 번만 NOPASSWD를 설정:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo visudo -f /etc/sudoers.d/sejong-office-deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
파일 내용 (SSH 사용자 이름으로 바꿀 것):
|
||||||
|
|
||||||
|
```
|
||||||
|
youruser ALL=(ALL) NOPASSWD: /bin/su, /usr/bin/su
|
||||||
|
```
|
||||||
|
|
||||||
|
저장 후 비밀번호 없이 root가 되는지 확인:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -n su -c 'whoami'
|
||||||
|
# root
|
||||||
|
```
|
||||||
|
|
||||||
|
`sudo -n`은 비밀번호를 묻지 않고, 설정이 안 되어 있으면 바로 실패합니다. Jenkinsfile에 비밀번호를 넣지 마세요.
|
||||||
|
|
||||||
|
서버 compose 경로는 Jenkinsfile `DEPLOY_DIR` (`/volume1/docker/sejong-office`)입니다. 이름이나 경로가 다르면 Jenkinsfile을 수정하면 됩니다.
|
||||||
|
|
||||||
### 서버 배포 체크리스트
|
### 서버 배포 체크리스트
|
||||||
|
|
||||||
1. **pull 전용** 토큰으로 서버에서 `docker login docker.sejong.dev` (CI push 토큰과 분리)
|
1. 배포 디렉터리에 `docker-compose.yml`과 `.env`를 두고, **pull 전용** 토큰으로 `docker login docker.sejong.dev` (CI push 토큰과 분리)
|
||||||
2. `.env`에 `JWT_SECRET`, URL 설정
|
2. `.env`에 `JWT_SECRET`, URL 설정
|
||||||
3. 이미지 갱신 후: `docker compose pull && docker compose up -d`
|
3. `main` 배포 잡이 이미지 push 후 그 디렉터리에서 compose pull/up 실행
|
||||||
4. `JWT_SECRET`은 Git·이미지·Actions Secret에 넣지 않음 (서버 `.env`만)
|
4. `JWT_SECRET`은 Git·이미지·Jenkins Credential에 넣지 않음 (서버 `.env`만)
|
||||||
|
|
||||||
## 로컬 개발
|
## 로컬 개발
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user