Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@

## Checklist

- [ ] `mise run check` passes
- [ ] `mise run ci` passes
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 12 additions & 9 deletions .yamllint
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -25,11 +20,19 @@ rules:

comments:
require-starting-space: true
comments-indentation: enable
min-spaces-from-content: 1
comments-indentation: disable

trailing-spaces: enable

empty-lines:
max: 2
max-start: 0
max-end: 0

braces:
max-spaces-inside: 1

octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true
5 changes: 5 additions & 0 deletions machines/.ansible-lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
exclude_paths:
- .ansible/
- .venv/
- collections/
5 changes: 0 additions & 5 deletions machines/roles/workstation/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
15 changes: 0 additions & 15 deletions machines/roles/workstation/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
57 changes: 44 additions & 13 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"]

Expand All @@ -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]
Expand Down Expand Up @@ -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"
Expand Down
35 changes: 35 additions & 0 deletions scripts/cluster-api-dry-run.sh
Original file line number Diff line number Diff line change
@@ -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"
56 changes: 1 addition & 55 deletions scripts/cluster-test-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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

Loading