diff --git a/.gitea/workflows/homelab-main.yml b/.gitea/workflows/homelab-main.yml new file mode 100644 index 0000000..b6e3382 --- /dev/null +++ b/.gitea/workflows/homelab-main.yml @@ -0,0 +1,75 @@ +name: Homelab Main + +"on": + push: + branches: + - main + +jobs: + validate-and-deploy: + runs-on: homelab-debian + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify Debian runner guardrails + run: | + set -euo pipefail + + test "$(uname -s)" = "Linux" + . /etc/os-release + test "${ID}" = "debian" + sudo -n true + + - name: Validate shell, Kubernetes manifests, and OpenTofu stacks + run: | + set -euo pipefail + + bash -n lab.sh + + mapfile -t manifests < <(find apps -type f \( -name '*.yaml' -o -name '*.yml' \) | sort) + kubectl --kubeconfig "${KUBECONFIG:-/home/jv/.kube/config}" apply --dry-run=server -f "${manifests[@]}" + + set +e + kubectl --kubeconfig "${KUBECONFIG:-/home/jv/.kube/config}" diff -f "${manifests[@]}" + diff_status="$?" + set -e + if (( diff_status > 1 )); then + exit "${diff_status}" + fi + + for stack in bootstrap/cluster bootstrap/platform bootstrap/apps bootstrap/edge; do + tofu -chdir="${stack}" init -input=false + tofu -chdir="${stack}" fmt -check + tofu -chdir="${stack}" validate + done + + - name: Block automatic deploy for high-impact changes + run: | + set -euo pipefail + + event_before="$( + python3 -c 'import json, os; p = os.environ.get("GITHUB_EVENT_PATH", ""); print(json.load(open(p, encoding="utf-8")).get("before", "") if p and os.path.exists(p) else "")' + )" + + if [[ -n "${event_before}" && ! "${event_before}" =~ ^0+$ ]]; then + changed_files="$(git diff --name-only "${event_before}" HEAD)" + else + changed_files="$(git diff-tree --no-commit-id --name-only -r HEAD)" + fi + printf '%s\n' "${changed_files}" + + if printf '%s\n' "${changed_files}" | grep -Eq '^(bootstrap/(cluster|platform|edge)/|lab[.]sh|[.]gitea/workflows/)'; then + echo "High-impact bootstrap, runner, or workflow changes require a manual Debian run." + exit 1 + fi + + - name: Deploy validated main branch + run: | + set -euo pipefail + + ./lab.sh up + kubectl --kubeconfig "${KUBECONFIG:-/home/jv/.kube/config}" -n argocd get applications diff --git a/README.md b/README.md index 6f97fec..898533e 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,46 @@ sudo systemctl start homelab-gitea-backup.service sudo ls -lh /var/backups/homelab/gitea ``` +## Gitea Actions + +This repo includes a Gitea Actions workflow at +`.gitea/workflows/homelab-main.yml`. It runs only on pushes to `main` and targets +a repository-scoped Debian host runner with the label `homelab-debian`. + +The workflow validates shell syntax, Kubernetes manifests, and all OpenTofu +stacks before deployment. It automatically stops when high-impact files under +`bootstrap/cluster`, `bootstrap/platform`, `bootstrap/edge`, `lab.sh`, or +`.gitea/workflows` change; those changes still require a manual Debian run. +Lower-risk app changes proceed to `./lab.sh up` after validation passes. + +Enable Actions for the repository in Gitea, then create a repository-level runner +token from: + +```text +https://lab2025.duckdns.org/git/jv/my-homelab-configs/settings/actions/runners +``` + +Register and start the Debian runner from the Debian server: + +```bash +cd ~/my-homelab-configs +GITEA_RUNNER_REGISTRATION_TOKEN='' ./lab.sh install-gitea-runner +``` + +The runner is installed as `homelab-gitea-runner.service`, runs as user `jv`, and +uses a host label instead of a Docker job container because deployment needs the +Debian host's Docker, OpenTofu, kubeconfig, SSH keys, and local state. + +The deployment job is non-interactive. User `jv` must be able to run `sudo -n +true` on the Debian host or the workflow will fail before deployment. + +Useful checks: + +```bash +systemctl status homelab-gitea-runner.service +journalctl -u homelab-gitea-runner.service -n 100 --no-pager +``` + ## Destructive Rebuilds `./lab.sh nuke` resets kubeadm, containerd runtime state, CNI files, Calico diff --git a/apps/gitea/deployment.yaml b/apps/gitea/deployment.yaml index f5f5634..c1c026f 100644 --- a/apps/gitea/deployment.yaml +++ b/apps/gitea/deployment.yaml @@ -43,6 +43,8 @@ spec: value: "true" - name: GITEA__migrations__ALLOW_LOCALNETWORKS value: "true" + - name: GITEA__actions__ENABLED + value: "true" - name: GITEA__repository__DEFAULT_PRIVATE value: public - name: GITEA__security__INSTALL_LOCK diff --git a/lab.sh b/lab.sh index 1fe2f9d..04a736e 100755 --- a/lab.sh +++ b/lab.sh @@ -467,6 +467,8 @@ cleanup() { trap cleanup EXIT kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" exec "\${pod}" -c "\${GITEA_CONTAINER}" -- rm -f "\${REMOTE_ARCHIVE}" >/dev/null 2>&1 || true +kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" exec "\${pod}" -c "\${GITEA_CONTAINER}" -- \ + sh -c 'mkdir -p /data/git/repositories && chown git:git /data/git /data/git/repositories' kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" exec "\${pod}" -c "\${GITEA_CONTAINER}" -- \ su-exec git gitea dump -c /data/gitea/conf/app.ini --file "\${REMOTE_ARCHIVE}" kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" cp -c "\${GITEA_CONTAINER}" \ @@ -516,6 +518,94 @@ backup_gitea() { sudo /usr/local/sbin/homelab-gitea-backup.sh } +install_gitea_runner() { + local runner_arch + local runner_home="${GITEA_RUNNER_HOME:-/home/jv/.local/share/gitea-runner/my-homelab-configs}" + local runner_instance="${GITEA_RUNNER_INSTANCE_URL:-https://lab2025.duckdns.org/git/}" + local runner_labels="${GITEA_RUNNER_LABELS:-homelab-debian:host}" + local runner_name="${GITEA_RUNNER_NAME:-homelab-debian-my-homelab-configs}" + local runner_token="${GITEA_RUNNER_REGISTRATION_TOKEN:-${1:-}}" + local runner_user="${GITEA_RUNNER_USER:-jv}" + local runner_version="${GITEA_ACT_RUNNER_VERSION:-0.2.11}" + local missing_packages="" + + require_debian_server "install-gitea-runner" + + case "$(dpkg --print-architecture)" in + amd64) + runner_arch="linux-amd64" + ;; + arm64) + runner_arch="linux-arm64" + ;; + *) + echo "Unsupported Debian architecture: $(dpkg --print-architecture)" >&2 + exit 1 + ;; + esac + + for package in ca-certificates curl git python3; do + if ! dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q "install ok installed"; then + missing_packages="$missing_packages $package" + fi + done + if [[ -n "${missing_packages}" ]]; then + sudo apt-get update + sudo apt-get install -y --no-install-recommends ${missing_packages} + fi + + sudo curl -fsSL \ + -o /usr/local/bin/act_runner \ + "https://gitea.com/gitea/act_runner/releases/download/v${runner_version}/act_runner-${runner_version}-${runner_arch}" + sudo chmod 0755 /usr/local/bin/act_runner + sudo chown root:root /usr/local/bin/act_runner + + sudo -u "${runner_user}" mkdir -p "${runner_home}" + + if [[ ! -f "${runner_home}/.runner" ]]; then + if [[ -z "${runner_token}" ]]; then + echo "Set GITEA_RUNNER_REGISTRATION_TOKEN to the repository-level runner token from Gitea." >&2 + exit 1 + fi + + sudo -u "${runner_user}" env \ + HOME="/home/${runner_user}" \ + GITEA_RUNNER_HOME="${runner_home}" \ + GITEA_RUNNER_INSTANCE_URL="${runner_instance}" \ + GITEA_RUNNER_REGISTRATION_TOKEN="${runner_token}" \ + GITEA_RUNNER_NAME="${runner_name}" \ + GITEA_RUNNER_LABELS="${runner_labels}" \ + bash -lc 'cd "${GITEA_RUNNER_HOME}" && /usr/local/bin/act_runner register --no-interactive --instance "${GITEA_RUNNER_INSTANCE_URL}" --token "${GITEA_RUNNER_REGISTRATION_TOKEN}" --name "${GITEA_RUNNER_NAME}" --labels "${GITEA_RUNNER_LABELS}"' + else + echo "Existing runner registration found at ${runner_home}/.runner; keeping it." + fi + + sudo tee /etc/systemd/system/homelab-gitea-runner.service >/dev/null </dev/null + sudo systemctl status homelab-gitea-runner.service --no-pager -l +} + recreate_pods_for_selector() { local namespace="$1" local selector="$2" @@ -818,11 +908,14 @@ case "${1:-}" in backup-gitea) backup_gitea ;; + install-gitea-runner) + install_gitea_runner "${2:-}" + ;; nuke) nuke ;; *) - echo "Usage: $0 {up|backup-gitea|nuke}" + echo "Usage: $0 {up|backup-gitea|install-gitea-runner|nuke}" exit 1 ;; esac