feat(miroirs) — un dépôt personnel n'est pas une organisation
Le rôle gitea_repo ne savait viser qu'un propriétaire : l'organisation, des deux côtés à la fois. Les dépôts qui vivent sous le compte personnel `arcodange` ne pouvaient donc pas sortir du homelab — ni être balayés par gitea_sync, qui n'interroge que /orgs/<org>/repos. Trois séparations, toutes rétrocompatibles (les défauts reconduisent le comportement org-vers-org des dix dépôts déjà en miroir) : - le propriétaire côté Gitea (`gitea_repo_owner`) n'est plus le même objet que celui d'en face (`github_owner`, `gitlab_owner`) ; - un compte personnel n'est pas une organisation : GitHub ne crée pas le dépôt au même endroit, d'où `github_owner_is_org` qui route vers POST /user/repos ; - GitLab devient facultatif (`gitea_mirror_gitlab`). Il ne l'était pas : sa création attendait un 201 sans ignore_errors, si bien qu'un échec GitLab avortait l'itération — y compris la moitié GitHub, qui n'y était pour rien. Deux défauts corrigés au passage, tous deux silencieux : - les trois listages de gitea_sync ne paginaient pas (30 chez GitHub, 20 chez GitLab). Sous la taille d'une page tout va bien ; au-delà, la différence entre forges désigne de FAUX dépôts manquants et le rôle les « répare » ; - la migration entrante posait `repo_owner: github_organization` pour désigner le propriétaire DANS Gitea. Et un piège découvert en exécutant : un dépôt GitHub créé vide adopte comme branche par défaut la PREMIÈRE branche que le miroir lui pousse — `kadans` a atterri sur `arcodange/adr-ddd-front`. Le rôle réaligne désormais sur la branche par défaut de Gitea ; le miroir étant asynchrone, l'alignement échoue au run qui crée le dépôt et réussit au suivant, d'où le failed_when permissif. Ce qui sort du homelab reste un CHOIX : playbooks/07_mirrors.yml parcourt une liste explicite et relue (`gitea_mirrored_repos`) plutôt que la différence automatique entre forges, qui recréerait un dépôt supprimé exprès. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
[vibe](../../../README.md) > [Guidebooks](../../README.md) > [Factory provisioning](../README.md) > [Ansible](README.md) > **07 · Mirrors**
|
||||
|
||||
# 07 · Mirrors — Gitea → GitHub / GitLab
|
||||
|
||||
> [!NOTE]
|
||||
> **Status:** ✅ active · **Last Updated:** 2026-07-27
|
||||
> **Upstream:** [Ansible sub-hub](README.md) · [Factory provisioning hub](../README.md)
|
||||
> **Downstream:** [Roles reference](roles.md) — `gitea_repo`, `gitea_sync`, `gitea_token`
|
||||
> **Related:** [Inventory & variables](inventory.md) · [03 · CI/CD](03-cicd.md)
|
||||
|
||||
Gitea is the **source of truth**; GitHub and GitLab hold a pushed copy. [`playbooks/07_mirrors.yml`](../../../../ansible/arcodange/factory/playbooks/07_mirrors.yml) walks the repos declared in `gitea_mirrored_repos` ([`inventory/group_vars/all/gitea.yml`](../../../../ansible/arcodange/factory/inventory/group_vars/all/gitea.yml)) and, for each, calls [`gitea_repo`](../../../../ansible/arcodange/factory/roles/gitea_repo): create the counterpart repo **private** if it is missing, then attach a push mirror refreshed every **8 h** *and* on **every commit**.
|
||||
|
||||
Nothing is pulled back. A mirror only ever pushes Gitea → forge, so a change made on GitHub is overwritten at the next sync.
|
||||
|
||||
```sh
|
||||
uv run ansible-playbook -i ansible/arcodange/factory/inventory \
|
||||
ansible/arcodange/factory/playbooks/07_mirrors.yml
|
||||
|
||||
# GitHub only — while the personal GitLab namespace is still unset:
|
||||
… -e gitea_mirror_gitlab=false
|
||||
```
|
||||
|
||||
The Gitea token is minted for the run by `gitea_token` and **revoked in `post_tasks`**. Everything is tagged `gitea_mirrors`.
|
||||
|
||||
---
|
||||
|
||||
## Two ways to pick repos, and when each fits
|
||||
|
||||
| | [`gitea_sync`](../../../../ansible/arcodange/factory/roles/gitea_sync) | `gitea_mirrored_repos` + `07_mirrors.yml` |
|
||||
| --- | --- | --- |
|
||||
| Selection | Automatic: diffs the three forges for **one owner**, reconciles whatever is missing somewhere | Explicit list, reviewed in the inventory |
|
||||
| Fits | The organisation, where every repo is meant to exist everywhere | The personal account, where each repo leaving the homelab is a deliberate call |
|
||||
| Blind spot | `repos_incomplete = all − common` says nothing about *why* a repo is missing — a repo deleted on purpose from GitHub is recreated | Anything absent from the list is silently never mirrored |
|
||||
|
||||
Both drive the same `gitea_repo` role, so the mirror they produce is identical.
|
||||
|
||||
---
|
||||
|
||||
## Owner mapping
|
||||
|
||||
A Gitea repo owned by the **user** `arcodange` does not belong on the GitHub **organisation** — and GitHub does not even create it the same way (`POST /user/repos` instead of `POST /orgs/<org>/repos`). Hence three knobs, all defaulting to the previous org-only behaviour:
|
||||
|
||||
| Var | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `gitea_repo_owner` | `gitea_organization` | Owner **on Gitea** |
|
||||
| `github_owner` / `gitlab_owner` | `github_organization` / `gitlab_root_group` | Owner **on the far forge** |
|
||||
| `github_owner_is_org` | `true` | `false` routes creation to `POST /user/repos` |
|
||||
| `gitea_mirror_github` / `gitea_mirror_gitlab` | `true` | Turn a forge off entirely |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> GitLab was **not optional** before. Its create call expected `201` with no `ignore_errors`, so a GitLab failure aborted the iteration — including the GitHub half that had nothing to do with it. `gitea_mirror_gitlab: false` is the way out.
|
||||
|
||||
> [!WARNING]
|
||||
> A GitHub repo created **empty** adopts as its default branch the *first branch the mirror pushes*, which is routinely a work branch rather than `main`. The role realigns it against Gitea's default branch, but the mirror is asynchronous: the alignment fails on the run that creates the repo and succeeds on the next one. Run the playbook twice, or fix the branch by hand.
|
||||
|
||||
---
|
||||
|
||||
## Current state (2026-07-27)
|
||||
|
||||
| Owner | Repos mirrored | Target |
|
||||
| --- | --- | --- |
|
||||
| `arcodange-org` | 10 (`factory`, `tools`, `erp`, `cms`, `webapp`, `url-shortener`, `docker.tofu`, `docker-build-workflow`, `super-linter-workflow`, `vault-action`) | `github.com/arcodange-org/*` + GitLab |
|
||||
| `arcodange` (user) | 5 (`kadans`, `kadans-api`, `kadans-dossier`, `kadans-jobs`, `video_analysis`) — all **private** | `github.com/arcodange/*` |
|
||||
|
||||
Not mirrored, deliberately left out of `gitea_mirrored_repos`: `documents`, `studio`, `prospection`, `kissmetrics_contract_proposal` (org) and `.profile`, `DanceVideos`, `SecondBrain`, `dance-lessons-coach`, `frame-sdk`, `telegram-gateway` (user).
|
||||
|
||||
> [!NOTE]
|
||||
> The personal repos have **no GitLab mirror yet**: `gitlab_personal_namespace_id` is still `~`. Fill it with the numeric namespace ID of the `arcodange` account on gitlab.com, otherwise creation would land the project in the `arcodange-org` group.
|
||||
|
||||
---
|
||||
|
||||
## Reading the truth from Gitea
|
||||
|
||||
The push mirrors live in Gitea, not in this repo — the playbook is idempotent precisely because it asks first:
|
||||
|
||||
```sh
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
https://gitea.arcodange.lab/api/v1/repos/arcodange/kadans/push_mirrors
|
||||
```
|
||||
|
||||
`last_update` tells you when the mirror last pushed. A repo with no entry has no mirror, whatever this page claims.
|
||||
@@ -5,7 +5,7 @@
|
||||
> [!NOTE]
|
||||
> **Status:** ✅ active · **Last Updated:** 2026-06-23
|
||||
> **Upstream:** [Factory provisioning hub](../README.md) · [Lab ecosystem · 01 factory](../../lab-ecosystem/01-factory.md)
|
||||
> **Downstream:** [01 · System](01-system.md) · [02 · Setup](02-setup.md) · [03 · CI/CD](03-cicd.md) · [04 · Tools](04-tools.md) · [05 · Backup](05-backup.md) · [06 · Recover](06-recover.md) · [Inventory & variables](inventory.md) · [Roles reference](roles.md)
|
||||
> **Downstream:** [01 · System](01-system.md) · [02 · Setup](02-setup.md) · [03 · CI/CD](03-cicd.md) · [04 · Tools](04-tools.md) · [05 · Backup](05-backup.md) · [06 · Recover](06-recover.md) · [07 · Mirrors](07-mirrors.md) · [Inventory & variables](inventory.md) · [Roles reference](roles.md)
|
||||
> **Related:** [Secrets & Vault](../../lab-ecosystem/secrets-and-vault.md) · [Storage & recovery](../../lab-ecosystem/storage-and-recovery.md) · [Naming conventions](../../lab-ecosystem/naming-conventions.md) · [ADR-0001 safe prod-like environment](../../../ADR/0001-safe-prod-like-environment.md)
|
||||
|
||||
Ansible is the **imperative half** of the factory: it takes three bare Raspberry Pis (`pi1`, `pi2`, `pi3`) and turns them into a running K3s cluster with Docker, Longhorn storage, Gitea CI runners, CrowdSec, and Vault. OpenTofu (the declarative half) then provisions everything that lives *outside* the cluster — see the [OpenTofu sub-hub](../opentofu/README.md).
|
||||
@@ -22,7 +22,7 @@ Everything ships as a single Ansible **collection** committed under [`ansible/ar
|
||||
| `requirements.yml` | [`ansible/requirements.yml`](../../../../ansible/requirements.yml) | External dependencies pulled at install time (see table below). |
|
||||
| `ansible.cfg` | [`ansible/arcodange/factory/ansible.cfg`](../../../../ansible/arcodange/factory/ansible.cfg) | `collections_path = ~/.ansible/collections` and `scp_if_ssh = True` for the SSH connection plugin. |
|
||||
| `inventory/` | [`ansible/arcodange/factory/inventory/`](../../../../ansible/arcodange/factory/inventory) | `hosts.yml` + `group_vars/`. Detailed in [Inventory & variables](inventory.md). |
|
||||
| `playbooks/` | [`ansible/arcodange/factory/playbooks/`](../../../../ansible/arcodange/factory/playbooks) | The numbered pipeline `01..05` plus the `recover/` branch. |
|
||||
| `playbooks/` | [`ansible/arcodange/factory/playbooks/`](../../../../ansible/arcodange/factory/playbooks) | The numbered pipeline `01..05`, the `recover/` branch, and the on-demand [`07_mirrors.yml`](../../../../ansible/arcodange/factory/playbooks/07_mirrors.yml). |
|
||||
| `roles/` | [`ansible/arcodange/factory/roles/`](../../../../ansible/arcodange/factory/roles) | Seven reusable roles. Detailed in [Roles reference](roles.md). |
|
||||
|
||||
### External dependencies (`requirements.yml`)
|
||||
|
||||
@@ -126,10 +126,10 @@ Smaller roles, mostly Gitea/forge plumbing and one-shot helpers. Shared roles li
|
||||
|
||||
| Role | Purpose | Key vars / notes | Secrets |
|
||||
| --- | --- | --- | --- |
|
||||
| [`gitea_repo`](../../../../ansible/arcodange/factory/roles/gitea_repo) | Ensure a repo exists across Gitea + GitHub + GitLab and add **8h push mirrors** (`sync_on_commit: true`) to GitHub/GitLab. | Creates missing repos on each forge; mirror URLs + namespace IDs in [`vars/main.yml`](../../../../ansible/arcodange/factory/roles/gitea_repo/vars/main.yml). | `github_api_token`, `gitlab_api_token` (from `gitea_vault`). |
|
||||
| [`gitea_repo`](../../../../ansible/arcodange/factory/roles/gitea_repo) | Ensure a repo exists across Gitea + GitHub + GitLab and add **8h push mirrors** (`sync_on_commit: true`) to GitHub/GitLab. | Creates missing repos on each forge (**private** by default). Owner is per-forge — `gitea_repo_owner` / `github_owner` / `gitlab_owner`, with `github_owner_is_org: false` for a personal account. Each forge can be switched off (`gitea_mirror_github` / `gitea_mirror_gitlab`). See [07 · Mirrors](07-mirrors.md). | `github_api_token`, `gitlab_api_token` (from `gitea_vault`). |
|
||||
| [`gitea_token`](../../../../ansible/arcodange/factory/roles/gitea_token) | Generate / replace / delete a Gitea access token via `docker exec … gitea admin user generate-access-token`. | Stores the raw token in the fact named by `gitea_token_fact_name`; `gitea_token_replace` / `gitea_token_delete` toggles; scopes default to `write:admin,organization,package,repository,user`. | The minted token itself (a fact, not persisted). |
|
||||
| [`gitea_secret`](../../../../ansible/arcodange/factory/roles/gitea_secret) | `PUT` a Gitea **Actions secret** at user or org scope. | `gitea_secret_name` / `_value`; `gitea_owner_type` (`user`\|`org`) selects the API path. | `gitea_api_token` (Authorization). |
|
||||
| [`gitea_sync`](../../../../ansible/arcodange/factory/roles/gitea_sync) | List repos on all **three forges**, diff them, and call `gitea_repo` for the repos missing somewhere. | Computes `repos_incomplete = all − common`; loops `gitea_repo` over the gaps. | GitHub/GitLab/Gitea API tokens. |
|
||||
| [`gitea_sync`](../../../../ansible/arcodange/factory/roles/gitea_sync) | List repos on all **three forges** for **one owner**, diff them, and call `gitea_repo` for the repos missing somewhere. | Computes `repos_incomplete = all − common`; loops `gitea_repo` over the gaps. `gitea_sync_owner` + `gitea_sync_owner_is_org` pick the owner (a user is not served at the same API paths). **Not currently invoked by any playbook** — the explicit list in [07 · Mirrors](07-mirrors.md) is what runs. | GitHub/GitLab/Gitea API tokens. |
|
||||
| [`traefik_certs`](../../../../ansible/arcodange/factory/roles/traefik_certs) | Extract the live **`*.arcodange.lab`** cert from Traefik's `acme.json`. | `kubectl exec` into Traefik → `jq` the LetsEncrypt wildcard cert → `traefik_cert_pem` fact; no-op if already set. | — (reads in-cluster acme.json). |
|
||||
| [`playwright`](../../../../ansible/arcodange/factory/roles/playwright) | Run a Playwright browser-automation script in Docker. | Builds `playwright:<version>` (default `1.47.0`) from `files/`, runs the script with `playwright_env` injected as `-e`; default script `loginGitea.js`. Used by `hashicorp_vault` for the OIDC app setup. | Script-specific env (e.g. Gitea admin creds). |
|
||||
| [`deploy_gitea`](../../../../ansible/arcodange/factory/playbooks/setup/roles/deploy_gitea) | Deploy Gitea: template [`app.ini.j2`](../../../../ansible/arcodange/factory/playbooks/setup/roles/deploy_gitea/tasks/main.yml), `docker compose up`, then **health-check `:3000`** until ready. | Compose source is `/home/pi/arcodange/docker_composes/gitea`; admin user `arcodange`. | (consumes the vaulted Gitea compose env). |
|
||||
|
||||
Reference in New Issue
Block a user