-
Notifications
You must be signed in to change notification settings - Fork 0
139 lines (130 loc) · 8.3 KB
/
Copy pathrelease.yml
File metadata and controls
139 lines (130 loc) · 8.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# master에 앱 코드가 들어오면 버전을 올리고 릴리스까지 자동으로 간다 (요구 2026-08-01).
#
# **이 파일은 docs/DESIGN.md §CI C3(「굽지 않는다」)를 뒤집는다.** C3의 근거는 편의가 아니라
# 「서명·공증은 사람 맥의 인증서로 한다 · 시크릿은 레포에 들어가지 않는다」(§배포)였다.
# 뒤집는 대가는 **인증서가 GitHub Actions 시크릿으로 올라가는 것**이다 — 레포 파일에는
# 여전히 안 들어가지만, 사람 맥 밖으로 나가는 것은 맞다. 아래 5개가 없으면 릴리스는
# `release.sh`의 선행 확인에서 **bump 전에** 멈추고 무엇이 없는지 찍는다(조용히 안 멈춘다).
#
# MACOS_CERT_P12 `security export`한 Developer ID Application .p12의 base64
# MACOS_CERT_PASSWORD 그 .p12의 암호
# APPLE_ID 공증 계정
# APPLE_APP_SPECIFIC_PASSWORD
# APPLE_TEAM_ID
#
# 굽는 절차 자체는 여기 없다. `release.sh` 하나가 사람 맥에서도 CI에서도 같은 순서로 돈다
# (§릴리스 R4) — 두 벌로 갈라지면 사람 맥에서만 맞는 릴리스가 생긴다.
name: release
on:
# **매 master 커밋마다다** — path 필터 없다(요구 2026-08-01). 대가를 알고 고른 값이다:
# 스펙·README만 고친 커밋도 patch를 올리고 받는 사람에게 내용이 같은 버전이 내려간다.
# 줄이려면 여기에 `paths: ['apps/**', 'tick.sh', 'tickets.py', 'templates/**']`를 넣으면 된다.
push:
branches: [master]
workflow_dispatch:
inputs:
bump:
description: 'patch | minor | major'
default: patch
# 두 개가 겹치면 둘 다 같은 버전을 bump하고 뒤엣것의 push가 거절된다. 취소하지는 않는다 —
# 굽던 릴리스를 중간에 죽이면 태그만 남고 자산이 없는 상태가 된다(R4가 막으려던 그것).
concurrency:
group: release
cancel-in-progress: false
jobs:
release:
# 사람이 자기 맥에서 `pnpm release minor`를 치면 release.sh 5번이 **사람 자격으로** master를
# 민다 — 아래 GITHUB_TOKEN 예외는 CI가 민 push에만 걸려서, 이게 없으면 러너가 그 위에
# patch를 한 번 더 얹는다(v0.2.0 뒤에 v0.2.1이 서고 뒤엣것의 릴리즈 노트는 빈 구간이다).
# 판정 문자열은 release.sh가 만드는 커밋 제목 `release v<x.y.z>` 하나뿐이다(§릴리스 R4-1).
# startsWith(...) 하나만 쓰는 것이 오답이다 — workflow_dispatch에는 head_commit이 아예 없어서
# 손으로 내는 minor·major의 판정이 GHA의 null 처리에 매달리고, 막히면 낼 자리가 사라진다.
# 첫 항(`!= 'push'`)이 앞에 있으면 dispatch는 거기서 끝나 startsWith에 닿지 않는다.
if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'release v')
runs-on: macos-latest
permissions:
contents: write
steps:
# ref를 안 주면 detached HEAD로 떨어져서 release.sh의 「브랜치가 master」 확인에 걸린다.
- uses: actions/checkout@v7
with:
ref: master
fetch-depth: 0
- uses: pnpm/action-setup@v6
with:
version: 11
- uses: actions/setup-node@v7
with:
node-version: 25
# 인증서를 임시 키체인에 넣고 **검색목록에 올린다.** electron-builder만 보는 게 아니다 —
# sign-preflight.sh와 sign-dmg.sh가 `security find-identity`로 찾는다(CSC_LINK만 주면
# 그 둘은 못 본다). set-key-partition-list를 빼면 codesign이 errSecInternalComponent로 죽는다.
- name: 인증서를 임시 키체인에 넣는다
env:
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
# preflight는 이 셋도 본다. 안 주면 여기서만 「공증 건너뜀」을 찍어서, 아래 릴리스
# 스텝이 멀쩡히 공증하는데도 로그를 읽는 사람은 공증이 빠진 줄 안다(실측 2026-08-10).
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
[ -n "$MACOS_CERT_P12" ] || { echo "MACOS_CERT_P12이 비어 있다 — 시크릿 5개를 채워야 한다(이 파일 머리말)." >&2; exit 1; }
kc="$RUNNER_TEMP/build.keychain-db"
security create-keychain -p "" "$kc"
security set-keychain-settings "$kc" # 잠금 타임아웃 없음 — 공증 대기 중에 잠기면 스테이플에서 죽는다
security unlock-keychain -p "" "$kc"
printf '%s' "$MACOS_CERT_P12" | base64 -d > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$kc" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
rm -f "$RUNNER_TEMP/cert.p12"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "$kc" >/dev/null
security list-keychains -d user -s "$kc" $(security list-keychains -d user | tr -d '" ')
./apps/desktop/sign-preflight.sh
- run: pnpm install
working-directory: apps/teams
- run: pnpm install
working-directory: apps/desktop
# ci.yml과 겹치지만 여기서 다시 돈다. 두 워크플로는 서로를 기다리지 않아서, 이게 없으면
# ci.yml이 빨간 것과 무관하게 남의 맥으로 나가는 `.dmg`가 구워진다. 초 단위라 아깝지 않다.
- run: python3 test_generic.py && python3 test_reap_manual.py && python3 test_claim_race.py && python3 test_feed_stall.py && python3 test_inbox.py
- run: pnpm test
working-directory: apps/teams
- run: pnpm test
working-directory: apps/desktop
# GA4 자격값을 번들에 넣는다 (§0-11 §자격값). **레포 파일에는 안 들어간다** — 시크릿에서
# 받아 `apps/desktop/ga.json`으로 떨어뜨리고 `build.files`가 `.app` 안으로 나른다.
# 그 파일은 `.gitignore`에 있어서 release.sh의 「워킹 트리가 깨끗하다」 확인에 안 걸린다.
#
# **없으면 그대로 성공한다** — 인증서 5개와 다른 규칙이다. 저건 없으면 남의 맥에서 안
# 열리는 자산이 나가지만, 자격값이 없는 앱은 통계를 안 보낼 뿐 정상 동작한다(§0-11).
# 값 둘을 실제로 올리는 것은 사람이 한다. 올라오면 다음 릴리스부터 실린다.
- name: GA4 자격값
working-directory: apps/desktop
env:
GA_MEASUREMENT_ID: ${{ secrets.GA_MEASUREMENT_ID }}
GA_API_SECRET: ${{ secrets.GA_API_SECRET }}
run: |
if [ -z "$GA_MEASUREMENT_ID" ] || [ -z "$GA_API_SECRET" ]; then
echo "GA 시크릿이 없다 — 통계를 안 보내는 앱으로 굽는다(정상)."
exit 0
fi
# 따옴표·개행이 섞인 값도 안전하게 넣으려고 node로 쓴다(printf면 이스케이프가 샌다).
node -e 'require("fs").writeFileSync("ga.json", JSON.stringify({GA_MEASUREMENT_ID: process.env.GA_MEASUREMENT_ID, GA_API_SECRET: process.env.GA_API_SECRET}))'
echo "ga.json을 만들었다 — 이 빌드는 통계를 보낸다."
# release.sh가 커밋·태그·push를 한다. 저자가 없으면 git commit이 거기서 죽는다.
#
# **되먹임이 없는 이유가 여기 걸려 있다.** bump 커밋은 `apps/desktop/package.json`만
# 건드리는데 그건 위 paths에 걸린다 — 그런데도 릴리스가 릴리스를 부르지 않는 것은
# `GITHUB_TOKEN`으로 나간 push가 워크플로를 다시 트리거하지 않기 때문이다. PAT로 바꾸면
# 그 규칙이 사라지고 무한 릴리스가 된다.
- name: 릴리스
working-directory: apps/desktop
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
./release.sh "${{ inputs.bump || 'patch' }}"