101 lines
6.1 KiB
Markdown
101 lines
6.1 KiB
Markdown
# macOS 인증 빌드
|
|
|
|
AppSok의 최종 배포 후보는 Flutter 검증, macOS release build, Developer ID signing, Apple notarization, stapling, Gatekeeper assessment를 모두 통과한 ZIP으로 본다.
|
|
|
|
## 최초 설정
|
|
|
|
remote Mac runner의 AppSok checkout에서 한 번 실행한다. credential을 교체할 때도 같은 명령을 다시 실행한다.
|
|
|
|
```bash
|
|
./scripts/setup-appsok-ci-secrets.sh
|
|
```
|
|
|
|
입력하는 값은 Mac login/keychain password, Apple ID email, Apple app-specific password, notary profile 이름, 그리고 Jenkins URL, Jenkins username, Jenkins API token이다. 기본 notary profile 이름은 `appsok-notary`다.
|
|
|
|
설정 스크립트는 다음 항목을 준비한다.
|
|
|
|
- `.sops.yaml`: AppSok CI secret 파일을 암호화할 age recipient rule
|
|
- `secrets/appsok.ci.sops.json`: SOPS로 암호화된 CI 입력값
|
|
- `$HOME/.config/sops/age/appsok-ci-key.txt`: runner local age private key
|
|
- macOS login Keychain의 notarytool credential profile
|
|
|
|
raw password, app-specific password, Jenkins API token, private endpoint, decrypted SOPS payload, private key 원문은 tracked file, task log, 최종 응답에 남기지 않는다.
|
|
|
|
## Jenkins Job 설정
|
|
|
|
`appsok-macos-certified` Jenkins job을 생성하거나 갱신하려면 아래 스크립트를 실행한다.
|
|
|
|
```bash
|
|
# 기본 동작은 설정 XML 파일만 stdout으로 출력 (dry-run)
|
|
./scripts/upsert-jenkins-certified-job.sh --dry-run
|
|
|
|
# 실제로 Jenkins API를 호출하여 job을 등록하거나 갱신
|
|
./scripts/upsert-jenkins-certified-job.sh --apply
|
|
```
|
|
|
|
### 동작 원리 및 보안 원칙
|
|
|
|
1. **Dry-run**: 기본 모드로, Jenkins XML 파일만 화면에 출력한다. API 토큰이나 비밀번호 등 secret 값은 노출되지 않는다. SCM URL은 로컬 Git remote origin 주소를 자동으로 추출해 사용한다.
|
|
2. **Apply**: `--apply` 플래그를 주면 `secrets/appsok.ci.sops.json`에 저장된 `jenkins_url`, `jenkins_username`, `jenkins_api_token`을 SOPS를 이용해 복호화한 뒤 Jenkins API(POST `/createItem` 혹은 POST `/config.xml`)를 호출하여 설정을 적용한다.
|
|
3. **Secret 비노출**: Jenkins API Token 및 credential 값은 job 설정 XML 내부에 보관되거나 repository에 커밋되지 않으며, 실행 중에도 노출되지 않는다.
|
|
|
|
### Trigger와 artifact access
|
|
|
|
1차 trigger는 Jenkins GitHub push webhook이다. job 설정 XML은 GitHub push trigger와 SCM branch spec `*/main`을 함께 사용하므로, `develop`에서 `main`으로 merge된 뒤 `main` push webhook이 들어올 때 인증 ZIP 빌드를 실행한다. cron polling trigger는 사용하지 않는다.
|
|
|
|
최신 성공 빌드 artifact URL은 Jenkins base URL 뒤에 아래 경로를 붙인다. Jenkins URL, username, API token 원문은 문서나 task log에 남기지 않는다.
|
|
|
|
```text
|
|
/job/appsok-macos-certified/lastSuccessfulBuild/artifact/build/macos/Build/Products/Release/AppSok-certified.zip
|
|
/job/appsok-macos-certified/lastSuccessfulBuild/artifact/build/macos/Build/Products/Release/AppSok-certified.zip.sha256
|
|
```
|
|
|
|
artifact 다운로드는 Jenkins 로그인 사용자 또는 사용자별 API token 인증으로만 허용한다. 공개/anonymous 다운로드 경로와 URL query token 방식은 이번 마일스톤 범위에서 사용하지 않는다.
|
|
|
|
## 인증 빌드
|
|
|
|
설정이 끝난 뒤 반복 빌드는 아래 한 줄로 실행한다.
|
|
|
|
```bash
|
|
./scripts/build-certified-macos.sh
|
|
```
|
|
|
|
스크립트는 아래 순서로 실행된다.
|
|
|
|
1. SOPS secret 복호화와 login Keychain unlock
|
|
2. `flutter pub get`
|
|
3. `flutter analyze`
|
|
4. `flutter test`
|
|
5. `flutter build macos`
|
|
6. bundled `Contents/Resources/adb-runtime/adb` Developer ID signing
|
|
7. `AppSok.app` Developer ID signing
|
|
8. `notarytool submit --wait`
|
|
9. `stapler staple`
|
|
10. `spctl --assess`
|
|
11. `AppSok-certified.zip` 생성과 `.sha256` checksum 파일 출력
|
|
|
|
최종 산출물은 `build/macos/Build/Products/Release/AppSok-certified.zip`이다.
|
|
Jenkins archived artifact 대상은 이 ZIP과 같은 경로의 `AppSok-certified.zip.sha256` 파일이다.
|
|
현재 pinned platform-tools `adb`는 `otool -L` 기준 비시스템 dylib 의존성이 없어서 certified bundle의 `Contents/Resources/adb-runtime/`에는 `adb`, `NOTICE.txt`, `source.properties`만 포함된다. 향후 Android platform-tools 구조가 바뀌어 별도 dylib가 필요해지면 Xcode "Copy ADB Runtime" phase와 signing/notarization 검증을 함께 갱신한다.
|
|
|
|
## 검증 기준
|
|
|
|
- `flutter analyze`가 `No issues found!`로 끝난다.
|
|
- `flutter test`가 모두 통과한다.
|
|
- `notarytool submit --wait` 결과가 `Accepted`다.
|
|
- `stapler`가 ticket을 붙인다.
|
|
- `spctl --assess --type execute --verbose=4`가 Notarized Developer ID source를 보고한다.
|
|
- 최종 ZIP의 `shasum -a 256` 값이 `AppSok-certified.zip.sha256`에 기록된다.
|
|
|
|
2026-06-17 검증에서는 `AppSok-certified.zip` 생성, notarization accepted, stapling, Gatekeeper assessment를 통과했고, `AppSok.app`과 bundled `adb`가 모두 `x86_64 arm64` universal binary임을 확인했다. 기록된 checksum은 `0b379602777efcf51b662581bcbaea9d2ab7c10737cab932b1aaf614c540bc6a`다.
|
|
|
|
## 주의 사항
|
|
|
|
Apple notarization은 앱 번들 안의 executable resource도 별도로 검사한다. `Contents/Resources/adb-runtime/adb`가 Developer ID, hardened runtime, timestamp로 먼저 서명되지 않으면 앱 본체 서명이 정상이어도 notarization이 `Invalid` 처리될 수 있다. `scripts/build-certified-macos.sh`는 이 순서를 고정한다.
|
|
|
|
## 문제 해결
|
|
|
|
- `error loading config: no matching creation rules found`: `./scripts/setup-appsok-ci-secrets.sh`를 다시 실행해 `.sops.yaml`과 encrypted secret 파일을 재생성한다.
|
|
- `HTTP status code: 401. Invalid credentials`: Apple ID, Team ID, app-specific password, notary profile 입력을 확인한다. Apple 계정 로그인 password가 아니라 app-specific password가 필요하다.
|
|
- `errSecInternalComponent` 또는 `User interaction is not allowed`: login Keychain unlock과 private key partition list 설정이 필요한 상태다. setup script를 interactive terminal에서 다시 실행한다.
|
|
- notarization log가 bundled `adb`를 지목한다: 인증 빌드 스크립트를 우회하지 말고 `./scripts/build-certified-macos.sh`로 다시 빌드한다.
|