gwimong's blog Software Engineer

artifact-transformer 작업 일지 #39

승인 게이트가 엉뚱한 잡을 막고 있던 날 — 그리고 스펙 없는 요구를 스텁으로 막아둔 결정


이 글은 2026-08-25 하루의 기록이다. 오전에는 산출물 저장소에서 티켓 15건의 검토 의견을 실제 문서에 반영했고, 저녁에는 배포 워크플로의 승인 게이트가 배포가 아니라 빌드를 막고 있다는 걸 발견해 구조를 갈아엎었다.

배경 — 검토 의견은 쌓여 있고, 파이프라인은 방금 생겼다

두 갈래 일이 하루에 겹쳤다.

한쪽은 SURAS 산출물이다. 위험도 평가 편의성 개선을 위해 산출물 단계별로 티켓(SURAS-1584~1598) 15건이 이미 만들어져 있었다. 개선 방향은 셋이었다 — 평가에 실제로 쓰이는 정보만 입력받기, SORA와 SRM을 분리하기, 기체·인적 같은 자원을 이 시스템이 직접 관리하지 않기(추후 OMS 연동).

다른 한쪽은 baton 저장소다. 오전에 모노레포로 합쳤고(978f1d4), 오후에 git flow와 전달 파이프라인을 세웠다(27659cb). 그 워크플로가 실제로 원하는 걸 막고 있는지는 세우고 나서야 들여다보게 됐다.

문제

  1. 검토 의견 15건 중 무엇을 실제 문서에 반영하고, 무엇을 반영하지 않을 것인가.
  2. 근거가 되는 외부 시스템 스펙이 아예 없는 요구는 어떻게 처리하는가.
  3. environment: 승인 게이트는 워크플로의 어느 잡에 붙어야 하는가.

결정 1 — 검토는 15건 전부에, 반영은 근거가 있는 것부터

먼저 산출물 저장소 실물을 뒤져 근거를 모은 다음에 댓글을 달았다. 티켓 하나하나에 “이 문서의 이 식별자가 이래서 문제다”를 적으려면 실제 데이터가 필요했다.

댓글은 세 방향을 태그로 구분했다.

태그대표 근거
[SORA/SRM 분리]유스케이스명 자체가 “위험도평가[SORA/SRM] 수행”, FUN-RSK-052가 두 기준을 한 모델로 등록
[자원 자체관리 배제]연동 데이터 소요서(IDR) 9건 중 기체·인적 연동 항목이 0건 — 자체 관리의 직접 증거
[입력 간소화]기체 속성 12개 중 필수 4개, 입력 필드 최다 기능 4개가 전부 기체·비행정보

논리 데이터 정의서는 이미 LDE-RSK-002(SORA평가)와 LDE-RSK-003(SRM평가)으로 나뉘어 있었다. 분리를 새로 설계할 필요가 없다는 뜻이라, 이걸 나머지 문서가 따라갈 기준선으로 삼았다.

우선순위대로 기능 명세서(function.json)부터 손댔다. 여기서 한 가지가 걸렸다 — 어떤 필드를 필수로 둘지 판단하려면 “그 필드가 실제로 위험도 산출에 쓰이는가”를 확인해야 했는데, 그 근거가 같은 문서 안에 있었다.

기능변경
FUN-INF-017 기체정보 등록형식·제조사·신고번호 필수→선택 / 최대이륙중량·최대속도 선택→필수(Containment·iGRC 산출 근거)
FUN-INF-013 비행정보 등록좌표·범위 필드 필수→선택 / 지상위험범위는 iGRC 근거로 필수 유지
FUN-RSK-052 모델 등록모델 정의 1개 → SORA 모델 / SRM 모델 2개로 분리

FUN-INF-013의 나머지 좌표·범위 필드는 실제 산출에 쓰인다는 근거 문구를 찾지 못했다. 필수에서 뺐지만 완전히 지우지는 않고, 논리 데이터 재검토 때 같이 확인하라는 메모를 remarks에 남겼다.

검사와 개정은 CLI가 결정한다.

python check.py --env env/project.suras.env
python revise.py apply --env env/project.suras.env --check --desc-per "function.json=..."

스키마·추적성·다이어그램 정합성 검사를 통과하고 function.json v1.1.4 → v1.1.5로 기록됐다(9c7a231).

결정 2 — 존재하지 않는 시스템을 근거로 명세를 들어내지 않는다

나머지 12건을 반영하려는 자리에서 멈췄다. [자원 자체관리 배제]는 13건 중 9건에 걸쳐 있는데, OMS가 어떤 시스템이고 어떤 필드로 연동되는지가 어느 댓글에도 없었다. participant.json에 OMS 참여자 자체가 없었고, 인터페이스 계약도 정의된 적이 없었다.

이 상태로 요구사항 12건·유스케이스 3건의 기존 CRUD를 실제로 들어내면, 존재하지 않는 시스템 연동을 근거로 지금 동작하는 명세를 깨뜨리는 셈이 된다.

그래서 세 갈래를 놓고 확인을 받았다 — 댓글 응답만 남기기 / OMS를 외부 참여자 스텁으로만 등록하기 / CRUD를 실제로 축소하기. 선택은 가운데였다.

결과적으로 participant.json에 oms 외부 참여자만 추가하고 컨텍스트·컨테이너·컴포넌트 3레벨 다이어그램을 동기화했다. 나머지 9종 문서의 CRUD 구조는 손대지 않고 remarks에 “OMS 스펙 확정 후 후속 이슈” 메모만 붙였다.

같은 회차에 근거가 확실한 것들은 전부 반영됐다. SORA/SRM 분리는 기존 ID를 SORA 전용으로 재정의하고 SRM 전용 ID를 신설하는 쌍 구조로 갔다.

LORAS-BIZ-RSK-003 · LORAS-BIZ-DAT-002 · LORAS-REQ-RSK-057
LORAS-USC-RSK-017 · LORAS-OSC-RSK-004 · LORAS-ASC-RSK-003
LORAS-IDE-SDSP-001

부수적으로 REQ-RSK-008/028/038이 SRM 관련 요구사항인데 SORA 업무를 참조하고 있던 오류도 이때 드러나 정정됐다. 분리를 하려고 참조를 훑지 않았으면 그대로 남았을 오류다.

최종 8종 JSON + 26종 .puml, 신규 다이어그램 1개까지 35개 파일이 개정 이력과 함께 커밋됐다(8c1852d). parse.py diagram --diff가 전부 [+0 -0 ~0]이었으니 JSON과 다이어그램은 어긋나지 않았다.

결정 3 — 승인 게이트는 빌드가 아니라 승격에 붙인다

저녁에 CD 워크플로를 열어 보니 environment:가 이미지를 빌드하는 잡에 붙어 있었다.

이러면 승인자가 승인해야 할 대상이 아직 존재하지 않는다. 승인은 “이 이미지를 프로덕션으로 올릴까”를 묻는 것인데, 게이트가 앞에 있으면 그 이미지가 만들어지기 전에 물어보는 셈이다.

구조를 GitHub이 권장하는 형태로 바꿨다 — 게시는 게이트 없이, 승인은 승격 잡이 갖는다.

  # Publishing is never gated: the artefact must exist before anyone can be asked
  # to approve its promotion. Approval belongs to the `promote` job below.
  publish:
    permissions:
      contents: read
      packages: write
      attestations: write
      id-token: write

publish의 각 매트릭스 레그는 자기가 게시한 다이제스트를 아티팩트로 올린다. promote가 그걸 되읽어 형식을 검증하고 image@sha256:…로 고정한 뒤 레지스트리에 실제로 있는지 확인한다.

digest="$(cat "${digest_file}")"
if [[ ! "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
  echo "Invalid digest for ${component}: ${digest}" >&2
  exit 1
fi
docker buildx imagetools inspect "ghcr.io/${OWNER}/baton-${component}@${digest}" > /dev/null

승인 게이트가 promote에 있으니 필수 리뷰어·대기 타이머·배포 브랜치 정책이 여기서 작동하고, GitHub Deployment로도 기록된다. 실제 롤아웃 단계를 나중에 붙일 자리도 이 잡 안이다 — 그래야 같은 승인을 물려받는다.

권한도 잡별로 쪼갰다. 워크플로 기본은 contents: read, 게시 잡만 packages: write와 attestation 권한, 릴리스 잡만 contents: write.

결정 4 — latest는 승인 이후에 움직인다

구조를 바꾸고 나서 한 군데가 남아 있었다. latest 태그가 여전히 게시 시점에 붙고 있었다.

-            type=raw,value=latest,enable=${{ ... release == 'true' }}

승인 전에 latest가 이미 새 이미지를 가리키면, latest를 보고 받아 가는 쪽에는 승인 여부가 아무 의미가 없다. 게이트가 형식만 남는다.

메타데이터 단계에서 latest를 빼고, 승인 이후 promote에서 다이제스트를 그대로 재태깅하도록 옮겼다.

docker buildx imagetools create --tag "ghcr.io/${OWNER}/baton-${component}:latest" "${reference}"

github-release도 needs: publish에서 needs: promote로 바꿨다. 거부된 배포에 릴리스 노트만 남는 일이 없게.

워크플로를 고칠 때는 실행해 볼 방법이 마땅치 않아, run 블록을 전부 뽑아 셸 문법 검사를 돌리고 promote의 스크립트는 가짜 다이제스트 파일을 만들어 로컬에서 그대로 실행해 봤다.

의도치 않은 함정 — 사라진 env 경로와, 이슈 키로 오인된 식별자

두 번째 세션을 시작하자마자 /scope suras가 실패했다. env/project.suras.env가 없다고 나왔고, env/ 디렉터리 자체가 비어 있었다.

오전에 모노레포로 합치면서 설정 파일이 저장소 루트 config/로 옮겨졌기 때문이다. 스킬 문서에 적힌 경로는 옛 구조를 가리키고 있었다. 이후 호출은 전부 --env ../../config/project.suras.env로 바꿨다.

구조를 옮기면 문서에 박힌 경로도 같이 옮겨야 한다. 이건 항상 옮기고 나서 알게 된다.

또 하나. 개정 시 --notify-issue로 설명문에서 이슈 키를 뽑아 댓글을 다는데, INF-001·RSK-016 같은 내부 식별자가 이슈 키 패턴에 걸려 404 경고가 떴다. 무해한 노이즈지만, 자동 추출이 프로젝트 내부 식별자 체계와 충돌한다는 건 남겨 둘 만하다. 반대 방향의 누락도 있었다 — 설명문에 이슈 키가 직접 안 적힌 5건에는 자동 댓글이 붙지 않았다.

마무리

오늘 확인한 것은 둘이다.

  • 근거 없는 요구는 스텁으로 표시만 하고 멈추는 게 반영보다 낫다. OMS 연동은 방향이 옳아도 인터페이스 계약이 없으면 현재 명세를 근거 없이 깨뜨린다. 참여자 스텁 하나와 remarks 메모가 “잊지 않았지만 아직 아니다”를 문서에 남기는 방법이었다.
  • 승인 게이트는 승인할 대상이 이미 존재하는 자리에 붙어야 한다. 빌드 잡에 붙은 게이트는 승인이 아니라 지연이고, 승인 전에 움직이는 latest는 게이트를 형식으로 만든다.

남은 일은 셋이다. docs/development/git-flow.md에 남아 있는 옛 설명(latest 적용 시점)을 새 구조에 맞게 고치는 것, 새로 들인 아티팩트 액션의 메이저 버전이 저장소의 나머지 액션과 어긋나는 문제를 정리하는 것, 그리고 실제 태그를 한 번 밀어 imagetools create가 SBOM/provenance 인덱스를 재태깅할 때 다이제스트를 보존하는지 확인하는 것이다.


Comments

Content