name: self-build-image # 자체 빌드 이미지(images/, 실행기 scripts/build/build-hardened-image.sh)의 # 빌드·검증·스캔·게이트·카탈로그 반영을 자동화한다. security-catalog 의 동일 워크플로에서 # 프레임워크만 포팅했다 — 아직 이 레포에 도입된 자체 빌드 이미지는 없다(images/ 디렉토리 # 자체가 비어 있음). 게이트(scripts/pipeline/cve-gate.py)가 상위 태그·베이스 OS 교체로 # 해소되지 않는 차단 CVE 를 찾으면 이 워크플로로 자체 빌드를 검토한다 — # 절차: .claude/image-authoring.md # # "이미지가 어느 차트의 어느 필드를 가리키는가" 는 images//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 가 없다. # 빌드는 호스트 러너여야 한다. # # 트리거 2종이 같은 스텝(빌드→verify.sh→SBOM→scan-sbom.sh→cve-gate.py)을 돈다. # 차이는 대상 이미지를 어떻게 정하는지, 그리고 push·카탈로그 PR 생성 여부뿐이다. # # pull_request(images/**) 변경된 images// 디렉토리를 diff 로 자동 탐지해 그 # 이미지들만 검증한다(push·카탈로그 PR 생성 없음). 여러 이미지가 # 한 PR 에서 바뀌면 각각 매트릭스로 병렬 실행된다. # workflow_dispatch `image` 입력으로 대상을 명시한다. 실제 빌드·push·카탈로그 PR # 생성은 이 트리거로만 일어난다(사람이 수동 실행) — sbom.yml 의 # 게이트는 현재 이 워크플로를 자동으로 호출하지 않는다. # push 입력이 false 면 검증만 한다. on: workflow_dispatch: inputs: image: description: '빌드할 이미지 디렉토리명 (images//)' required: true base_os: description: '빌드 변종 (images//.build.env). 비우면 catalog.env 의 DEFAULT_BASE_OS 사용' required: false default: '' push: description: '레지스트리 push + 카탈로그 PR 생성 여부' required: false default: 'true' pull_request: paths: - 'images/**' permissions: contents: write pull-requests: write env: REGISTRY_HOST: docker.io/paasup jobs: # --- 대상 이미지 결정 --------------------------------------------------------- # workflow_dispatch: inputs.image 하나. pull_request: images// 아래 변경이 있는 # 디렉토리를 전부 찾는다(여러 이미지가 한 PR 에서 바뀌면 각각 매트릭스로 돈다). discover: runs-on: ubuntu-latest outputs: images: ${{ steps.list.outputs.images }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: 대상 이미지 목록 산출 id: list run: | set -euo pipefail if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then img="${{ github.event.inputs.image }}" [ -n "$img" ] || { echo "::error::image 입력이 비어있다"; 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 != '[]' 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: - uses: actions/checkout@v4 - 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//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(검증만)의 안전장치다. - name: 게시 여부 결정 id: publish run: | case "${{ github.event_name }}" in pull_request) echo "enabled=false" >> "$GITHUB_OUTPUT" ;; workflow_dispatch) [ "${{ github.event.inputs.push }}" = "false" ] \ && echo "enabled=false" >> "$GITHUB_OUTPUT" \ || echo "enabled=true" >> "$GITHUB_OUTPUT" ;; *) echo "enabled=true" >> "$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 + 태그 변경만 확인한다. - name: 카탈로그 PR 생성 if: > steps.publish.outputs.enabled == 'true' && steps.build.outputs.rc == '0' && steps.current.outputs.tag != steps.build.outputs.tag env: GH_TOKEN: ${{ github.token }} 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] " git push -u origin "$BRANCH" # 본문을 파일로 만든다. heredoc 을 워크플로 run 블록 안에 그대로 두면 내용의 # 들여쓰기가 YAML 블록 스칼라 들여쓰기와 어긋난다. BODY="$GITHUB_WORKSPACE/pr_body.md" { echo "자체 빌드 이미지(\`${IMAGE}\`) 태그 갱신. 트리거: \`${{ github.event_name }}\`." echo echo "- 이전 태그: \`$OLD\`" echo "- 새 태그: \`$NEW\`" echo "- 게이트: PASS (아티팩트의 \`cve-gate.md\` 참고)" echo echo "**주의 — 이 PR 은 다른 워크플로를 자동으로 트리거하지 않는다**" echo "(\`GITHUB_TOKEN\` 이 만든 PR 에 대한 GitHub 의 재귀 방지 정책). 병합 전에 아래를 수동으로" echo "실행해 카탈로그 게이트를 확인한다." echo echo '```sh' echo "gh workflow run helm-catalog-sbom --ref $BRANCH" echo '```' echo echo "이미지가 실제로 동작하는지도 병합 전에 수동으로 확인한다(해당 차트 배포 후" echo "기능 점검 — 자동화된 배포 테스트 절차는 아직 없다)." } > "$BODY" gh pr create --title "${IMAGE}: ${OLD##*:} → ${NEW##*:}" --body-file "$BODY"