From b28662d3b4541078bde6745a6a10e16aba90c97c Mon Sep 17 00:00:00 2001 From: SBPro Date: Wed, 29 Apr 2026 06:31:21 +0300 Subject: [PATCH] =?UTF-8?q?Release=202.0.0=20=E2=80=94=20explicit=20monore?= =?UTF-8?q?po=5Furl,=20drop=20first-target-host=20inference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking change. The monorepo's gitea host was previously inferred from the first target's subrepo_url, silently coupling the two and breaking on any multi-host setup (monorepo on host A, target on host B). v2.0.0 replaces the inference with a required josh.monorepo_url field that mirrors subrepo_url — same parsing, same SSH/HTTPS auth options, same shape. - schema_version: 2 is now required; v1 configs are rejected with a clear migration error - josh.monorepo_url is required; josh.monorepo_auth is optional (default https) - mono_auth_url() in lib/auth.sh mirrors subrepo_auth_url() - MONOREPO_API derives from monorepo_url's host instead of .[0].gitea_host; env-var override preserved as escape hatch - initial_import() and force-push call mono_auth_url() instead of building URLs inline - ADR-012 documents the rationale; CHANGELOG includes migration snippet Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 37 +++++++ README.md | 4 + VERSION | 2 +- docs/adr/012-explicit-monorepo-url.md | 53 +++++++++ docs/adr/README.md | 1 + docs/config-reference.md | 17 ++- docs/guide.md | 6 +- examples/josh-sync.yml | 13 ++- lib/auth.sh | 24 +++-- lib/config.sh | 36 +++++-- lib/sync.sh | 7 +- schema/config-schema.json | 20 +++- tests/fixtures/minimal.yml | 2 + tests/fixtures/multi-target.yml | 2 + tests/unit/auth.bats | 36 +++++++ tests/unit/config.bats | 148 ++++++++++++++++++++++++++ 16 files changed, 377 insertions(+), 31 deletions(-) create mode 100644 docs/adr/012-explicit-monorepo-url.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 80ace72..711a774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## 2.0.0 + +### Breaking Changes + +- **`schema_version: 2` is now required.** v1 configs are rejected with a migration pointer. Add `schema_version: 2` at the top of `.josh-sync.yml`. +- **`josh.monorepo_url` is now required.** Set it to your monorepo's git clone URL — `git@host:org/repo.git` (SSH), `ssh://...`, or `https://host/org/repo.git`. Removes the silent inference of the gitea host from the first target's `subrepo_url`, which silently misbehaved when the monorepo and a target lived on different gitea instances. See [ADR-012](docs/adr/012-explicit-monorepo-url.md). +- **`MONOREPO_API` derivation changed.** The default is now `https://{monorepo_url's host}/api/v1/repos/{monorepo_path}`. The env-var override is preserved as an escape hatch for custom API endpoints. + +#### Migration + +```yaml +# Before (v1.x) +josh: + proxy_url: "https://josh.example.com" + monorepo_path: "org/monorepo" + +# After (v2.0.0) +schema_version: 2 +josh: + proxy_url: "https://josh.example.com" + monorepo_path: "org/monorepo" + monorepo_url: "git@gitea.example.com:org/monorepo.git" # required + monorepo_auth: "ssh" # optional, default "https" +``` + +If you previously set the `MONOREPO_API` env var to work around the first-target-host bug, you can remove it — the new derivation gets it right from `monorepo_url`. + +### Features + +- **Multi-host configs supported.** The monorepo and any subrepo may live on different gitea instances. Target ordering no longer affects how the monorepo's host is resolved. +- **SSH clone of the monorepo via `monorepo_auth: ssh`.** Uses the user's ssh-agent / `~/.ssh/config`; no token required for the clone itself. PR creation still uses `GITEA_TOKEN`. +- **Symmetric config shape.** Monorepo and subrepos now both declare a URL plus an auth method. + +### Fixes + +- First-target ordering no longer load-bearing on `MONOREPO_API` derivation (was `gitea_host=.[0].gitea_host` in v1.x). + ## 1.5.0 ### Breaking Changes diff --git a/README.md b/README.md index bd5c0dc..3eb74d9 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,13 @@ Bidirectional monorepo ↔ subrepo sync via [josh-proxy](https://josh-project.gi Create `.josh-sync.yml` in your monorepo root: ```yaml +schema_version: 2 + josh: proxy_url: "https://josh.example.com" monorepo_path: "org/monorepo" + monorepo_url: "git@gitea.example.com:org/monorepo.git" + monorepo_auth: "ssh" # optional, default "https" targets: - name: "billing" diff --git a/VERSION b/VERSION index bc80560..227cea2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.0 +2.0.0 diff --git a/docs/adr/012-explicit-monorepo-url.md b/docs/adr/012-explicit-monorepo-url.md new file mode 100644 index 0000000..46a40df --- /dev/null +++ b/docs/adr/012-explicit-monorepo-url.md @@ -0,0 +1,53 @@ +# 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`**: + +```bash +# 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 `die`s 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. diff --git a/docs/adr/README.md b/docs/adr/README.md index eefee96..36add1d 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -17,3 +17,4 @@ This directory contains Architecture Decision Records (ADRs) for josh-sync. Each | [009](009-tree-comparison-guard.md) | Tree comparison as sync skip guard | Accepted | | [010](010-onboard-checkpoint-resume.md) | Onboard workflow with checkpoint/resume | Accepted | | [011](011-linearize-fallback-reverse.md) | Linearize fallback for reverse sync | Accepted | +| [012](012-explicit-monorepo-url.md) | Explicit `monorepo_url` (drop first-target-host inference) | Accepted | diff --git a/docs/config-reference.md b/docs/config-reference.md index d13745f..c953083 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -5,17 +5,26 @@ Full reference for `.josh-sync.yml` fields and environment variables. ## `.josh-sync.yml` Structure ```yaml -josh: # josh-proxy settings (required) -targets: # sync targets (required, at least 1) -bot: # bot identity for sync commits (required) +schema_version: 2 # required since v2.0.0 +josh: # josh-proxy + monorepo settings (required) +targets: # sync targets (required, at least 1) +bot: # bot identity for sync commits (required) ``` +## `schema_version` + +| Type | Required | Description | +|------|----------|-------------| +| integer | Yes | Must be `2` for josh-sync v2.x configs. v1 configs are rejected; see [Changelog v2.0.0](../CHANGELOG.md#200) for migration. | + ## `josh` Section | Field | Type | Required | Description | |-------|------|----------|-------------| | `proxy_url` | string | Yes | Josh-proxy URL, no trailing slash. Must start with `http://` or `https://`. | | `monorepo_path` | string | Yes | Repository path as josh-proxy sees it (e.g., `org/monorepo`). | +| `monorepo_url` | string | Yes | Monorepo git clone URL — `git@host:org/repo.git` (SSH), `ssh://...`, or `https://host/org/repo.git`. The gitea host is derived from this URL and used for the PR API and direct clones, decoupled from any target's host. | +| `monorepo_auth` | string | No (default `"https"`) | Clone auth method: `"https"` (injects `${SYNC_BOT_USER}:${SYNC_BOT_TOKEN}` into the URL) or `"ssh"` (uses the user's ssh-agent / `~/.ssh/config`). PR creation always uses `GITEA_TOKEN` regardless. | ## `targets[]` Section @@ -73,7 +82,7 @@ targets: | `JOSH_SYNC_TARGET` | Restrict sync to a single target | All targets | | `JOSH_SYNC_STATE_BRANCH` | Name of the orphan branch for state storage | `josh-sync-state` | | `JOSH_SYNC_DEBUG` | Enable verbose logging (`1` to enable) | `0` | -| `MONOREPO_API` | Override monorepo API URL | Auto-derived from first target's host | +| `MONOREPO_API` | Override the monorepo gitea PR API URL. Escape hatch for custom API endpoints; rarely needed. | `https://{monorepo_url's host}/api/v1/repos/{monorepo_path}` | ## JSON Schema diff --git a/docs/guide.md b/docs/guide.md index fe883c9..a54be32 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -78,9 +78,13 @@ git ls-remote https://josh.example.com/org/monorepo.git HEAD Create `.josh-sync.yml` at the monorepo root. Each target maps a monorepo subfolder to an external subrepo: ```yaml +schema_version: 2 + josh: proxy_url: "https://josh.example.com" # josh-proxy URL (no trailing slash) - monorepo_path: "org/monorepo" # repo path as josh sees it + monorepo_path: "org/monorepo" # repo path as josh-proxy sees it + monorepo_url: "git@gitea.example.com:org/monorepo.git" # monorepo git clone URL + monorepo_auth: "ssh" # optional, default "https" targets: - name: "billing" # unique identifier diff --git a/examples/josh-sync.yml b/examples/josh-sync.yml index 09bb455..2cb4b07 100644 --- a/examples/josh-sync.yml +++ b/examples/josh-sync.yml @@ -1,11 +1,22 @@ # .josh-sync.yml — Multi-target configuration example # Place this at the root of your monorepo. +# Required since v2.0.0. Bump alongside the josh-sync major version when +# breaking config changes ship. +schema_version: 2 + josh: # Your josh-proxy instance URL (no trailing slash) proxy_url: "https://josh.example.com" - # Repo path as josh sees it (org/repo on your Gitea/GitHub) + # Repo path as josh-proxy sees it (org/repo) monorepo_path: "org/monorepo" + # Monorepo git clone URL — git@host:org/repo.git (SSH) or https://host/org/repo.git. + # The gitea host is derived from this URL (used for the PR API and direct clones). + # The monorepo and any subrepo may live on different gitea hosts. + monorepo_url: "git@gitea.example.com:org/monorepo.git" + # Optional. Default "https". Use "ssh" to clone the monorepo via SSH (no token + # needed for the clone — PR creation still requires GITEA_TOKEN). + monorepo_auth: "ssh" targets: - name: "billing" diff --git a/lib/auth.sh b/lib/auth.sh index fd432d4..bf4ce0a 100644 --- a/lib/auth.sh +++ b/lib/auth.sh @@ -2,7 +2,8 @@ # lib/auth.sh — Authenticated URLs, remote queries, and PR creation # # Requires: lib/core.sh and lib/config.sh sourced first -# Expects: JOSH_PROXY_URL, MONOREPO_PATH, BOT_USER, GITEA_TOKEN, JOSH_FILTER, +# Expects: JOSH_PROXY_URL, MONOREPO_PATH, MONOREPO_URL, MONOREPO_AUTH, +# BOT_USER, GITEA_TOKEN, JOSH_FILTER, # SUBREPO_URL, SUBREPO_AUTH, SUBREPO_TOKEN (set by parse_config + load_target) # ─── Josh-Proxy Auth URL ─────────────────────────────────────────── @@ -29,13 +30,24 @@ subrepo_auth_url() { } # ─── Monorepo Auth URL ───────────────────────────────────────────── -# Derives a push-capable HTTPS URL from MONOREPO_API. -# MONOREPO_API shape: https:///api/v1/repos/ +# HTTPS: injects user:token into MONOREPO_URL (auto-converts git@ form to https) +# SSH: returns bare MONOREPO_URL (auth via user's ssh-agent / ~/.ssh/config, +# or via GIT_SSH_COMMAND if the caller wired one up for the monorepo host) mono_auth_url() { - local api_host_path - api_host_path=$(echo "$MONOREPO_API" | sed 's|https://||; s|/api/v1/repos/|/|') - echo "https://${BOT_USER}:${GITEA_TOKEN}@${api_host_path}.git" + if [ "${MONOREPO_AUTH:-https}" = "ssh" ]; then + echo "$MONOREPO_URL" + else + local https_url="$MONOREPO_URL" + # Convert git@host:org/repo.git → https://host/org/repo.git so token injection works + if [[ "$https_url" == git@* ]]; then + https_url=$(echo "$https_url" | sed -E 's|^git@([^:]+):(.+)$|https://\1/\2|') + elif [[ "$https_url" == ssh://* ]]; then + https_url=$(echo "$https_url" | sed -E 's|^ssh://[^@]*@([^/]+)/(.+)$|https://\1/\2|') + fi + # shellcheck disable=SC2001 + echo "$https_url" | sed "s|https://|https://${BOT_USER}:${GITEA_TOKEN}@|" + fi } # ─── Remote Queries ───────────────────────────────────────────────── diff --git a/lib/config.sh b/lib/config.sh index bc12366..d8655ee 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -16,16 +16,21 @@ parse_config() { local config_json config_json=$(yq -o json "$config_file") || die "Failed to parse ${config_file}" - # Schema version check (optional field — absent means v1) + # Schema version check — required, must be 2 (v1 configs are no longer supported) local schema_version - schema_version=$(echo "$config_json" | jq -r '.schema_version // 1') - [ "$schema_version" = "1" ] || die "Unsupported schema_version ${schema_version} in ${config_file} (this version of josh-sync supports schema_version 1 — see CHANGELOG.md for migration instructions)" + schema_version=$(echo "$config_json" | jq -r '.schema_version // empty') + [ -n "$schema_version" ] || die "schema_version missing in ${config_file} (required since v2.0.0 — set 'schema_version: 2' at the top of the file; see CHANGELOG.md for migration instructions)" + [ "$schema_version" = "2" ] || die "Unsupported schema_version ${schema_version} in ${config_file} (this version of josh-sync requires schema_version 2 — see CHANGELOG.md for migration instructions)" # Export global values export JOSH_PROXY_URL JOSH_PROXY_URL=$(echo "$config_json" | jq -r '.josh.proxy_url') export MONOREPO_PATH MONOREPO_PATH=$(echo "$config_json" | jq -r '.josh.monorepo_path') + export MONOREPO_URL + MONOREPO_URL=$(echo "$config_json" | jq -r '.josh.monorepo_url') + export MONOREPO_AUTH + MONOREPO_AUTH=$(echo "$config_json" | jq -r '.josh.monorepo_auth // "https"') export BOT_NAME BOT_NAME=$(echo "$config_json" | jq -r '.bot.name') export BOT_EMAIL @@ -35,8 +40,27 @@ parse_config() { [ -n "$JOSH_PROXY_URL" ] && [ "$JOSH_PROXY_URL" != "null" ] || die "josh.proxy_url missing in config" [ -n "$MONOREPO_PATH" ] && [ "$MONOREPO_PATH" != "null" ] || die "josh.monorepo_path missing in config" + [ -n "$MONOREPO_URL" ] && [ "$MONOREPO_URL" != "null" ] || die "josh.monorepo_url is required (added in schema_version 2). Set it to your monorepo's git clone URL, e.g. 'git@code.example.io:org/repo.git'." + [ "$MONOREPO_AUTH" = "ssh" ] || [ "$MONOREPO_AUTH" = "https" ] || die "josh.monorepo_auth must be 'ssh' or 'https' (got '${MONOREPO_AUTH}')" [ -n "$BOT_TRAILER" ] && [ "$BOT_TRAILER" != "null" ] || die "bot.trailer missing in config" + # Derive monorepo gitea_host and repo_path from monorepo_url (mirrors target derivation below) + local monorepo_meta + monorepo_meta=$(jq -nr --arg url "$MONOREPO_URL" ' + $url as $u | + if ($u | test("^ssh://")) then + ($u | capture("ssh://[^@]*@(?[^/]+)/(?

.+?)(\\.git)?$") // {h: "", p: ""}) + elif ($u | test("^git@")) then + ($u | capture("git@(?[^:/]+)[:/](?

.+?)(\\.git)?$") // {h: "", p: ""}) + elif ($u | test("^https?://")) then + ($u | capture("https?://(?[^/]+)/(?

.+?)(\\.git)?$") // {h: "", p: ""}) + else + {h: "", p: ""} + end | "\(.h) \(.p)"') + export MONOREPO_GITEA_HOST="${monorepo_meta% *}" + export MONOREPO_REPO_PATH="${monorepo_meta#* }" + [ -n "$MONOREPO_GITEA_HOST" ] || die "Could not derive gitea host from josh.monorepo_url='${MONOREPO_URL}' (expected git@host:org/repo.git or https://host/org/repo.git)" + # Enrich targets with derived fields (gitea_host, subrepo_repo_path, auto josh_filter) export JOSH_SYNC_TARGETS JOSH_SYNC_TARGETS=$(echo "$config_json" | jq '[.targets[] | . + @@ -73,10 +97,8 @@ parse_config() { export GITEA_TOKEN="${GITEA_TOKEN:-${SYNC_BOT_TOKEN:-}}" export BOT_USER="${BOT_USER:-${SYNC_BOT_USER:-}}" - # Monorepo API URL (derived from first target's host, overridable via env) - local gitea_host - gitea_host=$(echo "$JOSH_SYNC_TARGETS" | jq -r '.[0].gitea_host') - export MONOREPO_API="${MONOREPO_API:-https://${gitea_host}/api/v1/repos/${MONOREPO_PATH}}" + # Monorepo API URL (derived from monorepo_url's host, overridable via env for custom API endpoints) + export MONOREPO_API="${MONOREPO_API:-https://${MONOREPO_GITEA_HOST}/api/v1/repos/${MONOREPO_PATH}}" log "INFO" "Config loaded: $(echo "$JOSH_SYNC_TARGETS" | jq 'length') target(s)" } diff --git a/lib/sync.sh b/lib/sync.sh index bd6ce33..045a106 100644 --- a/lib/sync.sh +++ b/lib/sync.sh @@ -475,12 +475,7 @@ initial_import() { log "INFO" "=== Initial import: subrepo/${subrepo_branch} → mono/${mono_branch}:${subfolder}/ ===" # 1. Clone monorepo (directly, not through josh — we need the real repo) - local mono_auth_url - local api_host_path - api_host_path=$(echo "$MONOREPO_API" | sed 's|https://||; s|/api/v1/repos/|/|') - mono_auth_url="https://${BOT_USER}:${GITEA_TOKEN}@${api_host_path}.git" - - git clone "$mono_auth_url" \ + git clone "$(mono_auth_url)" \ --branch "$mono_branch" --single-branch \ "${work_dir}/monorepo" || die "Failed to clone monorepo" diff --git a/schema/config-schema.json b/schema/config-schema.json index 4afeb72..eff6966 100644 --- a/schema/config-schema.json +++ b/schema/config-schema.json @@ -3,16 +3,16 @@ "title": "josh-sync configuration", "description": "Configuration for bidirectional monorepo ↔ subrepo sync via josh-proxy", "type": "object", - "required": ["josh", "targets", "bot"], + "required": ["schema_version", "josh", "targets", "bot"], "properties": { "schema_version": { "type": "integer", - "const": 1, - "description": "Schema version. Bump when config fields are removed or renamed (matches josh-sync major version)." + "const": 2, + "description": "Schema version. Required. Must be 2 for josh-sync v2.x configs." }, "josh": { "type": "object", - "required": ["proxy_url", "monorepo_path"], + "required": ["proxy_url", "monorepo_path", "monorepo_url"], "properties": { "proxy_url": { "type": "string", @@ -21,7 +21,17 @@ }, "monorepo_path": { "type": "string", - "description": "Repo path as josh sees it (org/repo)" + "description": "Repo path as josh-proxy sees it (org/repo)" + }, + "monorepo_url": { + "type": "string", + "description": "Monorepo git clone URL — git@host:org/repo.git (SSH) or https://host/org/repo.git. The gitea host is derived from this URL." + }, + "monorepo_auth": { + "type": "string", + "enum": ["https", "ssh"], + "default": "https", + "description": "Auth method for cloning the monorepo. SSH uses the user's ssh-agent / ~/.ssh/config; HTTPS injects ${SYNC_BOT_USER}:${SYNC_BOT_TOKEN} into the URL." } } }, diff --git a/tests/fixtures/minimal.yml b/tests/fixtures/minimal.yml index d767d4e..232c5d3 100644 --- a/tests/fixtures/minimal.yml +++ b/tests/fixtures/minimal.yml @@ -1,7 +1,9 @@ # Test fixture: minimal single-target config +schema_version: 2 josh: proxy_url: "https://josh.test.local" monorepo_path: "org/repo" + monorepo_url: "git@gitea.test.local:org/repo.git" targets: - name: "example" diff --git a/tests/fixtures/multi-target.yml b/tests/fixtures/multi-target.yml index d0ced19..8e89b4b 100644 --- a/tests/fixtures/multi-target.yml +++ b/tests/fixtures/multi-target.yml @@ -1,7 +1,9 @@ # Test fixture: multi-target config +schema_version: 2 josh: proxy_url: "https://josh.test.local" monorepo_path: "org/monorepo" + monorepo_url: "git@gitea.test.local:org/monorepo.git" targets: - name: "app-a" diff --git a/tests/unit/auth.bats b/tests/unit/auth.bats index e94b5cc..1517170 100644 --- a/tests/unit/auth.bats +++ b/tests/unit/auth.bats @@ -37,3 +37,39 @@ setup() { url=$(subrepo_auth_url) [ "$url" = "git@gitea.test.local:ext/app-a.git" ] } + +@test "mono_auth_url returns bare URL for SSH" { + export MONOREPO_AUTH="ssh" + export MONOREPO_URL="git@code.example.io:org/monorepo.git" + + local url + url=$(mono_auth_url) + [ "$url" = "git@code.example.io:org/monorepo.git" ] +} + +@test "mono_auth_url injects credentials for HTTPS git@-form monorepo_url" { + export MONOREPO_AUTH="https" + export MONOREPO_URL="git@code.example.io:org/monorepo.git" + + local url + url=$(mono_auth_url) + [ "$url" = "https://sync-bot:test-token-123@code.example.io/org/monorepo.git" ] +} + +@test "mono_auth_url injects credentials for HTTPS https-form monorepo_url" { + export MONOREPO_AUTH="https" + export MONOREPO_URL="https://code.example.io/org/monorepo.git" + + local url + url=$(mono_auth_url) + [ "$url" = "https://sync-bot:test-token-123@code.example.io/org/monorepo.git" ] +} + +@test "mono_auth_url defaults to HTTPS when MONOREPO_AUTH unset" { + unset MONOREPO_AUTH + export MONOREPO_URL="https://code.example.io/org/monorepo.git" + + local url + url=$(mono_auth_url) + [ "$url" = "https://sync-bot:test-token-123@code.example.io/org/monorepo.git" ] +} diff --git a/tests/unit/config.bats b/tests/unit/config.bats index 7c306d0..c995eba 100644 --- a/tests/unit/config.bats +++ b/tests/unit/config.bats @@ -104,3 +104,151 @@ setup() { run bash -c 'source "$JOSH_SYNC_ROOT/lib/core.sh"; source "$JOSH_SYNC_ROOT/lib/config.sh"; parse_config "nonexistent.yml"' [ "$status" -ne 0 ] } + +@test "parse_config fails when schema_version is missing (no v1 fallback)" { + cd "$(mktemp -d)" + cat > .josh-sync.yml <<'YAML' +josh: + proxy_url: "https://josh.test.local" + monorepo_path: "org/repo" + monorepo_url: "git@gitea.test.local:org/repo.git" +targets: + - name: "x" + subfolder: "x" + subrepo_url: "git@gitea.test.local:ext/x.git" + branches: { main: main } +bot: + name: "b" + email: "b@b" + trailer: "T" +YAML + run bash -c 'source "$JOSH_SYNC_ROOT/lib/core.sh"; source "$JOSH_SYNC_ROOT/lib/config.sh"; parse_config ".josh-sync.yml"' + [ "$status" -ne 0 ] + [[ "$output" == *"schema_version missing"* ]] +} + +@test "parse_config rejects schema_version: 1" { + cd "$(mktemp -d)" + cat > .josh-sync.yml <<'YAML' +schema_version: 1 +josh: + proxy_url: "https://josh.test.local" + monorepo_path: "org/repo" + monorepo_url: "git@gitea.test.local:org/repo.git" +targets: + - name: "x" + subfolder: "x" + subrepo_url: "git@gitea.test.local:ext/x.git" + branches: { main: main } +bot: + name: "b" + email: "b@b" + trailer: "T" +YAML + run bash -c 'source "$JOSH_SYNC_ROOT/lib/core.sh"; source "$JOSH_SYNC_ROOT/lib/config.sh"; parse_config ".josh-sync.yml"' + [ "$status" -ne 0 ] + [[ "$output" == *"Unsupported schema_version"* ]] || [[ "$output" == *"requires schema_version 2"* ]] +} + +@test "parse_config fails when josh.monorepo_url is missing" { + cd "$(mktemp -d)" + cat > .josh-sync.yml <<'YAML' +schema_version: 2 +josh: + proxy_url: "https://josh.test.local" + monorepo_path: "org/repo" +targets: + - name: "x" + subfolder: "x" + subrepo_url: "git@gitea.test.local:ext/x.git" + branches: { main: main } +bot: + name: "b" + email: "b@b" + trailer: "T" +YAML + run bash -c 'source "$JOSH_SYNC_ROOT/lib/core.sh"; source "$JOSH_SYNC_ROOT/lib/config.sh"; parse_config ".josh-sync.yml"' + [ "$status" -ne 0 ] + [[ "$output" == *"monorepo_url"* ]] +} + +@test "parse_config exports MONOREPO_URL, MONOREPO_AUTH, MONOREPO_GITEA_HOST, MONOREPO_REPO_PATH from SSH form" { + cd "$(mktemp -d)" + cp "$FIXTURES/minimal.yml" .josh-sync.yml + + parse_config ".josh-sync.yml" + + [ "$MONOREPO_URL" = "git@gitea.test.local:org/repo.git" ] + [ "$MONOREPO_AUTH" = "https" ] + [ "$MONOREPO_GITEA_HOST" = "gitea.test.local" ] + [ "$MONOREPO_REPO_PATH" = "org/repo" ] +} + +@test "parse_config derives MONOREPO_GITEA_HOST from HTTPS monorepo_url" { + cd "$(mktemp -d)" + cat > .josh-sync.yml <<'YAML' +schema_version: 2 +josh: + proxy_url: "https://josh.test.local" + monorepo_path: "org/repo" + monorepo_url: "https://gitea.example.io/org/repo.git" +targets: + - name: "x" + subfolder: "x" + subrepo_url: "git@gitea.test.local:ext/x.git" + branches: { main: main } +bot: + name: "b" + email: "b@b" + trailer: "T" +YAML + parse_config ".josh-sync.yml" + [ "$MONOREPO_GITEA_HOST" = "gitea.example.io" ] + [ "$MONOREPO_REPO_PATH" = "org/repo" ] +} + +@test "parse_config derives MONOREPO_API from monorepo_url's host (cross-host: monorepo and target on different hosts)" { + cd "$(mktemp -d)" + cat > .josh-sync.yml <<'YAML' +schema_version: 2 +josh: + proxy_url: "https://josh.example.io" + monorepo_path: "org/repo" + monorepo_url: "git@code.example.io:org/repo.git" +targets: + - name: "x" + subfolder: "x" + subrepo_url: "git@other-host.io:ext/x.git" + branches: { main: main } +bot: + name: "b" + email: "b@b" + trailer: "T" +YAML + parse_config ".josh-sync.yml" + [ "$MONOREPO_API" = "https://code.example.io/api/v1/repos/org/repo" ] +} + +@test "parse_config rejects invalid monorepo_auth value" { + cd "$(mktemp -d)" + cat > .josh-sync.yml <<'YAML' +schema_version: 2 +josh: + proxy_url: "https://josh.test.local" + monorepo_path: "org/repo" + monorepo_url: "git@gitea.test.local:org/repo.git" + monorepo_auth: "weird" +targets: + - name: "x" + subfolder: "x" + subrepo_url: "git@gitea.test.local:ext/x.git" + branches: { main: main } +bot: + name: "b" + email: "b@b" + trailer: "T" +YAML + run bash -c 'source "$JOSH_SYNC_ROOT/lib/core.sh"; source "$JOSH_SYNC_ROOT/lib/config.sh"; parse_config ".josh-sync.yml"' + [ "$status" -ne 0 ] + [[ "$output" == *"monorepo_auth"* ]] +}