Add the RepoFoundry foundation: config parsing, HTTP helper, prompts, tests

create-project.sh validates config.env and .env (parsed, never sourced),
checks the required tools, asks for the project details and prints a
summary. It makes no network call and no change on disk yet.

- config.env.example and .env.example hold placeholders only
- tokens go through a private curl config file, never the command line
- tests run in private directories with a stub curl; shellcheck and shfmt
  are part of tests/run-tests.sh

Task: MIL-001#1
Task: MIL-001#2
Task: MIL-001#3
Task: MIL-001#4
Task: MIL-001#5
Task: MIL-001#6
Closes #3
Closes #4
Closes #5
Closes #6
Closes #7
Closes #8

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 13:59:08 +08:00
co-authored by Claude Sonnet 5.5
parent d727f7ae60
commit 102dd2473d
12 changed files with 1510 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# test-security.sh - end-to-end tests: no token in any output, no network
# call, no change on disk, clean temporary files, safe failure paths
# (MIL-001 Go/No-Go criteria 2 to 4). Sourced by run-tests.sh.
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
readonly ANSWERS_GITHUB=$'my-app\nA test app\n\nTirSystem\ny\nacme-org\n\nn\n'
readonly ANSWERS_GITEA_ONLY=$'my-app\n\n\nTirSystem\nn\n\nn\n'
# listing: all files under the work directory except the test's own captures.
listing() {
find "$WORK" -type f ! -name out.txt ! -name err.txt ! -name snippet.sh \
! -name 'curl.*' | sort
}
test_full_run_with_github() {
write_fixtures
write_curl_stub
local before after
before="$(listing)"
run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env"
after="$(listing)"
assert_status "full run" 0 "$STATUS"
assert_contains "summary" "$OUT" "nothing has been created yet"
assert_contains "Gitea link derived from config" "$OUT" "https://git.example.test/TirSystem/my-app"
assert_contains "GitHub link uses the chosen organization" "$OUT" "https://github.com/acme-org/my-app"
assert_contains "AGPL noted" "$OUT" "AGPL license applied"
assert_contains "credential state" "$OUT" "GITEA_TOKEN set, GITHUB_PAT set"
assert_not_contains "no Gitea token in output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_not_contains "no GitHub token in output" "$OUT$ERR" "$FAKE_GITHUB_PAT"
assert_file_missing "no network call" "$WORK/curl.args"
assert_eq "no file created or changed" "$before" "$after"
assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)"
}
test_full_run_without_github() {
write_fixtures
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_cli "$ANSWERS_GITEA_ONLY" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "Gitea-only run needs no GitHub credentials" 0 "$STATUS"
assert_contains "GitHub not used" "$OUT" "GitHub : not used"
assert_not_contains "no AGPL line" "$OUT" "AGPL"
assert_contains "GITHUB_PAT not set" "$OUT" "GITHUB_PAT not set"
}
test_github_chosen_without_credentials_fails() {
write_fixtures
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "missing GitHub credentials" 1 "$STATUS"
assert_contains "names the key" "$ERR" "GITHUB_PAT is missing"
assert_not_contains "no token in the error" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)"
}
test_error_messages_never_contain_the_value() {
write_fixtures
printf 'GITEA_TOKEN=S3CR3T\nGITHUB_PAT=%s\n' "$FAKE_GITHUB_PAT" >"$WORK/.env"
run_cli "" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "invalid token" 1 "$STATUS"
assert_contains "says what is wrong" "$ERR" "GITEA_TOKEN"
assert_not_contains "invalid value not echoed" "$OUT$ERR" "S3CR3T"
assert_not_contains "valid PAT not echoed" "$OUT$ERR" "$FAKE_GITHUB_PAT"
printf 'this line holds %s as text\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
run_cli "" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "malformed line" 1 "$STATUS"
assert_not_contains "malformed line not echoed" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
}
test_unknown_credential_key_in_config_is_rejected() {
write_fixtures
printf 'GITEA_URL=https://git.example.test\nGITHUB_PAT=%s\n' "$FAKE_GITHUB_PAT" >"$WORK/config.env"
run_cli "" --config "$WORK/config.env" --env "$WORK/.env"
assert_status "credential in config.env" 1 "$STATUS"
assert_contains "explains" "$ERR" "keep it in the .env file only"
assert_not_contains "token not echoed" "$OUT$ERR" "$FAKE_GITHUB_PAT"
}
test_redaction() {
run_lib "" "SECRET_VALUES=('$FAKE_GITEA_TOKEN')
say 'token is $FAKE_GITEA_TOKEN here'
warn 'also $FAKE_GITEA_TOKEN'
die 'and $FAKE_GITEA_TOKEN'"
assert_status "die exits 1" 1 "$STATUS"
assert_eq "say redacts" "token is [redacted] here" "$OUT"
assert_not_contains "stderr redacted" "$ERR" "$FAKE_GITEA_TOKEN"
assert_contains "die message" "$ERR" "error: and [redacted]"
}
test_usage_errors() {
run_cli "" --bogus
assert_status "unknown option" 2 "$STATUS"
assert_contains "says so" "$ERR" "unknown option: --bogus"
run_cli "" --config
assert_status "option without value" 2 "$STATUS"
run_cli "" --help
assert_status "help" 0 "$STATUS"
assert_contains "help shows usage" "$OUT" "Usage"
assert_contains "help shows exit codes" "$OUT" "Exit codes"
run_cli "" --version
assert_status "version" 0 "$STATUS"
assert_contains "version output" "$OUT" "RepoFoundry 0.1.0"
}
test_warns_when_env_is_not_ignored_by_git() {
write_fixtures
git init -q "$WORK/repo"
cp "$WORK/.env" "$WORK/repo/.env"
cp "$WORK/config.env" "$WORK/repo/config.env"
run_cli "$ANSWERS_GITEA_ONLY" --config "$WORK/repo/config.env" --env "$WORK/repo/.env"
assert_status "still runs" 0 "$STATUS"
assert_contains "warns" "$ERR" "is not ignored by git"
assert_not_contains "no token in the warning" "$ERR" "$FAKE_GITEA_TOKEN"
find "$WORK/repo" -type f -delete
find "$WORK/repo" -depth -type d -exec rmdir {} +
}
test_script_uses_no_unsafe_constructs() {
local code
code="$(grep -vE '^[[:space:]]*#' "$SCRIPT")"
assert_not_contains "no rm -rf" "$code" "rm -rf"
assert_not_contains "no rm -r" "$code" "rm -r "
assert_not_contains "no eval" "$code" "eval "
assert_not_contains "no source" "$code" "source "
assert_not_contains "no dot-source" "$code" $'\n. '
assert_not_contains "no set -x" "$code" "set -x"
assert_not_contains "no fixed /tmp file" "$code" "/tmp/file"
}