chore(S15P11A705-154): add runtime secret handoff workflow - #100
Conversation
98587a2 to
b1f2f02
Compare
|
확인했습니다. 현재 PR workflow에는 아래 5개 런타임 Secret이 계약되어 있습니다.
리뷰 내용은 기존 OAuth
따라서 별도 제거 요청이 없다면 최종 runtime key 집합은 위 8개이고, Infra PR 생성용 bridge credential
|
|
minyongP
left a comment
There was a problem hiding this comment.
Claude Code 자동 리뷰입니다. 판정은 항상 comment이며, 승인·변경 요청은 @minyongP가 직접 남깁니다.
필수 변경은 없습니다. Infra 쪽 계약(84458bf 시점의 sealedsecret-infra-pr)과 하나씩 대조했고 어긋나는 곳이 없었습니다. back-prod policy가 요구하는 sourceWorkflow 경로와 이 파일 위치가 같고, env의 owner 키 5개가 policy의 ownerSecretKeys와 정확히 같고, bridge token 이름도 action 스크립트가 읽는 PINLOG_INFRA_SECRET_PR_TOKEN과 같습니다.
id-token: write와 checkout(ref: github.sha)는 과한 권한이 아니라 action이 요구하는 것이었습니다 — 각각 OIDC 클레임 대조(validate_oidc_claims)와, 체크아웃된 워크스페이스에서 이 workflow가 action SHA를 정확히 한 번 고정하는지 확인하는 verify_workflow_binding이 씁니다. 최소 권한 주장이 실제로 최소였습니다.
선택 제안 둘을 줄 단위로 달았습니다. 하나는 backend-ci.yml 한 파일만 지키는 비접근 계약을 workflow 전체로 넓히는 것, 다른 하나는 이 workflow가 dev에서만 실행된다는 사실을 어딘가에 남기는 것입니다(policy의 trustedRef가 refs/heads/dev라 다른 브랜치에서 dispatch하면 상대 레포 메시지로 실패합니다).
좋았던 점. Secret 참조 집합을 "정확히 이 6개"로 못 박아 둔 것이 이 workflow에서 가장 값어치 있는 부분이었습니다 — 키가 하나 늘 때 사람이 아니라 테스트가 먼저 말하게 됩니다. SnakeYAML이 on:을 Boolean.TRUE로 읽는 것을 주석으로 남겨 둔 것도 좋습니다.
확인 부탁. clean check가 Docker 부재로 34건 실패한 상태로 올라와 있으니 PR CI의 전체 gate 결과만 확인해 주세요.
Infra 계약 merge 완료Infra PR Team-PinLog/infra#82가 일반 merge commit으로 병합됐습니다.
Backend workflow의
기존 |
|
이 PR 이 처분은 그쪽 판단에 맡깁니다. |
프론트에 이메일을 표시하는 화면이 있어 값 없는 계정을 둘 수 없다. 공용 계약이 먼저 바뀌어야 했다. 06 §2.2가 "미동의·미제공 시 null일 수 있다"로 정하고 있어 구현만 바꾸면 계약 위반이다(CLAUDE.md 9번). docs#28을 먼저 병합했고, 그 PR에서 두 가지를 함께 정리했다. - 1차 보장은 공급자 콘솔의 필수 동의 설정이다. 사용자가 이메일만 거절하고 진행하는 선택지가 동의 화면에 없으므로, 이 구현이 막는 것은 일상 흐름이 아니라 방어선이다 - 마스킹은 치환이며 NULL이 아니다. 적지 않으면 탈퇴 구현이 NULL을 넣어 이 제약과 부딪힌다. provider_user_id가 이미 NOT NULL이면서 마스킹 대상이라 전제는 원래 있었다 RED — 정규화 4건 + 콜백 3건 실패(기존 계약을 반대로 고정하는 것이라 뒤집는 것 자체가 RED) GREEN - V6__social_account_email_not_null.sql - OAuthUserInfo 세 분기 모두 이메일을 required(...)로 끊음, @nullable 제거 - SocialAccount @column(nullable = false), 팩토리 파라미터 @nullable 제거 - FlywayMigrationTests에 스키마 제약 단언 추가 드러난 것 - 뒤집을 테스트가 예상보다 많았다. 사전 조사로 3건을 찾았는데 clean check에서 SocialAccountPersistenceTests가 걸렸다. emailIsOptional이 영속성 층에서 null 저장을 고정하고 있었고, 같은 파일의 조회 테스트도 준비 코드에 null 이메일이 섞여 함께 깨졌다 — useEmail(null)·isNull() 검색으로는 안 걸리는 형태다 - 두 방어선이 독립임을 뮤테이션이 보여 줬다. 정규화의 required(...)만 되돌리면 단위 테스트 4건은 실패하지만 콜백 테스트 3건은 통과한다 — DB NOT NULL이 대신 잡아 같은 OAUTH_FAILED로 귀결하기 때문이다. 콜백 테스트는 관측 가능한 계약을, 단위 테스트는 어느 층이 막는가를 고정한다. 반대 방향은 FlywayMigrationTests가 잡는다 - 백필을 넣지 않았다. 운영 DB에 NULL 행이 없고, 있었다면 채울 값이 없다 — 이메일은 공급자가 주는 값이다. 임의 값을 넣으면 "표시할 이메일"이라는 목적이 깨지므로 그런 환경에서는 마이그레이션이 실패하는 편이 맞다고 보고 SQL 주석에 남겼다 프론트 질문(실패 사유를 별도 error 값으로 가르는지)에는 기존 결정대로 OAUTH_FAILED로 묶인다고 답하고 08 §3.2에 명시했다. 기록은 BI-27이다. 세 PR이 BI-25를 동시에 선점했고 #98이 먼저 머지되며 그 번호를 가져갔다. 남은 #100이 BI-26, 이 기록이 BI-27이 되도록 미리 배정했다 — 재번호를 두 번 하지 않으려는 선택이고, #100 머지 전까지 색인에 BI-26 행이 없다. clean check 342개 통과, 실패 0 Refs #97 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
프론트에 이메일을 표시하는 화면이 있어 값 없는 계정을 둘 수 없다. 공용 계약이 먼저 바뀌어야 했다. 06 §2.2가 "미동의·미제공 시 null일 수 있다"로 정하고 있어 구현만 바꾸면 계약 위반이다(CLAUDE.md 9번). docs#28을 먼저 병합했고, 그 PR에서 두 가지를 함께 정리했다. - 1차 보장은 공급자 콘솔의 필수 동의 설정이다. 사용자가 이메일만 거절하고 진행하는 선택지가 동의 화면에 없으므로, 이 구현이 막는 것은 일상 흐름이 아니라 방어선이다 - 마스킹은 치환이며 NULL이 아니다. 적지 않으면 탈퇴 구현이 NULL을 넣어 이 제약과 부딪힌다. provider_user_id가 이미 NOT NULL이면서 마스킹 대상이라 전제는 원래 있었다 RED — 정규화 4건 + 콜백 3건 실패(기존 계약을 반대로 고정하는 것이라 뒤집는 것 자체가 RED) GREEN - V6__social_account_email_not_null.sql - OAuthUserInfo 세 분기 모두 이메일을 required(...)로 끊음, @nullable 제거 - SocialAccount @column(nullable = false), 팩토리 파라미터 @nullable 제거 - FlywayMigrationTests에 스키마 제약 단언 추가 드러난 것 - 뒤집을 테스트가 예상보다 많았다. 사전 조사로 3건을 찾았는데 clean check에서 SocialAccountPersistenceTests가 걸렸다. emailIsOptional이 영속성 층에서 null 저장을 고정하고 있었고, 같은 파일의 조회 테스트도 준비 코드에 null 이메일이 섞여 함께 깨졌다 — useEmail(null)·isNull() 검색으로는 안 걸리는 형태다 - 두 방어선이 독립임을 뮤테이션이 보여 줬다. 정규화의 required(...)만 되돌리면 단위 테스트 4건은 실패하지만 콜백 테스트 3건은 통과한다 — DB NOT NULL이 대신 잡아 같은 OAUTH_FAILED로 귀결하기 때문이다. 콜백 테스트는 관측 가능한 계약을, 단위 테스트는 어느 층이 막는가를 고정한다. 반대 방향은 FlywayMigrationTests가 잡는다 - 백필을 넣지 않았다. 운영 DB에 NULL 행이 없고, 있었다면 채울 값이 없다 — 이메일은 공급자가 주는 값이다. 임의 값을 넣으면 "표시할 이메일"이라는 목적이 깨지므로 그런 환경에서는 마이그레이션이 실패하는 편이 맞다고 보고 SQL 주석에 남겼다 프론트 질문(실패 사유를 별도 error 값으로 가르는지)에는 기존 결정대로 OAUTH_FAILED로 묶인다고 답하고 08 §3.2에 명시했다. 기록은 BI-27이다. 세 PR이 BI-25를 동시에 선점했고 머지 순서대로 확정됐다 — #98이 BI-25, #100이 BI-26, 이 기록이 BI-27이다. #100 머지 전에 BI-27을 미리 배정해 재번호를 한 번만 했다. clean check 378개 통과, 실패 0 (dev의 #98·#100 머지분 위에서 재실행) Refs #97 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
프론트에 이메일을 표시하는 화면이 있어 값 없는 계정을 둘 수 없다. 공용 계약이 먼저 바뀌어야 했다. 06 §2.2가 "미동의·미제공 시 null일 수 있다"로 정하고 있어 구현만 바꾸면 계약 위반이다(CLAUDE.md 9번). docs#28을 먼저 병합했고, 그 PR에서 두 가지를 함께 정리했다. - 1차 보장은 공급자 콘솔의 필수 동의 설정이다. 사용자가 이메일만 거절하고 진행하는 선택지가 동의 화면에 없으므로, 이 구현이 막는 것은 일상 흐름이 아니라 방어선이다 - 마스킹은 치환이며 NULL이 아니다. 적지 않으면 탈퇴 구현이 NULL을 넣어 이 제약과 부딪힌다. provider_user_id가 이미 NOT NULL이면서 마스킹 대상이라 전제는 원래 있었다 RED — 정규화 4건 + 콜백 3건 실패(기존 계약을 반대로 고정하는 것이라 뒤집는 것 자체가 RED) GREEN - V6__social_account_email_not_null.sql - OAuthUserInfo 세 분기 모두 이메일을 required(...)로 끊음, @nullable 제거 - SocialAccount @column(nullable = false), 팩토리 파라미터 @nullable 제거 - FlywayMigrationTests에 스키마 제약 단언 추가 드러난 것 - 뒤집을 테스트가 예상보다 많았다. 사전 조사로 3건을 찾았는데 clean check에서 SocialAccountPersistenceTests가 걸렸다. emailIsOptional이 영속성 층에서 null 저장을 고정하고 있었고, 같은 파일의 조회 테스트도 준비 코드에 null 이메일이 섞여 함께 깨졌다 — useEmail(null)·isNull() 검색으로는 안 걸리는 형태다 - 두 방어선이 독립임을 뮤테이션이 보여 줬다. 정규화의 required(...)만 되돌리면 단위 테스트 4건은 실패하지만 콜백 테스트 3건은 통과한다 — DB NOT NULL이 대신 잡아 같은 OAUTH_FAILED로 귀결하기 때문이다. 콜백 테스트는 관측 가능한 계약을, 단위 테스트는 어느 층이 막는가를 고정한다. 반대 방향은 FlywayMigrationTests가 잡는다 - 백필을 넣지 않았다. 운영 DB에 NULL 행이 없고, 있었다면 채울 값이 없다 — 이메일은 공급자가 주는 값이다. 임의 값을 넣으면 "표시할 이메일"이라는 목적이 깨지므로 그런 환경에서는 마이그레이션이 실패하는 편이 맞다고 보고 SQL 주석에 남겼다 프론트 질문(실패 사유를 별도 error 값으로 가르는지)에는 기존 결정대로 OAUTH_FAILED로 묶인다고 답하고 08 §3.2에 명시했다. 기록은 BI-27이다. 세 PR이 BI-25를 동시에 선점했고 머지 순서대로 확정됐다 — #98이 BI-25, 했다. clean check 378개 통과, 실패 0 (dev의 #98·#100 머지분 위에서 재실행) Refs #97 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
프론트에 이메일을 표시하는 화면이 있어 값 없는 계정을 둘 수 없다. 공용 계약이 먼저 바뀌어야 했다. 06 §2.2가 "미동의·미제공 시 null일 수 있다"로 정하고 있어 구현만 바꾸면 계약 위반이다(CLAUDE.md 9번). docs#28을 먼저 병합했고, 그 PR에서 두 가지를 함께 정리했다. - 1차 보장은 공급자 콘솔의 필수 동의 설정이다. 사용자가 이메일만 거절하고 진행하는 선택지가 동의 화면에 없으므로, 이 구현이 막는 것은 일상 흐름이 아니라 방어선이다 - 마스킹은 치환이며 NULL이 아니다. 적지 않으면 탈퇴 구현이 NULL을 넣어 이 제약과 부딪힌다. provider_user_id가 이미 NOT NULL이면서 마스킹 대상이라 전제는 원래 있었다 RED — 정규화 4건 + 콜백 3건 실패(기존 계약을 반대로 고정하는 것이라 뒤집는 것 자체가 RED) GREEN - V6__social_account_email_not_null.sql - OAuthUserInfo 세 분기 모두 이메일을 required(...)로 끊음, @nullable 제거 - SocialAccount @column(nullable = false), 팩토리 파라미터 @nullable 제거 - FlywayMigrationTests에 스키마 제약 단언 추가 드러난 것 - 뒤집을 테스트가 예상보다 많았다. 사전 조사로 3건을 찾았는데 clean check에서 SocialAccountPersistenceTests가 걸렸다. emailIsOptional이 영속성 층에서 null 저장을 고정하고 있었고, 같은 파일의 조회 테스트도 준비 코드에 null 이메일이 섞여 함께 깨졌다 — useEmail(null)·isNull() 검색으로는 안 걸리는 형태다 - 두 방어선이 독립임을 뮤테이션이 보여 줬다. 정규화의 required(...)만 되돌리면 단위 테스트 4건은 실패하지만 콜백 테스트 3건은 통과한다 — DB NOT NULL이 대신 잡아 같은 OAUTH_FAILED로 귀결하기 때문이다. 콜백 테스트는 관측 가능한 계약을, 단위 테스트는 어느 층이 막는가를 고정한다. 반대 방향은 FlywayMigrationTests가 잡는다 - 백필을 넣지 않았다. 운영 DB에 NULL 행이 없고, 있었다면 채울 값이 없다 — 이메일은 공급자가 주는 값이다. 임의 값을 넣으면 "표시할 이메일"이라는 목적이 깨지므로 그런 환경에서는 마이그레이션이 실패하는 편이 맞다고 보고 SQL 주석에 남겼다 프론트 질문(실패 사유를 별도 error 값으로 가르는지)에는 기존 결정대로 OAUTH_FAILED로 묶인다고 답하고 08 §3.2에 명시했다. 기록은 BI-27이다. 세 PR이 BI-25를 동시에 선점했고 머지 순서대로 확정됐다 — #98이 BI-25, 했다. clean check 378개 통과, 실패 0 (dev의 #98·#100 머지분 위에서 재실행) Refs #97 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
요약
pinlog-secrets-prodEnvironment에서만 읽는 수동 workflow를 추가합니다.back-prodpolicy로 SealedSecret Infra Draft PR을 만들도록 연결합니다.Jira (필수)
관련 GitHub Issue (선택)
변경 사항
.github/workflows/seal-runtime-secrets.ymlRuntimeSecretWorkflowContractTests테스트 / 검증
./gradlew test --tests com.pinlog.pinlogback.RuntimeSecretWorkflowContractTests --no-daemon→ workflow 부재로 3개 실패backend-ci.yml에${{ secrets['JWT_PRIVATE_KEY'] }}를 임시 주입 → 비접근 계약 테스트 실패 후 원복, GREEN 재확인./gradlew test --tests com.pinlog.pinlogback.RuntimeSecretWorkflowContractTests checkstyleTest --rerun-tasks --no-daemon→ 계약 테스트 4개·checkstyle 성공, exit 0actionlint .github/workflows/seal-runtime-secrets.yml(v1.7.12 checksum 검증 설치) → 성공, exit 0./gradlew clean check --no-daemon→ 실행했으나 runner에 Docker socket이 없어 Testcontainers 초기화 34건 실패(158개 중 124개 통과);checkstyleMain·checkstyleTest는 성공. PR CI에서 Docker 포함 전체 gate 확인 필요git diff --cached --check→ 성공리뷰 포인트
backend-ci에는 runtime Secret 참조나 권한을 추가하지 않았습니다.PINLOG_INFRA_SECRET_PR_TOKENbridge 1개 외 참조가 없습니다.미결 / 후속