Merge pull request #253 from jk-cluster/chore/gitea-migration-prep
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 7s
Publish Gotenberg Helm Chart / Publish Gotenberg Helm Chart (push) Successful in 7s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 6s
Publish SearXNG Helm Chart / Publish SearXNG Helm Chart (push) Successful in 6s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 7s
Publish Gotenberg Helm Chart / Publish Gotenberg Helm Chart (push) Successful in 7s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 6s
Publish SearXNG Helm Chart / Publish SearXNG Helm Chart (push) Successful in 6s
Gitea-Umzug vorbereiten: gemessene CI-Abweichungen, Renovate im Eigenbetrieb, Issue-Migrationsskript
This commit is contained in:
4 files changed
+303
-18
No files matched your search
@@ -199,11 +199,27 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
- name: "Install Helm"
|
||||
uses: azure/setup-helm@v5
|
||||
# A plain release download rather than aquasecurity/setup-trivy, and the
|
||||
# reason is measured on the Gitea probe PR of #225, not assumed. That
|
||||
# action runs actions/checkout against aquasecurity/trivy on github.com
|
||||
# and hands it `github.token` - which on Gitea is a GITEA token.
|
||||
# github.com answers 401, checkout retries twice and the job ends on
|
||||
# "fatal: could not read Username for 'https://github.com': terminal
|
||||
# prompts disabled", an error naming neither the action nor the token.
|
||||
# Its actions/cache step is inert on the instance as well ("Cache action
|
||||
# is only supported on GHES version >= 3.5"), and fetching the nested
|
||||
# actions cost 4m30s before the job even started. One tarball behaves
|
||||
# identically on both forges, and the `# renovate:` comment above still
|
||||
# keeps TRIVY_VERSION current - verified by dry run: pinned to v0.72.0
|
||||
# the lookup offers v0.74.0 as a minor update.
|
||||
- name: "Install Trivy"
|
||||
uses: aquasecurity/setup-trivy@v0.3.1
|
||||
with:
|
||||
version: ${{ env.TRIVY_VERSION }}
|
||||
cache: true
|
||||
run: |
|
||||
curl -sSfL -o /tmp/trivy.tar.gz \
|
||||
"https://github.com/aquasecurity/trivy/releases/download/${TRIVY_VERSION}/trivy_${TRIVY_VERSION#v}_Linux-64bit.tar.gz"
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
tar -xzf /tmp/trivy.tar.gz -C "$HOME/.local/bin" trivy
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/trivy" --version
|
||||
# Findings land in the job summary, not only in the log: exceptions with
|
||||
# their reasons come from ci/trivy/trivyignore.yaml, the trusted-registry
|
||||
# list from ci/trivy/data/ksv0125.yaml.
|
||||
@@ -214,7 +230,35 @@ jobs:
|
||||
name: "Install-test ${{ matrix.chart }}"
|
||||
needs: [detect-changed-charts, build-chart]
|
||||
if: needs.detect-changed-charts.outputs.any-install == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# The one job in this repo that does not simply say ubuntu-latest, and the
|
||||
# expression is not decoration - the same label means different machines on
|
||||
# the two forges this repo currently lives on (#225).
|
||||
#
|
||||
# This is the only job that needs a Docker socket: it creates a k3d
|
||||
# cluster, and k3d is a Docker client. On GitHub's hosted runners every
|
||||
# label carries Docker, so the question never came up. On the Gitea
|
||||
# instance it was MEASURED on a probe pull request, per job:
|
||||
# detect-changed-charts, build-chart, the quantity render and the Trivy
|
||||
# scan all pass on `ubuntu-latest` - none of them needs Docker - while the
|
||||
# install-test dies there at "Create k3d cluster" with "Cannot connect to
|
||||
# the Docker daemon at unix:///var/run/docker.sock", an error that points
|
||||
# at k3d and never at the runner label. `ubuntu-docker` is the label that
|
||||
# carries a socket, and Gitea does evaluate this expression: the probe job
|
||||
# was picked up by the docker runner with labels ["ubuntu-docker"].
|
||||
#
|
||||
# Why an expression and not a plain `ubuntu-docker`: that label does not
|
||||
# exist on GitHub, so every chart PR would sit queued forever for as long
|
||||
# as this repo still builds on github.com - blocking the merge with no
|
||||
# error anywhere. github.server_url is the one context value that separates
|
||||
# the two without naming our instance.
|
||||
#
|
||||
# KNOWN, and not fixed by this line: `ubuntu-docker`'s daemon is ROOTLESS
|
||||
# and reports "Cgroup Driver: none" plus "Running in rootless-mode without
|
||||
# cgroups". k3d then gets as far as starting the k3s node, which restarts
|
||||
# immediately with an empty log, because k3s needs cgroup delegation. Until
|
||||
# the instance's Docker-in-Docker provides it, this job cannot pass on
|
||||
# Gitea - see #225. The label is necessary, it is just not sufficient.
|
||||
runs-on: ${{ github.server_url == 'https://github.com' && 'ubuntu-latest' || 'ubuntu-docker' }}
|
||||
# Blocking since Renovate automerges patch/minor updates: a chart that no
|
||||
# longer starts must stop the PR before it reaches main and publishes. This
|
||||
# ends the stabilization phase of issue #22 - the job reported failures
|
||||
@@ -241,11 +285,80 @@ jobs:
|
||||
env:
|
||||
K3D_VERSION: v5.9.0
|
||||
K3S_IMAGE: docker.io/rancher/k3s:v1.35.5-k3s1
|
||||
# Pinned rather than taken from the runner image. The argument is not
|
||||
# "Gitea's image has no kubectl" - that is only how it was noticed. A
|
||||
# kubectl inherited from whatever the image happens to ship drifts
|
||||
# unannounced with every image rebuild, and an install-test is supposed
|
||||
# to run against a KNOWN client. That GitHub's hosted image already
|
||||
# carries one does not make the pin unnecessary; it only makes the drift
|
||||
# invisible there. The value mirrors K3S_IMAGE above (v1.35.5), which is
|
||||
# the cluster this client talks to.
|
||||
# renovate: datasource=github-releases depName=kubernetes/kubernetes
|
||||
KUBECTL_VERSION: v1.35.5
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v7
|
||||
- name: "Install Helm"
|
||||
uses: azure/setup-helm@v5
|
||||
- name: "Install kubectl"
|
||||
run: |
|
||||
# kubectl and the cluster are a pair: the client talks to the server
|
||||
# K3S_IMAGE starts, and Kubernetes supports at most one minor of
|
||||
# skew between them. Renovate updates KUBECTL_VERSION on its own
|
||||
# while K3S_IMAGE is tracked by nothing, so without this check the
|
||||
# two drift apart until the client is talking to a server it no
|
||||
# longer supports - silently, because kubectl only warns. And it is
|
||||
# not theoretical: measured by dry run, a lookup from v1.34.0
|
||||
# offers v1.37.0 in ONE step, because Renovate fills the non-major
|
||||
# bucket with the newest candidate rather than walking the minors.
|
||||
# So this check WILL fire one day, on purpose, with a message that
|
||||
# says which of the two pins to move.
|
||||
#
|
||||
# Deliberately not solved with an allowedVersions constraint pinning
|
||||
# kubectl to the k3s minor: that is the nginx trap the README
|
||||
# describes - a filter that silently stops offering updates the day
|
||||
# the other side moves. A loud failure beats a quiet freeze.
|
||||
read -r KMAJ KMIN <<<"$(printf '%s' "$KUBECTL_VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+).*/\1 \2/')"
|
||||
read -r SMAJ SMIN <<<"$(printf '%s' "$K3S_IMAGE" | sed -E 's/.*:v([0-9]+)\.([0-9]+).*/\1 \2/')"
|
||||
SKEW=$(( (KMAJ * 100 + KMIN) - (SMAJ * 100 + SMIN) ))
|
||||
if [ "${SKEW#-}" -gt 1 ]; then
|
||||
echo "::error::kubectl $KUBECTL_VERSION and k3s $K3S_IMAGE are more than one minor apart. Bump K3S_IMAGE to match, or pin KUBECTL_VERSION back."
|
||||
exit 1
|
||||
fi
|
||||
curl -sSfL -o /tmp/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
install -m 0755 /tmp/kubectl "$HOME/.local/bin/kubectl"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/kubectl" version --client
|
||||
- name: "Provide envsubst"
|
||||
run: |
|
||||
# ci/run-install-test.sh renders ci/<chart>/fixtures.env.yaml with
|
||||
# envsubst - today only freesailarr has one, and without the tool
|
||||
# the fixtures collapse to "error: no objects passed to apply",
|
||||
# gluetun never gets its Wireguard key, and nothing behind its kill
|
||||
# switch becomes Ready. GitHub's hosted image ships gettext-base,
|
||||
# the Gitea runner image does not. The script is fine as it is; a
|
||||
# standard tool is simply missing.
|
||||
#
|
||||
# CHECKED FOR, NOT PINNED - and that is a different decision from
|
||||
# kubectl above, on purpose. kubectl's VERSION changes what the test
|
||||
# means: its distance to the API server is a real source of failure,
|
||||
# which is why it is pinned, tracked and guarded. envsubst
|
||||
# substitutes text; which release performs the substitution does not
|
||||
# change the result. Pinning it would buy nothing and add a line for
|
||||
# Renovate to carry. So: present - do nothing, which keeps the
|
||||
# GitHub path free of any package install; missing - fetch it, and
|
||||
# the difference between the two runners is stated rather than
|
||||
# hidden behind an unconditional apt-get.
|
||||
if command -v envsubst >/dev/null 2>&1; then
|
||||
echo "envsubst present at $(command -v envsubst) - nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
echo "envsubst missing on this runner image - installing gettext-base"
|
||||
if [ "$(id -u)" -eq 0 ]; then SUDO=""; else SUDO="sudo"; fi
|
||||
$SUDO apt-get update -qq
|
||||
$SUDO apt-get install -y -qq --no-install-recommends gettext-base
|
||||
command -v envsubst
|
||||
# freesailarr is the one chart whose install-test needs credentials:
|
||||
# without a real Wireguard key gluetun never completes a handshake, and
|
||||
# behind its kill switch not a single app container becomes Ready. The key
|
||||
@@ -290,8 +403,124 @@ jobs:
|
||||
echo "Both VPN values arrived non-empty."
|
||||
- name: "Install k3d"
|
||||
run: curl -sSfL https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG="$K3D_VERSION" bash
|
||||
# One cluster per matrix leg, named after the chart. It used to be the
|
||||
# fixed name "install-test", and that was wrong on GitHub too - it just
|
||||
# could not show there, because GitHub gives every matrix leg its own VM
|
||||
# with its own Docker daemon, so two legs never saw each other's
|
||||
# cluster. The environment hid the bug for two years rather than
|
||||
# preventing it. Gitea runs all legs of a matrix CONCURRENTLY in ONE job
|
||||
# on ONE daemon, and there the fixed name collides at once: measured on
|
||||
# the probe PR of #225, one leg logs "Re-using existing network
|
||||
# 'k3d-install-test'", the other dies on "The container name
|
||||
# /k3d-install-test-server-0 is already in use" - and then the first
|
||||
# leg's rollback deletes the shared network out from under the second,
|
||||
# which fails with "network k3d-install-test not found". Both legs red,
|
||||
# neither cause in either message. DO NOT put the fixed name back.
|
||||
#
|
||||
# Why the bare chart name and not a prefixed one: k3d rejects a cluster
|
||||
# name over 32 characters ("Cluster name must be <= 32 characters",
|
||||
# measured against k3d v5.9.0 - 32 passes, 33 fails), and every
|
||||
# container is named k3d-<cluster>-server-0 from it. The longest chart
|
||||
# here is calibre-web-automated at 21, so "install-test-<chart>" would
|
||||
# be 34 and break exactly that one chart and no other - the kind of
|
||||
# prefix that is worse than the fixed name because it only sometimes
|
||||
# breaks. The guard below states the limit instead of leaving it to be
|
||||
# rediscovered.
|
||||
- name: "Create k3d cluster"
|
||||
run: k3d cluster create install-test --image "$K3S_IMAGE" --wait --timeout 180s
|
||||
run: |
|
||||
CLUSTER="${{ matrix.chart }}"
|
||||
if [ "${#CLUSTER}" -gt 32 ]; then
|
||||
echo "::error::chart name '$CLUSTER' is ${#CLUSTER} characters and k3d allows at most 32 for a cluster name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Does THIS job run inside a container? That, and not which forge we
|
||||
# are on, is what decides whether k3d's cluster is reachable (#227).
|
||||
#
|
||||
# k3d publishes the API server port on the Docker *host*. When the
|
||||
# job runs straight on the host - GitHub's hosted runners - that is
|
||||
# the same machine, and the "https://0.0.0.0:<port>" k3d writes into
|
||||
# the kubeconfig works. When the job itself is a container, as every
|
||||
# Gitea Actions job is, the host is the dind container and 0.0.0.0
|
||||
# points at the job container itself, where nothing listens. The
|
||||
# cluster comes up fine and the first kubectl call is refused.
|
||||
#
|
||||
# Deliberately NOT keyed on github.server_url, although that would
|
||||
# pick the right branch today: a self-hosted GitHub runner in a
|
||||
# container has exactly this problem, and a condition that says
|
||||
# "GitHub" while meaning "not containerised" is the kind of lie this
|
||||
# repo exists to remove. So the state is detected instead of
|
||||
# inferred, and the detection is the same question the fix needs
|
||||
# answered anyway: ask the daemon about ourselves. Inside a
|
||||
# container that the daemon knows, this returns our network and k3d
|
||||
# joins it, which puts k3s and this job on one bridge where Docker's
|
||||
# own DNS resolves the container names. On a host it returns nothing,
|
||||
# because the hostname is not a container, and the old path is taken
|
||||
# unchanged.
|
||||
# The "|| true" is load-bearing: this step runs under `bash -e -o
|
||||
# pipefail`, and on a host `docker inspect` exits non-zero, which
|
||||
# would abort the step instead of taking the else branch.
|
||||
JOB_NET="$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$(hostname)" 2>/dev/null | awk '{print $1}' || true)"
|
||||
|
||||
if [ -n "$JOB_NET" ]; then
|
||||
echo "Containerised job: $(hostname) on docker network '$JOB_NET' - k3d joins it."
|
||||
k3d cluster create "$CLUSTER" --image "$K3S_IMAGE" --network "$JOB_NET" --wait --timeout 180s
|
||||
# Same reason: the published port is useless from in here, so the
|
||||
# kubeconfig is pointed at the load balancer container by name,
|
||||
# over the shared network. If the API server certificate did not
|
||||
# carry that name this would fail on the TLS handshake and the
|
||||
# cluster-info below would say so - it holds without an extra
|
||||
# --tls-san, measured on the probe PR of #225.
|
||||
kubectl config set-cluster "k3d-$CLUSTER" --server="https://k3d-$CLUSTER-serverlb:6443"
|
||||
else
|
||||
echo "Job runs on the docker host - k3d's published port is reachable as written."
|
||||
k3d cluster create "$CLUSTER" --image "$K3S_IMAGE" --wait --timeout 180s
|
||||
fi
|
||||
|
||||
# Reachability is proven HERE, and the shape of this check is the
|
||||
# product of getting it wrong twice. Both mistakes are written down
|
||||
# because both look like something to simplify away.
|
||||
#
|
||||
# It is a LOOP and not a single call. k3d's --wait returns when the
|
||||
# node container reports ready, but the API server can still answer
|
||||
# discovery with 503 for a moment after that. Measured: a single
|
||||
# `kubectl cluster-info` right here failed with "ServiceUnavailable:
|
||||
# the server is currently unable to handle the request" on one of
|
||||
# two legs of the SAME GitHub run while the other passed. A check
|
||||
# that is itself racy produces exactly the sporadic red this job
|
||||
# must never have (#227) - and a test people have learned to re-run
|
||||
# is one they have stopped believing.
|
||||
#
|
||||
# It counts READY NODES and not "did kubectl answer". `kubectl get
|
||||
# nodes` exits 0 on an EMPTY list, so the first version passed
|
||||
# against a cluster whose API server was up and whose node had never
|
||||
# registered: the log said "No resources found", the step went
|
||||
# green, and the failure surfaced five minutes later as a pod stuck
|
||||
# in Pending with no node and no scheduling event - nowhere near its
|
||||
# cause. A check that a broken cluster passes is worse than no
|
||||
# check. `kubectl wait --for=condition=Ready nodes --all` is not the
|
||||
# shorter way to write this: it has the identical hole, because with
|
||||
# zero nodes the condition is vacuously satisfied.
|
||||
#
|
||||
# The evidence that this version earns its keep: only after it
|
||||
# counted nodes did the real cause become visible in the dump below
|
||||
# - k3s could not start containerd ("error creating fsnotify
|
||||
# watcher: too many open files", an inotify limit on the host), and
|
||||
# nothing above this line had ever said so.
|
||||
for ATTEMPT in $(seq 1 90); do
|
||||
READY_NODES="$(kubectl get nodes --no-headers 2>/dev/null | awk '$2 == "Ready"' | wc -l | tr -d ' ')"
|
||||
[ "$READY_NODES" -ge 1 ] && break
|
||||
if [ "$ATTEMPT" -eq 90 ]; then
|
||||
echo "::error::the k3d cluster '$CLUSTER' answers but has no Ready node after 180s"
|
||||
kubectl get nodes -o wide || true
|
||||
kubectl describe nodes || true
|
||||
echo "--- k3s server log ---"
|
||||
docker logs "k3d-$CLUSTER-server-0" 2>&1 | tail -80 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
kubectl get nodes -o wide
|
||||
# Every chart reaching this step is one the exclusion filter let through,
|
||||
# so the script's SKIP branch is never taken here - it exists for local
|
||||
# runs. The variables below are empty for every chart but the one that
|
||||
@@ -302,6 +531,10 @@ jobs:
|
||||
FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }}
|
||||
FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }}
|
||||
run: ci/run-install-test.sh "${{ matrix.chart }}"
|
||||
# Must carry the same per-leg name as the create step above: a blanket
|
||||
# cleanup, or the old fixed name, would tear down the neighbouring leg's
|
||||
# cluster on a runner that shares one Docker daemon - which is precisely
|
||||
# the half of the collision that turned one red leg into two.
|
||||
- name: "Delete k3d cluster"
|
||||
if: always()
|
||||
run: k3d cluster delete install-test || true
|
||||
run: k3d cluster delete "${{ matrix.chart }}" || true
|
||||
@@ -7,16 +7,28 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
The main Claude session acts as the **task manager** and does not implement issues itself:
|
||||
|
||||
1. **Plan first, get approval**: The manager presents a plan for the issue and waits for the user's explicit approval. No implementation before approval.
|
||||
2. **Document the final plan on the issue**: Once approved, the plan is posted as a comment on the GitHub issue before implementation starts (skip if the issue body already contains the full plan; later decisions go there as comments too).
|
||||
2. **Document the final plan on the issue**: Once approved, the plan is posted as a comment on the issue before implementation starts (skip if the issue body already contains the full plan; later decisions go there as comments too).
|
||||
3. **Delegate implementation**: After approval, the manager launches a dedicated **development agent**. The agent implements the issue completely on its own branch and opens a PR.
|
||||
4. **PR comment handling by the agent**: After opening the PR, the development agent watches it for review comments. New comments are addressed automatically (code changes, reply on the thread), and addressed threads are marked as **resolved**.
|
||||
5. **Agent lifetime**: The development agent terminates itself once its PR has been merged (or closed).
|
||||
6. Only after the PR has been **merged by the user** may work on the next issue begin. The user merges; never merge yourself.
|
||||
7. **After the merge, verify the linked issue was closed** (auto-close via "Fixes #n"); close it manually if not.
|
||||
7. **After the merge, verify the linked issue was closed** (auto-close via "Fixes #n"); close it manually if not. Gitea auto-closes on the same keywords, but only for an issue in the same repository — a migrated issue whose number changed is closed by hand.
|
||||
|
||||
Additional rules for all commits, PRs, issues, and comments:
|
||||
|
||||
- Do **not** add a `Co-Authored-By` trailer and do **not** append "Generated with Claude Code" footers.
|
||||
- **Commits must be signed.** Verify the signature before opening the PR; if `op-ssh-sign` fails, stop and report rather than pushing unsigned work.
|
||||
|
||||
## Which forge, and with which tool
|
||||
|
||||
The repository is moving from github.com to the Gitea instance JLab-Git (#225). The move is **not finished**, so both are live and the steps below differ by where the work happens. README, "Where this repo lives", holds the measured differences between the two CIs; this section holds only what changes about the *workflow*.
|
||||
|
||||
- **Remotes are named `github` and `JLab-Git`** — there is no `origin`. Say which one you mean; `git push` without a remote is ambiguous here.
|
||||
- **Today the merge flow still runs on GitHub.** Issues, pull requests and merges happen there until the maintainer switches Actions off and turns on the mirror. Until then, **do not push to `JLab-Git/main`**: with publish workflows armed on both sides, two CIs publish into the same registry, and that registry accepts a re-upload of an existing version silently, replacing the artifact (#160). Pushing a *branch* to Gitea is fine — only `main` triggers a publish.
|
||||
- **`gh` only as `op plugin run -- gh`**, `tea` only as `op plugin run -- tea`. A bare `gh` falls back to the wrong identity. Every call costs a 1Password confirmation, so bundle them and never poll.
|
||||
- **After the move: no merge queue.** Gitea has none. Its place is taken by **auto-merge on green checks** — the PR is armed once and merges itself when the required checks pass. The manager still decides *whether* to arm it; rule 6 above is unchanged: the user merges, and an agent never merges its own PR.
|
||||
- **Issue numbers will not survive the move.** Gitea renumbers, and issues and PRs share one counter, so a `#n` written today will not land on the same number over there. The migration itself is the maintainer's one-off job over the API; how the old references are dealt with afterwards is open and decided in #225. Until that is decided, a `#n` anywhere in this repo means the GitHub issue.
|
||||
- **Renovate is already running on the Gitea side, and not from this repo.** A shared `jlab-renovate-bot` instance serves the other repositories there and has this one enrolled; it reads our `renovate.json5` and its `postUpgradeTasks` hook is allow-listed (both verified on its first run). **Do not add a Renovate workflow here** — a second runner against the same repo means duplicate PRs and two schedulers rewriting each other's branches. README, "Renovate on Gitea", has the evidence.
|
||||
|
||||
## Keep the docs current
|
||||
|
||||
@@ -26,7 +38,7 @@ Additional rules for all commits, PRs, issues, and comments:
|
||||
|
||||
## What this repo is
|
||||
|
||||
A collection of self-made Helm charts, one directory per chart, published to a private, classic (index-based) Helm registry via GitHub Actions — an HTTP `POST` of the packaged `.tgz`, not an OCI `helm push`; the target URL and its credentials live in 1Password and never in this repository. Validation happens on three levels: locally with Helm, in the PR pipeline (`validate-charts.yml`: strict lint, package, version-bump check), and in the PR install-test stage (k3d, fixtures under `ci/` — see README):
|
||||
A collection of self-made Helm charts, one directory per chart, published to a private, classic (index-based) Helm registry by CI (GitHub Actions today, Gitea Actions after #225; the workflow files are read by both unchanged) — an HTTP `POST` of the packaged `.tgz`, not an OCI `helm push`; the target URL and its credentials live in 1Password and never in this repository. Validation happens on three levels: locally with Helm, in the PR pipeline (`validate-charts.yml`: strict lint, package, version-bump check), and in the PR install-test stage (k3d, fixtures under `ci/` — see README):
|
||||
|
||||
```bash
|
||||
helm lint --strict <chart-name>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# helm-charts
|
||||
|
||||
Collection of some self-made helm-charts. Each top-level directory is a standalone Helm chart that gets packaged and uploaded to a private, classic (index-based) Helm registry via GitHub Actions.
|
||||
Collection of some self-made helm-charts. Each top-level directory is a standalone Helm chart that gets packaged and uploaded to a private, classic (index-based) Helm registry by CI.
|
||||
|
||||
> **The repository is moving from github.com to the Gitea instance JLab-Git (#225).** The code is already on both and identical; what still runs the show is GitHub. The workflow files under `.github/workflows/` are read by both forges unchanged, and "Where this repo lives" below names the handful of places where the two behave differently — each of them measured on a probe pull request, not assumed.
|
||||
|
||||
## Charts
|
||||
|
||||
@@ -38,7 +40,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
- Bump the **chart version** in every change that touches a chart directory — a push to `main` publishes the chart.
|
||||
- **App-version updates go through PRs** (so the validate and install-test pipelines run). The manually dispatched `Change App-Version` workflow (rewrites `version:`/`appVersion:` and commits directly to `main`) exists but bypasses both pipelines. For app **major** jumps, update stepwise: first to the last release of the old major line (own PR, merge, publish), then to the newest major.
|
||||
|
||||
## CI/CD (GitHub Actions)
|
||||
## CI/CD
|
||||
|
||||
- `publish-<chart>.yml` — one thin workflow per chart; triggers on pushes to `main` touching `<chart>/**` (or manually) and calls the reusable workflow below. **A push to a chart directory on `main` publishes that chart**, so the chart version must be bumped in the same change.
|
||||
- `publish-helm-chart.yml` — reusable workflow: `helm package`, then a single HTTP `POST` of the packaged `.tgz` to the private chart registry. The target is a **classic, index-based** Helm registry (Gitea's `/api/charts` endpoint), not an OCI one: `helm push` speaks only OCI and is not used, there is no `helm registry login` session, and every request carries its own Basic auth. Credentials **and the target URL** come from 1Password Connect (`OP_CONNECT_TOKEN` secret, item `op://GitHub-Actions/jlab-registry-access/` with `username`, `password`, `charts-url`); the workflow appends `/api/charts` to `charts-url`. No registry address ever appears in this repository. The loaded values are read via **`steps.<id>.outputs.*`**, mapped into the step's own `env:` block and used as quoted shell variables — never interpolated into the command line. `load-secrets-action` **v5 no longer exports them as environment variables** for later steps the way v4 did, so after the v5 bump every `${{ env.* }}` reference resolved to an empty string while the loading step still logged `Populating variable` and stayed green. Each value is additionally guarded with `:?`, because an empty one used to collapse into a missing argument rather than an error. Three properties of the upload step are load-bearing:
|
||||
@@ -51,6 +53,22 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but one). It runs in **five** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in `publish-helm-chart.yml`, and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh` and `ci/run-trivy-config.sh`. See "Chart dependencies" in the style guidelines for what that costs.
|
||||
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (the same changed-charts matrix, minus the charts listed in `ci/excluded-charts.txt`) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` exercises nothing by itself; the Trivy scan has a fixed-sample fallback for that case, the install-test has none — bump the affected chart's patch version to pull it into the matrix. The **resource-quantity render test** is the exception to that rule: it runs over every chart on every PR — see below.
|
||||
|
||||
### Where this repo lives, and what Gitea Actions does differently
|
||||
|
||||
The move to the Gitea instance JLab-Git (#225) is in progress. Order matters and is not negotiable: **CI provably green on Gitea → GitHub Actions off → mirror on.** Any other order leaves a window in which either no CI exists or two of them publish into the same registry — and that registry accepts a re-upload of an existing version silently, replacing the artifact (#160).
|
||||
|
||||
Everything below was measured on a throwaway pull request against the Gitea instance, not taken from documentation. It is the reason for the two deviations now in `validate-charts.yml`.
|
||||
|
||||
- **Reusable workflows with `secrets: inherit` work.** The 23 thin `publish-<chart>.yml` hulls survive the move unchanged. (A missing secret does not fail the job — Gitea fails at *job creation*, so the run shows no steps at all, which is worth knowing before debugging the wrong thing.)
|
||||
- **`ubuntu-latest` on this instance has no Docker socket; `ubuntu-docker` has.** Same label, different meaning than on GitHub's hosted runners, and only one job in this repo cares: the install-test creates a k3d cluster, and k3d is a Docker client. Measured, per job: `detect-changed-charts`, `build-chart`, the quantity render and the Trivy config scan all pass on `ubuntu-latest` — none of them needs Docker. The install-test on `ubuntu-latest` dies at `Create k3d cluster` with `Cannot connect to the Docker daemon at unix:///var/run/docker.sock`, an error that names k3d and never the runner label.
|
||||
- **`ubuntu-docker` is necessary but not yet sufficient.** Its daemon (8 vCPU, 64 GB, Docker 29.5.2) is **rootless** and reports `Cgroup Driver: none` plus `WARNING: Running in rootless-mode without cgroups`. k3d gets as far as creating the network, pulling the images and starting the node; the k3s server container then restarts immediately with an empty log. k3s needs cgroup delegation. Until the instance's Docker-in-Docker gets it (rootful, or rootless with systemd delegation), **the install-test cannot run on Gitea at all** — see #225.
|
||||
- **`aquasecurity/setup-trivy` cannot be used.** It runs `actions/checkout` against `aquasecurity/trivy` on github.com with `github.token`, which on Gitea is a *Gitea* token; github.com answers 401 and checkout retries twice before ending on `fatal: could not read Username for 'https://github.com'`. Its `actions/cache` step is inert on the instance as well (`Cache action is only supported on GHES version >= 3.5`), and fetching the nested actions cost 4m30s before the job started. The workflow now downloads the release tarball instead — one request, identical behaviour on both forges, and the `# renovate:` comment above `TRIVY_VERSION` still keeps it current (proved by dry run: pinned to `v0.72.0`, the lookup offers `v0.74.0` as a minor).
|
||||
- **Matrix jobs lose their name in the status list.** Gitea does not expand `${{ matrix.chart }}` in a job `name:`; it substitutes the matrix *expression*, so the commit status reads `Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request)` and **all legs of a matrix share that one context**. Consequence for branch protection: a per-chart required check is not possible on Gitea, and the required-check names differ from GitHub's — they have to be read off a real run rather than copied.
|
||||
- **Actions are fetched from github.com anonymously and that works.** `actions/checkout`, `azure/setup-helm` and the 1Password action need no replacement; only an action that *itself* authenticates against github.com breaks (see Trivy above).
|
||||
- **No merge queue.** Gitea has none. Its place is taken by auto-merge on green checks, which for a single maintainer is equivalent in effect and different in one way worth remembering: auto-merge merges as soon as the checks it knows about are green, so it is only as strong as the *required* checks configured on `main`. With none configured it merges immediately.
|
||||
|
||||
Still on the maintainer's side and not delegable: branch protection plus the commit-signature requirement on the Gitea repo, the runner's cgroup problem above, and switching GitHub Actions off before enabling the mirror. The bot token gets `403` on `actions/runners`, `actions/secrets` and `branch_protections`, so none of that can be verified from here.
|
||||
|
||||
### Resource-quantity render test (all charts, every PR)
|
||||
|
||||
`ci/run-quantity-render-test.sh [chart …]` (no argument = all charts) renders every chart twice with the fixtures the install-test and the Trivy scan already use, injecting a bare integer memory request at **every** `resources` block the chart's `values.schema.json` declares:
|
||||
@@ -90,7 +108,7 @@ Fixture images avoid Docker Hub where possible (`public.ecr.aws`/`ghcr.io`) to d
|
||||
|
||||
## Dependency updates (Renovate)
|
||||
|
||||
Renovate runs as the self-hosted `jlab-renovate` GitHub App. Its config is **`renovate.json5`** in the repo root (JSON5 for the inline reasoning; requires Renovate >= 41 for `managerFilePatterns`/`customType`). There must be exactly one config file — do not add a second `renovate.json` or `.github/renovate.json`.
|
||||
Renovate runs as the self-hosted `jlab-renovate` GitHub App while the repo is on github.com, and on Gitea as the shared `jlab-renovate-bot` instance that already serves the other repositories there — see "Renovate on Gitea" below. **There is no Renovate workflow in this repo, and none should be added.** Which of the two runs it changes nothing about *what* gets updated: its config is **`renovate.json5`** in the repo root (JSON5 for the inline reasoning; requires Renovate >= 41 for `managerFilePatterns`/`customType`). There must be exactly one config file — do not add a second `renovate.json` or `.github/renovate.json`.
|
||||
|
||||
What Renovate keeps current:
|
||||
|
||||
@@ -134,9 +152,20 @@ Everything is derived from the base commit, so the script is idempotent across t
|
||||
|
||||
If the dependency dashboard reports `Failed to look up github-releases package …: no-result` for third-party repos, that is the instance's GitHub credentials, not this config: a GitHub App installation token is not a good fit for looking up repos the app is not installed on. Give the instance a separate read-only PAT as `GITHUB_COM_TOKEN` for github.com datasource lookups.
|
||||
|
||||
### Renovate on Gitea
|
||||
|
||||
**Do not add a Renovate workflow to this repository.** It is the obvious thing to reach for — Gitea has no app model, so a scheduled job in the repo looks like the only way to run the bot — and it is wrong here: a **shared `jlab-renovate-bot` instance already runs on the Gitea side** and already has this repository enrolled. A second runner against the same repo produces duplicate PRs and two schedulers rewriting each other's branches, which is not a safety net but a fault. This paragraph exists so the next reader who finds no workflow knows why instead of building one.
|
||||
|
||||
The instance is outside this repository, which has two consequences worth writing down:
|
||||
|
||||
- **It reads our `renovate.json5` and the per-chart rules do fire.** Measured, not assumed: its first run on the migrated repo opened PRs for `searxng` and `gotenberg` plus the dependency dashboard. Those two are `customManagers` targets of ours — no built-in manager finds an `appVersion:` in a `Chart.yaml` — so the config is demonstrably in effect.
|
||||
- **`allowedCommands` is already granted there, and the version-bump hook runs.** This was the expensive unknown: `postUpgradeTasks` only executes when the command is allow-listed in the *instance* config, and a missing entry is skipped **silently**, leaving every chart PR red on the version check. The proof is in that searxng PR's diff, which carries `version: 1.0.4+up2026.9.11.ffe96f8a6` → `1.0.5+up2026.9.13.96479fff6` alongside the `appVersion` change. Nothing but `ci/renovate-bump-chart.sh` writes `version:`. Nothing has to be configured for the move — but the allow-list is not visible from this repo either, so if it is ever dropped, the symptom here will be red chart PRs with no explanation.
|
||||
|
||||
One item does not come for free on either side and is named in "The version-bump hook" above: a `GITHUB_COM_TOKEN` for github.com datasource lookups. Under the GitHub App those rode on the installation token; a Gitea token is worth nothing at github.com, and unauthenticated lookups hit the rate limit inside a single pass. The symptom is a `no-result` line on the dependency dashboard, not an error.
|
||||
|
||||
### PR policy
|
||||
|
||||
- **Automerge** for `patch`, `minor` and `digest` updates, after a `minimumReleaseAge` of 3 days and only once the PR pipeline is green — install-test included, which is why that job no longer runs with `continue-on-error`. Minor is in scope because the sidecar images barely move in patch steps (`alpine 3.22` → `3.24`, `nginx 1.30.4` → `1.31.4` are both minor), so a patch-only rule would practically never fire. Renovate merges itself rather than using GitHub auto-merge, which would merge immediately as long as `main` has no required status checks. Majors are merged by hand.
|
||||
- **Automerge** for `patch`, `minor` and `digest` updates, after a `minimumReleaseAge` of 3 days and only once the PR pipeline is green — install-test included, which is why that job no longer runs with `continue-on-error`. Minor is in scope because the sidecar images barely move in patch steps (`alpine 3.22` → `3.24`, `nginx 1.30.4` → `1.31.4` are both minor), so a patch-only rule would practically never fire. Renovate merges itself (`platformAutomerge: false`) rather than handing the PR to the forge's own auto-merge, which on **both** GitHub and Gitea would merge immediately as long as `main` has no required status checks. That is a statement about the branch protection, not about the platform: once required checks exist on `main`, `platformAutomerge: true` becomes the better setting on either side. Majors are merged by hand.
|
||||
- **Majors** arrive one line at a time (`separateMultipleMajor`): a v1 → v3 jump produces a PR for the latest v2 first and then one for v3 — the stepwise procedure the versioning section requires.
|
||||
- `prConcurrentLimit: 10` / `prHourlyLimit: 4` keep the k3d install-tests from all starting at once.
|
||||
- The manually dispatched `change-app-version.yml` still exists for one-off bumps, but the normal path for app updates is now a Renovate PR.
|
||||
|
||||
+15
-4
@@ -50,6 +50,14 @@
|
||||
//
|
||||
// Without that entry Renovate skips the task silently and every chart PR
|
||||
// stays red on the version-bump check.
|
||||
//
|
||||
// On the Gitea side (#225) that entry EXISTS and was verified rather than
|
||||
// hoped for: the shared instance's first run on the migrated repo produced
|
||||
// a searxng PR whose diff carries `version: 1.0.4+up... -> 1.0.5+up...`
|
||||
// next to the appVersion. No built-in manager touches `version:`, so that
|
||||
// line can only come from this hook. Nothing has to be configured for the
|
||||
// move - but the allow-list lives on that instance, not in this repo, so it
|
||||
// is also not visible from here if it is ever removed.
|
||||
postUpgradeTasks: {
|
||||
commands: ["ci/renovate-bump-chart.sh {{{packageFileDir}}}"],
|
||||
fileFilters: ["**/Chart.yaml"],
|
||||
@@ -80,10 +88,13 @@
|
||||
matchUpdateTypes: ["patch", "minor", "digest"],
|
||||
automerge: true,
|
||||
automergeType: "pr",
|
||||
// Renovate waits for the checks itself instead of handing the PR to
|
||||
// GitHub's auto-merge. GitHub would merge immediately when the branch has
|
||||
// no required status checks configured — and validate-charts is not (yet)
|
||||
// a required check.
|
||||
// Renovate waits for the checks itself instead of handing the PR to the
|
||||
// forge's own auto-merge. Both of them — GitHub's, and the auto-merge
|
||||
// that takes the merge queue's place on Gitea, which has none — merge
|
||||
// immediately when the branch has no required status checks configured,
|
||||
// and validate-charts is not (yet) a required check on either side. This
|
||||
// is a statement about the branch protection, not about the platform:
|
||||
// once required checks exist on `main`, true is the better value.
|
||||
platformAutomerge: false,
|
||||
// A release that is hours old should not walk into main unattended; a
|
||||
// chart publish follows the merge straight away.
|
||||
|
||||
Reference in new issue
Block a user