Set the project license in config.env; AGPL-3.0 default only for a public GitHub project

- PROJECT_LICENSE (a Gitea license key, or none) is read, checked and never asked
- Without it AGPL-3.0 applies only when GitHub is chosen and the project is public
- The license is applied on Gitea with or without GitHub, and checked against the server first
- Plan and summary name the license and where it came from
- MIL-006 accepted; README and config.env.example document the key
- Tests: new test-license.sh; existing tests use a public project where they expect AGPL-3.0

Task: MIL-006#1
Task: MIL-006#2
Task: MIL-006#3
Task: MIL-006#4
Refs #44
Refs #45
Refs #46
Refs #50
This commit is contained in:
2026-10-07 13:08:49 +08:00
parent b324acbfae
commit e960dd9da8
19 changed files with 279 additions and 40 deletions
+3 -2
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
+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
}
+1
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() {
+3 -1
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,7 +34,7 @@ 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"
+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)?$ ]]