Merge pull request 'Implement MIL-006: project license in config.env; AGPL-3.0 default only for a public GitHub project' (#52) from mil-006-license into mil-006-plan

Reviewed-on: #52
This commit was merged in pull request #52.
This commit is contained in:
2026-10-07 08:18:47 +02:00
23 changed files with 682 additions and 62 deletions
+82 -10
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
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
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
@@ -103,6 +163,7 @@ A detail that is set is used and not asked; the summary marks it with
| `GITHUB_OWNER` | GitHub user or organization | letters, digits, `-`; used only when GitHub is used |
| `PROJECT_DIRECTORY` | local directory | not empty, not starting with `-` |
| `ENABLE_PLAN_GATE` | enable the plan gate | `yes` or `no` |
| `PROJECT_LICENSE` | license of the project | a Gitea license key (letters, digits, `.`, `+`, `-`; at most 64), such as `AGPL-3.0` or `MIT`, or `none` |
- A key that is present counts as set, even when its value is empty. Only
`PROJECT_DESCRIPTION` may be empty (no description); an empty value for any
@@ -111,7 +172,12 @@ A detail that is set is used and not asked; the summary marks it with
key. The script never falls back to asking for it.
- `USE_GITHUB=no` skips the GitHub owner and every GitHub step; a
`GITHUB_OWNER` set at the same time is ignored, with a warning.
- Only these eight details can be set. The confirmations stay questions that
- `PROJECT_LICENSE` is never asked. When set, that license is put on the Gitea
repository with or without GitHub, and `none` means no license. When absent,
AGPL-3.0 is applied only if GitHub is used **and** the project is public;
a private project, or one without GitHub, gets no license. The Gitea server
must offer the license, or the run stops before anything is created.
- Only these nine details can be set. The confirmations stay questions that
default to no: create now, reusing an existing repository, an existing
directory, `core.hooksPath` and replacing a template file.
- These keys are accepted in `config.env` only, never in `.env`.
@@ -119,7 +185,7 @@ A detail that is set is used and not asked; the summary marks it with
value there. Put a description that contains ` #` in double quotes, for
example `PROJECT_DESCRIPTION="Tool for #mirrors"`.
With all eight set, a run asks only the confirmations:
With all of them set, a run asks only the confirmations:
```bash
src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
@@ -145,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
@@ -153,8 +224,8 @@ with read-only requests and prints a plan:
```text
Plan:
Gitea repository : create (private) with the AGPL-3.0 license https://git.example.org/Team/my-app
GitHub repository : create (private), empty https://github.com/acme/my-app
Gitea repository : create (public) with the AGPL-3.0 license (default: GitHub and a public project) https://git.example.org/Team/my-app
GitHub repository : create (public), empty https://github.com/acme/my-app
Push mirror : Gitea -> GitHub every 10m0s
Local project : create ./my-app (new directory), git on main, no commit
Local origin : will use SSH (the SSH test passed)
@@ -167,7 +238,7 @@ Without `--apply` that is all that happens. With `--apply` the script asks
"Create these now" (default no) and then creates, in this order:
1. the GitHub repository (empty), if chosen;
2. the Gitea repository (with the AGPL-3.0 license if GitHub was chosen);
2. the Gitea repository (with the license that applies: `PROJECT_LICENSE`, or AGPL-3.0 for a public project with GitHub);
3. the push mirror Gitea -> GitHub, and a request for its first sync;
4. the local directory, `git init` on `main`, the `origin` remote (and `github`
if chosen), and, if the Gitea repository holds the license commit, that
@@ -181,10 +252,11 @@ hooks refuse commits on `main`.
### Choices
- **GitHub or not.** Choosing GitHub also applies the AGPL-3.0 license to the
Gitea repository (so it is not empty) and sets up the mirror. Without GitHub
the Gitea repository is empty and has no license, and `GITHUB_PAT` is not
needed.
- **GitHub or not.** Choosing GitHub sets up the mirror. For a public project it
also applies the AGPL-3.0 license to the Gitea repository (so it is not
empty), unless `PROJECT_LICENSE` says otherwise. A private project with
GitHub gets no license by default. Without GitHub the Gitea repository has
no license unless `PROJECT_LICENSE` sets one, and `GITHUB_PAT` is not needed.
- **Owners.** The Gitea owner and the GitHub owner are chosen separately and
may be a user or an organization. `GITHUB_USER` is only the suggested default
for the GitHub owner prompt; it identifies who authenticates.
+6
View File
@@ -46,6 +46,12 @@ GITEA_API_URL=https://<your gitea instance>/api/v1
#PROJECT_DIRECTORY=./my-project
#ENABLE_PLAN_GATE=no # yes or no
# Optional. The license of the project, as a Gitea license key (AGPL-3.0, MIT,
# ...), or none for no license. It is never asked. When absent, AGPL-3.0 is
# applied only if GitHub is used and the project is public; otherwise there
# is no license. The Gitea server must offer the license.
#PROJECT_LICENSE=AGPL-3.0
# Optional. OWNER/NAME of the SQA-QC-Framework repository on the Gitea server;
# it is added to the new project as a submodule over SSH.
# Default: TirSystem/SQA-QC-Framework.
+2 -2
View File
@@ -9,8 +9,8 @@
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-07 | Proposed | Jens Tirsvad Nielsen | S02 | AGPL-3.0 default only when GitHub is chosen and the project is public (purpose, resolution order, criterion 3, tasks 1 and 4) | [1cd27f7] |
| 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 | Deprecated | Jens Tirsvad Nielsen | S02 | AGPL-3.0 default only when GitHub is chosen and the project is public (purpose, resolution order, criterion 3, tasks 1 and 4) | [1cd27f7] |
| 2026-10-07 | Accepted | Jens Tirsvad Nielsen | S02 | Task 4 renamed so its issue title is unique (the sync matches issues by title) | [be759e3] |
---
@@ -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] |
---
+34 -11
View File
@@ -10,8 +10,9 @@
# repositories and the mirror, then the local project: its directory, git
# repository, remotes (no credential in any address), the framework as a
# submodule, the framework's skills and git hooks (and the plan gate if
# chosen) and its templates. Choosing GitHub also applies the AGPL-3.0
# license to the Gitea repository. No commit is made in the new project.
# chosen) and its templates. The license of the Gitea repository is
# PROJECT_LICENSE in config.env (none means no license); without it AGPL-3.0
# applies only when GitHub is chosen and the project is public. No commit is made in the new project.
#
# Dry run by default
# Without --apply the script only reads from GitHub and Gitea (GET
@@ -85,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
@@ -162,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
+2 -2
View File
@@ -47,9 +47,9 @@ confirm_reuse() {
die "stopped: choose another name or remove the existing repository"
fi
done
if ((${STATE[reuse_gitea]:-0})) && ((PROJECT[has_github])) &&
if ((${STATE[reuse_gitea]:-0})) && [[ -n ${PROJECT[license]} ]] &&
[[ ${STATE[gitea_repo]} == empty ]]; then
warn "the empty Gitea repository is reused as it is: the $AGPL_LICENSE_KEY license is not added to it"
warn "the empty Gitea repository is reused as it is: the ${PROJECT[license]} license is not added to it"
fi
}
+2
View File
@@ -9,6 +9,8 @@
usage() {
cat <<EOF
Usage: ${0##*/} [--apply] [--config FILE] [--env FILE]
Without --config and --env, ./config.env and ./.env in the current folder
are read, then the ones in the checkout.
${0##*/} --help | --version
EOF
}
+48
View File
@@ -142,6 +142,7 @@ validate_project_presets() {
check_preset GITHUB_OWNER is_valid_github_owner "$HINT_GITHUB_OWNER"
check_preset PROJECT_DIRECTORY is_valid_directory "$HINT_DIRECTORY"
check_preset_choice ENABLE_PLAN_GATE yes no
check_preset PROJECT_LICENSE is_valid_license "$HINT_LICENSE"
}
validate_credentials() {
@@ -197,9 +198,56 @@ warn_if_env_unsafe() {
fi
}
# locate_config_file NAME FLAG FILE_VAR ORIGIN_VAR: the file named with FLAG;
# otherwise ./NAME in the working folder; otherwise NAME in the checkout.
# The origin is "named", "folder" or "checkout".
locate_config_file() {
local name="$1" flag="$2"
local -n file_ref="$3" origin_ref="$4"
if [[ -n $file_ref ]]; then
origin_ref="named"
elif [[ $WORKING_FOLDER != "$PROJECT_ROOT" && -f $WORKING_FOLDER/$name ]]; then
file_ref="$WORKING_FOLDER/$name"
origin_ref="folder"
elif [[ -f $PROJECT_ROOT/$name ]]; then
file_ref="$PROJECT_ROOT/$name"
origin_ref="checkout"
else
die "$name is not named with $flag and is in neither the working folder ($WORKING_FOLDER) nor the checkout ($PROJECT_ROOT); name it with $flag FILE"
fi
}
# origin_words ORIGIN: how the origin of a configuration file reads.
origin_words() {
case "$1" in
named) printf 'named on the command line' ;;
folder) printf 'from the working folder' ;;
*) printf 'from the checkout' ;;
esac
}
# confirm_folder_files: a file taken from the working folder may point the
# Gitea address elsewhere and so send the token there, so it is named with
# the address and needs a yes (default no) before the first request.
confirm_folder_files() {
local files=()
[[ $CONFIG_ORIGIN == folder ]] && files+=("$CONFIG_FILE")
[[ $ENV_ORIGIN == folder ]] && files+=("$ENV_FILE")
((${#files[@]} > 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"
+11 -3
View File
@@ -18,6 +18,7 @@ readonly DEFAULT_MIRROR_INTERVAL="10m0s"
readonly DEFAULT_SSH_PORT=10022
readonly DEFAULT_FRAMEWORK_REPO="TirSystem/SQA-QC-Framework"
readonly AGPL_LICENSE_KEY="AGPL-3.0"
readonly NO_LICENSE_WORD="none"
readonly DEFAULT_BRANCH="main"
# What the prompts and the preset keys in config.env both tell the Maintainer
# when a value is refused.
@@ -25,6 +26,7 @@ readonly HINT_REPO_NAME="use letters, digits, '.', '_' or '-' (at most 100), not
readonly HINT_DESCRIPTION="at most $MAX_DESCRIPTION_LENGTH characters and no control characters"
readonly HINT_GITEA_OWNER="use letters, digits, '.', '_' or '-' (at most 39)"
readonly HINT_GITHUB_OWNER="use letters, digits or '-' (at most 39)"
readonly HINT_LICENSE="use a Gitea license key (letters, digits, '.', '+' or '-', at most 64), such as AGPL-3.0 or MIT, or none"
readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters"
readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror"
"Local project" "Framework" "Skills and hooks" "Templates")
@@ -32,11 +34,17 @@ readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror"
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO
PROJECT_NAME PROJECT_DESCRIPTION PROJECT_VISIBILITY GITEA_OWNER USE_GITHUB
GITHUB_OWNER PROJECT_DIRECTORY ENABLE_PLAN_GATE)
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
+15 -1
View File
@@ -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"
}
+3 -1
View File
@@ -72,7 +72,7 @@ checkout_gitea_history() {
}
# create_local_project: the directory, the git repository on main, the
# remotes and, when GitHub is chosen, the license history. No commit is made.
# remotes and, when a license applies, the license history. No commit is made.
create_local_project() {
local label="Local project" dir="${PROJECT[directory]}"
begin_step "$label"
@@ -84,6 +84,8 @@ create_local_project() {
ensure_remote "$dir" origin "$(origin_url)"
if ((PROJECT[has_github])); then
ensure_remote "$dir" github "$(github_remote_url)"
fi
if [[ -n ${PROJECT[license]} ]]; then
checkout_gitea_history "$dir"
fi
finish_step "$label" "created" "$dir (origin over $(origin_protocol))"
+2 -2
View File
@@ -22,8 +22,8 @@ print_plan() {
say "Plan:"
if ((STATE[reuse_gitea])); then
gitea_action="reuse the existing repository (you will be asked to confirm)"
elif ((PROJECT[has_github])); then
gitea_action="create (${PROJECT[visibility]}) with the $AGPL_LICENSE_KEY license"
elif [[ -n ${PROJECT[license]} ]]; then
gitea_action="create (${PROJECT[visibility]}) with the ${PROJECT[license]} license$(license_note)"
else
gitea_action="create (${PROJECT[visibility]}), empty"
fi
+4 -4
View File
@@ -23,8 +23,8 @@ check_gitea_organization() {
check_gitea_license() {
api_call gitea GET /licenses
expect_status "cannot list the licenses of the Gitea server" 200
json_has_value "$HTTP_BODY_FILE" key "$AGPL_LICENSE_KEY" ||
die "the Gitea server does not offer the $AGPL_LICENSE_KEY license"
json_has_value "$HTTP_BODY_FILE" key "${PROJECT[license]}" ||
die "the Gitea server does not offer the ${PROJECT[license]} license"
}
# inspect_repository HOST: record in STATE[HOST_repo] whether the repository
@@ -70,7 +70,7 @@ preflight_gitea() {
check_gitea_organization "$owner" "$login"
STATE[gitea_owner_kind]="organization"
fi
if ((PROJECT[has_github])); then
if [[ -n ${PROJECT[license]} ]]; then
check_gitea_license
fi
inspect_repository gitea
@@ -143,7 +143,7 @@ decide_existing_repositories() {
free) ;;
empty) STATE[reuse_$host]=1 ;;
initial_only)
if [[ $host == gitea ]] && ((PROJECT[has_github])); then
if [[ $host == gitea && -n ${PROJECT[license]} ]]; then
STATE[reuse_$host]=1
else
die "the $(host_label "$host") repository $owner/${PROJECT[name]} already exists and has content; choose another name or remove it first"
+32 -3
View File
@@ -4,7 +4,7 @@
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: preset_detail, collect_project_details, collect_github_details, source_note, yes_no, credential_state, print_summary
# Provides: preset_detail, resolve_license, license_note, collect_project_details, collect_github_details, source_note, yes_no, credential_state, print_summary
# preset_detail KEY NAME: a detail set in config.env is used and not asked;
# the value is returned in REPLY and marked in PRESET[NAME].
@@ -14,6 +14,33 @@ preset_detail() {
PRESET[$2]=1
}
# resolve_license: the license that applies, in PROJECT[license] (empty means
# none). PROJECT_LICENSE in config.env decides, and "none" means no license;
# without it AGPL-3.0 applies only when GitHub is chosen and the project is
# public. The license is never asked.
resolve_license() {
PROJECT[license]=""
if [[ -n ${CONFIG[PROJECT_LICENSE]+set} ]]; then
PRESET[license]=1
if [[ ${CONFIG[PROJECT_LICENSE],,} != "$NO_LICENSE_WORD" ]]; then
PROJECT[license]="${CONFIG[PROJECT_LICENSE]}"
fi
elif ((PROJECT[has_github])) && [[ ${PROJECT[visibility]} == public ]]; then
PROJECT[license]="$AGPL_LICENSE_KEY"
fi
}
# license_note: where the license on the Gitea repository comes from.
license_note() {
if [[ -n ${PRESET[license]:-} ]]; then
source_note license
elif [[ -n ${PROJECT[license]} ]]; then
printf ' (default: GitHub and a public project)'
elif ((PROJECT[has_github])); then
printf ' (no default for a private project)'
fi
}
# Ask for each project detail, except those set in config.env.
collect_project_details() {
preset_detail PROJECT_NAME name ||
@@ -29,6 +56,7 @@ collect_project_details() {
prompt_value "Gitea owner (user or organization)" "" is_valid_gitea_owner "$HINT_GITEA_OWNER"
PROJECT[gitea_owner]="$REPLY"
collect_github_details
resolve_license
preset_detail PROJECT_DIRECTORY directory ||
prompt_value "Local directory" "./${PROJECT[name]}" is_valid_directory "$HINT_DIRECTORY"
PROJECT[directory]="$REPLY"
@@ -46,7 +74,7 @@ collect_github_details() {
if preset_detail USE_GITHUB has_github; then
[[ $REPLY == yes ]] && REPLY=1 || REPLY=0
else
prompt_yes_no "Also create a GitHub repository (applies the AGPL license)" y
prompt_yes_no "Also create a GitHub repository" y
fi
PROJECT[has_github]="$REPLY"
PROJECT[github_owner]=""
@@ -90,10 +118,11 @@ print_summary() {
say " Description : ${PROJECT[description]:-(none)}$(source_note description)"
say " Gitea : ${CONFIG[GITEA_URL]}/${PROJECT[gitea_owner]}/${PROJECT[name]}$(source_note gitea_owner)"
if ((PROJECT[has_github])); then
say " GitHub : ${CONFIG[GITHUB_WEB_URL]}/${PROJECT[github_owner]}/${PROJECT[name]} (AGPL license applied)$(source_note github_owner)"
say " GitHub : ${CONFIG[GITHUB_WEB_URL]}/${PROJECT[github_owner]}/${PROJECT[name]}$(source_note github_owner)"
else
say " GitHub : not used$(source_note has_github)"
fi
say " License : ${PROJECT[license]:-none}$(license_note)"
say " Directory : ${PROJECT[directory]}$(source_note directory)"
say " Plan gate : $(yes_no "${PROJECT[is_plan_gate_enabled]}")$(source_note is_plan_gate_enabled)"
say "Credentials : GITEA_TOKEN $(credential_state GITEA_TOKEN)," \
+2 -2
View File
@@ -18,8 +18,8 @@ repo_body() {
"${PROJECT[name]}" "$description" "$private"
return 0
fi
if ((PROJECT[has_github])); then
extra=',"auto_init":true,"license":"'"$AGPL_LICENSE_KEY"'"'
if [[ -n ${PROJECT[license]} ]]; then
extra=',"auto_init":true,"license":"'"${PROJECT[license]}"'"'
else
extra=',"auto_init":false'
fi
+5
View File
@@ -58,6 +58,11 @@ is_valid_port() {
[[ $1 =~ ^[0-9]{1,5}$ ]] && ((10#$1 >= 1 && 10#$1 <= 65535))
}
# A Gitea license key such as AGPL-3.0 or MIT, or the word none.
is_valid_license() {
[[ -n $1 && ${#1} -le 64 && $1 =~ ^[A-Za-z0-9.+-]+$ ]]
}
# A Go duration such as 10m0s or 8h0m0s, the form Gitea expects.
is_valid_interval() {
[[ -n $1 && $1 =~ ^([0-9]+h)?([0-9]+m)?([0-9]+s)?$ ]]
+16 -2
View File
@@ -21,6 +21,16 @@ readonly FAKE_GITHUB_PAT="ghpFAKEtoken1234567890"
# Answers to the prompts: name, description, visibility, Gitea owner, GitHub
# yes or no, GitHub owner, directory, plan gate.
readonly ANSWERS_GITHUB=$'my-app\nA test app\n\nTirSystem\ny\nacme-org\n\nn\n'
# The same, for a public project (the AGPL-3.0 default applies with GitHub).
readonly ANSWERS_GITHUB_PUBLIC=$'my-app
A test app
public
TirSystem
y
acme-org
n
'
readonly ANSWERS_GITEA_ONLY=$'my-app\n\n\nTirSystem\nn\n\nn\n'
SHARED_REMOTES=""
@@ -110,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
@@ -123,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
@@ -153,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=""
+23 -9
View File
@@ -31,7 +31,7 @@ test_dry_run_prints_the_plan_and_only_reads() {
setup_hosts
run_dry "$ANSWERS_GITHUB"
assert_status "dry run" 0 "$STATUS"
assert_contains "Gitea plan" "$OUT" "Gitea repository : create (private) with the AGPL-3.0 license https://git.example.test/TirSystem/my-app"
assert_contains "Gitea plan" "$OUT" "Gitea repository : create (private), empty https://git.example.test/TirSystem/my-app"
assert_contains "GitHub plan" "$OUT" "GitHub repository : create (private), empty https://github.com/acme-org/my-app"
assert_contains "mirror plan" "$OUT" "Push mirror : Gitea -> GitHub every 10m0s"
assert_contains "origin plan" "$OUT" "Local origin : will use SSH (the SSH test passed)"
@@ -148,14 +148,18 @@ test_github_owner_must_be_a_member() {
assert_contains "pending message" "$ERR" "membership of the GitHub organization 'acme-org' is not active"
}
test_license_must_be_offered_when_github_is_chosen() {
test_license_must_be_offered_when_a_public_project_has_github() {
setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
run_apply "$ANSWERS_GITHUB"
run_apply "$ANSWERS_GITHUB_PUBLIC"
assert_status "no AGPL" 1 "$STATUS"
assert_contains "message" "$ERR" "does not offer the AGPL-3.0 license"
assert_not_contains "nothing created" "$(calls)" "POST"
# Without GitHub no license is needed, so the same server is fine.
# Without GitHub, or for a private project, no license is needed, so the same server is fine.
setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
run_dry "$ANSWERS_GITHUB"
assert_status "private with GitHub" 0 "$STATUS"
setup_hosts
prepend_route 'GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"}]'
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
@@ -215,8 +219,7 @@ test_apply_sends_the_right_request_bodies() {
bodies="$(cat "$WORK/curl.bodies")"
assert_contains "GitHub name" "$bodies" '"name":"my-app"'
assert_contains "GitHub is created empty" "$bodies" '"private":true,"auto_init":false}'
assert_contains "Gitea gets the license" "$bodies" '"license":"AGPL-3.0"'
assert_contains "Gitea is initialised with it" "$bodies" '"auto_init":true'
assert_not_contains "a private project gets no license" "$bodies" '"license"'
assert_contains "default branch" "$bodies" '"default_branch":"main"'
assert_contains "description" "$bodies" '"description":"A test app"'
assert_contains "mirror target without credentials" "$bodies" '"remote_address":"https://github.com/acme-org/my-app.git"'
@@ -239,6 +242,17 @@ test_apply_never_prints_or_passes_a_token() {
assert_contains "GitHub token in the private config" "$(cat "$WORK/curl.config")" "Authorization: Bearer $FAKE_GITHUB_PAT"
}
test_apply_sends_the_license_for_a_public_project_with_github() {
setup_hosts
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
local bodies
bodies="$(cat "$WORK/curl.bodies")"
assert_contains "Gitea gets the license" "$bodies" '"license":"AGPL-3.0"'
assert_contains "Gitea is initialised with it" "$bodies" '"auto_init":true'
assert_contains "public" "$bodies" '"private":false'
assert_contains "plan names the rule" "$OUT" "with the AGPL-3.0 license (default: GitHub and a public project)"
}
test_apply_with_gitea_only() {
setup_hosts
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
@@ -337,7 +351,7 @@ test_gitea_repository_with_only_the_license_can_be_reused() {
setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}'
prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"LICENSE","type":"file","path":"LICENSE"}]'
run_apply "$ANSWERS_GITHUB"$'y\ny\n'
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "license only" 0 "$STATUS"
assert_not_contains "Gitea repository not created again" "$(calls)" "$GITEA_REPO_CALL"
assert_eq "mirror created once" "1" "$(calls | grep -c -x -F "$MIRROR_CALL")"
@@ -355,7 +369,7 @@ test_gitea_repository_with_only_the_license_can_be_reused() {
test_reusing_an_empty_gitea_repository_warns_about_the_license() {
setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":true}'
run_apply "$ANSWERS_GITHUB"$'y\ny\n'
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "reuse empty Gitea repository" 0 "$STATUS"
assert_contains "warning" "$ERR" "the AGPL-3.0 license is not added to it"
}
@@ -485,7 +499,7 @@ test_a_repository_this_script_created_earlier_can_be_reused() {
setup_hosts
prepend_route 'GET|/api/v1/repos/TirSystem/my-app|200|{"empty":false}'
prepend_route 'GET|/api/v1/repos/TirSystem/my-app/contents|200|[{"name":"README.md","type":"file"},{"name":"LICENSE","type":"file"}]'
run_apply "$ANSWERS_GITHUB"$'y\ny\n'
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\ny\n'
assert_status "LICENSE and README.md" 0 "$STATUS"
assert_not_contains "not created again" "$(calls)" "$GITEA_REPO_CALL"
assert_contains "reported" "$OUT" "Gitea repository : reused"
+219
View File
@@ -0,0 +1,219 @@
#!/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,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.
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
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 "" "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 "" "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"
}
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
# 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"
}
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 "" "setup_temp_dir; 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"
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
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" "/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"
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" && MSYS=winsymlinks:nativestrict ln -s first second)
run_from "$bin/second" "$FOLDER" "" --version
assert_status "second link" 0 "$STATUS"
}
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env bash
# test-license.sh - tests for the project license (MIL-006): PROJECT_LICENSE
# in config.env, the default (AGPL-3.0 only for a public project with GitHub),
# "none", invalid values and a license the server does not offer. Sourced by
# run-tests.sh.
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
readonly ANSWERS_GITEA_ONLY_PUBLIC=$'my-app\n\npublic\nTirSystem\nn\n\nn\n'
readonly MIT_ROUTE='GET|/api/v1/licenses|200|[{"key":"MIT","name":"MIT"},{"key":"AGPL-3.0","name":"AGPL-3.0"}]'
# use_license LINE: add a line to config.env and make the server offer MIT.
use_license() {
printf '%s\n' "$1" >>"$WORK/config.env"
prepend_route "$MIT_ROUTE"
}
# gitea_only_env: a Gitea-only run needs no GitHub credentials.
gitea_only_env() {
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
}
# ------------------------------------------------------------- key is set
test_a_license_in_config_is_used_with_github_for_a_private_project() {
setup_hosts
use_license "PROJECT_LICENSE=MIT"
run_apply "$ANSWERS_GITHUB"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_contains "Gitea gets MIT" "$(cat "$WORK/curl.bodies")" '"license":"MIT"'
assert_not_contains "no AGPL" "$(cat "$WORK/curl.bodies")" "AGPL"
assert_contains "plan" "$OUT" "create (private) with the MIT license (from config.env)"
assert_contains "summary" "$OUT" "License : MIT (from config.env)"
}
test_a_license_in_config_is_used_without_github() {
setup_hosts
gitea_only_env
use_license "PROJECT_LICENSE=MIT"
run_apply "$ANSWERS_GITEA_ONLY"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_contains "Gitea gets MIT" "$(cat "$WORK/curl.bodies")" '"license":"MIT"'
assert_contains "initialised with it" "$(cat "$WORK/curl.bodies")" '"auto_init":true'
assert_not_contains "no GitHub call" "$(calls)" "api.github.com"
assert_contains "the server is asked" "$(calls)" "/licenses"
}
test_the_license_key_is_taken_in_any_case_for_none() {
setup_hosts
use_license "PROJECT_LICENSE=NONE"
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_not_contains "no license" "$(cat "$WORK/curl.bodies")" '"license"'
}
# ------------------------------------------------------------------- none
test_none_gives_no_license_even_for_a_public_project_with_github() {
setup_hosts
use_license "PROJECT_LICENSE=none"
run_apply "$ANSWERS_GITHUB_PUBLIC"$'y\n'
assert_status "apply" 0 "$STATUS"
assert_not_contains "no license" "$(cat "$WORK/curl.bodies")" '"license"'
assert_not_contains "no license lookup" "$(calls)" "/licenses"
assert_contains "plan" "$OUT" "create (public), empty"
assert_contains "summary" "$OUT" "License : none (from config.env)"
}
# ----------------------------------------------------------------- absent
test_without_the_key_a_public_project_with_github_gets_agpl() {
setup_hosts
run_dry "$ANSWERS_GITHUB_PUBLIC"
assert_status "dry run" 0 "$STATUS"
assert_contains "plan" "$OUT" "with the AGPL-3.0 license (default: GitHub and a public project)"
assert_contains "summary" "$OUT" "License : AGPL-3.0 (default: GitHub and a public project)"
}
test_without_the_key_a_private_project_with_github_gets_no_license() {
setup_hosts
run_dry "$ANSWERS_GITHUB"
assert_status "dry run" 0 "$STATUS"
assert_contains "summary" "$OUT" "License : none (no default for a private project)"
assert_not_contains "no license lookup" "$(calls)" "/licenses"
}
test_without_the_key_a_project_without_github_gets_no_license() {
setup_hosts
gitea_only_env
run_dry "$ANSWERS_GITEA_ONLY_PUBLIC"
assert_status "dry run" 0 "$STATUS"
assert_contains "summary" "$OUT" "License : none"
assert_not_contains "no marker" "$OUT" "License : none ("
assert_not_contains "no license lookup" "$(calls)" "/licenses"
}
# ---------------------------------------------------- empty and invalid
test_an_empty_license_stops_before_any_request() {
setup_hosts
printf 'PROJECT_LICENSE=\n' >>"$WORK/config.env"
run_dry "$ANSWERS_GITHUB"
assert_status "empty" 1 "$STATUS"
assert_contains "key named" "$ERR" "PROJECT_LICENSE in"
assert_contains "says empty" "$ERR" "is empty"
assert_eq "no request to any host" "" "$(calls)"
}
test_an_invalid_license_stops_before_any_request_and_is_never_asked() {
local value
for value in 'bad license' 'MIT/2' 'MIT;rm' "$(printf 'a%.0s' {1..65})"; do
remove_workdir
new_workdir
setup_hosts
printf 'PROJECT_LICENSE=%s\n' "$value" >>"$WORK/config.env"
run_dry "$ANSWERS_GITHUB"
assert_status "invalid '$value'" 1 "$STATUS"
assert_contains "key named" "$ERR" "PROJECT_LICENSE in"
assert_eq "no request to any host" "" "$(calls)"
assert_not_contains "never asked" "$ERR" "License ("
done
}
# ------------------------------------------------------ not offered
test_a_license_the_server_does_not_offer_stops_before_anything_is_created() {
setup_hosts
use_license "PROJECT_LICENSE=Zlib"
run_apply "$ANSWERS_GITHUB"$'y\n'
assert_status "not offered" 1 "$STATUS"
assert_contains "license named" "$ERR" "does not offer the Zlib license"
assert_not_contains "nothing created" "$(calls)" "POST"
}
# ------------------------------------------------------- never asked
test_the_license_is_never_asked() {
setup_hosts
run_dry "$ANSWERS_GITHUB_PUBLIC"
assert_not_contains "no license prompt" "$ERR" "icense ("
assert_not_contains "no license prompt, any case" "$ERR" "License:"
remove_workdir
new_workdir
setup_hosts
use_license "PROJECT_LICENSE=MIT"
run_dry "$ANSWERS_GITHUB"
assert_not_contains "no license prompt with the key" "$ERR" "icense ("
}
# ------------------------------------------- mirror and local history
test_the_license_file_reaches_the_local_project_without_github() {
setup_hosts
gitea_only_env
use_license "PROJECT_LICENSE=MIT"
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_eq "the Gitea license commit is the whole history" "1" "$(GIT_CONFIG_GLOBAL="$WORK/gitconfig" GIT_CONFIG_NOSYSTEM=1 git -C "$dir" rev-list --count HEAD)"
assert_file_exists "LICENSE from Gitea" "$dir/LICENSE"
}
+1 -1
View File
@@ -12,7 +12,7 @@
# LOCAL_ANSWERS (kept in a variable because $(...) would drop the last newline).
local_answers() {
if [[ $2 == y ]]; then
printf -v LOCAL_ANSWERS 'my-app\nA test app\n\nTirSystem\ny\nacme-org\n%s\n%s\n' "$1" "$3"
printf -v LOCAL_ANSWERS 'my-app\nA test app\npublic\nTirSystem\ny\nacme-org\n%s\n%s\n' "$1" "$3"
else
printf -v LOCAL_ANSWERS 'my-app\n\n\nTirSystem\nn\n%s\n%s\n' "$1" "$3"
fi
+1 -1
View File
@@ -205,7 +205,7 @@ test_summary_marks_the_values_from_config_env() {
assert_contains "name" "$OUT" "Repository : my-app (from config.env) (private (from config.env))"
assert_contains "description" "$OUT" "Description : A test app (from config.env)"
assert_contains "Gitea" "$OUT" "/TirSystem/my-app (from config.env)"
assert_contains "GitHub" "$OUT" "(AGPL license applied) (from config.env)"
assert_contains "GitHub" "$OUT" "/acme-org/my-app (from config.env)"
assert_contains "directory" "$OUT" "Directory : ./my-app (from config.env)"
assert_contains "plan gate" "$OUT" "Plan gate : no (from config.env)"
}
+8 -6
View File
@@ -19,10 +19,10 @@ test_full_run_with_github() {
after="$(listing)"
assert_status "full run" 0 "$STATUS"
assert_contains "dry run" "$OUT" "Dry run: nothing was created"
assert_contains "plan" "$OUT" "create (private) with the AGPL-3.0 license"
assert_contains "plan" "$OUT" "create (private), empty"
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 "no default license for a private project" "$OUT" "License : none (no default for a private project)"
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"
@@ -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"
}