Add the GitHub and Gitea steps and split the script into library files

Dry run by default: the script reads from both hosts (tokens, owners,
names, license, SSH) and prints a plan; --apply creates the repositories
and the Gitea -> GitHub push mirror after a final yes. Choosing GitHub
applies the AGPL-3.0 license to the Gitea repository. A failed step is
reported with what exists and how to continue; nothing is ever deleted.

create-project.sh is now the entry point; the work lives in src/lib/, one
responsibility per file. .gitignore gets !src/lib (the Python template
ignores any lib/ folder). Tests grow to 599 checks, with a stub curl and
ssh, and guards for the file structure.

Task: MIL-002#1
Task: MIL-002#2
Task: MIL-002#3
Task: MIL-002#4
Task: MIL-002#5
Task: MIL-002#6
Refs #9
Refs #10
Refs #11
Refs #12
Refs #13
Refs #14

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 15:29:10 +08:00
co-authored by Claude Sonnet 5.5
parent 903c948d85
commit 736cdb42fc
30 changed files with 2269 additions and 668 deletions
+55
View File
@@ -0,0 +1,55 @@
# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# json.sh - Reading and writing the small amount of JSON the script needs.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: json_escape, json_get, json_has_value, json_values
# json_escape TEXT: escape TEXT for use inside a JSON string.
json_escape() {
local text="$1"
text="${text//\\/\\\\}"
text="${text//\"/\\\"}"
text="${text//$'\n'/\\n}"
text="${text//$'\r'/\\r}"
text="${text//$'\t'/\\t}"
printf '%s' "$text"
}
# json_get FILE KEY: print the string, number or boolean value of KEY.
# With jq only the top-level key is read. Without jq the first occurrence of
# the key anywhere in the file is used, which is enough for the flat fields
# the GitHub and Gitea APIs return (name, id, html_url, ...).
json_get() {
local file="$1" key="$2"
[[ $key =~ ^[A-Za-z0-9_]+$ ]] || die "internal error: invalid JSON key"
if ((HAS_JQ)); then
# jq on Windows ends lines with CRLF; strip the CR so values stay clean.
jq -r --arg key "$key" \
'if has($key) and .[$key] != null then .[$key] | tostring else empty end' \
"$file" | tr -d '\r'
else
# grep exits 1 when the key is absent; that is not an error here.
{ grep -o "\"$key\"[[:space:]]*:[[:space:]]*\(\"[^\"]*\"\|[0-9][0-9]*\|true\|false\)" "$file" || true; } |
head -n 1 |
sed -e 's/^[^:]*:[[:space:]]*//' -e 's/^"\(.*\)"$/\1/'
fi
}
# json_has_value FILE KEY VALUE: succeed if the file holds "KEY": "VALUE",
# written with or without a space after the colon.
json_has_value() {
local file="$1" key="$2" value="$3"
grep -Fq "\"$key\":\"$value\"" "$file" ||
grep -Fq "\"$key\": \"$value\"" "$file"
}
# json_values FILE KEY: print every string value of KEY, one per line.
json_values() {
local file="$1" key="$2"
[[ $key =~ ^[A-Za-z0-9_]+$ ]] || die "internal error: invalid JSON key"
# grep exits 1 when the key is absent; that is not an error here.
{ grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$file" || true; } |
sed -e 's/^[^:]*:[[:space:]]*//' -e 's/^"\(.*\)"$/\1/'
}