Files
josh-sync/docs/adr/012-explicit-monorepo-url.md
sync-bot b35703d271 Import josh-sync from subrepo/main
Sync-Origin: import/josh-sync/20260528-041502
2026-05-28 04:15:02 +01:00

3.9 KiB

ADR-012: Explicit monorepo_url (drop first-target-host inference)

Status: Accepted Date: 2026-04

Context

josh-sync v1.x had no first-class field for the monorepo's git location. To call the gitea PR API and to clone the monorepo for initial_import / force-push, the script derived the gitea host from the first target's subrepo_url:

# lib/config.sh (v1.x)
gitea_host=$(echo "$JOSH_SYNC_TARGETS" | jq -r '.[0].gitea_host')
export MONOREPO_API="${MONOREPO_API:-https://${gitea_host}/api/v1/repos/${MONOREPO_PATH}}"

This worked silently for every existing config — studio, kinanah/stores, itkan/sdlc — because in each, target 0's host happens to match the monorepo's host. The portal monorepo broke the assumption: monorepo on code.atome.ac:atome-portal/portal, single target bff on code.itkan.io:sdlc/odoo_bff_core. The derivation produced https://code.itkan.io/atome-portal/portal.git (host borrowed from the wrong target), giving a 404 on josh-sync import.

Two issues stack:

  1. Implicit coupling: target ordering becomes load-bearing in a non-obvious way. The MONOREPO_API env var was the only escape hatch, hidden in shell init.
  2. Asymmetry: subrepo_url is declared explicitly per target with full SSH/HTTPS support; the monorepo had only a monorepo_path and was always cloned via HTTPS+token. Local users with SSH keys for the monorepo still had to provide a token.

Alternatives considered

  1. Keep deriving, document the env override loudly. Cheapest. Leaves the booby trap in place — still fails closed for any future cross-host config.
  2. Add josh.gitea_host only. Fixes the host issue without enabling SSH cloning of the monorepo. Half a fix, and asymmetric with how subrepos are declared.
  3. Add josh.monorepo_url (full URL), mirror subrepo_url exactly. Removes the inference; enables SSH clone; symmetric with subrepos. Breaking — every existing config needs to add the field.

Decision

Add a required josh.monorepo_url field plus an optional josh.monorepo_auth (default "https"). Mirror the subrepo_url parsing and subrepo_auth_url() helper exactly:

  • parse_config extracts MONOREPO_GITEA_HOST and MONOREPO_REPO_PATH from monorepo_url using the same jq regex as subrepo_url.
  • MONOREPO_API derives from MONOREPO_GITEA_HOST instead of .[0].gitea_host. The env-var override stays as a real escape hatch (custom API endpoints).
  • mono_auth_url() in lib/auth.sh mirrors subrepo_auth_url(): SSH returns the bare URL, HTTPS injects ${BOT_USER}:${GITEA_TOKEN}@ (auto-converting git@host:org/repo.git to https://host/org/repo.git first).
  • initial_import() and the force-push path in lib/sync.sh call mono_auth_url() instead of constructing the URL inline from MONOREPO_API.

The change is shipped as v2.0.0 with schema_version: 2 required. v1 configs are rejected with a migration pointer; configs missing monorepo_url are rejected with the field name and an example.

Consequences

Positive:

  • No more first-target-ordering coupling. Add or reorder targets freely.
  • Multi-host setups (monorepo on host A, subrepo on host B) are first-class.
  • SSH clone of the monorepo works without a token (the user's ssh-agent / ~/.ssh/config resolves the key). PR creation still needs GITEA_TOKEN.
  • Symmetric config: monorepo and subrepos both declare a URL + auth method.

Negative:

  • Breaking change. Every existing config must add schema_version: 2 and josh.monorepo_url. Mitigated by the validation dies pointing at the field name and changelog.
  • Two related-but-distinct fields (monorepo_path for the josh-proxy filter; monorepo_url for the gitea clone) — kept separate because josh-proxy's path may not always equal the gitea path in custom setups.

Risks:

  • Users may set monorepo_url to a host that doesn't match their proxy_url's upstream. Detection is out of scope for this ADR; a follow-up josh-sync preflight check could compare hosts and warn.