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
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# test-structure.sh - guards for the split of create-project.sh into files
# with one responsibility each. Sourced by run-tests.sh.
# shellcheck disable=SC2016 # snippet and pattern text is literal on purpose
# lib_names: the names of the files in src/lib, one per line.
lib_names() {
local file
for file in "$SRC_DIR"/lib/*.sh; do
file="${file##*/}"
printf '%s\n' "${file%.sh}"
done
}
test_every_library_file_is_loaded_and_nothing_else() {
local loaded on_disk
loaded="$(grep -E '^source "\$SCRIPT_DIR/lib/' "$SCRIPT" | sed -e 's|.*/lib/||' -e 's|\.sh"$||' | sort)"
on_disk="$(lib_names | sort)"
assert_eq "the files that are loaded are the files in src/lib" "$on_disk" "$loaded"
}
test_every_library_file_states_one_responsibility() {
local name line
for name in $(lib_names); do
line="$(head -n 4 "$SRC_DIR/lib/$name.sh" | grep -m 1 "^# $name.sh - " || true)"
check
if [[ -z $line ]]; then
fail "src/lib/$name.sh must name its responsibility in its first lines ('# $name.sh - ...')"
fi
check
if ! grep -q '^# Provides: ' "$SRC_DIR/lib/$name.sh" && [[ $name != constants ]]; then
fail "src/lib/$name.sh does not list what it provides"
fi
done
}
test_no_function_is_defined_twice() {
local duplicates
duplicates="$(cat "$SCRIPT" "$SRC_DIR"/lib/*.sh | grep -oE '^[a-z_]+\(\) \{' | sort | uniq -d)"
assert_eq "each function is defined in exactly one file" "" "$duplicates"
}
test_the_listed_functions_exist_in_their_file() {
local name list fn
for name in $(lib_names); do
list="$(sed -n 's/^# Provides: //p' "$SRC_DIR/lib/$name.sh" | tr -d ',')"
for fn in $list; do
check
if ! grep -q "^$fn() {" "$SRC_DIR/lib/$name.sh"; then
fail "src/lib/$name.sh says it provides $fn but does not define it"
fi
done
done
}
test_library_files_have_no_side_effects_when_sourced_alone() {
# A library file only defines functions and constants: running it by itself
# (after the constants) prints nothing and makes no request.
local name
write_curl_stub
for name in $(lib_names); do
if [[ $name == constants ]]; then
continue
fi
STATUS=0
PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" "$BASH" -c \
'SCRIPT_DIR="$1"; PROJECT_ROOT="$2"; source "$1/lib/constants.sh"; source "$1/lib/$3.sh"' \
_ "$SRC_DIR" "$REPO_ROOT" "$name" >"$WORK/out.txt" 2>"$WORK/err.txt" || STATUS=$?
assert_status "sourcing lib/$name.sh" 0 "$STATUS"
assert_eq "lib/$name.sh prints nothing" "" "$(cat "$WORK/out.txt" "$WORK/err.txt")"
done
assert_eq "no request made by sourcing" "" "$(calls)"
}
test_the_entry_point_is_small_and_holds_no_helpers() {
local helpers
helpers="$(grep -oE '^[a-z_]+\(\) \{' "$SCRIPT" | tr -d '(){ ' | sort | tr '\n' ' ')"
assert_eq "only finish and main live in the entry point" "finish main " "$helpers"
}
test_no_library_file_is_ignored_by_git() {
# The Python template in .gitignore ignores any folder named lib/; a file
# that git ignores would silently be left out of every commit.
local name
for name in $(lib_names); do
check
if git -C "$REPO_ROOT" check-ignore -q --no-index "src/lib/$name.sh"; then
fail "src/lib/$name.sh is ignored by git; check .gitignore (!src/lib)"
fi
done
}