24 KiB
Project Infra Phase 1 validation and repository structure Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto execute this plan task by task. Usesuperpowers:test-driven-developmentfor validator changes andsuperpowers:verification-before-completionbefore 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.tomlonly 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.exampleunchanged 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.shis the user-facing tool diagnostic entrypoint.scripts/ci/validate-structure.shvalidates filesystem shape, sensitive file rules, source modes, shell syntax, and activated IaC syntax.scripts/ci/validate.showns the project contract and sourcesscripts/lib/kustomize.shfor every render.VALIDATION_PROFILE=localmay skip optional heavyweight validators with an explicit warning; core render, test, structure, docs-YAML syntax, and path contracts never skip.VALIDATION_PROFILE=fulltreats every declared validator as required.make validateruns the local profile once.make checkruns the full profile once and fails if a required tool is absent.validate_yaml_fences DOCS_ROOTis sourceable fromscripts/ci/validate-docs.shfor 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:
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:
doctorruns@./scripts/bin/doctor.sh.validate-structureruns@./scripts/ci/validate-structure.sh.validate-projectruns@VALIDATION_PROFILE=local bash ./scripts/ci/validate.sh.validatedepends onvalidate-structure validate-projectand has no second recipe.checkdepends onvalidate-structureand 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
*.yamland*.ymlsource to100644
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.mdto 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:
- write the exact block to a mode-
0600temporary file; - run
yq eval-all '.' BLOCKunconditionally; - classify it as a complete Kubernetes example only when it is not bad/abbreviated and every YAML document contains non-empty
apiVersion,kind, andmetadata.name; - run kubeconform with the repository CRD catalog configuration for complete examples;
- run kube-linter with
.kube-linter.yamlfor complete examples; - report
file:opening-lineon 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.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:
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.tomlonly 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 validateis the friendly local profile;make checkis the complete repository contract and fails on missing tools;.mise.tomlhelps developers reproduce the expected tool set locally;- the sibling
cicd-platformrepository 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.mdonly 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:
- dev path/labels and operator GitOps;
- ForwardAuth/Keycloak rendered security;
- context/Vault/bootstrap/teardown safety;
- validation and docs;
- 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.