zstd Compression Measurement + cache hit/miss Visibility Integration — Sprint 165 Option C Optimization Phase (Seed #165-2 + D1 Recovery)
Date
ImpactCritical
Goal
- After Sprint 166 baseline (tarball size) established, optimization phase step 1: seed #165-2 zstd compression measurement + Sprint 166 D1 deferred cache visibility recovery in same sprint
- A/B dual build only on ai-analysis service → docker tar vs OCI zstd tar generated simultaneously → compression saving % auto-calculated
- Common to 3 build jobs (services/frontend/blog): add build duration +
buildx ducache size + cache entries - "Security gate (Sprint 165) → Visibility (Sprint 166) → Optimization (Sprint 167)" 3-sprint cycle step 3
Decisions
D0. A/B Dual Build (ai-analysis only) + buildx du + duration — User Selection (Recommended)
- A adopted among zstd measurement options A~C (user decision):
- A (A/B dual build, ai-analysis only) / B (gradual transition + post-comparison) / C (measurement-only PR, all 8 services)
- Rationale: balance single-sprint comparison data acquisition + PR-level regression risk minimization
- A adopted for cache visibility signal options A~C: buildx du + build duration (indirect indicator combination)
- If data is clear this sprint, consider full adoption on 8 services in Sprint 168
D1. ai-analysis Selection — Maximum Compression Measurement Value
- Python FastAPI service — base image (python:3.12-slim) + pip wheels multi-layer + source layer accumulation
- Heavier library dependencies than Node.js TypeScript services (gateway/identity/submission/problem/github-worker)
- Best candidate for measuring zstd effect — provides decision basis for other services
D2. buildx multi-output Conditional Expression — Empty Line = Ignore Pattern
outputs:multi-line uses${{ matrix.service == 'ai-analysis' && format(...) || '' }}conditional branching- Other matrix services evaluate to empty line → buildx parser ignores empty lines (zero regression)
- buildx multi-output generates docker tar + oci zstd tar simultaneously in single build — zero build time impact (compression only)
docker/build-push-action@v7+ buildx v0.13+ officially supporttype=oci,compression=zstd- Trivy v0.69.2 supports OCI tar + zstd, but this sprint scans only docker tar (zstd is measurement-only)
D3. cache Visibility = buildx du Disk Usage + tail -n +2 | wc -l entries
docker buildx du --verboseTotal line awk-parsed → cache disk usage displayeddocker buildx du | tail -n +2 | wc -l→ cache entries count2>/dev/null+[ -z "$VAR" ] && VAR=N/Agraceful fallback — cache visibility is nice-to-have, hard fail prohibited- Direct hit rate not exposed (buildx structural limitation) → indirect indicator: disk usage + entries + duration 3-signal combination
D4. BUILD_START env Propagation = >> "$GITHUB_ENV"
- "Record build start time" step uses
BUILD_START=$(date +%s) >> $GITHUB_ENV→ environment variable auto-exposed to next step - Standard GitHub Actions env propagation pattern (cross-step communication)
- Avoids shell variable scope limitation (same step internal only)
D5. "Report tarball size" → "Report build artifact metrics" Naming Evolution
- Single signal (size) → 4 signals (size/duration/cache size/cache entries) expansion → name reflects essence
- Sprint 166 step name
Report tarball size→ Sprint 167Report build artifact metrics - ai-analysis additionally exposes zstd branch (5th signal)
D6. frontend/blog skip Guard Inconsistency Intentionally Maintained (Deferred to Sprint 168+)
- Only build-services has
if: steps.check.outputs.skip == 'false'guard, frontend/blog do not - Sprint 166 pattern maintained — because paths filter differs (build-services supports per-matrix skip, frontend/blog has no job-level skip guard)
- Avoid scope expansion this sprint — consistent policy decided in separate sprint
Implementation (1 PR, 35 sprints consecutive branch discipline)
Branch: feat/sprint-167-zstd-cache-visibility (new from main 76b1520)
Phase A — A/B Dual Build (ai-analysis only)
.github/workflows/ci.yml build-services job changes:
YAML
- name: Record build start time
if: steps.check.outputs.skip == 'false'
run: echo "BUILD_START=$(date +%s)" >> "$GITHUB_ENV"
- uses: docker/build-push-action@v7
if: steps.check.outputs.skip == 'false'
with:
outputs: |
type=image,push=${{ github.ref == 'refs/heads/main' }},name=...
type=docker,dest=/tmp/image-${{ matrix.service }}.tar
${{ matrix.service == 'ai-analysis' && format('type=oci,compression=zstd,dest=/tmp/image-{0}-zstd.tar', matrix.service) || '' }}
ai-analysis branch in "Report build artifact metrics" step:
Bash
if [ "${{ matrix.service }}" = "ai-analysis" ]; then
ZSTD_TARBALL=/tmp/image-${{ matrix.service }}-zstd.tar
if [ -f "$ZSTD_TARBALL" ]; then
ZSTD_BYTES=$(stat -c %s "$ZSTD_TARBALL")
ZSTD_MB=$(awk -v b="$ZSTD_BYTES" 'BEGIN {printf "%.1f", b/1024/1024}')
SAVE_PCT=$(awk -v d="$SIZE_BYTES" -v z="$ZSTD_BYTES" 'BEGIN {printf "%.1f", (1 - z/d) * 100}')
echo "- tarball size (oci+zstd): **${ZSTD_MB} MB** (${ZSTD_BYTES} bytes)"
echo "- compression saving: **-${SAVE_PCT}%** (zstd vs docker)"
fi
fi
Phase B — cache Visibility (3 build jobs common)
3 build jobs each add "Record build start time" before build-push-action step + integrate 4 signals in "Report build artifact metrics":
Bash
BUILD_END=$(date +%s)
BUILD_DURATION=$((BUILD_END - BUILD_START))
DURATION_FMT=$(awk -v s="$BUILD_DURATION" 'BEGIN {printf "%dm %ds", int(s/60), s%60}')
CACHE_DU=$(docker buildx du --verbose 2>/dev/null | awk '/^Total:/ {print $2, $3; exit}')
[ -z "$CACHE_DU" ] && CACHE_DU="N/A"
CACHE_ENTRIES=$(docker buildx du 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')
[ -z "$CACHE_ENTRIES" ] && CACHE_ENTRIES="0"
{
echo "### 📦 ${{ matrix.service }} build artifact"
echo "- tarball size (docker): **${SIZE_MB} MB** (${SIZE_BYTES} bytes)"
echo "- build duration: **${DURATION_FMT}** (${BUILD_DURATION}s)"
echo "- cache size: **${CACHE_DU}**"
echo "- cache entries: **${CACHE_ENTRIES}**"
...
} >> "$GITHUB_STEP_SUMMARY"
All 3 build jobs use identical pattern (matrix vs hardcoded only difference) — Sprint 166 DRY policy inherited.
Phase C — Critic R1 P3 forward-fix (<TBD>)
.github/workflows/ci.yml 3 build job CACHE_ENTRIES count logic modified:
- R1 P3 finding:
docker buildx duoutput includes footer rows (Shared:,Private:,Reclaimable:,Total:), andtail -n +2 | wc -lcounts footer as entries → inflated metric → weakens cache visibility signal accuracy - fix applied:
awk 'NR > 1 && !/^(ID|Reclaimable|Shared|Private|Total):/ && NF > 0 {count++} END {print count+0}'pattern explicitly excludes header + footer + empty lines - Inherits Sprint 164 R1 P3 self-fix policy (same PR same-day forward-fix)
Phase D — ADR Recording (this commit)
docs/adr/sprints/sprint-167.md(KR) +docs/adr-en/sprints/sprint-167.md(EN 1:1 mapping)docs/adr/README.mdcount 106→107, range 62166→62167 (lines 18/52/54)
Critic Cycle
R1 (codex review --base 76b1520, background PID 53829, completed successfully):
- Counts: P0: 0, P1: 0, P2: 0, P3: 1
- Judgement: "No blocking CI regression found; only low-severity cache metrics accuracy issue."
- P3 finding: cache entries count inflated by
docker buildx dufooter rows (Shared/Private/Reclaimable/Total) → same-PR same-day forward-fix (Phase C) - Checked Areas 7 items all OK:
- Shell injection / quoting: no new exploitable issue. Matrix values + locally computed variables + paths properly quoted
- Sprint goal consistency: consistent. Only ai-analysis gets zstd A/B artifact, all Docker build jobs get duration/cache visibility
- buildx multi-output empty expression: safe.
docker/build-push-actionparses outputs viaUtil.getInputList(...)— empty list items not passed as--output - Non-ai-analysis services: no regression from empty zstd branch
- Frontend/blog jobs: no zstd branch — only metrics added, no regression
docker buildx du Total:parsing: matches current Docker documented output format (text parsing inherently brittle)BUILD_STARTpropagation:$GITHUB_ENVexposes to subsequent steps in same job — metrics step runs after record step
- Sources used: Docker buildx du official docs + docker/build-push-action source (Util.getInputList)
Risk / Regression Prevention
Prediction 1: buildx multi-output Conditional Expression Empty Line Handling
${{ ... && format(...) || '' }}evaluates to empty string on false branch → empty line in multi-line|- buildx outputs parser processes line by line + ignores empty lines → zero regression for other matrix services (gateway/identity etc.)
- Trivy
--inputuses only docker tar (zstd tar measurement-only) → zero scan regression
Prediction 2: docker buildx du Output Format Assumption
- Assumes "Total: " line awk parsing. On format change → empty result →
[ -z ]fallback → "N/A" displayed - Zero work failure —
2>/dev/null || ...pattern blocks hard fail
Prediction 3: BUILD_START env cross-step Exposure
- GitHub Actions
>> "$GITHUB_ENV"standard pattern — auto-exposed to subsequent steps within same job - If
$BUILD_STARTundefined,BUILD_END - BUILD_STARTbecomes large value → no fallback mechanism needed (always propagated in normal flow)
Prediction 4: zstd tarball Not Produced Case
- For non-ai-analysis services, buildx ignores empty zstd outputs line →
/tmp/image-{service}-zstd.tarnot generated - "Report build artifact metrics"
[ -f "$ZSTD_TARBALL" ]guard → displays "N/A (file not produced)" if absent — zero runtime error
Verification
- Local:
python3 yaml.safe_loadPASS,bash scripts/check-adr-en-coverage.sh --strict116/116 PASS,bash scripts/check-doc-refs.sh0 broken refs - CI (PR phase):
- 37 SUCCESS + 0 FAILURE, mergeStateStatus CLEAN
- ai-analysis build job Summary:
### 📦 ai-analysis build artifactH3 + docker MB + duration + cache size + cache entries + oci+zstd MB + compression saving % displayed - Other services (gateway/identity/submission/problem/github-worker): only docker MB + duration + cache size + entries (no zstd branch)
- frontend/blog: identical pattern (no zstd branch)
- Trivy scan 8 matrix no regression (
--inputdocker tar matching unchanged)
- UAT new 1 item: visual confirmation of compression saving % in ai-analysis Summary (user direct)
Result
4 files changed:
- Modified 1:
.github/workflows/ci.yml(+80 -13, 3 build job multi-output conditional + step add + Report step expansion) - New 2:
docs/adr/sprints/sprint-167.md(KR) +docs/adr-en/sprints/sprint-167.md(EN 1:1 mapping) - Modified 1:
docs/adr/README.md(lines 18/52/54 — count 106→107, range 62166→62167)
Commits:
07828d8feat(ci): Sprint 167 — zstd 압축 실측 + cache 가시성 통합<TBD>docs(adr): Sprint 167 ADR (KR + EN)- Squash merge:
<TBD-MERGE-SHA>(PR #292)
New Patterns
- buildx multi-output Conditional Branching Pattern —
${{ matrix.X == 'Y' && format(...) || '' }}conditional expression combined with buildx multi-line outputs empty-line-ignore = only some matrix services generate additional output. Zero regression + minimal diff. Future standard pattern for measurement/experiment steps - A/B Measurement + Build 1x Simultaneous Generation — buildx multi-output simultaneous support results in zero build time impact. Measurement cost = compression computation + disk I/O only. Avoids separate measurement build job pattern
- 4-Signal Integrated Output + graceful fallback Policy — single step integrates size + duration + cache size + cache entries 4 signals → future signal addition expands same step (Sprint 168 follow-up).
|| N/Afallback blocks hard fail of nice-to-have signals - Seed Compression Recovery = Next-Sprint Data Verification Pattern — Sprint 165 Option C established → Sprint 166 baseline → Sprint 167 measurement. 3-sprint cycle complete. Without baseline data, comparison meaningless → sequential recovery policy established
- Critic R1 P3 Self-fix Same-PR Policy Established (Sprint 164 → 167 Accumulated) — Codex gpt-5 R1 detected P3 accuracy issue → same-PR same-day forward-fix → ADR documents fix circumstances. Sprint 164 ADR Critic R1 P3 fix pattern reproduces in Sprint 167 = avoid split sprint + prioritize single-sprint completeness policy established
Deferred Items (Sprint 168+)
- Sprint 167 new deferred seeds:
- Seed #167-1: full adoption on 8 services if zstd measurement clear (when ai-analysis data shows 30%+ compression rate + time regression within 5%)
- Seed #167-2: frontend/blog skip guard inconsistency consistent policy (unify with build-services pattern)
- Seed #167-3:
$GITHUB_STEP_SUMMARYstandardization helper (seed #new-6 recovery)
- Sprint 166 deferred continuing:
- Sprint 164 seeds #new-4/5/7 CI visibility (deploy gate simulation / aether-gitops kustomization auto PR /
_parse_group_responseenvelope expansion) - Seeds #30/#31 (Sprint 158 i18n/lint): build artifact Korean residue CI step + i18n 3-layer checklist
- Seeds #24/#18 (Sprint 157): plan template i18n bidirectional auto + blog post merge cross-check automation
- Seeds #26/27/28 (Sprint 157 ADR/blog reinforcement): README paths filter / build-blog
ls out// check-adr-links ROOT auto-detection - UAT 23-sprint accumulated user direct verification (#5/#9 + Sprint 160~166 accumulated + Sprint 167 new 1 item: visual confirmation of oci+zstd MB + compression saving % in ai-analysis Summary)
- Maintained — seed #23 (rebase cumulative count fix checklist)
- Optional follow-up 10 items (Sprint 166 deferred as-is)
- Sprint 164 seeds #new-4/5/7 CI visibility (deploy gate simulation / aether-gitops kustomization auto PR /
- ADR: sprint-167.md (KR) + sprint-167.md EN