Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a7159c18 |
+5
-2
@@ -1,6 +1,9 @@
|
||||
# RepoFoundry credentials. Placeholders only: never put a real value in this
|
||||
# file or commit one.
|
||||
#
|
||||
# Everything in this file is optional: a credential that is missing here is
|
||||
# asked for when the script runs (the token is not echoed).
|
||||
#
|
||||
# Copy this file to .env, fill in the values and keep it private
|
||||
# (chmod 600 .env on Linux and macOS). .env is ignored by git. The file is
|
||||
# read as plain KEY=VALUE lines and never executed. Values may be wrapped in
|
||||
@@ -13,7 +16,7 @@
|
||||
GITHUB_PAT=
|
||||
|
||||
########################################
|
||||
# Secrets for workframe
|
||||
# Secrets for framework
|
||||
########################################
|
||||
|
||||
# GitHub account the token belongs to. It identifies who authenticates; it is
|
||||
@@ -21,6 +24,6 @@ GITHUB_PAT=
|
||||
# belong to an organization.
|
||||
GITHUB_USER=
|
||||
|
||||
# Gitea access token (required). It needs permission to create repositories
|
||||
# Gitea access token. It needs permission to create repositories
|
||||
# for the chosen owner and to manage the repository's push mirror.
|
||||
GITEA_TOKEN=
|
||||
|
||||
@@ -129,14 +129,40 @@ src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
|
||||
|
||||
| Key | Meaning |
|
||||
| --- | --- |
|
||||
| `GITEA_TOKEN` | Gitea access token (required) |
|
||||
| `GITEA_TOKEN` | Gitea access token |
|
||||
| `GITHUB_PAT` | GitHub personal access token (only when you choose GitHub) |
|
||||
| `GITHUB_USER` | the GitHub account the token belongs to; only a default for the owner prompt |
|
||||
| `GITHUB_USER` | the GitHub account the token belongs to (only when you choose GitHub) |
|
||||
|
||||
`.env` is ignored by git. The script warns if it is readable by other users or
|
||||
not ignored by git. See [Token permissions](#token-permissions) for what each
|
||||
token needs.
|
||||
|
||||
`.env` is optional, and so is each key in it. A credential that is not
|
||||
provided (the file is missing, the key is absent or its value is empty) is
|
||||
asked for: the Gitea token at the start, the GitHub token and account name once
|
||||
you choose GitHub. A token is read without echo and checked like one read from
|
||||
`.env`; a refused value is asked again and never shown. If input ends before a
|
||||
valid value is entered, the run stops before any request to a host.
|
||||
|
||||
### The project's own `.env`
|
||||
|
||||
When the project exists, the script asks whether to create a `.env` in it
|
||||
(default no). On a yes the file holds only the credentials the project needs:
|
||||
`GITEA_TOKEN`, and `GITHUB_PAT` and `GITHUB_USER` when you chose GitHub, as read
|
||||
from your `.env` or typed.
|
||||
|
||||
- The file is created readable by you only (mode 600), never readable by
|
||||
others even for a moment, and is never written by anything else.
|
||||
- Git ignores it: the script adds `.env` to `.git/info/exclude` of the new
|
||||
project. No tracked file changes and nothing is committed.
|
||||
- An existing `.env` in the project is never replaced without a second yes, and
|
||||
a `.env` that git already tracks is never written.
|
||||
- The summary names the keys, never the values.
|
||||
|
||||
This is the one place the script writes a token to disk. It is plain text: keep
|
||||
the project directory private, do not copy the file around, and say no if you
|
||||
do not need it. Tokens are written nowhere else.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
@@ -264,7 +290,9 @@ script stops before it creates anything when a preflight check is refused.
|
||||
## Security decisions
|
||||
|
||||
- **Tokens never appear** in output, logs, remote URLs, `.git/config`,
|
||||
`.gitmodules`, command lines or leftover files. They go to `curl` through a
|
||||
`.gitmodules`, command lines or leftover files, and are written to disk only
|
||||
in the new project's own `.env`, after a yes (see
|
||||
[The project's own `.env`](#the-projects-own-env)). They go to `curl` through a
|
||||
private configuration file that is removed right after the request, and to
|
||||
`git` (HTTPS fetch only) through a `GIT_ASKPASS` helper and the environment
|
||||
of that one command. Output is filtered, so even a server message that echoes
|
||||
@@ -368,11 +396,13 @@ file. The files are loaded from that directory only, by a fixed path.
|
||||
| `json.sh` | the little JSON the script reads and writes |
|
||||
| `http.sh` | the one place that runs `curl`; tokens stay off the command line |
|
||||
| `api.sh` | GitHub and Gitea API calls and reporting a refused call |
|
||||
| `prompts.sh` | interactive questions with validation |
|
||||
| `prompts.sh` | interactive questions with validation (secrets are read without echo) |
|
||||
| `credentials.sh` | asking for a credential that `.env` does not provide |
|
||||
| `project.sh` | the project details: asking for them and showing them |
|
||||
| `hosts.sh` | names, links and remote addresses of the repositories |
|
||||
| `preflight.sh` | read-only checks of both hosts |
|
||||
| `steps.sh` | the outcome of each step and the final report |
|
||||
| `envfile.sh` | the new project's own `.env`: created private, ignored by git, never replaced without a yes |
|
||||
| `plan.sh` | printing what the script is about to do |
|
||||
| `repositories.sh` | creating the GitHub and Gitea repositories |
|
||||
| `mirror.sh` | the Gitea to GitHub push mirror |
|
||||
|
||||
+15
-6
@@ -11,7 +11,9 @@
|
||||
# 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.
|
||||
# license to the Gitea repository. After a yes (default no) it also writes
|
||||
# the new project's own .env with the credentials the project needs. No
|
||||
# commit is made in the new project.
|
||||
#
|
||||
# Dry run by default
|
||||
# Without --apply the script only reads from GitHub and Gitea (GET
|
||||
@@ -27,7 +29,8 @@
|
||||
# Options
|
||||
# --apply create the repositories and the mirror (after a final yes)
|
||||
# --config FILE service addresses (default: config.env in the project root)
|
||||
# --env FILE credentials (default: .env in the project root)
|
||||
# --env FILE credentials (default: .env in the project root); optional:
|
||||
# a credential it does not provide is asked, not echoed
|
||||
# -h, --help show this help
|
||||
# --version show the version
|
||||
#
|
||||
@@ -36,7 +39,8 @@
|
||||
# the optional GITEA_SSH_PORT (default 10022), MIRROR_INTERVAL
|
||||
# (default 10m0s) and FRAMEWORK_REPO (default
|
||||
# TirSystem/SQA-QC-Framework, the submodule's OWNER/NAME)
|
||||
# .env GITHUB_PAT, GITHUB_USER, GITEA_TOKEN
|
||||
# .env GITHUB_PAT, GITHUB_USER, GITEA_TOKEN (all optional, each
|
||||
# asked when missing)
|
||||
#
|
||||
# Environment
|
||||
# REPOFOUNDRY_NAME project name used in messages (default: RepoFoundry)
|
||||
@@ -65,8 +69,8 @@
|
||||
# This file is the entry point. The work is split by responsibility into
|
||||
# the files in lib/ next to it (one job per file, see the first lines of
|
||||
# each file): constants, output, temp, util, validate, config, tools, json,
|
||||
# http, api, prompts, project, hosts, preflight, steps, plan, repositories,
|
||||
# mirror, git, localproject, framework, apply and cli. The files are loaded
|
||||
# http, api, prompts, credentials, project, hosts, preflight, steps, plan,
|
||||
# repositories, mirror, git, localproject, framework, envfile, apply and cli. The files are loaded
|
||||
# from this directory only.
|
||||
#
|
||||
# Exit codes
|
||||
@@ -121,6 +125,8 @@ source "$SCRIPT_DIR/lib/http.sh"
|
||||
source "$SCRIPT_DIR/lib/api.sh"
|
||||
# shellcheck source=lib/prompts.sh
|
||||
source "$SCRIPT_DIR/lib/prompts.sh"
|
||||
# shellcheck source=lib/credentials.sh
|
||||
source "$SCRIPT_DIR/lib/credentials.sh"
|
||||
# shellcheck source=lib/project.sh
|
||||
source "$SCRIPT_DIR/lib/project.sh"
|
||||
# shellcheck source=lib/hosts.sh
|
||||
@@ -141,6 +147,8 @@ source "$SCRIPT_DIR/lib/git.sh"
|
||||
source "$SCRIPT_DIR/lib/localproject.sh"
|
||||
# shellcheck source=lib/framework.sh
|
||||
source "$SCRIPT_DIR/lib/framework.sh"
|
||||
# shellcheck source=lib/envfile.sh
|
||||
source "$SCRIPT_DIR/lib/envfile.sh"
|
||||
# shellcheck source=lib/apply.sh
|
||||
source "$SCRIPT_DIR/lib/apply.sh"
|
||||
# shellcheck source=lib/cli.sh
|
||||
@@ -166,9 +174,10 @@ main() {
|
||||
check_tools
|
||||
setup_temp_dir
|
||||
load_configuration
|
||||
collect_credentials GITEA_TOKEN
|
||||
collect_project_details
|
||||
if ((PROJECT[has_github])); then
|
||||
require_github_credentials
|
||||
collect_credentials GITHUB_PAT GITHUB_USER
|
||||
fi
|
||||
init_steps
|
||||
print_summary
|
||||
|
||||
@@ -19,6 +19,7 @@ create_all() {
|
||||
add_framework
|
||||
install_framework
|
||||
copy_templates
|
||||
create_env_file
|
||||
}
|
||||
|
||||
# confirm_framework_access: the framework comes over SSH. Without SSH the
|
||||
|
||||
+14
-18
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# 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, require_github_credentials, warn_if_env_unsafe, load_configuration
|
||||
# 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.
|
||||
@@ -144,20 +144,23 @@ validate_project_presets() {
|
||||
check_preset_choice ENABLE_PLAN_GATE yes no
|
||||
}
|
||||
|
||||
# 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() {
|
||||
if [[ -z ${CREDENTIALS[GITEA_TOKEN]:-} ]]; then
|
||||
die "GITEA_TOKEN is missing in $ENV_FILE (see .env.example)"
|
||||
fi
|
||||
# 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
|
||||
is_valid_token "${CREDENTIALS[GITEA_TOKEN]}" ||
|
||||
die "GITEA_TOKEN in $ENV_FILE is not a valid token (8 to 255 letters, digits or _ . ~ + / = -)"
|
||||
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 (8 to 255 letters, digits or _ . ~ + / = -)"
|
||||
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
|
||||
@@ -165,16 +168,6 @@ validate_credentials() {
|
||||
fi
|
||||
}
|
||||
|
||||
# GitHub credentials are only needed when the Maintainer chose GitHub.
|
||||
require_github_credentials() {
|
||||
local key
|
||||
for key in GITHUB_PAT GITHUB_USER; do
|
||||
if [[ -z ${CREDENTIALS[$key]:-} ]]; then
|
||||
die "GitHub was chosen but $key is missing in $ENV_FILE (see .env.example)"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
warn_if_env_unsafe() {
|
||||
local file="$1" dir mode
|
||||
case "$(uname -s 2>/dev/null || true)" in
|
||||
@@ -200,7 +193,10 @@ warn_if_env_unsafe() {
|
||||
load_configuration() {
|
||||
parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG
|
||||
validate_config
|
||||
# .env is optional: a credential it does not provide is asked.
|
||||
if [[ -e $ENV_FILE ]]; then
|
||||
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
|
||||
validate_credentials
|
||||
warn_if_env_unsafe "$ENV_FILE"
|
||||
fi
|
||||
validate_credentials
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ readonly HINT_DESCRIPTION="at most $MAX_DESCRIPTION_LENGTH characters and no con
|
||||
readonly HINT_GITEA_OWNER="use letters, digits, '.', '_' or '-' (at most 39)"
|
||||
readonly HINT_GITHUB_OWNER="use letters, digits or '-' (at most 39)"
|
||||
readonly HINT_DIRECTORY="must not be empty, start with '-' or contain control characters"
|
||||
readonly HINT_TOKEN="8 to 255 letters, digits or _ . ~ + / = -"
|
||||
readonly ENV_FILE_NAME=".env"
|
||||
readonly PLAN_STEPS=("GitHub repository" "Gitea repository" "Push mirror"
|
||||
"Local project" "Framework" "Skills and hooks" "Templates")
|
||||
"Local project" "Framework" "Skills and hooks" "Templates" "Project .env")
|
||||
# shellcheck disable=SC2034 # read through namerefs (parse_env_file)
|
||||
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
|
||||
GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# shellcheck shell=bash
|
||||
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
|
||||
# credentials.sh - Asking for a credential that .env does not provide.
|
||||
#
|
||||
# Part of create-project.sh: sourced by it, never run on its own.
|
||||
#
|
||||
# Provides: credential_label, collect_credentials
|
||||
|
||||
# credential_label KEY: the name of a credential as the Maintainer sees it.
|
||||
credential_label() {
|
||||
case "$1" in
|
||||
GITEA_TOKEN) printf 'Gitea access token' ;;
|
||||
GITHUB_PAT) printf 'GitHub personal access token' ;;
|
||||
GITHUB_USER) printf 'GitHub account name (the account the token belongs to)' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# collect_credentials KEY...: ask for each credential that is not already
|
||||
# provided. A token is read without echo and registered as a secret at once,
|
||||
# so no later message can show it; the GitHub account name is not secret and
|
||||
# is read like any other answer. An empty value in .env counts as not provided.
|
||||
collect_credentials() {
|
||||
local key
|
||||
for key in "$@"; do
|
||||
if [[ -n ${CREDENTIALS[$key]:-} ]]; then
|
||||
continue
|
||||
fi
|
||||
case "$key" in
|
||||
GITHUB_USER)
|
||||
prompt_value "$(credential_label "$key")" "" is_valid_github_owner \
|
||||
"use letters, digits or '-' (at most 39)"
|
||||
;;
|
||||
*)
|
||||
prompt_secret "$(credential_label "$key")" is_valid_token "$HINT_TOKEN"
|
||||
SECRET_VALUES+=("$REPLY")
|
||||
;;
|
||||
esac
|
||||
CREDENTIALS[$key]="$REPLY"
|
||||
REPLY=""
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# shellcheck shell=bash
|
||||
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
|
||||
# envfile.sh - The .env file of the new project: the one place a credential is written.
|
||||
#
|
||||
# Part of create-project.sh: sourced by it, never run on its own.
|
||||
#
|
||||
# Provides: env_file_keys, env_file_key_list, exclude_env_file, write_env_file, create_env_file
|
||||
|
||||
# env_file_keys: the credentials the new project needs, one per line: the
|
||||
# Gitea token, and the GitHub token and account name when GitHub was chosen.
|
||||
env_file_keys() {
|
||||
printf '%s\n' GITEA_TOKEN
|
||||
if ((PROJECT[has_github])); then
|
||||
printf '%s\n' GITHUB_PAT GITHUB_USER
|
||||
fi
|
||||
}
|
||||
|
||||
# env_file_key_list: the same keys on one line, for messages.
|
||||
env_file_key_list() {
|
||||
local keys
|
||||
keys="$(env_file_keys | tr '\n' ' ')"
|
||||
printf '%s' "${keys% }"
|
||||
}
|
||||
|
||||
# exclude_env_file DIR: make git ignore .env in DIR without touching a tracked
|
||||
# file: the entry goes into .git/info/exclude, which is never committed. It
|
||||
# does nothing when .env is already ignored.
|
||||
exclude_env_file() {
|
||||
local dir="$1" gitdir exclude
|
||||
if git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME"; then
|
||||
return 0
|
||||
fi
|
||||
gitdir="$(git_project "$dir" rev-parse --absolute-git-dir)"
|
||||
exclude="$gitdir/info/exclude"
|
||||
mkdir -p -- "$gitdir/info"
|
||||
# Start on a fresh line when the file does not end with one.
|
||||
if [[ -s $exclude && -n "$(tail -c 1 -- "$exclude")" ]]; then
|
||||
printf '\n' >>"$exclude"
|
||||
fi
|
||||
printf '%s\n' "# RepoFoundry: the credentials file of this project" "$ENV_FILE_NAME" >>"$exclude"
|
||||
git_project "$dir" check-ignore -q -- "$ENV_FILE_NAME" ||
|
||||
die "could not make git ignore $ENV_FILE_NAME in $dir; nothing was written to it"
|
||||
}
|
||||
|
||||
# write_env_file DIR IS_REPLACE: write the credentials to DIR/.env. The file is
|
||||
# created private (mode 600) from the start, never readable by others, even
|
||||
# for a moment: it is written under umask 077 as a temporary file next to the
|
||||
# target and moved into place. An existing file is only replaced when
|
||||
# IS_REPLACE is 1, and a file that appears in the meantime is never replaced.
|
||||
write_env_file() {
|
||||
local dir="$1" is_replace="$2" target tmp key
|
||||
target="$dir/$ENV_FILE_NAME"
|
||||
tmp="$(umask 077 && mktemp "$dir/$ENV_FILE_NAME.XXXXXX")"
|
||||
TEMP_FILES+=("$tmp")
|
||||
{
|
||||
while IFS= read -r key; do
|
||||
printf '%s=%s\n' "$key" "${CREDENTIALS[$key]}"
|
||||
done < <(env_file_keys)
|
||||
} >"$tmp"
|
||||
if ((is_replace)); then
|
||||
mv -f -- "$tmp" "$target"
|
||||
else
|
||||
mv -n -- "$tmp" "$target"
|
||||
if [[ -e $tmp ]]; then
|
||||
die "$target appeared while it was being written; it was not replaced"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# create_env_file: the last step. Only after a yes (default no) is the .env
|
||||
# written, and an existing one is only replaced after another yes. Nothing
|
||||
# printed names a value, only the keys.
|
||||
create_env_file() {
|
||||
local label="Project .env" dir="${PROJECT[directory]}" keys is_replace=0
|
||||
keys="$(env_file_key_list)"
|
||||
begin_step "$label"
|
||||
prompt_yes_no "Create a $ENV_FILE_NAME file in the project with the credentials it needs ($keys); only you can read it and git ignores it" n
|
||||
if ! ((REPLY)); then
|
||||
finish_step "$label" "skipped" "(you declined)"
|
||||
return 0
|
||||
fi
|
||||
if git_project "$dir" ls-files --error-unmatch -- "$ENV_FILE_NAME" >/dev/null 2>&1; then
|
||||
finish_step "$label" "skipped" "($ENV_FILE_NAME is tracked by git; it was not written)"
|
||||
return 0
|
||||
fi
|
||||
if [[ -e $dir/$ENV_FILE_NAME || -L $dir/$ENV_FILE_NAME ]]; then
|
||||
prompt_yes_no "$ENV_FILE_NAME already exists in the project. Replace it" n
|
||||
if ! ((REPLY)); then
|
||||
finish_step "$label" "kept" "(the existing $ENV_FILE_NAME was left as it was)"
|
||||
return 0
|
||||
fi
|
||||
is_replace=1
|
||||
fi
|
||||
exclude_env_file "$dir"
|
||||
write_env_file "$dir" "$is_replace"
|
||||
finish_step "$label" "created" "($keys; only you can read it, git ignores it)"
|
||||
}
|
||||
@@ -54,4 +54,5 @@ print_plan() {
|
||||
else
|
||||
say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")"
|
||||
fi
|
||||
say "$(printf ' %-18s: %s' "Project .env" "you are asked whether to create it ($(env_file_key_list))")"
|
||||
}
|
||||
|
||||
+22
-1
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# Part of create-project.sh: sourced by it, never run on its own.
|
||||
#
|
||||
# Provides: prompt_value, prompt_choice, prompt_yes_no
|
||||
# Provides: prompt_value, prompt_secret, prompt_choice, prompt_yes_no
|
||||
|
||||
# prompt_value LABEL DEFAULT VALIDATOR HINT: ask until VALIDATOR accepts the
|
||||
# answer; the accepted answer is returned in REPLY.
|
||||
@@ -27,6 +27,27 @@ prompt_value() {
|
||||
done
|
||||
}
|
||||
|
||||
# prompt_secret LABEL VALIDATOR HINT: like prompt_value for a secret. What is
|
||||
# typed is not shown (read -s) and a refused answer is never repeated in the
|
||||
# message. An empty answer is refused; there is no default.
|
||||
prompt_secret() {
|
||||
local label="$1" validator="$2" hint="$3" answer
|
||||
while true; do
|
||||
printf '%s (input is hidden): ' "$label" >&2
|
||||
IFS= read -rs answer || {
|
||||
printf '\n' >&2
|
||||
die "no input available for '$label'"
|
||||
}
|
||||
printf '\n' >&2 # the newline that hidden input did not echo
|
||||
answer="$(trim "$answer")"
|
||||
if [[ -n $answer ]] && "$validator" "$answer"; then
|
||||
REPLY="$answer"
|
||||
return 0
|
||||
fi
|
||||
warn "invalid $label: $hint"
|
||||
done
|
||||
}
|
||||
|
||||
# prompt_choice LABEL DEFAULT CHOICE...: the answer is returned in REPLY.
|
||||
prompt_choice() {
|
||||
local label="$1" default="$2" answer
|
||||
|
||||
@@ -96,7 +96,6 @@ validate_credentials"
|
||||
assert_status "$case_name" 1 "$STATUS"
|
||||
assert_contains "$case_name message" "$ERR" "$expected"
|
||||
done <<'EOF'
|
||||
no Gitea token|GITHUB_USER=octo\n|GITEA_TOKEN is missing
|
||||
token too short|GITEA_TOKEN=short\n|not a valid token
|
||||
token with a backslash|GITEA_TOKEN=abc\\defgh12345\n|not a valid token
|
||||
bad GitHub token|GITEA_TOKEN=abcdefgh12345\nGITHUB_PAT=bad token\n|GITHUB_PAT
|
||||
@@ -104,18 +103,6 @@ bad GitHub user|GITEA_TOKEN=abcdefgh12345\nGITHUB_USER=-bad-\n|GITHUB_USER
|
||||
EOF
|
||||
}
|
||||
|
||||
test_github_credentials_required_only_when_chosen() {
|
||||
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/e.env"
|
||||
run_lib "" "parse_env_file \"$WORK/e.env\" CREDENTIAL_KEYS CREDENTIALS
|
||||
validate_credentials
|
||||
echo no-github-ok
|
||||
ENV_FILE=\"$WORK/e.env\"
|
||||
require_github_credentials"
|
||||
assert_contains "Gitea-only .env is valid" "$OUT" "no-github-ok"
|
||||
assert_status "GitHub credentials missing" 1 "$STATUS"
|
||||
assert_contains "names the missing key" "$ERR" "GITHUB_PAT is missing"
|
||||
}
|
||||
|
||||
test_validators() {
|
||||
local fn value expected
|
||||
while IFS='|' read -r fn value expected; do
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-credentials.sh - tests for the credentials that .env does not provide
|
||||
# and for the .env file of the new project (MIL-005): they are asked without
|
||||
# echo, the project .env is only written after a yes, owner-only and ignored
|
||||
# by git, and no token appears anywhere else. Sourced by run-tests.sh.
|
||||
|
||||
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
|
||||
|
||||
readonly NL=$'\n'
|
||||
|
||||
# credentials_input: the answers of a run with no .env at all and GitHub
|
||||
# chosen: the Gitea token, the details, then the GitHub token and account.
|
||||
credentials_input() {
|
||||
printf '%s' "$FAKE_GITEA_TOKEN$NL$ANSWERS_GITHUB$FAKE_GITHUB_PAT${NL}octo-user$NL"
|
||||
}
|
||||
|
||||
run_dry_cred() {
|
||||
run_cli "$1" --config "$WORK/config.env" --env "$WORK/.env"
|
||||
}
|
||||
|
||||
run_apply_cred() {
|
||||
run_cli "$1" --apply --config "$WORK/config.env" --env "$WORK/.env"
|
||||
}
|
||||
|
||||
# without_env: remove the .env of the fixtures.
|
||||
without_env() {
|
||||
rm -f -- "$WORK/.env"
|
||||
}
|
||||
|
||||
# env_mode FILE: the permission bits, empty where the platform has none.
|
||||
env_mode() {
|
||||
case "$(uname -s 2>/dev/null || true)" in
|
||||
MINGW* | MSYS* | CYGWIN*) ;;
|
||||
*) stat -c '%a' -- "$1" 2>/dev/null || stat -f '%Lp' -- "$1" 2>/dev/null || true ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------ prompt_secret
|
||||
|
||||
test_prompt_secret_asks_again_and_never_repeats_the_answer() {
|
||||
run_lib $'short\n\nlongenoughtoken1\n' \
|
||||
'prompt_secret "Gitea access token" is_valid_token "needs 8 characters"; echo "[$REPLY]"'
|
||||
assert_status "valid answer found" 0 "$STATUS"
|
||||
assert_eq "valid answer returned" "[longenoughtoken1]" "$OUT"
|
||||
assert_contains "told why" "$ERR" "invalid Gitea access token: needs 8 characters"
|
||||
assert_not_contains "refused answer not repeated" "$ERR" "short"
|
||||
assert_contains "says the input is hidden" "$ERR" "(input is hidden)"
|
||||
}
|
||||
|
||||
test_prompt_secret_stops_when_input_ends() {
|
||||
run_lib "" 'prompt_secret "Gitea access token" is_valid_token "x" </dev/null'
|
||||
assert_status "end of input" 1 "$STATUS"
|
||||
assert_contains "message names the credential" "$ERR" "no input available for 'Gitea access token'"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------ collect_credentials
|
||||
|
||||
test_collect_credentials_asks_only_what_is_missing() {
|
||||
run_lib "$FAKE_GITHUB_PAT${NL}octo-user$NL" \
|
||||
'CREDENTIALS[GITEA_TOKEN]=giteaFAKEtoken1234567890
|
||||
collect_credentials GITEA_TOKEN GITHUB_PAT GITHUB_USER
|
||||
printf "%s|%s\n" "${CREDENTIALS[GITHUB_USER]}" "${#SECRET_VALUES[@]}"'
|
||||
assert_status "asked" 0 "$STATUS"
|
||||
assert_eq "account name kept, one secret registered" "octo-user|1" "$OUT"
|
||||
assert_contains "GitHub token asked" "$ERR" "GitHub personal access token"
|
||||
assert_contains "account asked" "$ERR" "GitHub account name"
|
||||
assert_not_contains "Gitea token not asked" "$ERR" "Gitea access token"
|
||||
assert_not_contains "no token shown" "$ERR$OUT" "$FAKE_GITHUB_PAT"
|
||||
}
|
||||
|
||||
test_a_token_that_is_asked_is_registered_as_a_secret() {
|
||||
run_lib "$FAKE_GITEA_TOKEN$NL" \
|
||||
'collect_credentials GITEA_TOKEN
|
||||
warn "the token is giteaFAKEtoken1234567890 here"'
|
||||
assert_status "asked" 0 "$STATUS"
|
||||
assert_not_contains "redacted in later messages" "$ERR" "$FAKE_GITEA_TOKEN"
|
||||
}
|
||||
|
||||
test_an_empty_value_in_env_counts_as_not_provided() {
|
||||
printf 'GITEA_TOKEN=\nGITHUB_USER=\n' >"$WORK/e.env"
|
||||
run_lib "$FAKE_GITEA_TOKEN$NL" \
|
||||
'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
|
||||
validate_credentials
|
||||
collect_credentials GITEA_TOKEN
|
||||
echo asked-ok'
|
||||
assert_status "empty value tolerated" 0 "$STATUS"
|
||||
assert_contains "asked instead" "$ERR" "Gitea access token"
|
||||
}
|
||||
|
||||
test_validate_credentials_no_longer_requires_any() {
|
||||
printf '# nothing\n' >"$WORK/e.env"
|
||||
run_lib "" 'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
|
||||
validate_credentials
|
||||
echo fine'
|
||||
assert_status "no credential required" 0 "$STATUS"
|
||||
assert_eq "no error" "fine" "$OUT"
|
||||
printf 'GITEA_TOKEN=short\n' >"$WORK/e.env"
|
||||
run_lib "" 'parse_env_file "'"$WORK"'/e.env" CREDENTIAL_KEYS CREDENTIALS
|
||||
validate_credentials'
|
||||
assert_status "a provided bad token is still refused" 1 "$STATUS"
|
||||
assert_contains "named" "$ERR" "GITEA_TOKEN"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------- the run
|
||||
|
||||
test_env_is_optional_and_the_token_is_asked_without_echo() {
|
||||
setup_hosts
|
||||
without_env
|
||||
run_dry_cred "$FAKE_GITEA_TOKEN$NL$ANSWERS_GITEA_ONLY"
|
||||
assert_status "dry run without .env" 0 "$STATUS"
|
||||
assert_contains "token asked" "$ERR" "Gitea access token (input is hidden)"
|
||||
assert_contains "plan printed" "$OUT" "Plan:"
|
||||
assert_not_contains "token not shown" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
|
||||
assert_not_contains "only reads" "$(calls)" "POST"
|
||||
}
|
||||
|
||||
test_a_provided_credential_is_not_asked() {
|
||||
setup_hosts
|
||||
run_dry_cred "$ANSWERS_GITHUB"
|
||||
assert_status "all provided" 0 "$STATUS"
|
||||
assert_not_contains "no token prompt" "$ERR" "(input is hidden)"
|
||||
assert_not_contains "no account prompt" "$ERR" "GitHub account name"
|
||||
}
|
||||
|
||||
test_github_credentials_are_asked_only_when_github_is_chosen() {
|
||||
setup_hosts
|
||||
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
|
||||
run_dry_cred "$ANSWERS_GITEA_ONLY"
|
||||
assert_status "Gitea only" 0 "$STATUS"
|
||||
assert_not_contains "no GitHub token asked" "$ERR" "GitHub personal access token"
|
||||
assert_not_contains "no GitHub account asked" "$ERR" "GitHub account name"
|
||||
run_dry_cred "$ANSWERS_GITHUB$FAKE_GITHUB_PAT${NL}octo-user$NL"
|
||||
assert_status "GitHub chosen" 0 "$STATUS"
|
||||
assert_contains "GitHub token asked" "$ERR" "GitHub personal access token (input is hidden)"
|
||||
assert_contains "GitHub account asked" "$ERR" "GitHub account name"
|
||||
assert_not_contains "token not shown" "$OUT$ERR" "$FAKE_GITHUB_PAT"
|
||||
}
|
||||
|
||||
test_an_invalid_asked_value_is_asked_again_and_never_shown() {
|
||||
setup_hosts
|
||||
without_env
|
||||
run_dry_cred "bad token${NL}$FAKE_GITEA_TOKEN$NL$ANSWERS_GITEA_ONLY"
|
||||
assert_status "second answer accepted" 0 "$STATUS"
|
||||
assert_contains "told it is invalid" "$ERR" "invalid Gitea access token"
|
||||
assert_not_contains "refused value not shown" "$ERR$OUT" "bad token"
|
||||
}
|
||||
|
||||
test_input_that_ends_stops_before_any_request() {
|
||||
setup_hosts
|
||||
without_env
|
||||
run_dry_cred ""
|
||||
assert_status "stopped" 1 "$STATUS"
|
||||
assert_contains "key named" "$ERR" "no input available for 'Gitea access token'"
|
||||
assert_eq "no request made" "" "$(calls)"
|
||||
assert_not_contains "no creation report" "$OUT" "This is what exists now"
|
||||
}
|
||||
|
||||
test_asked_tokens_do_not_leak_under_bash_x() {
|
||||
setup_hosts
|
||||
without_env
|
||||
STATUS=0
|
||||
PATH="$WORK/bin:$PATH" STUB_DIR="$WORK" TMPDIR="$WORK/tmp" "$BASH" -x "$SCRIPT" \
|
||||
--config "$WORK/config.env" --env "$WORK/.env" <<<"$(credentials_input)" \
|
||||
>"$WORK/out.txt" 2>"$WORK/err.txt" || STATUS=$?
|
||||
assert_status "run under bash -x" 0 "$STATUS"
|
||||
assert_not_contains "no Gitea token in the trace" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "$FAKE_GITEA_TOKEN"
|
||||
assert_not_contains "no GitHub token in the trace" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "$FAKE_GITHUB_PAT"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------- the project .env
|
||||
|
||||
test_the_dry_run_names_the_env_step_and_writes_nothing() {
|
||||
setup_hosts
|
||||
run_dry_cred "$ANSWERS_GITHUB"
|
||||
assert_contains "plan line" "$OUT" "Project .env : you are asked whether to create it (GITEA_TOKEN GITHUB_PAT GITHUB_USER)"
|
||||
assert_file_missing "no .env" "$WORK/my-app/.env"
|
||||
}
|
||||
|
||||
test_the_project_env_is_not_created_without_a_yes() {
|
||||
setup_hosts
|
||||
run_apply_cred "${ANSWERS_GITHUB}y$NL$NL"
|
||||
assert_status "run" 0 "$STATUS"
|
||||
assert_file_missing "default is no" "$WORK/my-app/.env"
|
||||
assert_contains "reported" "$OUT" "Project .env : skipped (you declined)"
|
||||
setup_hosts
|
||||
run_apply_cred "${ANSWERS_GITHUB}y${NL}n$NL"
|
||||
assert_file_missing "explicit no" "$WORK/my-app/.env"
|
||||
}
|
||||
|
||||
test_the_project_env_holds_only_the_needed_keys_and_is_private() {
|
||||
setup_hosts
|
||||
run_apply_cred "${ANSWERS_GITHUB}y${NL}y$NL"
|
||||
assert_status "run" 0 "$STATUS"
|
||||
assert_file_exists ".env created" "$WORK/my-app/.env"
|
||||
assert_eq "exactly the needed keys" "GITEA_TOKEN=$FAKE_GITEA_TOKEN${NL}GITHUB_PAT=$FAKE_GITHUB_PAT${NL}GITHUB_USER=octo-user" "$(cat "$WORK/my-app/.env")"
|
||||
assert_eq "owner-only" "$(env_mode "$WORK/my-app/.env")" "$([[ -z "$(env_mode "$WORK/my-app/.env")" ]] || echo 600)"
|
||||
assert_contains "reported with the keys, not the values" "$OUT" "Project .env : created (GITEA_TOKEN GITHUB_PAT GITHUB_USER;"
|
||||
assert_not_contains "no token in the output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
|
||||
assert_not_contains "no GitHub token in the output" "$OUT$ERR" "$FAKE_GITHUB_PAT"
|
||||
# Git ignores it without any tracked file changing.
|
||||
check
|
||||
if ! git -C "$WORK/my-app" check-ignore -q -- .env; then
|
||||
fail ".env is not ignored by git"
|
||||
fi
|
||||
assert_not_contains "not listed by git status" "$(git -C "$WORK/my-app" status --porcelain)" ".env"
|
||||
assert_contains "excluded locally" "$(cat "$WORK/my-app/.git/info/exclude")" ".env"
|
||||
check
|
||||
if [[ -e $WORK/my-app/.gitignore ]]; then
|
||||
fail "a .gitignore was written; only .git/info/exclude may change"
|
||||
fi
|
||||
# No temporary file is left behind.
|
||||
assert_eq "no leftover file" "" "$(find "$WORK/my-app" -maxdepth 1 -name '.env.*' -print)"
|
||||
}
|
||||
|
||||
test_the_project_env_without_github_holds_only_the_gitea_token() {
|
||||
setup_hosts
|
||||
run_apply_cred "${ANSWERS_GITEA_ONLY}y${NL}y$NL"
|
||||
assert_status "run" 0 "$STATUS"
|
||||
assert_eq "one key" "GITEA_TOKEN=$FAKE_GITEA_TOKEN" "$(cat "$WORK/my-app/.env")"
|
||||
assert_contains "reported" "$OUT" "Project .env : created (GITEA_TOKEN;"
|
||||
}
|
||||
|
||||
test_asked_credentials_are_what_the_project_env_holds() {
|
||||
setup_hosts
|
||||
without_env
|
||||
run_apply_cred "$(credentials_input)${NL}y${NL}y$NL"
|
||||
assert_status "run" 0 "$STATUS"
|
||||
assert_eq "the asked values" "GITEA_TOKEN=$FAKE_GITEA_TOKEN${NL}GITHUB_PAT=$FAKE_GITHUB_PAT${NL}GITHUB_USER=octo-user" "$(cat "$WORK/my-app/.env")"
|
||||
assert_not_contains "no token in the output" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
|
||||
}
|
||||
|
||||
test_no_token_is_in_any_file_but_the_project_env() {
|
||||
setup_hosts
|
||||
without_env
|
||||
run_apply_cred "$(credentials_input)${NL}y${NL}y$NL"
|
||||
assert_status "run" 0 "$STATUS"
|
||||
local hits
|
||||
# The new project (with its .git folder), the script's temporary directory
|
||||
# and its output; the stub curl's own request log is not part of the product.
|
||||
hits="$(grep -rIl -F -e "$FAKE_GITEA_TOKEN" -e "$FAKE_GITHUB_PAT" "$WORK/my-app" "$WORK/tmp" "$WORK/out.txt" "$WORK/err.txt" 2>/dev/null |
|
||||
grep -v -e '/my-app/\.env$' || true)"
|
||||
assert_eq "only the project .env holds a token" "" "$hits"
|
||||
}
|
||||
|
||||
test_an_existing_env_is_kept_unless_the_maintainer_says_replace() {
|
||||
setup_hosts
|
||||
mkdir -p "$WORK/my-app"
|
||||
printf 'keep\n' >"$WORK/my-app/.env"
|
||||
run_apply_cred "${ANSWERS_GITHUB}y${NL}y${NL}y${NL}n$NL"
|
||||
assert_status "declined replacing" 0 "$STATUS"
|
||||
assert_eq "unchanged" "keep" "$(cat "$WORK/my-app/.env")"
|
||||
assert_contains "reported" "$OUT" "Project .env : kept (the existing .env was left as it was)"
|
||||
remove_workdir
|
||||
new_workdir
|
||||
setup_hosts
|
||||
mkdir -p "$WORK/my-app"
|
||||
printf 'keep\n' >"$WORK/my-app/.env"
|
||||
run_apply_cred "${ANSWERS_GITHUB}y${NL}y${NL}y${NL}y$NL"
|
||||
assert_status "agreed to replace" 0 "$STATUS"
|
||||
assert_contains "replaced" "$(cat "$WORK/my-app/.env")" "GITEA_TOKEN=$FAKE_GITEA_TOKEN"
|
||||
assert_eq "private after replacing" "$(env_mode "$WORK/my-app/.env")" "$([[ -z "$(env_mode "$WORK/my-app/.env")" ]] || echo 600)"
|
||||
}
|
||||
|
||||
test_a_tracked_env_is_never_written() {
|
||||
mkdir -p "$WORK/p"
|
||||
git -C "$WORK/p" init -q
|
||||
printf 'tracked\n' >"$WORK/p/.env"
|
||||
git -C "$WORK/p" add .env
|
||||
git -C "$WORK/p" -c user.name=t -c user.email=t@example.test commit -q -m init
|
||||
run_lib "y$NL" \
|
||||
'PROJECT[directory]="'"$WORK"'/p"; PROJECT[has_github]=0
|
||||
CREDENTIALS[GITEA_TOKEN]=giteaFAKEtoken1234567890
|
||||
init_steps
|
||||
create_env_file
|
||||
echo "${STEP_STATUS["Project .env"]}"'
|
||||
assert_status "run" 0 "$STATUS"
|
||||
assert_eq "skipped" "skipped" "$OUT"
|
||||
assert_eq "unchanged" "tracked" "$(cat "$WORK/p/.env")"
|
||||
assert_contains "says why" "$(cat "$WORK/err.txt" "$WORK/out.txt")" "skipped"
|
||||
}
|
||||
|
||||
test_exclude_is_added_once_and_keeps_the_existing_entries() {
|
||||
mkdir -p "$WORK/p"
|
||||
git -C "$WORK/p" init -q
|
||||
printf 'build/' >"$WORK/p/.git/info/exclude" # no trailing newline
|
||||
run_lib "" \
|
||||
'exclude_env_file "'"$WORK"'/p"
|
||||
exclude_env_file "'"$WORK"'/p"
|
||||
echo done'
|
||||
assert_status "run" 0 "$STATUS"
|
||||
local exclude
|
||||
exclude="$(cat "$WORK/p/.git/info/exclude")"
|
||||
assert_contains "old entry kept" "$exclude" "build/"
|
||||
assert_eq "entry added once" "1" "$(grep -c '^\.env$' "$WORK/p/.git/info/exclude")"
|
||||
assert_eq "old entry still on its own line" "1" "$(grep -c '^build/$' "$WORK/p/.git/info/exclude")"
|
||||
}
|
||||
|
||||
test_exclude_does_nothing_when_env_is_already_ignored() {
|
||||
mkdir -p "$WORK/p"
|
||||
git -C "$WORK/p" init -q
|
||||
printf '.env\n' >"$WORK/p/.gitignore"
|
||||
run_lib "" 'exclude_env_file "'"$WORK"'/p"; echo done'
|
||||
assert_status "run" 0 "$STATUS"
|
||||
assert_eq "exclude file untouched" "0" "$(grep -c '^\.env$' "$WORK/p/.git/info/exclude" || true)"
|
||||
}
|
||||
@@ -47,12 +47,12 @@ test_full_run_without_github() {
|
||||
assert_contains "plan says GitHub is not used" "$OUT" "GitHub repository : not used"
|
||||
}
|
||||
|
||||
test_github_chosen_without_credentials_fails() {
|
||||
test_github_chosen_without_credentials_stops_when_input_ends() {
|
||||
write_fixtures
|
||||
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
|
||||
run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env"
|
||||
assert_status "missing GitHub credentials" 1 "$STATUS"
|
||||
assert_contains "names the key" "$ERR" "GITHUB_PAT is missing"
|
||||
assert_status "missing GitHub credentials, no answer" 1 "$STATUS"
|
||||
assert_contains "names the credential" "$ERR" "no input available for 'GitHub personal access token'"
|
||||
assert_not_contains "no token in the error" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
|
||||
assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user