Generate and maintain Gitea Actions CI/CD pipelines that build & publish Docker images (applications-backend/*, applications-frontend/*) and OCI Helm charts (helm-charts/*) from a pnpm + Turborepo monorepo to a Harbor registry, authenticated by a project robot account via Gitea org secret + global vars. - SKILL.md: auth model, convention-driven discovery, publish gating, gotchas - references/harbor-auth.md: docker/helm login, robot accounts, secret/var taxonomy, bake-into-runner alternative, troubleshooting - references/pipeline-workflow.md: jobs, discovery loops, tagging, turbo prune - references/conventions.md: Dockerfile/chart locations, image & chart naming - assets/workflows/ci-cd.yaml: ready-to-drop .gitea/workflows pipeline Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
159 lines
7.4 KiB
Markdown
159 lines
7.4 KiB
Markdown
# Harbor authentication for Gitea Actions
|
|
|
|
How a pipeline authenticates to the Harbor registry to **pull base images and push built
|
|
images/charts** — the robot account model, the exact Gitea secret/variable layout, the login
|
|
commands, and the "bake into the runner" alternative.
|
|
|
|
---
|
|
|
|
## 1. Robot accounts — why, and what they look like
|
|
|
|
CI must never push with a person's Harbor credentials. Harbor's answer is a **robot account**:
|
|
a non-human account scoped to a **project**, with a fixed long username and a **generated**
|
|
secret.
|
|
|
|
- **Username** (public, not a secret): `robot$<project>+<name>` — e.g.
|
|
`robot$gondor-v1+gitea-actions-v1`. The `$` is part of the literal string, not shell syntax.
|
|
- **Secret** (sensitive): a generated token shown **once, at creation**. Harbor never returns
|
|
it again on a GET. If you lose it, you regenerate (which invalidates the old one).
|
|
- **Scope**: project-level. A robot with `push`/`pull` on project `gondor-v1` can only act
|
|
within `gondor-v1`. To publish images/charts to a project, the robot must have **push** there.
|
|
|
|
In this platform the robot is created and cached by the **palantir** `harbor` module — do not
|
|
create robots by hand in the UI:
|
|
|
|
```bash
|
|
# from the palantir-v1 repo, with KUBECONFIG set
|
|
task harbor:project:assure-one -- --name=gondor-v1 --public=false
|
|
task harbor:robot:gitea-actions-v1:assure-one # pull,push -> .harbor/gondor-v1-gitea-actions-v1.yml
|
|
task harbor:robot:gondor-v1:assure-one # pull -> used by the cluster's imagePullSecret
|
|
```
|
|
|
|
The secret is cached in git-ignored `.harbor/<project>-<robot>.yml`. Read it with:
|
|
|
|
```bash
|
|
yq '.secret' .harbor/gondor-v1-gitea-actions-v1.yml
|
|
```
|
|
|
|
There are two robots with different jobs — do not mix them up:
|
|
|
|
| Robot | Permissions | Used by | Credential lands in |
|
|
|---|---|---|---|
|
|
| `gitea-actions-v1` | pull, **push** | Gitea Actions (this skill) | Gitea org secret (below) |
|
|
| `gondor-v1` | pull only | cluster `imagePullSecret` (Argo CD / workloads) | Bitwarden → ESO `ClusterExternalSecret` |
|
|
|
|
---
|
|
|
|
## 2. The Gitea secret/variable layout
|
|
|
|
Gitea Actions has two stores — **variables** (plaintext, visible) and **secrets** (masked) —
|
|
each available at repo, org, and (variables only) **global/admin** level:
|
|
|
|
| | Repo | Org / User | Global (admin) |
|
|
|---|---|---|---|
|
|
| Variables | ✅ | ✅ | ✅ `…/-/admin/actions/variables` |
|
|
| Secrets | ✅ | ✅ | ❌ **no global level** |
|
|
|
|
Because the username and address are **not secret**, put them in **global variables** — set
|
|
once, seen by every organisation. The robot secret is the only value that must be a secret, and
|
|
since there is no global secret, it is an **org secret** (repeat per org you onboard).
|
|
|
|
**Global variables** — Site Administration → Actions → Variables, once:
|
|
|
|
| Name | Value |
|
|
|---|---|
|
|
| `HL_V1_HARBOR_ADDRESS` | `harbor-v1.apps.lego-cloud.eu` |
|
|
| `HL_V1_HARBOR_ROBOT_GITEA_ACTIONS_V1_USERNAME` | `robot$gondor-v1+gitea-actions-v1` |
|
|
|
|
Optionally also global: `HL_V1_HARBOR_PROJECT` = `gondor-v1` (the push target; the template
|
|
defaults to `gondor-v1` if unset).
|
|
|
|
**Org secret** — `https://gitea.lego-cloud.eu/org/<ORG>/settings/actions/secrets`, per org:
|
|
|
|
| Name | Value |
|
|
|---|---|
|
|
| `HL_V1_HARBOR_ROBOT_GITEA_ACTIONS_V1_SECRET` | output of `yq '.secret' .harbor/gondor-v1-gitea-actions-v1.yml` |
|
|
|
|
Referencing them in a workflow:
|
|
|
|
```yaml
|
|
env:
|
|
HARBOR_ADDRESS: ${{ vars.HL_V1_HARBOR_ADDRESS }}
|
|
HARBOR_USERNAME: ${{ vars.HL_V1_HARBOR_ROBOT_GITEA_ACTIONS_V1_USERNAME }}
|
|
HARBOR_SECRET: ${{ secrets.HL_V1_HARBOR_ROBOT_GITEA_ACTIONS_V1_SECRET }}
|
|
HARBOR_PROJECT: ${{ vars.HL_V1_HARBOR_PROJECT || 'gondor-v1' }}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Logging in — docker and helm are separate credential stores
|
|
|
|
```bash
|
|
# container images
|
|
echo "${HARBOR_SECRET}" | docker login "${HARBOR_ADDRESS}" -u "${HARBOR_USERNAME}" --password-stdin
|
|
|
|
# OCI helm charts — a DIFFERENT store; docker login does not cover it
|
|
echo "${HARBOR_SECRET}" | helm registry login "${HARBOR_ADDRESS}" -u "${HARBOR_USERNAME}" --password-stdin
|
|
```
|
|
|
|
- Always `--password-stdin`. A `-p <secret>` argument leaks into the process list and runner
|
|
logs.
|
|
- `helm registry login` is required in addition to `docker login`; `helm push` uses Helm's own
|
|
registry credential store and will return `unauthorized` without it, even right after a
|
|
successful `docker login`.
|
|
- Log out at the end of the job if the runner is long-lived and shared:
|
|
`docker logout "${HARBOR_ADDRESS}"; helm registry logout "${HARBOR_ADDRESS}"`.
|
|
|
|
**Rotation.** Re-run the palantir robot task to regenerate the secret (`.harbor/…` is
|
|
re-cached), then update the Gitea **org secret** with the new value. The username and address
|
|
do not change, so the global variables stay put.
|
|
|
|
---
|
|
|
|
## 4. Alternative — bake the login into the runner
|
|
|
|
If attaching an org secret to every organisation is more chore than you want, pre-authenticate
|
|
the **runner** itself so every job — in every org — can pull/push Harbor with **no** Gitea
|
|
secret referenced.
|
|
|
|
Because act_runner runs each job in a container that talks to the host Docker daemon over the
|
|
mounted socket, `docker` inside a job reads *its own* config, not the host's. So seed a
|
|
`config.json` into every job container:
|
|
|
|
1. Build the auth file locally (secret never printed to the terminal):
|
|
```bash
|
|
U='robot$gondor-v1+gitea-actions-v1'
|
|
S=$(yq '.secret' .harbor/gondor-v1-gitea-actions-v1.yml)
|
|
AUTH=$(printf '%s:%s' "$U" "$S" | base64 | tr -d '\n')
|
|
umask 077
|
|
printf '{ "auths": { "harbor-v1.apps.lego-cloud.eu": { "auth": "%s" } } }\n' "$AUTH" \
|
|
> /tmp/harbor-docker-config.json
|
|
```
|
|
2. Copy it onto the NAS to a host path the runner can mount, e.g.
|
|
`/mnt/<pool>/gitea-runner-harbor/config.json`, then `rm /tmp/harbor-docker-config.json`.
|
|
3. Wire it into the runner:
|
|
- **TrueNAS SCALE app**: Edit → add a read-only Host Path Volume
|
|
(`…/gitea-runner-harbor` → `/opt/harbor-docker`) and env `DOCKER_CONFIG=/opt/harbor-docker`.
|
|
- **Raw act_runner `config.yaml`**: under `container:` add
|
|
`options: "-v /mnt/<pool>/gitea-runner-harbor:/opt/harbor-docker:ro -e DOCKER_CONFIG=/opt/harbor-docker"`,
|
|
then restart.
|
|
4. Jobs now run `docker pull/push harbor-v1.apps.lego-cloud.eu/...` with no secret reference.
|
|
|
|
**Trade-off.** Zero per-org setup and it covers all orgs, but the Harbor secret is duplicated
|
|
onto the NAS and rotating it means rewriting that file. It also does **not** cover `helm push`
|
|
(Helm has its own store) — for charts you still `helm registry login` in the job, or add a
|
|
second baked Helm credential. Prefer the org-secret model (§2) when you want one source of
|
|
truth and central rotation.
|
|
|
|
---
|
|
|
|
## 5. Troubleshooting
|
|
|
|
| Symptom | Cause | Fix |
|
|
|---|---|---|
|
|
| `docker login` → `unauthorized: unauthorized to access repository` | wrong robot secret, or robot lacks push on the project | re-read `.harbor/…`, confirm the org secret matches; confirm the robot has push on `HARBOR_PROJECT` |
|
|
| `helm push` → `unauthorized` right after a good `docker login` | missing `helm registry login` | add the helm login step |
|
|
| `docker: not found` in the job | runner label maps to an image without the docker CLI | pick a label/image that has docker (and helm) |
|
|
| `denied: requested access to the resource is denied` on push | pushing to a project the robot can't write, or a typo in `HARBOR_PROJECT` | set `HARBOR_PROJECT` to the robot's project |
|
|
| login works locally, fails in CI | job can't reach Harbor (edge/DNS) or cert not trusted | confirm the runner resolves and trusts `harbor-v1.apps.lego-cloud.eu` |
|