diff --git a/bin/josh-sync b/bin/josh-sync index 610460c..e3c244d 100755 --- a/bin/josh-sync +++ b/bin/josh-sync @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC1091 # Source paths resolved at runtime # bin/josh-sync — CLI entrypoint for josh-sync # # Usage: josh-sync [flags] @@ -291,11 +292,14 @@ cmd_preflight() { parse_config "$config_file" + # shellcheck disable=SC2015 # A && B || C is intentional — pass/warn/fail never fail + { [ -n "$JOSH_PROXY_URL" ] && pass "proxy_url: ${JOSH_PROXY_URL}" || fail "proxy_url missing" [ -n "$MONOREPO_PATH" ] && pass "monorepo_path: ${MONOREPO_PATH}" || fail "monorepo_path missing" [ -n "$BOT_TRAILER" ] && pass "trailer: ${BOT_TRAILER}" || fail "trailer missing" [ -n "${GITEA_TOKEN:-}" ] && pass "SYNC_BOT_TOKEN is set" || warn "SYNC_BOT_TOKEN missing in .env" [ -n "${BOT_USER:-}" ] && pass "BOT_USER: ${BOT_USER}" || warn "BOT_USER missing in .env" + } local target_count target_count=$(echo "$JOSH_SYNC_TARGETS" | jq 'length') @@ -316,8 +320,11 @@ cmd_preflight() { load_target "$TARGET_JSON" + # shellcheck disable=SC2015 + { [ -n "$JOSH_FILTER" ] && pass "josh_filter: ${JOSH_FILTER}" || fail "josh_filter missing" [ -n "$SUBREPO_URL" ] && pass "subrepo_url: ${SUBREPO_URL}" || fail "subrepo_url missing" + } # Subfolder exists if [ -d "$subfolder" ]; then diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e5578c1 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1770562336, + "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d6c71932130818840fc8fe9509cf50be8c64634f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lib/auth.sh b/lib/auth.sh index 58e404d..229ff88 100644 --- a/lib/auth.sh +++ b/lib/auth.sh @@ -11,6 +11,7 @@ josh_auth_url() { local base="${JOSH_PROXY_URL}/${MONOREPO_PATH}.git${JOSH_FILTER}.git" + # shellcheck disable=SC2001 # sed is clearer than ${var//} for URL injection echo "$base" | sed "s|https://|https://${BOT_USER}:${GITEA_TOKEN}@|" } @@ -22,6 +23,7 @@ subrepo_auth_url() { if [ "${SUBREPO_AUTH:-https}" = "ssh" ]; then echo "$SUBREPO_URL" else + # shellcheck disable=SC2001 echo "$SUBREPO_URL" | sed "s|https://|https://${BOT_USER}:${SUBREPO_TOKEN}@|" fi } diff --git a/lib/core.sh b/lib/core.sh index 64ab9b7..e9dabb5 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -7,10 +7,7 @@ set -euo pipefail # ─── Exit Codes ───────────────────────────────────────────────────── -readonly E_OK=0 readonly E_GENERAL=1 -readonly E_CONFIG=10 -readonly E_AUTH=11 # ─── Logging ──────────────────────────────────────────────────────── # All log output goes to stderr. Sync functions use stdout for return values. diff --git a/lib/state.sh b/lib/state.sh index 90a4c41..2410fac 100644 --- a/lib/state.sh +++ b/lib/state.sh @@ -62,7 +62,7 @@ write_state() { echo "$state_json" | jq '.' > "${tmp_dir}/${key}.json" ( - cd "$tmp_dir" + cd "$tmp_dir" || exit git add -A if ! git diff --cached --quiet 2>/dev/null; then git -c user.name="$BOT_NAME" -c user.email="$BOT_EMAIL" \ diff --git a/lib/sync.sh b/lib/sync.sh index f18a472..91d2903 100644 --- a/lib/sync.sh +++ b/lib/sync.sh @@ -18,6 +18,7 @@ forward_sync() { local subrepo_branch="$SYNC_BRANCH_SUBREPO" local work_dir work_dir=$(mktemp -d) + # shellcheck disable=SC2064 # Intentional early expansion — work_dir is local trap "rm -rf '$work_dir'" EXIT log "INFO" "=== Forward sync: mono/${mono_branch} → subrepo/${subrepo_branch} ===" @@ -28,7 +29,7 @@ forward_sync() { --branch "$mono_branch" --single-branch \ "${work_dir}/filtered" || die "Failed to clone through josh-proxy" - cd "${work_dir}/filtered" + cd "${work_dir}/filtered" || exit git config user.name "$BOT_NAME" git config user.email "$BOT_EMAIL" @@ -56,7 +57,8 @@ forward_sync() { # 5. Compare trees — skip if identical local mono_tree subrepo_tree - mono_tree=$(git rev-parse HEAD^{tree}) + mono_tree=$(git rev-parse 'HEAD^{tree}') + # shellcheck disable=SC1083 # {tree} is git syntax, not shell brace expansion subrepo_tree=$(git rev-parse "subrepo/${subrepo_branch}^{tree}" 2>/dev/null || echo "none") if [ "$mono_tree" = "$subrepo_tree" ]; then @@ -110,8 +112,10 @@ ${BOT_TRAILER}: forward/${mono_branch}/$(date -u +%Y-%m-%dT%H:%M:%SZ)" >&2 git push "$(subrepo_auth_url)" "${conflict_branch}" # Create PR on subrepo - local pr_body - pr_body="## Sync Conflict\n\nMonorepo \`${mono_branch}\` has changes that conflict with \`${subrepo_branch}\`.\n\n**Conflicted files:**\n$(echo "$conflicted" | sed 's/^/- /')\n\nPlease resolve and merge this PR to complete the sync." + local pr_body conflicted_list + # shellcheck disable=SC2001 + conflicted_list=$(echo "$conflicted" | sed 's/^/- /') + pr_body="## Sync Conflict\n\nMonorepo \`${mono_branch}\` has changes that conflict with \`${subrepo_branch}\`.\n\n**Conflicted files:**\n${conflicted_list}\n\nPlease resolve and merge this PR to complete the sync." create_pr "${SUBREPO_API}" "${SUBREPO_TOKEN}" \ "$subrepo_branch" "$conflict_branch" \ @@ -134,6 +138,7 @@ reverse_sync() { local subrepo_branch="$SYNC_BRANCH_SUBREPO" local work_dir work_dir=$(mktemp -d) + # shellcheck disable=SC2064 # Intentional early expansion — work_dir is local trap "rm -rf '$work_dir'" EXIT log "INFO" "=== Reverse sync: subrepo/${subrepo_branch} → mono/${mono_branch} ===" @@ -143,7 +148,7 @@ reverse_sync() { --branch "$subrepo_branch" --single-branch \ "${work_dir}/subrepo" || die "Failed to clone subrepo" - cd "${work_dir}/subrepo" + cd "${work_dir}/subrepo" || exit git config user.name "$BOT_NAME" git config user.email "$BOT_EMAIL" @@ -205,13 +210,16 @@ initial_import() { '.[] | select(.name == $n) | .subfolder') local work_dir work_dir=$(mktemp -d) + # shellcheck disable=SC2064 # Intentional early expansion — work_dir is local trap "rm -rf '$work_dir'" EXIT 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 - mono_auth_url=$(echo "https://${BOT_USER}:${GITEA_TOKEN}@$(echo "$MONOREPO_API" | sed 's|https://||; s|/api/v1/repos/|/|').git") + 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" \ --branch "$mono_branch" --single-branch \ @@ -227,7 +235,7 @@ initial_import() { log "INFO" "Subrepo has ${file_count} files" # 3. Copy subrepo content into monorepo subfolder - cd "${work_dir}/monorepo" + cd "${work_dir}/monorepo" || exit git config user.name "$BOT_NAME" git config user.email "$BOT_EMAIL" @@ -277,6 +285,7 @@ subrepo_reset() { local subrepo_branch="$SYNC_BRANCH_SUBREPO" local work_dir work_dir=$(mktemp -d) + # shellcheck disable=SC2064 # Intentional early expansion — work_dir is local trap "rm -rf '$work_dir'" EXIT log "INFO" "=== Subrepo reset: josh-filtered mono/${mono_branch} → subrepo/${subrepo_branch} ===" @@ -287,7 +296,7 @@ subrepo_reset() { --branch "$mono_branch" --single-branch \ "${work_dir}/filtered" || die "Failed to clone through josh-proxy" - cd "${work_dir}/filtered" + cd "${work_dir}/filtered" || exit local head_sha head_sha=$(git rev-parse HEAD)