Josh-proxy's parser treats "/" in :+ paths as a filter separator, so :+.josh-filters/backend fails. Use flat naming at repo root: .josh-filter-<target>.josh referenced as :+.josh-filter-<target>. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
168 lines
6.9 KiB
Bash
168 lines
6.9 KiB
Bash
#!/usr/bin/env bash
|
|
# lib/config.sh — Config loading and target resolution
|
|
#
|
|
# Two-phase loading:
|
|
# 1. parse_config() — reads .josh-sync.yml via yq+jq, exports globals + JOSH_SYNC_TARGETS
|
|
# 2. load_target() — called per-target during iteration, sets target-specific env vars
|
|
#
|
|
# Requires: lib/core.sh sourced first, yq and jq on PATH
|
|
|
|
# ─── Josh Filter File Generation ──────────────────────────────────
|
|
# Generates .josh-filter-<target>.josh for targets with exclude patterns.
|
|
# These files must be committed to the monorepo — josh-proxy reads them
|
|
# from the repo at clone time via the :+ stored filter syntax.
|
|
# Files are at the repo root (flat naming) because josh-proxy's parser
|
|
# treats "/" in :+ paths as a filter separator.
|
|
|
|
_generate_josh_filters() {
|
|
local has_excludes
|
|
has_excludes=$(echo "$JOSH_SYNC_TARGETS" | jq '[.[] | select((.exclude // []) | length > 0)] | length')
|
|
|
|
if [ "$has_excludes" -eq 0 ]; then
|
|
return
|
|
fi
|
|
|
|
local target_name subfolder exclude_patterns filter_content
|
|
while IFS= read -r target_name; do
|
|
subfolder=$(echo "$JOSH_SYNC_TARGETS" | jq -r --arg n "$target_name" \
|
|
'.[] | select(.name == $n) | .subfolder')
|
|
exclude_patterns=$(echo "$JOSH_SYNC_TARGETS" | jq -r --arg n "$target_name" \
|
|
'.[] | select(.name == $n) | .exclude | map(" ::" + .) | join("\n")')
|
|
|
|
filter_content=":/${subfolder}:exclude[
|
|
${exclude_patterns}
|
|
]"
|
|
|
|
local filter_file=".josh-filter-${target_name}.josh"
|
|
local existing=""
|
|
if [ -f "$filter_file" ]; then
|
|
existing=$(cat "$filter_file")
|
|
fi
|
|
|
|
if [ "$filter_content" != "$existing" ]; then
|
|
echo "$filter_content" > "$filter_file"
|
|
log "WARN" "Generated ${filter_file} — commit this file to the monorepo"
|
|
fi
|
|
done < <(echo "$JOSH_SYNC_TARGETS" | jq -r '.[] | select((.exclude // []) | length > 0) | .name')
|
|
}
|
|
|
|
# ─── Phase 1: Parse Config ─────────────────────────────────────────
|
|
|
|
parse_config() {
|
|
local config_file="${1:-.josh-sync.yml}"
|
|
[ -f "$config_file" ] || die "Config not found: ${config_file} (run from monorepo root)"
|
|
|
|
local config_json
|
|
config_json=$(yq -o json "$config_file") || die "Failed to parse ${config_file}"
|
|
|
|
# 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 BOT_NAME
|
|
BOT_NAME=$(echo "$config_json" | jq -r '.bot.name')
|
|
export BOT_EMAIL
|
|
BOT_EMAIL=$(echo "$config_json" | jq -r '.bot.email')
|
|
export BOT_TRAILER
|
|
BOT_TRAILER=$(echo "$config_json" | jq -r '.bot.trailer')
|
|
|
|
[ -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 "$BOT_TRAILER" ] && [ "$BOT_TRAILER" != "null" ] || die "bot.trailer missing in config"
|
|
|
|
# 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[] | . +
|
|
# Auto-derive josh_filter from subfolder if not set
|
|
# When exclude patterns are present, use a stored josh filter (:+.josh-filter-<name>)
|
|
(if (.exclude // [] | length) > 0 then
|
|
{josh_filter: (":+.josh-filter-" + .name)}
|
|
elif (.josh_filter // "") == "" then
|
|
{josh_filter: (":/" + .subfolder)}
|
|
else {} end) +
|
|
# Derive gitea_host and subrepo_repo_path from subrepo_url
|
|
(.subrepo_url as $url |
|
|
if ($url | test("^ssh://")) then
|
|
($url | capture("ssh://[^@]*@(?<h>[^/]+)/(?<p>.+?)(\\.git)?$") //
|
|
{h: "", p: ""} | {gitea_host: .h, subrepo_repo_path: .p})
|
|
elif ($url | test("^git@")) then
|
|
($url | capture("git@(?<h>[^:/]+)[:/](?<p>.+?)(\\.git)?$") //
|
|
{h: "", p: ""} | {gitea_host: .h, subrepo_repo_path: .p})
|
|
elif ($url | test("^https?://")) then
|
|
($url | capture("https?://(?<h>[^/]+)/(?<p>.+?)(\\.git)?$") //
|
|
{h: "", p: ""} | {gitea_host: .h, subrepo_repo_path: .p})
|
|
else
|
|
{gitea_host: "", subrepo_repo_path: ""}
|
|
end
|
|
)
|
|
]')
|
|
|
|
# Generate .josh-filter-*.josh for targets with exclude patterns
|
|
_generate_josh_filters
|
|
|
|
# Load .env credentials (if present, not required — CI sets these via secrets)
|
|
if [ -f .env ]; then
|
|
# shellcheck source=/dev/null
|
|
source .env
|
|
fi
|
|
|
|
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}}"
|
|
|
|
log "INFO" "Config loaded: $(echo "$JOSH_SYNC_TARGETS" | jq 'length') target(s)"
|
|
}
|
|
|
|
# ─── Phase 2: Load Target ──────────────────────────────────────────
|
|
# Called per-target during iteration. Takes a JSON object (one element
|
|
# of JOSH_SYNC_TARGETS) and sets all target-specific env vars.
|
|
|
|
load_target() {
|
|
local tj="$1"
|
|
|
|
export JOSH_SYNC_TARGET_NAME
|
|
JOSH_SYNC_TARGET_NAME=$(echo "$tj" | jq -r '.name')
|
|
export JOSH_FILTER
|
|
JOSH_FILTER=$(echo "$tj" | jq -r '.josh_filter')
|
|
export SUBREPO_URL
|
|
SUBREPO_URL=$(echo "$tj" | jq -r '.subrepo_url')
|
|
export SUBREPO_AUTH
|
|
SUBREPO_AUTH=$(echo "$tj" | jq -r '.subrepo_auth // "https"')
|
|
|
|
# API URL from pre-derived fields
|
|
local gitea_host subrepo_repo_path
|
|
gitea_host=$(echo "$tj" | jq -r '.gitea_host')
|
|
subrepo_repo_path=$(echo "$tj" | jq -r '.subrepo_repo_path')
|
|
export SUBREPO_API="https://${gitea_host}/api/v1/repos/${subrepo_repo_path}"
|
|
|
|
# Per-target credential resolution (indirect variable reference)
|
|
local token_var ssh_key_var
|
|
token_var=$(echo "$tj" | jq -r '.subrepo_token_var // "SUBREPO_TOKEN"')
|
|
ssh_key_var=$(echo "$tj" | jq -r '.subrepo_ssh_key_var // "SUBREPO_SSH_KEY"')
|
|
|
|
# Resolve: per-target var → default var → SYNC_BOT_TOKEN fallback
|
|
export SUBREPO_TOKEN="${!token_var:-${SUBREPO_TOKEN:-${SYNC_BOT_TOKEN:-}}}"
|
|
local ssh_key_value="${!ssh_key_var:-${SUBREPO_SSH_KEY:-}}"
|
|
|
|
# Clean up previous SSH state and set up new if needed
|
|
if [ -n "${JOSH_SSH_DIR:-}" ]; then
|
|
rm -rf "$JOSH_SSH_DIR"
|
|
unset JOSH_SSH_DIR GIT_SSH_COMMAND
|
|
fi
|
|
|
|
if [ "$SUBREPO_AUTH" = "ssh" ] && [ -n "$ssh_key_value" ]; then
|
|
JOSH_SSH_DIR=$(mktemp -d)
|
|
echo "$ssh_key_value" > "${JOSH_SSH_DIR}/subrepo_key"
|
|
chmod 600 "${JOSH_SSH_DIR}/subrepo_key"
|
|
export GIT_SSH_COMMAND="ssh -i ${JOSH_SSH_DIR}/subrepo_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
|
log "INFO" "SSH auth configured for target ${JOSH_SYNC_TARGET_NAME}"
|
|
fi
|
|
|
|
log "INFO" "Loaded target: ${JOSH_SYNC_TARGET_NAME} (${SUBREPO_AUTH})"
|
|
}
|