feat: Phase 1 mobile tablet — Jenkins APK, PC tab, in-app update
Capacitor 확장으로 git push 시 Mobile Jenkins 빌드, PC 탭 Android/iPad 다운로드, 앱 내 APK 업데이트, iOS TestFlight API 및 Capacitor ios 프로젝트를 추가한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# GoldenChart Android APK — 로컬(CI) 빌드 → data/mobile-releases/
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
APK_REMOTE_NAME="${APK_REMOTE_NAME:-goldenchart-android.apk}"
|
||||
LOCAL_APK="${LOCAL_APK:-$ROOT/data/mobile-releases/$APK_REMOTE_NAME}"
|
||||
|
||||
log() { echo "[build-mobile-apk $(date '+%H:%M:%S')] $*"; }
|
||||
|
||||
sign_release_apk() {
|
||||
local unsigned="$ROOT/app/android/app/build/outputs/apk/release/app-release-unsigned.apk"
|
||||
local signed="$ROOT/app/android/app/build/outputs/apk/release/app-release.apk"
|
||||
local keystore="$ROOT/app/android/goldenchart-release.keystore"
|
||||
|
||||
if [[ ! -f "$unsigned" ]]; then
|
||||
echo "unsigned APK 없음: $unsigned" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$keystore" ]]; then
|
||||
log "keystore 생성..."
|
||||
"${JAVA_HOME}/bin/keytool" -genkeypair -v -keystore "$keystore" -alias goldenchart \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-storepass goldenchart -keypass goldenchart \
|
||||
-dname "CN=GoldenChart, OU=Mobile, O=GoldenChart, L=Seoul, ST=Seoul, C=KR"
|
||||
fi
|
||||
|
||||
local apksigner
|
||||
apksigner="${ANDROID_HOME:-$HOME/Library/Android/sdk}/build-tools/35.0.0/apksigner"
|
||||
if [[ ! -x "$apksigner" ]]; then
|
||||
apksigner=$(command -v apksigner 2>/dev/null || true)
|
||||
fi
|
||||
if [[ -z "$apksigner" || ! -x "$apksigner" ]]; then
|
||||
echo "apksigner 없음. Android SDK build-tools 설치 필요." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$unsigned" "$signed"
|
||||
"$apksigner" sign --ks "$keystore" --ks-key-alias goldenchart \
|
||||
--ks-pass pass:goldenchart --key-pass pass:goldenchart "$signed"
|
||||
mkdir -p "$(dirname "$LOCAL_APK")"
|
||||
cp "$signed" "$LOCAL_APK"
|
||||
log "서명 완료: $LOCAL_APK"
|
||||
}
|
||||
|
||||
if [[ -z "${JAVA_HOME:-}" ]] && [[ -x "$HOME/.local/jdk-21.0.11+10/Contents/Home/bin/java" ]]; then
|
||||
export JAVA_HOME="$HOME/.local/jdk-21.0.11+10/Contents/Home"
|
||||
fi
|
||||
if [[ -z "${JAVA_HOME:-}" ]]; then
|
||||
echo "JAVA_HOME 미설정" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "google-services.json 확인"
|
||||
"$ROOT/scripts/ensure-android-google-services.sh"
|
||||
|
||||
log "cap:sync"
|
||||
npm run cap:sync
|
||||
|
||||
log "assembleRelease"
|
||||
(cd "$ROOT/app/android" && ./gradlew assembleRelease --no-daemon)
|
||||
|
||||
sign_release_apk
|
||||
|
||||
log "완료 $(du -h "$LOCAL_APK" | cut -f1)"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# Capacitor iOS 플랫폼 추가·동기화 (Mac + CocoaPods 권장)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT/app"
|
||||
|
||||
npm run build
|
||||
|
||||
if command -v pod >/dev/null 2>&1; then
|
||||
npx cap add ios 2>/dev/null || true
|
||||
npx cap sync ios
|
||||
else
|
||||
echo "CocoaPods 없음 — iOS 템플릿만 추출합니다 (pod install은 나중에)."
|
||||
node --input-type=module <<'NODE'
|
||||
import { loadConfig } from '@capacitor/cli/dist/config.js';
|
||||
import { addIOS } from '@capacitor/cli/dist/ios/add.js';
|
||||
import { editProjectSettingsIOS } from '@capacitor/cli/dist/ios/common.js';
|
||||
const config = await loadConfig({ cwd: process.cwd() });
|
||||
const fs = await import('fs');
|
||||
if (!fs.existsSync('ios')) {
|
||||
await addIOS(config);
|
||||
await editProjectSettingsIOS(config);
|
||||
}
|
||||
NODE
|
||||
echo "완료. brew install cocoapods 후: cd ios/App && pod install"
|
||||
fi
|
||||
|
||||
echo "Xcode: npm run cap:open:ios"
|
||||
echo "TestFlight: docs/ios-testflight-setup.md"
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Jenkins goldenChart-Mobile-Pipeline — Android APK 빌드·배포
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
|
||||
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
|
||||
for prefix in "$HOME/homebrew" /opt/homebrew; do
|
||||
[[ -x "$prefix/bin/brew" ]] && eval "$("$prefix/bin/brew" shellenv)"
|
||||
done
|
||||
|
||||
log() { echo "[mobile-pipeline $(date '+%H:%M:%S')] $*"; }
|
||||
|
||||
log "=== goldenChart Mobile Pipeline #${BUILD_NUMBER:-local} ==="
|
||||
log "WORK_TREE=$WORK_TREE"
|
||||
|
||||
cd "$WORK_TREE"
|
||||
|
||||
if [[ -d "${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}" ]]; then
|
||||
git --git-dir="${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}" \
|
||||
--work-tree="$WORK_TREE" checkout -f main 2>/dev/null || true
|
||||
fi
|
||||
|
||||
chmod +x "$ROOT/scripts/"*.sh 2>/dev/null || true
|
||||
|
||||
"$ROOT/scripts/build-mobile-apk.sh"
|
||||
|
||||
VERSION_NAME="$(grep -E 'versionName\s+"' "$ROOT/app/android/app/build.gradle" | head -1 | sed -E 's/.*versionName\s+"([^"]+)".*/\1/' || true)"
|
||||
|
||||
if [[ -n "$VERSION_NAME" ]]; then
|
||||
log "APK version → $VERSION_NAME"
|
||||
ENV_FILE="$WORK_TREE/.env"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
{ grep -v '^GC_MOBILE_APP_VERSION=' "$ENV_FILE" || true; echo "GC_MOBILE_APP_VERSION=${VERSION_NAME}"; } > "${ENV_FILE}.tmp"
|
||||
mv "${ENV_FILE}.tmp" "$ENV_FILE"
|
||||
else
|
||||
echo "GC_MOBILE_APP_VERSION=${VERSION_NAME}" >> "$ENV_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -x "$WORK_TREE/scripts/restart-backend-if-running.sh" ]]; then
|
||||
WORK_TREE="$WORK_TREE" "$WORK_TREE/scripts/restart-backend-if-running.sh" || true
|
||||
elif docker compose -f "$WORK_TREE/docker-compose.yml" ps -q backend 2>/dev/null | grep -q .; then
|
||||
log "backend 재시작 (APK 볼륨 반영)"
|
||||
(cd "$WORK_TREE" && docker compose up -d --no-deps backend) || true
|
||||
fi
|
||||
|
||||
log "Artifacts: $WORK_TREE/data/mobile-releases/"
|
||||
log "Pipeline complete."
|
||||
@@ -62,6 +62,19 @@ while read -r OLDREV NEWREV REFNAME; do
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] trigger-desktop-jenkins-build.sh 없음: $TRIGGER"
|
||||
fi
|
||||
|
||||
# Mobile(Android APK) Jenkins 빌드 — git push 성공 후 자동
|
||||
TRIGGER_MOBILE="${WORK_TREE}/scripts/trigger-mobile-jenkins-build.sh"
|
||||
if [ -x "$TRIGGER_MOBILE" ]; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Mobile Jenkins 빌드 트리거"
|
||||
if "$TRIGGER_MOBILE" >> "$DEPLOY_LOG" 2>&1; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Mobile 빌드 큐 등록 완료"
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] Mobile Jenkins 트리거 실패 (웹 배포는 성공)"
|
||||
fi
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] trigger-mobile-jenkins-build.sh 없음: $TRIGGER_MOBILE"
|
||||
fi
|
||||
else
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG '$BRANCH_NAME' — 배포 스킵 (main만)"
|
||||
fi
|
||||
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# docker compose backend가 실행 중이면 재시작 (APK·env 반영)
|
||||
set -euo pipefail
|
||||
|
||||
WORK_TREE="${WORK_TREE:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
cd "$WORK_TREE"
|
||||
|
||||
if docker compose ps -q backend 2>/dev/null | grep -q .; then
|
||||
echo "[restart-backend] docker compose up -d --no-deps backend"
|
||||
docker compose up -d --no-deps backend
|
||||
fi
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# exdev Jenkins — goldenChart-Mobile-Pipeline Job 생성
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
|
||||
|
||||
JENKINS_HOME="${JENKINS_HOME:-$HOME/.jenkins}"
|
||||
JENKINS_JOB="goldenChart-Mobile-Pipeline"
|
||||
JENKINS_TRIGGER_TOKEN="${JENKINS_MOBILE_TRIGGER_TOKEN:-${GC_MOBILE_JENKINS_TRIGGER_TOKEN:-goldenchart-mobile-build}}"
|
||||
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}"
|
||||
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
log() { echo "[install-mobile-jenkins] $*"; }
|
||||
|
||||
log "Jenkins job 생성: $JENKINS_JOB"
|
||||
mkdir -p "${JENKINS_HOME}/jobs/${JENKINS_JOB}"
|
||||
|
||||
cat > "${JENKINS_HOME}/jobs/${JENKINS_JOB}/config.xml" << JENKINS_XML
|
||||
<?xml version='1.1' encoding='UTF-8'?>
|
||||
<project>
|
||||
<description>GoldenChart Mobile — Capacitor Android APK</description>
|
||||
<keepDependencies>false</keepDependencies>
|
||||
<properties/>
|
||||
<scm class="hudson.plugins.git.GitSCM" plugin="git@5.0.0">
|
||||
<configVersion>2</configVersion>
|
||||
<userRemoteConfigs>
|
||||
<hudson.plugins.git.UserRemoteConfig>
|
||||
<url>file://${GIT_DIR}</url>
|
||||
</hudson.plugins.git.UserRemoteConfig>
|
||||
</userRemoteConfigs>
|
||||
<branches>
|
||||
<hudson.plugins.git.BranchSpec>
|
||||
<name>*/main</name>
|
||||
</hudson.plugins.git.BranchSpec>
|
||||
</branches>
|
||||
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
|
||||
<submoduleCfg class="empty-list"/>
|
||||
<extensions/>
|
||||
</scm>
|
||||
<canRoam>true</canRoam>
|
||||
<disabled>false</disabled>
|
||||
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
|
||||
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
|
||||
<authToken>${JENKINS_TRIGGER_TOKEN}</authToken>
|
||||
<triggers/>
|
||||
<concurrentBuild>false</concurrentBuild>
|
||||
<builders>
|
||||
<hudson.tasks.Shell>
|
||||
<command>#!/bin/bash
|
||||
set -euo pipefail
|
||||
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:\$PATH"
|
||||
export WORK_TREE="${WORK_TREE}"
|
||||
export GIT_DIR="${GIT_DIR}"
|
||||
export BUILD_NUMBER="\${BUILD_NUMBER}"
|
||||
export ANDROID_HOME="\${ANDROID_HOME:-\$HOME/Library/Android/sdk}"
|
||||
export JAVA_HOME="\${JAVA_HOME:-\$HOME/.local/jdk-21.0.11+10/Contents/Home}"
|
||||
"${WORK_TREE}/scripts/jenkins-mobile-pipeline.sh"
|
||||
</command>
|
||||
</hudson.tasks.Shell>
|
||||
</builders>
|
||||
<publishers>
|
||||
<hudson.tasks.ArtifactArchiver>
|
||||
<artifacts>data/mobile-releases/**</artifacts>
|
||||
<allowEmptyArchive>true</allowEmptyArchive>
|
||||
<onlyIfSuccessful>false</onlyIfSuccessful>
|
||||
<fingerprint>false</fingerprint>
|
||||
<defaultExcludes>true</defaultExcludes>
|
||||
</hudson.tasks.ArtifactArchiver>
|
||||
</publishers>
|
||||
<buildWrappers/>
|
||||
</project>
|
||||
JENKINS_XML
|
||||
|
||||
if curl -sf -o /dev/null "http://127.0.0.1:8090/login" 2>/dev/null; then
|
||||
curl -sf -X POST "http://127.0.0.1:8090/reload" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log "완료: http://127.0.0.1:8090/job/${JENKINS_JOB}/"
|
||||
log "원격 트리거 토큰: $JENKINS_TRIGGER_TOKEN"
|
||||
log "post-receive: trigger-mobile-jenkins-build.sh (git push main)"
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# Git push(main) 후 Jenkins goldenChart-Mobile-Pipeline 트리거
|
||||
set -euo pipefail
|
||||
|
||||
JENKINS_URL="${JENKINS_URL:-${GC_MOBILE_JENKINS_URL:-http://127.0.0.1:8090}}"
|
||||
JENKINS_JOB="${JENKINS_JOB:-${GC_MOBILE_JENKINS_JOB:-goldenChart-Mobile-Pipeline}}"
|
||||
JENKINS_USER="${JENKINS_USER:-${GC_MOBILE_JENKINS_USER:-}}"
|
||||
JENKINS_TOKEN="${JENKINS_TOKEN:-${GC_MOBILE_JENKINS_TOKEN:-}}"
|
||||
JENKINS_TRIGGER_TOKEN="${JENKINS_TRIGGER_TOKEN:-${GC_MOBILE_JENKINS_TRIGGER_TOKEN:-goldenchart-mobile-build}}"
|
||||
LOG_TAG="[trigger-mobile]"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_TAG $*"; }
|
||||
|
||||
JENKINS_URL="${JENKINS_URL%/}"
|
||||
JOB_API="${JENKINS_URL}/job/${JENKINS_JOB// /%20}"
|
||||
|
||||
curl_get() {
|
||||
if [[ -n "$JENKINS_USER" && -n "$JENKINS_TOKEN" ]]; then
|
||||
curl -sf -u "${JENKINS_USER}:${JENKINS_TOKEN}" "$@"
|
||||
else
|
||||
curl -sf "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
curl_post() {
|
||||
if [[ -n "$JENKINS_USER" && -n "$JENKINS_TOKEN" ]]; then
|
||||
curl -sf -X POST -u "${JENKINS_USER}:${JENKINS_TOKEN}" "$@"
|
||||
else
|
||||
curl -sf -X POST "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
log "Jenkins mobile build — $JENKINS_JOB @ $JENKINS_URL"
|
||||
|
||||
if ! curl -sf -o /dev/null "${JENKINS_URL}/login" 2>/dev/null; then
|
||||
log "[ERROR] Jenkins unreachable: $JENKINS_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if curl_get "${JOB_API}/lastBuild/api/json?tree=building" 2>/dev/null | grep -q '"building":true'; then
|
||||
log "[SKIP] 이미 빌드 진행 중"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CRUMB_HDR=()
|
||||
CRUMB_JSON="$(curl_get "${JENKINS_URL}/crumbIssuer/api/json" 2>/dev/null || true)"
|
||||
if [[ -n "$CRUMB_JSON" ]]; then
|
||||
FIELD="$(node -e "try{console.log(JSON.parse(process.argv[1]).crumbRequestField||'')}catch{}" "$CRUMB_JSON" 2>/dev/null || true)"
|
||||
VAL="$(node -e "try{console.log(JSON.parse(process.argv[1]).crumb||'')}catch{}" "$CRUMB_JSON" 2>/dev/null || true)"
|
||||
if [[ -n "$FIELD" && -n "$VAL" ]]; then
|
||||
CRUMB_HDR=(-H "${FIELD}: ${VAL}")
|
||||
fi
|
||||
fi
|
||||
|
||||
if ((${#CRUMB_HDR[@]})); then
|
||||
if curl_post "${CRUMB_HDR[@]}" -H "Content-Type: application/x-www-form-urlencoded" \
|
||||
"${JOB_API}/build?token=${JENKINS_TRIGGER_TOKEN}"; then
|
||||
log "빌드 큐 등록 완료 (token)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if curl_post "${CRUMB_HDR[@]}" -H "Content-Type: application/x-www-form-urlencoded" "${JOB_API}/build"; then
|
||||
log "빌드 큐 등록 완료"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
if curl_post -H "Content-Type: application/x-www-form-urlencoded" \
|
||||
"${JOB_API}/build?token=${JENKINS_TRIGGER_TOKEN}"; then
|
||||
log "빌드 큐 등록 완료 (token)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if curl_post -H "Content-Type: application/x-www-form-urlencoded" "${JOB_API}/build"; then
|
||||
log "빌드 큐 등록 완료"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log "[ERROR] Jenkins build trigger failed"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user