Skip to content

fix(app): align day chip, checkbox, and scrollbar with Figma (#137) #27

fix(app): align day chip, checkbox, and scrollbar with Figma (#137)

fix(app): align day chip, checkbox, and scrollbar with Figma (#137) #27

Workflow file for this run

name: Build Moa App
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., v1.0.0)'
required: true
type: string
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
# 빌드 대상 버전과 실행 여부를 결정한다.
# - 태그 push / workflow_dispatch: 항상 빌드 (명시적 의도)
# - main push: 릴리스 PR 머지로 tauri.conf.json 이 변경되고, 해당 버전 릴리스가
# 아직 없을 때만 빌드 (일반 push 는 skip)
resolve:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.v.outputs.version }}
tag: ${{ steps.v.outputs.tag }}
is_prerelease: ${{ steps.v.outputs.is_prerelease }}
should_build: ${{ steps.v.outputs.should_build }}
steps:
- uses: actions/checkout@v4
- name: Resolve version
id: v
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
INPUT_VERSION: ${{ inputs.version }}
BEFORE: ${{ github.event.before }}
SHA: ${{ github.sha }}
run: |
if [ "$EVENT_NAME" = "push" ] && [ "$REF_TYPE" = "tag" ]; then
VERSION="${REF_NAME#v}"
SHOULD_BUILD=true
elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then
VERSION="${INPUT_VERSION#v}"
SHOULD_BUILD=true
else
# main push: 버전은 package.json 에서 읽고, 릴리스 커밋인지 검증한다.
VERSION="$(jq -r .version apps/app/package.json)"
SHOULD_BUILD=false
CHANGED=$(gh api "repos/$REPO/compare/${BEFORE}...${SHA}" --jq '.files[].filename' 2>/dev/null || true)
if echo "$CHANGED" | grep -qx 'apps/app/src-tauri/tauri.conf.json'; then
if ! gh release view "v$VERSION" --repo "$REPO" >/dev/null 2>&1; then
SHOULD_BUILD=true
else
echo "릴리스 v$VERSION 가 이미 존재하여 빌드를 건너뜁니다."
fi
else
echo "tauri.conf.json 변경이 없어 릴리스 push 가 아닙니다. 빌드를 건너뜁니다."
fi
fi
TAG="v${VERSION}"
if [[ "$VERSION" == *-* ]]; then IS_PRE=true; else IS_PRE=false; fi
{
echo "version=$VERSION"
echo "tag=$TAG"
echo "is_prerelease=$IS_PRE"
echo "should_build=$SHOULD_BUILD"
} >> "$GITHUB_OUTPUT"
echo "resolved: version=$VERSION tag=$TAG prerelease=$IS_PRE should_build=$SHOULD_BUILD"
notify-start:
needs: [resolve]
if: needs.resolve.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- name: Discord Build Start Notification
continue-on-error: true
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
VERSION: ${{ needs.resolve.outputs.tag }}
ACTOR: ${{ github.actor }}
run: |
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
PAYLOAD=$(jq -n \
--arg version "$VERSION" \
--arg actor "$ACTOR" \
--arg timestamp "$TIMESTAMP" \
'{
embeds: [{
title: ("🚀 Moa App " + $version + " 빌드 시작"),
color: 3447003,
fields: [
{name: "버전", value: $version, inline: true},
{name: "배포자", value: $actor, inline: true},
{name: "대상", value: "macOS (aarch64), macOS (x86_64), Windows"}
],
timestamp: $timestamp
}]
}')
curl -f -H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$DISCORD_WEBHOOK"
# draft 릴리스를 매트릭스 이전에 단 한 번 생성한다.
# 매트릭스 잡들이 각자 릴리스를 생성/조회하면 동시 접근 경쟁으로
# GitHub 이 간헐적 401(Requires authentication) 을 반환하므로,
# 생성을 분리하고 매트릭스는 release_id 로 업로드만 한다.
create-release:
needs: [resolve, notify-start]
if: needs.resolve.outputs.should_build == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
release_id: ${{ steps.create.outputs.result }}
steps:
- name: Create draft release
id: create
uses: actions/github-script@v7
env:
TAG: ${{ needs.resolve.outputs.tag }}
IS_PRERELEASE: ${{ needs.resolve.outputs.is_prerelease }}
with:
result-encoding: string
script: |
const body = [
`## Moa ${process.env.TAG}`,
'',
'### 설치 방법',
'',
'#### macOS',
'- **Apple Silicon**: `moa_<version>_aarch64.dmg`',
'- **Intel** (x86_64): `moa_<version>_x64.dmg`',
'',
'`.dmg` 파일을 다운로드하여 Applications 폴더로 드래그하세요.',
'',
'#### Windows',
'- **EXE**: `moa_<version>_x64-setup.exe`',
'',
'#### 자동 업데이트',
'기존 사용자는 자동 업데이트 알림을 받습니다.',
].join('\n')
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: process.env.TAG,
name: `Moa ${process.env.TAG}`,
body,
draft: true,
prerelease: process.env.IS_PRERELEASE === 'true',
})
return data.id
publish-tauri:
needs: [resolve, create-release]
if: needs.resolve.outputs.should_build == 'true'
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-latest'
args: '--target aarch64-apple-darwin'
- platform: 'macos-latest'
args: '--target x86_64-apple-darwin'
- platform: 'windows-latest'
args: '--bundles nsis,updater'
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: Install frontend dependencies
run: bun install
- name: Import Apple Certificate (macOS)
if: matrix.platform == 'macos-latest'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Setup Apple API Key (macOS)
if: matrix.platform == 'macos-latest'
env:
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
run: |
KEY_PATH="$HOME/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8"
mkdir -p "$(dirname "$KEY_PATH")"
echo -n "$APPLE_API_KEY_CONTENT" | base64 --decode -o "$KEY_PATH"
echo "APPLE_API_KEY_PATH=$KEY_PATH" >> $GITHUB_ENV
- name: Build and release
uses: tauri-apps/tauri-action@action-v0.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }}
KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CLIENT_ID: ${{ secrets.APPLE_CLIENT_ID }}
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_PRIVATE_KEY: ${{ secrets.APPLE_PRIVATE_KEY }}
with:
# 릴리스는 create-release 잡에서 이미 생성됨 → ID 로 업로드만 수행.
releaseId: ${{ needs.create-release.outputs.release_id }}
includeUpdaterJson: true
args: ${{ matrix.args }}
# GitHub 자동 생성 릴리스 노트("What's Changed")를 draft 릴리스 본문에 덧붙인다.
# 베타/정식 모두 적용 — 베타는 draft 로 남지만 노트는 미리 채워둔다.
release-notes:
needs: [resolve, publish-tauri]
if: needs.resolve.outputs.should_build == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Generate and append release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ needs.resolve.outputs.tag }}
run: |
CUR=$(gh release view "$TAG" --repo "$REPO" --json body --jq .body)
if echo "$CUR" | grep -q "What's Changed"; then
echo "릴리스 노트가 이미 존재하여 건너뜁니다."
exit 0
fi
NOTES=$(gh api "repos/$REPO/releases/generate-notes" \
-f tag_name="$TAG" --jq .body 2>/dev/null || true)
if [ -z "$NOTES" ]; then
echo "생성된 노트가 없어 본문을 그대로 둡니다."
exit 0
fi
gh release edit "$TAG" --repo "$REPO" --notes "$CUR
$NOTES"
# 정식 릴리스만 draft 를 해제(publish)한다 → 전체 사용자 자동 업데이트 트리거.
# 베타(-beta 등 prerelease)는 draft 로 남겨 자동 업데이트를 막는다.
publish-release:
needs: [resolve, release-notes]
if: needs.resolve.outputs.is_prerelease == 'false'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ needs.resolve.outputs.tag }}
run: gh release edit "$TAG" --repo "$REPO" --draft=false
# publish 는 GITHUB_TOKEN 으로 수행되어 release:published 이벤트가 deploy 를
# 트리거하지 못하므로, 같은 런에서 reusable workflow 로 직접 호출한다.
deploy:
needs: [resolve, publish-release]
if: needs.resolve.outputs.is_prerelease == 'false'
uses: ./.github/workflows/deploy-app.yml
with:
version: ${{ needs.resolve.outputs.tag }}
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
notify-end:
needs: [resolve, publish-tauri]
runs-on: ubuntu-latest
if: always() && needs.resolve.outputs.should_build == 'true'
steps:
- name: Discord Notification
continue-on-error: true
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
BUILD_RESULT: ${{ needs.publish-tauri.result }}
VERSION: ${{ needs.resolve.outputs.tag }}
ACTOR: ${{ github.actor }}
REPO: ${{ github.repository }}
run: |
if [ "$BUILD_RESULT" == "success" ]; then
COLOR=5763719
TITLE="✅ Moa App $VERSION 빌드 성공"
else
COLOR=15548997
TITLE="❌ Moa App $VERSION 빌드 실패"
fi
RELEASE_URL="https://github.com/${REPO}/releases"
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
PAYLOAD=$(jq -n \
--arg title "$TITLE" \
--argjson color "$COLOR" \
--arg version "$VERSION" \
--arg actor "$ACTOR" \
--arg release_url "$RELEASE_URL" \
--arg timestamp "$TIMESTAMP" \
'{
embeds: [{
title: $title,
color: $color,
fields: [
{name: "버전", value: $version, inline: true},
{name: "배포자", value: $actor, inline: true},
{name: "릴리스", value: $release_url}
],
timestamp: $timestamp
}]
}')
curl -f -H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$DISCORD_WEBHOOK"