diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 073607f..9de5c92 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,4 +8,4 @@ ## Checklist -- [ ] `mise run check` passes +- [ ] `mise run ci` passes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8c2920e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI +# Quote `on` for YAML 1.1 compatibility with yamllint. +"on": + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install mise + uses: jdx/mise-action@v2 + with: + version: 2026.9.15 + cache: true + + - name: Run repository checks + run: mise run ci diff --git a/.yamllint b/.yamllint index 4eb9c28..a8d2f1d 100644 --- a/.yamllint +++ b/.yamllint @@ -1,22 +1,17 @@ --- extends: default - -ignore: - - "*/flux-system/*" - - "**/secrets.yaml" - +ignore: | + **/flux-system/** rules: - # Ansible allows yes/no; Kubernetes prefers true/false + # Ansible allows yes/no; Kubernetes and GitHub Actions prefer true/false. truthy: allowed-values: ['true', 'false', 'yes', 'no'] - # Both ecosystems use 120 line-length: max: 120 allow-non-breakable-words: true allow-non-breakable-inline-mappings: true - # Ansible files don't always need --- document-start: disable indentation: @@ -25,7 +20,8 @@ rules: comments: require-starting-space: true - comments-indentation: enable + min-spaces-from-content: 1 + comments-indentation: disable trailing-spaces: enable @@ -33,3 +29,10 @@ rules: max: 2 max-start: 0 max-end: 0 + + braces: + max-spaces-inside: 1 + + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true diff --git a/machines/.ansible-lint b/machines/.ansible-lint new file mode 100644 index 0000000..3f2be9e --- /dev/null +++ b/machines/.ansible-lint @@ -0,0 +1,5 @@ +--- +exclude_paths: + - .ansible/ + - .venv/ + - collections/ diff --git a/machines/roles/workstation/defaults/main.yml b/machines/roles/workstation/defaults/main.yml index e2705e1..5375663 100644 --- a/machines/roles/workstation/defaults/main.yml +++ b/machines/roles/workstation/defaults/main.yml @@ -1,11 +1,6 @@ --- workstation_brew_packages: - mise - - pipx - -workstation_pipx_packages: - - ansible-core - - ansible-lint # SSH keypair for homelab server access. # Role asserts it exists — never generates or overwrites. diff --git a/machines/roles/workstation/tasks/main.yml b/machines/roles/workstation/tasks/main.yml index d80211b..6d0c8d6 100644 --- a/machines/roles/workstation/tasks/main.yml +++ b/machines/roles/workstation/tasks/main.yml @@ -62,21 +62,6 @@ state: present tags: [workstation, workstation_install] -- name: List installed pipx tools - ansible.builtin.command: - cmd: pipx list --short - register: _workstation_pipx_list - changed_when: false - tags: [workstation, workstation_install] - -- name: Install pipx-managed tools - ansible.builtin.command: - cmd: "pipx install {{ item }}" - loop: "{{ workstation_pipx_packages }}" - when: item not in _workstation_pipx_list.stdout - register: _workstation_pipx_install - changed_when: _workstation_pipx_install.rc == 0 - tags: [workstation, workstation_install] - name: Ensure .ssh directory exists ansible.builtin.file: diff --git a/mise.toml b/mise.toml index eb33239..00a1c7b 100644 --- a/mise.toml +++ b/mise.toml @@ -4,10 +4,14 @@ python = "3.14.7" age = "1.3.2" flux2 = "2.9.5" helm = "4.3.0" +kind = "0.33.0" kubectl = "1.35.3" kustomize = "5.8.1" +yq = "4.53.6" +ctlptl = "0.9.6" mkcert = "1.4.4" shellcheck = "0.11.0" +actionlint = "1.7.12" sops = "3.13.3" yamllint = "1.36.1" @@ -17,7 +21,7 @@ _.python.venv = { path = "machines/.venv", create = true } SOPS_CONFIG = "{{config_root}}/.sops.yaml" # ── Ansible ─────────────────────────────────────────────────────── -[tasks.setup] +[tasks.ansible-setup] description = "Prepare the Ansible controller environment" depends = ["collections"] @@ -39,8 +43,6 @@ dir = "machines" run = [ "ansible --version", "ansible-galaxy collection list", - "ansible-inventory -i inventory/workstation.yml --list >/dev/null", - "ansible-inventory -i inventory/server.yml --list >/dev/null", ] [tasks.workstation-deploy] @@ -78,33 +80,62 @@ dir = "machines" run = "ansible-playbook playbooks/server.yml -i inventory/server.yml -c ssh --check --diff" # ── Lint ────────────────────────────────────────────────────────── +[tasks.yaml-lint] +description = "Lint tracked YAML files" +depends = ["python-deps"] +run = "git ls-files -z -- '*.yml' '*.yaml' | xargs -0 -I{} yamllint {}" + +[tasks.ansible-lint] +description = "Lint Ansible content" +depends = ["collections"] +dir = "machines" +run = "ansible-lint roles/ playbooks/" + [tasks.lint] -description = "Run all linters" +description = "Run repository linters" +depends = ["yaml-lint", "ansible-lint"] +run = [ + "actionlint .github/workflows/*.yml", + "shellcheck scripts/*.sh", +] + +[tasks.ansible-validate] +description = "Validate Ansible syntax and inventories" depends = ["collections"] dir = "machines" run = [ - "shellcheck ../scripts/*.sh", - "ansible-lint roles/", + "ansible-inventory -i inventory/workstation.yml --list >/dev/null", + "ansible-inventory -i inventory/server.yml --list >/dev/null", "ansible-playbook playbooks/workstation.yml -i inventory/workstation.yml --syntax-check", "ansible-playbook playbooks/server.yml -i inventory/server.yml --syntax-check", - "yamllint roles/ inventory/ playbooks/ ../clusters/tinycloud/infrastructure/ ../clusters/tinycloud/platform/", ] -# ── Check ───────────────────────────────────────────────────────── -[tasks.check] -description = "Run all checks" -depends = ["lint", "cluster-test"] +[tasks.ci] +description = "Run all non-mutating CI validation" +depends = [ + "lint", + "ansible-validate", + "cluster-test", +] # ── Cluster ─────────────────────────────────────────────────────── [tasks.cluster-setup] description = "Install the pinned Flux schema plugin" run = "flux plugin install schema@0.5.0" -[tasks.cluster-test] -description = "Catch invalid Flux resources before deployment" +[tasks.cluster-schema] +description = "Validate rendered resources against Flux schemas" depends = ["cluster-setup"] run = "scripts/cluster-test-config.sh" +[tasks.cluster-api-dry-run] +description = "Validate resources against a disposable Kubernetes API" +run = "scripts/cluster-api-dry-run.sh" + +[tasks.cluster-test] +description = "Run all cluster validation" +depends = ["cluster-schema", "cluster-api-dry-run"] + # ── TLS ────────────────────────────────────────────────────────── [tasks.tls-generate] description = "Generate TLS cert and store encrypted secret in Git" diff --git a/scripts/cluster-api-dry-run.sh b/scripts/cluster-api-dry-run.sh new file mode 100755 index 0000000..3e0ac05 --- /dev/null +++ b/scripts/cluster-api-dry-run.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +cluster_name="${CLUSTER_NAME:-kind-tinycloud-ci}" +manifest_file="$(mktemp)" + +cleanup() { + rm -f "$manifest_file" + ctlptl delete cluster "$cluster_name" >/dev/null 2>&1 || true +} + +trap cleanup EXIT INT TERM + +ctlptl delete cluster "$cluster_name" >/dev/null 2>&1 || true +ctlptl create cluster kind --name="$cluster_name" + +kustomize build clusters/tinycloud >"$manifest_file" + +yq 'select(.kind == "CustomResourceDefinition")' "$manifest_file" \ + | kubectl apply --server-side --field-manager=ci -f - + +while IFS= read -r namespace; do + kubectl create namespace "$namespace" \ + --dry-run=client \ + -o yaml \ + | kubectl apply --server-side --field-manager=ci -f - +done < <(yq -r 'select(.kind == "Namespace") | .metadata.name' "$manifest_file") + +kubectl wait --for=condition=Established crd --all --timeout=120s +kubectl apply \ + --server-side \ + --dry-run=server \ + --validate=strict \ + --field-manager=ci \ + -f "$manifest_file" diff --git a/scripts/cluster-test-config.sh b/scripts/cluster-test-config.sh index c97f3f3..6d286d3 100755 --- a/scripts/cluster-test-config.sh +++ b/scripts/cluster-test-config.sh @@ -16,20 +16,6 @@ validate_kustomization() { | flux schema validate --config "$schema_config" } -validate_helm_chart() { - local release_name="$1" - local chart="$2" - local version="$3" - local namespace="$4" - shift 4 - - helm template "$release_name" "$chart" \ - --version "$version" \ - --namespace "$namespace" \ - "$@" \ - | flux schema validate --config "$schema_config" -} - kustomize build "$cluster_dir" \ | flux schema validate --config "$schema_config" @@ -42,44 +28,4 @@ validate_kustomization \ platform \ "$cluster_dir/platform" \ "$cluster_dir/platform.yaml" - - -validate_helm_chart \ - semantic-router \ - oci://ghcr.io/vllm-project/charts/semantic-router \ - 0.3.0 \ - semantic-router - -validate_helm_chart \ - vllm \ - vllm-stack \ - 0.1.12 \ - vllm \ - --repo https://vllm-project.github.io/production-stack - -validate_helm_chart \ - metallb \ - metallb \ - 0.16.1 \ - metallb-system \ - --repo https://metallb.github.io/metallb - -validate_helm_chart \ - nvidia-device-plugin \ - nvidia-device-plugin \ - 0.20.0 \ - nvidia-device-plugin \ - --repo https://nvidia.github.io/k8s-device-plugin - -validate_helm_chart \ - prometheus \ - kube-prometheus-stack \ - 91.5.0 \ - prometheus \ - --repo https://prometheus-community.github.io/helm-charts - -validate_helm_chart \ - envoy-gateway \ - oci://docker.io/envoyproxy/gateway-helm \ - v1.9.1 \ - envoy-gateway-system + \ No newline at end of file