256 lines
10 KiB
Bash
256 lines
10 KiB
Bash
# shellcheck shell=bash
|
|
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
|
|
# config.sh - Reading and checking config.env and .env (parsed, never sourced).
|
|
#
|
|
# Part of create-project.sh: sourced by it, never run on its own.
|
|
#
|
|
# Provides: unquote_value, parse_env_file, parse_env_entry, validate_config, check_preset, check_preset_choice, validate_project_presets, validate_credentials, warn_if_env_unsafe, load_configuration
|
|
|
|
# unquote_value RAW: strip matching quotes (or a trailing " # comment" on an
|
|
# unquoted value) and return the value in REPLY. Fails on unbalanced quotes.
|
|
unquote_value() {
|
|
local raw quote
|
|
raw="$(trim "$1")"
|
|
quote="${raw:0:1}"
|
|
if [[ $quote == '"' || $quote == "'" ]]; then
|
|
[[ ${#raw} -ge 2 && ${raw: -1} == "$quote" ]] || return 1
|
|
raw="${raw:1:${#raw}-2}"
|
|
[[ $raw != *"$quote"* ]] || return 1
|
|
else
|
|
raw="${raw%%[[:space:]]#*}"
|
|
raw="$(trim "$raw")"
|
|
[[ $raw != *'"'* && $raw != *"'"* ]] || return 1
|
|
fi
|
|
REPLY="$raw"
|
|
}
|
|
|
|
# parse_env_file FILE ALLOWED_ARRAY TARGET_ARRAY
|
|
# Read KEY=VALUE lines without source or eval. Only keys named in
|
|
# ALLOWED_ARRAY are accepted; they are stored in the associative array
|
|
# TARGET_ARRAY. Messages name the key and the line, never the value.
|
|
parse_env_file() {
|
|
local file="$1" line key line_number=0
|
|
local pattern='^([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*)$'
|
|
[[ -f $file && -r $file ]] || die "cannot read '$file'"
|
|
# shellcheck disable=SC2094 # the loop body only uses $file in messages
|
|
while IFS= read -r line || [[ -n $line ]]; do
|
|
line_number=$((line_number + 1))
|
|
if ((line_number == 1)); then
|
|
line="${line#$'\xEF\xBB\xBF'}" # byte order mark from some Windows editors
|
|
fi
|
|
line="$(trim "${line%$'\r'}")"
|
|
if [[ -z $line || $line == \#* ]]; then
|
|
continue
|
|
fi
|
|
[[ $line =~ $pattern ]] ||
|
|
die "$file line $line_number: expected KEY=VALUE"
|
|
key="${BASH_REMATCH[1]}"
|
|
parse_env_entry "$file" "$line_number" "$key" "${BASH_REMATCH[2]}" \
|
|
"$2" "$3"
|
|
done <"$file"
|
|
}
|
|
|
|
# parse_env_entry FILE LINE KEY RAW_VALUE ALLOWED_ARRAY TARGET_ARRAY
|
|
parse_env_entry() {
|
|
local file="$1" line_number="$2" key="$3" raw="$4"
|
|
local -n allowed_keys="$5"
|
|
local -n target_map="$6"
|
|
local value
|
|
if ! in_list "$key" "${allowed_keys[@]}"; then
|
|
if in_list "$key" "${CREDENTIAL_KEYS[@]}"; then
|
|
die "$file line $line_number: '$key' is a credential; keep it in the .env file only"
|
|
fi
|
|
die "$file line $line_number: unknown key '$key'"
|
|
fi
|
|
if [[ -n ${target_map[$key]+set} ]]; then
|
|
die "$file line $line_number: '$key' is set twice"
|
|
fi
|
|
unquote_value "$raw" ||
|
|
die "$file line $line_number: unbalanced or misplaced quotes"
|
|
value="$REPLY"
|
|
if has_control_character "$value"; then
|
|
die "$file line $line_number: '$key' contains a control character"
|
|
fi
|
|
if ((${#value} > MAX_VALUE_LENGTH)); then
|
|
die "$file line $line_number: '$key' is too long"
|
|
fi
|
|
# shellcheck disable=SC2004 # target_map is an associative array: $key is a string
|
|
target_map[$key]="$value"
|
|
}
|
|
|
|
validate_config() {
|
|
local key url
|
|
if [[ -z ${CONFIG[GITEA_URL]:-} ]]; then
|
|
die "GITEA_URL is missing in $CONFIG_FILE (see config.env.example)"
|
|
fi
|
|
CONFIG[GITHUB_API_URL]="${CONFIG[GITHUB_API_URL]:-https://api.github.com}"
|
|
CONFIG[GITHUB_WEB_URL]="${CONFIG[GITHUB_WEB_URL]:-https://github.com}"
|
|
for key in GITHUB_API_URL GITHUB_WEB_URL GITEA_URL; do
|
|
url="$(normalize_url "${CONFIG[$key]}")"
|
|
is_valid_base_url "$url" ||
|
|
die "$key in $CONFIG_FILE must be an https URL without credentials, query or fragment"
|
|
CONFIG[$key]="$url"
|
|
done
|
|
CONFIG[GITEA_API_URL]="$(normalize_url "${CONFIG[GITEA_API_URL]:-${CONFIG[GITEA_URL]}/api/v1}")"
|
|
is_valid_base_url "${CONFIG[GITEA_API_URL]}" ||
|
|
die "GITEA_API_URL in $CONFIG_FILE must be an https URL without credentials, query or fragment"
|
|
CONFIG[GITEA_SSH_PORT]="${CONFIG[GITEA_SSH_PORT]:-$DEFAULT_SSH_PORT}"
|
|
is_valid_port "${CONFIG[GITEA_SSH_PORT]}" ||
|
|
die "GITEA_SSH_PORT in $CONFIG_FILE must be a port number from 1 to 65535"
|
|
CONFIG[MIRROR_INTERVAL]="${CONFIG[MIRROR_INTERVAL]:-$DEFAULT_MIRROR_INTERVAL}"
|
|
is_valid_interval "${CONFIG[MIRROR_INTERVAL]}" ||
|
|
die "MIRROR_INTERVAL in $CONFIG_FILE must look like 10m0s or 8h0m0s"
|
|
CONFIG[FRAMEWORK_REPO]="${CONFIG[FRAMEWORK_REPO]:-$DEFAULT_FRAMEWORK_REPO}"
|
|
is_valid_framework_repo "${CONFIG[FRAMEWORK_REPO]}" ||
|
|
die "FRAMEWORK_REPO in $CONFIG_FILE must look like OWNER/NAME"
|
|
validate_project_presets
|
|
}
|
|
|
|
# check_preset KEY VALIDATOR HINT: when KEY is set in config.env its value must
|
|
# pass VALIDATOR. A key that is present counts as set; only the description
|
|
# may be empty. The message names the key, never the value.
|
|
check_preset() {
|
|
local key="$1" validator="$2" hint="$3"
|
|
[[ -n ${CONFIG[$key]+set} ]] || return 0
|
|
if [[ -z ${CONFIG[$key]} && $key != PROJECT_DESCRIPTION ]]; then
|
|
die "$key in $CONFIG_FILE is empty; remove the line to be asked, or give a value ($hint)"
|
|
fi
|
|
"$validator" "${CONFIG[$key]}" ||
|
|
die "$key in $CONFIG_FILE is not valid: $hint"
|
|
}
|
|
|
|
# check_preset_choice KEY CHOICE...: like check_preset for a fixed list of
|
|
# words; the value is stored in lower case.
|
|
check_preset_choice() {
|
|
local key="$1"
|
|
shift
|
|
[[ -n ${CONFIG[$key]+set} ]] || return 0
|
|
[[ -n ${CONFIG[$key]} ]] ||
|
|
die "$key in $CONFIG_FILE is empty; remove the line to be asked, or give one of: $*"
|
|
CONFIG[$key]="${CONFIG[$key],,}"
|
|
in_list "${CONFIG[$key]}" "$@" ||
|
|
die "$key in $CONFIG_FILE must be one of: $*"
|
|
}
|
|
|
|
# The optional project details that may be preset in config.env.
|
|
validate_project_presets() {
|
|
check_preset PROJECT_NAME is_valid_repo_name "$HINT_REPO_NAME"
|
|
check_preset PROJECT_DESCRIPTION is_valid_description "$HINT_DESCRIPTION"
|
|
check_preset_choice PROJECT_VISIBILITY private public
|
|
check_preset GITEA_OWNER is_valid_gitea_owner "$HINT_GITEA_OWNER"
|
|
check_preset_choice USE_GITHUB yes no
|
|
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: check the credentials that .env provides. A credential
|
|
# that is not provided is not an error: it is asked later (collect_credentials).
|
|
validate_credentials() {
|
|
# Register secrets first so that no later message can show them.
|
|
if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]]; then
|
|
SECRET_VALUES+=("${CREDENTIALS[GITEA_TOKEN]}")
|
|
fi
|
|
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]]; then
|
|
SECRET_VALUES+=("${CREDENTIALS[GITHUB_PAT]}")
|
|
fi
|
|
if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]] &&
|
|
! is_valid_token "${CREDENTIALS[GITEA_TOKEN]}"; then
|
|
die "GITEA_TOKEN in $ENV_FILE is not a valid token ($HINT_TOKEN)"
|
|
fi
|
|
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]] &&
|
|
! is_valid_token "${CREDENTIALS[GITHUB_PAT]}"; then
|
|
die "GITHUB_PAT in $ENV_FILE is not a valid token ($HINT_TOKEN)"
|
|
fi
|
|
if [[ -n ${CREDENTIALS[GITHUB_USER]:-} ]] &&
|
|
! is_valid_github_owner "${CREDENTIALS[GITHUB_USER]}"; then
|
|
die "GITHUB_USER in $ENV_FILE is not a valid GitHub account name"
|
|
fi
|
|
}
|
|
|
|
warn_if_env_unsafe() {
|
|
local file="$1" dir mode
|
|
case "$(uname -s 2>/dev/null || true)" in
|
|
MINGW* | MSYS* | CYGWIN*) ;;
|
|
*)
|
|
mode="$(stat -c '%a' -- "$file" 2>/dev/null ||
|
|
stat -f '%Lp' -- "$file" 2>/dev/null || true)"
|
|
if [[ -n $mode ]] && (((8#$mode & 8#077) != 0)); then
|
|
warn "$file is readable by other users (mode $mode); run: chmod 600 $file"
|
|
fi
|
|
;;
|
|
esac
|
|
dir="."
|
|
if [[ $file == */* ]]; then
|
|
dir="${file%/*}"
|
|
fi
|
|
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 &&
|
|
! git -C "$dir" check-ignore -q -- "$file"; then
|
|
warn "$file is not ignored by git; add it to .gitignore before committing"
|
|
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". With a fifth word, optional,
|
|
# a file found nowhere is not an error: the file stays empty and the origin
|
|
# is "none".
|
|
locate_config_file() {
|
|
local name="$1" flag="$2" optional="${5:-}"
|
|
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"
|
|
elif [[ -n $optional ]]; then
|
|
origin_ref="none"
|
|
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' ;;
|
|
none) printf 'none found; a missing credential is asked' ;;
|
|
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 optional
|
|
say "Config file : $CONFIG_FILE ($(origin_words "$CONFIG_ORIGIN"))"
|
|
say "Credentials file: ${ENV_FILE:-(none)} ($(origin_words "$ENV_ORIGIN"))"
|
|
parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG
|
|
validate_config
|
|
confirm_folder_files
|
|
# .env is optional: a credential it does not provide is asked.
|
|
if [[ -n $ENV_FILE && -e $ENV_FILE ]]; then
|
|
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
|
|
warn_if_env_unsafe "$ENV_FILE"
|
|
fi
|
|
validate_credentials
|
|
}
|