Skip to content
256 changes: 256 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
name: CD

on:
workflow_call:
inputs:
target:
description: Moving image tag to publish
required: true
type: string
revision:
description: Full Git SHA to publish
required: true
type: string
ref_name:
description: Source branch name
required: true
type: string
secrets:
DISCORD_WEBHOOK_URL:
required: true
workflow_dispatch:
inputs:
target:
description: Moving image tag to publish
required: true
type: choice
options:
- main
- production
revision:
description: Full Git SHA (leave blank to use the selected branch HEAD)
required: false
type: string

permissions:
contents: read
packages: write

concurrency:
group: cd-${{ inputs.target }}
cancel-in-progress: true

jobs:
publish:
name: Publish ${{ inputs.target }} image
runs-on: ubuntu-latest
timeout-minutes: 15
env:
IMAGE_REVISION: ${{ inputs.revision || github.sha }}
IMAGE_TAGS: ${{ inputs.target }}
IMAGE_ARCHITECTURES: amd64,arm64
SOURCE_BRANCH: ${{ inputs.ref_name || github.ref_name }}

steps:
- name: Configure image repository
run: echo "IMAGE_REPOSITORY=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"

- name: Validate publish target
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
if [[ ! "$IMAGE_REVISION" =~ ^[0-9a-f]{40}$ ]]; then
echo "revision must be a full Git SHA" >&2
exit 1
fi

if [[ -z "$DISCORD_WEBHOOK_URL" ]]; then
echo "DISCORD_WEBHOOK_URL secret is required" >&2
exit 1
fi

if [[ "$IMAGE_TAGS" == "main" && "$SOURCE_BRANCH" != "main" ]]; then
echo "the main image can only be published from the main branch" >&2
exit 1
fi

if [[ "$IMAGE_TAGS" == "production" && "$SOURCE_BRANCH" != release/* ]]; then
echo "the production image can only be published from a release/** branch" >&2
exit 1
fi

- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.revision || github.sha }}
fetch-depth: 0

- name: Verify revision belongs to source branch
run: |
git fetch --no-tags origin "$SOURCE_BRANCH"
if ! git merge-base --is-ancestor "$IMAGE_REVISION" FETCH_HEAD; then
echo "revision does not belong to $SOURCE_BRANCH" >&2
exit 1
fi

- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Find previous published revision
id: previous
run: |
previous_revision=""
image_reference="$IMAGE_REPOSITORY:$IMAGE_TAGS"

if manifest=$(docker buildx imagetools inspect \
"$image_reference" \
--format '{{json .Manifest}}' 2>/dev/null); then
platform_digest=$(jq -r '
[.manifests[]?
| select(.platform.os == "linux" and .platform.architecture == "amd64")
| .digest]
| first // empty
' <<< "$manifest")

if [[ -n "$platform_digest" ]]; then
image_reference="$IMAGE_REPOSITORY@$platform_digest"
fi
fi

if labels=$(docker buildx imagetools inspect \
"$image_reference" \
--format '{{json .Image.Config.Labels}}' 2>/dev/null); then
candidate=$(jq -r '."org.opencontainers.image.revision" // empty' <<< "$labels")
if [[ "$candidate" =~ ^[0-9a-f]{40}$ ]]; then
previous_revision="$candidate"
fi
fi

echo "revision=$previous_revision" >> "$GITHUB_OUTPUT"

- name: Run checks for manual publish
if: ${{ github.event_name == 'workflow_dispatch' }}
run: ./gradlew check --no-daemon

- name: Prepare commit summary
id: changes
env:
PREVIOUS_REVISION: ${{ steps.previous.outputs.revision }}
run: |
repository_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
comparison_url="$repository_url/commit/$IMAGE_REVISION"

if [[ -n "$PREVIOUS_REVISION" ]] && git cat-file -e "$PREVIOUS_REVISION^{commit}"; then
if [[ "$PREVIOUS_REVISION" == "$IMAGE_REVISION" ]]; then
commit_summary="변경된 커밋 없음 (동일 revision 재게시)"
else
comparison_url="$repository_url/compare/$PREVIOUS_REVISION...$IMAGE_REVISION"
commit_count=$(git rev-list --count "$PREVIOUS_REVISION..$IMAGE_REVISION")
commit_summary=""

while IFS=$'\t' read -r full_sha short_sha subject; do
subject="${subject:0:60}"
commit_summary+="• [\`$short_sha\`]($repository_url/commit/$full_sha) $subject"$'\n'
done < <(git log --max-count=5 --format='%H%x09%h%x09%s' "$PREVIOUS_REVISION..$IMAGE_REVISION")

if (( commit_count > 5 )); then
commit_summary+="• 그 외 $((commit_count - 5))개 커밋"$'\n'
fi

if [[ -z "$commit_summary" ]]; then
commit_summary="비교할 수 있는 새 커밋이 없습니다."
else
commit_summary="${commit_summary%$'\n'}"
fi
fi
else
short_sha=$(git rev-parse --short=8 "$IMAGE_REVISION")
subject=$(git show -s --format='%s' "$IMAGE_REVISION")
subject="${subject:0:60}"
commit_summary="• [\`$short_sha\`]($repository_url/commit/$IMAGE_REVISION) $subject"$'\n'"• 이전 이미지 revision 없음"
fi

{
echo "comparison_url=$comparison_url"
echo 'commit_summary<<__MOMOGO_COMMITS__'
printf '%s\n' "$commit_summary"
echo '__MOMOGO_COMMITS__'
} >> "$GITHUB_OUTPUT"

- name: Build and push image with Jib
run: ./gradlew jib --no-daemon

- name: Send Discord notification
env:
COMMIT_SUMMARY: ${{ steps.changes.outputs.commit_summary }}
COMPARISON_URL: ${{ steps.changes.outputs.comparison_url }}
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
image_digest=$(tr -d '\n' < build/jib-image.digest)
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
short_sha="${IMAGE_REVISION:0:8}"

if [[ "$IMAGE_TAGS" == "main" ]]; then
deployment_target="dev"
deployment_emoji="🧪"
color=3447003
else
deployment_target="production"
deployment_emoji="🚀"
color=15158332
fi

jq -n \
--arg title "$deployment_emoji $deployment_target 이미지 푸시 완료" \
--arg branch "$SOURCE_BRANCH" \
--arg actor "$GITHUB_ACTOR" \
--arg revision "$short_sha" \
--arg revision_url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$IMAGE_REVISION" \
--arg image_sha "$IMAGE_REPOSITORY:$IMAGE_REVISION" \
--arg image_alias "$IMAGE_REPOSITORY:$IMAGE_TAGS" \
--arg digest "$image_digest" \
--arg commits "$COMMIT_SUMMARY" \
--arg comparison_url "$COMPARISON_URL" \
--arg run_url "$run_url" \
--arg timestamp "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
--argjson color "$color" \
'{
username: "monogo-deploy",
allowed_mentions: {parse: []},
embeds: [{
title: $title,
url: $run_url,
color: $color,
fields: [
{name: "브랜치", value: $branch, inline: true},
{name: "실행자", value: $actor, inline: true},
{name: "Revision", value: ("[" + $revision + "](" + $revision_url + ")"), inline: true},
{name: "이미지", value: ("`" + $image_sha + "`\n`" + $image_alias + "`"), inline: false},
{name: "Digest", value: ("`" + $digest + "`"), inline: false},
{name: "배포 대상 커밋", value: $commits, inline: false},
{name: "링크", value: ("[변경사항 비교](" + $comparison_url + ") · [Actions 실행](" + $run_url + ")"), inline: false}
],
timestamp: $timestamp
}]
}' > "$RUNNER_TEMP/discord-payload.json"

curl --fail-with-body --silent --show-error \
-H 'Content-Type: application/json' \
--data-binary "@$RUNNER_TEMP/discord-payload.json" \
"$DISCORD_WEBHOOK_URL"
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
branches:
- main
- 'release/**'

permissions:
contents: read
Expand Down Expand Up @@ -72,3 +73,18 @@ jobs:
build/test-results/test
if-no-files-found: warn
retention-days: 7

publish-image:
name: Publish image
needs: test
if: ${{ github.event_name == 'push' }}
permissions:
contents: read
packages: write
uses: ./.github/workflows/cd.yml
with:
target: ${{ github.ref_name == 'main' && 'main' || 'production' }}
revision: ${{ github.sha }}
ref_name: ${{ github.ref_name }}
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
40 changes: 40 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
id 'org.jetbrains.kotlin.plugin.spring' version '2.3.21'
id 'org.springframework.boot' version '4.1.0'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.google.cloud.tools.jib' version '3.5.3'
}

group = 'com.mogumogu'
Expand Down Expand Up @@ -51,3 +52,42 @@ kotlin {
tasks.named('test') {
useJUnitPlatform()
}

def imageRepository = System.getenv('IMAGE_REPOSITORY') ?: 'ghcr.io/nexters/momogo-server'
def imageRevision = System.getenv('IMAGE_REVISION') ?: 'local'
def additionalImageTags = (System.getenv('IMAGE_TAGS') ?: '')
.split(',')
.collect { it.trim() }
.findAll { !it.isEmpty() }
.toSet()
def imageArchitectures = (System.getenv('IMAGE_ARCHITECTURES') ?: 'amd64')
.split(',')
.collect { it.trim() }
.findAll { !it.isEmpty() }
.toSet()

jib {
from {
image = 'eclipse-temurin:21-jre@sha256:d2b9f8f12212cadcfdf889461531784e8fd097feade954d65b31ee7a71c473ec'
platforms {
imageArchitectures.each { imageArchitecture ->
platform {
architecture = imageArchitecture
os = 'linux'
}
}
}
}
to {
image = "${imageRepository}:${imageRevision}"
tags = additionalImageTags
}
container {
user = '1000:1000'
ports = ['8080']
labels = [
'org.opencontainers.image.source': 'https://github.com/Nexters/momogo-server',
'org.opencontainers.image.revision': imageRevision,
]
}
}
12 changes: 12 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
spring:
config:
activate:
on-profile: dev

server:
shutdown: graceful

logging:
level:
root: INFO
com.mogumogu.momogo: DEBUG