From 03df5934f822b27384d30d5412e0d576dfd489b3 Mon Sep 17 00:00:00 2001 From: toki Date: Wed, 17 Jun 2026 09:30:46 +0900 Subject: [PATCH] update: security-distribution phase and certified build scripts --- .../milestones/jenkins-thin-artifact-ci.md | 25 ++++++----- .../phase/security-distribution/PHASE.md | 4 +- docs/macos-certified-build.md | 13 ++++++ scripts/upsert-jenkins-certified-job.sh | 31 ++++++++++++-- test/certified_macos_scripts_test.dart | 41 ++++++++++++++++--- 5 files changed, 91 insertions(+), 23 deletions(-) rename agent-roadmap/{ => archive}/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md (74%) diff --git a/agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md b/agent-roadmap/archive/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md similarity index 74% rename from agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md rename to agent-roadmap/archive/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md index ad7fca9..e65d115 100644 --- a/agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md +++ b/agent-roadmap/archive/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md @@ -11,7 +11,7 @@ OTO 없이 Jenkins job만 얇게 구성해 AppSok 인증 ZIP을 반복 빌드하 ## 상태 -[진행중] +[완료] ## 승격 조건 @@ -20,6 +20,7 @@ OTO 없이 Jenkins job만 얇게 구성해 AppSok 인증 ZIP을 반복 빌드하 ## 구현 잠금 - 상태: 해제 +- SDD: 불필요 - Jenkins thin job 설정, 문서화, 1회 smoke evidence로 닫히는 운영 작업이며 앱 API/proto/schema 계약을 바꾸지 않는다. - 결정 필요: 아래 체크리스트 - [x] Jenkins job 생성/갱신은 사용자 제공 Jenkins API credential을 사용한다. 원격 SOPS 저장과 Jenkins API 인증 확인을 완료했다. - [x] artifact 다운로드는 Jenkins 로그인 사용자에게만 허용한다. 별도 공개 다운로드 경로는 이번 마일스톤 범위에서 만들지 않는다. @@ -50,20 +51,20 @@ Jenkins job은 AppSok 빌드 스크립트를 호출하고 artifact를 보관하 사용자가 Jenkins에서 빌드를 실행하고 최신 성공 artifact를 확인할 수 있게 한다. -- [ ] [trigger] 1차 trigger 방식을 적용한다. -- [ ] [download-url] `lastSuccessfulBuild/artifact/.../AppSok-certified.zip` 다운로드 URL을 확인하고 문서화한다. -- [ ] [auth-policy] Jenkins artifact 다운로드 권한 정책을 결정 사항에 맞게 반영한다. -- [ ] [smoke] Jenkins build를 1회 실행하고 archived ZIP과 checksum을 다운로드 가능한 상태로 검증한다. 검증: Jenkins 최신 성공 빌드의 artifact URL이 인증된 사용자 기준으로 ZIP 응답을 반환한다. +- [x] [trigger] 1차 trigger 방식을 적용한다. +- [x] [download-url] `lastSuccessfulBuild/artifact/.../AppSok-certified.zip` 다운로드 URL을 확인하고 문서화한다. +- [x] [auth-policy] Jenkins artifact 다운로드 권한 정책을 결정 사항에 맞게 반영한다. +- [x] [smoke] Jenkins build를 1회 실행하고 archived ZIP과 checksum을 다운로드 가능한 상태로 검증한다. 검증: Jenkins 최신 성공 빌드의 artifact URL이 인증된 사용자 기준으로 ZIP 응답을 반환한다. ## 완료 리뷰 -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증 충족 후 기록한다. +- 상태: 승인됨 +- 요청일: 2026-06-17 +- 완료 근거: Jenkins job 설정 apply가 성공했고, Jenkins build #2가 SUCCESS로 종료되었다. 최신 성공 빌드의 `AppSok-certified.zip`과 `.sha256` artifact가 인증 사용자 기준 HTTP 200으로 다운로드되었고 ZIP size는 24,326,080 bytes, sha256은 `557339ba024afaedc614a5a5d88716a5d7daa4882c88903155e3318cc6363104`로 확인되었다. - 리뷰 필요: - - [ ] 사용자가 완료 결과를 확인했다 - - [ ] archive 이동을 승인했다 -- 리뷰 코멘트: 없음 + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 사용자가 빌드 자체 정상 확인을 완료했고, 기능적인 부분은 추후 확인하기로 했다. ## 범위 제외 @@ -78,6 +79,8 @@ Jenkins job은 AppSok 빌드 스크립트를 호출하고 artifact를 보관하 - 관련 경로: `scripts/build-certified-macos.sh`, `docs/macos-certified-build.md`, `agent-roadmap/phase/security-distribution/` - 표준선(선택): Jenkins는 1차로 thin launcher와 artifact archive만 담당하고, 빌드 절차의 source of truth는 AppSok repository의 스크립트와 문서에 둔다. - 표준선(선택): Jenkins API token, SSH credential, Apple credential, Keychain password 원문은 tracked 파일, task log, 최종 응답에 기록하지 않는다. +- 진행 현황: `trigger`, `download-url`, `auth-policy`는 Jenkins job XML dry-run, XML parse, 문서화, 스크립트 테스트, `flutter analyze`, `flutter test` evidence로 반영했다. +- 진행 현황: `smoke`는 Jenkins job apply, build #2 SUCCESS, 인증 사용자 기준 ZIP/checksum HTTP 200 응답으로 반영했다. - 선행 작업: macOS 빌드 CI와 인증 빌드 - 후속 작업: macOS 배포와 sandbox 검증, 향후 OTO 기반 CI/CD 마이그레이션 후보 - 확인 필요: 없음 diff --git a/agent-roadmap/phase/security-distribution/PHASE.md b/agent-roadmap/phase/security-distribution/PHASE.md index e919943..7ba6aa4 100644 --- a/agent-roadmap/phase/security-distribution/PHASE.md +++ b/agent-roadmap/phase/security-distribution/PHASE.md @@ -14,8 +14,8 @@ AppSok을 개인용 도구에서 사내 표준 도구로 확장하기 위한 배 완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. 스케치 Milestone은 아직 구현 가능한 계획이 아니므로 계획 Milestone보다 아래에 둔다. -- [진행중] Jenkins 얇은 빌드와 artifact 다운로드 - - 경로: `agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md` +- [완료] Jenkins 얇은 빌드와 artifact 다운로드 + - 경로: `agent-roadmap/archive/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md` - 요약: OTO 없이 Jenkins job만 얇게 구성해 AppSok 인증 ZIP을 빌드하고 Jenkins artifact 링크로 바로 다운로드할 수 있게 한다. - [계획] macOS 배포와 sandbox 검증 - 경로: `agent-roadmap/phase/security-distribution/milestones/macos-distribution.md` diff --git a/docs/macos-certified-build.md b/docs/macos-certified-build.md index a2aa866..a2b6b77 100644 --- a/docs/macos-certified-build.md +++ b/docs/macos-certified-build.md @@ -39,6 +39,19 @@ raw password, app-specific password, Jenkins API token, private endpoint, decryp 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 방식은 이번 마일스톤 범위에서 사용하지 않는다. + ## 인증 빌드 설정이 끝난 뒤 반복 빌드는 아래 한 줄로 실행한다. diff --git a/scripts/upsert-jenkins-certified-job.sh b/scripts/upsert-jenkins-certified-job.sh index 2c4d974..22a3a94 100755 --- a/scripts/upsert-jenkins-certified-job.sh +++ b/scripts/upsert-jenkins-certified-job.sh @@ -62,6 +62,11 @@ XML_CONTENT=$(cat <-1 10 + + + + + set -euo pipefail @@ -115,9 +120,27 @@ fi # Remove trailing slash if present in JENKINS_URL JENKINS_URL="${JENKINS_URL%/}" +CURL_CONFIG="$(mktemp)" +chmod 600 "$CURL_CONFIG" +trap 'rm -f "$CURL_CONFIG"' EXIT +curl_config_escape() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + printf '%s' "$value" +} + +{ + printf 'user = "' + curl_config_escape "$JENKINS_USER" + printf ':' + curl_config_escape "$JENKINS_TOKEN" + printf '"\n' +} >"$CURL_CONFIG" + # 1. Fetch Crumb CRUMB_HEADER="" -CRUMB_RESPONSE=$(curl -s -u "$JENKINS_USER:$JENKINS_TOKEN" "${JENKINS_URL}/crumbIssuer/api/json" || true) +CRUMB_RESPONSE=$(curl -s --config "$CURL_CONFIG" "${JENKINS_URL}/crumbIssuer/api/json" || true) if [ -n "$CRUMB_RESPONSE" ] && echo "$CRUMB_RESPONSE" | grep -q "crumb"; then CRUMB_FIELD=$(echo "$CRUMB_RESPONSE" | python3 -c "import json, sys; print(json.load(sys.stdin)['crumbRequestField'])") @@ -127,7 +150,7 @@ fi # 2. Check if job exists CHECK_URL="${JENKINS_URL}/job/appsok-macos-certified/config.xml" -HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -u "$JENKINS_USER:$JENKINS_TOKEN" "$CHECK_URL" || echo "500") +HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --config "$CURL_CONFIG" "$CHECK_URL" || echo "500") CURL_OPTS=(-s -w "%{http_code}" -o /dev/null) if [ -n "$CRUMB_HEADER" ]; then @@ -137,7 +160,7 @@ fi if [ "$HTTP_STATUS" = "200" ]; then echo "Updating existing job appsok-macos-certified..." UPDATE_URL="${JENKINS_URL}/job/appsok-macos-certified/config.xml" - STATUS=$(curl "${CURL_OPTS[@]}" -u "$JENKINS_USER:$JENKINS_TOKEN" -H "Content-Type: application/xml" --data-binary "$XML_CONTENT" "$UPDATE_URL" || echo "500") + STATUS=$(curl "${CURL_OPTS[@]}" --config "$CURL_CONFIG" -H "Content-Type: application/xml" --data-binary "$XML_CONTENT" "$UPDATE_URL" || echo "500") if [ "$STATUS" = "200" ] || [ "$STATUS" = "201" ] || [ "$STATUS" = "302" ]; then echo "Successfully updated job configuration." else @@ -147,7 +170,7 @@ if [ "$HTTP_STATUS" = "200" ]; then elif [ "$HTTP_STATUS" = "404" ]; then echo "Creating new job appsok-macos-certified..." CREATE_URL="${JENKINS_URL}/createItem?name=appsok-macos-certified" - STATUS=$(curl "${CURL_OPTS[@]}" -u "$JENKINS_USER:$JENKINS_TOKEN" -H "Content-Type: application/xml" --data-binary "$XML_CONTENT" "$CREATE_URL" || echo "500") + STATUS=$(curl "${CURL_OPTS[@]}" --config "$CURL_CONFIG" -H "Content-Type: application/xml" --data-binary "$XML_CONTENT" "$CREATE_URL" || echo "500") if [ "$STATUS" = "200" ] || [ "$STATUS" = "201" ] || [ "$STATUS" = "302" ]; then echo "Successfully created job." else diff --git a/test/certified_macos_scripts_test.dart b/test/certified_macos_scripts_test.dart index 11cf61b..3022f20 100644 --- a/test/certified_macos_scripts_test.dart +++ b/test/certified_macos_scripts_test.dart @@ -28,11 +28,23 @@ void main() { expect(script, contains('xcrun notarytool history --keychain-profile')); expect(script, contains('prompt_plain "Jenkins URL" JENKINS_URL')); - expect(script, contains('prompt_plain "Jenkins username" JENKINS_USERNAME')); - expect(script, contains('prompt_plain "Jenkins API token" JENKINS_API_TOKEN')); + expect( + script, + contains('prompt_plain "Jenkins username" JENKINS_USERNAME'), + ); + expect( + script, + contains('prompt_plain "Jenkins API token" JENKINS_API_TOKEN'), + ); expect(script, contains('"jenkins_url": os.environ["JENKINS_URL"]')); - expect(script, contains('"jenkins_username": os.environ["JENKINS_USERNAME"]')); - expect(script, contains('"jenkins_api_token": os.environ["JENKINS_API_TOKEN"]')); + expect( + script, + contains('"jenkins_username": os.environ["JENKINS_USERNAME"]'), + ); + expect( + script, + contains('"jenkins_api_token": os.environ["JENKINS_API_TOKEN"]'), + ); }); test('certified build keeps Flutter gates before release packaging', () { @@ -123,6 +135,9 @@ void main() { expect(script, contains('appsok-macos-certified')); expect(script, contains('macbook-ultra')); expect(script, contains('*/main')); + expect(script, contains('')); + expect(script, contains('com.cloudbees.jenkins.GitHubPushTrigger')); + expect(script, isNot(contains('hudson.triggers.SCMTrigger'))); expect(script, contains('./scripts/build-certified-macos.sh')); expect( script, @@ -130,7 +145,9 @@ void main() { ); expect( script, - contains('build/macos/Build/Products/Release/AppSok-certified.zip.sha256'), + contains( + 'build/macos/Build/Products/Release/AppSok-certified.zip.sha256', + ), ); expect(script, contains('10')); expect(script, contains('10')); @@ -142,12 +159,24 @@ void main() { expect(script, contains('sops -d')); expect(script, isNot(contains('jenkins_api_token='))); + expect(script, isNot(contains(r'-u "$JENKINS_USER:$JENKINS_TOKEN"'))); + expect( + script, + isNot(contains(r'python3 - "$JENKINS_USER" "$JENKINS_TOKEN"')), + ); + expect(script, contains(r'CURL_CONFIG="$(mktemp)"')); + expect(script, contains(r'--config "$CURL_CONFIG"')); + expect(script, contains("trap 'rm -f \"\$CURL_CONFIG\"' EXIT")); expect(script, contains('APPLY=false')); expect(script, contains('DRY_RUN=true')); expect(script, contains('--apply')); expect(script, contains('--dry-run')); - for (final key in ['jenkins_url', 'jenkins_username', 'jenkins_api_token']) { + for (final key in [ + 'jenkins_url', + 'jenkins_username', + 'jenkins_api_token', + ]) { expect(script, contains('read_secret')); expect(script, contains(key)); expect(setupScript, contains('"$key": os.environ'));