제 배포 파이프라인이 내 릴리스를 출하하는 것을 거부했다. 그것은 옳았다.
요약
개발자가 배포 파이프라인의 실패를 통해 시스템의 견고성을 입증한 사례입니다. 릴리스 워크플로우는 아티팩트가 게시되기 전, `agentwatch verify-release`를 통해 재검증하는 강력한 게이트를 가지고 있습니다. 이 과정에서 예상치 못한 `ModuleNotFoundError: No module named 'cryptography'` 오류가 발생하며 배포가 중단되었고, 이는 시스템이 설계대로 작동했음을 의미합니다.
핵심 포인트
- 배포 전 검증(verify-release)은 단순한 스모크 테스트를 넘어선 필수 게이트입니다.
- 의존성 관점에서 선택적 라이브러리(`cryptography`)가 하드 의존성처럼 동작할 수 있습니다.
- 최소한의 설치 환경에서도 임포트 체인 분석을 통해 잠재적 버그를 포착했습니다.
저는 v0.2.0 태그를 푸시했습니다. 릴리스 워크플로우는 휠(wheel)을 빌드하고, SBOM을 생성하고, 체크섬을 작성하고, keyless Sigstore로 모든 것을 서명한 후 — 마지막 단계에서 실패했습니다:
X Verify the release before publishing
ModuleNotFoundError: No module named 'cryptography'
워크플로우는 절대 게시되지 않았습니다. GitHub 릴리스: 없음. 이것은 시스템이 설계된 대로 정확하게 작동하고 있다는 것을 의미하며, 이 글은 그 버그가 포착한 이야기입니다. 이는 제 테스트 스위트로는 구조적으로 잡을 수 없었던, 제가 몇 년 전에 생각하는 것을 멈춘 지점에서 발생한 문제입니다.
이는 agentwatch v0.2.1 릴리스에서 가져온 내용입니다. agentsec-ecosystem의 에이전트 보안 도구입니다. 레포: agentsec-ecosystem/agentwatch.
릴리스를 구한 설계
릴리스 워크플로우에는 게시 전에 단계가 있습니다: 패키지를 설치하고 agentwatch verify-release를 실행하여 깨끗한 환경 내부에서 빌드된 아티팩트를 재검증하는 것입니다. 워크플로우 파일은 한 줄로 자체적인 계약을 문서화합니다:
The workflow refuses to publish an artifact that
agentwatch verify-releasecannot confirm.
이것은 스모크 테스트(smoke test)가 아닙니다. 이것은 게시 전제 조건입니다. CLI가 실행될 수 없다면, 아무것도 출하되지 않습니다. 따라서 CLI가 실행될 수 없었을 때, 그날 출하된 유일한 것은... 아무것도 아니었습니다.
이 단 하나의 설계 결정은 'v0.2.0이 라이브다, 앗'이 될 뻔했던 상황을 실패한 실행과 패치 번호로 바꾸어 놓았습니다.
천천히 읽을 가치가 있는 트레이스백
실패는 검증 로직에 있지 않았습니다. import에 있었습니다:
File ".../bin/agentwatch", line 3, in <module>
from agentwatch.cli import main
File ".../agentwatch/cli/main.py", line 71, in <module>
...
릴리스 환경은 pip install -e packages/python-sdk를 실행합니다 — 추가 옵션(extras) 없이요. 그리고 이 프로젝트에서 cryptography는 선택적 추가 옵션([signing])입니다. 하드 의존성으로 만들면 잘못되기 쉬운데, 대부분의 설치는 아무것도 서명하지 않기 때문입니다.
하지만 agent_card.py — A2A agent-card 서명 검증기(signature verifier) —가 **모듈 상단(module top)**에서 InvalidSignature를 가져오고 있었습니다. 그리고 agent_card는 adapters 패키지에 의해 임포트됩니다. 이 adapters는 다시 demo에 의해, 그리고 최종적으로 CLI에 의해 임포트됩니다. 따라서:
단순히 pip install agentsec-agentwatch를 실행하는 것만으로도 --help에서 작동을 멈추는 CLI가 생성되었습니다. 서명 과정은 전혀 없었습니다. A2A 관련 로직도 없습니다. 단지 임포트 체인(import chain)이 임포트 체인이 하는 일, 즉 시작 시 모든 것을 메모리로 미리 끌어들이는 과정을 수행했을 뿐입니다.
이 문제가 발생한 환경들은 모두 cryptography 라이브러리가 설치되어 있었습니다. 개발 환경 설정에서는 추가 기능(extras)을 설치하고, CI 품질 테스트 작업에서도 추가 기능을 설치했습니다. 오직 release 작업만 순수하게(bare) 설치되었습니다. 즉, 실제 사용자를 대표한다고 보장되는 — 깨끗하고 추가 기능이 없는 — 환경에서 이 버그가 포착된 것입니다.
"선택적(optional)"이라는 것이 의미해야 하는 것
해결책은 작고, 저는 이제 이것을 규칙으로 간주합니다:
def _verify_signature(public: Any, alg: str, signing_input: bytes, signature: bytes) -> bool:
from cryptography.exceptions import InvalidSignature # lazy
from cryptography.hazmat.primitives import hashes
...
임포트 구문을 실제로 사용하는 단 하나의 함수 — 즉 검증 경로(verification path)에 안으로 이동시켰습니다. 이 경로는 어차피 추가 기능이 존재해야만 작동하는 경로이기도 합니다. 그 위쪽의 모든 것 — 모듈 로드, CLI 시작, 어댑터 임포트, 전체 비서명 영역(non-signing surface) — 은 더 이상 cryptography가 존재하는지 여부에 신경 쓰지 않습니다.
그리고 증거를 제시합니다. 이전에 임포트에 대해 해보지 않았던 테스트입니다: 모듈을 **차단(blocks)**하는 테스트를 먼저 실행한 다음, CLI를 임포트하는 것입니다:
class Blocker:
def find_spec(self, name, path=None, target=None):
if name == "cryptography" or name.startswith("cryptography."):
...
이것은 cryptography가 임포트 불가능하도록 만드는 메타 경로 후크(meta-path hook)이며, 이를 통해 CLI를 테스트합니다. 만약 누군가 이 임포트를 다시 모듈 상단으로 옮긴다면, 이 테스트는 다음 릴리스에서 문제가 생기는 대신 CI 환경에서 빨간색 경고를 띄우게 됩니다. A2A 카드 테스트(총 26개)는 cryptography가 존재해도 여전히 통과하므로, 기능은 단 1인치도 이동하지 않았습니다.
팁: 추가 의존성(extra)은 패키지가 그것 없이도 작동할 때만 선택 사항입니다. 그리고 '작동한다'는 것은 진실인 곳에서 확인됩니다. 즉, 엔트리 포인트를 가져오는 깨끗한 no-extras 설치 환경에서 말이죠.
내 테스트 스위트가 이것을 잡아내지 못한 이유
이것은 제가 나중에 앉아서 고민했던 부분입니다. 패키징을 보호하는 이 스위트는 견고합니다. 여러 표면(surface)에 걸쳐 하나의 버전을 강제하고, SBOM(Software Bill of Materials)을 검증하며, 휠(wheel) 내용을 확인합니다. 하지만 모든 테스트 환경은 추가 의존성(extras)을 설치합니다. 선택적 의존성이 누락된 버그는 해당 의존성을 가진 어떤 환경에서도 보이지 않습니다. 그리고 그 환경은 사용자가 받는 환경을 제외하고는 모두 그렇습니다.
이러한 종류의 버그가 나타날 수 있는 곳은 단 두 군데입니다:
- 릴리스 파이프라인에서의 깨끗한 설치 — 이것이
verify-release가 하는 일이며, 왜 그것이 사후 배포의 작은 편의 기능(nicety)이 아니라 _전제 조건(precondition)_인지에 대한 이유입니다. - 배포 후 레지스트리에서 새롭게 가상 환경을 만들어 설치하는 확인 과정 — 이것은 사람이 직접 수행하는 확인 과정으로, 한 릴리스가 지난 후에 파이프라인이 잡아낼 수 없었던 버전 배너 버그를 포착했습니다 (CLI는 정상적으로 가져왔지만, 잘못된 숫자만 출력했을 뿐입니다).
이 두 가지 검사 사이에서는 이러한 종류의 버그가 존재할 곳이 없습니다. 이것이 제가 이 글에서 얻어내는 실제 아키텍처적 교훈입니다: 환경이 당신에게 거짓말하는 것을 멈추는 경계 지점에 게이트를 설치해야 합니다.
일어날 수 있었던 결말
verify 단계를 거치지 않고 재생해 봅시다: 태그 푸시, 워크플로우 빌드, 서명, 배포. v0.2.0은 시작할 수 없는 CLI를 가진 채 PyPI로 갑니다. 모든 깨끗한 설치 — 즉 대다수의 경우 — 는 서명되고, SBOM이 생성되며, 출처가 증명된 휠을 가지고 있지만 작동하지 않는 도구를 받게 됩니다. 공급망 전반의 엄격함은 온전하지만, 가져오기(import) 단계에서 죽는 패키지를 배포하는 것입니다.
대신: 실패한 실행, 두 줄짜리 수정(PR #495), 재태그, 그리고 8개의 자산(wheel, sdist, SBOM, 체크섬)을 가진 릴리스 — 각 자산은 Sigstore 번들을 가지고 있습니다 — 이 모든 것이 실제로 작동합니다. v0.2.1도 같은 방식으로 배포되었는데, 첫 시도에 성공한 이유는 이미 이러한 종류의 버그가 사라졌기 때문입니다.
당신의 릴리스 파이프라인에 던져야 할 질문은 이것입니다. publish 전에 실행되는 마지막 것은 무엇이며, 깨진 임포트(broken import)를 잡아낼 수 있을까요? 만약 답이 '아무것도 아니다, 빌드를 신뢰한다'라면 — 당신의 다음 릴리스는 이미 예정되어 있지만, 그것이 무엇에 관한 것인지는 모릅니다.
참고 자료 (References)
- Release: agentwatch v0.2.1 on GitHub · on PyPI
- The fix: PR #495 — lazy
cryptographyimport · the release workflow ("verify-release가 확인할 수 없는 아티팩트를 게시하는 것을 거부함") - The no-extras import proof:
cryptography를 차단하고 CLI를 임포트한 다음 실행되는sys.meta_path훅; 이 패턴은 테스트로 커밋할 가치가 있습니다. - v0.2.0 field test report
- Suite: agentsec-ecosystem — 에이전트 보안 도구 전체 모음
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기