자체 빌드 이미지 프레임워크를 security-images 레포로 이관하고 카탈로그 쪽을 정리한다
images/·scripts/build/build-hardened-image.sh·suggest-go-upgrades.py·
build-image.yml·.claude/image-authoring.md·이미지 ADR(0001·0002·0004)을 삭제했다 —
전부 별도 public 레포 security-images 로 이미 이관됐다.
카탈로그 쪽에는 "무엇을 배포 중인가"를 아는 부분만 남긴다:
- catalog/image-map/<image>.env — 옛 catalog.env 의 카탈로그 레이아웃 정보만 뗀 것
- scripts/build/check-rebuild-needed.py — 드리프트 탐지(A 파트)만 남기고 핀 판단
(B 파트: pin_changes/apply_changes/parse_module_specs)은 제거
- scripts/build/apply-published-tags.py(신규) — security-images 의 published.json
을 읽어 카탈로그 values 를 패치
- .github/workflows/{self-build-drift-check,catalog-tag-update}.yml(신규) — 각각
드리프트 스캔+트리거, 발행 태그 반영
effective_severity 를 cve-gate.py 로 옮겼다 — check-rebuild-needed.py 가 핀 도구를
거치지 않고 게이트를 직접 로드하게 하기 위한 선행 작업이다.
두 레포의 계약은 published.json 스키마 하나뿐이다 — security-images 는 이 카탈로그를
모른다(단방향 의존). 이관 배경·결합점 전체는
doc/migrations/self-build-images-to-security-images.md.
부수 수정: 자체 빌드 이미지를 참조하는 차트 values/README 의 죽은 링크(images/**,
doc/decisions/000{1,2,4}, .claude/image-authoring.md)를 security-images 레포를
가리키는 서술로 교체. deploy-test 스크립트·CUSTOM-README 의 개인 Docker Hub 계정
(docker.io/wbsong111) 을 docker.io/paasup 로 교체.
pitfalls.md 의 "스캐너 결과를 그대로 믿지 말 것" 절은 sbom-cve-gate skill 이 차트
축 설명에 실제로 참조하고 있어 남겼다 — "이미지 태그의 베이스 OS" 절만 제거했다
(다른 참조 없음, security-images 문서로 이관 완료).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,25 +4,21 @@
|
||||
왜 필요한가
|
||||
-----------
|
||||
자체 빌드 이미지는 **소스를 안 바꿔도 시간이 지나면 게이트를 벗어난다.** 새 CVE 가 공개되고,
|
||||
베이스 OS 패키지가 갱신되고, 툴체인 패치가 나오는 동안 우리가 push 해 둔 이미지는 그대로다.
|
||||
2026-08-19 에 etcd·cloudnative-pg 가 이 상태로 차단됐는데 **문서 전용 PR 이 우연히
|
||||
`images/**` 를 건드려 검증 빌드가 돌면서야** 발견됐다. 의도적으로 보는 장치가 없었다.
|
||||
베이스 OS 패키지가 갱신되고, 툴체인 패치가 나오는 동안 우리가 참조해 둔 태그는 그대로다.
|
||||
의도적으로 보는 장치가 없으면 문서 전용 PR 이 우연히 검증 빌드를 돌릴 때만 발견된다.
|
||||
|
||||
`build-image.yml` 이 주간으로 이것을 돌려 재빌드까지 한다.
|
||||
`self-build-drift-check.yml` 이 주간으로 이것을 돌린다.
|
||||
|
||||
무엇이 "재빌드로 고쳐지는가" 인가 — 세 경우 모두 그렇다
|
||||
-----------------------------------------------------
|
||||
처음에는 "핀이 뒤처진 경우" 만 트리거로 잡았는데 **실측에서 그게 틀렸다.** 배포 중인 이미지
|
||||
8개를 스캔했더니 차단이 있는 5개 중 핀 변경이 필요한 것은 하나도 없었다.
|
||||
무엇이 "재빌드로 고쳐지는가" 인가
|
||||
--------------------------------
|
||||
"핀이 뒤처진 경우" 만으로는 부족하다. 재빌드로 고쳐지는 경우는 셋이다.
|
||||
|
||||
1. 핀이 뒤처졌다 → 핀을 올리고 빌드 (GO_BUILDER_TAG 등)
|
||||
1. 핀이 뒤처졌다 → 핀을 올리고 빌드
|
||||
2. 핀은 맞는데 이미지가 그 핀으로 안 빌드됐다 → 그냥 빌드
|
||||
(핀만 고친 PR 이 머지되고 재빌드가 아직 안 된 상태)
|
||||
3. 베이스 OS 패키지가 뒤처졌다 → 그냥 빌드
|
||||
(재빌드하면 zypper 가 최신을 깐다 — 핀과 무관하다)
|
||||
3. 베이스 OS 패키지가 뒤처졌다 → 그냥 빌드 (재빌드하면 zypper 가 최신을 깐다)
|
||||
|
||||
그래서 트리거는 **"수정 버전이 있는 차단 CVE 가 있는가"** 다. 핀 변경은 필요하면 함께 적용하는
|
||||
부수 작업이고, 트리거 조건이 아니다.
|
||||
그래서 트리거는 **"수정 버전이 있는 차단 CVE 가 있는가"** 다. 이 스크립트는 그 판정만
|
||||
한다 — 핀을 무엇으로 올려야 하는지는 판단하지 않는다(아래 "레포 분리" 참고).
|
||||
|
||||
수정 버전이 없는 차단(`affected`·`will_not_fix`)은 재빌드해도 그대로이므로 트리거가 아니다 —
|
||||
`no-fix` 로 따로 보고해 사람이 다른 레버(태그 교체·예외 승인)를 판단하게 한다.
|
||||
@@ -33,45 +29,25 @@
|
||||
`scripts/pipeline/cve-gate.py` 의 함수를 그대로 가져다 쓴다. 규칙이 두 곳으로 갈리면
|
||||
"게이트는 PASS 인데 재빌드하라고 한다" 가 나온다.
|
||||
|
||||
핀 산출은 `suggest-go-upgrades.py` 를 재사용한다(그것이 다시 게이트의 심각도 규칙을 쓴다).
|
||||
|
||||
기준은 카탈로그가 실제로 가리키는 이미지다
|
||||
----------------------------------------
|
||||
images/<image>/catalog.env → 카탈로그 values 의 현재 ref
|
||||
→ 그 ref 의 스캔 리포트
|
||||
↕
|
||||
<variant>.build.env 의 핀
|
||||
catalog/image-map/<image>.env → 카탈로그 values 의 현재 ref
|
||||
→ 그 ref 의 스캔 리포트
|
||||
|
||||
재빌드해 보지 않고도 "지금 배포 중인 것이 규정을 벗어났는가" 를 답한다.
|
||||
|
||||
레포 분리 시 이 파일은 둘로 갈린다
|
||||
--------------------------------
|
||||
커스텀 이미지가 별도 레포로 분리될 예정이고(`.claude/image-authoring.md` 참고), 그때 이
|
||||
스크립트는 **카탈로그를 읽는 쪽과 build.env 를 읽는 쪽이 서로 다른 레포에 놓인다.** 절단면은
|
||||
이미 함수 경계로 나뉘어 있으니 그대로 쓴다:
|
||||
|
||||
A (카탈로그 레포) resolve_current_ref + blocking_cves
|
||||
→ "이 이미지가 차단됐는가". 카탈로그 values 와 스캔 리포트만 쓴다.
|
||||
카탈로그가 무엇을 배포하는지는 카탈로그가 안다.
|
||||
|
||||
B (이미지 레포) pin_changes + apply_changes
|
||||
→ "핀을 올려야 하는가". build.env 만 쓴다.
|
||||
어떻게 만드는지는 이미지 레포가 안다.
|
||||
|
||||
check_image 둘을 엮는 오케스트레이션 — 분리 시 여기가 갈라진다.
|
||||
A 가 탐지해 B 에 빌드를 요청하고, B 가 핀을 판단한다.
|
||||
|
||||
이 주석의 목적은 분리 작업을 **고고학이 아니라 기계적 작업**으로 만드는 것이다. 새 함수를
|
||||
추가할 때 어느 쪽에 속하는지 함께 적는다.
|
||||
레포 분리 이후 — 이 스크립트는 절반이다
|
||||
----------------------------------------
|
||||
커스텀 이미지는 security-images(별도 레포)로 나갔다. "무엇을 배포 중인가"는 이 카탈로그가
|
||||
알고, "그 이미지를 어떻게 만드는가"(핀 판단·재빌드)는 security-images 가 안다. 이 스크립트는
|
||||
**탐지만** 한다 — 재빌드가 필요하다고 판단되면 `self-build-drift-check.yml` 이
|
||||
security-images 의 `build-image.yml` 을 `workflow_dispatch` 로 부른다. 핀을 무엇으로
|
||||
올릴지는 그 레포의 `suggest-go-upgrades.py --apply` 가 한다.
|
||||
|
||||
사용
|
||||
----
|
||||
python3 scripts/build/check-rebuild-needed.py --list-refs # 무엇을 스캔해야 하나
|
||||
python3 scripts/build/check-rebuild-needed.py --reports <디렉토리>
|
||||
python3 scripts/build/check-rebuild-needed.py --reports ... --image etcd --apply
|
||||
|
||||
`--apply` 는 핀 편집만 대신한다. 커밋·빌드·검증은 사람이 한다 — 핀은 "우리가 무엇을
|
||||
검증했는지의 기록" 이므로 자동 커밋하지 않는다.
|
||||
|
||||
종료 코드
|
||||
0 정상 (재빌드 대상이 있어도 0 — --fail-on-drift 를 주면 1)
|
||||
@@ -88,7 +64,7 @@ import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.normpath(os.path.join(HERE, "..", ".."))
|
||||
IMAGES_DIR = os.path.join(ROOT, "images")
|
||||
IMAGE_MAP_DIR = os.path.join(ROOT, "catalog", "image-map")
|
||||
DEFAULT_EXCEPTIONS = os.path.join(ROOT, "doc", "cve-exceptions.json")
|
||||
|
||||
# 카탈로그 values 는 이 순서로 찾는다. 자체 빌드 태그는 custom-values.yaml 이 기준이고
|
||||
@@ -103,9 +79,8 @@ def load(path, name):
|
||||
return mod
|
||||
|
||||
|
||||
SUGGEST = load(os.path.join(HERE, "suggest-go-upgrades.py"), "suggest_go_upgrades")
|
||||
PATCH = load(os.path.join(HERE, "patch-catalog-tag.py"), "patch_catalog_tag")
|
||||
GATE = SUGGEST.load_gate()
|
||||
GATE = load(os.path.join(HERE, "..", "pipeline", "cve-gate.py"), "cve_gate")
|
||||
|
||||
|
||||
def read_env(path):
|
||||
@@ -125,36 +100,18 @@ def read_env(path):
|
||||
|
||||
|
||||
def report_path(reports_dir, image_ref):
|
||||
"""이미지 ref → 리포트 파일 경로. scan-sbom.sh 가 `tr '/:@' '___'` 로 만든다."""
|
||||
"""이미지 ref → 리포트 파일 경로. `trivy image <ref>` 스캔 결과를
|
||||
`tr '/:@' '___'` 로 만든 파일명으로 저장했다고 가정한다."""
|
||||
return os.path.join(reports_dir, re.sub(r"[/:@]", "_", image_ref) + ".json")
|
||||
|
||||
|
||||
def numeric_prefix(v):
|
||||
"""'1.26.5-trixie' → ((1,26,5), '-trixie'). 접미사는 그대로 유지해야 한다."""
|
||||
m = re.match(r"^(\d+(?:\.\d+)*)(.*)$", v or "")
|
||||
if not m:
|
||||
return (0,), ""
|
||||
return tuple(int(x) for x in m.group(1).split(".")), m.group(2)
|
||||
|
||||
|
||||
def parse_module_specs(raw):
|
||||
"""'mod@v1.2.3 mod2@v0.4.0' → {mod: (1,2,3)}"""
|
||||
out = {}
|
||||
for tok in (raw or "").split():
|
||||
if "@" not in tok:
|
||||
continue
|
||||
name, ver = tok.rsplit("@", 1)
|
||||
out[name] = SUGGEST.version_tuple(ver)
|
||||
return out
|
||||
|
||||
|
||||
def resolve_current_ref(catalog_env):
|
||||
def resolve_current_ref(chart_env):
|
||||
"""카탈로그 values 에서 현재 이미지 ref 를 읽는다. (ref, 읽은 파일) 또는 (None, 이유)."""
|
||||
chart_dirs = (catalog_env.get("CHART_DIRS") or "").split()
|
||||
style = catalog_env.get("TAG_STYLE") or ""
|
||||
block = catalog_env.get("TAG_BLOCK") or "image"
|
||||
chart_dirs = (chart_env.get("CHART_DIRS") or "").split()
|
||||
style = chart_env.get("TAG_STYLE") or ""
|
||||
block = chart_env.get("TAG_BLOCK") or "image"
|
||||
if not chart_dirs or style not in ("imageName", "split"):
|
||||
return None, "catalog.env 에 CHART_DIRS/TAG_STYLE 이 없다"
|
||||
return None, "catalog/image-map/<image>.env 에 CHART_DIRS/TAG_STYLE 이 없다"
|
||||
for d in chart_dirs:
|
||||
for fname in VALUES_CANDIDATES:
|
||||
p = os.path.join(ROOT, d, fname)
|
||||
@@ -179,7 +136,10 @@ def blocking_cves(report, image_ref, rank, floor, exceptions):
|
||||
cve = v.get("VulnerabilityID")
|
||||
if not cve:
|
||||
continue
|
||||
eff = SUGGEST.effective_severity(v, GATE, rank)
|
||||
eff = GATE.effective_severity(
|
||||
v.get("Severity"),
|
||||
GATE.nvd_severity(((v.get("CVSS") or {}).get("nvd") or {}).get("V3Score")),
|
||||
)
|
||||
if rank.get(eff, 0) < floor:
|
||||
continue
|
||||
if any(GATE.exception_applies(e, cve, image_ref) for e in exceptions):
|
||||
@@ -193,68 +153,11 @@ def blocking_cves(report, image_ref, rank, floor, exceptions):
|
||||
return out
|
||||
|
||||
|
||||
def pin_changes(paths, pins, rank, floor):
|
||||
"""핀을 올려야 하는 항목. 재빌드 트리거가 아니라 **부수 작업**이다."""
|
||||
changes, notes = [], []
|
||||
|
||||
std_alts = SUGGEST.collect_stdlib(paths, GATE, rank, floor)
|
||||
if std_alts:
|
||||
rows = SUGGEST.builder_candidates(std_alts)
|
||||
cur = pins.get("GO_BUILDER_TAG", "")
|
||||
if not rows:
|
||||
notes.append(f"stdlib 차단 {len(std_alts)}건 — 정식 릴리스 대안이 없다"
|
||||
"(프리릴리스 툴체인이 필요할 수 있다)")
|
||||
elif not cur:
|
||||
notes.append(f"stdlib 차단 {len(std_alts)}건 — 이 이미지에 GO_BUILDER_TAG 가 없다")
|
||||
else:
|
||||
cur_nums, suffix = numeric_prefix(cur)
|
||||
# 현재와 같은 마이너의 후보가 있으면 그것을 쓴다(마이너 점프를 강요하지 않는다).
|
||||
same_minor = [r for r in rows if r[0] == cur_nums[:2]]
|
||||
(a, b), c = (same_minor or rows)[-1]
|
||||
if cur_nums[:3] < (a, b, c):
|
||||
changes.append({"key": "GO_BUILDER_TAG", "current": cur,
|
||||
"required": f"{a}.{b}.{c}{suffix}",
|
||||
"reason": f"stdlib 차단 {len(std_alts)}건", "appliable": True})
|
||||
|
||||
mods = SUGGEST.collect_modules(paths, GATE, rank, floor)
|
||||
if mods:
|
||||
cur_raw = pins.get("GO_MODULE_UPGRADES")
|
||||
cur_mods = parse_module_specs(cur_raw)
|
||||
behind = {n: e["fixed"] for n, e in mods.items()
|
||||
if cur_mods.get(n) is None or cur_mods[n] < SUGGEST.version_tuple(e["fixed"])}
|
||||
if behind:
|
||||
listing = " ".join(f"{n}@v{behind[n]}" for n in sorted(behind))
|
||||
if cur_raw is None:
|
||||
# 이 이미지는 GO_MODULE_UPGRADES 를 소비하지 않는다(etcd 의 XTEXT_FIX_VERSION
|
||||
# 처럼 이미지별 ARG). 값을 넣어도 무효이므로 사람이 판단하게 한다.
|
||||
notes.append(f"모듈 {len(behind)}건 뒤처짐({listing}) — 이 이미지는 "
|
||||
"GO_MODULE_UPGRADES 를 쓰지 않는다. 이미지별 FIX_VERSION 수동 확인")
|
||||
else:
|
||||
merged = dict(cur_mods)
|
||||
merged.update({n: SUGGEST.version_tuple(v) for n, v in behind.items()})
|
||||
spec = " ".join(f"{n}@v{'.'.join(map(str, merged[n]))}" for n in sorted(merged))
|
||||
changes.append({"key": "GO_MODULE_UPGRADES", "current": cur_raw,
|
||||
"required": spec,
|
||||
"reason": f"모듈 {len(behind)}건 뒤처짐", "appliable": True})
|
||||
return changes, notes
|
||||
|
||||
|
||||
def check_image(image, reports_dir, rank, floor, exceptions):
|
||||
idir = os.path.join(IMAGES_DIR, image)
|
||||
res = {"image": image, "status": "ok", "notes": [], "changes": [],
|
||||
"blocking": 0, "fixable": 0}
|
||||
res = {"image": image, "status": "ok", "notes": [], "blocking": 0, "fixable": 0}
|
||||
|
||||
catalog_env = read_env(os.path.join(idir, "catalog.env"))
|
||||
variant = catalog_env.get("DEFAULT_BASE_OS") or ""
|
||||
if not variant:
|
||||
res["status"] = "skip"
|
||||
res["notes"].append("catalog.env 에 DEFAULT_BASE_OS 가 없다")
|
||||
return res
|
||||
build_env_path = os.path.join(idir, f"{variant}.build.env")
|
||||
res["build_env"] = os.path.relpath(build_env_path, ROOT)
|
||||
pins = read_env(build_env_path)
|
||||
|
||||
ref, where = resolve_current_ref(catalog_env)
|
||||
chart_env = read_env(os.path.join(IMAGE_MAP_DIR, f"{image}.env"))
|
||||
ref, where = resolve_current_ref(chart_env)
|
||||
if not ref:
|
||||
res["status"] = "skip"
|
||||
res["notes"].append(where)
|
||||
@@ -279,8 +182,6 @@ def check_image(image, reports_dir, rank, floor, exceptions):
|
||||
if not cves:
|
||||
return res # ok
|
||||
|
||||
res["changes"], res["notes"] = pin_changes([rp], pins, rank, floor)
|
||||
|
||||
if fixable:
|
||||
res["status"] = "rebuild"
|
||||
else:
|
||||
@@ -290,26 +191,6 @@ def check_image(image, reports_dir, rank, floor, exceptions):
|
||||
return res
|
||||
|
||||
|
||||
def apply_changes(build_env_path, changes):
|
||||
"""build.env 의 KEY=value 줄만 제자리 치환한다(주석·순서 보존)."""
|
||||
p = pathlib.Path(build_env_path)
|
||||
text = p.read_text()
|
||||
applied = []
|
||||
for ch in changes:
|
||||
if not ch.get("appliable"):
|
||||
continue
|
||||
key = ch["key"]
|
||||
pattern = re.compile(rf'^({re.escape(key)}=)(")?.*?(")?$', re.MULTILINE)
|
||||
if not pattern.search(text):
|
||||
continue
|
||||
quote = '"' if " " in ch["required"] else ""
|
||||
text = pattern.sub(lambda m: f'{m.group(1)}{quote}{ch["required"]}{quote}', text, count=1)
|
||||
applied.append(key)
|
||||
if applied:
|
||||
p.write_text(text)
|
||||
return applied
|
||||
|
||||
|
||||
def render_md(results):
|
||||
rebuild = [r for r in results if r["status"] == "rebuild"]
|
||||
nofix = [r for r in results if r["status"] == "no-fix"]
|
||||
@@ -319,16 +200,14 @@ def render_md(results):
|
||||
if not rebuild and not nofix:
|
||||
out.append(f"배포 중인 자체 빌드 이미지 {len(ok)}개에 차단 CVE 가 없다.")
|
||||
if rebuild:
|
||||
out += [f"**{len(rebuild)}개 이미지가 재빌드로 고쳐진다.** 차단 CVE 에 수정 버전이 있다 "
|
||||
"— 재빌드하면 베이스 패키지·툴체인이 최신으로 반영된다.", "",
|
||||
"| 이미지 | 차단 | 수정 가능 | 패키지 | 함께 올릴 핀 |",
|
||||
"|---|---:|---:|---|---|"]
|
||||
out += [f"**{len(rebuild)}개 이미지가 재빌드 대상이다.** 차단 CVE 에 수정 버전이 있다 "
|
||||
"— security-images 레포에서 재빌드하면 해소될 가능성이 높다. 핀 조정이 "
|
||||
"필요한지는 그 레포의 `suggest-go-upgrades.py` 가 판단한다.", "",
|
||||
"| 이미지 | 현재 ref | 차단 | 수정 가능 | 패키지 |",
|
||||
"|---|---|---:|---:|---|"]
|
||||
for r in rebuild:
|
||||
pins = "<br>".join(
|
||||
f"`{c['key']}` `{c['current']}` → **`{c['required']}`**" for c in r["changes"]
|
||||
) or "없음 (핀은 이미 맞다 — 빌드만 안 됐다)"
|
||||
pkgs = ", ".join(f"`{p}`" for p in r["packages"][:4])
|
||||
out.append(f"| `{r['image']}` | {r['blocking']} | {r['fixable']} | {pkgs} | {pins} |")
|
||||
out.append(f"| `{r['image']}` | `{r['ref']}` | {r['blocking']} | {r['fixable']} | {pkgs} |")
|
||||
if nofix:
|
||||
out += ["", f"**{len(nofix)}개 이미지는 재빌드로 해소되지 않는다.** 다른 레버가 필요하다.",
|
||||
"", "| 이미지 | 차단 | 패키지 |", "|---|---:|---|"]
|
||||
@@ -349,14 +228,15 @@ def render_md(results):
|
||||
out += ["", "</details>"]
|
||||
|
||||
out += ["", "> 판정 기준은 게이트와 같다 — 실효 등급 `max(벤더, NVD)` · 승인 예외 적용 · "
|
||||
"기본 HIGH 이상. 핀 제안은 **제안**이고 채택은 사람이 커밋한다.", ""]
|
||||
"기본 HIGH 이상. 재빌드는 security-images 레포의 `build-image.yml` 을 "
|
||||
"`workflow_dispatch` 로 부른다.", ""]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="배포 중인 자체 빌드 이미지의 재빌드 필요 여부 판정")
|
||||
ap.add_argument("--reports", help="trivy 리포트(JSON) 디렉토리 (--list-refs 면 불필요)")
|
||||
ap.add_argument("--image", default="", help="이 이미지만 판정 (images/<image>)")
|
||||
ap.add_argument("--image", default="", help="이 이미지만 판정 (catalog/image-map/<image>.env)")
|
||||
ap.add_argument("--exceptions", default=DEFAULT_EXCEPTIONS,
|
||||
help="승인 예외 목록 (기본 doc/cve-exceptions.json)")
|
||||
ap.add_argument("--min-severity", default="HIGH",
|
||||
@@ -368,14 +248,12 @@ def main():
|
||||
"새지 않게 한다")
|
||||
ap.add_argument("--summary-md", metavar="FILE", help="마크다운 요약을 이 파일에 쓴다")
|
||||
ap.add_argument("--json-out", metavar="FILE", help="결과 JSON 을 이 파일에 쓴다")
|
||||
ap.add_argument("--apply", action="store_true",
|
||||
help="핀 제안을 build.env 에 제자리 반영한다 (커밋·빌드는 사람이 한다)")
|
||||
ap.add_argument("--fail-on-drift", action="store_true",
|
||||
help="재빌드 대상이 있으면 1 로 종료 (기본은 보고만 하고 0)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.isdir(IMAGES_DIR):
|
||||
print(f"::error::images/ 를 찾지 못했다: {IMAGES_DIR}", file=sys.stderr)
|
||||
if not os.path.isdir(IMAGE_MAP_DIR):
|
||||
print(f"::error::catalog/image-map/ 를 찾지 못했다: {IMAGE_MAP_DIR}", file=sys.stderr)
|
||||
return 2
|
||||
if not args.list_refs and not args.reports:
|
||||
print("::error::--reports 가 필요하다", file=sys.stderr)
|
||||
@@ -384,17 +262,16 @@ def main():
|
||||
print(f"::error::리포트 디렉토리가 없다: {args.reports}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
names = sorted(d for d in os.listdir(IMAGES_DIR)
|
||||
if os.path.isdir(os.path.join(IMAGES_DIR, d)))
|
||||
names = sorted(f[:-4] for f in os.listdir(IMAGE_MAP_DIR) if f.endswith(".env"))
|
||||
if args.image:
|
||||
names = [n for n in names if n == args.image]
|
||||
if not names:
|
||||
print(f"::error::images/{args.image} 가 없다", file=sys.stderr)
|
||||
print(f"::error::catalog/image-map/{args.image}.env 가 없다", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.list_refs:
|
||||
for n in names:
|
||||
ref, why = resolve_current_ref(read_env(os.path.join(IMAGES_DIR, n, "catalog.env")))
|
||||
ref, why = resolve_current_ref(read_env(os.path.join(IMAGE_MAP_DIR, f"{n}.env")))
|
||||
if not ref:
|
||||
print(f"::warning::{n}: ref 를 읽지 못했다 — {why}", file=sys.stderr)
|
||||
continue
|
||||
@@ -414,9 +291,8 @@ def main():
|
||||
|
||||
for r in results:
|
||||
if r["status"] == "rebuild":
|
||||
pins = "; ".join(f"{c['key']} {c['current']} → {c['required']}" for c in r["changes"])
|
||||
print(f"::warning::{r['image']}: 재빌드 필요 — 차단 {r['blocking']}건"
|
||||
f"(수정 가능 {r['fixable']})" + (f" / {pins}" if pins else ""))
|
||||
print(f"::warning::{r['image']}: 재빌드 대상 — 차단 {r['blocking']}건"
|
||||
f"(수정 가능 {r['fixable']})")
|
||||
elif r["status"] == "no-fix":
|
||||
print(f"::warning::{r['image']}: 차단 {r['blocking']}건이나 수정 버전 없음 — "
|
||||
"재빌드로 해소되지 않는다")
|
||||
@@ -434,13 +310,6 @@ def main():
|
||||
pathlib.Path(args.json_out).write_text(
|
||||
json.dumps(results, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
if args.apply:
|
||||
for r in results:
|
||||
if r["changes"]:
|
||||
applied = apply_changes(os.path.join(ROOT, r["build_env"]), r["changes"])
|
||||
if applied:
|
||||
print(f"반영: {r['build_env']} — {', '.join(applied)}")
|
||||
|
||||
return 1 if ([r for r in results if r["status"] == "rebuild"] and args.fail_on_drift) else 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user