appsok/docs/macos-certified-build.md

6.1 KiB

macOS 인증 빌드

AppSok의 최종 배포 후보는 Flutter 검증, macOS release build, Developer ID signing, Apple notarization, stapling, Gatekeeper assessment를 모두 통과한 ZIP으로 본다.

최초 설정

remote Mac runner의 AppSok checkout에서 한 번 실행한다. credential을 교체할 때도 같은 명령을 다시 실행한다.

./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을 생성하거나 갱신하려면 아래 스크립트를 실행한다.

# 기본 동작은 설정 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에 남기지 않는다.

/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 방식은 이번 마일스톤 범위에서 사용하지 않는다.

인증 빌드

설정이 끝난 뒤 반복 빌드는 아래 한 줄로 실행한다.

./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 adbotool -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 analyzeNo 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로 다시 빌드한다.