Author SHA1 Message Date
TirsvadandClaude Sonnet 5.5 05a7159c18 Ask for missing credentials and create the project's own .env
.env becomes optional. A credential it does not provide (an absent file, an
absent or empty key) is asked, without echo: GITEA_TOKEN at the start,
GITHUB_PAT and GITHUB_USER once GitHub is chosen. An invalid value is asked
again and never shown; when input ends the run stops before any request.
Asked tokens are registered for redaction at once.

After the local project exists the script asks (default no) whether to
create a .env in it. On a yes it holds only the needed keys, is created
private (mode 600) from the start, is excluded from git through
.git/info/exclude (no tracked file changes), is never replaced without a
second yes and is never written when git tracks it. The summary names the
keys, never the values.

New library files credentials.sh and envfile.sh; README, .env.example and
the security decisions updated; tests cover every case.

Task: MIL-005#1
Task: MIL-005#2
Task: MIL-005#3
Task: MIL-005#4
Task: MIL-005#5
Closes #35
Closes #36
Closes #37
Closes #38
Closes #39

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 13:29:01 +08:00
Tirsvad 3804ef7556 Merge pull request 'Add the Design Class Diagrams and plan MIL-005 (credentials asked, project .env)' (#40) from dcd-uc-001 into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 4s
Reviewed-on: #40
2026-10-06 07:01:40 +02:00
13 changed files with 544 additions and 50 deletions
+5 -2
View File
@@ -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=
+34 -4
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
+16 -20
View File
@@ -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.
SECRET_VALUES+=("${CREDENTIALS[GITEA_TOKEN]}")
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
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
# .env is optional: a credential it does not provide is asked.
if [[ -e $ENV_FILE ]]; then
parse_env_file "$ENV_FILE" CREDENTIAL_KEYS CREDENTIALS
warn_if_env_unsafe "$ENV_FILE"
fi
validate_credentials
warn_if_env_unsafe "$ENV_FILE"
}
+3 -1
View File
@@ -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
+42
View File
@@ -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
}
+97
View File
@@ -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)"
}
+1
View File
@@ -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
View File
@@ -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
-13
View File
@@ -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
+305
View File
@@ -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)"
}
+3 -3
View File
@@ -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)"
}