자체 빌드 이미지 프레임워크를 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:
@@ -1,432 +0,0 @@
|
||||
name: self-build-image
|
||||
|
||||
# 자체 빌드 이미지(images/<image>, 실행기 scripts/build/build-hardened-image.sh)의
|
||||
# 빌드·검증·스캔·게이트·카탈로그 반영을 자동화한다. security-catalog 의 동일 워크플로에서
|
||||
# 프레임워크를 포팅했고 이 레포 CI 에서 빌드→검증→게이트→push→카탈로그 브랜치 push 까지
|
||||
# 실제로 검증됐다(2026-08 기준). 대상 이미지 목록은 images/ 디렉토리가 단일 출처다.
|
||||
# 게이트(scripts/pipeline/cve-gate.py)가 상위 태그·베이스
|
||||
# OS 교체로 해소되지 않는 차단 CVE 를 찾으면 이 워크플로로 자체 빌드를 검토한다 —
|
||||
# 절차: .claude/image-authoring.md, 트리거·제약 요약: doc/sbom-pipeline.md 의 "자체 빌드" 절
|
||||
#
|
||||
# "이미지가 어느 차트의 어느 필드를 가리키는가" 는 images/<image>/catalog.env 가 선언한다
|
||||
# (CHART_DIRS·TAG_STYLE·TAG_BLOCK·DEFAULT_BASE_OS). 태그 표기 스타일이 두 가지다:
|
||||
# imageName 단일 필드 문자열 (`imageName: "repo:tag"`)
|
||||
# split registry/repository/tag 세 필드로 분리된 블록
|
||||
# 둘 다 scripts/build/patch-catalog-tag.py 하나로 다룬다(YAML 파서 없이 텍스트 치환만
|
||||
# 해서 기존 주석·포매팅을 보존한다 — 예상 패턴을 못 찾으면 조용히 넘어가지 않고 실패한다).
|
||||
#
|
||||
# 이 워크플로는 helm-catalog-sbom(sbom.yml)과 별도 파일이다. sbom.yml 은
|
||||
# vars.SBOM_PIPELINE_IMAGE 컨테이너 안에서 도는데 거기엔 docker/buildx 가 없다.
|
||||
# 빌드는 호스트 러너여야 한다.
|
||||
#
|
||||
# 트리거 3종이 같은 스텝(빌드→verify.sh→SBOM→scan-sbom.sh→cve-gate.py)을 돈다.
|
||||
# 차이는 대상 이미지를 어떻게 정하는지, 그리고 push·카탈로그 브랜치 push 여부뿐이다.
|
||||
#
|
||||
# pull_request(images/**) 변경된 images/<image>/ 디렉토리를 diff 로 자동 탐지해 그
|
||||
# 이미지들만 검증한다(push·카탈로그 브랜치 없음). 여러 이미지가
|
||||
# 한 PR 에서 바뀌면 각각 매트릭스로 병렬 실행된다.
|
||||
# workflow_dispatch `image` 입력으로 대상을 명시한다. 실제 빌드·push·카탈로그 태그
|
||||
# 갱신 브랜치 push 는 이 트리거로만 일어난다(사람이 수동 실행) —
|
||||
# push 입력이 false 면 검증만 한다.
|
||||
# schedule 재빌드 트리거. 배포 중인 이미지를 직접 스캔해
|
||||
# (scripts/build/check-rebuild-needed.py) **수정 버전이 있는
|
||||
# 차단 CVE** 가 있는 이미지를 찾아 빌드·검증·게이트만 돈다.
|
||||
# 핀이 뒤처졌으면 핀을 올린 브랜치를 push 하고 그 브랜치로
|
||||
# 빌드한다. 핀이 이미 맞아도 재빌드한다 — 그 핀으로 아직 안
|
||||
# 빌드됐거나 베이스 OS 패키지가 뒤처졌으면 재빌드가 해소한다
|
||||
# (실측: 차단 있는 4개 중 핀 변경 필요는 0개였다).
|
||||
# 레지스트리 push 와 카탈로그 반영은 하지 않는다 — 반영은 사람이
|
||||
# workflow_dispatch 로 한다.
|
||||
#
|
||||
# 카탈로그 PR 은 자동 생성하지 않는다 — GitHub Actions 는 GITHUB_TOKEN 으로 PR 을 만들 수
|
||||
# 없다는 조직 정책("Allow GitHub Actions to create and approve pull requests" 미허용,
|
||||
# 리포 설정으로 못 바꿈)에 실측으로 막혔다(2026-08-04, run 30882785612, GraphQL:
|
||||
# "GitHub Actions is not permitted to create or approve pull requests"). 브랜치·커밋·push
|
||||
# 까지만 워크플로가 하고, PR 오픈은 Job Summary 에 남는 compare 링크로 사람이 직접 연다
|
||||
# (사람의 gh/브라우저 인증은 이 제약을 받지 않는다).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: 'image=지정한 이미지 하나 | drift=재빌드가 필요한 이미지를 찾아 전부'
|
||||
required: false
|
||||
default: 'image'
|
||||
type: choice
|
||||
options: [image, drift]
|
||||
image:
|
||||
description: '빌드할 이미지 디렉토리명 (images/<image>/). mode=drift 면 비워둔다'
|
||||
required: false
|
||||
default: ''
|
||||
base_os:
|
||||
description: '빌드 변종 (images/<image>/<base_os>.build.env). 비우면 catalog.env 의 DEFAULT_BASE_OS 사용'
|
||||
required: false
|
||||
default: ''
|
||||
push:
|
||||
description: '레지스트리 push + 카탈로그 태그 갱신 브랜치 push 여부 (mode=drift·schedule 은 무조건 false)'
|
||||
required: false
|
||||
default: 'true'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'images/**'
|
||||
schedule:
|
||||
# sbom.yml 주간 전체 스캔(일요일 18:00 UTC) 다음날. 이 잡은 그 산출물에 의존하지 않고
|
||||
# 배포 중인 이미지를 직접 스캔한다 — 워크플로 간 아티팩트 결합을 만들지 않기 위함이다.
|
||||
- cron: '0 2 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
REGISTRY_HOST: docker.io/paasup
|
||||
|
||||
jobs:
|
||||
# --- 대상 이미지 결정 ---------------------------------------------------------
|
||||
# workflow_dispatch: inputs.image 하나. pull_request: images/<image>/ 아래 변경이 있는
|
||||
# 디렉토리를 전부 찾는다(여러 이미지가 한 PR 에서 바뀌면 각각 매트릭스로 돈다).
|
||||
discover:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
images: ${{ steps.list.outputs.images }}
|
||||
ref: ${{ steps.list.outputs.ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# 드리프트 경로에서만 필요하다. trivy 로 **배포 중인 이미지**를 직접 스캔한다
|
||||
# (docker 불필요 — trivy 가 자체 레지스트리 클라이언트로 pull 한다).
|
||||
- name: trivy 설치 (재빌드 판정용)
|
||||
if: github.event_name == 'schedule' || github.event.inputs.mode == 'drift'
|
||||
run: |
|
||||
curl -fsSL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
||||
| sh -s -- -b /usr/local/bin
|
||||
trivy --version | head -1
|
||||
|
||||
# 무엇을 스캔해야 하는지는 스크립트가 안다(catalog.env → 카탈로그 values → ref).
|
||||
# ref 해석 규칙이 워크플로로 새면 두 곳이 어긋난다.
|
||||
- name: 재빌드 판정 — 배포 중인 이미지 스캔
|
||||
id: drift
|
||||
if: github.event_name == 'schedule' || github.event.inputs.mode == 'drift'
|
||||
run: |
|
||||
set -uo pipefail
|
||||
mkdir -p /tmp/rebuild-reports
|
||||
python3 scripts/build/check-rebuild-needed.py --list-refs > /tmp/refs.tsv
|
||||
while IFS=$'\t' read -r img ref fname; do
|
||||
[ -n "${fname:-}" ] || continue
|
||||
echo "스캔: $ref"
|
||||
trivy image --quiet --scanners vuln \
|
||||
--severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL \
|
||||
--format json "$ref" > "/tmp/rebuild-reports/$fname" \
|
||||
|| { echo "::warning::$img: 스캔 실패 ($ref) — 이 이미지는 판정에서 빠진다"
|
||||
rm -f "/tmp/rebuild-reports/$fname"; }
|
||||
done < /tmp/refs.tsv
|
||||
|
||||
# --apply 는 핀이 뒤처진 경우에만 build.env 를 고친다. 핀이 이미 맞으면 아무것도
|
||||
# 건드리지 않고, 그래도 재빌드 대상이다(베이스 패키지가 갱신되므로).
|
||||
python3 scripts/build/check-rebuild-needed.py \
|
||||
--reports /tmp/rebuild-reports \
|
||||
--summary-md /tmp/rebuild.md \
|
||||
--json-out /tmp/rebuild.json \
|
||||
--apply
|
||||
cat /tmp/rebuild.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
python3 - <<'PY' >> "$GITHUB_OUTPUT"
|
||||
import json
|
||||
rows = json.load(open("/tmp/rebuild.json"))
|
||||
# 트리거는 "수정 버전이 있는 차단 CVE 가 있는가" 다. 핀 변경 여부가 아니다 —
|
||||
# 핀이 이미 맞아도(핀만 고친 PR 이 머지된 뒤) 이미지가 그 핀으로 안 빌드됐거나
|
||||
# 베이스 OS 패키지가 뒤처졌으면 재빌드가 해소한다. 실측으로 확인한 사실이다.
|
||||
print("images=" + json.dumps([r["image"] for r in rows if r["status"] == "rebuild"]))
|
||||
PY
|
||||
|
||||
# 핀이 바뀐 경우에만 브랜치가 필요하다. 핀 변경이 없으면 기본 ref 로 그냥 빌드한다.
|
||||
- name: 핀이 바뀌었으면 브랜치 push
|
||||
id: branch
|
||||
if: steps.drift.outputs.images != '' && steps.drift.outputs.images != '[]'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git diff --quiet -- images/; then
|
||||
echo "핀 변경 없음 — 브랜치를 만들지 않고 기본 ref 로 빌드한다"
|
||||
echo "핀은 이미 맞다 — 재빌드만으로 해소된다(베이스 패키지·미반영 핀)." \
|
||||
>> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
BRANCH="chore/pin-drift-$(date -u +%Y%m%d%H%M%S)"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$BRANCH"
|
||||
git add images/
|
||||
git diff --cached --stat
|
||||
git commit \
|
||||
-m "자체 빌드 이미지 버전 핀 갱신 (자동 감지)" \
|
||||
-m "배포 중인 이미지를 스캔해 핀이 뒤처진 것을 찾아 올렸다. 소스는 바꾸지 않았다 —" \
|
||||
-m "CVE 데이터가 갱신되면서 같은 핀이 차단 대상이 된 것이다. 산출은" \
|
||||
-m "scripts/build/check-rebuild-needed.py 가 했고 근거는 이 실행의 Job Summary 에 있다." \
|
||||
-m "이 브랜치로 빌드·검증·게이트까지 자동으로 돈다. 레지스트리 push 와 카탈로그" \
|
||||
-m "반영은 하지 않았다 — 사람이 확인 후 workflow_dispatch 로 한다." \
|
||||
-m "Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
||||
git push -u origin "$BRANCH"
|
||||
echo "ref=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo
|
||||
echo "핀을 올린 브랜치를 push 했다: \`$BRANCH\`"
|
||||
echo
|
||||
echo "아래 빌드가 **이 브랜치로** 돌아 핀이 실제로 해소하는지 확인한다."
|
||||
echo "게이트가 PASS 면 사람이 PR 을 열고, 반영은 \`workflow_dispatch\` 로 한다."
|
||||
echo
|
||||
echo "https://github.com/${{ github.repository }}/compare/main...${BRANCH}?expand=1"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: 대상 이미지 목록 산출
|
||||
id: list
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "ref=${{ steps.branch.outputs.ref }}" >> "$GITHUB_OUTPUT"
|
||||
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event.inputs.mode }}" = "drift" ]; then
|
||||
# 판정 결과가 곧 대상이다. 핀이 안 바뀌어 브랜치가 없어도 빌드한다
|
||||
# (기본 ref 로 재빌드하면 베이스 패키지·미반영 핀이 해소된다).
|
||||
json='${{ steps.drift.outputs.images }}'
|
||||
[ -n "$json" ] || json="[]"
|
||||
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
img="${{ github.event.inputs.image }}"
|
||||
[ -n "$img" ] || { echo "::error::image 입력이 비어있다 (mode=drift 가 아니면 필수)"; exit 1; }
|
||||
[ -d "images/$img" ] || { echo "::error::이미지 디렉토리 없음: images/$img"; exit 1; }
|
||||
json="[\"$img\"]"
|
||||
else
|
||||
changed="$(git diff --name-only \
|
||||
"${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" \
|
||||
-- images/ | awk -F/ 'NF>1 {print $2}' | sort -u)"
|
||||
if [ -z "$changed" ]; then
|
||||
json="[]"
|
||||
else
|
||||
json="$(printf '%s\n' "$changed" \
|
||||
| python3 -c 'import json,sys; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))')"
|
||||
fi
|
||||
fi
|
||||
echo "images=$json" >> "$GITHUB_OUTPUT"
|
||||
echo "대상 이미지: $json"
|
||||
|
||||
build:
|
||||
needs: discover
|
||||
if: needs.discover.outputs.images != '[]' && needs.discover.outputs.images != ''
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: ${{ fromJson(needs.discover.outputs.images) }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
IMAGE_DIR: images/${{ matrix.image }}
|
||||
steps:
|
||||
# 드리프트 경로면 discover 가 핀을 올려 push 한 브랜치를 받는다(비어 있으면 기본 ref).
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.discover.outputs.ref }}
|
||||
|
||||
- name: Preflight — 도구 확인
|
||||
run: |
|
||||
set -e
|
||||
for t in docker python3 bash; do
|
||||
command -v "$t" >/dev/null || { echo "::error::러너에 $t 없음"; exit 1; }
|
||||
done
|
||||
docker version
|
||||
|
||||
- name: trivy 설치
|
||||
run: |
|
||||
curl -fsSL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
||||
| sh -s -- -b /usr/local/bin
|
||||
trivy --version | head -1
|
||||
|
||||
# "이 이미지가 어느 차트의 어느 필드를 가리키는가" 는 이미지 디렉토리 자신이
|
||||
# 선언한다(catalog.env) — 이 워크플로가 이미지별 지식을 갖지 않게 하기 위함
|
||||
# (build.env 계약과 같은 원칙, .claude/image-authoring.md).
|
||||
- name: 이미지 메타데이터 로드 (catalog.env)
|
||||
id: meta
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ENV_FILE="$IMAGE_DIR/catalog.env"
|
||||
[ -f "$ENV_FILE" ] || { echo "::error::catalog.env 없음: $ENV_FILE — images/<image>/catalog.env 를 추가해야 한다"; exit 1; }
|
||||
# shellcheck disable=SC1090
|
||||
. "$ENV_FILE"
|
||||
: "${DEFAULT_BASE_OS:?catalog.env 에 DEFAULT_BASE_OS 가 없다}"
|
||||
: "${CHART_DIRS:?catalog.env 에 CHART_DIRS 가 없다}"
|
||||
: "${TAG_STYLE:?catalog.env 에 TAG_STYLE 이 없다}"
|
||||
BASE_OS="${{ github.event.inputs.base_os }}"
|
||||
BASE_OS="${BASE_OS:-$DEFAULT_BASE_OS}"
|
||||
{
|
||||
echo "base_os=$BASE_OS"
|
||||
echo "chart_dirs=$CHART_DIRS"
|
||||
echo "tag_style=$TAG_STYLE"
|
||||
echo "tag_block=${TAG_BLOCK:-image}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo " image=${{ matrix.image }} base_os=$BASE_OS chart_dirs=$CHART_DIRS tag_style=$TAG_STYLE"
|
||||
|
||||
# push 여부를 트리거별로 정한다. build-hardened-image.sh 는 REGISTRY 가 비어 있으면
|
||||
# push 를 생략하고 TAG 를 localhost/... 로 둔다 — pull_request(검증만)의 안전장치다.
|
||||
# 자동 트리거(schedule)와 드리프트 모드는 **절대 push 하지 않는다.** 자동 빌드는
|
||||
# "핀을 이만큼 올리면 정말 해소되는가" 까지만 답한다 — 레지스트리 반영은 사람이
|
||||
# workflow_dispatch 로 한다(CLAUDE.md 의 정책). catch-all 을 false 로 두어 앞으로
|
||||
# 트리거가 추가돼도 실수로 push 되지 않게 한다.
|
||||
- name: 게시 여부 결정
|
||||
id: publish
|
||||
run: |
|
||||
case "${{ github.event_name }}" in
|
||||
workflow_dispatch)
|
||||
if [ "${{ github.event.inputs.mode }}" = "drift" ] \
|
||||
|| [ "${{ github.event.inputs.push }}" = "false" ]; then
|
||||
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
||||
fi ;;
|
||||
*) echo "enabled=false" >> "$GITHUB_OUTPUT" ;;
|
||||
esac
|
||||
|
||||
- name: 레지스트리 로그인
|
||||
if: steps.publish.outputs.enabled == 'true'
|
||||
env:
|
||||
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
run: |
|
||||
[ -n "$DOCKERHUB_USER" ] || { echo "::error::DOCKERHUB_USER 시크릿 없음"; exit 1; }
|
||||
echo "$DOCKERHUB_TOKEN" | docker login docker.io -u "$DOCKERHUB_USER" --password-stdin
|
||||
|
||||
- name: 빌드 → 검증 → SBOM → 스캔 → 게이트
|
||||
id: build
|
||||
env:
|
||||
IMAGE: ${{ matrix.image }}
|
||||
BASE_OS: ${{ steps.meta.outputs.base_os }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
OUT="$GITHUB_WORKSPACE/build-out"
|
||||
mkdir -p "$OUT"
|
||||
# build-hardened-image.sh 의 build.log 는 그 안에서 docker build 출력만 담는
|
||||
# 별도 파일이다(스크립트 자신의 tag= 안내는 그 파일에 없다). 태그를 뒤에서
|
||||
# 뽑으려면 스크립트 자신의 표준출력을 따로 남겨야 한다 — tee 로 wrapper.log
|
||||
# 에도 기록한다.
|
||||
#
|
||||
# GitHub Actions 의 run: 스텝은 기본으로 bash -e 다. 빌드 스크립트의 실패를
|
||||
# $rc 로 정상 캡처하려면 그 호출 동안만 -e 를 끈다 — 아니면 실패 시 여기서
|
||||
# 바로 중단돼 아래의 rc 기록·output 기록이 실행되지 않는다.
|
||||
set +e
|
||||
if [ "${{ steps.publish.outputs.enabled }}" = "true" ]; then
|
||||
REGISTRY="$REGISTRY_HOST" bash scripts/build/build-hardened-image.sh "$OUT" 2>&1 | tee "$OUT/wrapper.log"
|
||||
else
|
||||
bash scripts/build/build-hardened-image.sh "$OUT" 2>&1 | tee "$OUT/wrapper.log" # push 없음 — 검증만
|
||||
fi
|
||||
rc="${PIPESTATUS[0]}"
|
||||
set -e
|
||||
echo "rc=$rc" >> "$GITHUB_OUTPUT"
|
||||
# 끝에 `|| true` 를 붙인다 — grep 이 매치를 못 찾아 실패해도(pipefail 이
|
||||
# 그 실패를 대입식 전체의 실패로 만든다) -e 아래에서 스크립트가 중단되지
|
||||
# 않게 한다. tag 가 비어도 그만이다 — rc!=0 이면 어차피 이후 단계가 건너뛴다.
|
||||
tag="$(grep -h '^ tag=' "$OUT/wrapper.log" 2>/dev/null | tail -1 | cut -d= -f2- || true)"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
exit "$rc"
|
||||
|
||||
- name: 아티팩트 업로드
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.image }}-build
|
||||
path: |
|
||||
build-out/build.log
|
||||
build-out/verify.log
|
||||
build-out/cve-gate.md
|
||||
build-out/sbom
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Job Summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f build-out/cve-gate.md ]; then
|
||||
{ echo "## ${{ matrix.image }}"; cat build-out/cve-gate.md; echo; } >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
# --- 카탈로그 반영 (push/workflow_dispatch 이고 게이트 PASS 일 때만) --------------
|
||||
|
||||
- name: 현재 카탈로그 태그 확인
|
||||
id: current
|
||||
if: steps.publish.outputs.enabled == 'true' && steps.build.outputs.rc == '0'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# CHART_DIRS 가 여러 개일 수 있으나(공백 구분) 첫 번째 디렉토리를 기준값으로
|
||||
# 삼는다 — 지금까지 이미지 하나가 차트 여러 개에 걸친 사례가 없다.
|
||||
first_dir="$(echo "${{ steps.meta.outputs.chart_dirs }}" | awk '{print $1}')"
|
||||
cur="$(python3 scripts/build/patch-catalog-tag.py \
|
||||
--style "${{ steps.meta.outputs.tag_style }}" \
|
||||
--block "${{ steps.meta.outputs.tag_block }}" \
|
||||
--read "$first_dir/custom-values.yaml")"
|
||||
echo "tag=$cur" >> "$GITHUB_OUTPUT"
|
||||
echo "현재 카탈로그 태그: $cur"
|
||||
echo "새 빌드 태그: ${{ steps.build.outputs.tag }}"
|
||||
|
||||
# 이 워크플로를 트리거하는 것 자체가 이미 "조치가 필요하다" 는 판단(sbom.yml 의
|
||||
# 게이트가 차단+수정가능 CVE 를 확인)이거나 사람의 명시적 실행이다. 예전에는
|
||||
# 블라인드 스케줄 재빌드가 있어서 "정말 개선인지" 를 여기서 재확인해야 했는데,
|
||||
# 그 트리거를 없앤 뒤로는 불필요해졌다 — 게이트 PASS + 태그 변경만 확인한다.
|
||||
#
|
||||
# PR 은 만들지 않는다(위 파일 헤더 참고 — GITHUB_TOKEN 으로 PR 생성이 조직 정책으로
|
||||
# 막혀 있고 우리 쪽에서 그 정책을 못 바꾼다, 실측: run 30882785612). 브랜치 push 까지만
|
||||
# 하고 compare 링크를 Job Summary 에 남겨 사람이 직접 PR 을 연다.
|
||||
- name: 카탈로그 브랜치 push + PR 안내
|
||||
if: >
|
||||
steps.publish.outputs.enabled == 'true' && steps.build.outputs.rc == '0' &&
|
||||
steps.current.outputs.tag != steps.build.outputs.tag
|
||||
run: |
|
||||
set -uo pipefail
|
||||
NEW="${{ steps.build.outputs.tag }}"
|
||||
OLD="${{ steps.current.outputs.tag }}"
|
||||
IMAGE="${{ matrix.image }}"
|
||||
BRANCH="build/${IMAGE}-$(date -u +%Y%m%d%H%M%S)"
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$BRANCH"
|
||||
|
||||
TOUCHED=()
|
||||
for dir in ${{ steps.meta.outputs.chart_dirs }}; do
|
||||
candidates=()
|
||||
[ -f "$dir/custom-values.yaml" ] && candidates+=("$dir/custom-values.yaml")
|
||||
[ -f "$dir/dip-values.yaml" ] && candidates+=("$dir/dip-values.yaml")
|
||||
[ "${#candidates[@]}" -eq 0 ] && continue
|
||||
out="$(python3 scripts/build/patch-catalog-tag.py \
|
||||
--style "${{ steps.meta.outputs.tag_style }}" \
|
||||
--block "${{ steps.meta.outputs.tag_block }}" \
|
||||
--old "$OLD" --new "$NEW" "${candidates[@]}")"
|
||||
echo "$out"
|
||||
TOUCHED+=("${candidates[@]}")
|
||||
done
|
||||
git diff --stat
|
||||
|
||||
git add "${TOUCHED[@]}"
|
||||
git commit \
|
||||
-m "${IMAGE} 이미지 태그 갱신: ${OLD##*:} → ${NEW##*:}" \
|
||||
-m "게이트 PASS(build-image.yml, ${{ github.event_name }} 트리거)로 확인된 태그로 교체한다." \
|
||||
-m "Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
||||
git push -u origin "$BRANCH"
|
||||
|
||||
COMPARE_URL="https://github.com/${{ github.repository }}/compare/main...${BRANCH}?expand=1"
|
||||
{
|
||||
echo "## ${IMAGE} 카탈로그 태그 갱신 — 브랜치 push 완료, PR 은 직접 열어야 함"
|
||||
echo
|
||||
echo "- 이전 태그: \`$OLD\`"
|
||||
echo "- 새 태그: \`$NEW\`"
|
||||
echo "- 브랜치: \`$BRANCH\`"
|
||||
echo "- 게이트: PASS (아티팩트의 \`cve-gate.md\` 참고)"
|
||||
echo
|
||||
echo "**PR 은 자동 생성되지 않는다** — GitHub 조직 정책상 \`GITHUB_TOKEN\`(Actions 자체 토큰)으로는"
|
||||
echo "PR 을 만들 수 없다. 아래 링크에서 사람이 직접 연다:"
|
||||
echo
|
||||
echo "$COMPARE_URL"
|
||||
echo
|
||||
echo "PR을 연 뒤 병합 전에 아래를 수동으로 실행해 카탈로그 게이트를 확인한다."
|
||||
echo
|
||||
echo '```sh'
|
||||
echo "gh workflow run helm-catalog-sbom --ref $BRANCH"
|
||||
echo '```'
|
||||
echo
|
||||
echo "이미지가 실제로 동작하는지도 병합 전에 수동으로 확인한다(해당 차트 배포 후"
|
||||
echo "기능 점검 — 자동화된 배포 테스트 절차는 아직 없다)."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "브랜치 push 완료 — PR은 사람이 직접 열어야 한다: $COMPARE_URL"
|
||||
@@ -0,0 +1,88 @@
|
||||
name: catalog-tag-update
|
||||
|
||||
# security-images(자체 빌드 이미지 레포, public)의 `published.json` 을 읽어 카탈로그
|
||||
# values 의 이미지 태그를 갱신한다. 그 레포는 이 카탈로그를 모른다 — 게이트 PASS + push
|
||||
# 가 실제로 일어난 이미지의 ref 를 `published.json` 에 기록해 둘 뿐이다. "그 태그를 어느
|
||||
# 차트에 반영할지" 는 이 카탈로그가 판단한다(catalog/image-map/<image>.env).
|
||||
#
|
||||
# public raw URL 로 읽으므로 인증이 필요 없다. security-images 쪽 시크릿·PAT 도 없다 —
|
||||
# 의존 방향은 카탈로그 → 이미지 단방향이다(docs/image-authoring.md "카탈로그 레포와의
|
||||
# 계약", security-images 레포).
|
||||
#
|
||||
# PR 은 자동 생성하지 않는다 — GitHub Actions 는 GITHUB_TOKEN 으로 PR 을 만들 수 없다는
|
||||
# 조직 정책에 막혀 있다(build-image.yml 이 자체 빌드 축에서도 같은 제약을 받았다).
|
||||
# 브랜치·커밋·push 까지만 하고 compare 링크를 Job Summary 에 남긴다.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# 자체 빌드는 workflow_dispatch 수동 실행뿐이라 발행이 잦지 않다 — 하루 한 번이면 충분하다.
|
||||
- cron: '0 3 * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
SECURITY_IMAGES_REPO: paasup/security-images
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: published.json 조회
|
||||
run: |
|
||||
curl -fsSL "https://raw.githubusercontent.com/${SECURITY_IMAGES_REPO}/main/published.json" \
|
||||
-o /tmp/published.json
|
||||
python3 -c "import json; json.load(open('/tmp/published.json'))" # 스키마 최소 확인
|
||||
|
||||
- name: 반영 대상 계산
|
||||
id: apply
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 scripts/build/apply-published-tags.py --published /tmp/published.json \
|
||||
--json-out /tmp/changes.json --summary-md /tmp/changes.md
|
||||
cat /tmp/changes.md >> "$GITHUB_STEP_SUMMARY"
|
||||
n="$(python3 -c "import json;print(len(json.load(open('/tmp/changes.json'))))")"
|
||||
echo "count=$n" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 브랜치 push + PR 안내
|
||||
if: steps.apply.outputs.count != '0'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BRANCH="build/catalog-tag-update-$(date -u +%Y%m%d%H%M%S)"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$BRANCH"
|
||||
|
||||
# apply-published-tags.py 가 이미 파일을 고쳤다 — 여기서는 그 결과를 커밋만 한다.
|
||||
FILES="$(python3 -c "
|
||||
import json
|
||||
for c in json.load(open('/tmp/changes.json')):
|
||||
print(c['file'])
|
||||
" | sort -u)"
|
||||
git add $FILES
|
||||
git diff --cached --stat
|
||||
|
||||
IMAGES="$(python3 -c "
|
||||
import json
|
||||
print(', '.join(sorted({c['image'] for c in json.load(open('/tmp/changes.json'))})))
|
||||
")"
|
||||
git commit \
|
||||
-m "자체 빌드 이미지 태그 갱신: $IMAGES" \
|
||||
-m "security-images 레포의 published.json(게이트 PASS + push 확인된 발행 기록)을" \
|
||||
-m "반영한다. 상세는 이 실행의 Job Summary 참고." \
|
||||
-m "Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
||||
git push -u origin "$BRANCH"
|
||||
|
||||
COMPARE_URL="https://github.com/${{ github.repository }}/compare/main...${BRANCH}?expand=1"
|
||||
{
|
||||
echo
|
||||
echo "브랜치 push 완료 — PR은 사람이 직접 연다: $COMPARE_URL"
|
||||
echo
|
||||
echo "병합 전 확인할 것:"
|
||||
echo '1. `gh workflow run helm-catalog-sbom --ref '"$BRANCH"'` 로 카탈로그 게이트 확인'
|
||||
echo "2. 배포 검증 — 게이트 PASS 는 동작을 증명하지 않는다"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "브랜치 push 완료: $COMPARE_URL"
|
||||
@@ -0,0 +1,83 @@
|
||||
name: self-build-drift-check
|
||||
|
||||
# 배포 중인 자체 빌드 이미지(security-images 레포가 만든 것)가 새 CVE 로 규정을 벗어났는지
|
||||
# 이 카탈로그가 스스로 스캔해 판정한다. "무엇이 배포 중인가"는 이 카탈로그만 안다 —
|
||||
# security-images 는 자기 스스로 이 판단을 하지 않는다
|
||||
# (security-images 레포 docs/image-authoring.md "재빌드는 이 레포가 스스로 트리거하지 않는다").
|
||||
#
|
||||
# 판정(scripts/build/check-rebuild-needed.py)이 재빌드 대상이라고 하면 security-images 의
|
||||
# `build-image.yml` 을 `workflow_dispatch` 로 부른다. 그 워크플로가 실제 빌드·검증·게이트·
|
||||
# push·`published.json` 갱신을 한다 — 이 워크플로는 트리거만 한다.
|
||||
#
|
||||
# 레지스트리 push 와 카탈로그 반영은 이 워크플로가 하지 않는다. `catalog-tag-update.yml` 이
|
||||
# `published.json` 을 읽어가는 별도 경로로 반영한다.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 2 * * 1' # 매주 월요일 02:00 UTC
|
||||
|
||||
env:
|
||||
SECURITY_IMAGES_REPO: paasup/security-images
|
||||
|
||||
jobs:
|
||||
drift-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: trivy 설치
|
||||
run: |
|
||||
curl -fsSL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
||||
| sh -s -- -b /usr/local/bin
|
||||
trivy --version | head -1
|
||||
|
||||
- name: 배포 중인 자체 빌드 이미지 스캔
|
||||
id: scan
|
||||
run: |
|
||||
set -uo pipefail
|
||||
mkdir -p /tmp/drift-reports
|
||||
python3 scripts/build/check-rebuild-needed.py --list-refs > /tmp/refs.tsv
|
||||
while IFS=$'\t' read -r img ref fname; do
|
||||
[ -n "${fname:-}" ] || continue
|
||||
echo "스캔: $ref"
|
||||
trivy image --quiet --scanners vuln \
|
||||
--severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL \
|
||||
--format json "$ref" > "/tmp/drift-reports/$fname" \
|
||||
|| { echo "::warning::$img: 스캔 실패 ($ref) — 이 이미지는 판정에서 빠진다"
|
||||
rm -f "/tmp/drift-reports/$fname"; }
|
||||
done < /tmp/refs.tsv
|
||||
|
||||
python3 scripts/build/check-rebuild-needed.py \
|
||||
--reports /tmp/drift-reports \
|
||||
--summary-md /tmp/drift.md \
|
||||
--json-out /tmp/drift.json
|
||||
cat /tmp/drift.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
python3 - <<'PY' >> "$GITHUB_OUTPUT"
|
||||
import json
|
||||
rows = json.load(open("/tmp/drift.json"))
|
||||
print("images=" + json.dumps([r["image"] for r in rows if r["status"] == "rebuild"]))
|
||||
PY
|
||||
|
||||
- name: security-images 재빌드 트리거
|
||||
if: steps.scan.outputs.images != '' && steps.scan.outputs.images != '[]'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.SECURITY_IMAGES_DISPATCH_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "$GH_TOKEN" ] || { echo "::error::SECURITY_IMAGES_DISPATCH_TOKEN 시크릿 없음 — ${SECURITY_IMAGES_REPO} 에 workflow_dispatch 를 걸 권한이 있는 PAT 필요"; exit 1; }
|
||||
python3 -c "import json,sys; print('\n'.join(json.loads(sys.argv[1])))" '${{ steps.scan.outputs.images }}' | \
|
||||
while read -r img; do
|
||||
[ -n "$img" ] || continue
|
||||
echo "재빌드 트리거: $img"
|
||||
gh workflow run self-build-image --repo "$SECURITY_IMAGES_REPO" -f "image=$img"
|
||||
done
|
||||
{
|
||||
echo
|
||||
echo "재빌드를 트리거했다: ${{ steps.scan.outputs.images }}"
|
||||
echo "진행 상황: https://github.com/${SECURITY_IMAGES_REPO}/actions/workflows/self-build-image.yml"
|
||||
echo
|
||||
echo "게이트 PASS + push 성공 시 그 레포의 \`published.json\` 이 갱신되고,"
|
||||
echo "\`catalog-tag-update.yml\` 이 다음 실행에서 카탈로그에 반영한다."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
Reference in New Issue
Block a user