From 7d5d7bb180a5b56704681d23c97b9d876966dde2 Mon Sep 17 00:00:00 2001 From: Goder-0 Date: Wed, 27 May 2026 21:28:48 +0900 Subject: [PATCH] Add backend release workflows Split backend automation into two paths. Main and pull request runs now create a local Docker bundle artifact. Tag-based runs handle AWS deployment and GitHub Release creation. Closes: #233 --- .github/workflows/backend-cd.yml | 250 ++++++++++++++++--------- .github/workflows/release.yml | 93 +++++++++ .gitignore | 2 + docker/local-bundle/Dockerfile | 6 + docker/local-bundle/README.md | 25 +++ docker/local-bundle/docker-compose.yml | 49 +++++ scripts/prepare-local-bundle.sh | 50 +++++ 7 files changed, 391 insertions(+), 84 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 docker/local-bundle/Dockerfile create mode 100644 docker/local-bundle/README.md create mode 100644 docker/local-bundle/docker-compose.yml create mode 100644 scripts/prepare-local-bundle.sh diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index e8ceedae..c28630c8 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -1,84 +1,166 @@ -name: ✨ Linkiving backend CD ✨ - -on: - workflow_dispatch: - pull_request: - types: [ closed ] - branches: - - main - -jobs: - backend-docker-build-and-push: - if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' }} - runs-on: ubuntu-latest - - steps: - - name: ✨ Checkout repository - uses: actions/checkout@v3 - - - name: ✨ JDK 17 설정 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - - name: ✨ Gradle Caching - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: 🗂️ Make config - run: | - # src/main/resources 경로 이동 - mkdir -p ./src/main/resources - cd ./src/main/resources - - # yml 파일 생성 - - touch ./application.yml - echo "$APPLICATION" > ./application.yml - - env: - APPLICATION: ${{ secrets.APPLICATION }} - shell: bash - - - name: ✨ Gradlew 권한 설정 - run: chmod +x ./gradlew - - - name: ✨ Jar 파일 빌드 - run: | - ./gradlew bootJar - - - name: ✨ DockerHub에 로그인 - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: ✨ Docker Image 빌드 후 DockerHub에 Push - uses: docker/build-push-action@v4 - with: - context: . - file: ./docker/Dockerfile - push: true - platforms: linux/amd64 - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest - - backend-docker-pull-and-run: - runs-on: [ self-hosted, prod ] - needs: [ backend-docker-build-and-push ] - if: ${{ needs.backend-docker-build-and-push.result == 'success' && (github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true) }} - - steps: - - name: ✨ Checkout repository - uses: actions/checkout@v5 - - - name: ✨ 배포 스크립트 실행 - run: | - chmod +x deploy.sh - ./deploy.sh +name: ✨ Linkiving backend local bundle CD ✨ + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +concurrency: + group: backend-local-bundle-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build-local-bundle: + runs-on: ubuntu-latest + + steps: + - name: ✨ Checkout repository + uses: actions/checkout@v5 + + - name: ✨ JDK 17 설정 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: ✨ Gradle caching + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: 🗂️ Create local application profile + shell: bash + run: | + mkdir -p ./src/main/resources + cat <<'EOF' > ./src/main/resources/application-local.yml + spring: + datasource: + driver-class-name: org.postgresql.Driver + url: ${DB_URL:jdbc:postgresql://localhost:5432/linkiving_local} + username: ${DB_USERNAME:linkiving} + password: ${DB_PASSWORD:linkiving} + + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + ssl: + enabled: ${REDIS_SSL_ENABLED:false} + repositories: + enabled: false + + jpa: + database: postgresql + hibernate: + ddl-auto: update + show-sql: true + open-in-view: false + properties: + hibernate: + format_sql: true + use_sql_comments: true + + security: + oauth2: + client: + registration: + google: + redirect-uri: ${GOOGLE_REDIRECT_URI:http://localhost:8080/login/oauth2/code/google} + client-id: ${GOOGLE_CLIENT_ID:local-google-client-id} + client-secret: ${GOOGLE_CLIENT_SECRET:local-google-client-secret} + scope: + - email + - profile + client-name: linkiving + authorization-grant-type: authorization_code + + cloud: + aws: + s3: + bucket: ${AWS_S3_BUCKET:linkiving-local} + credentials: + access-key: ${AWS_ACCESS_KEY:local-access-key} + secret-key: ${AWS_SECRET_KEY:local-secret-key} + region: + static: ${AWS_REGION:ap-northeast-2} + stack: + auto: false + + logging: + level: + org.hibernate.SQL: debug + org.hibernate.orm.jdbc.bind: trace + + security: + jwt: + secret: ${JWT_SECRET:local-jwt-secret-local-jwt-secret-local-jwt-secret-local-jwt-secret} + access-token-valid-minute: 60 + refresh-token-valid-month: 6 + + app: + oauth2: + success-redirect-url: http://localhost:3000/home + failure-redirect-url: http://localhost:3000/ + cors: + allowed-origins: + - http://localhost:3000 + extension-allowed-origins: + - ${CHROME_EXTENSION_ORIGIN:chrome-extension://replace-me} + cookie: + domain: ${COOKIE_DOMAIN:} + link: + default-image-url: ${DEFAULT_IMAGE_URL:https://linkiving-s3.s3.ap-northeast-2.amazonaws.com/static/default-thumbnail.png} + + feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: FULL + + summary: + worker: + sleep-duration: 1s + + ai: + server: + url: ${AI_SERVER_URL:http://api.linkiving.com:5678} + + linkiving: + default-image: + url: "https://linkiving-s3.s3.ap-northeast-2.amazonaws.com/static/default-thumbnail.png" + EOF + + - name: ✨ Executable permissions + run: chmod +x ./gradlew ./scripts/prepare-local-bundle.sh + + - name: ✨ Jar 파일 빌드 + run: ./gradlew bootJar + + - name: 🐳 Build local Docker bundle + env: + BUNDLE_VERSION: ${{ github.event_name == 'pull_request' && format('pr-{0}-{1}', github.event.pull_request.number, github.sha) || format('main-{0}', github.sha) }} + IMAGE_TAG: linkiving-local:${{ github.sha }} + run: ./scripts/prepare-local-bundle.sh + + - name: 📦 Upload local bundle artifact + uses: actions/upload-artifact@v4 + with: + name: linkiving-core-local-bundle-${{ github.run_number }} + path: | + dist/*.zip + dist/*.sha256 + retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..7c694b1d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,93 @@ +name: 🚀 Linkiving backend release deploy 🚀 + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +concurrency: + group: backend-release-${{ github.ref }} + cancel-in-progress: false + +jobs: + backend-docker-build-and-push: + runs-on: ubuntu-latest + + steps: + - name: ✨ Checkout repository + uses: actions/checkout@v5 + + - name: ✨ JDK 17 설정 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: ✨ Gradle caching + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: 🗂️ Make production config + shell: bash + env: + APPLICATION: ${{ secrets.APPLICATION }} + run: | + mkdir -p ./src/main/resources + printf '%s' "$APPLICATION" > ./src/main/resources/application.yml + + - name: ✨ Gradlew 권한 설정 + run: chmod +x ./gradlew + + - name: ✨ Jar 파일 빌드 + run: ./gradlew bootJar + + - name: ✨ DockerHub에 로그인 + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: ✨ Docker image 빌드 후 DockerHub에 push + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + platforms: linux/amd64 + tags: | + ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:${{ github.ref_name }} + ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest + + backend-docker-pull-and-run: + runs-on: [ self-hosted, prod ] + needs: [ backend-docker-build-and-push ] + if: ${{ needs.backend-docker-build-and-push.result == 'success' }} + + steps: + - name: ✨ Checkout repository + uses: actions/checkout@v5 + + - name: ✨ 배포 스크립트 실행 + run: | + chmod +x deploy.sh + ./deploy.sh + + create-release: + runs-on: ubuntu-latest + needs: [ backend-docker-pull-and-run ] + if: ${{ needs.backend-docker-pull-and-run.result == 'success' }} + + steps: + - name: 📝 Create GitHub release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/.gitignore b/.gitignore index a0cb7de3..06236e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ out/ ### Spring *.yml +!.github/workflows/*.yml +!docker/local-bundle/*.yml .editorconfig diff --git a/docker/local-bundle/Dockerfile b/docker/local-bundle/Dockerfile new file mode 100644 index 00000000..afc58190 --- /dev/null +++ b/docker/local-bundle/Dockerfile @@ -0,0 +1,6 @@ +FROM amazoncorretto:17-alpine-jdk + +COPY app.jar /app.jar + +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/docker/local-bundle/README.md b/docker/local-bundle/README.md new file mode 100644 index 00000000..6782e547 --- /dev/null +++ b/docker/local-bundle/README.md @@ -0,0 +1,25 @@ +# linkiving-core local bundle + +## Included files + +- `docker-compose.yml` +- `linkiving-core-local-image.tar.gz` + +## Run + +```bash +docker load -i linkiving-core-local-image.tar.gz +docker compose up -d +``` + +## Verify + +```bash +curl -fsS http://localhost:8080/health-check +``` + +## Stop + +```bash +docker compose down -v +``` diff --git a/docker/local-bundle/docker-compose.yml b/docker/local-bundle/docker-compose.yml new file mode 100644 index 00000000..d929d40d --- /dev/null +++ b/docker/local-bundle/docker-compose.yml @@ -0,0 +1,49 @@ +services: + app: + image: __IMAGE_TAG__ + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + - SPRING_PROFILES_ACTIVE=local + - DB_URL=jdbc:postgresql://postgres:5432/linkiving_local + - DB_USERNAME=linkiving + - DB_PASSWORD=linkiving + - REDIS_HOST=redis + - REDIS_PORT=6379 + - AI_SERVER_URL=http://api.linkiving.com:5678 + - SERVER_ADDRESS=0.0.0.0 + - TZ=Asia/Seoul + ports: + - "8080:8080" + + postgres: + image: postgres:16-alpine + environment: + - POSTGRES_DB=linkiving_local + - POSTGRES_USER=linkiving + - POSTGRES_PASSWORD=linkiving + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U linkiving -d linkiving_local"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 10 + +volumes: + pgdata: diff --git a/scripts/prepare-local-bundle.sh b/scripts/prepare-local-bundle.sh new file mode 100644 index 00000000..a9893dc8 --- /dev/null +++ b/scripts/prepare-local-bundle.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +DIST_DIR="${REPO_ROOT}/dist" +TEMPLATE_DIR="${REPO_ROOT}/docker/local-bundle" +BUNDLE_VERSION="${BUNDLE_VERSION:-dev}" +IMAGE_TAG="${IMAGE_TAG:-linkiving-local:${BUNDLE_VERSION}}" +BUNDLE_NAME="linkiving-core-local-${BUNDLE_VERSION}" +BUNDLE_DIR="${DIST_DIR}/${BUNDLE_NAME}" +ARCHIVE_PATH="${DIST_DIR}/${BUNDLE_NAME}.zip" +CHECKSUM_PATH="${ARCHIVE_PATH}.sha256" + +APP_JAR="$(find "${REPO_ROOT}/build/libs" -maxdepth 1 -type f -name '*.jar' ! -name '*plain.jar' | head -n 1)" + +if [ -z "${APP_JAR}" ]; then + echo "Built jar not found in build/libs" >&2 + exit 1 +fi + +rm -rf "${BUNDLE_DIR}" "${ARCHIVE_PATH}" "${CHECKSUM_PATH}" +mkdir -p "${BUNDLE_DIR}" + +cp "${TEMPLATE_DIR}/README.md" "${BUNDLE_DIR}/README.md" +cp "${TEMPLATE_DIR}/docker-compose.yml" "${BUNDLE_DIR}/docker-compose.yml" +cp "${APP_JAR}" "${BUNDLE_DIR}/app.jar" +cp "${TEMPLATE_DIR}/Dockerfile" "${BUNDLE_DIR}/Dockerfile" + +sed -i.bak "s|__IMAGE_TAG__|${IMAGE_TAG}|g" "${BUNDLE_DIR}/docker-compose.yml" +rm -f "${BUNDLE_DIR}/docker-compose.yml.bak" + +docker build \ + --file "${BUNDLE_DIR}/Dockerfile" \ + --tag "${IMAGE_TAG}" \ + "${BUNDLE_DIR}" + +docker save "${IMAGE_TAG}" | gzip > "${BUNDLE_DIR}/linkiving-core-local-image.tar.gz" + +rm -f "${BUNDLE_DIR}/app.jar" "${BUNDLE_DIR}/Dockerfile" + +( + cd "${DIST_DIR}" + zip -qr "${ARCHIVE_PATH}" "${BUNDLE_NAME}" +) + +sha256sum "${ARCHIVE_PATH}" > "${CHECKSUM_PATH}" + +echo "Created local bundle archive: ${ARCHIVE_PATH}"