From 1a283430b3451f252c634deeca2ae14a6ce36034 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 7 Oct 2026 13:40:42 +0800 Subject: [PATCH 1/2] WIP MIL-007: fetch the framework's qc, follow links, default configuration files - git submodule update --init --recursive after adding or reusing the framework - Follow links to the script so SCRIPT_DIR and the checkout are the real ones - config.env and .env: --config/--env, else ./ in the working folder, else the checkout's - Name the files used; a file from the working folder needs a yes before any request - MIL-007 accepted; test fixtures for the qc checklists; tests/test-launch.sh Work in progress: the README section is not written yet, and four tests of test-launch.sh fail and two shellcheck warnings remain; they are fixed next. Task: MIL-007#1 Task: MIL-007#2 Refs #47 Refs #48 Refs #51 --- .../mil-007-framework-checklists.md | 4 +- src/create-project.sh | 40 +++- src/lib/cli.sh | 2 + src/lib/config.sh | 47 ++++ src/lib/constants.sh | 10 +- src/lib/framework.sh | 16 +- tests/lib.sh | 6 +- tests/test-launch.sh | 218 ++++++++++++++++++ 8 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 tests/test-launch.sh diff --git a/docs/milestones/mil-007-framework-checklists.md b/docs/milestones/mil-007-framework-checklists.md index c856655..a65722c 100644 --- a/docs/milestones/mil-007-framework-checklists.md +++ b/docs/milestones/mil-007-framework-checklists.md @@ -9,8 +9,8 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | Task 4 renamed so its issue title is unique (the sync matches issues by title) | [be759e3] | -| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | The configuration files default to the working folder's, then the checkout's, confirmed and named (deliverable 3, criteria 9 and 10, tasks 2 to 4) | [0ab5006] | +| 2026-10-07 | Deprecated | Jens Tirsvad Nielsen | S02 | Task 4 renamed so its issue title is unique (the sync matches issues by title) | [be759e3] | +| 2026-10-07 | Accepted | Jens Tirsvad Nielsen | S02 | The configuration files default to the working folder's, then the checkout's, confirmed and named (deliverable 3, criteria 9 and 10, tasks 2 to 4) | [0ab5006] | --- diff --git a/src/create-project.sh b/src/create-project.sh index 1a44148..784a7fb 100644 --- a/src/create-project.sh +++ b/src/create-project.sh @@ -86,18 +86,39 @@ if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4))); fi # Where this script lives; the library files and the project root are found -# from here, never from the current directory. -readonly SCRIPT_FILE="${BASH_SOURCE[0]}" -case "${BASH_SOURCE[0]}" in - */*) script_path_dir="${BASH_SOURCE[0]%/*}" ;; +# from here, never from the current directory. A link to the script (a +# command in a folder on PATH) is followed to the real file, however many +# links lie on the way; readlink without -f exists on Linux, macOS and Git +# Bash alike. +script_path="${BASH_SOURCE[0]}" +script_link_count=0 +while [[ -L $script_path ]]; do + ((++script_link_count <= 40)) || { + printf 'error: too many links when following %s\n' "${BASH_SOURCE[0]}" >&2 + exit 1 + } + script_link_target="$(readlink -- "$script_path")" + case "$script_link_target" in + /*) script_path="$script_link_target" ;; + *) + case "$script_path" in + */*) script_path="${script_path%/*}/$script_link_target" ;; + *) script_path="$script_link_target" ;; + esac + ;; + esac +done +readonly SCRIPT_FILE="$script_path" +case "$script_path" in + */*) script_path_dir="${script_path%/*}" ;; *) script_path_dir="." ;; esac -SCRIPT_DIR="$(cd "$script_path_dir" && pwd)" +SCRIPT_DIR="$(cd -P "$script_path_dir" && pwd -P)" readonly SCRIPT_DIR -unset script_path_dir -# The script lives in src/; the configuration files live one level up, in -# the project root, next to config.env.example and .env.example. -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +unset script_path script_path_dir script_link_count script_link_target +# The script lives in src/; the checkout is one level up, next to +# config.env.example and .env.example. +PROJECT_ROOT="$(cd -P "$SCRIPT_DIR/.." && pwd -P)" readonly PROJECT_ROOT # shellcheck source=lib/constants.sh @@ -163,6 +184,7 @@ main() { trap finish EXIT is_valid_repo_name "$PROJECT_NAME" || die "REPOFOUNDRY_NAME is not a valid project name" + WORKING_FOLDER="$(pwd -P)" parse_args "$@" check_tools setup_temp_dir diff --git a/src/lib/cli.sh b/src/lib/cli.sh index 5fa862d..d14de76 100644 --- a/src/lib/cli.sh +++ b/src/lib/cli.sh @@ -9,6 +9,8 @@ usage() { cat < 0)) || return 0 + say "The working folder supplies: ${files[*]}" + say "Gitea would be ${CONFIG[GITEA_URL]}; the token from the credentials file is sent there." + prompt_yes_no "Use ${files[*]}" n + ((REPLY)) || die "stopped before any request: choose the files with --config and --env, or run from the checkout" +} + load_configuration() { + locate_config_file config.env --config CONFIG_FILE CONFIG_ORIGIN + locate_config_file .env --env ENV_FILE ENV_ORIGIN + say "Config file : $CONFIG_FILE ($(origin_words "$CONFIG_ORIGIN"))" + say "Credentials file: $ENV_FILE ($(origin_words "$ENV_ORIGIN"))" parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG validate_config + confirm_folder_files parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS validate_credentials warn_if_env_unsafe "$ENV_FILE" diff --git a/src/lib/constants.sh b/src/lib/constants.sh index 959b60d..621928c 100644 --- a/src/lib/constants.sh +++ b/src/lib/constants.sh @@ -37,8 +37,14 @@ readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL GITHUB_OWNER PROJECT_DIRECTORY ENABLE_PLAN_GATE PROJECT_LICENSE) readonly CREDENTIAL_KEYS=(GITHUB_PAT GITHUB_USER GITEA_TOKEN) -CONFIG_FILE="$PROJECT_ROOT/config.env" -ENV_FILE="$PROJECT_ROOT/.env" +# The configuration files: named by --config and --env, or chosen by +# locate_config_file. The origin is named, folder or checkout. +CONFIG_FILE="" +ENV_FILE="" +CONFIG_ORIGIN="" +ENV_ORIGIN="" +# The folder the Maintainer started the script in. +WORKING_FOLDER="" TMP_DIR="" HAS_JQ=0 HTTP_STATUS=0 diff --git a/src/lib/framework.sh b/src/lib/framework.sh index 6005b4b..ee25452 100644 --- a/src/lib/framework.sh +++ b/src/lib/framework.sh @@ -4,7 +4,7 @@ # # Part of create-project.sh: sourced by it, never run on its own. # -# Provides: is_framework_skipped, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates +# Provides: is_framework_skipped, init_framework_submodules, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates # is_framework_skipped: succeed when the framework steps are left out because # SSH to Gitea is not available (the Maintainer agreed to that). @@ -12,6 +12,18 @@ is_framework_skipped() { [[ ${STATE[skip_framework]:-0} == 1 ]] } +# init_framework_submodules DIR: fetch the submodules the framework holds +# itself (the qc checklists). Without a submodule of its own this changes +# nothing. A failure names the command to run by hand. +init_framework_submodules() { + local dir="$1" err + make_temp_file + err="$REPLY" + if ! git_project "$dir" submodule update -q --init --recursive 2>"$err"; then + die "the framework was added but git could not fetch its own submodules (the qc checklists). Run in $dir: git submodule update --init --recursive. Git said: $(head -n 2 "$err" | tr '\n' ' ')" + fi +} + # add_framework: git submodule add of the framework as "framework". SSH is # needed for it; a failure says how to test the access. add_framework() { @@ -27,6 +39,7 @@ add_framework() { if [[ $existing != "$url" ]]; then die "'framework' already exists in $dir and is not the framework submodule ($url)" fi + init_framework_submodules "$dir" finish_step "$label" "reused" "$url (already a submodule)" return 0 fi @@ -35,6 +48,7 @@ add_framework() { if ! git_project "$dir" submodule add -q "$url" framework 2>"$err"; then die "git could not add the framework from $url. Check the SSH access first: ssh -p ${CONFIG[GITEA_SSH_PORT]} -T git@$(gitea_host). Git said: $(head -n 2 "$err" | tr '\n' ' ')" fi + init_framework_submodules "$dir" finish_step "$label" "created" "$url" } diff --git a/tests/lib.sh b/tests/lib.sh index c3e8641..dff2e91 100644 --- a/tests/lib.sh +++ b/tests/lib.sh @@ -120,12 +120,15 @@ write_gitconfig() { insteadOf = https://git.example.test/ [url "file://$WORK/remote/"] insteadOf = ssh://git@git.example.test:10022/ +[url "file://$WORK/remote/"] + insteadOf = ssh://git@git.tirsystem.com:10022/ EOF } # ensure_shared_remotes: build, once per run of the suite, the local bare # repositories that stand in for Gitea (TirSystem/my-app.git, holding the -# license commit) and for the framework (a copy of the real one). +# license commit), for the framework and for the checklists the framework +# holds as its own submodule (copies of the real ones). ensure_shared_remotes() { if [[ -n $SHARED_REMOTES && -d $SHARED_REMOTES ]]; then return 0 @@ -133,6 +136,7 @@ ensure_shared_remotes() { SHARED_REMOTES="$(mktemp -d "${TMPDIR:-/tmp}/repofoundry-remotes.XXXXXX")" mkdir -p "$SHARED_REMOTES/TirSystem" git clone -q --bare "$REPO_ROOT/framework" "$SHARED_REMOTES/TirSystem/SQA-QC-Framework.git" + git clone -q --bare "$REPO_ROOT/framework/qc" "$SHARED_REMOTES/TirSystem/SQA-QC-Checklists.git" git init -q --bare "$SHARED_REMOTES/TirSystem/my-app.git" git init -q "$SHARED_REMOTES/seed" git -C "$SHARED_REMOTES/seed" symbolic-ref HEAD refs/heads/main diff --git a/tests/test-launch.sh b/tests/test-launch.sh new file mode 100644 index 0000000..dfa1293 --- /dev/null +++ b/tests/test-launch.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# test-launch.sh - tests for MIL-007: the framework's own submodules (qc) are +# fetched, the script starts through a link, and the configuration files are +# --config and --env, else ./config.env and ./.env, else the checkout's. +# Sourced by run-tests.sh. + +# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose + +# make_checkout: a copy of the script's own files with config.env and .env +# beside them, standing in for the checkout; the path is in CHECKOUT. +make_checkout() { + CHECKOUT="$(cd "$WORK" && pwd -P)/checkout" + mkdir -p "$CHECKOUT" + cp -R "$SRC_DIR" "$CHECKOUT/src" + cp "$WORK/config.env" "$CHECKOUT/config.env" + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$CHECKOUT/.env" +} + +# make_folder: an empty working folder; the path is in FOLDER. +make_folder() { + FOLDER="$(cd "$WORK" && pwd -P)/folder" + mkdir -p "$FOLDER" +} + +# run_from SCRIPT_PATH DIR INPUT ARGS...: run the script with DIR as the +# current folder. +run_from() { + local script="$1" dir="$2" input="$3" + shift 3 + STATUS=0 + (cd "$dir" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" \ + REPOFOUNDRY_SYNC_WAIT=0 GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 \ + "$BASH" "$script" "$@" <<<"$input" >"$WORK/out.txt" 2>"$WORK/err.txt") || + STATUS=$? + OUT="$(cat "$WORK/out.txt")" + ERR="$(cat "$WORK/err.txt")" +} + +# --------------------------------------------------- the framework's qc + +test_the_framework_checklists_are_fetched() { + setup_hosts + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" + local dir="$WORK/project" answers + printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir" + run_apply "$answers"$'y\n' + assert_status "apply" 0 "$STATUS" + assert_file_exists "the framework" "$dir/framework/README.md" + assert_file_exists "qc is filled" "$dir/framework/qc/qc-business-case.md" + assert_eq "no submodule is left uninitialised" "" "$(GIT_CONFIG_GLOBAL="$WORK/gitconfig" git -C "$dir" submodule status --recursive | grep '^-' || true)" +} + +test_an_empty_qc_is_filled_by_a_second_run_and_a_complete_one_is_not_changed() { + setup_hosts + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" + local dir="$WORK/project" answers git_in + printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir" + run_apply "$answers"$'y\n' + assert_status "first run" 0 "$STATUS" + git_in() { GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" "$@"; } + git_in submodule deinit -f qc >/dev/null 2>&1 + assert_file_missing "qc emptied" "$dir/framework/qc/qc-business-case.md" + run_lib "" "init_framework_submodules '$dir'" + assert_status "repair" 0 "$STATUS" + assert_file_exists "qc filled again" "$dir/framework/qc/qc-business-case.md" + local before after + before="$(git_in status --porcelain)" + run_lib "" "init_framework_submodules '$dir'" + after="$(git_in status --porcelain)" + assert_status "second call" 0 "$STATUS" + assert_eq "nothing else changed" "$before" "$after" +} + +test_a_failed_qc_fetch_names_the_command_to_run_by_hand() { + setup_hosts + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" + local dir="$WORK/project" answers + printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir" + run_apply "$answers"$'y\n' + GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" submodule deinit -f qc >/dev/null 2>&1 + printf '[url "file://%s/nowhere/"]\n\tinsteadOf = ssh://git@git.tirsystem.com:10022/\n' "$WORK" >>"$WORK/gitconfig" + GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" config --unset-all submodule.qc.url >/dev/null 2>&1 || true + run_lib "" "init_framework_submodules '$dir'" + if ((STATUS == 0)); then + # The earlier alias still wins in this git; remove the working alias instead. + sed -i '/remote\/"\]/,+1d' "$WORK/gitconfig" + run_lib "" "init_framework_submodules '$dir'" + fi + assert_status "fetch fails" 1 "$STATUS" + assert_contains "the command" "$ERR" "git submodule update --init --recursive" + assert_not_contains "no token" "$ERR" "$FAKE_GITEA_TOKEN" +} + +test_a_framework_without_a_submodule_of_its_own_is_not_a_failure() { + setup_hosts + local dir="$WORK/plain" + mkdir -p "$dir" + git init -q "$dir" + run_lib "" "init_framework_submodules '$dir'" + assert_status "nothing to fetch" 0 "$STATUS" +} + +# ------------------------------------------------ the configuration files + +test_files_in_the_working_folder_are_used_after_a_yes() { + setup_hosts + make_checkout + make_folder + cp "$WORK/config.env" "$FOLDER/config.env" + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$FOLDER/.env" + run_from "$SCRIPT" "$FOLDER" $'y\n'"$ANSWERS_GITEA_ONLY" + assert_status "folder files" 0 "$STATUS" + assert_contains "config named" "$OUT" "Config file : $FOLDER/config.env (from the working folder)" + assert_contains "credentials named" "$OUT" "Credentials file: $FOLDER/.env (from the working folder)" + assert_contains "the address is named" "$OUT" "Gitea would be https://git.example.test" + assert_contains "asked" "$ERR" "Use $FOLDER/config.env $FOLDER/.env (y/n) [n]" +} + +test_files_in_the_working_folder_are_not_used_without_a_yes() { + setup_hosts + make_checkout + make_folder + cp "$WORK/config.env" "$FOLDER/config.env" + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$FOLDER/.env" + rm -f "$WORK/curl.calls" + run_from "$SCRIPT" "$FOLDER" $'\n'"$ANSWERS_GITEA_ONLY" + assert_status "default no" 1 "$STATUS" + assert_contains "stopped" "$ERR" "stopped before any request" + assert_eq "no request to any host" "" "$(calls)" +} + +test_named_files_win_and_are_not_confirmed() { + setup_hosts + make_checkout + make_folder + cp "$WORK/config.env" "$FOLDER/config.env" + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$FOLDER/.env" + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" + run_from "$SCRIPT" "$FOLDER" "$ANSWERS_GITEA_ONLY" --config "$WORK/config.env" --env "$WORK/.env" + assert_status "named files" 0 "$STATUS" + assert_contains "named" "$OUT" "Config file : $WORK/config.env (named on the command line)" + assert_not_contains "no confirmation" "$OUT" "The working folder supplies" +} + +test_the_checkouts_files_are_used_when_the_folder_has_none() { + setup_hosts + make_checkout + make_folder + run_from "$CHECKOUT/src/create-project.sh" "$FOLDER" "$ANSWERS_GITEA_ONLY" + assert_status "checkout files" 0 "$STATUS" + assert_contains "config named" "$OUT" "Config file : $CHECKOUT/config.env (from the checkout)" + assert_contains "credentials named" "$OUT" "Credentials file: $CHECKOUT/.env (from the checkout)" + assert_not_contains "no confirmation" "$OUT" "The working folder supplies" +} + +test_each_file_is_chosen_on_its_own() { + setup_hosts + make_checkout + make_folder + printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$FOLDER/.env" + run_from "$CHECKOUT/src/create-project.sh" "$FOLDER" $'y\n'"$ANSWERS_GITEA_ONLY" + assert_status "mixed" 0 "$STATUS" + assert_contains "config from the checkout" "$OUT" "Config file : $CHECKOUT/config.env (from the checkout)" + assert_contains "credentials from the folder" "$OUT" "Credentials file: $FOLDER/.env (from the working folder)" + assert_contains "asked about the folder file only" "$ERR" "Use $FOLDER/.env (y/n) [n]" +} + +test_files_found_nowhere_stop_before_any_request_and_name_both_places() { + setup_hosts + make_checkout + make_folder + rm -f "$CHECKOUT/.env" "$WORK/curl.calls" + run_from "$CHECKOUT/src/create-project.sh" "$FOLDER" "$ANSWERS_GITEA_ONLY" + assert_status "no .env anywhere" 1 "$STATUS" + assert_contains "working folder named" "$ERR" "working folder ($FOLDER)" + assert_contains "checkout named" "$ERR" "checkout ($CHECKOUT)" + assert_contains "the option" "$ERR" "--env FILE" + assert_eq "no request to any host" "" "$(calls)" +} + +# ----------------------------------------------------------- a link to it + +test_the_script_runs_through_a_link_and_creates_the_project_in_the_current_folder() { + setup_hosts + make_checkout + make_folder + local bin="$WORK/linkbin" + mkdir -p "$bin" + ln -s "$CHECKOUT/src/create-project.sh" "$bin/repo-foundry" 2>/dev/null || true + if [[ ! -L $bin/repo-foundry ]]; then + printf 'skipped: this shell cannot make symbolic links\n' + return 0 + fi + run_from "$bin/repo-foundry" "$FOLDER" "" --version + assert_status "version through the link" 0 "$STATUS" + assert_contains "found its files" "$OUT" "RepoFoundry" + run_from "$bin/repo-foundry" "$FOLDER" "$ANSWERS_GITEA_ONLY"$'y\n' --apply + assert_status "apply through the link" 0 "$STATUS" + assert_contains "the checkout's files" "$OUT" "Config file : $CHECKOUT/config.env (from the checkout)" + assert_file_exists "the project is in the current folder" "$FOLDER/my-app/.git" + assert_file_missing "not in the checkout" "$CHECKOUT/my-app" +} + +test_a_link_to_a_link_is_followed() { + setup_hosts + make_checkout + make_folder + local bin="$WORK/linkbin" + mkdir -p "$bin" + ln -s "$CHECKOUT/src/create-project.sh" "$bin/first" 2>/dev/null || true + [[ -L $bin/first ]] || { + printf 'skipped: this shell cannot make symbolic links\n' + return 0 + } + (cd "$bin" && ln -s first second) + run_from "$bin/second" "$FOLDER" "" --version + assert_status "second link" 0 "$STATUS" +} -- 2.54.0 From 3204e20cac57edae3b48ebcab11ca04b044d045f Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 7 Oct 2026 14:09:27 +0800 Subject: [PATCH 2/2] MIL-007: fix the new tests and document starting from any folder - README: start from the folder where the project is created, make the script a global command, where config.env and .env are read from and the confirmation - Tests: qc tests set up the temp directory and force a real failure; the default-files test runs from a folder without files of its own; the link test allows for path aliases; the work directory cleanup deletes links Task: MIL-007#3 Task: MIL-007#4 Refs #49 Refs #51 --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++- tests/lib.sh | 2 +- tests/test-launch.sh | 33 +++++++++++---------- tests/test-security.sh | 10 ++++--- 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 54a48c5..95fcdb5 100644 --- a/README.md +++ b/README.md @@ -59,13 +59,73 @@ src/create-project.sh --help Nothing has to be installed system-wide: the script runs from the checkout and loads its own files from `src/lib/`. +### Run it from the folder where the project is to be created + +The new project is created under the folder you start the script in: the +default directory is `./`. Go to the folder that should hold +the project, then start the script from there, by its path or by a global +command (below): + +```bash +cd ~/work # the folder that will hold my-app +~/src/RepoFoundry/src/create-project.sh # creates ~/work/my-app +``` + +### Make it a global command + +Put a link to the script in a folder that is on your `PATH`. The script +follows the link to the checkout, so it still finds its own files there. + +Linux, macOS and Git Bash on Windows (run this once, from the checkout): + +```bash +mkdir -p ~/bin +ln -s "$PWD/src/create-project.sh" ~/bin/repo-foundry +``` + +If `~/bin` is not on your `PATH` yet, add it and open a new shell: + +```bash +echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc # ~/.zshrc on macOS +``` + +Check that it works, from any folder: + +```bash +cd ~/work +repo-foundry --version # prints the name and version +repo-foundry # a dry run that creates nothing +``` + +On Windows, Git Bash makes a *copy* instead of a link unless symbolic links +are allowed (Developer Mode, or an administrator shell). Either allow them and +run `export MSYS=winsymlinks:nativestrict` before the `ln -s`, or use an alias +in `~/.bashrc`, which works because the script finds its own folder: + +```bash +alias repo-foundry='bash /c/Users/me/RepoFoundry/src/create-project.sh' +``` + ## Configuration -The script reads two plain files from the project root. They are **parsed, +The script reads two plain files. They are **parsed, never executed** (`source` is not used): only `KEY=VALUE` lines with known keys are accepted, and anything else stops the run with a message that names the key and the line, never the value. +Each file is chosen on its own, in this order: + +1. the file named with `--config` or `--env`; +2. `./config.env` or `./.env` in the folder you start the script in; +3. `config.env` or `.env` in the checkout (next to `src/`). + +The script names the files it uses before it contacts any host. A file taken +from the folder you started in is also confirmed: the script shows the file +names and the Gitea address and asks for a yes (default no) before the first +request, because a `config.env` in a folder you do not control could point +Gitea at another host and so send your token there. A file you name with +`--config` or `--env`, or the checkout's own, is not asked about. + ```bash cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL) cp .env.example .env # credentials: keep private @@ -151,6 +211,11 @@ src/create-project.sh --apply # creates everything after a final yes src/create-project.sh --config /path/to/config.env --env /path/to/.env ``` +With a global command (see [Installation](#installation)) the same commands are +`repo-foundry`, `repo-foundry --apply` and so on, started from the folder that +should hold the project. Without `--config` and `--env` the files are looked +for as described under [Configuration](#configuration). + The script asks for, in this order: repository name, description, visibility, Gitea owner, whether to also create a GitHub repository (and its owner), the local directory and whether to enable the plan gate (a detail set in diff --git a/tests/lib.sh b/tests/lib.sh index dff2e91..80f8764 100644 --- a/tests/lib.sh +++ b/tests/lib.sh @@ -167,7 +167,7 @@ setup_local_remotes() { # first, then the now empty directories from the bottom up. remove_workdir() { if [[ -n $WORK && -d $WORK ]]; then - find "$WORK" \( -type f -o -type p \) -delete + find "$WORK" \( -type f -o -type p -o -type l \) -delete find "$WORK" -depth -type d -exec rmdir {} + fi WORK="" diff --git a/tests/test-launch.sh b/tests/test-launch.sh index dfa1293..883e665 100644 --- a/tests/test-launch.sh +++ b/tests/test-launch.sh @@ -4,7 +4,7 @@ # --config and --env, else ./config.env and ./.env, else the checkout's. # Sourced by run-tests.sh. -# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose +# shellcheck disable=SC2016,SC2153 # snippet and fixture text is literal on purpose; SCRIPT comes from lib.sh # make_checkout: a copy of the script's own files with config.env and .env # beside them, standing in for the checkout; the path is in CHECKOUT. @@ -53,19 +53,19 @@ test_the_framework_checklists_are_fetched() { test_an_empty_qc_is_filled_by_a_second_run_and_a_complete_one_is_not_changed() { setup_hosts printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" - local dir="$WORK/project" answers git_in + local dir="$WORK/project" answers printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir" run_apply "$answers"$'y\n' assert_status "first run" 0 "$STATUS" git_in() { GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" "$@"; } git_in submodule deinit -f qc >/dev/null 2>&1 assert_file_missing "qc emptied" "$dir/framework/qc/qc-business-case.md" - run_lib "" "init_framework_submodules '$dir'" + run_lib "" "setup_temp_dir; init_framework_submodules '$dir'" assert_status "repair" 0 "$STATUS" assert_file_exists "qc filled again" "$dir/framework/qc/qc-business-case.md" local before after before="$(git_in status --porcelain)" - run_lib "" "init_framework_submodules '$dir'" + run_lib "" "setup_temp_dir; init_framework_submodules '$dir'" after="$(git_in status --porcelain)" assert_status "second call" 0 "$STATUS" assert_eq "nothing else changed" "$before" "$after" @@ -78,14 +78,15 @@ test_a_failed_qc_fetch_names_the_command_to_run_by_hand() { printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir" run_apply "$answers"$'y\n' GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" submodule deinit -f qc >/dev/null 2>&1 - printf '[url "file://%s/nowhere/"]\n\tinsteadOf = ssh://git@git.tirsystem.com:10022/\n' "$WORK" >>"$WORK/gitconfig" - GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" config --unset-all submodule.qc.url >/dev/null 2>&1 || true - run_lib "" "init_framework_submodules '$dir'" - if ((STATUS == 0)); then - # The earlier alias still wins in this git; remove the working alias instead. - sed -i '/remote\/"\]/,+1d' "$WORK/gitconfig" - run_lib "" "init_framework_submodules '$dir'" + # Drop the cached copy of the checklists and refuse local fetches, so they + # cannot be fetched again. + local cache="$dir/.git/modules/framework/modules/qc" + if [[ -d $cache ]]; then + find "$cache" \( -type f -o -type l \) -delete + find "$cache" -depth -type d -exec rmdir {} + fi + printf '[protocol "file"]\n\tallow = never\n' >>"$WORK/gitconfig" + run_lib "" "setup_temp_dir; init_framework_submodules '$dir'" assert_status "fetch fails" 1 "$STATUS" assert_contains "the command" "$ERR" "git submodule update --init --recursive" assert_not_contains "no token" "$ERR" "$FAKE_GITEA_TOKEN" @@ -96,7 +97,7 @@ test_a_framework_without_a_submodule_of_its_own_is_not_a_failure() { local dir="$WORK/plain" mkdir -p "$dir" git init -q "$dir" - run_lib "" "init_framework_submodules '$dir'" + run_lib "" "setup_temp_dir; init_framework_submodules '$dir'" assert_status "nothing to fetch" 0 "$STATUS" } @@ -186,7 +187,7 @@ test_the_script_runs_through_a_link_and_creates_the_project_in_the_current_folde make_folder local bin="$WORK/linkbin" mkdir -p "$bin" - ln -s "$CHECKOUT/src/create-project.sh" "$bin/repo-foundry" 2>/dev/null || true + MSYS=winsymlinks:nativestrict ln -s "$CHECKOUT/src/create-project.sh" "$bin/repo-foundry" 2>/dev/null || true if [[ ! -L $bin/repo-foundry ]]; then printf 'skipped: this shell cannot make symbolic links\n' return 0 @@ -196,7 +197,7 @@ test_the_script_runs_through_a_link_and_creates_the_project_in_the_current_folde assert_contains "found its files" "$OUT" "RepoFoundry" run_from "$bin/repo-foundry" "$FOLDER" "$ANSWERS_GITEA_ONLY"$'y\n' --apply assert_status "apply through the link" 0 "$STATUS" - assert_contains "the checkout's files" "$OUT" "Config file : $CHECKOUT/config.env (from the checkout)" + assert_contains "the checkout's files" "$OUT" "/checkout/config.env (from the checkout)" assert_file_exists "the project is in the current folder" "$FOLDER/my-app/.git" assert_file_missing "not in the checkout" "$CHECKOUT/my-app" } @@ -207,12 +208,12 @@ test_a_link_to_a_link_is_followed() { make_folder local bin="$WORK/linkbin" mkdir -p "$bin" - ln -s "$CHECKOUT/src/create-project.sh" "$bin/first" 2>/dev/null || true + MSYS=winsymlinks:nativestrict ln -s "$CHECKOUT/src/create-project.sh" "$bin/first" 2>/dev/null || true [[ -L $bin/first ]] || { printf 'skipped: this shell cannot make symbolic links\n' return 0 } - (cd "$bin" && ln -s first second) + (cd "$bin" && MSYS=winsymlinks:nativestrict ln -s first second) run_from "$bin/second" "$FOLDER" "" --version assert_status "second link" 0 "$STATUS" } diff --git a/tests/test-security.sh b/tests/test-security.sh index 950f546..b5d081a 100644 --- a/tests/test-security.sh +++ b/tests/test-security.sh @@ -201,13 +201,15 @@ test_default_files_are_in_the_project_root() { cp "$WORK/config.env" "$WORK/project/config.env" cp "$WORK/.env" "$WORK/project/.env" STATUS=0 - PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ - <<<"$ANSWERS_GITHUB" >"$WORK/out.txt" 2>"$WORK/err.txt" || STATUS=$? + (cd "$WORK/project" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ + <<<"$ANSWERS_GITHUB" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$? assert_status "run with the default files" 0 "$STATUS" assert_contains "found config.env in the project root" "$(cat "$WORK/out.txt")" "https://git.example.test/TirSystem/my-app" - # Run from another directory: the defaults follow the script, not the cwd. + # Run from another folder that holds no files of its own: the checkout's + # files are used; the defaults follow the script, not the current folder. + mkdir -p "$WORK/elsewhere" STATUS=0 - (cd "$WORK" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ + (cd "$WORK/elsewhere" && PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ <<<"$ANSWERS_GITEA_ONLY" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$? assert_status "run from another directory" 0 "$STATUS" } -- 2.54.0