Files
project-infra/docs/superpowers/plans/2026-08-02-project-infra-phase-1-validation-structure.md
T

24 KiB

Project Infra Phase 1 validation and repository structure Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to execute this plan task by task. Use superpowers:test-driven-development for validator changes and superpowers:verification-before-completion before reporting success.

Goal: Make repository-local validation deterministic, put scripts and presentation assets in their declared directory classes, normalize source modes, and remove misleading empty implementation leaves.

Architecture: make validate is the developer-friendly local contract and make check is the complete repository contract. Project tests and validators remain in this repository; workflow orchestration and authoritative tool provisioning remain owned by cicd-platform. Entrypoints live under scripts/bin or scripts/ci, shared code lives under scripts/lib, generated presentation artifacts are ignored, and empty IaC leaves are not represented as implemented environments.

Tech Stack: Make, Bash, Kustomize/Helm, kubeconform, kube-linter, ShellCheck, shfmt, gitleaks, yq 4.47.2, Python 3, python-pptx, Pillow, Git file modes.

Global constraints

  • Complete the dev GitOps and automation-safety plans first.
  • Read docs/standards/infra/STYLE.md, scripts.md, kustomize.md, and their approved examples before changing validation or examples.
  • Do not edit .github/**, .gitea/**, or /home/donghyeon/workspace/desktop-server-git/cicd-platform.
  • Treat .mise.toml only as a local developer convenience. Do not describe it as the authoritative CI lock.
  • Do not create a Terraform/OpenTofu root; deleting placeholder leaves does not choose an IaC engine.
  • Keep docs/examples/infra/** as the only approved manifest-example catalog.
  • Keep .github/CODEOWNERS.example unchanged until real organization/team identifiers are supplied.
  • Commit only task-scoped files and inspect the worktree before every commit.

Dependency and public interfaces

  • scripts/bin/doctor.sh is the user-facing tool diagnostic entrypoint.
  • scripts/ci/validate-structure.sh validates filesystem shape, sensitive file rules, source modes, shell syntax, and activated IaC syntax.
  • scripts/ci/validate.sh owns the project contract and sources scripts/lib/kustomize.sh for every render.
  • VALIDATION_PROFILE=local may skip optional heavyweight validators with an explicit warning; core render, test, structure, docs-YAML syntax, and path contracts never skip.
  • VALIDATION_PROFILE=full treats every declared validator as required.
  • make validate runs the local profile once. make check runs the full profile once and fails if a required tool is absent.
  • validate_yaml_fences DOCS_ROOT is sourceable from scripts/ci/validate-docs.sh for fixture tests.

Task 1: Move repository entrypoints into their declared script classes

Files:

  • Move: scripts/doctor.shscripts/bin/doctor.sh
  • Move: scripts/validate.shscripts/ci/validate-structure.sh
  • Modify: scripts/bin/doctor.sh
  • Modify: scripts/ci/validate-structure.sh
  • Modify: Makefile
  • Modify: scripts/README.md
  • Modify: README.md
  • Create: tests/contracts/script-layout-test.sh

Step 1: Write the failing layout and entrypoint contract

Assert:

scripts/doctor.sh is absent
scripts/validate.sh is absent
scripts/bin/doctor.sh exists and is executable
scripts/ci/validate-structure.sh exists and is executable
both moved files contain set -Eeuo pipefail and IFS=$'\n\t'
both moved files define main and end through a main "$@" guard
doctor defines usage and accepts -h/--help
Makefile references only the moved paths
make -n validate includes local project validation exactly once
make -n check includes VALIDATION_PROFILE=full exactly once

Run:

bash tests/run.sh script-layout

Expected: non-zero because both scripts still live at the scripts/ root.

Step 2: Move and refactor the scripts

Use git mv. Both scripts must calculate the repository root from their new location:

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"

Wrap the current top-level validation flow in named functions and:

main() {
  validate_required_structure
  validate_directory_names
  collect_sources
  validate_sensitive_sources
  validate_secret_manifests
  validate_replacement_tokens
  validate_source_modes
  validate_shell_syntax
  validate_kustomization_layout
  validate_local_helm_charts
  validate_activated_iac
  report_result
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi

Use these exact function names so the entrypoint contract can call them independently. Preserve existing checks; do not silently drop the Secret/SOPS, replacement-token, Helm, or IaC checks while refactoring.

Remove the Kustomize render loop from validate-structure.sh; deployable render ownership already lives in scripts/ci/validate.sh and rendering every catalog Kustomization is a duplicate network-heavy contract. Keep Kustomization path/type checks in structure validation, keep local Chart.yaml linting, and make script-layout-test.sh assert that validate-structure.sh contains neither kustomize build nor kubectl kustomize.

Step 3: Separate Make local and full profiles

The Make targets must have these exact dependencies and recipe commands:

  • doctor runs @./scripts/bin/doctor.sh.
  • validate-structure runs @./scripts/ci/validate-structure.sh.
  • validate-project runs @VALIDATION_PROFILE=local bash ./scripts/ci/validate.sh.
  • validate depends on validate-structure validate-project and has no second recipe.
  • check depends on validate-structure and runs @VALIDATION_PROFILE=full bash ./scripts/ci/validate.sh.

Keep the existing help comments so make help remains useful.

Step 4: Verify and commit

Run:

bash tests/run.sh script-layout
bash -n scripts/bin/doctor.sh scripts/ci/validate-structure.sh
make -n validate
make -n check
git diff --check

Expected: all commands exit 0.

git add scripts Makefile README.md tests/contracts/script-layout-test.sh
git commit -m "refactor: 스크립트 entrypoint 위치 정규화"

Task 2: Remove false implementation leaves, fix copy examples, and normalize modes

Files:

  • Delete: infrastructure/components/compute/README.md
  • Delete: infrastructure/components/database/README.md
  • Delete: infrastructure/components/networking/README.md
  • Delete: infrastructure/components/storage/README.md
  • Delete: infrastructure/live/dev/cluster/README.md
  • Delete: infrastructure/live/staging/cluster/README.md
  • Delete: infrastructure/live/prod/cluster/README.md
  • Modify: infrastructure/components/README.md
  • Modify: infrastructure/live/README.md
  • Modify: README.md
  • Modify: docs/guides/getting-started.md
  • Modify: scripts/ci/validate-structure.sh
  • Create: tests/contracts/repository-shape-test.sh
  • Change mode: every tracked *.yaml and *.yml source to 100644

Step 1: Write the failing shape and mode contract

Assert:

the seven placeholder leaf directories listed above do not exist
the three parent roots and all `_template` roots still exist
tracked YAML/YML files have Git mode 100644
tracked shell files under scripts have Git mode 100755
no copy instruction uses `cp -R SOURCE/_template DEST`
copy instructions create a destination then copy `SOURCE/_template/.` into it
CODEOWNERS.example still exists

Run:

bash tests/run.sh repository-shape

Expected: non-zero because placeholder leaves and thirteen executable YAML/value files exist.

Step 2: Remove only the misleading leaves

Delete the seven tracked README files and their now-empty directories. Update the parent READMEs to state:

  • components/<unit> exists only when a real reusable primitive is selected;
  • live/<env>/<root> exists only when an executable plan/apply root and backend decision exist;
  • the IaC engine/provider/backend/state choice remains a Phase 2 decision.

Do not remove infrastructure/components/_template, infrastructure/live/_template, parent README files, or infrastructure/.iac-engine.example.

Step 3: Correct template copy commands

For each README/getting-started example, use this pattern with its actual destination:

mkdir -p infrastructure/live/dev/cluster
cp -R infrastructure/live/_template/. infrastructure/live/dev/cluster/

Apply the same mkdir -p plus SOURCE/. DEST/ form to component, stack, app, platform, and cluster examples. Use gitops/clusters/staging/main for the new-cluster example so instructions never copy a template over the active gitops/clusters/dev/main. This behaves the same whether a new destination was just created or already exists and never creates a nested _template directory.

Step 4: Normalize tracked source modes

Use chmod 0644 on every tracked Kubernetes YAML, Kustomization YAML, and Helm values YAML. In particular verify the moved VSO values file, registry base, secret-delivery base, and Vault base files identified by the review.

Add a structural check based on the Git index:

while IFS= read -r -d '' record; do
  mode="${record%% *}"
  path="${record#*$'\t'}"
  case "$path" in
    *.yaml | *.yml)
      [[ "$mode" == "100644" ]] || fail "YAML source must be mode 0644: $path"
      ;;
    scripts/*.sh)
      [[ "$mode" == "100755" ]] || fail "script must be executable: $path"
      ;;
  esac
done < <(git ls-files -s -z)

Handle untracked files in the existing source collection separately with filesystem modes so the check also fails before a new file is added to Git.

Step 5: Verify and commit

Run:

bash tests/run.sh repository-shape
./scripts/ci/validate-structure.sh
git ls-files -s '*.yaml' '*.yml' | awk '$1 != "100644" {print; bad=1} END {exit bad}'
git diff --check

Expected: all commands exit 0 and the mode query prints nothing.

git add infrastructure README.md docs/guides/getting-started.md scripts/ci/validate-structure.sh tests/contracts/repository-shape-test.sh gitops
git commit -m "refactor: 저장소 shape과 YAML mode 정규화"

Task 3: Enforce inventory, render, and source-of-truth contracts

Files:

  • Modify: scripts/ci/validate.sh
  • Modify: tests/kustomize-entrypoints.txt
  • Create: tests/contracts/entrypoint-inventory-test.sh
  • Modify: tests/README.md

Step 1: Write the failing inventory contract

Derive actual dev entrypoints from these paths:

gitops/clusters/dev/main/namespaces/kustomization.yaml
gitops/clusters/dev/main/all/kustomization.yaml
gitops/clusters/dev/main/stages/*/kustomization.yaml

Convert them to parent-directory paths, sort them, and compare byte-for-byte with the sorted non-comment inventory. Assert additionally:

no cluster Kustomization outside gitops/clusters/_template imports a `_template` path
no scripts/bin or scripts/tasks file references the cluster `all` entrypoint
no deployable rendered output contains example.com/environment: lab
every inventory entry renders only through kustomize_render
every render has no replace-in-overlay or __REPLACE_ME_* token
the existing ForwardAuth, Keycloak admin, and operator render contracts run from the full test runner

Run:

bash tests/run.sh entrypoint-inventory

Expected: non-zero if the inventory is stale or if the current validator still has a second render implementation.

Step 2: Add deterministic inventory validation

Add validate_entrypoint_inventory before manifest rendering in scripts/ci/validate.sh. It must use temporary sorted files and diff -u; on mismatch, print the unified diff and fail the validation group.

Reject template imports with rg over cluster Kustomization source and reject apply-code references to /all with rg over scripts/bin and scripts/tasks. Exclude comments only when the parser proves the line is a comment; do not broadly suppress files.

Step 3: Run contract tests from project validation

Add:

validate_contract_tests() {
  bash "$REPO_ROOT/tests/run.sh"
}

Call it once from main. Do not run tests a second time through Make dependencies.

Step 4: Make full-profile tool requirements explicit

In full mode require:

kustomize
helm
kubeconform
kube-linter
shellcheck
shfmt
gitleaks
yq
rg

In local mode, Kustomize/Helm rendering, Bash contract tests, rg, and yq remain core and cannot skip. kubeconform, kube-linter, ShellCheck, shfmt, and gitleaks may skip only with a warning that names the missing tool.

Step 5: Verify and commit

Run:

bash tests/run.sh entrypoint-inventory
VALIDATION_PROFILE=local bash scripts/ci/validate.sh
git diff --check

Expected: all commands exit 0 and each contract test runs exactly once.

git add scripts/ci/validate.sh tests/kustomize-entrypoints.txt tests/contracts/entrypoint-inventory-test.sh tests/README.md
git commit -m "test: GitOps entrypoint 계약 강화"

Task 4: Validate every documentation YAML fence

Files:

  • Modify: .mise.toml
  • Modify: scripts/bin/doctor.sh
  • Modify: scripts/ci/validate-docs.sh
  • Create: tests/contracts/docs-yaml-test.sh
  • Modify: Markdown files under docs/** only where a block is genuinely abbreviated, intentionally bad, or fails the approved example standards
  • Modify: docs/standards/infra/STYLE.md to document the example marker contract

Step 1: Write fixture-driven failing tests

Refactor validate-docs.sh so sourcing it does not execute main. The test sources it and calls validate_yaml_fences on temporary Markdown fixtures. Cover:

valid YAML fence succeeds
syntactically invalid YAML fence fails even below a bad-example heading
unclosed YAML fence fails
normal complete Kubernetes resource invokes fake yq, kubeconform, and kube-linter
`나쁜 예시`, `❌`, and case-insensitive `bad example` headings invoke yq but skip schema/policy
an immediately preceding `<!-- infra-example: abbreviated -->` marker invokes yq but skips schema/policy
a normal complete resource that fails kubeconform fails the docs gate
a normal complete resource that fails kube-linter fails the docs gate
an abbreviated marker cannot suppress YAML syntax failure

Run:

bash tests/run.sh docs-yaml

Expected: non-zero because current docs validation does not parse fenced YAML.

Step 2: Pin and diagnose the syntax parser

Add this local convenience pin:

yq = "4.47.2"

Add yq to required doctor tools. In both local and full validation profiles, a missing yq is a hard error because syntax checking is a core docs contract. Document that cicd-platform, not .mise.toml, owns authoritative CI tool delivery.

Step 3: Implement fence extraction and classification

validate_yaml_fences DOCS_ROOT must read every tracked or present *.md file under the supplied root in sorted order and maintain:

current Markdown heading
whether a YAML/YML fence is open
source file and opening line
whether the immediately preceding non-blank line is the abbreviated marker
whether the current heading contains 나쁜 예시, ❌, or bad example

For every closed fence:

  1. write the exact block to a mode-0600 temporary file;
  2. run yq eval-all '.' BLOCK unconditionally;
  3. classify it as a complete Kubernetes example only when it is not bad/abbreviated and every YAML document contains non-empty apiVersion, kind, and metadata.name;
  4. run kubeconform with the repository CRD catalog configuration for complete examples;
  5. run kube-linter with .kube-linter.yaml for complete examples;
  6. report file:opening-line on every failure.

Bad and abbreviated classifications skip only steps 4 and 5. They never skip syntax.

Add this exact marker contract to STYLE.md:

<!-- infra-example: abbreviated -->
```yaml
apiVersion: apps/v1
kind: Deployment
```

When documenting the marker inside a Markdown fence, use a four-backtick outer fence so the standard itself remains well formed.

Step 4: Audit existing docs instead of blanket-suppressing them

Run the new gate. For each failure:

  • fix syntax in normal examples;
  • fix security/resource/probe defects in examples presented as approved;
  • add the abbreviated marker only when omitted fields are intentional prose;
  • rely on a bad-example heading only for genuinely rejected examples.

Do not mark all files or all standards as abbreviated. docs/examples/infra/** normal good examples must pass schema/policy when complete.

Step 5: Verify and commit

Run:

bash tests/run.sh docs-yaml
VALIDATION_PROFILE=full bash scripts/ci/validate-docs.sh
git diff --check

Expected: all commands exit 0; validation output reports YAML fence count and complete Kubernetes example count.

git add .mise.toml scripts/bin/doctor.sh scripts/ci/validate-docs.sh tests/contracts/docs-yaml-test.sh docs
git commit -m "test: 문서 YAML 예시 검증 추가"

Task 5: Move and make the presentation source reproducible

Files:

  • Move: presentation/diagrams/**docs/presentation/diagrams/**
  • Move: presentation/exports/**docs/presentation/exports/**
  • Move: presentation/scripts/build_pptx.pydocs/presentation/scripts/build_pptx.py
  • Delete after move: docs/presentation/build/deck.pptx
  • Create: docs/presentation/README.md
  • Create: docs/presentation/requirements.txt
  • Modify: docs/presentation/scripts/build_pptx.py
  • Modify: .gitignore
  • Create: tests/contracts/presentation-layout-test.sh

Step 1: Write the failing layout/reproducibility contract

Assert:

root presentation directory is absent
docs/presentation exists
docs/presentation/build is ignored
no PPTX file is tracked under docs/presentation
build_pptx.py contains no /home/ path
ROOT is derived from Path(__file__).resolve().parents[1]
requirements pin python-pptx and Pillow with ==
every PNG path referenced by build_pptx.py exists under exports
the script compiles with python3 -m py_compile

Run:

bash tests/run.sh presentation-layout

Expected: non-zero because the material is at repository root and the Python script hardcodes an absolute path.

Step 2: Move source assets and remove the generated deck

Use git mv presentation docs/presentation, then remove the tracked docs/presentation/build/deck.pptx. Add:

docs/presentation/build/

Keep every Draw.io source and every PNG referenced by the Python script. Do not delete source inputs merely to reduce repository size.

Step 3: Make paths dynamic and dependencies explicit

Replace the hardcoded root with:

ROOT = Path(__file__).resolve().parents[1]
EXPORTS = ROOT / "exports"
OUT = ROOT / "build" / "deck.pptx"

Use this requirements.txt:

python-pptx==1.0.2
Pillow==11.3.0

The README must contain commands that work from any checkout:

python3 -m venv docs/presentation/.venv
docs/presentation/.venv/bin/pip install --requirement docs/presentation/requirements.txt
docs/presentation/.venv/bin/python docs/presentation/scripts/build_pptx.py

Explain that build/deck.pptx is generated and untracked.

Step 4: Rebuild once in an isolated environment

Create a temporary virtual environment outside the repository, install the pinned requirements, run the script, and validate the generated ZIP container:

PRESENTATION_VENV="$(mktemp -d -t project-infra-presentation.XXXXXX)"
python3 -m venv "$PRESENTATION_VENV"
"$PRESENTATION_VENV/bin/pip" install --requirement docs/presentation/requirements.txt
"$PRESENTATION_VENV/bin/python" docs/presentation/scripts/build_pptx.py
"$PRESENTATION_VENV/bin/python" -m zipfile -t docs/presentation/build/deck.pptx

If dependency download is unavailable, request network approval or use an already-populated package cache; do not claim reproducibility without a successful build. The generated deck remains ignored and is not committed.

Step 5: Verify and commit

Run:

bash tests/run.sh presentation-layout
git check-ignore docs/presentation/build/deck.pptx
git ls-files 'docs/presentation/*.pptx' 'docs/presentation/**/*.pptx'
git diff --check

Expected: tests and ignore check exit 0; git ls-files prints nothing.

git add .gitignore docs/presentation tests/contracts/presentation-layout-test.sh
git commit -m "docs: presentation source를 문서 트리로 이동"

Task 6: Finalize Make validation semantics and repository documentation

Files:

  • Modify: Makefile
  • Modify: README.md
  • Modify: docs/validation-report.md
  • Modify: docs/architecture/repository-structure.md
  • Modify: scripts/README.md
  • Modify: tests/README.md
  • Modify: .mise.toml only if tool names changed during implementation
  • Create: tests/contracts/make-contract-test.sh

Step 1: Write the failing Make contract

Use temporary fake validators to prove:

make validate uses VALIDATION_PROFILE=local
make validate emits a warning and succeeds when an optional validator is absent
make check uses VALIDATION_PROFILE=full
make check fails when any declared full validator is absent
make check runs structure, contracts, manifests, docs, shell, and secret groups once each
neither target invokes or references a workflow file
documentation states that cicd-platform owns authoritative CI execution and tool delivery
documentation does not claim delivery-platform.yaml is already active for project-infra

Run:

bash tests/run.sh make-contract

Expected: non-zero until final Make and validation reporting are aligned.

Step 2: Update validation documentation with exact ownership

Document these commands:

make doctor
make validate
make check

State explicitly:

  • make validate is the friendly local profile;
  • make check is the complete repository contract and fails on missing tools;
  • .mise.toml helps developers reproduce the expected tool set locally;
  • the sibling cicd-platform repository owns triggers, runners, authoritative versions, evidence publication, and future consumer onboarding;
  • current project workflow definitions are transitional and were not modified by Phase 1.

Update docs/validation-report.md with the final gate list and the date of the latest local run. Do not record a passing result until Task 7 produces it.

Step 3: Verify and commit

Run:

bash tests/run.sh make-contract
make validate
git diff --check

Expected: tests and local validation exit 0.

git add Makefile README.md docs/validation-report.md docs/architecture/repository-structure.md scripts/README.md tests/README.md .mise.toml tests/contracts/make-contract-test.sh
git commit -m "docs: 로컬 검증과 중앙 CI 경계 명확화"

Task 7: Full Phase 1 verification and review handoff

Files:

  • Modify: docs/validation-report.md only with observed results

Step 1: Confirm no forbidden workflow or sibling-repository change

Run:

git diff --name-only d0d90ea..HEAD -- .github .gitea
git -C /home/donghyeon/workspace/desktop-server-git/cicd-platform status --short

Expected: the first command prints nothing. The second is read-only evidence; do not modify or clean any pre-existing sibling worktree changes.

Step 2: Run the complete repository contract

Run:

make check

Expected: exit 0 with no skipped validator. Capture the group summary, not secrets or rendered Secret data, in docs/validation-report.md.

Step 3: Run acceptance searches

find gitops/clusters gitops/apps gitops/platform gitops/policies -type d -name lab -print
rg -n 'example.com/environment:[[:space:]]*lab' gitops
rg -n 'vault login|\.vault-token|vault_exec_sh|vault_login_root_from_keyfile' scripts bootstrap
rg -n 'helm[[:space:]]+(install|upgrade|uninstall)' scripts
git ls-files -s '*.yaml' '*.yml' | awk '$1 != "100644" {print; bad=1} END {exit bad}'
git ls-files 'docs/presentation/*.pptx' 'docs/presentation/**/*.pptx'
git diff --check

Expected: every search prints nothing and every command exits 0.

Step 4: Review the final diff by ownership area

Run:

git diff --stat d0d90ea..HEAD
git diff --name-status d0d90ea..HEAD
git status --short --branch

Review separately:

  1. dev path/labels and operator GitOps;
  2. ForwardAuth/Keycloak rendered security;
  3. context/Vault/bootstrap/teardown safety;
  4. validation and docs;
  5. repository moves, deletes, and file modes.

Confirm that no live-cluster mutation was performed during implementation.

Step 5: Record evidence and commit

Update docs/validation-report.md only with the commands actually run, their observed status, and any environment prerequisites. Then:

git add docs/validation-report.md
git commit -m "docs: Phase 1 검증 결과 기록"

Do not merge, push, or start Phase 2 without a separate user decision.