# 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`로 다시 빌드한다.
