Compare commits

..
Author SHA1 Message Date
jakman85andClaude Opus 5.5 8a9baa92bc feat(gitea): upgrade 1.27.3 -> 28.3.0
gate / gate (pull_request) Successful in 3m13s
gate / conformance (pull_request) Skipped
Gitea dropped the "1." prefix; 28.x is the successor of 1.27.
- image 28.3.0, update.range >=28.0.0 <29.0.0, Renovate ceiling <29.0.0
- drop GITEA__server__DOMAIN (no longer read in 28, ROOT_URL is the source)
- declare RUN_RETENTION_DAYS=400, EGRESS_MODE=lax (no allow list: the OIDC
  login source is not gated by the egress policy in 28.3.0), and
  DISABLE_REGISTRATION=true (28's new default)
- document autoRollbackOnFailedHealth as inert (no ImagePolicy is rendered);
  rollback is a PVC restore because the DB migration is one-way

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:55:06 -06:00
13 changed files with 125 additions and 115 deletions

No files matched your search

+1 -1
View File
@@ -80,7 +80,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: with:
fetch-depth: 0 # full history so ci/touched-requirements.sh can diff vs main fetch-depth: 0 # full history so ci/touched-requirements.sh can diff vs main
+1 -1
View File
@@ -107,7 +107,7 @@ jobs:
timeout-minutes: 75 timeout-minutes: 75
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: with:
fetch-depth: 0 # the per-image change detection diffs against the base commit fetch-depth: 0 # the per-image change detection diffs against the base commit
+10 -10
View File
@@ -60,9 +60,9 @@ jobs:
spec-immutability: spec-immutability:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # the implementation repo - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 # the implementation repo
- name: Check out the read-only spec (sibling path) - name: Check out the read-only spec (sibling path)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: with:
repository: ${{ vars.SPEC_REPO_SLUG }} # e.g. Home-Network/home-network-k3s (set in Gitea) repository: ${{ vars.SPEC_REPO_SLUG }} # e.g. Home-Network/home-network-k3s (set in Gitea)
path: ../home-network-k3s path: ../home-network-k3s
@@ -74,7 +74,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [spec-immutability] needs: [spec-immutability]
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { fetch-depth: 0 } # full history so touched-requirement diff works with: { fetch-depth: 0 } # full history so touched-requirement diff works
- name: Install pinned tools - name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh lint run: ./.gitea/workflows/scripts/install-tools.sh lint
@@ -85,7 +85,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint] needs: [lint]
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { fetch-depth: 0 } with: { fetch-depth: 0 }
- name: Install pinned tools - name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh l0 run: ./.gitea/workflows/scripts/install-tools.sh l0
@@ -96,7 +96,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [l0] needs: [l0]
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { fetch-depth: 0 } with: { fetch-depth: 0 }
- name: Install pinned tools - name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh l1 run: ./.gitea/workflows/scripts/install-tools.sh l1
@@ -107,7 +107,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [render-golden] needs: [render-golden]
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { fetch-depth: 0 } with: { fetch-depth: 0 }
- name: Install pinned tools - name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh l1 run: ./.gitea/workflows/scripts/install-tools.sh l1
@@ -115,7 +115,7 @@ jobs:
run: make -C ci l1 run: make -C ci l1
- name: Upload L1 artifacts - name: Upload L1 artifacts
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: l1-junit-coverage name: l1-junit-coverage
path: | path: |
@@ -131,7 +131,7 @@ jobs:
needs: [l1] needs: [l1]
if: ${{ vars.CLUSTER_AVAILABLE == 'true' }} if: ${{ vars.CLUSTER_AVAILABLE == 'true' }}
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { fetch-depth: 0 } with: { fetch-depth: 0 }
- name: Install pinned tools - name: Install pinned tools
run: ./.gitea/workflows/scripts/install-tools.sh conformance run: ./.gitea/workflows/scripts/install-tools.sh conformance
@@ -142,7 +142,7 @@ jobs:
run: make -C ci conformance run: make -C ci conformance
- name: Upload L2/L3 artifacts - name: Upload L2/L3 artifacts
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: conformance-junit-coverage name: conformance-junit-coverage
path: | path: |
@@ -160,7 +160,7 @@ jobs:
needs: [lint, l0, render-golden, l1, conformance] needs: [lint, l0, render-golden, l1, conformance]
if: always() if: always()
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Decide - name: Decide
env: env:
R_LINT: ${{ needs.lint.result }} R_LINT: ${{ needs.lint.result }}
+7 -3
View File
@@ -78,7 +78,7 @@ spec:
spec: spec:
containers: containers:
- name: gitea - name: gitea
image: gitea/gitea:1.27.3 image: gitea/gitea:28.3.0
ports: ports:
- containerPort: 3000 - containerPort: 3000
env: env:
@@ -89,14 +89,18 @@ spec:
secretKeyRef: secretKeyRef:
name: gitea-bootstrap name: gitea-bootstrap
key: secret-key key: secret-key
- name: GITEA__server__DOMAIN
value: gitea.${BASE_DOMAIN}
- name: GITEA__server__ROOT_URL - name: GITEA__server__ROOT_URL
value: https://gitea.${BASE_DOMAIN}/ value: https://gitea.${BASE_DOMAIN}/
- name: GITEA__actions__ENABLED - name: GITEA__actions__ENABLED
value: 'true' value: 'true'
- name: GITEA__actions__DEFAULT_ACTIONS_URL - name: GITEA__actions__DEFAULT_ACTIONS_URL
value: github value: github
- name: GITEA__actions__RUN_RETENTION_DAYS
value: '400'
- name: GITEA__security__EGRESS_MODE
value: lax
- name: GITEA__service__DISABLE_REGISTRATION
value: 'true'
- name: GITEA_ADMIN_USERNAME - name: GITEA_ADMIN_USERNAME
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
+39 -5
View File
@@ -10,7 +10,7 @@
name: gitea name: gitea
kind: container kind: container
source: source:
image: gitea/gitea:1.27.3 # pinned recent stable tag (R-UPD-01); update.range governs advancement image: gitea/gitea:28.3.0 # pinned recent stable tag (R-UPD-01); update.range governs advancement
# Headless-install bootstrap (formerly apps/gitea-bootstrap-patch.yaml; now first-class via the # Headless-install bootstrap (formerly apps/gitea-bootstrap-patch.yaml; now first-class via the
# ServiceDefinition env/envFrom/lifecycle fields, schema >=1.2.0). Gitea must come up INSTALLED # ServiceDefinition env/envFrom/lifecycle fields, schema >=1.2.0). Gitea must come up INSTALLED
# without the interactive web wizard and with a FIXED, persistent SECRET_KEY, so all of this is # without the interactive web wizard and with a FIXED, persistent SECRET_KEY, so all of this is
@@ -23,14 +23,43 @@ env:
secretKeyRef: secretKeyRef:
name: gitea-bootstrap name: gitea-bootstrap
key: secret-key key: secret-key
- name: GITEA__server__DOMAIN # correct public clone/redirect URLs for the TLS-terminated host # ROOT_URL is the ONLY source of the instance domain since Gitea 28 (go-gitea/gitea#39400): [server]
value: gitea.${BASE_DOMAIN} # DOMAIN is no longer read, and the SSH domain defaults from ROOT_URL too. GITEA__server__DOMAIN was
# removed in the 28 upgrade; the stale DOMAIN line environment-to-ini already wrote into app.ini is
# inert. Correct public clone/redirect URLs for the TLS-terminated host come from this one value.
- name: GITEA__server__ROOT_URL - name: GITEA__server__ROOT_URL
value: https://gitea.${BASE_DOMAIN}/ value: https://gitea.${BASE_DOMAIN}/
- name: GITEA__actions__ENABLED # Phase 6 CI/CD: declared (not implicit upstream default) so the - name: GITEA__actions__ENABLED # Phase 6 CI/CD: declared (not implicit upstream default) so the
value: "true" # in-cluster act_runner (ns gitea-actions) registers against it value: "true" # in-cluster act_runner (ns gitea-actions) registers against it
- name: GITEA__actions__DEFAULT_ACTIONS_URL # resolve `uses: <org>/<action>@ref` from github.com (cluster - name: GITEA__actions__DEFAULT_ACTIONS_URL # resolve `uses: <org>/<action>@ref` from github.com (cluster
value: github # has internet egress; not air-gapped) value: github # has internet egress; not air-gapped)
# Gitea 28 added run retention (go-gitea/gitea#38855): completed runs, with their jobs, logs and
# artifacts, are deleted by the cleanup_action_runs cron after RUN_RETENTION_DAYS. Upstream's default
# (400) is kept but DECLARED, so the deletion horizon is visible here rather than implicit. The spec
# repo states no CI-history retention requirement; 0 would mean keep forever.
- name: GITEA__actions__RUN_RETENTION_DAYS
value: "400"
# Gitea 28 (go-gitea/gitea#39426) puts webhooks, the OAuth2 avatar fetch and OpenID 2.0 behind an
# egress policy: in the default lax mode, PUBLIC targets are allowed and private/loopback/CGNAT
# targets need an IP or CIDR entry in [security] ALLOWED_HOST_LIST (a hostname entry is NOT enough
# for a private address). DECLARED lax with an EMPTY allow list, deliberately:
# * the Keycloak OIDC login source is NOT gated by it. In 28.3.0 the openidConnect provider is
# goth's, built with its default HTTP client (services/auth/source/oauth2/providers_openid.go);
# egress.NewSecurityPolicy is used only by routers/web/auth/oauth.go (avatar fetch, and
# [oauth2_client] UPDATE_AVATAR is off here) and modules/auth/openid (OpenID 2.0, unused).
# So keycloak.<domain> resolving to the private Gateway address in-cluster does not matter.
# * this instance has no webhooks and no mirrors (measured 2026-11-07), and 1.27 already refused
# private webhook targets by default, so nothing that worked before is newly denied.
# Adding an entry here would only WIDEN the webhook SSRF surface. Add the narrowest CIDR (e.g. the
# Gateway address /32) only if a real private webhook target appears.
- name: GITEA__security__EGRESS_MODE
value: lax
# Gitea 28 (go-gitea/gitea#39400) disables LOCAL self-registration unless DISABLE_REGISTRATION is
# explicitly false; 1.27 defaulted it open. Declared true: accounts arrive via Keycloak only.
# OAuth2 auto-registration below is unaffected — routers/web/auth/oauth.go gates it on
# ALLOW_ONLY_INTERNAL_REGISTRATION and ENABLE_AUTO_REGISTRATION, not on DISABLE_REGISTRATION.
- name: GITEA__service__DISABLE_REGISTRATION
value: "true"
- name: GITEA_ADMIN_USERNAME # break-glass local admin creds (consumed by the postStart hook) - name: GITEA_ADMIN_USERNAME # break-glass local admin creds (consumed by the postStart hook)
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -233,9 +262,14 @@ auth:
# recovery tools (docs/15 "Availability caveat"). No secret is embedded here. # recovery tools (docs/15 "Availability caveat"). No secret is embedded here.
update: update:
channel: stable # R-UPD-01 channel: stable # R-UPD-01
range: ">=1.22.0 <2.0.0" # stay on the 1.x stable line range: ">=28.0.0 <29.0.0" # Gitea dropped the "1." prefix at 28.0.0 (successor of 1.27); stay on 28.x
cadence: "0 4 * * *" cadence: "0 4 * * *"
autoRollbackOnFailedHealth: true # R-UPD-03 # R-UPD-03. INERT FOR THIS SERVICE, and it must stay that way: this flag renders only an annotation
# on a Flux ImagePolicy, and apps/ is rendered --no-image-automation (ci/render-golden.sh), so no
# ImagePolicy exists and the Deployment (strategy Recreate) is never auto-reverted. That matters
# because a Gitea upgrade (1.27 -> 28 included) MIGRATES THE SQLITE DB ONE-WAY: an image revert onto
# a migrated DB is not a rollback. Roll Gitea back by RESTORING THE DATA PVC from its pre-upgrade backup.
autoRollbackOnFailedHealth: true
observability: observability:
metrics: true metrics: true
slis: slis:
@@ -1,54 +0,0 @@
---
date: 2026-11-07
branch: feat/devcontainer-velero-cli
title: Bake the velero CLI into the dev toolbox, pinned to the prod server version
---
## What changed
- `infra/devcontainer/versions.env`: new `VELERO_VERSION=1.18.2`, with a comment explaining that
the CLI and server are version-coupled, so this pin follows the server and is not bumped on
its own. It has **no** `# renovate:` marker on purpose. When a `velero` chart bump in
`platform/velero/helmrelease.yaml` changes the appVersion, bump this pin in the same PR.
- `infra/devcontainer/Containerfile`: `ARG VELERO_VERSION`; the binary comes from the
vmware-tanzu GitHub release tarball and is installed in the same RUN as flux/helm. There is
no checksum check, which matches the other tools in that file (none of them verify one).
`velero=` is recorded in `/etc/dev-toolchain-versions`. The verify stage now checks that
`velero` is on PATH, and `check velero v${VELERO_VERSION}` asserts the output of
`velero version --client-only`.
- `infra/devcontainer/build-image.sh`: `VELERO_VERSION` added to `BUILD_ARGS`. CI reads that
list generically through `--print-build-args` (`.gitea/workflows/scripts/images.sh`), so
**no sensitive path (`.gitea/**`, `ci/**`) was touched**.
- `infra/devcontainer/README.md`: added a Backups row to the tool table.
- `runbooks/backup-failure.md`: under "First commands", a read-only
`velero backup describe --details` / `backup logs` example run through dev.sh.
## What I verified
- Live server version, read-only: `kubectl --context k3s-prod -n velero get deploy velero`
shows image `docker.io/velero/velero:v1.18.2`, and HelmRelease `velero` has
lastAttemptedRevision `12.2.3`.
- Local build: `IMAGE=localhost/home-k3s-dev:velero-wt ./build-image.sh` returned rc 0. The
verify stage printed `ok velero` (on PATH), `ok velero v1.18.2` (pin check) and
`TOOLBOX VERIFIED`. The image was built under a separate local tag and **not pushed**.
- Smoke test with that image against prod (read-only): `velero version` reports Client v1.18.2
and Server v1.18.2. `velero backup get` lists the daily backups, all `Completed` with 0 errors.
- Recurring warning (reported only, not fixed): every daily backup since
`velero-daily-20261031030050` has 1 warning. Backups up to 20261030 have 0. The warning text is:
`gitea-actions: resource: /pods name: /act-runner-5944dfdfb8-69bvq message: /Skip pod volume
data error: /pod is not in the expected status, ... phase=Failed: pod is not running`.
It is the same pod in 20261031, 20261104 and 20261108. `gitea-actions` holds 11 `Failed`
pods with reason `Evicted` from ReplicaSet `act-runner-5944dfdfb8`, ten of them started
2026-10-30T08:26Z. The running runner is `act-runner-fc597f6b5-vsvkl`. So the warning comes from a
leftover evicted (`Failed`) pod that Velero still tries to back up volumes for. Cleaning up the evicted pods is an
operator action.
- Gate: see the PR description for the toolbox `ci/gate.sh` rc and the skipped stages.
## What I did NOT verify
- The CI image build (`.gitea/workflows/images.yaml`) on this branch. It runs on push/PR and I
have not seen its result while writing this.
- An arm64 build. Only amd64 was built (`TARGETARCH` defaults to amd64).
- Any mutating velero command (`backup create`, `restore`, `delete`). None were run.
- Why only `-69bvq` of the 11 evicted act-runner pods shows up in the warning. A likely guess is
that only that pod's volumes match the backup's pod-volume selection, but I did not check.
+62
View File
@@ -0,0 +1,62 @@
---
date: 2026-11-07
branch: feat/gitea-28
title: "Gitea 1.27.3 -> 28.3.0 (upstream dropped the 1. prefix)"
---
## What changed
- `catalog/gitea.yaml`: `source.image` `gitea/gitea:1.27.3` -> `gitea/gitea:28.3.0` (still the
newest stable tag on Docker Hub and GitHub on 2026-11-07: 28.0.0, 28.1.0, 28.2.0, 28.3.0).
`update.range` `">=1.22.0 <2.0.0"` -> `">=28.0.0 <29.0.0"`.
- 28.x breaking changes handled in the env block:
- `GITEA__server__DOMAIN` **removed**: 28 no longer reads `[server] DOMAIN` (go-gitea/gitea#39400);
`ROOT_URL` was already set and is now the only source of the domain. Nothing else in the repo
reads the variable (the bootstrap script sets ROOT_URL/SSH_PORT only).
- `GITEA__actions__RUN_RETENTION_DAYS=400` **declared** (go-gitea/gitea#38855 added the 400-day
default; the spec repo states no CI-history retention requirement).
- `GITEA__security__EGRESS_MODE=lax` **declared, with no allow list.** Read from the 28.3.0
source: the Keycloak OIDC login source does NOT go through the new egress policy (goth's
openidConnect provider, default HTTP client). The policy covers webhooks, the OAuth2 avatar
fetch (`UPDATE_AVATAR` is off) and OpenID 2.0. A hostname entry would not even help for a
private address — `WithLocalNeedsIPAllow` requires an IP/CIDR entry. So the brief's
"Keycloak SSO breaks" premise does not hold for 28.3.0, and an allow-list entry would only
widen webhook SSRF reach.
- `GITEA__service__DISABLE_REGISTRATION=true` **declared** (28's new default; 1.27 defaulted it
open). OAuth2 auto-registration is gated separately and keeps working.
- `update.autoRollbackOnFailedHealth`: kept `true`, commented as **inert**. It renders only an
annotation on a Flux ImagePolicy, and `apps/` is rendered `--no-image-automation`, so no
ImagePolicy exists. The Deployment is `Recreate` with no probes, so nothing reverts the image
automatically. Rollback = restore the PVC, never an image revert onto the migrated DB.
- `apps/wave-0/gitea.yaml` re-rendered with the canonical command
(`render.py --all catalog -o apps --wave-layout --no-image-automation`, in the toolbox). Only
gitea changed.
- `renovate.json5`: gitea `allowedVersions` `<2.0.0` -> `<29.0.0`; the description no longer calls
28.x a stale tag.
## What I verified
- Docker Hub tag list and GitHub tags: 28.3.0 is the newest 28.x stable tag.
- Read the v28.3.0 source: `modules/egress/policy/policy.go`, `modules/egress/policies.go`, every
`egress.New*` caller, `services/auth/source/oauth2/providers_openid.go`,
`modules/setting/{security,actions,service}.go`, `routers/web/auth/{oauth,linkaccount}.go`,
`cmd/admin_user_list.go`, `cmd/admin_auth*.go`, the Dockerfile, and `docker/root` (the s6 setup
still runs `environment-to-ini`). The postStart hook's CLI is unchanged in 28.3.0: `admin user
list --admin` still has Username in column 2, `admin auth list` still has ID/Name in columns 1/2,
`add-oauth`/`update-oauth` keep the same flags, and curl and su-exec are still in the image.
- Live Gitea (API, 2026-11-07): version 1.27.3. Zero repo, org and system webhooks, no mirrors.
- `renovate-config-validator` at 44.167.0 (the tag renovate.yaml pins): "Config validated
successfully".
- Ran the toolbox gate on the committed tree (numbers are in the PR).
## What I did NOT verify
- **The upgrade itself.** Nothing booted 28.3.0 against a copy of the production DB. Migration
time, migration success and the post-upgrade SSO login are all UNVERIFIED, and the PR's
post-merge checklist covers them.
- **The age of the latest gitea-data Longhorn/off-cluster backup.** The cluster read failed from
the worktree (no k3s-prod context with the kubeconfig available), so the operator must confirm
it before merging.
- How the Envoy Gateway handles the new `/-/ws` WebSocket. If upgrades are not forwarded, live
notifications fall back to polling (not breaking). UNVERIFIED.
- Whether the L2/L3 conformance suite passes. It does not run in CI.
+3 -12
View File
@@ -30,9 +30,6 @@ ARG CHECK_JSONSCHEMA_VERSION
# Pinned to the Prometheus version prod actually runs — see versions.env for why that lockstep is # Pinned to the Prometheus version prod actually runs — see versions.env for why that lockstep is
# the point, and for the live-cluster command that establishes it. # the point, and for the live-cluster command that establishes it.
ARG PROMTOOL_VERSION ARG PROMTOOL_VERSION
# velero: operator CLI for the backup server. Pinned to the server version prod runs — the CLI
# and server are version-coupled; see versions.env.
ARG VELERO_VERSION
ARG TARGETARCH=amd64 ARG TARGETARCH=amd64
# Distro packages: libvirt client libs (harmless disk-image utilities), ansible, ssh, # Distro packages: libvirt client libs (harmless disk-image utilities), ansible, ssh,
@@ -69,10 +66,6 @@ RUN set -eux; \
curl -fsSL "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/${TARGETARCH}/kubectl" -o /usr/local/bin/kubectl; chmod +x /usr/local/bin/kubectl; \ curl -fsSL "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/${TARGETARCH}/kubectl" -o /usr/local/bin/kubectl; chmod +x /usr/local/bin/kubectl; \
curl -fsSL "https://get.helm.sh/helm-v${HELM_VERSION}-linux-${TARGETARCH}.tar.gz" | tar -xz -C /tmp; \ curl -fsSL "https://get.helm.sh/helm-v${HELM_VERSION}-linux-${TARGETARCH}.tar.gz" | tar -xz -C /tmp; \
install -m0755 "/tmp/linux-${TARGETARCH}/helm" /usr/local/bin/helm; \ install -m0755 "/tmp/linux-${TARGETARCH}/helm" /usr/local/bin/helm; \
VELERO_TARBALL="velero-v${VELERO_VERSION}-linux-${TARGETARCH}"; \
curl -fsSL "https://github.com/vmware-tanzu/velero/releases/download/v${VELERO_VERSION}/${VELERO_TARBALL}.tar.gz" \
| tar -xz -C /usr/local/bin --strip-components=1 "${VELERO_TARBALL}/velero"; \
chmod +x /usr/local/bin/velero; \
rm -rf /tmp/* rm -rf /tmp/*
# Pinned conformance test toolchain (L0/L1 static gate + the L2/L3 Chainsaw runner). # Pinned conformance test toolchain (L0/L1 static gate + the L2/L3 Chainsaw runner).
@@ -100,7 +93,7 @@ RUN { echo "opentofu=${OPENTOFU_VERSION}"; echo "age=${AGE_VERSION}"; echo "sops
echo "kyverno=${KYVERNO_VERSION}"; echo "kyverno-json=${KYVERNO_JSON_VERSION}"; \ echo "kyverno=${KYVERNO_VERSION}"; echo "kyverno-json=${KYVERNO_JSON_VERSION}"; \
echo "chainsaw=${CHAINSAW_VERSION}"; echo "yamllint=${YAMLLINT_VERSION}"; \ echo "chainsaw=${CHAINSAW_VERSION}"; echo "yamllint=${YAMLLINT_VERSION}"; \
echo "yq=${YQ_VERSION}"; echo "check-jsonschema=${CHECK_JSONSCHEMA_VERSION}"; \ echo "yq=${YQ_VERSION}"; echo "check-jsonschema=${CHECK_JSONSCHEMA_VERSION}"; \
echo "promtool=${PROMTOOL_VERSION}"; echo "velero=${VELERO_VERSION}"; \ echo "promtool=${PROMTOOL_VERSION}"; \
} > /etc/dev-toolchain-versions } > /etc/dev-toolchain-versions
WORKDIR /work WORKDIR /work
@@ -119,7 +112,6 @@ ARG KUBECTL_VERSION
ARG CHAINSAW_VERSION ARG CHAINSAW_VERSION
ARG KYVERNO_VERSION ARG KYVERNO_VERSION
ARG SOPS_VERSION ARG SOPS_VERSION
ARG VELERO_VERSION
RUN --network=none /bin/bash -euo pipefail <<'VERIFY' RUN --network=none /bin/bash -euo pipefail <<'VERIFY'
echo "--- no network (the build-time stand-in for run --network none) ---" echo "--- no network (the build-time stand-in for run --network none) ---"
if getent hosts registry.fedoraproject.org >/dev/null 2>&1 || curl -sS --max-time 3 -o /dev/null https://1.1.1.1 2>/dev/null; then if getent hosts registry.fedoraproject.org >/dev/null 2>&1 || curl -sS --max-time 3 -o /dev/null https://1.1.1.1 2>/dev/null; then
@@ -130,13 +122,13 @@ echo "--- baked versions ---"; cat /etc/dev-toolchain-versions
echo "--- every tool is on PATH ---" echo "--- every tool is on PATH ---"
for t in tofu age age-keygen sops flux kubectl helm kyverno kyverno-json chainsaw yq \ for t in tofu age age-keygen sops flux kubectl helm kyverno kyverno-json chainsaw yq \
yamllint check-jsonschema promtool ansible-playbook git make jq diff python3 \ yamllint check-jsonschema promtool ansible-playbook git make jq diff python3 \
wg qrencode velero; do wg qrencode; do
if command -v "$t" >/dev/null 2>&1; then printf " ok %s\n" "$t"; if command -v "$t" >/dev/null 2>&1; then printf " ok %s\n" "$t";
else printf " MISSING %s\n" "$t"; missing=1; fi else printf " MISSING %s\n" "$t"; missing=1; fi
done done
[ -z "${missing:-}" ] || { echo "toolbox is missing tools"; exit 1; } [ -z "${missing:-}" ] || { echo "toolbox is missing tools"; exit 1; }
echo "--- version-sensitive tools report their pins ---" echo "--- version-sensitive tools report their pins ---"
: "${PROMTOOL_VERSION:?}" "${KUBECTL_VERSION:?}" "${CHAINSAW_VERSION:?}" "${KYVERNO_VERSION:?}" "${SOPS_VERSION:?}" "${VELERO_VERSION:?}" : "${PROMTOOL_VERSION:?}" "${KUBECTL_VERSION:?}" "${CHAINSAW_VERSION:?}" "${KYVERNO_VERSION:?}" "${SOPS_VERSION:?}"
check() { # name pin actual-output check() { # name pin actual-output
if printf "%s" "$3" | grep -qF "$2"; then printf " ok %-10s %s\n" "$1" "$2"; if printf "%s" "$3" | grep -qF "$2"; then printf " ok %-10s %s\n" "$1" "$2";
else printf " DRIFT %-10s want %s, got: %s\n" "$1" "$2" "$(printf "%s" "$3" | head -1)"; drift=1; fi else printf " DRIFT %-10s want %s, got: %s\n" "$1" "$2" "$(printf "%s" "$3" | head -1)"; drift=1; fi
@@ -146,7 +138,6 @@ check kubectl "${KUBECTL_VERSION}" "$(kubectl version --client 2>&1)"
check chainsaw "${CHAINSAW_VERSION}" "$(chainsaw version 2>&1)" check chainsaw "${CHAINSAW_VERSION}" "$(chainsaw version 2>&1)"
check kyverno "${KYVERNO_VERSION}" "$(kyverno version 2>&1)" check kyverno "${KYVERNO_VERSION}" "$(kyverno version 2>&1)"
check sops "${SOPS_VERSION}" "$(sops --version --disable-version-check 2>&1 || sops --version 2>&1)" check sops "${SOPS_VERSION}" "$(sops --version --disable-version-check 2>&1 || sops --version 2>&1)"
check velero "v${VELERO_VERSION}" "$(velero version --client-only 2>&1)"
[ -z "${drift:-}" ] || { echo "a tool does not report its pinned version"; exit 1; } [ -z "${drift:-}" ] || { echo "a tool does not report its pinned version"; exit 1; }
echo "--- python can import what the renderer and the gate need ---" echo "--- python can import what the renderer and the gate need ---"
python3 -c "import yaml; print(' PyYAML', yaml.__version__)" python3 -c "import yaml; print(' PyYAML', yaml.__version__)"
-1
View File
@@ -14,7 +14,6 @@ baseline, and the *same* binaries CI uses.
| Provisioning | OpenTofu (`bpg/proxmox` provider), `ansible-core`, `openssh-clients` | | Provisioning | OpenTofu (`bpg/proxmox` provider), `ansible-core`, `openssh-clients` |
| GitOps / cluster | `flux`, `kubectl`, `helm` | | GitOps / cluster | `flux`, `kubectl`, `helm` |
| Secrets | `sops`, `age` + `age-keygen` | | Secrets | `sops`, `age` + `age-keygen` |
| Backups (`platform/velero`) | `velero` CLI — pinned to the **server** version prod runs (version-coupled; see `versions.env`) |
| VPN (`platform/vpn-ingress`) | `wg` (wireguard-tools) — mint the server key, derive client public keys; `qrencode` — render a client profile as a phone-scannable QR | | VPN (`platform/vpn-ingress`) | `wg` (wireguard-tools) — mint the server key, derive client public keys; `qrencode` — render a client profile as a phone-scannable QR |
| **CI gate** | `yamllint`, `check-jsonschema`, `kyverno` CLI, `kyverno-json`, `chainsaw`, `yq`, `diffutils`, `make` | | **CI gate** | `yamllint`, `check-jsonschema`, `kyverno` CLI, `kyverno-json`, `chainsaw`, `yq`, `diffutils`, `make` |
| Misc | `qemu-img`, `cloud-utils`/`genisoimage`, `libvirt-client` (disk-image utilities only — leftovers from the libvirt era, harmless) | | Misc | `qemu-img`, `cloud-utils`/`genisoimage`, `libvirt-client` (disk-image utilities only — leftovers from the libvirt era, harmless) |
-1
View File
@@ -54,7 +54,6 @@ BUILD_ARGS=(
"KYVERNO_JSON_VERSION=$KYVERNO_JSON_VERSION" "CHAINSAW_VERSION=$CHAINSAW_VERSION" "KYVERNO_JSON_VERSION=$KYVERNO_JSON_VERSION" "CHAINSAW_VERSION=$CHAINSAW_VERSION"
"YAMLLINT_VERSION=$YAMLLINT_VERSION" "YQ_VERSION=$YQ_VERSION" "YAMLLINT_VERSION=$YAMLLINT_VERSION" "YQ_VERSION=$YQ_VERSION"
"CHECK_JSONSCHEMA_VERSION=$CHECK_JSONSCHEMA_VERSION" "PROMTOOL_VERSION=$PROMTOOL_VERSION" "CHECK_JSONSCHEMA_VERSION=$CHECK_JSONSCHEMA_VERSION" "PROMTOOL_VERSION=$PROMTOOL_VERSION"
"VELERO_VERSION=$VELERO_VERSION"
) )
if [ "${PRINT_ARGS}" -eq 1 ]; then printf '%s\n' "${BUILD_ARGS[@]}"; exit 0; fi if [ "${PRINT_ARGS}" -eq 1 ]; then printf '%s\n' "${BUILD_ARGS[@]}"; exit 0; fi
-15
View File
@@ -44,19 +44,4 @@ CHECK_JSONSCHEMA_VERSION=0.29.4
# promtool ships INSIDE the prometheus release tarball (prometheus-<v>.linux-<arch>.tar.gz), so # promtool ships INSIDE the prometheus release tarball (prometheus-<v>.linux-<arch>.tar.gz), so
# the version string here is a Prometheus release, not a separate promtool release. # the version string here is a Prometheus release, not a separate promtool release.
PROMTOOL_VERSION=3.14.0 PROMTOOL_VERSION=3.14.0
# velero — the operator CLI for the Velero backup server (platform/velero): `velero backup
# describe <name> --details`, `velero backup logs`, `velero backup create --from-schedule`.
#
# THIS PIN IS NOT FREE-FLOATING: the CLI and server are VERSION-COUPLED (the CLI talks to the
# server's CRDs and its download-request protocol; upstream supports only a matching minor), so
# it MUST equal the Velero server version actually running on prod.
# Verified against the live cluster on 2026-11-07:
# kubectl --context k3s-prod -n velero get deploy velero \
# -o jsonpath='{.spec.template.spec.containers[0].image}'
# -> docker.io/velero/velero:v1.18.2 (HelmRelease velero, chart velero-12.2.3)
# Deliberately carries NO `# renovate:` marker: it must move WITH the server, i.e. when a
# vmware-tanzu `velero` chart bump (platform/velero/helmrelease.yaml, range >=12 <13) changes the
# chart's appVersion, bump this in the same PR. Never bump it on its own. The verify stage
# asserts `velero version --client-only` reports exactly this version.
VELERO_VERSION=1.18.2
# Proxmox provider (bpg/proxmox) is pinned in the tofu module (infra/tofu/versions.tf), not here. # Proxmox provider (bpg/proxmox) is pinned in the tofu module (infra/tofu/versions.tf), not here.
+2 -2
View File
@@ -316,9 +316,9 @@
"enabled": false "enabled": false
}, },
{ {
"description": "ADDED 2026-10-30. MIRRORS catalog/<svc>.yaml `update.range` — the operator's declared per-service policy (schemas/servicedefinition.schema.json), which Renovate does not read. Without it the dry run proposed gitea 28.2.0, qbittorrent 20.04.1 (both stale/odd tags, not real successors), radarr 6.x and nextcloud 35 (four majors past 31; Nextcloud cannot skip majors). KEEP IN STEP with the catalog ranges.", "description": "ADDED 2026-10-30. MIRRORS catalog/<svc>.yaml `update.range` — the operator's declared per-service policy (schemas/servicedefinition.schema.json), which Renovate does not read. Without it the dry run proposed gitea 28.2.0, qbittorrent 20.04.1 (the qbittorrent tag is a stale/odd one, not a real successor; gitea 28.x IS the real successor of 1.27 — upstream dropped the \"1.\" prefix at 28.0.0 — and the catalog moved to it on 2026-11-07, so the gitea ceiling is now <29.0.0: 28.x patches/minors are proposed, never auto-merged per the blast-radius rule, and 29 waits for a human), radarr 6.x and nextcloud 35 (four majors past 31; Nextcloud cannot skip majors). KEEP IN STEP with the catalog ranges.",
"matchPackageNames": ["gitea/gitea"], "matchPackageNames": ["gitea/gitea"],
"allowedVersions": "<2.0.0" "allowedVersions": "<29.0.0"
}, },
{ "matchPackageNames": ["nextcloud"], "allowedVersions": "<32.0.0" }, { "matchPackageNames": ["nextcloud"], "allowedVersions": "<32.0.0" },
{ "matchPackageNames": ["lscr.io/linuxserver/qbittorrent"], "allowedVersions": "<6.0.0" }, { "matchPackageNames": ["lscr.io/linuxserver/qbittorrent"], "allowedVersions": "<6.0.0" },
-10
View File
@@ -99,16 +99,6 @@ infra/devcontainer/dev.sh run bash -c \
-o custom-columns=NAME:.metadata.name,BACKUP:.metadata.labels.velero\\.io/backup-name,PHASE:.status.phase,BYTES:.status.progress.bytesDone' -o custom-columns=NAME:.metadata.name,BACKUP:.metadata.labels.velero\\.io/backup-name,PHASE:.status.phase,BYTES:.status.progress.bytesDone'
``` ```
The `velero` CLI is in the toolbox (pinned to the server version) and gives the per-item errors and
warnings the CR only counts. Read-only; `velero backup create/delete` and `velero restore` are
operator actions:
```
infra/devcontainer/dev.sh run bash -c \
'velero --kubecontext k3s-prod -n velero backup describe <name> --details; \
velero --kubecontext k3s-prod -n velero backup logs <name> | grep -iE "level=(error|warn)" | tail -25'
```
The plugin pin (cause 1 depends entirely on this): The plugin pin (cause 1 depends entirely on this):
``` ```