feat: add runner conversion scripts and strengthen cutover automation
This commit is contained in:
@@ -29,17 +29,35 @@ phase_header 10 "Local Repo Remote Cutover"
|
||||
|
||||
LOCAL_REPO_ROOT="${PHASE10_LOCAL_ROOT:-/Users/s/development}"
|
||||
EXPECTED_REPO_COUNT="${PHASE10_EXPECTED_REPO_COUNT:-3}"
|
||||
INCLUDE_PATHS=()
|
||||
DRY_RUN=false
|
||||
FORCE_WITH_LEASE=false
|
||||
ASKPASS_SCRIPT=""
|
||||
PHASE10_GITEA_REPO_EXISTS=false
|
||||
PHASE10_REMOTE_BRANCHES=""
|
||||
PHASE10_REMOTE_TAGS=""
|
||||
PHASE10_LAST_CURL_ERROR=""
|
||||
PHASE10_LAST_HTTP_CODE=""
|
||||
PHASE10_HTTP_CONNECT_TIMEOUT="${PHASE10_HTTP_CONNECT_TIMEOUT:-15}"
|
||||
PHASE10_HTTP_LOW_SPEED_LIMIT="${PHASE10_HTTP_LOW_SPEED_LIMIT:-1}"
|
||||
PHASE10_HTTP_LOW_SPEED_TIME="${PHASE10_HTTP_LOW_SPEED_TIME:-30}"
|
||||
PHASE10_PUSH_TIMEOUT_SEC="${PHASE10_PUSH_TIMEOUT_SEC:-120}"
|
||||
PHASE10_LSREMOTE_TIMEOUT_SEC="${PHASE10_LSREMOTE_TIMEOUT_SEC:-45}"
|
||||
PHASE10_API_CONNECT_TIMEOUT_SEC="${PHASE10_API_CONNECT_TIMEOUT_SEC:-8}"
|
||||
PHASE10_API_MAX_TIME_SEC="${PHASE10_API_MAX_TIME_SEC:-20}"
|
||||
|
||||
if [[ -n "${PHASE10_INCLUDE_PATHS:-}" ]]; then
|
||||
# Space-delimited list of extra repo roots to include in phase10 discovery.
|
||||
read -r -a INCLUDE_PATHS <<< "${PHASE10_INCLUDE_PATHS}"
|
||||
fi
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--local-root=*) LOCAL_REPO_ROOT="${arg#*=}" ;;
|
||||
--expected-count=*) EXPECTED_REPO_COUNT="${arg#*=}" ;;
|
||||
--include-path=*) INCLUDE_PATHS+=("${arg#*=}") ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--force-with-lease) FORCE_WITH_LEASE=true ;;
|
||||
--help|-h)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
@@ -47,7 +65,9 @@ Usage: $(basename "$0") [options]
|
||||
Options:
|
||||
--local-root=PATH Root folder containing local repos (default: /Users/s/development)
|
||||
--expected-count=N Require exactly N discovered repos (default: 3, 0 disables)
|
||||
--include-path=PATH Explicit repo root to include (repeatable)
|
||||
--dry-run Print planned actions only (no mutations)
|
||||
--force-with-lease Use force-with-lease when pushing branches/tags to Gitea
|
||||
--help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
@@ -63,6 +83,14 @@ if ! [[ "$EXPECTED_REPO_COUNT" =~ ^[0-9]+$ ]]; then
|
||||
log_error "--expected-count must be a non-negative integer"
|
||||
exit 1
|
||||
fi
|
||||
for timeout_var in PHASE10_HTTP_CONNECT_TIMEOUT PHASE10_HTTP_LOW_SPEED_LIMIT PHASE10_HTTP_LOW_SPEED_TIME \
|
||||
PHASE10_PUSH_TIMEOUT_SEC PHASE10_LSREMOTE_TIMEOUT_SEC \
|
||||
PHASE10_API_CONNECT_TIMEOUT_SEC PHASE10_API_MAX_TIME_SEC; do
|
||||
if ! [[ "${!timeout_var}" =~ ^[0-9]+$ ]] || [[ "${!timeout_var}" -le 0 ]]; then
|
||||
log_error "${timeout_var} must be a positive integer"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$ASKPASS_SCRIPT" ]]; then
|
||||
@@ -92,6 +120,52 @@ git_with_auth() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
run_with_timeout() {
|
||||
local timeout_sec="$1"
|
||||
shift
|
||||
if command -v perl >/dev/null 2>&1; then
|
||||
perl -e '
|
||||
my $timeout = shift @ARGV;
|
||||
my $pid = fork();
|
||||
if (!defined $pid) { exit 125; }
|
||||
if ($pid == 0) {
|
||||
setpgrp(0, 0);
|
||||
exec @ARGV;
|
||||
exit 125;
|
||||
}
|
||||
my $timed_out = 0;
|
||||
local $SIG{ALRM} = sub {
|
||||
$timed_out = 1;
|
||||
kill "TERM", -$pid;
|
||||
select(undef, undef, undef, 0.5);
|
||||
kill "KILL", -$pid;
|
||||
};
|
||||
alarm $timeout;
|
||||
waitpid($pid, 0);
|
||||
alarm 0;
|
||||
if ($timed_out) { exit 124; }
|
||||
my $rc = $?;
|
||||
if ($rc == -1) { exit 125; }
|
||||
if ($rc & 127) { exit(128 + ($rc & 127)); }
|
||||
exit($rc >> 8);
|
||||
' "$timeout_sec" "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
git_with_auth_timed() {
|
||||
local timeout_sec="$1"
|
||||
shift
|
||||
run_with_timeout "$timeout_sec" \
|
||||
env \
|
||||
GIT_TERMINAL_PROMPT=0 \
|
||||
GIT_ASKPASS="$ASKPASS_SCRIPT" \
|
||||
GITEA_GIT_USERNAME="$GITEA_ADMIN_USER" \
|
||||
GITEA_GIT_TOKEN="$GITEA_ADMIN_TOKEN" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
ensure_github_remote() {
|
||||
local repo_path="$1" repo_name="$2" github_url="$3"
|
||||
local existing origin_existing has_bad_github
|
||||
@@ -138,8 +212,14 @@ ensure_github_remote() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_error "${repo_name}: could not find GitHub remote in 'origin' or 'github'"
|
||||
return 1
|
||||
# Explicit include-path repos may have no GitHub remote yet.
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would add github remote -> ${github_url}"
|
||||
else
|
||||
git -C "$repo_path" remote add github "$github_url"
|
||||
log_success "${repo_name}: added github remote (${github_url})"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_gitea_origin() {
|
||||
@@ -176,33 +256,76 @@ ensure_gitea_origin() {
|
||||
|
||||
ensure_gitea_repo_exists() {
|
||||
local repo_name="$1"
|
||||
local create_payload http_code
|
||||
local create_payload create_response
|
||||
|
||||
get_gitea_repo_http_code() {
|
||||
local target_repo="$1"
|
||||
local tmpfile curl_code
|
||||
local tmpfile errfile curl_code
|
||||
tmpfile=$(mktemp)
|
||||
errfile=$(mktemp)
|
||||
curl_code=$(curl \
|
||||
-s \
|
||||
-sS \
|
||||
-o "$tmpfile" \
|
||||
-w "%{http_code}" \
|
||||
--connect-timeout "$PHASE10_API_CONNECT_TIMEOUT_SEC" \
|
||||
--max-time "$PHASE10_API_MAX_TIME_SEC" \
|
||||
-H "Authorization: token ${GITEA_ADMIN_TOKEN}" \
|
||||
-H "Accept: application/json" \
|
||||
"${GITEA_INTERNAL_URL}/api/v1/repos/${GITEA_ORG_NAME}/${target_repo}") || {
|
||||
"${GITEA_INTERNAL_URL}/api/v1/repos/${GITEA_ORG_NAME}/${target_repo}" 2>"$errfile") || {
|
||||
PHASE10_LAST_CURL_ERROR="$(tr '\n' ' ' < "$errfile" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')"
|
||||
rm -f "$tmpfile"
|
||||
rm -f "$errfile"
|
||||
return 1
|
||||
}
|
||||
rm -f "$tmpfile"
|
||||
printf '%s' "$curl_code"
|
||||
rm -f "$errfile"
|
||||
PHASE10_LAST_CURL_ERROR=""
|
||||
PHASE10_LAST_HTTP_CODE="$curl_code"
|
||||
return 0
|
||||
}
|
||||
|
||||
create_gitea_repo() {
|
||||
local payload="$1"
|
||||
local tmpfile errfile curl_code
|
||||
tmpfile=$(mktemp)
|
||||
errfile=$(mktemp)
|
||||
curl_code=$(curl \
|
||||
-sS \
|
||||
-o "$tmpfile" \
|
||||
-w "%{http_code}" \
|
||||
--connect-timeout "$PHASE10_API_CONNECT_TIMEOUT_SEC" \
|
||||
--max-time "$PHASE10_API_MAX_TIME_SEC" \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GITEA_ADMIN_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d "$payload" \
|
||||
"${GITEA_INTERNAL_URL}/api/v1/orgs/${GITEA_ORG_NAME}/repos" 2>"$errfile") || {
|
||||
PHASE10_LAST_CURL_ERROR="$(tr '\n' ' ' < "$errfile" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')"
|
||||
rm -f "$tmpfile"
|
||||
rm -f "$errfile"
|
||||
return 1
|
||||
}
|
||||
create_response="$(cat "$tmpfile")"
|
||||
rm -f "$tmpfile"
|
||||
rm -f "$errfile"
|
||||
PHASE10_LAST_CURL_ERROR=""
|
||||
PHASE10_LAST_HTTP_CODE="$curl_code"
|
||||
return 0
|
||||
}
|
||||
|
||||
PHASE10_GITEA_REPO_EXISTS=false
|
||||
if ! http_code="$(get_gitea_repo_http_code "$repo_name")"; then
|
||||
PHASE10_LAST_CURL_ERROR=""
|
||||
PHASE10_LAST_HTTP_CODE=""
|
||||
if ! get_gitea_repo_http_code "$repo_name"; then
|
||||
log_error "${repo_name}: failed to query Gitea API for repo existence"
|
||||
if [[ -n "$PHASE10_LAST_CURL_ERROR" ]]; then
|
||||
log_error "${repo_name}: curl error: ${PHASE10_LAST_CURL_ERROR}"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$http_code" == "200" ]]; then
|
||||
if [[ "$PHASE10_LAST_HTTP_CODE" == "200" ]]; then
|
||||
PHASE10_GITEA_REPO_EXISTS=true
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: Gitea repo already exists (${GITEA_ORG_NAME}/${repo_name})"
|
||||
@@ -210,8 +333,8 @@ ensure_gitea_repo_exists() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$http_code" != "404" ]]; then
|
||||
log_error "${repo_name}: unexpected Gitea API status while checking repo (${http_code})"
|
||||
if [[ "$PHASE10_LAST_HTTP_CODE" != "404" ]]; then
|
||||
log_error "${repo_name}: unexpected Gitea API status while checking repo (${PHASE10_LAST_HTTP_CODE})"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -224,12 +347,31 @@ ensure_gitea_repo_exists() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if gitea_api POST "/orgs/${GITEA_ORG_NAME}/repos" "$create_payload" >/dev/null 2>&1; then
|
||||
log_info "${repo_name}: creating missing Gitea repo ${GITEA_ORG_NAME}/${repo_name}"
|
||||
PHASE10_LAST_HTTP_CODE=""
|
||||
if ! create_gitea_repo "$create_payload"; then
|
||||
log_error "${repo_name}: failed to create Gitea repo ${GITEA_ORG_NAME}/${repo_name} (network/API call failed)"
|
||||
if [[ -n "$PHASE10_LAST_CURL_ERROR" ]]; then
|
||||
log_error "${repo_name}: curl error: ${PHASE10_LAST_CURL_ERROR}"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$PHASE10_LAST_HTTP_CODE" == "201" ]]; then
|
||||
log_success "${repo_name}: created missing Gitea repo ${GITEA_ORG_NAME}/${repo_name}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "${repo_name}: failed to create Gitea repo ${GITEA_ORG_NAME}/${repo_name}"
|
||||
# If another process created the repo concurrently, treat it as success.
|
||||
if [[ "$PHASE10_LAST_HTTP_CODE" == "409" ]]; then
|
||||
log_warn "${repo_name}: Gitea repo already exists (HTTP 409), continuing"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "${repo_name}: failed to create Gitea repo ${GITEA_ORG_NAME}/${repo_name} (HTTP ${PHASE10_LAST_HTTP_CODE})"
|
||||
if [[ -n "${create_response:-}" ]]; then
|
||||
log_error "${repo_name}: API response: ${create_response}"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -249,12 +391,21 @@ list_contains() {
|
||||
|
||||
fetch_remote_refs() {
|
||||
local url="$1"
|
||||
local refs ref short
|
||||
local refs ref short rc
|
||||
|
||||
PHASE10_REMOTE_BRANCHES=""
|
||||
PHASE10_REMOTE_TAGS=""
|
||||
|
||||
refs=$(git_with_auth git ls-remote --heads --tags "$url" 2>/dev/null) || return 1
|
||||
refs="$(git_with_auth_timed "$PHASE10_LSREMOTE_TIMEOUT_SEC" \
|
||||
git \
|
||||
-c "http.connectTimeout=${PHASE10_HTTP_CONNECT_TIMEOUT}" \
|
||||
-c "http.lowSpeedLimit=${PHASE10_HTTP_LOW_SPEED_LIMIT}" \
|
||||
-c "http.lowSpeedTime=${PHASE10_HTTP_LOW_SPEED_TIME}" \
|
||||
ls-remote --heads --tags "$url" 2>/dev/null)"
|
||||
rc=$?
|
||||
if [[ "$rc" -ne 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
[[ -n "$refs" ]] || return 0
|
||||
|
||||
while IFS= read -r line; do
|
||||
@@ -352,6 +503,7 @@ dry_run_compare_local_and_remote() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "${repo_name}: reading remote refs from Gitea (timeout ${PHASE10_LSREMOTE_TIMEOUT_SEC}s)"
|
||||
if ! fetch_remote_refs "$gitea_url"; then
|
||||
log_warn "${repo_name}: could not read Gitea refs via ls-remote; skipping diff"
|
||||
return 0
|
||||
@@ -367,18 +519,69 @@ dry_run_compare_local_and_remote() {
|
||||
|
||||
push_all_refs_to_origin() {
|
||||
local repo_path="$1" repo_name="$2"
|
||||
local push_output push_args push_rc
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would push all branches to origin"
|
||||
log_info "${repo_name}: would push all tags to origin"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! git_with_auth git -C "$repo_path" push --all origin >/dev/null; then
|
||||
log_error "${repo_name}: failed pushing branches to Gitea origin"
|
||||
push_args=(push --no-verify --all origin)
|
||||
if [[ "$FORCE_WITH_LEASE" == "true" ]]; then
|
||||
push_args=(push --no-verify --force-with-lease --all origin)
|
||||
fi
|
||||
|
||||
log_info "${repo_name}: pushing branches to origin (timeout ${PHASE10_PUSH_TIMEOUT_SEC}s)"
|
||||
push_output="$(git_with_auth_timed "$PHASE10_PUSH_TIMEOUT_SEC" \
|
||||
git \
|
||||
-c "http.connectTimeout=${PHASE10_HTTP_CONNECT_TIMEOUT}" \
|
||||
-c "http.lowSpeedLimit=${PHASE10_HTTP_LOW_SPEED_LIMIT}" \
|
||||
-c "http.lowSpeedTime=${PHASE10_HTTP_LOW_SPEED_TIME}" \
|
||||
-C "$repo_path" "${push_args[@]}" 2>&1)"
|
||||
push_rc=$?
|
||||
if [[ "$push_rc" -ne 0 ]]; then
|
||||
if [[ "$push_rc" -eq 124 ]]; then
|
||||
log_error "${repo_name}: branch push timed out after ${PHASE10_PUSH_TIMEOUT_SEC}s"
|
||||
log_error "${repo_name}: check network reachability to ${GITEA_DOMAIN} and retry"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$push_output" == *"non-fast-forward"* ]] || [[ "$push_output" == *"[rejected]"* ]]; then
|
||||
log_error "${repo_name}: branch push rejected (non-fast-forward)"
|
||||
log_error "${repo_name}: run with --dry-run first to review diffs, then re-run with --force-with-lease if local should win"
|
||||
else
|
||||
log_error "${repo_name}: failed pushing branches to Gitea origin"
|
||||
fi
|
||||
printf '%s\n' "$push_output" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! git_with_auth git -C "$repo_path" push --tags origin >/dev/null; then
|
||||
log_error "${repo_name}: failed pushing tags to Gitea origin"
|
||||
|
||||
push_args=(push --no-verify --tags origin)
|
||||
if [[ "$FORCE_WITH_LEASE" == "true" ]]; then
|
||||
push_args=(push --no-verify --force-with-lease --tags origin)
|
||||
fi
|
||||
|
||||
log_info "${repo_name}: pushing tags to origin (timeout ${PHASE10_PUSH_TIMEOUT_SEC}s)"
|
||||
push_output="$(git_with_auth_timed "$PHASE10_PUSH_TIMEOUT_SEC" \
|
||||
git \
|
||||
-c "http.connectTimeout=${PHASE10_HTTP_CONNECT_TIMEOUT}" \
|
||||
-c "http.lowSpeedLimit=${PHASE10_HTTP_LOW_SPEED_LIMIT}" \
|
||||
-c "http.lowSpeedTime=${PHASE10_HTTP_LOW_SPEED_TIME}" \
|
||||
-C "$repo_path" "${push_args[@]}" 2>&1)"
|
||||
push_rc=$?
|
||||
if [[ "$push_rc" -ne 0 ]]; then
|
||||
if [[ "$push_rc" -eq 124 ]]; then
|
||||
log_error "${repo_name}: tag push timed out after ${PHASE10_PUSH_TIMEOUT_SEC}s"
|
||||
log_error "${repo_name}: check network reachability to ${GITEA_DOMAIN} and retry"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$push_output" == *"non-fast-forward"* ]] || [[ "$push_output" == *"[rejected]"* ]]; then
|
||||
log_error "${repo_name}: tag push rejected (non-fast-forward/conflict)"
|
||||
log_error "${repo_name}: re-run with --force-with-lease only if replacing remote tags is intended"
|
||||
else
|
||||
log_error "${repo_name}: failed pushing tags to Gitea origin"
|
||||
fi
|
||||
printf '%s\n' "$push_output" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
@@ -398,7 +601,12 @@ retarget_tracking_to_origin() {
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "${repo_name}: would create origin/${branch} by pushing local ${branch}"
|
||||
else
|
||||
if ! git_with_auth git -C "$repo_path" push origin "refs/heads/${branch}:refs/heads/${branch}" >/dev/null; then
|
||||
if ! git_with_auth_timed "$PHASE10_PUSH_TIMEOUT_SEC" \
|
||||
git \
|
||||
-c "http.connectTimeout=${PHASE10_HTTP_CONNECT_TIMEOUT}" \
|
||||
-c "http.lowSpeedLimit=${PHASE10_HTTP_LOW_SPEED_LIMIT}" \
|
||||
-c "http.lowSpeedTime=${PHASE10_HTTP_LOW_SPEED_TIME}" \
|
||||
-C "$repo_path" push --no-verify origin "refs/heads/${branch}:refs/heads/${branch}" >/dev/null; then
|
||||
log_error "${repo_name}: could not create origin/${branch} while setting tracking"
|
||||
return 1
|
||||
fi
|
||||
@@ -430,7 +638,20 @@ retarget_tracking_to_origin() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if ! phase10_discover_local_repos "$LOCAL_REPO_ROOT" "$GITHUB_USERNAME" "$SCRIPT_DIR" "$EXPECTED_REPO_COUNT"; then
|
||||
if ! phase10_discover_local_repos "$LOCAL_REPO_ROOT" "$GITHUB_USERNAME" "$SCRIPT_DIR" 0; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for include_path in "${INCLUDE_PATHS[@]}"; do
|
||||
[[ -z "$include_path" ]] && continue
|
||||
if ! phase10_include_repo_path "$include_path" "$GITHUB_USERNAME"; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
phase10_sort_repo_arrays
|
||||
|
||||
if ! phase10_enforce_expected_count "$EXPECTED_REPO_COUNT" "$LOCAL_REPO_ROOT"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user