2026-02-12 09:20:55 +03:00
#!/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
# ─── 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 } "
2026-04-29 06:31:21 +03:00
# Schema version check — required, must be 2 (v1 configs are no longer supported)
2026-04-01 00:56:33 +03:00
local schema_version
2026-04-29 06:31:21 +03:00
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) "
2026-04-01 00:56:33 +03:00
2026-02-12 09:20:55 +03:00
# 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' )
2026-04-29 06:31:21 +03:00
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"' )
2026-02-12 09:20:55 +03:00
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"
2026-04-29 06:31:21 +03:00
[ -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 } ') "
2026-02-12 09:20:55 +03:00
[ -n " $BOT_TRAILER " ] && [ " $BOT_TRAILER " != "null" ] || die "bot.trailer missing in config"
2026-04-29 06:31:21 +03:00
# 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) "
2026-02-12 09:20:55 +03:00
# 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
2026-02-14 10:19:41 +03:00
# When exclude patterns are present, append inline :exclude[::p1,::p2,...] to the filter
2026-02-13 22:45:13 +03:00
( if ( .exclude // [ ] | length) > 0 then
2026-02-14 10:19:41 +03:00
{ josh_filter: ( ":/" + .subfolder + ":exclude[" + ( .exclude | map( "::" + .) | join( "," ) ) + "]" ) }
2026-02-13 22:45:13 +03:00
elif ( .josh_filter // "" ) = = "" then
2026-02-12 09:20:55 +03:00
{ 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
)
] ' )
2026-05-27 03:32:04 +01:00
# Validate targets[].history_lock against the schema enum (preserve|rewrite).
# Caught at config parse time so typos don't sit silently until `onboard` runs.
local bad_lock
bad_lock = $( echo " $JOSH_SYNC_TARGETS " | jq -r '
.[ ] | select ( .history_lock != null)
| select ( .history_lock != "preserve" and .history_lock != "rewrite" )
| "\(.name): \(.history_lock)" ' | head -1)
[ -z " $bad_lock " ] || die " Invalid targets[].history_lock value (must be 'preserve' or 'rewrite'): ${ bad_lock } "
2026-02-12 09:20:55 +03:00
# 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 :- } } "
2026-04-29 06:31:21 +03:00
# 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 } } "
2026-02-12 09:20:55 +03:00
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 } ) "
}