# 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.sh` → `scripts/bin/doctor.sh` - Move: `scripts/validate.sh` → `scripts/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: ```text 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 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: ```bash 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: ```bash 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 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. ```bash 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: ```text 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 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/` exists only when a real reusable primitive is selected; - `live//` 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: ```bash 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: ```bash 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 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. ```bash 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: ```text 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: ```text 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 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: ```bash 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: ```text 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 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. ```bash 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: ```text 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 `` 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 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: ```toml 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: ```text 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`: ````markdown ```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 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. ```bash 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.py` → `docs/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: ```text 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 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: ```gitignore 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: ```python ROOT = Path(__file__).resolve().parents[1] EXPORTS = ROOT / "exports" OUT = ROOT / "build" / "deck.pptx" ``` Use this `requirements.txt`: ```text python-pptx==1.0.2 Pillow==11.3.0 ``` The README must contain commands that work from any checkout: ```bash 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: ```bash 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 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. ```bash 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: ```text 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 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: ```bash 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 bash tests/run.sh make-contract make validate git diff --check ``` Expected: tests and local validation exit 0. ```bash 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: ```bash 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: ```bash 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** ```bash 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: ```bash 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: ```bash 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.