Skip to content

fix: 워크플로 계약 테스트의 스텝 조회를 이름 기반으로 (dev CI 복구) - #121

Merged
cherry-go-round merged 3 commits into
devfrom
fix/seal-workflow-contract-step-lookup
Jul 31, 2026
Merged

fix: 워크플로 계약 테스트의 스텝 조회를 이름 기반으로 (dev CI 복구)#121
cherry-go-round merged 3 commits into
devfrom
fix/seal-workflow-contract-step-lookup

Conversation

@cherry-go-round

@cherry-go-round cherry-go-round commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

요약

devbackend-ci가 빨간불이라 모든 PR이 병합 불가 상태다. RuntimeSecretWorkflowContractTestsseal-runtime-secrets.yml의 실제 내용과 어긋났다. 스텝을 위치가 아니라 신원으로 집게 바꾸고, 리뷰 판단에 따라 진단 스텝을 제거해 푼다.

Jira (필수)

  • 없다. dev CI 복구이고 티켓 없이 진행했다. 필요하면 키를 받아 브랜치명을 맞추겠다

관련 GitHub Issue (선택)

왜 지금까지 안 보였는가

failure    76ce76e  fix: pin exact untracked path validation (#119)
cancelled  3d0d4e1  (#118)
cancelled  27fd75a  (#117)
cancelled  9728c27  (#116)
failure    1a03583  fix: add safe OIDC claim diagnostics (#115)

#115에서 처음 실패했고 #116~#118은 연달아 푸시돼 concurrency: cancel-in-progress취소되면서 실패가 드러나지 않았다. #119에서 다시 돌아 확정됐다 — 네 PR이 빨간불을 못 보고 병합됐다.

무엇이 깨져 있었나

:62  assertThat(steps).hasSize(2)                → 실제 3개
:76  list(secretJob().get("steps")).get(1)       → 인덱스가 밀림

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 판단 반영
  • 인라인 스크립트 금지를 잡 전체 계약으로noStepRunsAnInlineScriptInsideTheSecretEnvironment
  • 신원 중복 방어 — 색인 크기가 원본 리스트와 같은지 확인. @minyongP 리뷰 반영
  • composite action SHA를 ee325683으로 갱신 — infra#100 merge commit, @tpals0409 확인 완료

테스트 / 검증

  • ./gradlew clean check --no-daemon395개 통과, 실패 0

보증이 남았는지 뮤테이션 2종으로 확인했다.

넣은 것 실패
이름이 다른 스텝 하나 추가 집합 단언 1건
이름이 같은 스텝 + run: + secrets.JWT_PRIVATE_KEY 참조 3건 — 색인 크기·상세 계약·인라인 스크립트 금지

두 번째가 리뷰에서 지적된 시나리오다. Map.put이 조용히 덮어써서 고치기 전에는 통과했을 형태다 — 색인은 2개로 남고 가려진 쪽은 어떤 단언도 보지 못했다.

리뷰 포인트

진단 스텝 전용 단언을 삭제하지 않고 잡 전체로 넓혔다.

diagnosticStepCannotReadRuntimeOrBridgeSecrets   (스텝이 사라지면 검증 대상도 사라진다)
  → noStepRunsAnInlineScriptInsideTheSecretEnvironment

@tpals0409의 판단 근거("Environment 경계 안에서 스크립트를 영구 경로에 중복 유지할 실익이 없다")가 이번 한 스텝이 아니라 이 잡의 성질이라고 읽었다. 새 형태는 "이 잡은 SHA로 고정된 action만 쓴다"를 고정하므로 같은 종류가 이름이 무엇이든 다시 들어오면 걸린다. 과하다면 원래 계약(actionrun 없음)만 남기는 형태로 되돌릴 수 있다.

미결 / 후속

이 테스트의 입력이 Gradle에 등록돼 있지 않다. .github/workflows/*.yml을 고쳐도 :testUP-TO-DATE로 통과한다.

$ (워크플로 수정 후) ./gradlew test --tests "...RuntimeSecretWorkflowContractTests"
> Task :test UP-TO-DATE
BUILD SUCCESSFUL

--rerun-tasks가 필요하다. 로컬에서 이 드리프트가 안 보이는 이유가 이것이고(위 뮤테이션도 처음엔 아무 반응이 없어 발견했다), CI는 매번 새 체크아웃이라 무관하다. @tpals0409가 분리에 동의했고, test 태스크에 두 워크플로를 inputs.files로 등록하는 형태를 별건으로 올릴 예정이다.

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>
@tpals0409

Copy link
Copy Markdown
Contributor

확인했습니다.

  1. Action SHA ee325683ee2fb99ee47b07c11d02396b33701e0b는 의도한 값이 맞습니다. Infra PR Team-PinLog/infra#100의 merge commit이며, 신규 provenance 파일을 git status --untracked-files=all로 정확히 열거하도록 수정한 리비전입니다. 관련 Infra 검증(guardrails, helm-lint, service-render, platform-schema, monitoring-render, helm, ci-gate)도 통과한 것을 다시 확인했습니다. 따라서 이 PR에서 해당 SHA로 계약 핀을 갱신해 주세요.

  2. Diagnose OIDC endpoint metadata safely 스텝은 제거하는 쪽으로 부탁드립니다. 이 스텝은 당시 OIDC 실패 원인 확인을 위해 넣은 진단용이었고, 현재 고정된 Infra Action 자체가 OIDC fetch/claim 검증 및 단계별 안전 진단을 수행합니다. Environment 경계 안에서 JWT를 별도로 한 번 더 발급·파싱하고 claim을 로그로 남기는 스크립트를 영구 경로에 중복 유지할 실익이 없습니다. 따라서 워크플로에서 진단 스텝을 제거하고, 이 PR의 허용 스텝 집합에서도 DIAGNOSE_STEP을 빼는 방향이 맞습니다.

스텝을 위치가 아니라 신원으로 조회하도록 바꾼 접근과, 허용된 신원 집합을 fail-closed로 고정한 방향은 좋습니다. 진단 스텝 제거 후에도 checkout/action 상세 계약은 이름 기반으로 유지해 주세요.

추가로 발견한 Gradle 입력 추적 문제는 이번 CI 복구와 분리하는 데 동의합니다. 후속에서는 .github/workflows/seal-runtime-secrets.ymlbackend-ci.yml을 해당 테스트 태스크 입력으로 등록해 로컬 UP-TO-DATE 오판을 막는 게 좋겠습니다.

#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>
@cherry-go-round

Copy link
Copy Markdown
Contributor Author

확인 감사합니다. 두 판단 모두 반영해 ad5a38b로 푸시했습니다.

1. Action SHA — 유지했습니다

ee325683ee2fb99ee47b07c11d02396b33701e0binfra#100 merge commit이고 Infra 검증 7종이 통과했다는 근거까지 확인했습니다. 계약 핀을 그 값으로 둡니다.

2. 진단 스텝 — 제거했습니다

seal-runtime-secrets.yml에서 스텝을 지우고 허용 신원 집합도 둘로 줄였습니다.

assertThat(stepsByIdentity().keySet()).containsExactlyInAnyOrder(CHECKOUT, INFRA_PR_STEP);

한 가지는 삭제하지 않고 형태를 바꿨습니다. 진단 스텝 전용이던 단언을 잡 전체로 넓혔습니다.

diagnosticStepCannotReadRuntimeOrBridgeSecrets   (스텝이 사라지면 검증 대상도 사라짐)
  → noStepRunsAnInlineScriptInsideTheSecretEnvironment

이유는 말씀하신 판단 근거 그대로입니다 — "Environment 경계 안에서 ... 스크립트를 영구 경로에 중복 유지할 실익이 없다." 그 판단이 이번 한 스텝에만 해당하는 게 아니라 이 잡의 성질이라고 읽었습니다. 새 테스트는 "이 잡은 SHA로 고정된 action만 쓴다"를 고정하므로, 같은 종류의 인라인 스크립트가 이름이 무엇이든 다시 들어오면 걸립니다.

과하다고 보시면 빼겠습니다 — 원래 계약(actionrun 없음)만 남기는 형태로 되돌릴 수 있습니다.

검증

./gradlew clean check --no-daemon395개 통과, 실패 0.

이 PR이 머지되면 #120(회원 탈퇴)의 CI도 함께 풀립니다. 그쪽은 이 파손을 상속받아 빨간불이지만 두 파일 중 어느 것도 건드리지 않았습니다.

후속 동의 확인

Gradle 입력 추적은 별건으로 남기겠습니다. test 태스크에 두 워크플로를 inputs.files로 등록하는 형태를 생각하고 있는데, 빌드 스크립트 변경이라 이 PR 밖입니다 — 별도 이슈로 올릴까요?

@minyongP

Copy link
Copy Markdown
Contributor

Claude Code 자동 리뷰입니다. 판정은 항상 comment이며, 승인·변경 요청은 @minyongP가 직접 남깁니다.

필수 변경 2건

  • src/test/java/com/pinlog/pinlogback/RuntimeSecretWorkflowContractTests.java:147byIdentity.put(...)이 같은 신원의 스텝을 조용히 덮어쓰기 때문에, hasSize(2)가 주던 "검토된 스텝만 있다"는 보증이 :68의 집합 단언에서 약해집니다. 이름이 같은 스텝을 하나 더 넣으면 map은 2개로 남아 집합 단언이 통과하고, 그 중 먼저 오는 것이 run:을 갖고 있으면 새로 넣은 noStepRunsAnInlineScriptInsideTheSecretEnvironment도 그 스텝을 보지 못합니다. stepsByIdentity().size()가 원래 스텝 리스트의 크기와 같은지 함께 단언하면 닫힙니다.
  • PR 본문이 head보다 한 리비전 앞서 있습니다. 본문의 변경 사항확인 부탁드립니다 ②는 진단 스텝을 남기는 쪽(DIAGNOSE_STEP을 집합에 포함)을 전제하는데, head는 그 스텝을 seal-runtime-secrets.yml에서 제거했습니다(-23). @tpals0409가 이미 반영된 질문에 답하게 되므로 본문을 head에 맞춰 주시면 좋겠습니다.

스텝을 위치가 아니라 신원으로 집게 바꾼 방향과, 보증이 남았는지 뮤테이션으로 확인한 점이 좋습니다.

nit 수준 제안은 내지 않습니다.

#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>
@cherry-go-round

Copy link
Copy Markdown
Contributor Author

리뷰 감사합니다. 두 지적 모두 맞습니다. 1cced26으로 고쳤습니다.

1. 신원 중복으로 스텝이 가려지는 구멍 — 고쳤습니다

지적 그대로입니다. Map.put이 조용히 덮어쓰므로 제가 주장한 fail-closed가 성립하지 않았습니다. 색인 크기를 원본 리스트와 비교해 닫았습니다.

assertThat(byIdentity)
    .as("신원이 겹치는 스텝이 있어 색인에서 가려졌다: %s", steps)
    .hasSameSizeAs(steps);

테스트 하나가 아니라 helper 안에 뒀습니다. step()noStepRunsAnInlineScriptInsideTheSecretEnvironment도 같은 색인에 기대므로, 한 곳에서 끊으면 세 경로가 함께 닫힙니다.

지적하신 시나리오를 그대로 넣어 확인했습니다.

      - 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...
noStepRunsAnInlineScriptInsideTheSecretEnvironment()  FAILED
secretJobHasExactlyTheReviewedEnvironmentAndSteps()   FAILED
infraActionHasExactlyTheReviewedInputsAndSecretMapping() FAILED
5 tests completed, 3 failed

고치기 전에는 통과했을 형태입니다. 시크릿을 로그로 내보내는 스텝이 색인 뒤에 숨는 경로였으니, 이 리뷰가 막은 것이 형식 문제가 아니라 실제 노출 경로입니다.

2. PR 본문이 head보다 뒤처진 것 — 갱신했습니다

맞습니다. ad5a38b에서 진단 스텝을 제거했는데 본문은 그 스텝을 남기는 전제였고, 확인 부탁드립니다 ②는 이미 답을 받은 질문이었습니다. @tpals0409가 같은 질문에 두 번 답하게 되는 상태였습니다.

본문을 head 기준으로 다시 썼습니다 — 변경 사항에 진단 스텝 제거·신원 중복 방어·SHA 확정을 반영하고, 해소된 질문 절은 지웠습니다. 남긴 리뷰 포인트는 하나입니다(잡 전체로 넓힌 단언이 과한지).

검증

./gradlew clean check --no-daemon395개 통과, 실패 0.

@cherry-go-round
cherry-go-round merged commit 9c5044f into dev Jul 31, 2026
2 checks passed
@cherry-go-round
cherry-go-round deleted the fix/seal-workflow-contract-step-lookup branch July 31, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants