diff --git a/.gitea/workflows/renovate-ci.yaml b/.gitea/workflows/renovate-ci.yaml new file mode 100644 index 0000000..2639963 --- /dev/null +++ b/.gitea/workflows/renovate-ci.yaml @@ -0,0 +1,45 @@ +name: renovate-ci + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +jobs: + validate-renovate: + runs-on: [self-hosted, linux, arch, homelab] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate Renovate Compose draft + shell: bash + run: | + set -euo pipefail + trap 'rm -f renovate/.env' EXIT + printf '%s\n' \ + 'RENOVATE_ENDPOINT=https://gitea.example/api/v1' \ + 'RENOVATE_TOKEN=test-token' \ + 'RENOVATE_REPOSITORIES=forust/homelab' \ + > renovate/.env + docker compose -f renovate/renovate-compose.yaml config --quiet + + - name: Validate Kubernetes manifests + shell: bash + run: | + set -euo pipefail + for manifest in renovate/k8s/namespace.yaml renovate/k8s/configmap.yaml renovate/k8s/cronjob.yaml; do + kubectl apply --dry-run=client --validate=false -f "$manifest" >/dev/null + done + + - name: Validate Renovate repository config + shell: bash + run: | + set -euo pipefail + docker run --rm \ + -v "$PWD:/work" \ + -w /work \ + renovate/renovate:44.83.2 \ + renovate-config-validator renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..340952e --- /dev/null +++ b/renovate.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "enabledManagers": [ + "docker-compose", + "kubernetes" + ], + "kubernetes": { + "managerFilePatterns": [ + "/k8s/.+\\.ya?ml$/" + ] + }, + "packageRules": [ + { + "description": "Keep private homelab images unchanged", + "matchDatasources": [ + "docker" + ], + "matchPackageNames": [ + "/gcr\\.forust\\.xyz\\/forust\\/.+/" + ], + "enabled": false + }, + { + "description": "Require approval for major upgrades", + "matchUpdateTypes": [ + "major" + ], + "dependencyDashboardApproval": true, + "automerge": false + }, + { + "description": "Group container patch updates", + "matchDatasources": [ + "docker" + ], + "matchUpdateTypes": [ + "patch" + ], + "groupName": "container patch updates" + } + ] +} diff --git a/renovate/.env.example b/renovate/.env.example new file mode 100644 index 0000000..18fbc48 --- /dev/null +++ b/renovate/.env.example @@ -0,0 +1,4 @@ +RENOVATE_ENDPOINT=https://gitea.forust.xyz/api/v1 +RENOVATE_TOKEN= +RENOVATE_REPOSITORIES=forust/homelab +LOG_LEVEL=info diff --git a/renovate/README.md b/renovate/README.md new file mode 100644 index 0000000..93741ca --- /dev/null +++ b/renovate/README.md @@ -0,0 +1,59 @@ +# Renovate for Gitea + +Renovate runs as a Kubernetes CronJob and creates container image update pull +requests in Gitea. It does not deploy changes itself. + +## Kubernetes + +Create a dedicated Gitea user named `renovate-bot`, create a repository access +token, and grant it repository read/write plus issue read/write permissions. +Add `read:packages` if Renovate must inspect private Gitea registry images. + +Create the ignored Secret locally; never commit the PAT: + +```sh +cp renovate/k8s/secrets.yaml.example renovate/k8s/secrets.yaml +$EDITOR renovate/k8s/secrets.yaml +kubectl apply -f renovate/k8s/namespace.yaml +kubectl apply -f renovate/k8s/secrets.yaml +kubectl apply -f renovate/k8s/configmap.yaml +kubectl apply -f renovate/k8s/cronjob.yaml +``` + +The `renovate/k8s/active` marker makes the normal deployment workflow include +the namespace, ConfigMap, and CronJob. The Secret is intentionally excluded +from Git and must be applied separately after every new cluster. + +Run it immediately instead of waiting for the six-hour schedule: + +```sh +kubectl create job --from=cronjob/renovate renovate-manual-$(date +%s) -n renovate +``` + +Inspect runs with: + +```sh +kubectl get cronjob,jobs,pods -n renovate +kubectl logs -n renovate job/ +``` + +`RENOVATE_GITHUB_COM_TOKEN` is optional but recommended for changelogs and +GitHub API rate limits. Set it in the Kubernetes Secret if available. + +## Compose + +Copy `.env.example` to `.env`, set the PAT, and run: + +```sh +docker compose -f renovate-compose.yaml run --rm renovate +``` + +The Compose file is intentionally named `renovate-compose.yaml`, so the +repository's automatic deployment discovery does not start it accidentally. + +## How updates flow + +Renovate scans both `compose.yaml` files and Kubernetes manifests, opens a +branch and PR with image tag changes, and waits for CI. After merge, the +existing deployment workflow applies Kubernetes changes or redeploys Compose +stacks. Renovate never updates running workloads directly. diff --git a/renovate/config.js b/renovate/config.js new file mode 100644 index 0000000..7681799 --- /dev/null +++ b/renovate/config.js @@ -0,0 +1,41 @@ +module.exports = { + platform: 'gitea', + endpoint: process.env.RENOVATE_ENDPOINT, + enabledManagers: ['docker-compose', 'kubernetes'], + kubernetes: { + managerFilePatterns: ['/k8s/.+\\.ya?ml$/'], + }, + repositories: (process.env.RENOVATE_REPOSITORIES || '') + .split(',') + .map((repository) => repository.trim()) + .filter(Boolean), + onboarding: false, + requireConfig: 'optional', + autodiscover: false, + dependencyDashboard: true, + prCreation: 'immediate', + labels: ['dependencies', 'automated'], + extends: [ + 'config:recommended', + ':dependencyDashboard', + ], + packageRules: [ + { + description: 'Do not update private homelab images', + matchDatasources: ['docker'], + matchPackageNames: ['/gcr\\.forust\\.xyz\\/forust\\/.+/'], + enabled: false, + }, + { + description: 'Keep major upgrades manual', + matchUpdateTypes: ['major'], + dependencyDashboardApproval: true, + automerge: false, + }, + { + description: 'Group patch updates', + matchUpdateTypes: ['patch'], + groupName: 'container patch updates', + }, + ], +}; diff --git a/renovate/k8s/active b/renovate/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/renovate/k8s/configmap.yaml b/renovate/k8s/configmap.yaml new file mode 100644 index 0000000..e98fb92 --- /dev/null +++ b/renovate/k8s/configmap.yaml @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: renovate-config + namespace: renovate +data: + config.js: | + module.exports = { + platform: 'gitea', + endpoint: process.env.RENOVATE_ENDPOINT, + enabledManagers: ['docker-compose', 'kubernetes'], + kubernetes: { + managerFilePatterns: ['/k8s/.+\\.ya?ml$/'], + }, + repositories: (process.env.RENOVATE_REPOSITORIES || '') + .split(',') + .map((repository) => repository.trim()) + .filter(Boolean), + onboarding: false, + requireConfig: 'optional', + autodiscover: false, + dependencyDashboard: true, + prCreation: 'immediate', + labels: ['dependencies', 'automated'], + extends: ['config:recommended', ':dependencyDashboard'], + packageRules: [ + { + description: 'Do not update private homelab images', + matchDatasources: ['docker'], + matchPackageNames: ['/gcr\\.forust\\.xyz\\/forust\\/.+/'], + enabled: false, + }, + { + description: 'Keep major upgrades manual', + matchUpdateTypes: ['major'], + dependencyDashboardApproval: true, + automerge: false, + }, + { + description: 'Group patch updates', + matchUpdateTypes: ['patch'], + groupName: 'container patch updates', + }, + ], + }; diff --git a/renovate/k8s/cronjob.yaml b/renovate/k8s/cronjob.yaml new file mode 100644 index 0000000..0a176ca --- /dev/null +++ b/renovate/k8s/cronjob.yaml @@ -0,0 +1,58 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: renovate + namespace: renovate +spec: + schedule: "17 */6 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 2 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + template: + spec: + restartPolicy: Never + containers: + - name: renovate + image: renovate/renovate:44.83.2 + env: + - name: RENOVATE_PLATFORM + value: gitea + - name: RENOVATE_ENDPOINT + valueFrom: + secretKeyRef: + name: renovate-secrets + key: RENOVATE_ENDPOINT + - name: RENOVATE_TOKEN + valueFrom: + secretKeyRef: + name: renovate-secrets + key: RENOVATE_TOKEN + - name: RENOVATE_REPOSITORIES + valueFrom: + secretKeyRef: + name: renovate-secrets + key: RENOVATE_REPOSITORIES + - name: RENOVATE_CONFIG_FILE + value: /opt/renovate/config.js + - name: RENOVATE_BASE_DIR + value: /tmp/renovate + - name: RENOVATE_GITHUB_COM_TOKEN + valueFrom: + secretKeyRef: + name: renovate-secrets + key: RENOVATE_GITHUB_COM_TOKEN + optional: true + - name: LOG_LEVEL + value: info + volumeMounts: + - name: config + mountPath: /opt/renovate/config.js + subPath: config.js + readOnly: true + volumes: + - name: config + configMap: + name: renovate-config diff --git a/renovate/k8s/namespace.yaml b/renovate/k8s/namespace.yaml new file mode 100644 index 0000000..722779a --- /dev/null +++ b/renovate/k8s/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: renovate + labels: + app.kubernetes.io/part-of: renovate diff --git a/renovate/k8s/secrets.yaml.example b/renovate/k8s/secrets.yaml.example new file mode 100644 index 0000000..b1ef1ef --- /dev/null +++ b/renovate/k8s/secrets.yaml.example @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: renovate-secrets + namespace: renovate +type: Opaque +stringData: + RENOVATE_TOKEN: "" + RENOVATE_ENDPOINT: "https://gitea.forust.xyz/api/v1" + RENOVATE_REPOSITORIES: "forust/homelab" + RENOVATE_GITHUB_COM_TOKEN: "" diff --git a/renovate/renovate-compose.yaml b/renovate/renovate-compose.yaml new file mode 100644 index 0000000..68f92d2 --- /dev/null +++ b/renovate/renovate-compose.yaml @@ -0,0 +1,17 @@ +services: + renovate: + image: renovate/renovate:44.83.2 + container_name: renovate + restart: "no" + env_file: + - .env + environment: + RENOVATE_PLATFORM: gitea + RENOVATE_ENDPOINT: ${RENOVATE_ENDPOINT:?set RENOVATE_ENDPOINT} + RENOVATE_TOKEN: ${RENOVATE_TOKEN:?set RENOVATE_TOKEN} + RENOVATE_REPOSITORIES: ${RENOVATE_REPOSITORIES:?set RENOVATE_REPOSITORIES} + RENOVATE_CONFIG_FILE: /opt/renovate/config.js + RENOVATE_BASE_DIR: /tmp/renovate + LOG_LEVEL: ${LOG_LEVEL:-info} + volumes: + - ./config.js:/opt/renovate/config.js:ro