본문 바로가기
C.W.K.
Stream
Lesson 02 of 04 · published

출생증명서를 단 복사본

~13 min · provenance, tooling, deploy, implementation

Level 0흩어진 부품
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

어디서 왔는지 스스로 말하는 파일

복사본의 제일 중요한 성질 하나는, 여섯 달 뒤에 일해본 적도 없는 저장소에서 버그를 쫓다가 이 파일을 연 사람이 즉시 세 가지를 안다는 거야. 이 파일은 여기서 쓰인 게 아니다, 쓰이는 자리는 여기다, 바꾸려면 이걸 돌려라.

그래서 배달된 파일마다 첫 줄이 붙어. 키트 원본 경로, 본문이 나온 키트 커밋, 그리고 지시.

GENERATED FROM the-kit/kit/python/kit_pippa.py @ <kit-sha> — DO NOT EDIT HERE. Edit the kit and run the sync script.

주석 문법은 확장자마다 골라져. 작은 디테일인데 결과가 커. 머리말이 Python 에서도 TypeScript 에서도 JavaScript 에서도 CSS 에서도 Swift 에서도 주석이거든. 그래서 배달 기계 하나가 가족의 모든 언어를 섬기고, 어느 언어도 빌드 때 서문을 떼낼 필요가 없어.

머리말을 비교하면 안 되는 이유

머리말은 그 파일이 배달될 때 최신이던 키트 커밋을 적어. 디스크 위의 파일은 요동치지 않아. 재배달은 본문이 실제로 바뀐 복사본만 다시 쓰거든. 그런데 기대되는 머리말은 지금 이 순간의 키트 HEAD 로 만들어져. 그러니까 키트가 뭐든 커밋하는 순간, 이미 배달된 모든 파일은 비교가 계산해낼 SHA 보다 오래된 SHA 를 달고 있게 돼. 그 비교에 머리말을 넣으면 키트 커밋마다 모든 소비자가 영원히 어긋남을 보고해. 아무도 안 건드린 파일에 대해서.

계속 울리는 검사는 꺼지는 검사야. 그래서 비교는 첫 줄에 표식이 있으면 떼고 그 뒤만 비교해. 그러면 어긋남은 정확히 한 가지 뜻이 돼. 본문이 다르다. 그 정밀함이 아무도 원망 안 하면서 이 검사를 테스트에 물릴 수 있게 만들어.

바뀌는 메타데이터를 중요한 내용에서 떼놔. 안 그러면 검사가 늑대야 소리를 질러. 이건 복사본 얘기 훨씬 너머에서 나타나. 빌드 산출물의 생성 시각, 버전 파일의 커밋 해시, 헤더의 날짜. 휘발성 메타데이터가 박힌 내용을 비교하면 잡음이 나오고, 잡음은 정상으로 받아들여지고, 신호는 사라져. 휘발성인 쪽은 비교가 못 보는 자리에 둬.

어색한 경우: 증명서만 새로 찍기

알아둘 만한 주름이 하나 있어. 도구 버그처럼 보이는 종류거든. 어떤 때는 키트 변경을 커밋하기 전에 소비자 테스트가 통과해야 해. 그래서 배달을 먼저 돌리고, 그때 찍힌 머리말은 키트의 이전 커밋을 적게 돼. 본문은 맞고, 증명서만 한 커밋 낡은 거야.

도구는 이걸 좁고 명시적인 모드로 처리해. 키트가 커밋한 다음, 지정한 원본들에 대해서만 출처를 새로 찍으라는 지시로 배달을 다시 돌려. 그 머리말들만 다시 쓰고 나머진 안 건드려. 그래서 상관없는 복사본들은 자기 본문을 실제로 배달한 그 커밋을 그대로 갖고 있어. 그게 정직한 기록이야. 작은 기능이고, 대안 (머리말 전부 갈아엎기) 이 머리말이 존재하는 유일한 정보를 조용히 부숴버릴 거라서 있는 거야.

Code

머리말: 언어마다 쓰고, 비교 전에 떼기·python
HEADER_MARK = "GENERATED FROM the-kit"

# One deploy mechanism, every language in the family. The header has
# to be a COMMENT in the target language or the file will not parse.
COMMENT_STYLES = {
    ".py":    ("# ", ""),
    ".ts":    ("// ", ""),
    ".tsx":   ("// ", ""),
    ".js":    ("// ", ""),
    ".css":   ("/* ", " */"),   # CSS needs a closing delimiter
    ".swift": ("// ", ""),
}


def header_for(source_rel: str, target: Path, sha: str) -> str:
    prefix, suffix = COMMENT_STYLES.get(target.suffix, ("# ", ""))
    return (
        f"{prefix}{HEADER_MARK}/{source_rel} @ {sha} - DO NOT EDIT HERE. "
        f"Edit the kit and run the sync script.{suffix}\n"
    )


def strip_header(text: str) -> str:
    """Remove the provenance line before comparing bodies.

    Guarded on the MARKER, not on 'is line 1 a comment'. A kit file may
    legitimately open with a comment of its own; only a line carrying
    the marker is ours to drop.
    """
    lines = text.splitlines(keepends=True)
    if lines and HEADER_MARK in lines[0]:
        return "".join(lines[1:])
    return text


# Deploy: header + body. Compare: body only.
expected_body = source_text
expected_file = header_for(source_rel, target, kit_sha) + expected_body

actual = target.read_text()
is_drifted = strip_header(actual) != expected_body      # <- the body

# The header question is only meaningful INSIDE the body-matches arm.
# Asked on its own it is true whenever `is_drifted` is true, so it
# would not be cosmetic at all - it would just be drift, reported
# twice under two names.
needs_refresh = (not is_drifted) and actual != expected_file
#
# Two DIFFERENT questions, and the nesting is what keeps them apart.
# `is_drifted` fails a test suite. `needs_refresh` is cosmetic, and
# is only acted on when a deploy explicitly asks for it.

External links

Exercise

네가 다루는 프로젝트에서 생성된 파일을 하나 찾아. 스키마 바인딩, API 클라이언트, 락파일 근처 산출물 뭐든. 그게 어디서 왔고 어떻게 다시 만드는지 말하는지 확인해. 아니면 둘 다 담은 한 줄 머리말을 붙여. 그다음 그 프로젝트의 어떤 비교나 diff 리뷰나 테스트가 그 머리말 때문에 헷갈릴지 확인하고, 파일에서 빼지 말고 비교에서 빼.
Hint
재밌는 실패는 원본 리비전 대신 시각을 적은 머리말이야. 시각은 다시 만들 때마다 바뀌니까 아무것도 안 바뀌었는데도 파일이 요동치고, 리뷰어는 그 diff 를 건너뛰는 걸 배워. 출처가 존재하는 이유의 정반대지.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.