update: security-distribution phase and certified build scripts
This commit is contained in:
parent
1aded8d618
commit
03df5934f8
5 changed files with 91 additions and 23 deletions
|
|
@ -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 마이그레이션 후보
|
||||
- 확인 필요: 없음
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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 방식은 이번 마일스톤 범위에서 사용하지 않는다.
|
||||
|
||||
## 인증 빌드
|
||||
|
||||
설정이 끝난 뒤 반복 빌드는 아래 한 줄로 실행한다.
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ XML_CONTENT=$(cat <<EOF
|
|||
<artifactDaysToKeep>-1</artifactDaysToKeep>
|
||||
<artifactNumToKeep>10</artifactNumToKeep>
|
||||
</logRotator>
|
||||
<triggers>
|
||||
<com.cloudbees.jenkins.GitHubPushTrigger plugin="github">
|
||||
<spec></spec>
|
||||
</com.cloudbees.jenkins.GitHubPushTrigger>
|
||||
</triggers>
|
||||
<builders>
|
||||
<hudson.tasks.Shell>
|
||||
<command>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
|
||||
|
|
|
|||
|
|
@ -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('<triggers>'));
|
||||
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('<numToKeep>10</numToKeep>'));
|
||||
expect(script, contains('<artifactNumToKeep>10</artifactNumToKeep>'));
|
||||
|
|
@ -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'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue