fix: 워크플로 계약 테스트의 스텝 조회를 이름 기반으로 (dev CI 복구) - #121
Conversation
dev의 backend-ci가 빨간불이다. #115가 처음 실패했고 #116~#118이 연달아 푸시돼 concurrency로 취소되면서 드러나지 않았다가 #119에서 확정됐다 — 네 PR이 실패를 못 보고 병합된 상태다. RuntimeSecretWorkflowContractTests의 두 단언이 깨져 있었다. :62 assertThat(steps).hasSize(2) → 실제 3개 :76 list(secretJob().get("steps")).get(1) → 인덱스가 밀림 원인은 seal-runtime-secrets.yml에 진단 스텝(Diagnose OIDC endpoint metadata safely)이 checkout과 SealedSecret 액션 사이에 삽입된 것이다. 두 번째가 특히 나쁘게 부러졌다 — 스텝을 위치로 집는 탓에 무관한 단언이 엉뚱한 스텝을 검사하다 실패했다. 고친 방식 스텝을 신원(name, 없으면 uses)으로 색인한다. 개수 대신 신원의 집합을 고정하므로 "검토된 스텝만 있다"는 보증은 그대로이고, 상세 단언은 순서·삽입에 영향받지 않는다. 뮤테이션으로 확인했다 — 스텝을 하나 더 넣으면 집합 단언 1건만 실패한다(전에는 무관한 단언까지 함께 부러졌다). 진단 스텝에 계약을 하나 붙였다. 이 Environment 경계 안에서 도는 임의 스크립트이므로 런타임 Secret을 참조하지 않아야 한다 — 참조하면 그 값이 공개 저장소의 Actions 로그로 나갈 수 있다. 현재 코드는 claim만 찍고 토큰이나 시크릿은 출력하지 않는다. 확인이 필요한 것 composite action SHA도 함께 바뀌어 있었다. 테스트를 통과시키려면 갱신이 불가피해 반영했지만, 이 핀은 "누군가 이 리비전을 검토했다"는 뜻이므로 내 검토는 없었다. b3f26ab8909ed7732e15aa64f432a720ec531401 (테스트가 고정하던 값) ee325683ee2fb99ee47b07c11d02396b33701e0b (#114~#119에서 바뀐 현재 값) 별개로 관측한 것: 이 테스트가 읽는 .github/workflows/*.yml이 Gradle의 test 입력이 아니어서, 워크플로만 고치면 :test가 UP-TO-DATE로 통과한다. 로컬에서 드리프트가 안 보이는 이유다(CI는 매번 새 체크아웃이라 무관하다). --rerun-tasks가 필요하다. clean check 395개 통과, 실패 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
확인했습니다.
스텝을 위치가 아니라 신원으로 조회하도록 바꾼 접근과, 허용된 신원 집합을 fail-closed로 고정한 방향은 좋습니다. 진단 스텝 제거 후에도 checkout/action 상세 계약은 이름 기반으로 유지해 주세요. 추가로 발견한 Gradle 입력 추적 문제는 이번 CI 복구와 분리하는 데 동의합니다. 후속에서는 |
#121 리뷰 반영. @tpals0409 판단은 둘이었다. 1. Action SHA ee325683은 의도한 값이다(infra#100 merge commit). 계약 핀 갱신을 유지한다 2. 진단 스텝은 제거한다. 고정된 Infra Action이 이미 OIDC fetch·claim 검증과 단계별 진단을 수행하므로, Environment 경계 안에서 JWT를 한 번 더 발급·파싱해 claim을 로그로 남기는 스크립트를 영구 경로에 중복 유지할 실익이 없다 그래서 seal-runtime-secrets.yml에서 스텝을 지우고 허용 신원 집합도 둘로 줄였다. 진단 스텝 전용이던 단언은 삭제하지 않고 잡 전체로 넓혔다. diagnosticStepCannotReadRuntimeOrBridgeSecrets → noStepRunsAnInlineScriptInsideTheSecretEnvironment 전자는 그 스텝이 사라지면 검증 대상이 없어 함께 사라진다. 후자는 "이 잡은 SHA로 고정된 action만 쓴다"를 고정하므로, 같은 종류의 스크립트가 다시 들어오면 이름이 무엇이든 걸린다. 런타임 Secret 9개에 접근하는 경계 안에서 임의 스크립트가 도는 것을 막는 것이 원래 의도였고, 그 의도를 스텝 이름에 묶지 않는 형태로 옮긴 것이다. clean check 395개 통과, 실패 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
확인 감사합니다. 두 판단 모두 반영해 1. Action SHA — 유지했습니다
2. 진단 스텝 — 제거했습니다
assertThat(stepsByIdentity().keySet()).containsExactlyInAnyOrder(CHECKOUT, INFRA_PR_STEP);한 가지는 삭제하지 않고 형태를 바꿨습니다. 진단 스텝 전용이던 단언을 잡 전체로 넓혔습니다. 이유는 말씀하신 판단 근거 그대로입니다 — "Environment 경계 안에서 ... 스크립트를 영구 경로에 중복 유지할 실익이 없다." 그 판단이 이번 한 스텝에만 해당하는 게 아니라 이 잡의 성질이라고 읽었습니다. 새 테스트는 "이 잡은 SHA로 고정된 action만 쓴다"를 고정하므로, 같은 종류의 인라인 스크립트가 이름이 무엇이든 다시 들어오면 걸립니다. 과하다고 보시면 빼겠습니다 — 원래 계약( 검증
이 PR이 머지되면 후속 동의 확인Gradle 입력 추적은 별건으로 남기겠습니다. |
필수 변경 2건
스텝을 위치가 아니라 신원으로 집게 바꾼 방향과, 보증이 남았는지 뮤테이션으로 확인한 점이 좋습니다.
|
#121 리뷰 반영(@minyongP). 내 fail-closed 주장에 구멍이 있었다. stepsByIdentity()가 Map.put으로 색인하므로 이름이 같은 스텝은 조용히 덮어써진다. 그래서 스텝을 하나 더 넣어도 색인은 2개로 남아 집합 단언이 통과하고, 가려진 쪽은 어떤 단언도 보지 못한다 — run:과 시크릿 참조를 가진 스텝을 그렇게 숨길 수 있었다. 색인 크기가 원본 리스트와 같은지 helper 안에서 확인해 닫았다. 테스트 하나에 단언을 추가하는 대신 helper에 둔 이유는 모든 호출자가 같은 전제에 기대기 때문이다 — step()과 noStepRunsAnInlineScript...도 이 색인을 쓴다. 뮤테이션으로 확인했다. 리뷰가 지적한 정확한 시나리오를 넣었다. - name: Create canonical Infra SealedSecret Draft PR run: echo "${{ secrets.JWT_PRIVATE_KEY }}" 고치기 전에는 통과했을 형태이고, 이제 3건이 실패한다 — 색인 크기(집합 단언), 상세 계약, 인라인 스크립트 금지가 모두 걸린다. clean check 395개 통과, 실패 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
리뷰 감사합니다. 두 지적 모두 맞습니다. 1. 신원 중복으로 스텝이 가려지는 구멍 — 고쳤습니다지적 그대로입니다. assertThat(byIdentity)
.as("신원이 겹치는 스텝이 있어 색인에서 가려졌다: %s", steps)
.hasSameSizeAs(steps);테스트 하나가 아니라 helper 안에 뒀습니다. 지적하신 시나리오를 그대로 넣어 확인했습니다. - name: Create canonical Infra SealedSecret Draft PR
run: echo "${{ secrets.JWT_PRIVATE_KEY }}"
- name: Create canonical Infra SealedSecret Draft PR
uses: Team-PinLog/infra/.github/actions/sealedsecret-infra-pr@ee325683...고치기 전에는 통과했을 형태입니다. 시크릿을 로그로 내보내는 스텝이 색인 뒤에 숨는 경로였으니, 이 리뷰가 막은 것이 형식 문제가 아니라 실제 노출 경로입니다. 2. PR 본문이 head보다 뒤처진 것 — 갱신했습니다맞습니다. 본문을 head 기준으로 다시 썼습니다 — 변경 사항에 진단 스텝 제거·신원 중복 방어·SHA 확정을 반영하고, 해소된 질문 절은 지웠습니다. 남긴 리뷰 포인트는 하나입니다(잡 전체로 넓힌 단언이 과한지). 검증
|
요약
dev의backend-ci가 빨간불이라 모든 PR이 병합 불가 상태다.RuntimeSecretWorkflowContractTests가seal-runtime-secrets.yml의 실제 내용과 어긋났다. 스텝을 위치가 아니라 신원으로 집게 바꾸고, 리뷰 판단에 따라 진단 스텝을 제거해 푼다.Jira (필수)
devCI 복구이고 티켓 없이 진행했다. 필요하면 키를 받아 브랜치명을 맞추겠다관련 GitHub Issue (선택)
왜 지금까지 안 보였는가
#115에서 처음 실패했고#116~#118은 연달아 푸시돼concurrency: cancel-in-progress로 취소되면서 실패가 드러나지 않았다.#119에서 다시 돌아 확정됐다 — 네 PR이 빨간불을 못 보고 병합됐다.무엇이 깨져 있었나
Diagnose OIDC endpoint metadata safely스텝이 checkout과 SealedSecret 액션 사이에 삽입됐다. 두 번째가 특히 나쁘게 부러졌다 — 스텝을 위치로 집는 탓에infraActionHasExactly...가 엉뚱한 스텝을 검사하다 실패해, 메시지가 원인을 가리키지 않는다.변경 사항
stepsByIdentity()·step(identity). 신원은name이고,name이 없는 checkout은uses다containsExactlyInAnyOrder(CHECKOUT, INFRA_PR_STEP)seal-runtime-secrets.yml에서 삭제(-23행). @tpals0409 판단 반영noStepRunsAnInlineScriptInsideTheSecretEnvironmentee325683으로 갱신 —infra#100merge commit, @tpals0409 확인 완료테스트 / 검증
./gradlew clean check --no-daemon— 395개 통과, 실패 0보증이 남았는지 뮤테이션 2종으로 확인했다.
run:+secrets.JWT_PRIVATE_KEY참조두 번째가 리뷰에서 지적된 시나리오다.
Map.put이 조용히 덮어써서 고치기 전에는 통과했을 형태다 — 색인은 2개로 남고 가려진 쪽은 어떤 단언도 보지 못했다.리뷰 포인트
진단 스텝 전용 단언을 삭제하지 않고 잡 전체로 넓혔다.
@tpals0409의 판단 근거("Environment 경계 안에서 스크립트를 영구 경로에 중복 유지할 실익이 없다")가 이번 한 스텝이 아니라 이 잡의 성질이라고 읽었다. 새 형태는 "이 잡은 SHA로 고정된 action만 쓴다"를 고정하므로 같은 종류가 이름이 무엇이든 다시 들어오면 걸린다. 과하다면 원래 계약(
action에run없음)만 남기는 형태로 되돌릴 수 있다.미결 / 후속
이 테스트의 입력이 Gradle에 등록돼 있지 않다.
.github/workflows/*.yml을 고쳐도:test가UP-TO-DATE로 통과한다.--rerun-tasks가 필요하다. 로컬에서 이 드리프트가 안 보이는 이유가 이것이고(위 뮤테이션도 처음엔 아무 반응이 없어 발견했다), CI는 매번 새 체크아웃이라 무관하다. @tpals0409가 분리에 동의했고,test태스크에 두 워크플로를inputs.files로 등록하는 형태를 별건으로 올릴 예정이다.