Implement MIL-007: qc submodules fetched, start through a link, default configuration files #53

Merged
Tirsvad merged 2 commits from mil-007-impl into mil-006-license 2026-10-07 08:17:23 +02:00
4 changed files with 90 additions and 22 deletions
Showing only changes of commit 3204e20cac - Show all commits
+66 -1
View File
@@ -59,13 +59,73 @@ src/create-project.sh --help
Nothing has to be installed system-wide: the script runs from the checkout and Nothing has to be installed system-wide: the script runs from the checkout and
loads its own files from `src/lib/`. 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 `./<repository name>`. 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 ## 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 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 are accepted, and anything else stops the run with a message that names the key
and the line, never the value. 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 ```bash
cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL) cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL)
cp .env.example .env # credentials: keep private 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 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, The script asks for, in this order: repository name, description, visibility,
Gitea owner, whether to also create a GitHub repository (and its owner), the 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 local directory and whether to enable the plan gate (a detail set in
+1 -1
View File
@@ -167,7 +167,7 @@ setup_local_remotes() {
# first, then the now empty directories from the bottom up. # first, then the now empty directories from the bottom up.
remove_workdir() { remove_workdir() {
if [[ -n $WORK && -d $WORK ]]; then 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 {} + find "$WORK" -depth -type d -exec rmdir {} +
fi fi
WORK="" WORK=""
+17 -16
View File
@@ -4,7 +4,7 @@
# --config and --env, else ./config.env and ./.env, else the checkout's. # --config and --env, else ./config.env and ./.env, else the checkout's.
# Sourced by run-tests.sh. # 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 # 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. # 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() { test_an_empty_qc_is_filled_by_a_second_run_and_a_complete_one_is_not_changed() {
setup_hosts setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env" 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" printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir"
run_apply "$answers"$'y\n' run_apply "$answers"$'y\n'
assert_status "first run" 0 "$STATUS" assert_status "first run" 0 "$STATUS"
git_in() { GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" "$@"; } 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 git_in submodule deinit -f qc >/dev/null 2>&1
assert_file_missing "qc emptied" "$dir/framework/qc/qc-business-case.md" 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_status "repair" 0 "$STATUS"
assert_file_exists "qc filled again" "$dir/framework/qc/qc-business-case.md" assert_file_exists "qc filled again" "$dir/framework/qc/qc-business-case.md"
local before after local before after
before="$(git_in status --porcelain)" 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)" after="$(git_in status --porcelain)"
assert_status "second call" 0 "$STATUS" assert_status "second call" 0 "$STATUS"
assert_eq "nothing else changed" "$before" "$after" 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" printf -v answers 'my-app\n\n\nTirSystem\nn\n%s\nn\n' "$dir"
run_apply "$answers"$'y\n' 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 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" # Drop the cached copy of the checklists and refuse local fetches, so they
GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir/framework" config --unset-all submodule.qc.url >/dev/null 2>&1 || true # cannot be fetched again.
run_lib "" "init_framework_submodules '$dir'" local cache="$dir/.git/modules/framework/modules/qc"
if ((STATUS == 0)); then if [[ -d $cache ]]; then
# The earlier alias still wins in this git; remove the working alias instead. find "$cache" \( -type f -o -type l \) -delete
sed -i '/remote\/"\]/,+1d' "$WORK/gitconfig" find "$cache" -depth -type d -exec rmdir {} +
run_lib "" "init_framework_submodules '$dir'"
fi 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_status "fetch fails" 1 "$STATUS"
assert_contains "the command" "$ERR" "git submodule update --init --recursive" assert_contains "the command" "$ERR" "git submodule update --init --recursive"
assert_not_contains "no token" "$ERR" "$FAKE_GITEA_TOKEN" 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" local dir="$WORK/plain"
mkdir -p "$dir" mkdir -p "$dir"
git init -q "$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" 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 make_folder
local bin="$WORK/linkbin" local bin="$WORK/linkbin"
mkdir -p "$bin" 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 if [[ ! -L $bin/repo-foundry ]]; then
printf 'skipped: this shell cannot make symbolic links\n' printf 'skipped: this shell cannot make symbolic links\n'
return 0 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" assert_contains "found its files" "$OUT" "RepoFoundry"
run_from "$bin/repo-foundry" "$FOLDER" "$ANSWERS_GITEA_ONLY"$'y\n' --apply run_from "$bin/repo-foundry" "$FOLDER" "$ANSWERS_GITEA_ONLY"$'y\n' --apply
assert_status "apply through the link" 0 "$STATUS" 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_exists "the project is in the current folder" "$FOLDER/my-app/.git"
assert_file_missing "not in the checkout" "$CHECKOUT/my-app" assert_file_missing "not in the checkout" "$CHECKOUT/my-app"
} }
@@ -207,12 +208,12 @@ test_a_link_to_a_link_is_followed() {
make_folder make_folder
local bin="$WORK/linkbin" local bin="$WORK/linkbin"
mkdir -p "$bin" 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 ]] || { [[ -L $bin/first ]] || {
printf 'skipped: this shell cannot make symbolic links\n' printf 'skipped: this shell cannot make symbolic links\n'
return 0 return 0
} }
(cd "$bin" && ln -s first second) (cd "$bin" && MSYS=winsymlinks:nativestrict ln -s first second)
run_from "$bin/second" "$FOLDER" "" --version run_from "$bin/second" "$FOLDER" "" --version
assert_status "second link" 0 "$STATUS" assert_status "second link" 0 "$STATUS"
} }
+6 -4
View File
@@ -201,13 +201,15 @@ test_default_files_are_in_the_project_root() {
cp "$WORK/config.env" "$WORK/project/config.env" cp "$WORK/config.env" "$WORK/project/config.env"
cp "$WORK/.env" "$WORK/project/.env" cp "$WORK/.env" "$WORK/project/.env"
STATUS=0 STATUS=0
PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" "$WORK/project/src/create-project.sh" \ (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=$? <<<"$ANSWERS_GITHUB" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$?
assert_status "run with the default files" 0 "$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" 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 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=$? <<<"$ANSWERS_GITEA_ONLY" >"$WORK/out.txt" 2>"$WORK/err.txt") || STATUS=$?
assert_status "run from another directory" 0 "$STATUS" assert_status "run from another directory" 0 "$STATUS"
} }