diff --git a/.gitea/workflows/deploy-stack.yml b/.gitea/workflows/deploy-stack.yml index cc3668e..81176bf 100644 --- a/.gitea/workflows/deploy-stack.yml +++ b/.gitea/workflows/deploy-stack.yml @@ -77,9 +77,13 @@ on: jobs: deploy: - runs-on: - - self-hosted - - ${{ inputs.host }} + # Single-label `runs-on` (not the [self-hosted, ] array form). In + # Gitea/act_runner, the array is matched loosely — a runner advertising + # only `self-hosted` will pick up jobs intended for a specific host. Using + # the bare host label routes deterministically to the correct runner. + # Every runner advertises both `self-hosted` and its host short name (see + # Charlie/runners envs/.env GITEA_RUNNER_LABELS). + runs-on: ${{ inputs.host }} steps: - name: Resolve stack path and ref id: resolve diff --git a/docs/automation.md b/docs/automation.md index 4286ea3..978bbbb 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -56,6 +56,8 @@ credentials. ## Caller patterns +> The `uses:` URL has to be the **absolute** form with a PAT prefix. The relative form (`Charlie/project-charlie/...`) makes act default to github.com; the unauthenticated absolute form fails because Gitea has `REQUIRE_SIGNIN_VIEW=true`. See [Gitea quirks](#gitea-quirks-leave-them-here-for-the-next-time-this-bites) below for the full story. + ### Single-host stack, with secrets ```yaml @@ -68,23 +70,27 @@ on: jobs: deploy: - uses: Charlie/project-charlie/.gitea/workflows/deploy-stack.yml@main + uses: https://sys-gitea-runner:${{ secrets.RUNNER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/deploy-stack.yml@main with: host: morgott decrypt: | secrets/env.sops.yaml -> .env + secrets: inherit ``` +`RUNNER_REPO_PAT` is set as a `Charlie` org-level secret (Settings -> Actions -> Secrets); every stack repo inherits it via `secrets: inherit`. PAT belongs to the LDAP-backed `sys-gitea-runner` service account with read on the `Charlie` org. + ### Single-host stack, no secrets -Just drop the `decrypt:` input. +Just drop the `decrypt:` input. `secrets: inherit` is still required so the called workflow can clone its sibling actions. ```yaml jobs: deploy: - uses: Charlie/project-charlie/.gitea/workflows/deploy-stack.yml@main + uses: https://sys-gitea-runner:${{ secrets.RUNNER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/deploy-stack.yml@main with: host: morgott + secrets: inherit ``` ### Multiple secrets / non-`.env` formats @@ -110,11 +116,12 @@ jobs: strategy: matrix: host: [morgott, melina, mohg, miquella] - uses: Charlie/project-charlie/.gitea/workflows/deploy-stack.yml@main + uses: https://sys-gitea-runner:${{ secrets.RUNNER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie/project-charlie/.gitea/workflows/deploy-stack.yml@main with: host: ${{ matrix.host }} decrypt: | secrets/${{ matrix.host }}.env.sops.yaml -> envs/${{ matrix.host }}.env + secrets: inherit ``` `deploy-stack.yml` then resolves compose files per @@ -140,13 +147,15 @@ with: | Input | Required | Default | Description | |---|---|---|---| -| `host` | yes | — | Self-hosted runner label. The runner with `[self-hosted, ]` picks up the job. | +| `host` | yes | — | Runner label. Bare label, **not** a `[self-hosted, ]` array — see [Gitea quirks](#gitea-quirks-leave-them-here-for-the-next-time-this-bites). | | `stack-path` | no | `/mnt/nas/stacks/` | Absolute working-tree path on the host. Must already be a `git` repo. | | `decrypt` | no | `""` | Multi-line `src -> dst` SOPS decrypt list. Skipped if empty. | | `compose-args` | no | `""` | Extra args appended to `docker compose ... up -d`. Use sparingly. | | `pull` | no | `true` | Run `docker compose ... pull` before `up -d`. | | `ref` | no | The branch the workflow ran on | Git ref to check out into the working tree. | +Requires the `RUNNER_REPO_PAT` secret to be in scope (set at org level; callers pass via `secrets: inherit`). + ## Inputs reference — `sops-decrypt` | Input | Required | Default | Description | @@ -217,6 +226,62 @@ sudo rm -f .env Same as above, minus the decrypt and the `rm -f .env` lines. +## Gitea quirks (leave them here for the next time this bites) + +Hard-won lessons from getting the first end-to-end pipeline working. Worth re-reading before debugging anything in this area. + +### Cross-repo `uses:` needs absolute URL with PAT + +The relative form `Charlie/project-charlie/...@main` makes `act` default to GitHub. The absolute form without auth fails because Gitea has `REQUIRE_SIGNIN_VIEW=true` instance-wide. So every `uses:` referencing a sibling repo has to be: + +``` +uses: https://sys-gitea-runner:${{ secrets.RUNNER_REPO_PAT }}@gitea.alexandru.macocian.me/Charlie//...@ +``` + +This applies **transitively** \u2014 inside `deploy-stack.yml`, the `uses:` for the `sops-decrypt` composite action also has to be absolute-URL form, otherwise act tries github.com from inside the workflow. + +The PAT belongs to the LDAP-backed `sys-gitea-runner` service account (LDAP -> synced into Gitea via Authentik OAuth -> placed in a `runners` team in the `Charlie` org with read on all repos). PAT itself is set as a `Charlie` org-level secret so every stack repo inherits it. Callers must include `secrets: inherit` so the called workflow sees the secret. + +References: +- https://github.com/go-gitea/gitea/issues/25929 \u2014 the canonical issue +- https://github.com/go-gitea/gitea/pull/36173 \u2014 the fix that landed in Gitea 1.27 (we pre-date it; the PAT-in-URL workaround is needed) + +### `runs-on` array form matches loosely + +A workflow with `runs-on: [self-hosted, morgott]` will sometimes get picked up by melina's runner (which advertises `self-hosted, melina`). The `[label_a, label_b]` array is supposed to mean "AND" per GitHub semantics, but Gitea's matching is loose in our version. + +Use the bare-string form to route deterministically: + +```yaml +runs-on: ${{ inputs.host }} +``` + +Every runner advertises both `self-hosted` and its host short name (see `Charlie/runners/envs/.env` `GITEA_RUNNER_LABELS=self-hosted,`). The bare host label is unique to one runner, so routing is unambiguous. + +### `act` aggressively caches reusable workflows / composite actions + +After pushing a fix to `Charlie/project-charlie`, the runner still uses the old version because act caches at `/root/.cache/act/Charlie-project-charlie@main`. The cache key is the ref string, not the commit. To force a refresh after pushing a fix: + +```bash +sudo docker exec runner rm -rf /root/.cache/act +``` + +Long-term fix: stop using `@main` for cross-repo refs. Pin to specific tags (`@v1`, `@`). New ref string -> new cache entry -> automatic refresh. We've left this for later (until the contract stabilises). + +### `safe.directory` for bind-mounted working trees + +The runner runs as root inside the container, but `/mnt/nas/stacks//` is owned by your user account on the host. Git refuses to operate on it ("dubious ownership"). The runner image's [`charlie-entrypoint.sh`](https://gitea.alexandru.macocian.me/amacocian/gitea-runner/src/branch/main/charlie-entrypoint.sh) sets: + +``` +git config --global --add safe.directory '/mnt/nas/stacks/*' +``` + +at container start. If you ever rebuild the runner image and forget this, `Sync working tree` will fail the same way every stack hit during the pilot. + +### Workflow logs collapse + +The Gitea Actions UI shows everything happening in `Set up job` rather than separate cards per step. Cosmetic; doesn't reflect the actual step structure. The full chronological log inside `Set up job` is what to scroll through. Suspected to be a Gitea / act_runner UI bug; not impacting correctness. + ## Adding new reusable parts If you find yourself copying logic across stack workflows, pull it in here.