Compare commits

..
Author SHA1 Message Date
forust 73d2af73e5 fix(cicd): support Helm 4 release listing
ci / build (pull_request) Skipped
ci / Workflows (pull_request) Successful in 7s
ci / Python and tests (pull_request) Successful in 5s
ci / Compose (pull_request) Successful in 11s
ci / Shell (pull_request) Successful in 17s
ci / Formatting (pull_request) Successful in 17s
ci / YAML (pull_request) Successful in 8s
ci / Dockerfiles (pull_request) Successful in 5s
ci / Kubernetes (pull_request) Successful in 7s
ci / image-plan (pull_request) Skipped
ci / Image (${{ matrix.name }}) (pull_request) Skipped
2026-10-07 15:51:57 +02:00
forust 64253e005e Merge pull request 'fix(cicd): use Gitea artifact v4 backend' (#109) from fix/gitea-v4-artifacts into main
ci / Workflows (push) Successful in 8s
ci / Shell (push) Successful in 22s
ci / YAML (push) Successful in 9s
ci / image-plan (push) Successful in 49s
ci / Compose (push) Successful in 14s
ci / Formatting (push) Successful in 19s
ci / Python and tests (push) Successful in 8s
ci / Dockerfiles (push) Successful in 5s
ci / Kubernetes (push) Successful in 8s
ci / Image (error-pages) (push) Successful in 39s
ci / Image (forust-homepage) (push) Successful in 16s
ci / Image (xdfnx-homepage) (push) Successful in 13s
ci / build (push) Successful in 17s
Reviewed-on: #109
2026-10-07 13:35:36 +00:00
forust 69accd1752 fix(cicd): use Gitea artifact v4 backend
ci / Shell (push) Skipped
ci / Formatting (push) Skipped
ci / Python and tests (push) Skipped
ci / YAML (push) Skipped
ci / Dockerfiles (push) Skipped
ci / Kubernetes (push) Skipped
ci / Compose (pull_request) Successful in 11s
ci / Kubernetes (pull_request) Successful in 7s
ci / Compose (push) Skipped
ci / Workflows (push) Skipped
ci / Workflows (pull_request) Successful in 7s
ci / Shell (pull_request) Successful in 18s
ci / Formatting (pull_request) Successful in 20s
ci / Python and tests (pull_request) Successful in 7s
ci / YAML (pull_request) Successful in 8s
ci / Dockerfiles (pull_request) Successful in 4s
ci / image-plan (pull_request) Skipped
ci / Image (${{ matrix.name }}) (pull_request) Skipped
ci / build (pull_request) Skipped
2026-10-07 15:29:49 +02:00
forust 0cf4b08a95 Merge pull request 'fix(cicd): isolate pull request runner jobs' (#108) from fix/cicd-pr-runner into main
ci / Workflows (push) Successful in 7s
ci / Shell (push) Successful in 21s
ci / Python and tests (push) Successful in 5s
ci / Compose (push) Successful in 15s
ci / Formatting (push) Successful in 17s
ci / Kubernetes (push) Successful in 6s
ci / YAML (push) Successful in 9s
ci / Dockerfiles (push) Successful in 4s
renovate-ci / validate-renovate (push) Successful in 2m21s
ci / image-plan (push) Successful in 18s
ci / Image (error-pages) (push) Successful in 13s
ci / Image (xdfnx-homepage) (push) Successful in 12s
ci / Image (forust-homepage) (push) Successful in 11s
ci / build (push) Successful in 16s
Reviewed-on: #108
2026-10-07 13:03:01 +00:00
forust 86df5d9048 docs(cicd): document user-scoped PR runner
ci / Compose (push) Skipped
ci / Workflows (push) Skipped
ci / Shell (push) Skipped
ci / Formatting (push) Skipped
ci / Python and tests (push) Skipped
ci / Dockerfiles (push) Skipped
ci / Workflows (pull_request) Successful in 14s
ci / Shell (pull_request) Successful in 33s
ci / Formatting (pull_request) Successful in 36s
ci / YAML (pull_request) Successful in 28s
ci / Kubernetes (pull_request) Successful in 11s
ci / YAML (push) Skipped
ci / Kubernetes (push) Skipped
ci / Compose (pull_request) Successful in 23s
ci / Python and tests (pull_request) Successful in 16s
ci / Dockerfiles (pull_request) Successful in 11s
ci / Image (${{ matrix.name }}) (pull_request) Skipped
ci / build (pull_request) Skipped
ci / image-plan (pull_request) Skipped
2026-10-07 14:50:32 +02:00
forust d7441bbbc2 fix(cicd): isolate pull request runner jobs
ci / Compose (push) Skipped
ci / Workflows (push) Skipped
ci / Shell (push) Skipped
ci / Formatting (push) Skipped
ci / Python and tests (push) Skipped
ci / Compose (pull_request) Successful in 37s
ci / Shell (pull_request) Successful in 18s
ci / YAML (push) Skipped
ci / Dockerfiles (push) Skipped
ci / Kubernetes (push) Skipped
ci / Workflows (pull_request) Successful in 8s
ci / Python and tests (pull_request) Successful in 11s
ci / Kubernetes (pull_request) Successful in 8s
ci / Formatting (pull_request) Successful in 18s
ci / YAML (pull_request) Successful in 11s
ci / Dockerfiles (pull_request) Successful in 7s
ci / image-plan (pull_request) Skipped
ci / Image (${{ matrix.name }}) (pull_request) Skipped
ci / build (pull_request) Skipped
2026-10-07 14:39:57 +02:00
forust 2be089e048 Merge pull request 'Collect Headscale, NetBird, Gitea and Immich metrics' (#100) from feat/service-metrics into main
ci / Compose (push) Successful in 11s
ci / Workflows (push) Successful in 7s
ci / YAML (push) Successful in 9s
ci / Dockerfiles (push) Successful in 5s
ci / Shell (push) Successful in 16s
ci / Formatting (push) Successful in 17s
ci / Python and tests (push) Successful in 7s
ci / Kubernetes (push) Successful in 6s
ci / image-plan (push) Successful in 15s
ci / Image (error-pages) (push) Successful in 12s
ci / Image (forust-homepage) (push) Successful in 12s
ci / Image (xdfnx-homepage) (push) Successful in 11s
ci / build (push) Successful in 13s
Reviewed-on: #100
2026-10-07 11:46:16 +00:00
forust 83b2e68371 feat(metrics): collect Headscale, NetBird, Gitea and Immich metrics 2026-10-07 11:46:16 +00:00
forust 597f64cbb0 Merge pull request 'Show CI checks and deployment results' (#99) from codex/ci-visible-checks into main
ci / Workflows (push) Successful in 7s
ci / Formatting (push) Successful in 17s
ci / Python and tests (push) Successful in 7s
ci / YAML (push) Successful in 9s
ci / Compose (push) Successful in 11s
ci / Shell (push) Successful in 16s
ci / Kubernetes (push) Successful in 7s
ci / Dockerfiles (push) Successful in 5s
ci / Image (forust-homepage) (push) Successful in 12s
ci / Image (xdfnx-homepage) (push) Successful in 11s
renovate-ci / validate-renovate (push) Successful in 11s
ci / image-plan (push) Successful in 16s
ci / Image (error-pages) (push) Successful in 41s
ci / build (push) Successful in 14s
Reviewed-on: #99
2026-10-07 11:46:05 +00:00
23 changed files with 312 additions and 45 deletions

No files matched your search

+1
View File
@@ -7,4 +7,5 @@ self-hosted-runner:
labels: labels:
- arch - arch
- homelab - homelab
- homelab-pr
- prod - prod
+35 -6
View File
@@ -1,17 +1,20 @@
# Homelab CI/CD # Homelab CI/CD
The native Gitea runner runs on **vps**; production runs on **workstation**. The native Gitea runners run on **vps**; production runs on **workstation**.
Compose, workflow, shell, Python, formatting, YAML, Dockerfile and Kubernetes Main-branch checks and image builds use `homelab:host`. Pull request and
checks appear as separate jobs. Jobs run on `homelab:host`, one at a time; the non-main checks use `homelab-pr:host` under a separate account without Docker
build waits for every check to pass. CI and deploy runs also show a summary with access. The `homelab-pr` runner is registered at User scope for `forust`, so
any repository under that account can schedule jobs that request this label.
Each runner accepts one job at a time; the build waits for every check to pass.
CI and deploy runs also show a summary with
the release SHA, image build or reuse results, deploy mode, selected services, the release SHA, image build or reuse results, deploy mode, selected services,
and image digests. Failed runs keep a summary of completed image builds, stage and image digests. Failed runs keep a summary of completed image builds, stage
results, apply results, and recorded Kubernetes recovery. The final deploy results, apply results, and recorded Kubernetes recovery. The final deploy
summary is in the smoke job; earlier jobs show the state observed at that time. summary is in the smoke job; earlier jobs show the state observed at that time.
Apply success is separate from health and recovery. Update the installed Apply success is separate from health and recovery. Update the installed
workstation controller with `setup-workstation.sh` when no deploy is running. workstation controller with `setup-workstation.sh` when no deploy is running.
No job images or Kubernetes credentials No job images or Kubernetes credentials are needed on the VPS. Builds use one
are needed on the VPS. Builds use one pinned BuildKit helper container. CI and deploy are separate workflows. pinned BuildKit helper container. CI and deploy are separate workflows.
## Runner installation ## Runner installation
@@ -37,6 +40,32 @@ pushes directly to the registry, and caps retained local cache at 1 GiB with a
2 GiB free-space target. This is not a hard limit on peak build disk usage. 2 GiB free-space target. This is not a hard limit on peak build disk usage.
Nothing runs `docker system prune`, removes unrelated images, or deletes volumes. Nothing runs `docker system prune`, removes unrelated images, or deletes volumes.
### Pull request runner
Install the unprivileged host runner on the VPS:
```sh
sudo bash .gitea/runner/setup-pr-runner.sh
```
Get a registration token from the user Actions runner settings. Run the
installer in a terminal. It asks for the token without echoing it, registers the
runner as `homelab-pr` with label `homelab-pr:host`, then enables the service.
The work directory is `/var/lib/gitea-pr-runner`. Confirm that Gitea lists the
runner as User scope before merging the workflow change. An unmatched label can
fall back to the default job image.
Renovate PR validation uses `pull_request_target`, which reads the workflow from
the base branch. It checks out the PR head only after runner selection and runs
that code on `homelab-pr`. Keep this workflow read-only and do not add secrets.
The PR runner has a separate home and tool cache. Do not add it to the `docker`
group or give it access to `/var/run/docker.sock`. It runs repository code from
pull requests, so keep its registration and permissions separate from the
trusted `homelab` runner. This separates users and host permissions, but both
runners still share the VPS kernel and network. Use a disposable VM if PRs from
untrusted external authors must be fully isolated.
## Workstation setup ## Workstation setup
As the existing SSH deploy user on workstation: As the existing SSH deploy user on workstation:
+8
View File
@@ -0,0 +1,8 @@
runner:
file: /var/lib/gitea-pr-runner/.runner
capacity: 1
timeout: 5h
labels:
- homelab-pr:host
cache:
enabled: false
+27
View File
@@ -0,0 +1,27 @@
[Unit]
Description=Gitea Actions untrusted pull request runner
After=network-online.target
Wants=network-online.target
[Service]
User=gitea-pr-runner
Group=gitea-pr-runner
WorkingDirectory=/var/lib/gitea-pr-runner
Environment=HOME=/var/lib/gitea-pr-runner
Environment=PATH=/var/lib/gitea-pr-runner/.cache/homelab-ci/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-pr-runner/config.yaml
Restart=on-failure
RestartSec=5
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
UMask=0077
[Install]
WantedBy=multi-user.target
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Install a native runner for untrusted PR jobs without Docker access.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ "$(id -u)" -eq 0 ] || { echo 'Run with sudo on the runner host' >&2; exit 1; }
for tool in cp cut date getent id install runuser systemctl useradd; do
command -v "$tool" >/dev/null || { echo "Install missing prerequisite: $tool" >&2; exit 1; }
done
command -v /usr/local/bin/gitea-runner >/dev/null || {
echo 'Install gitea-runner 3.0.2 at /usr/local/bin/gitea-runner first' >&2
exit 1
}
id gitea-pr-runner >/dev/null 2>&1 || \
useradd --system --create-home --home-dir /var/lib/gitea-pr-runner --shell /usr/bin/bash gitea-pr-runner
runner_home="$(getent passwd gitea-pr-runner | cut -d: -f6)"
[ "$runner_home" = /var/lib/gitea-pr-runner ] || {
echo 'Unexpected PR runner home; inspect the existing service first' >&2
exit 1
}
case " $(id -nG gitea-pr-runner) " in
*' docker '*)
echo 'The PR runner account must not belong to the docker group' >&2
exit 1
;;
esac
install -d -m 0755 /etc/gitea-pr-runner
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
for existing in /etc/gitea-pr-runner/config.yaml /etc/systemd/system/gitea-pr-runner.service; do
[ ! -f "$existing" ] || cp -p "$existing" "$existing.before-$stamp"
done
install -m 0644 "$here/pr-config.yaml" /etc/gitea-pr-runner/config.yaml
install -m 0644 "$here/pr-runner.service" /etc/systemd/system/gitea-pr-runner.service
if [ ! -f /var/lib/gitea-pr-runner/.runner ]; then
read -r -s -p 'Enter the Gitea repository runner registration token: ' runner_token
printf '\n'
[ -n "$runner_token" ] || { echo 'Runner token is required' >&2; exit 1; }
export GITEA_RUNNER_REGISTRATION_TOKEN="$runner_token"
unset runner_token
runuser --preserve-environment -u gitea-pr-runner -- \
/usr/local/bin/gitea-runner register \
--config /etc/gitea-pr-runner/config.yaml \
--instance https://gitea.forust.xyz \
--name homelab-pr \
--labels homelab-pr:host \
--no-interactive
unset GITEA_RUNNER_REGISTRATION_TOKEN
fi
chmod 0600 /var/lib/gitea-pr-runner/.runner
systemctl daemon-reload
systemctl enable --now gitea-pr-runner.service
systemctl restart gitea-pr-runner.service
echo "PR runner ready. Configuration backups: *.before-$stamp"
+13 -13
View File
@@ -14,7 +14,7 @@ concurrency:
jobs: jobs:
compose: compose:
name: Compose name: Compose
runs-on: homelab runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -66,7 +66,7 @@ jobs:
fi fi
workflows: workflows:
name: Workflows name: Workflows
runs-on: homelab runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -102,7 +102,7 @@ jobs:
fi fi
shell: shell:
name: Shell name: Shell
runs-on: homelab runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -146,7 +146,7 @@ jobs:
fi fi
formatting: formatting:
name: Formatting name: Formatting
runs-on: homelab runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -194,7 +194,7 @@ jobs:
fi fi
python: python:
name: Python and tests name: Python and tests
runs-on: homelab runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -232,7 +232,7 @@ jobs:
fi fi
yaml: yaml:
name: YAML name: YAML
runs-on: homelab runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -280,7 +280,7 @@ jobs:
fi fi
dockerfiles: dockerfiles:
name: Dockerfiles name: Dockerfiles
runs-on: homelab runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -326,7 +326,7 @@ jobs:
fi fi
kubernetes: kubernetes:
name: Kubernetes name: Kubernetes
runs-on: homelab runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -395,7 +395,7 @@ jobs:
run: python3 .gitea/workflows/release.py prepare --output build-plan.json run: python3 .gitea/workflows/release.py prepare --output build-plan.json
- name: Store the image plan - name: Store the image plan
id: artifact id: artifact
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: build-plan name: build-plan
path: build-plan.json path: build-plan.json
@@ -435,7 +435,7 @@ jobs:
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Download the checked image plan - name: Download the checked image plan
id: inputs id: inputs
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: build-plan name: build-plan
- name: Build or reuse this image - name: Build or reuse this image
@@ -447,7 +447,7 @@ jobs:
run: python3 .gitea/workflows/release.py image --image "$IMAGE_NAME" --output image.json run: python3 .gitea/workflows/release.py image --image "$IMAGE_NAME" --output image.json
- name: Store the image result - name: Store the image result
id: artifact id: artifact
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: image-${{ matrix.name }} name: image-${{ matrix.name }}
path: image.json path: image.json
@@ -482,7 +482,7 @@ jobs:
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Download all image results - name: Download all image results
id: inputs id: inputs
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
path: artifacts path: artifacts
- name: Pin SHA tags and write the complete release - name: Pin SHA tags and write the complete release
@@ -495,7 +495,7 @@ jobs:
--plan artifacts/build-plan/build-plan.json --plan artifacts/build-plan/build-plan.json
- name: Store commit release - name: Store commit release
id: artifact id: artifact
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: release-${{ github.sha }} name: release-${{ github.sha }}
path: release.json path: release.json
+2 -1
View File
@@ -141,7 +141,8 @@ def make_plan(directory):
planner = load_module('deploy_plan', source / '.gitea/workflows/deploy-plan.py') planner = load_module('deploy_plan', source / '.gitea/workflows/deploy-plan.py')
request = json.loads((directory / 'request.json').read_text()) request = json.loads((directory / 'request.json').read_text())
previous = json.loads((STATE / 'last-success.json').read_text()) if (STATE / 'last-success.json').exists() else None previous = json.loads((STATE / 'last-success.json').read_text()) if (STATE / 'last-success.json').exists() else None
helm = json.loads(command('helm', 'list', '--all', '-A', '-o', 'json')) # Helm 4 lists every release status by default and removed the --all flag.
helm = json.loads(command('helm', 'list', '-A', '-o', 'json'))
plan = planner.make_plan(source, CONFIG_REPO, request['release'], previous, request['mode'], helm) plan = planner.make_plan(source, CONFIG_REPO, request['release'], previous, request['mode'], helm)
if request['refresh_images']: if request['refresh_images']:
plan['selected']['compose'] = plan['active']['compose'] plan['selected']['compose'] = plan['active']['compose']
+2 -1
View File
@@ -180,7 +180,8 @@ save_snapshot() {
| select(any(.metadata.ownerReferences[]?; .uid == $w.metadata.uid)) | select(any(.metadata.ownerReferences[]?; .uid == $w.metadata.uid))
| select($w.kind != "StatefulSet" or .metadata.name == $w.status.currentRevision) | .revision] | max // 0) end) | select($w.kind != "StatefulSet" or .metadata.name == $w.status.currentRevision) | .revision] | max // 0) end)
}]' "$dir/workloads.json" >"$dir/revisions.json" || return 1 }]' "$dir/workloads.json" >"$dir/revisions.json" || return 1
releases="$(helm list --all -A -o json)" || return 1 # Helm 4 lists every release status by default and removed the --all flag.
releases="$(helm list -A -o json)" || return 1
for entry in "${HELM_RELEASES[@]}"; do for entry in "${HELM_RELEASES[@]}"; do
IFS='|' read -r release _ namespace _ _ _ <<<"$entry" IFS='|' read -r release _ namespace _ _ _ <<<"$entry"
if ! jq -e --arg r "$release" --arg n "$namespace" \ if ! jq -e --arg r "$release" --arg n "$namespace" \
+27 -17
View File
@@ -1,7 +1,9 @@
name: renovate-ci name: renovate-ci
on: on:
pull_request: # Read the workflow from the trusted base branch. PR code runs only on the
# unprivileged runner selected below.
pull_request_target:
paths: paths:
- "renovate/**" - "renovate/**"
- ".gitea/workflows/renovate-ci.yaml" - ".gitea/workflows/renovate-ci.yaml"
@@ -26,37 +28,47 @@ permissions:
jobs: jobs:
validate-renovate: validate-renovate:
runs-on: homelab runs-on: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }}
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
# renovate/k8s/cronjob.yaml is the single source of truth for the image tag, # renovate/k8s/cronjob.yaml is the single source of truth for the version.
# so the same version that runs in the cluster is the one validated here. - name: Resolve the deployed Renovate version
- name: Resolve the deployed Renovate image
id: image id: image
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
image="$(sed -n 's|.*image:[[:space:]]*\(renovate/renovate:[^[:space:]]*\).*|\1|p' \ image="$(sed -n 's|.*image:[[:space:]]*\(renovate/renovate:[^[:space:]]*\).*|\1|p' \
renovate/k8s/cronjob.yaml | head -1)" renovate/k8s/cronjob.yaml | head -1)"
if [ -z "$image" ]; then if [[ ! "$image" =~ ^renovate/renovate:([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
echo "::error::no renovate/renovate image found in renovate/k8s/cronjob.yaml" echo "::error::expected a pinned renovate/renovate semantic version in renovate/k8s/cronjob.yaml"
exit 1 exit 1
fi fi
echo "using $image" version="${BASH_REMATCH[1]}"
echo "image=$image" >> "$GITHUB_OUTPUT" echo "using Renovate $version"
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
- name: Validate Renovate repository config - name: Prepare pinned validation tools
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
docker run --rm \ tools_dir="$(bash .gitea/workflows/install-ci-tools.sh kubeconform node)"
-v "$PWD/renovate:/opt/renovate:ro" \ echo "$tools_dir" >> "$GITHUB_PATH"
-e RENOVATE_CONFIG_FILE=/opt/renovate/renovate.json \
"${{ steps.image.outputs.image }}" \ - name: Validate Renovate repository config
renovate-config-validator /opt/renovate/renovate.json shell: bash
env:
RENOVATE_VERSION: ${{ steps.image.outputs.version }}
run: |
set -euo pipefail
npm_cache="$(mktemp -d "${RUNNER_TEMP:-/tmp}/renovate-npm-cache.XXXXXXXX")"
trap 'rm -rf "$npm_cache"' EXIT
NPM_CONFIG_CACHE="$npm_cache" RENOVATE_CONFIG_FILE="$PWD/renovate/renovate.json" \
npm exec --yes --package="renovate@${RENOVATE_VERSION}" -- renovate-config-validator
# The CronJob cannot read the repository, so renovate/k8s/configmap.yaml # The CronJob cannot read the repository, so renovate/k8s/configmap.yaml
# carries an inlined copy of the config. Fail if it no longer matches. # carries an inlined copy of the config. Fail if it no longer matches.
@@ -70,8 +82,6 @@ jobs:
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
tools_dir="$(bash .gitea/workflows/install-ci-tools.sh kubeconform)"
export PATH="$tools_dir:$PATH"
kubeconform \ kubeconform \
-strict \ -strict \
-ignore-missing-schemas \ -ignore-missing-schemas \
+11 -5
View File
@@ -32,11 +32,14 @@ concurrency:
jobs: jobs:
run-renovate: run-renovate:
if: github.ref == 'refs/heads/main'
runs-on: homelab runs-on: homelab
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: refs/heads/main
# renovate/k8s/cronjob.yaml is the single source of truth for the image tag. # renovate/k8s/cronjob.yaml is the single source of truth for the image tag.
# Reading it here means this workflow validates and runs the exact version # Reading it here means this workflow validates and runs the exact version
@@ -48,21 +51,23 @@ jobs:
set -euo pipefail set -euo pipefail
image="$(sed -n 's|.*image:[[:space:]]*\(renovate/renovate:[^[:space:]]*\).*|\1|p' \ image="$(sed -n 's|.*image:[[:space:]]*\(renovate/renovate:[^[:space:]]*\).*|\1|p' \
renovate/k8s/cronjob.yaml | head -1)" renovate/k8s/cronjob.yaml | head -1)"
if [ -z "$image" ]; then if [[ ! "$image" =~ ^renovate/renovate:[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::no renovate/renovate image found in renovate/k8s/cronjob.yaml" echo "::error::expected a pinned renovate/renovate semantic version in renovate/k8s/cronjob.yaml"
exit 1 exit 1
fi fi
echo "using $image" echo "using $image"
echo "image=$image" >> "$GITHUB_OUTPUT" printf 'image=%s\n' "$image" >> "$GITHUB_OUTPUT"
- name: Validate Renovate config - name: Validate Renovate config
shell: bash shell: bash
env:
RENOVATE_IMAGE: ${{ steps.image.outputs.image }}
run: | run: |
set -euo pipefail set -euo pipefail
docker run --rm \ docker run --rm \
-v "$PWD/renovate/renovate.json:/opt/renovate/renovate.json:ro" \ -v "$PWD/renovate/renovate.json:/opt/renovate/renovate.json:ro" \
-e RENOVATE_CONFIG_FILE=/opt/renovate/renovate.json \ -e RENOVATE_CONFIG_FILE=/opt/renovate/renovate.json \
"${{ steps.image.outputs.image }}" \ "$RENOVATE_IMAGE" \
renovate-config-validator renovate-config-validator
- name: Run Renovate - name: Run Renovate
@@ -73,6 +78,7 @@ jobs:
RENOVATE_REPOSITORIES: ${{ inputs.repositories }} RENOVATE_REPOSITORIES: ${{ inputs.repositories }}
RENOVATE_DRY_RUN: ${{ inputs.dry_run && 'full' || '' }} RENOVATE_DRY_RUN: ${{ inputs.dry_run && 'full' || '' }}
LOG_LEVEL: ${{ inputs.log_level }} LOG_LEVEL: ${{ inputs.log_level }}
RENOVATE_IMAGE: ${{ steps.image.outputs.image }}
run: | run: |
set -euo pipefail set -euo pipefail
@@ -89,4 +95,4 @@ jobs:
-e RENOVATE_CONFIG_FILE=/opt/renovate/renovate.json \ -e RENOVATE_CONFIG_FILE=/opt/renovate/renovate.json \
-e RENOVATE_BASE_DIR=/tmp/renovate \ -e RENOVATE_BASE_DIR=/tmp/renovate \
-e LOG_LEVEL="${LOG_LEVEL:-info}" \ -e LOG_LEVEL="${LOG_LEVEL:-info}" \
"${{ steps.image.outputs.image }}" "$RENOVATE_IMAGE"
+2
View File
@@ -20,6 +20,8 @@ data:
GITEA__mailer__ENABLED: "false" GITEA__mailer__ENABLED: "false"
GITEA__metrics__ENABLED: "true"
# No code/issue search needed: bleve reindexes the whole issue index on # No code/issue search needed: bleve reindexes the whole issue index on
# every pod restart (cron.rebuild_issue_indexer RUN_AT_START) and hammers # every pod restart (cron.rebuild_issue_indexer RUN_AT_START) and hammers
# the rotational disk for an hour. "db" serves issue search from postgres. # the rotational disk for an hour. "db" serves issue search from postgres.
+2
View File
@@ -3,6 +3,8 @@ kind: Service
metadata: metadata:
name: gitea-service name: gitea-service
namespace: gitea namespace: gitea
labels:
app: gitea
spec: spec:
selector: selector:
app: gitea app: gitea
+2 -1
View File
@@ -7,7 +7,8 @@ spec:
entryPoints: entryPoints:
- websecure - websecure
routes: routes:
- match: Host(`gitea.forust.xyz`) || Host(`git.forust.xyz`) # Metrics are scraped directly through the cluster Service.
- match: (Host(`gitea.forust.xyz`) || Host(`git.forust.xyz`)) && !PathPrefix(`/metrics`)
kind: Rule kind: Rule
services: services:
- name: gitea-service - name: gitea-service
+16
View File
@@ -0,0 +1,16 @@
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: gitea
namespace: gitea
labels:
release: prometheus-stack
spec:
selector:
matchLabels:
app: gitea
endpoints:
- port: http
path: /metrics
interval: 30s
scrapeTimeout: 10s
+1 -1
View File
@@ -7,7 +7,7 @@
# # Dev server_url # # Dev server_url
# server_url: https://hs.dev_internal_domain.internal # server_url: https://hs.dev_internal_domain.internal
listen_addr: 0.0.0.0:8080 listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090 metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 127.0.0.1:50443 grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false grpc_allow_insecure: false
noise: noise:
@@ -3,6 +3,8 @@ kind: Service
metadata: metadata:
name: headscale-server-external name: headscale-server-external
namespace: headscale namespace: headscale
labels:
app: headscale
spec: spec:
ports: ports:
- port: 8080 - port: 8080
+18
View File
@@ -0,0 +1,18 @@
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
name: headscale
namespace: headscale
labels:
release: prometheus-stack
spec:
# The external Service has a manually managed EndpointSlice, not Endpoints.
discoveryRole: endpointslice
selector:
matchLabels:
app: headscale
endpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
+4
View File
@@ -6,6 +6,10 @@ metadata:
data: data:
TZ: "Europe/Bratislava" TZ: "Europe/Bratislava"
IMMICH_TELEMETRY_INCLUDE: "all"
IMMICH_API_METRICS_PORT: "8081"
IMMICH_MICROSERVICES_METRICS_PORT: "8082"
# The database in this namespace, not the shared one in the database # The database in this namespace, not the shared one in the database
# namespace: v3 needs VectorChord, and only the dedicated image carries it. # namespace: v3 needs VectorChord, and only the dedicated image carries it.
DB_HOSTNAME: "immich-postgres" DB_HOSTNAME: "immich-postgres"
+12
View File
@@ -3,6 +3,8 @@ kind: Service
metadata: metadata:
name: immich-service name: immich-service
namespace: immich namespace: immich
labels:
app: immich
spec: spec:
selector: selector:
app: immich app: immich
@@ -10,6 +12,12 @@ spec:
- name: http - name: http
port: 2283 port: 2283
targetPort: 2283 targetPort: 2283
- name: api-metrics
port: 8081
targetPort: api-metrics
- name: worker-metrics
port: 8082
targetPort: worker-metrics
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
@@ -41,6 +49,10 @@ spec:
ports: ports:
- name: http - name: http
containerPort: 2283 containerPort: 2283
- name: api-metrics
containerPort: 8081
- name: worker-metrics
containerPort: 8082
volumeMounts: volumeMounts:
- name: immich-data - name: immich-data
mountPath: /data mountPath: /data
+20
View File
@@ -0,0 +1,20 @@
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: immich
namespace: immich
labels:
release: prometheus-stack
spec:
selector:
matchLabels:
app: immich
endpoints:
- port: api-metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
- port: worker-metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
+9
View File
@@ -3,6 +3,8 @@ kind: Service
metadata: metadata:
name: netbird-server-service name: netbird-server-service
namespace: netbird namespace: netbird
labels:
app: netbird-server
spec: spec:
selector: selector:
app: netbird-server app: netbird-server
@@ -11,6 +13,10 @@ spec:
name: http name: http
targetPort: 80 targetPort: 80
protocol: TCP protocol: TCP
- port: 9090
name: metrics
targetPort: metrics
protocol: TCP
- port: 3478 - port: 3478
name: stun name: stun
targetPort: 3478 targetPort: 3478
@@ -59,6 +65,9 @@ spec:
- containerPort: 80 - containerPort: 80
name: http name: http
protocol: TCP protocol: TCP
- containerPort: 9090
name: metrics
protocol: TCP
- containerPort: 3478 - containerPort: 3478
name: stun name: stun
protocol: UDP protocol: UDP
+16
View File
@@ -0,0 +1,16 @@
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: netbird-server
namespace: netbird
labels:
release: prometheus-stack
spec:
selector:
matchLabels:
app: netbird-server
endpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
+26
View File
@@ -15,3 +15,29 @@ The VictoriaMetrics Operator chart and its CRDs are installed before the
Kubernetes manifests by the normal deploy workflow. On a cluster where the Kubernetes manifests by the normal deploy workflow. On a cluster where the
operator CRDs are not installed yet, CI skips the server-side dry-run of the operator CRDs are not installed yet, CI skips the server-side dry-run of the
`VMAgent` resource; the deploy installs the chart before applying that resource. `VMAgent` resource; the deploy installs the chart before applying that resource.
## Application metrics
The application ServiceMonitors use a 30s interval and a 10s timeout:
- Headscale: the external Service points to the Compose host on port 19090.
A VMServiceScrape uses EndpointSlice discovery for this manually managed target.
The Compose configuration must bind metrics to `0.0.0.0:9090`.
- NetBird: the combined server exports `/metrics` on port 9090. The existing
`server.metricsPort` setting enables the listener.
- Gitea: `GITEA__metrics__ENABLED` enables `/metrics` on the HTTP port. The public
ingress excludes this path. The monitor uses the internal Service directly.
- Immich: `IMMICH_TELEMETRY_INCLUDE=all` enables API and worker metrics on ports
8081 and 8082. The monitor scrapes both ports on each server replica.
Deploy through the existing CI and deploy workflow. Gitea and Immich reload their
ConfigMap changes through Reloader. Check the VMAgent targets after deployment
and query `up{scraper="victoria",namespace=~"netbird|gitea|immich|headscale"}` in
VictoriaMetrics. All targets should report 1.
For rollback, revert the application metrics changes, run CI, and deploy the
revert. Remove the three application ServiceMonitors and the Headscale VMServiceScrape explicitly: the deployment
workflow applies manifests and does not prune removed resources.
For Headscale rollback, remove its VMServiceScrape and Service label, restore the
previous Compose metrics bind address, and restart only the Headscale service.