Table of Contents
- Forge and tooling
- Gitea is the forge, GitHub is a mirror
- tea, not gh
- The remote is origin, and it points at Gitea
- Cut every worktree from a freshly fetched origin/main
- Signed commits are enforced
- No merge queue — auto-merge takes its place
- A required check per chart is not possible
- Issue numbers changed with the move
- Renovate runs outside this repository
- What a Gitea runner does differently
- The wiki is a separate git repository
Forge and tooling
Where this repository lives, which CLI talks to it, and the handful of things that are different from GitHub in a way that costs time if you find out late.
Gitea is the forge, GitHub is a mirror
Everything runs on the Gitea instance JLab-Git: issues, pull requests, CI and Renovate. main there is the leading copy.
github.com is a push mirror with Actions and Renovate switched off, and it lags until the mirror runs. Nothing there is authoritative, and a green or absent check there means nothing.
The workflow files under .github/workflows/ are read by Gitea unchanged — the directory name is a GitHub convention Gitea adopted, not a sign that the workflows run there.
tea, not gh
Issues and pull requests are driven with tea:
tea pr list
tea pr create --base main --head <branch> --title … --description …
Work happens in a Coder workspace, and it has no op. That is the normal case, not a broken environment — do not wait for the 1Password CLI and do not propose installing it as a precondition. tea carries the workspace's stored login, so anything it creates is authored by the bot account; that is accepted, and worth naming in the report when something is opened that way.
op plugin run -- tea is the older instruction and applies only where op actually exists. Where it does, every call costs a 1Password confirmation, so bundle calls and never poll.
The remote is origin, and it points at Gitea
The workspace checkout — and every worktree cut from it — has exactly one remote:
origin ssh://git@git.jk-dev.de:30022/jlab-cluster/helm-charts.git
So a bare git push goes to Gitea, the leading copy. The GitHub mirror is fed by Gitea itself and is not a remote here; nothing is ever pushed to it by hand.
A clone elsewhere may have more than one — the maintainer's machine also carries the mirror, as a remote named github. Wherever a second remote exists, say which one you mean, in commands and in prose.
Cut every worktree from a freshly fetched origin/main
git fetch origin main
git switch -c <branch> origin/main
Never from the local main ref, and never on the assumption that the shared checkout is current. Both can be many merges behind, and nothing about them says so.
What lifts this above tidiness is how an agent receives its instructions. The project-instructions block a subagent is handed at the head of its context — the binding copy of CLAUDE.md — is a snapshot of the spawning session's working directory, taken when that session started. It is not a live read of the file, and nothing re-reads it afterwards: fast-forwarding the directory mid-session leaves the block exactly as it was, so the file on disk can be correct while every agent started from then on still receives the superseded rules. The agent cannot tell the difference, because a stale block looks exactly as authoritative as a current one.
Measured on 2026-09-27:
- The manager session's working directory hung several merges back. An agent started from it reported a "contradiction" in
CLAUDE.md— the remote bullet — that had been fixed two PRs earlier, and quoted the stale text as the rule in force. - Later the same day, with that working directory deliberately fast-forwarded to the tip beforehand and the result checked ("Already up to date"), the next agent's block was still from before Gitea #47 — while the agent's own worktree was identical to
origin/main,git rev-list --left-right --count origin/main...HEADreturning0 0. This is the measurement that identifies the block as a snapshot from session start, rather than merely a symptom of a stale checkout. - In the shared checkout,
main..HEADshowed 105 differing files, while the difference againstorigin/mainwas a single line.
Two things follow, and only these two help:
- The spawning session's working directory has to be current before the session starts. Pulling afterwards is too late for every agent that session goes on to start.
- An agent verifies a rule against the file in its own worktree, and against a diff with
origin/main— never againstmain, and never against its own context block. The first point cannot be enforced from inside an agent; this one can, and it is what kept both incidents above harmless. A worktree that turns out to be behind is rebased ontoorigin/mainbefore the work starts, not after, which makes the fetch and the comparison the first thing to run in a fresh worktree.
Signed commits are enforced
Branch protection on main requires a signature, and signatures verify on Gitea. A commit that is not signed does not reach main.
In the Coder workspace this works without any 1Password step: signing runs through the workspace's own SSH signer (gpg.format = ssh, gpg.ssh.program = coder-git-ssh-sign, commit.gpgsign = true).
Check before opening the PR, not at merge time:
git log --format='%G?' -1 # must print G
git log --show-signature -1 # the long form
If signing fails, stop and report. Pushing unsigned work only moves the failure to a place where it costs a review.
The same setting makes a lightweight tag impossible
tag.gpgsign = true comes with it, and it forces every tag into the annotated form. git tag <name> <sha> therefore cannot create a lightweight tag here — it aborts with fatal: no tag message?.
That matters where a tag series is expected to be uniform. workflow-util/pipelines versions its shared building blocks with lightweight tags (v1, v2, v3), and a mixed series is an inconsistency nobody can explain a year later. The way through:
git update-ref refs/tags/<name> <sha>
git push origin refs/tags/<name>
git cat-file -t <name> # must print "commit", not "tag"
cat-file -t is the check, not the assumption: a lightweight tag points straight at the commit, an annotated one at a tag object. ls-remote --tags shows the same difference — an annotated tag adds a second line ending in ^{}. Measured on 2026-10-01 while tagging v3 in that repository.
No merge queue — auto-merge takes its place
Gitea has no merge queue. Its place is taken by auto-merge on green checks: a PR is armed once and merges itself when the required check passes and the required review exists. Both are active on main — an armed PR that nobody has reviewed sits there instead of merging, which is what makes Renovate's global platformAutomerge: true something other than unattended merging (see Renovate → PR policy).
This does not change who merges. The user merges. An agent never merges its own PR and never arms auto-merge on it.
A required check per chart is not possible
main requires validate-charts, as the one status context it reports under. Per chart is the part that cannot be had: Gitea runs a whole matrix as one job under one status context. The install-test therefore reports once for all charts, and the context name carries the unexpanded matrix expression:
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request)
Required-check names have to be read off a real run; they cannot be copied from GitHub. Relevant whenever branch protection is touched.
The name above is from before the install-test job moved into the reusable install-test.yml (Gitea #52): the matrix expression now reads ${{ fromJSON(inputs.charts) }}, and a called workflow's job may carry the calling job's name in the context as well. validate-charts itself is unaffected — but read the current name off a real run before putting it into branch protection.
Issue numbers changed with the move
#ninside this repo's files means the GitHub tracker. Most of those numbers name closed issues, which were not migrated and exist only there.- The issues open at the time are Gitea #5–#21 and #24. The mapping is a comment on Gitea #14.
- Gitea silently resolves a bare
#nagainst its own counter. An old reference can therefore render as a link to something unrelated — on Gitea,#22is an unrelated throwaway pull request, not the install-test issue the README means. - When you write a new reference, use the Gitea number.
Renovate runs outside this repository
As the shared jlab-renovate-bot instance, which also serves the other repositories there. It reads our renovate.json5, and its postUpgradeTasks hook is allow-listed in the instance config.
Do not add a Renovate workflow here. Gitea has no app model, so a scheduled job in the repo looks like the only way to run the bot — it is not, and a second runner against the same repo produces duplicate PRs and two schedulers rewriting each other's branches. Details and the evidence: Renovate.
What a Gitea runner does differently
All of it measured on this instance, not taken from documentation. The full list with numbers is in CI/CD; the short version:
- Reusable workflows with
secrets: inheritwork. But a missing secret fails job creation, not the job — the run then shows no steps at all, which looks like something else entirely. - Runner labels mean different machines.
ubuntu-latesthas no Docker socket here;ubuntu-dockerhas one. Only the install-test cares. - A matrix runs concurrently inside one job, sharing one Docker daemon. That is what makes per-leg resource names mandatory.
aquasecurity/setup-trivycannot be used — it checks out from github.com with a Gitea token and fails with a message naming neither the action nor the token.- Every job is itself a container, so a published port on the Docker host is not reachable as written.
The wiki is a separate git repository
git clone ssh://git@git.jk-dev.de:30022/jlab-cluster/helm-charts.wiki.git
Its default branch is main, not master. Worth knowing before the first push, because the wiki has no PR flow and every push is preceded by a fetch and a rebase — reach for origin/master and the rebase fails. That happened on 2026-09-27; it was harmless, since the push afterwards was a clean fast-forward and no force was involved, but it is avoidable by knowing the name.
Flat layout, one Markdown file per page, _Sidebar.md for navigation. Editing it is a required step of the workflow, not an afterthought.
helm-charts wiki
How we work
How it is built
How charts are written
Charts
- apache-tika
- bentopdf
- bulwark
- calibre-web-automated
- convertx
- cyberchef
- freesailarr
- garage
- gotenberg
- homeassistant
- keycloak
- matrix-synapse
- minecraft-router
- minecraft-server
- outline
- paperless-ngx
- patchmon
- rallly
- rss-bridge
- samba
- satisfactory
- searxng
- stalwart
- sure
- teamspeak-server
- template-chart
- urlaubsverwaltung
- valkey
- voucher-vault
- ytdlp-web-player
- zipline