Release 2.0.0 — explicit monorepo_url, drop first-target-host inference

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 06:31:21 +03:00
parent 5e9b134168
commit b28662d3b4
16 changed files with 377 additions and 31 deletions

View File

@@ -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://<host>/api/v1/repos/<org/repo>
# 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 ─────────────────────────────────────────────────

View File

@@ -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://[^@]*@(?<h>[^/]+)/(?<p>.+?)(\\.git)?$") // {h: "", p: ""})
elif ($u | test("^git@")) then
($u | capture("git@(?<h>[^:/]+)[:/](?<p>.+?)(\\.git)?$") // {h: "", p: ""})
elif ($u | test("^https?://")) then
($u | capture("https?://(?<h>[^/]+)/(?<p>.+?)(\\.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)"
}

View File

@@ -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"