From e6feb735aa7a792033f0038fb7f4e9c29edfc0d7 Mon Sep 17 00:00:00 2001 From: toki Date: Tue, 16 Jun 2026 22:36:29 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20jenkins=20thin=20artifact=20CI=20?= =?UTF-8?q?=EB=A7=88=EC=9D=BC=EC=8A=A4=ED=86=A4=20=EB=B0=8F=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jenkins-thin-artifact-ci 마일스톤 계획 수립 - macos certified build 스크립트 및 테스트 업데이트 - docs/macos-certified-build.md 문서 갱신 - ci secrets setup 스크립트 추가 --- .../milestones/jenkins-thin-artifact-ci.md | 14 +- .../CODE_REVIEW-cloud-G07.md | 161 +++++++++++ .../PLAN-cloud-G07.md | 249 ++++++++++++++++++ docs/macos-certified-build.md | 5 +- scripts/build-certified-macos.sh | 6 +- test/certified_macos_scripts_test.dart | 12 +- 6 files changed, 435 insertions(+), 12 deletions(-) create mode 100644 agent-task/m-jenkins-thin-artifact-ci/CODE_REVIEW-cloud-G07.md create mode 100644 agent-task/m-jenkins-thin-artifact-ci/PLAN-cloud-G07.md diff --git a/agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md b/agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md index af7390e..4d374ab 100644 --- a/agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md +++ b/agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md @@ -19,12 +19,12 @@ OTO 없이 Jenkins job만 얇게 구성해 AppSok 인증 ZIP을 반복 빌드하 ## 구현 잠금 -- 상태: 잠금 +- 상태: 해제 - 결정 필요: 아래 체크리스트 - [x] Jenkins job 생성/갱신은 사용자 제공 Jenkins API credential을 사용한다. 원격 SOPS 저장과 Jenkins API 인증 확인을 완료했다. - - [ ] artifact 다운로드를 Jenkins 로그인 사용자에게만 허용할지, 별도 공개 다운로드 경로를 마련할지 결정한다. - - [ ] build trigger를 수동 실행만으로 시작할지, Git push webhook 또는 polling까지 포함할지 결정한다. - - [ ] job 이름과 artifact 보관 개수 기본값을 확정한다. 기본 후보는 `appsok-macos-certified`, 최근 20개 build/artifact 보관이다. + - [x] artifact 다운로드는 Jenkins 로그인 사용자에게만 허용한다. 별도 공개 다운로드 경로는 이번 마일스톤 범위에서 만들지 않는다. + - [x] build trigger는 `develop` branch에서 평소 개발 빌드를 수행하고, `develop`에서 `main`으로 merge될 때 Jenkins 인증 ZIP 빌드를 자동 실행한다. 자동 실행은 `main` branch filter가 걸린 Git push webhook 기준으로 두고 polling은 제외한다. + - [x] Jenkins job 이름은 `appsok-macos-certified`로 하고, build log와 archived artifact는 최근 10개 build 기준으로 보관한다. ## 범위 @@ -42,9 +42,9 @@ Jenkins job은 AppSok 빌드 스크립트를 호출하고 artifact를 보관하 - [ ] [job-config] `macbook-ultra` node, AppSok SCM, `main` branch, shell build step을 포함한 Jenkins job config를 만든다. - [ ] [script-call] job build step이 `./scripts/build-certified-macos.sh`를 호출하고 secret 원문을 job config나 repository에 남기지 않는다. -- [ ] [artifact-copy] `AppSok-certified.zip`과 `sha256` 값을 Jenkins workspace의 archive 대상 경로로 모은다. +- [x] [artifact-copy] `AppSok-certified.zip`과 `sha256` 값을 Jenkins workspace의 archive 대상 경로로 모은다. - [ ] [archive] Jenkins archived artifact 설정으로 ZIP과 checksum을 보관한다. -- [ ] [retention] build log와 artifact 보관 정책을 AppSok 1차 운영 기준에 맞춘다. +- [ ] [retention] build log와 artifact 보관 정책을 최근 10개 build 기준에 맞춘다. ### Epic: [access] 빌드 실행과 다운로드 확인 @@ -80,4 +80,4 @@ Jenkins job은 AppSok 빌드 스크립트를 호출하고 artifact를 보관하 - 표준선(선택): Jenkins API token, SSH credential, Apple credential, Keychain password 원문은 tracked 파일, task log, 최종 응답에 기록하지 않는다. - 선행 작업: macOS 빌드 CI와 인증 빌드 - 후속 작업: macOS 배포와 sandbox 검증, 향후 OTO 기반 CI/CD 마이그레이션 후보 -- 확인 필요: Jenkins API token 또는 reload 권한, artifact 다운로드 권한 정책, trigger 방식, job 이름과 보관 정책 +- 확인 필요: 없음 diff --git a/agent-task/m-jenkins-thin-artifact-ci/CODE_REVIEW-cloud-G07.md b/agent-task/m-jenkins-thin-artifact-ci/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..8085489 --- /dev/null +++ b/agent-task/m-jenkins-thin-artifact-ci/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,161 @@ + + +# Code Review Reference - JENKINS + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-16 +task=m-jenkins-thin-artifact-ci, plan=0, tag=JENKINS + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md` +- Task ids: + - `job-config`: `macbook-ultra` node, AppSok SCM, `main` branch, shell build step을 포함한 Jenkins job config를 만든다. + - `script-call`: job build step이 `./scripts/build-certified-macos.sh`를 호출하고 secret 원문을 job config나 repository에 남기지 않는다. + - `archive`: Jenkins archived artifact 설정으로 ZIP과 checksum을 보관한다. + - `retention`: build log와 artifact 보관 정책을 최근 10개 build 기준에 맞춘다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-jenkins-thin-artifact-ci/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [JENKINS-1] Jenkins job upsert script를 추가하고 dry-run XML로 검증 | [ ] | +| [JENKINS-2] 문서/테스트 갱신 및 remote Jenkins API apply evidence | [ ] | + +## 구현 체크리스트 + +- [ ] [JENKINS-1] Jenkins job upsert script를 추가하고 `macbook-ultra`, `main`, `./scripts/build-certified-macos.sh`, ZIP/sha256 archive, 최근 10개 보관 정책을 dry-run XML로 검증한다. +- [ ] [JENKINS-2] 문서와 테스트를 갱신하고 remote runner에서 Jenkins API apply 및 config 조회 evidence를 남긴다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-jenkins-thin-artifact-ci/`를 `agent-task/archive/YYYY/MM/m-jenkins-thin-artifact-ci/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `scripts/upsert-jenkins-certified-job.sh`가 secret 원문을 출력하거나 XML에 넣지 않는지 확인한다. +- Jenkins job XML이 `macbook-ultra`, `*/main`, `./scripts/build-certified-macos.sh`, ZIP/sha256 archive, retention 10을 모두 포함하는지 확인한다. +- Jenkins apply evidence가 endpoint/token 원문 없이 job 존재와 config 반영을 증명하는지 확인한다. +- Roadmap Targets의 네 Task만 PASS 완료 후보로 삼고, `access` Epic Task를 임의로 체크하지 않는다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### JENKINS-1 중간 검증 +```text +$ git diff --check -- scripts/upsert-jenkins-certified-job.sh test/certified_macos_scripts_test.dart docs/macos-certified-build.md +(output) + +$ flutter test test/certified_macos_scripts_test.dart +(output) +``` + +### JENKINS-2 중간 검증 +```text +$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && export PATH="$HOME/SDK/flutter/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" && ./scripts/upsert-jenkins-certified-job.sh --dry-run >/tmp/appsok-macos-certified-config.xml && for marker in "appsok-macos-certified" "macbook-ultra" "*/main" "AppSok-certified.zip.sha256" "10" "10"; do grep -F "$marker" /tmp/appsok-macos-certified-config.xml >/dev/null && printf "%s=present\n" "$marker"; done'\''' +(output) +``` + +### 최종 검증 +```text +$ git diff --check -- scripts/upsert-jenkins-certified-job.sh test/certified_macos_scripts_test.dart docs/macos-certified-build.md +(output) + +$ flutter test test/certified_macos_scripts_test.dart +(output) + +$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && export PATH="$HOME/SDK/flutter/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" && ./scripts/upsert-jenkins-certified-job.sh --dry-run >/tmp/appsok-macos-certified-config.xml && for marker in "appsok-macos-certified" "macbook-ultra" "*/main" "AppSok-certified.zip.sha256" "10" "10"; do grep -F "$marker" /tmp/appsok-macos-certified-config.xml >/dev/null && printf "%s=present\n" "$marker"; done'\''' +(output) + +$ ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && export PATH="$HOME/SDK/flutter/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" && ./scripts/upsert-jenkins-certified-job.sh --apply'\''' +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Sections and Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these | +| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only | +| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content | +| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus list | +| 검증 결과 | Implementing agent | Fill command output only | diff --git a/agent-task/m-jenkins-thin-artifact-ci/PLAN-cloud-G07.md b/agent-task/m-jenkins-thin-artifact-ci/PLAN-cloud-G07.md new file mode 100644 index 0000000..8f932f0 --- /dev/null +++ b/agent-task/m-jenkins-thin-artifact-ci/PLAN-cloud-G07.md @@ -0,0 +1,249 @@ + + +# Jenkins Thin Job Plan + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채우는 것까지가 필수 작업이다. 구현, 검증, 실제 출력 기록을 끝낸 뒤 active 파일을 유지하고 리뷰 준비 상태로 보고한다. 최종 판정, log rename, `complete.log` 작성, archive 이동은 code-review 스킬 전용이다. + +구현 중 사용자만 결정할 수 있는 외부 환경 전제, secret 준비, scope 충돌이 발견되면 사용자에게 직접 묻지 말고 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 정확한 결정, 근거, 실행한 명령과 출력, 재개 조건을 기록하고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 evidence 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 계획 대비 변경 사항에 기록한다. + +## 배경 + +현재 마일스톤은 AppSok 인증 ZIP을 Jenkins thin job으로 빌드하고 archived artifact로 배포하는 것이다. `artifact-copy`는 즉시 처리되어 ZIP 옆 `.sha256` 파일을 남기지만, Jenkins job 생성/갱신은 외부 서비스 상태를 바꾸므로 계획 루프로 처리한다. 이 계획은 `appsok-macos-certified` job을 생성 또는 갱신하고, `macbook-ultra` node, `main` SCM, 빌드 스크립트 호출, archive, 최근 10개 보관 정책을 한 번에 적용한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 채팅으로 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. code-review가 요청의 타당성을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md` +- Task ids: + - `job-config`: `macbook-ultra` node, AppSok SCM, `main` branch, shell build step을 포함한 Jenkins job config를 만든다. + - `script-call`: job build step이 `./scripts/build-certified-macos.sh`를 호출하고 secret 원문을 job config나 repository에 남기지 않는다. + - `archive`: Jenkins archived artifact 설정으로 ZIP과 checksum을 보관한다. + - `retention`: build log와 artifact 보관 정책을 최근 10개 build 기준에 맞춘다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `AGENTS.md` +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/update-roadmap/SKILL.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` +- `agent-roadmap/current.md` +- `agent-roadmap/ROADMAP.md` +- `agent-roadmap/phase/security-distribution/PHASE.md` +- `agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md` +- `agent-ops/rules/project/domain/artifact-flow/rules.md` +- `agent-ops/rules/project/domain/macos-platform/rules.md` +- `agent-test/local/rules.md` +- `agent-test/local/artifact-flow-smoke.md` +- `agent-test/local/macos-platform-smoke.md` +- `scripts/build-certified-macos.sh` +- `scripts/setup-appsok-ci-secrets.sh` +- `docs/macos-certified-build.md` +- `test/certified_macos_scripts_test.dart` +- `.gitignore` +- `pubspec.yaml` + +### 테스트 환경 규칙 + +- 선택 env: `local`. +- `agent-test/local/rules.md`를 읽었다. 기본 완료 evidence는 remote Mac runner `toki@toki-labs.com`의 `$HOME/docker/services/code-server/data/volume/workspace/appsok` checkout 기준이다. +- 매칭 profile: `agent-test/local/artifact-flow-smoke.md`, `agent-test/local/macos-platform-smoke.md`. +- 적용 명령: `flutter analyze`, `flutter test`, macOS platform 변경 시 remote runner의 `flutter build macos`. +- Jenkins 실제 조회/다운로드와 job API 검증은 secret 원문 없이 SOPS payload와 Jenkins API 상태 코드로 확인한다. +- 현재 checkout에서 이미 실행한 작은 작업 검증: `flutter test test/certified_macos_scripts_test.dart` 통과, `git diff --check` 통과. + +### 테스트 커버리지 공백 + +- 새 Jenkins upsert script의 XML 구조, secret 비노출, endpoint 호출 순서는 `test/certified_macos_scripts_test.dart`에 문자열/순서 테스트를 추가한다. +- 실제 Jenkins job 생성/갱신은 unit test로 대체할 수 없다. `--dry-run` XML 생성 검증과 remote runner의 Jenkins API `whoAmI`, crumb, job config 조회로 보완한다. +- 실제 인증 ZIP 빌드는 시간이 길고 notarization 외부 상태에 의존한다. 이 계획의 최종 검증은 job config 적용과 Jenkins config 조회까지로 두고, 실제 build 실행은 `access` Epic의 `smoke` Task에서 다룬다. + +### 심볼 참조 + +- renamed/removed symbol: none. +- 새 파일 후보: `scripts/upsert-jenkins-certified-job.sh`. + +### 분할 판단 + +- split policy를 먼저 평가했다. +- 단일 계획을 선택한다. `job-config`, `script-call`, `archive`, `retention`은 모두 하나의 Jenkins freestyle job XML에 들어가며, 일부만 적용하면 job이 의도와 다르게 동작한다. +- ownership boundary는 `scripts/**`, `docs/**`, Jenkins API apply 한 곳으로 묶인다. +- 검증도 동일한 dry-run XML, unit test, Jenkins config 조회로 판단 가능하다. +- `artifact-copy`는 이미 작은 작업으로 즉시 처리되어 이 계획의 Roadmap Targets에서 제외했다. + +### 범위 결정 근거 + +- `access` Epic의 `trigger`, `download-url`, `auth-policy`, `smoke`는 이 계획에 포함하지 않는다. 사용자가 요청한 범위가 `job` Epic이기 때문이다. +- Jenkins plugin, Auth Broker, OTO runner, 중앙 감사 서버는 마일스톤 범위 제외다. +- Jenkins API token, Apple credential, keychain password, private endpoint 원문은 tracked 파일, task log, 최종 응답에 기록하지 않는다. +- SCM credential id는 새로 만들지 않는다. job config는 repository `remote.origin.url`을 사용하고 credential이 필요해 실패하면 구현 에이전트가 사용자 리뷰 요청으로 멈춘다. + +### 빌드 등급 + +- `cloud-G07`: 외부 Jenkins 상태 변경, shell/API orchestration, secret redaction, terminal verification이 핵심이다. + +## 구현 체크리스트 + +- [ ] [JENKINS-1] Jenkins job upsert script를 추가하고 `macbook-ultra`, `main`, `./scripts/build-certified-macos.sh`, ZIP/sha256 archive, 최근 10개 보관 정책을 dry-run XML로 검증한다. +- [ ] [JENKINS-2] 문서와 테스트를 갱신하고 remote runner에서 Jenkins API apply 및 config 조회 evidence를 남긴다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [JENKINS-1] Jenkins Job Upsert Script + +#### 문제 + +현재 `scripts/build-certified-macos.sh:42`는 SOPS secret을 읽고 인증 ZIP을 만들지만, Jenkins job을 생성하거나 갱신하는 entrypoint가 없다. 마일스톤의 `job-config`, `script-call`, `archive`, `retention` Task를 닫으려면 job XML을 repeatable하게 생성하고 Jenkins API로 upsert할 수 있어야 한다. + +Before: + +```bash +# scripts/build-certified-macos.sh:42 +read_secret() { + local key="$1" + sops -d "$SECRET_FILE" | python3 -c "import json, sys; print(json.load(sys.stdin)['$key'])" +} +``` + +#### 해결 방법 + +새 파일 `scripts/upsert-jenkins-certified-job.sh`를 만든다. 기존 secret 패턴을 재사용하되 `jenkins_url`, `jenkins_username`, `jenkins_api_token`만 읽고 원문을 출력하지 않는다. 기본은 `--dry-run`으로 XML만 stdout에 쓰고, `--apply`를 명시해야 Jenkins `createItem` 또는 `config.xml` 업데이트를 수행한다. + +핵심 XML은 Jenkins freestyle `project`로 만든다. + +```xml + + + AppSok certified macOS artifact thin launcher. + false + + + + + */main + + + + + ${SCM_URL} + + + + macbook-ultra + false + + -1 + 10 + -1 + 10 + + + + set -euo pipefail +./scripts/build-certified-macos.sh + + + + + build/macos/Build/Products/Release/AppSok-certified.zip,build/macos/Build/Products/Release/AppSok-certified.zip.sha256 + false + true + + + + +``` + +Jenkins 호출은 crumb issuer를 먼저 확인한다. 대상 job `appsok-macos-certified`가 없으면 `POST /createItem?name=appsok-macos-certified`, 있으면 `POST /job/appsok-macos-certified/config.xml`을 사용한다. HTTP 200/201/302를 성공으로 보고, 실패 시 status만 출력한다. + +#### 수정 파일 및 체크리스트 + +- [ ] `scripts/upsert-jenkins-certified-job.sh` 추가. +- [ ] `chmod +x scripts/upsert-jenkins-certified-job.sh`. +- [ ] `test/certified_macos_scripts_test.dart`에 job XML/secret/API 호출 구조 테스트 추가. +- [ ] `docs/macos-certified-build.md`에 Jenkins job upsert 사용법, dry-run/apply, archive 대상, secret 비노출 원칙 추가. + +#### 테스트 작성 + +- `test/certified_macos_scripts_test.dart` + - test name: `jenkins job upsert configures thin certified job` + - assertion: script contains `appsok-macos-certified`, `macbook-ultra`, `*/main`, `./scripts/build-certified-macos.sh`, archive artifact paths, `numToKeep>10`, `artifactNumToKeep>10`. + - test name: `jenkins job upsert keeps credentials out of generated config` + - assertion: XML generation references SOPS only for API auth, no API token value is interpolated into config XML, script has `--dry-run` default and explicit `--apply`. + +#### 중간 검증 + +```bash +git diff --check -- scripts/upsert-jenkins-certified-job.sh test/certified_macos_scripts_test.dart docs/macos-certified-build.md +flutter test test/certified_macos_scripts_test.dart +``` + +Expected: no whitespace errors and all tests pass. + +### [JENKINS-2] Jenkins Apply And Evidence + +#### 문제 + +Jenkins 접근은 비파괴 preflight에서 확인됐다: `whoAmI=200 authenticated=true`, `crumb=200`, root API `200`, `appsok-macos-certified` job `404`, `newJob` page `200`, `macbook-ultra` node `200 offline=false`, git plugin present. 그러나 실제 job config는 아직 Jenkins에 없다. + +Before: + +```text +agent-roadmap/phase/security-distribution/milestones/jenkins-thin-artifact-ci.md:43 +- [ ] [job-config] `macbook-ultra` node, AppSok SCM, `main` branch, shell build step을 포함한 Jenkins job config를 만든다. +``` + +#### 해결 방법 + +remote Mac runner에서 `--dry-run`으로 XML을 생성해 non-secret structure를 확인한 뒤 `--apply`를 실행한다. 적용 후 Jenkins API로 `job/appsok-macos-certified/config.xml`과 `api/json`을 조회해 node, branch, archive path, retention 값이 반영되었는지 확인한다. endpoint, username, token 원문은 기록하지 않는다. + +#### 수정 파일 및 체크리스트 + +- [ ] remote runner에서 `./scripts/upsert-jenkins-certified-job.sh --dry-run` 실행. +- [ ] dry-run XML에서 secret/token-like value가 없는지 확인. +- [ ] remote runner에서 `./scripts/upsert-jenkins-certified-job.sh --apply` 실행. +- [ ] Jenkins job API 조회로 `appsok-macos-certified`가 존재하는지 확인. +- [ ] Jenkins config XML 조회로 `macbook-ultra`, `*/main`, `./scripts/build-certified-macos.sh`, artifact archive, retention 10 확인. + +#### 테스트 작성 + +- 실제 Jenkins apply는 unit test가 아니라 smoke evidence다. unit test는 JENKINS-1에서 script 구조를 검증한다. + +#### 중간 검증 + +```bash +ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && export PATH="$HOME/SDK/flutter/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" && ./scripts/upsert-jenkins-certified-job.sh --dry-run >/tmp/appsok-macos-certified-config.xml && for marker in "appsok-macos-certified" "macbook-ultra" "*/main" "AppSok-certified.zip.sha256" "10" "10"; do grep -F "$marker" /tmp/appsok-macos-certified-config.xml >/dev/null && printf "%s=present\n" "$marker"; done'\''' +``` + +Expected: each required XML marker is present and no secret is printed. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `scripts/upsert-jenkins-certified-job.sh` | JENKINS-1, JENKINS-2 | +| `test/certified_macos_scripts_test.dart` | JENKINS-1 | +| `docs/macos-certified-build.md` | JENKINS-1 | +| Jenkins job `appsok-macos-certified` | JENKINS-2 | + +## 최종 검증 + +```bash +git diff --check -- scripts/upsert-jenkins-certified-job.sh test/certified_macos_scripts_test.dart docs/macos-certified-build.md +flutter test test/certified_macos_scripts_test.dart +ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && export PATH="$HOME/SDK/flutter/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" && ./scripts/upsert-jenkins-certified-job.sh --dry-run >/tmp/appsok-macos-certified-config.xml && for marker in "appsok-macos-certified" "macbook-ultra" "*/main" "AppSok-certified.zip.sha256" "10" "10"; do grep -F "$marker" /tmp/appsok-macos-certified-config.xml >/dev/null && printf "%s=present\n" "$marker"; done'\''' +ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && export PATH="$HOME/SDK/flutter/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" && ./scripts/upsert-jenkins-certified-job.sh --apply'\''' +``` + +Expected: whitespace check passes, targeted Flutter test passes, dry-run XML markers are present, Jenkins apply succeeds without printing secret values. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/docs/macos-certified-build.md b/docs/macos-certified-build.md index 4ad95b6..0b4698a 100644 --- a/docs/macos-certified-build.md +++ b/docs/macos-certified-build.md @@ -41,9 +41,10 @@ raw password, app-specific password, decrypted SOPS payload, private key 원문 8. `notarytool submit --wait` 9. `stapler staple` 10. `spctl --assess` -11. `AppSok-certified.zip` 생성과 checksum 출력 +11. `AppSok-certified.zip` 생성과 `.sha256` checksum 파일 출력 최종 산출물은 `build/macos/Build/Products/Release/AppSok-certified.zip`이다. +Jenkins archived artifact 대상은 이 ZIP과 같은 경로의 `AppSok-certified.zip.sha256` 파일이다. ## 검증 기준 @@ -52,7 +53,7 @@ raw password, app-specific password, decrypted SOPS payload, private key 원문 - `notarytool submit --wait` 결과가 `Accepted`다. - `stapler`가 ticket을 붙인다. - `spctl --assess --type execute --verbose=4`가 Notarized Developer ID source를 보고한다. -- 최종 ZIP의 `shasum -a 256` 값을 기록한다. +- 최종 ZIP의 `shasum -a 256` 값이 `AppSok-certified.zip.sha256`에 기록된다. 2026-06-14 검증에서는 `AppSok-certified.zip` 생성, notarization accepted, stapling, Gatekeeper assessment를 통과했다. 기록된 checksum은 `1c8e89816d4cbf92bcf7c70aa40a4e36ae29efeaeb3cc64ce391c22bc6deef2a`다. diff --git a/scripts/build-certified-macos.sh b/scripts/build-certified-macos.sh index 53f078b..1ef396a 100755 --- a/scripts/build-certified-macos.sh +++ b/scripts/build-certified-macos.sh @@ -51,6 +51,7 @@ APP_PATH="build/macos/Build/Products/Release/AppSok.app" ADB_PATH="$APP_PATH/Contents/Resources/adb-runtime/adb" NOTARY_ZIP="build/macos/Build/Products/Release/AppSok-notary.zip" FINAL_ZIP="build/macos/Build/Products/Release/AppSok-certified.zip" +FINAL_SHA256="$FINAL_ZIP.sha256" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$HOME/Library/Keychains/login.keychain-db" security set-keychain-settings -lut 21600 "$HOME/Library/Keychains/login.keychain-db" @@ -78,7 +79,7 @@ codesign \ "$APP_PATH" codesign --verify --deep --strict --verbose=2 "$APP_PATH" -rm -f "$NOTARY_ZIP" "$FINAL_ZIP" +rm -f "$NOTARY_ZIP" "$FINAL_ZIP" "$FINAL_SHA256" ditto -c -k --keepParent "$APP_PATH" "$NOTARY_ZIP" xcrun notarytool submit "$NOTARY_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait @@ -86,5 +87,6 @@ xcrun stapler staple -v "$APP_PATH" spctl --assess --type execute --verbose=4 "$APP_PATH" ditto -c -k --keepParent "$APP_PATH" "$FINAL_ZIP" -shasum -a 256 "$FINAL_ZIP" +shasum -a 256 "$FINAL_ZIP" | tee "$FINAL_SHA256" ls -lh "$FINAL_ZIP" +ls -lh "$FINAL_SHA256" diff --git a/test/certified_macos_scripts_test.dart b/test/certified_macos_scripts_test.dart index d5e6cea..40038c8 100644 --- a/test/certified_macos_scripts_test.dart +++ b/test/certified_macos_scripts_test.dart @@ -81,7 +81,12 @@ void main() { 'FINAL_ZIP="build/macos/Build/Products/Release/AppSok-certified.zip"', ), ); - expect(script, contains(r'shasum -a 256 "$FINAL_ZIP"')); + expect(script, contains(r'FINAL_SHA256="$FINAL_ZIP.sha256"')); + expect( + script, + contains(r'shasum -a 256 "$FINAL_ZIP" | tee "$FINAL_SHA256"'), + ); + expect(script, contains(r'ls -lh "$FINAL_SHA256"')); _expectOrder( script, @@ -98,6 +103,11 @@ void main() { r'spctl --assess --type execute --verbose=4 "$APP_PATH"', r'ditto -c -k --keepParent "$APP_PATH" "$FINAL_ZIP"', ); + _expectOrder( + script, + r'ditto -c -k --keepParent "$APP_PATH" "$FINAL_ZIP"', + r'shasum -a 256 "$FINAL_ZIP" | tee "$FINAL_SHA256"', + ); }); }); }