Skip to content

Commit 08092a5

Browse files
GREENOVERzekexros
andauthored
[Infra] CI/CD 배포 자동 일원화 스크립트 추가 작업 (#14)
* infra: CI/CD 배포 자동 일원화 스크립트 추가 작업 * �infra: mise install 설치 시 과정 생략 Co-authored-by: JoonHyeok Yang <[email protected]> * infra: zshrc 기존 추가 확인 시 과정 생략 Co-authored-by: JoonHyeok Yang <[email protected]> --------- Co-authored-by: JoonHyeok Yang <[email protected]>
1 parent 0e06754 commit 08092a5

11 files changed

+2112
-12
lines changed

Diff for: .gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,8 @@ Derived/
149149
Tuist/Dependencies
150150
Tuist/.build
151151

152+
# Node.js
153+
node_modules
154+
.grunt-env
155+
152156
# End of https://www.toptal.com/developers/gitignore/api/macos,swift,swiftpackagemanager,xcode

Diff for: .grunt-env.example

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PERSONAL_PRIVATE_TOKEN=

Diff for: Gemfile.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ GEM
1010
artifactory (3.0.17)
1111
atomos (0.1.3)
1212
aws-eventstream (1.3.0)
13-
aws-partitions (1.932.0)
13+
aws-partitions (1.933.0)
1414
aws-sdk-core (3.196.1)
1515
aws-eventstream (~> 1, >= 1.3.0)
1616
aws-partitions (~> 1, >= 1.651.0)
1717
aws-sigv4 (~> 1.8)
1818
jmespath (~> 1, >= 1.6.1)
19-
aws-sdk-kms (1.81.0)
19+
aws-sdk-kms (1.82.0)
2020
aws-sdk-core (~> 3, >= 3.193.0)
2121
aws-sigv4 (~> 1.1)
2222
aws-sdk-s3 (1.151.0)

Diff for: Gruntfile.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const onboarding = require("./grunt/onboarding");
2+
const deployLocal = require("./grunt/deploy-local");
3+
4+
module.exports = function (grunt) {
5+
grunt.initConfig({
6+
availabletasks: {
7+
tasks: {},
8+
},
9+
});
10+
grunt.loadNpmTasks("grunt-available-tasks");
11+
12+
grunt.registerTask("default", ["availabletasks"]);
13+
14+
grunt.registerTask("onboarding", "Github Personal AccessToken을 설정합니다.", onboarding);
15+
16+
grunt.registerTask("deploy", "fastlane을 통해 local에서 배포를 진행합니다.", deployLocal);
17+
};

Diff for: README.md

+26-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1-
# gabbangzip-iOS
1+
# 가자 iOS 빵집으로~ 🍞
22

3-
# 프로젝트 설정 및 실행
3+
# 🤖 프로젝트 설정 및 실행
44
1. `make bootstrap`
55
- 최초 클론 시 터미널에서 한번만 실행하면 됩니다.
6-
- Bundler & fastlane & mise 설치
6+
- Bundler & fastlane & npm & grunt & mise 설치
7+
- 설치 중 워닝 시 해당 워닝 메시지 명령어를 기입하여 진행합니다.
78
2. `make generate`
89
- Tuist를 이용한 워크스페이스 및 프로젝트 생성하는 과정입니다.
910
- swiftLint를 적용해두어 기존 tuist generate를 사용해도 되지만, 아래와 같이 사용하셔야 합니다.
1011
`TUIST_ROOT_DIR=$PWD tuist generate`
1112
- 가급적이면 해당 `make generate`로 프로젝트 생성하시길 권장드립니다.
13+
***
14+
# 🚀 배포하기
15+
### 1️⃣ grunt 명령어를 이용한 배포 및 후속 처리 (사용 권장)
16+
1. 개인 Github personal access tokens 설정을 위한 온보딩
17+
`grunt onboarding`
18+
해당 명령어를 통해 개인 깃헙 토큰을 입력하여 환경 변수에 저장해둡니다.
19+
2. 배포 및 후속처리
20+
`grunt deploy`
21+
해당 터미널에 안내되는 스텝에 따라 진행하면 됩니다.
22+
항상 develop branch가 배포됩니다.
23+
- fastlane 배포 (beta - dev + prod)
24+
- 릴리즈 커밋 자동 생성 및 푸쉬
25+
- 빌드 버전 넘버 업데이트
26+
- 스텝 예시
27+
1️⃣ Apple Developer 이메일을 입력해 주세요. (ex. [email protected])
28+
29+
2️⃣ bump type (no, patch, minor, major) 또는 특정 버전(1.1.0)을 입력하세요. / 현재 버전은 1.0.0 입니다.
30+
patch
31+
3️⃣ git reset --h 명령어가 실행됩니다. 커밋되지 않은 변경사항은 모두 삭제됩니다. (y/n)
32+
y
1233

13-
# 배포
14-
### fastlane을 이용한 배포
34+
### 2️⃣ fastlane 명령어를 이용한 배포
1535
- 추후 앱 등록 후 팀 계정 ID 등 환경값 변경 후 사용 가능 (추후 업데이트 예정)
1636
- Dev / Prod 원하는 앱 배포가 가능합니다.
1737
`bundle exec fastlane dev || prod || beta(dev + prod)`
1838
- gabbangzip.shared.xccofing 파일에서 마케팅 버전 수정하여 사용
19-
- 추후 스크립트로 일원화 하는 작업 시간나면 할 예정
39+
***

Diff for: etc/script/bootstrap.sh

+31-4
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,40 @@ gem install bundler
5858
echo "\n[3] > Updating Bundle (with Fastlane) ...\n"
5959
BUNDLE_GEMFILE="${PROJECT_DIR}/Gemfile" bundle update
6060

61+
# npm 설치
62+
if ! command -v npm &> /dev/null; then
63+
echo "\n[4] > Installing npm ...\n"
64+
brew install node
65+
fi
66+
67+
# grunt-cli 설치
68+
cd "${PROJECT_DIR}"
69+
if ! command -v grunt &> /dev/null; then
70+
echo "\n[5] > Installing grunt-cli ...\n"
71+
npm install -g grunt-cli
72+
fi
73+
74+
# 로컬 grunt 설치
75+
if [[ ! -d node_modules/grunt ]]; then
76+
echo "\n[6] > Installing grunt ...\n"
77+
npm install
78+
fi
79+
6180
# mise 설치
62-
echo "\n[4] > Installing mise ...\n"
63-
curl https://mise.run | sh
81+
if ! command -v mise &> /dev/null; then
82+
echo "\n[7] > Installing mise ...\n"
83+
curl https://mise.run | sh
84+
else
85+
echo "mise is already installed."
86+
fi
6487

6588
# mise 활성화
66-
echo "\n[5] > Activating mise ...\n"
67-
echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc
89+
echo "\n[8] > Activating mise ...\n"
90+
if ! grep -q 'eval "$(~/.local/bin/mise activate zsh)"' ~/.zshrc; then
91+
echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc
92+
else
93+
echo "mise is already activated in ~/.zshrc"
94+
fi
6895

6996
echo "\n---------------------------------"
7097
echo "::: Bootstrap Script Finished :::"

Diff for: grunt/deploy-local.js

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
const fs = require("fs");
2+
const { question } = require("./helpers");
3+
const shell = require("shelljs");
4+
const semver = require("semver");
5+
6+
module.exports = async function deployLocal() {
7+
const done = this.async();
8+
9+
const { appleID, newVersionNumber, newBuildNumber } = await fetchEnv();
10+
11+
await checkoutBranch(newVersionNumber, newBuildNumber);
12+
13+
await setBuildInfo(newVersionNumber, newBuildNumber);
14+
createReleaseCommit(newVersionNumber, newBuildNumber);
15+
16+
await deploy(appleID, newVersionNumber, newBuildNumber);
17+
18+
done();
19+
};
20+
21+
async function setBuildInfo(newVersionNumber, newBuildNumber) {
22+
const { execa } = await import("execa");
23+
const configFilePath = "./Projects/App/xcconfigs/Gabbangzip.shared.xcconfig";
24+
const xcodeProjPath = "./Projects/App/App.xcodeproj"
25+
26+
_setXCConfigValue("MARKETING_VERSION", newVersionNumber, configFilePath);
27+
28+
await execa("bundle", ["exec", "fastlane", "run", "increment_build_number", `build_number:${newBuildNumber}`, `xcodeproj:${xcodeProjPath}`], {
29+
stdio: "inherit",
30+
});
31+
}
32+
33+
function createReleaseCommit(newVersionNumber, newBuildNumber) {
34+
const configFilePath = "./Projects/App/xcconfigs/Gabbangzip.shared.xcconfig";
35+
const commitMessage = `:bookmark: v${newVersionNumber}-${newBuildNumber}`;
36+
const infoPlistFilePath = "./Projects/App/Info.plist"
37+
38+
if (shell.exec(`git add ${infoPlistFilePath} ${configFilePath} && git commit -m "${commitMessage}" && git push`).code > 0) {
39+
shell.echo("❌ 'createReleaseCommit' failed");
40+
shell.exit(1);
41+
}
42+
}
43+
44+
async function fetchEnv() {
45+
const appleID = await question(`🙏 Apple Developer 이메일을 입력해 주세요. (ex. [email protected]) \n> `);
46+
47+
const currentVersion = fetchCurrentVersion();
48+
const bumpType = await question(
49+
`\n🙏 bump type (no, patch, minor, major) 또는, 특정 버전(1.1.0)을 입력하세요. / 현재 버전은, '${currentVersion}' 입니다. \n> `
50+
);
51+
52+
/** @type {string} */
53+
const newVersionNumber = _newVersion(currentVersion, bumpType);
54+
const newBuildNumber = _newBuildNumber();
55+
56+
shell.echo(`\n===입력 정보 확인===\n🔑 Account: ${appleID}\n🎯 Version: '${newVersionNumber} - ${newBuildNumber}'\n`);
57+
58+
return {
59+
appleID,
60+
newVersionNumber,
61+
newBuildNumber,
62+
};
63+
}
64+
65+
async function deploy(appleID, newVersionNumber, newBuildNumber) {
66+
const { execa } = await import("execa");
67+
await execa("grunt", ["gp"]);
68+
await execa(
69+
"bundle",
70+
["exec", "fastlane", "ios", "beta", `new_version_number:${newVersionNumber}`, `new_build_number:${newBuildNumber}`],
71+
{
72+
env: { ...process.env, APPLE_ID: appleID },
73+
stdio: "inherit",
74+
}
75+
);
76+
}
77+
78+
async function checkoutBranch(newVersionNumber, newBuildNumber) {
79+
const { execa } = await import("execa");
80+
const res = await question("⚠️ 'git reset --h' 명령어가 실행됩니다. 커밋되지 않은 변경사항은 모두 삭제됩니다. [y/n]\n>");
81+
82+
switch (res) {
83+
case "Y":
84+
case "y":
85+
shell.echo("\n🌀 'git reset --h' 실행중");
86+
await execa("git", ["reset", "--h"]);
87+
shell.echo("✅ 'git reset --h' 완료\n");
88+
break;
89+
90+
case "N":
91+
case "n":
92+
shell.echo("👋 종료됨");
93+
shell.exit(1);
94+
default:
95+
shell.echo("👋 선택지에 없는 입력입니다.");
96+
shell.exit(1);
97+
}
98+
99+
await execa("git", ["fetch"]);
100+
shell.echo("✅ 'git fetch' 완료\n");
101+
102+
shell.echo("🌀 'git checkout develop' 실행중");
103+
await execa("git", ["checkout", "develop"]);
104+
shell.echo("✅ 'git checkout develop' 완료\n");
105+
106+
await execa("git", ["pull"]);
107+
shell.echo("✅ 'git pull' 완료\n");
108+
109+
shell.echo(`🌀 'git checkout -b release/v${newVersionNumber}-${newBuildNumber}' 실행중`);
110+
await execa("git", ["checkout", "-b", `release/v${newVersionNumber}-${newBuildNumber}`]);
111+
shell.echo(`✅ 'git checkout -b release/v${newVersionNumber}-${newBuildNumber}' 완료\n`);
112+
113+
shell.echo(`🌀 'git push --set-upstream origin release/v${newVersionNumber}-${newBuildNumber}'실행중`);
114+
await execa("git", ["push", "--set-upstream", `origin`, `release/v${newVersionNumber}-${newBuildNumber}`]);
115+
shell.echo(`✅ 'git push --set-upstream origin release/v${newVersionNumber}-${newBuildNumber}' 완료\n`);
116+
}
117+
118+
function fetchCurrentVersion() {
119+
const configFilePath = "./Projects/App/xcconfigs/Gabbangzip.shared.xcconfig";
120+
121+
const currentVersion = _getXCConfigValue("MARKETING_VERSION", configFilePath);
122+
123+
if (currentVersion === undefined) {
124+
shell.echo("failed to get xcconfig value 'MARKETING_VERSION'");
125+
shell.exit(1);
126+
}
127+
128+
return currentVersion;
129+
}
130+
131+
function _newVersion(currentVersion, bumpType) {
132+
switch (bumpType) {
133+
case "patch":
134+
case "minor":
135+
case "major":
136+
return semver.inc(currentVersion, bumpType);
137+
case "no":
138+
return currentVersion;
139+
default:
140+
const specificVersion = bumpType;
141+
return specificVersion;
142+
}
143+
}
144+
145+
function _newBuildNumber() {
146+
const now = new Date();
147+
const year = now.getFullYear().toString().padStart(4, "0");
148+
const month = (now.getMonth() + 1).toString().padStart(2, "0");
149+
const date = now.getDate().toString().padStart(2, "0");
150+
const hour = now.getHours().toString().padStart(2, "0");
151+
const minute = now.getMinutes().toString().padStart(2, "0");
152+
return `${year}${month}${date}${hour}${minute}`;
153+
}
154+
155+
/**
156+
*
157+
* @param {string} key
158+
* @param {string} value
159+
* @param {string} configFilePath
160+
*/
161+
function _setXCConfigValue(key, value, configFilePath) {
162+
const entities = _xcconfigEntities(configFilePath);
163+
const newEntities = entities.map((entity) => {
164+
if (entity.length !== 2) {
165+
return entity;
166+
}
167+
168+
const [_key, _] = entity;
169+
if (_key !== key) {
170+
return entity;
171+
}
172+
173+
return [_key, value];
174+
});
175+
const lines = newEntities.map((entity) => entity.join("="));
176+
const data = lines.join("\n");
177+
fs.writeFileSync(configFilePath, data);
178+
}
179+
180+
/**
181+
*
182+
* @param {string} key
183+
* @param {string} configFilePath
184+
* @returns {(string|undefined)} value
185+
*/
186+
function _getXCConfigValue(key, configFilePath) {
187+
const entities = _xcconfigEntities(configFilePath);
188+
const entity =
189+
entities.find((entity) => {
190+
return entity[0] === key;
191+
}) ?? [];
192+
return entity[1];
193+
}
194+
195+
/**
196+
*
197+
* @param {string} configFilePath
198+
* @returns {string[][]} entities
199+
*/
200+
function _xcconfigEntities(configFilePath) {
201+
const xcconfigStr = fs.readFileSync(configFilePath, {
202+
encoding: "utf8",
203+
});
204+
const lines = xcconfigStr.split("\n");
205+
return lines.map((line) => {
206+
return line.split("=", 2);
207+
});
208+
}

Diff for: grunt/helpers.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
*
3+
* @param {string} questionText The question text to prompt to user
4+
* @returns {Promise<string>}
5+
*/
6+
async function question(questionText) {
7+
return new Promise((resolve, reject) => {
8+
const readline = require("readline").createInterface({
9+
input: process.stdin,
10+
output: process.stdout,
11+
});
12+
readline.question(questionText, (answer) => {
13+
resolve(answer);
14+
readline.close();
15+
readline.removeAllListeners();
16+
});
17+
});
18+
}
19+
20+
module.exports = {
21+
question,
22+
};

0 commit comments

Comments
 (0)