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
|
# RepoFoundry credentials. Placeholders only: never put a real value in this
|
||||||
# file or commit one.
|
# 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
|
# 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
|
# (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
|
# read as plain KEY=VALUE lines and never executed. Values may be wrapped in
|
||||||
@@ -13,7 +16,7 @@
|
|||||||
GITHUB_PAT=
|
GITHUB_PAT=
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
# Secrets for workframe
|
# Secrets for framework
|
||||||
########################################
|
########################################
|
||||||
|
|
||||||
# GitHub account the token belongs to. It identifies who authenticates; it is
|
# GitHub account the token belongs to. It identifies who authenticates; it is
|
||||||
@@ -21,6 +24,6 @@ GITHUB_PAT=
|
|||||||
# belong to an organization.
|
# belong to an organization.
|
||||||
GITHUB_USER=
|
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.
|
# for the chosen owner and to manage the repository's push mirror.
|
||||||
GITEA_TOKEN=
|
GITEA_TOKEN=
|
||||||
|
|||||||
@@ -129,14 +129,40 @@ src/create-project.sh --apply # asks only "Create these now (y/n) [n]"
|
|||||||
|
|
||||||
| Key | Meaning |
|
| 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_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
|
`.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
|
not ignored by git. See [Token permissions](#token-permissions) for what each
|
||||||
token needs.
|
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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -264,7 +290,9 @@ script stops before it creates anything when a preflight check is refused.
|
|||||||
## Security decisions
|
## Security decisions
|
||||||
|
|
||||||
- **Tokens never appear** in output, logs, remote URLs, `.git/config`,
|
- **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
|
private configuration file that is removed right after the request, and to
|
||||||
`git` (HTTPS fetch only) through a `GIT_ASKPASS` helper and the environment
|
`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
|
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 |
|
| `json.sh` | the little JSON the script reads and writes |
|
||||||
| `http.sh` | the one place that runs `curl`; tokens stay off the command line |
|
| `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 |
|
| `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 |
|
| `project.sh` | the project details: asking for them and showing them |
|
||||||
| `hosts.sh` | names, links and remote addresses of the repositories |
|
| `hosts.sh` | names, links and remote addresses of the repositories |
|
||||||
| `preflight.sh` | read-only checks of both hosts |
|
| `preflight.sh` | read-only checks of both hosts |
|
||||||
| `steps.sh` | the outcome of each step and the final report |
|
| `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 |
|
| `plan.sh` | printing what the script is about to do |
|
||||||
| `repositories.sh` | creating the GitHub and Gitea repositories |
|
| `repositories.sh` | creating the GitHub and Gitea repositories |
|
||||||
| `mirror.sh` | the Gitea to GitHub push mirror |
|
| `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
|
# repository, remotes (no credential in any address), the framework as a
|
||||||
# submodule, the framework's skills and git hooks (and the plan gate if
|
# 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
|
# 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
|
# Dry run by default
|
||||||
# Without --apply the script only reads from GitHub and Gitea (GET
|
# Without --apply the script only reads from GitHub and Gitea (GET
|
||||||
@@ -27,7 +29,8 @@
|
|||||||
# Options
|
# Options
|
||||||
# --apply create the repositories and the mirror (after a final yes)
|
# --apply create the repositories and the mirror (after a final yes)
|
||||||
# --config FILE service addresses (default: config.env in the project root)
|
# --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
|
# -h, --help show this help
|
||||||
# --version show the version
|
# --version show the version
|
||||||
#
|
#
|
||||||
@@ -36,7 +39,8 @@
|
|||||||
# the optional GITEA_SSH_PORT (default 10022), MIRROR_INTERVAL
|
# the optional GITEA_SSH_PORT (default 10022), MIRROR_INTERVAL
|
||||||
# (default 10m0s) and FRAMEWORK_REPO (default
|
# (default 10m0s) and FRAMEWORK_REPO (default
|
||||||
# TirSystem/SQA-QC-Framework, the submodule's OWNER/NAME)
|
# 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
|
# Environment
|
||||||
# REPOFOUNDRY_NAME project name used in messages (default: RepoFoundry)
|
# 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
|
# 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
|
# 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,
|
# each file): constants, output, temp, util, validate, config, tools, json,
|
||||||
# http, api, prompts, project, hosts, preflight, steps, plan, repositories,
|
# http, api, prompts, credentials, project, hosts, preflight, steps, plan,
|
||||||
# mirror, git, localproject, framework, apply and cli. The files are loaded
|
# repositories, mirror, git, localproject, framework, envfile, apply and cli. The files are loaded
|
||||||
# from this directory only.
|
# from this directory only.
|
||||||
#
|
#
|
||||||
# Exit codes
|
# Exit codes
|
||||||
@@ -121,6 +125,8 @@ source "$SCRIPT_DIR/lib/http.sh"
|
|||||||
source "$SCRIPT_DIR/lib/api.sh"
|
source "$SCRIPT_DIR/lib/api.sh"
|
||||||
# shellcheck source=lib/prompts.sh
|
# shellcheck source=lib/prompts.sh
|
||||||
source "$SCRIPT_DIR/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
|
# shellcheck source=lib/project.sh
|
||||||
source "$SCRIPT_DIR/lib/project.sh"
|
source "$SCRIPT_DIR/lib/project.sh"
|
||||||
# shellcheck source=lib/hosts.sh
|
# shellcheck source=lib/hosts.sh
|
||||||
@@ -141,6 +147,8 @@ source "$SCRIPT_DIR/lib/git.sh"
|
|||||||
source "$SCRIPT_DIR/lib/localproject.sh"
|
source "$SCRIPT_DIR/lib/localproject.sh"
|
||||||
# shellcheck source=lib/framework.sh
|
# shellcheck source=lib/framework.sh
|
||||||
source "$SCRIPT_DIR/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
|
# shellcheck source=lib/apply.sh
|
||||||
source "$SCRIPT_DIR/lib/apply.sh"
|
source "$SCRIPT_DIR/lib/apply.sh"
|
||||||
# shellcheck source=lib/cli.sh
|
# shellcheck source=lib/cli.sh
|
||||||
@@ -166,9 +174,10 @@ main() {
|
|||||||
check_tools
|
check_tools
|
||||||
setup_temp_dir
|
setup_temp_dir
|
||||||
load_configuration
|
load_configuration
|
||||||
|
collect_credentials GITEA_TOKEN
|
||||||
collect_project_details
|
collect_project_details
|
||||||
if ((PROJECT[has_github])); then
|
if ((PROJECT[has_github])); then
|
||||||
require_github_credentials
|
collect_credentials GITHUB_PAT GITHUB_USER
|
||||||
fi
|
fi
|
||||||
init_steps
|
init_steps
|
||||||
print_summary
|
print_summary
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ create_all() {
|
|||||||
add_framework
|
add_framework
|
||||||
install_framework
|
install_framework
|
||||||
copy_templates
|
copy_templates
|
||||||
|
create_env_file
|
||||||
}
|
}
|
||||||
|
|
||||||
# confirm_framework_access: the framework comes over SSH. Without SSH the
|
# confirm_framework_access: the framework comes over SSH. Without SSH the
|
||||||
|
|||||||
+16
-20
@@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# Part of create-project.sh: sourced by it, never run on its own.
|
# 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
|
# unquote_value RAW: strip matching quotes (or a trailing " # comment" on an
|
||||||
# unquoted value) and return the value in REPLY. Fails on unbalanced quotes.
|
# 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
|
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() {
|
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.
|
# 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
|
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]]; then
|
||||||
SECRET_VALUES+=("${CREDENTIALS[GITHUB_PAT]}")
|
SECRET_VALUES+=("${CREDENTIALS[GITHUB_PAT]}")
|
||||||
fi
|
fi
|
||||||
is_valid_token "${CREDENTIALS[GITEA_TOKEN]}" ||
|
if [[ -n ${CREDENTIALS[GITEA_TOKEN]:-} ]] &&
|
||||||
die "GITEA_TOKEN in $ENV_FILE is not a valid token (8 to 255 letters, digits or _ . ~ + / = -)"
|
! 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]:-} ]] &&
|
if [[ -n ${CREDENTIALS[GITHUB_PAT]:-} ]] &&
|
||||||
! is_valid_token "${CREDENTIALS[GITHUB_PAT]}"; then
|
! 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
|
fi
|
||||||
if [[ -n ${CREDENTIALS[GITHUB_USER]:-} ]] &&
|
if [[ -n ${CREDENTIALS[GITHUB_USER]:-} ]] &&
|
||||||
! is_valid_github_owner "${CREDENTIALS[GITHUB_USER]}"; then
|
! is_valid_github_owner "${CREDENTIALS[GITHUB_USER]}"; then
|
||||||
@@ -165,16 +168,6 @@ validate_credentials() {
|
|||||||
fi
|
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() {
|
warn_if_env_unsafe() {
|
||||||
local file="$1" dir mode
|
local file="$1" dir mode
|
||||||
case "$(uname -s 2>/dev/null || true)" in
|
case "$(uname -s 2>/dev/null || true)" in
|
||||||
@@ -200,7 +193,10 @@ warn_if_env_unsafe() {
|
|||||||
load_configuration() {
|
load_configuration() {
|
||||||
parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG
|
parse_env_file "$CONFIG_FILE" CONFIG_KEYS CONFIG
|
||||||
validate_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
|
validate_credentials
|
||||||
warn_if_env_unsafe "$ENV_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_GITEA_OWNER="use letters, digits, '.', '_' or '-' (at most 39)"
|
||||||
readonly HINT_GITHUB_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_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"
|
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)
|
# shellcheck disable=SC2034 # read through namerefs (parse_env_file)
|
||||||
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
|
readonly CONFIG_KEYS=(GITHUB_API_URL GITHUB_WEB_URL GITEA_URL GITEA_API_URL
|
||||||
GITEA_SSH_PORT MIRROR_INTERVAL FRAMEWORK_REPO
|
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
|
else
|
||||||
say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")"
|
say "$(printf ' %-18s: %s' "Framework" "NOT possible without SSH to Gitea; you will be asked whether to go on without it")"
|
||||||
fi
|
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.
|
# 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
|
# prompt_value LABEL DEFAULT VALIDATOR HINT: ask until VALIDATOR accepts the
|
||||||
# answer; the accepted answer is returned in REPLY.
|
# answer; the accepted answer is returned in REPLY.
|
||||||
@@ -27,6 +27,27 @@ prompt_value() {
|
|||||||
done
|
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 LABEL DEFAULT CHOICE...: the answer is returned in REPLY.
|
||||||
prompt_choice() {
|
prompt_choice() {
|
||||||
local label="$1" default="$2" answer
|
local label="$1" default="$2" answer
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ validate_credentials"
|
|||||||
assert_status "$case_name" 1 "$STATUS"
|
assert_status "$case_name" 1 "$STATUS"
|
||||||
assert_contains "$case_name message" "$ERR" "$expected"
|
assert_contains "$case_name message" "$ERR" "$expected"
|
||||||
done <<'EOF'
|
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 too short|GITEA_TOKEN=short\n|not a valid token
|
||||||
token with a backslash|GITEA_TOKEN=abc\\defgh12345\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
|
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
|
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() {
|
test_validators() {
|
||||||
local fn value expected
|
local fn value expected
|
||||||
while IFS='|' read -r fn value expected; do
|
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"
|
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
|
write_fixtures
|
||||||
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
|
printf 'GITEA_TOKEN=%s\n' "$FAKE_GITEA_TOKEN" >"$WORK/.env"
|
||||||
run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env"
|
run_cli "$ANSWERS_GITHUB" --config "$WORK/config.env" --env "$WORK/.env"
|
||||||
assert_status "missing GitHub credentials" 1 "$STATUS"
|
assert_status "missing GitHub credentials, no answer" 1 "$STATUS"
|
||||||
assert_contains "names the key" "$ERR" "GITHUB_PAT is missing"
|
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_not_contains "no token in the error" "$OUT$ERR" "$FAKE_GITEA_TOKEN"
|
||||||
assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)"
|
assert_eq "temporary files removed" "" "$(find "$WORK/tmp" -mindepth 1)"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user