Files
RepoFoundry/src/create-project.sh
T
TirsvadandClaude Sonnet 5.5 ceaa7d18d8 Code review of MIL-004: document quoting of values, add RC-019
- Say in the README and config.env.example that a value containing " #"
  must be quoted, and pin both behaviours with a test.
- Say in the script header and the README that details set in config.env
  are not asked.
- Record the review as RC-019 and link it in the traceability matrix.

Task: MIL-004#4
Task: MIL-004#5
Refs #30
Refs #31

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 23:18:29 +08:00

188 lines
6.9 KiB
Bash

#!/usr/bin/env bash
# create-project.sh - set up a new project on Gitea (and optionally GitHub).
#
# Purpose
# RepoFoundry creates a Gitea repository, optionally an empty GitHub
# repository with a Gitea -> GitHub push mirror, and a local project with
# the SQA-QC-Framework. It validates the configuration and credentials,
# asks for the project details (those set in config.env are not asked), checks both hosts with read-only requests
# (tokens, owners, names, license, SSH) and, with --apply, creates the
# repositories and the mirror, then the local project: its directory, git
# repository, remotes (no credential in any address), the framework as a
# submodule, the framework's skills and git hooks (and the plan gate if
# chosen) and its templates. Choosing GitHub also applies the AGPL-3.0
# license to the Gitea repository. No commit is made in the new project.
#
# Dry run by default
# Without --apply the script only reads from GitHub and Gitea (GET
# requests) and prints what it would create. With --apply it prints the plan
# and asks for a final yes before it creates anything. Nothing is ever
# deleted: if a step fails, the script reports what exists and how to
# continue, and a repeated run offers to reuse the empty repositories.
#
# Usage
# create-project.sh [--apply] [--config FILE] [--env FILE]
# create-project.sh --help | --version
#
# 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)
# -h, --help show this help
# --version show the version
#
# Files (parsed, never sourced)
# config.env GITHUB_API_URL, GITHUB_WEB_URL, GITEA_URL, GITEA_API_URL and
# 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
#
# Environment
# REPOFOUNDRY_NAME project name used in messages (default: RepoFoundry)
# REPOFOUNDRY_SYNC_WAIT seconds to wait before reading the first mirror
# sync result (default: 3)
# TMPDIR where the private temporary directory is created
#
# Requires
# bash 4.4 or later, git, curl, mktemp; jq and ssh are optional (jq is used
# for JSON when present; ssh is used for the Gitea SSH test).
# Also the base tools sed, grep, head, tr, sleep, find, cp, mkdir, chmod, env, rm,
# rmdir and uname, and
# stat (GNU "stat -c" or BSD "stat -f"; only used outside Windows).
#
# Implements
# MIL-001 tasks 1 to 6, MIL-002 tasks 1 to 5 and MIL-003 tasks 1 to 4
# (issues #3 to #13 and #15 to #18), user stories US-001.01 to US-001.03,
# UC-001 steps 1 to 10; see docs/. Deviation from the request: its second
# GITEA_URL key is named GITEA_API_URL.
#
# Tracing
# set -x is switched off while the script runs, because a trace would print
# every secret the script handles.
#
# Structure
# 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
# from this directory only.
#
# Exit codes
# 0 success (or a dry run, or a "no" at the final question), 1 a failed
# check, bad input or a failed step, 2 a usage error.
set -Eeuo pipefail
if [[ $- == *x* ]]; then
set +x
printf 'warning: tracing (set -x) is disabled because it would print secrets\n' >&2
fi
if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4))); then
printf 'error: bash 4.4 or later is required (found %s)\n' "$BASH_VERSION" >&2
exit 1
fi
# Where this script lives; the library files and the project root are found
# from here, never from the current directory.
readonly SCRIPT_FILE="${BASH_SOURCE[0]}"
case "${BASH_SOURCE[0]}" in
*/*) script_path_dir="${BASH_SOURCE[0]%/*}" ;;
*) script_path_dir="." ;;
esac
SCRIPT_DIR="$(cd "$script_path_dir" && pwd)"
readonly SCRIPT_DIR
unset script_path_dir
# The script lives in src/; the configuration files live one level up, in
# the project root, next to config.env.example and .env.example.
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
readonly PROJECT_ROOT
# shellcheck source=lib/constants.sh
source "$SCRIPT_DIR/lib/constants.sh"
# shellcheck source=lib/output.sh
source "$SCRIPT_DIR/lib/output.sh"
# shellcheck source=lib/temp.sh
source "$SCRIPT_DIR/lib/temp.sh"
# shellcheck source=lib/util.sh
source "$SCRIPT_DIR/lib/util.sh"
# shellcheck source=lib/validate.sh
source "$SCRIPT_DIR/lib/validate.sh"
# shellcheck source=lib/config.sh
source "$SCRIPT_DIR/lib/config.sh"
# shellcheck source=lib/tools.sh
source "$SCRIPT_DIR/lib/tools.sh"
# shellcheck source=lib/json.sh
source "$SCRIPT_DIR/lib/json.sh"
# shellcheck source=lib/http.sh
source "$SCRIPT_DIR/lib/http.sh"
# shellcheck source=lib/api.sh
source "$SCRIPT_DIR/lib/api.sh"
# shellcheck source=lib/prompts.sh
source "$SCRIPT_DIR/lib/prompts.sh"
# shellcheck source=lib/project.sh
source "$SCRIPT_DIR/lib/project.sh"
# shellcheck source=lib/hosts.sh
source "$SCRIPT_DIR/lib/hosts.sh"
# shellcheck source=lib/preflight.sh
source "$SCRIPT_DIR/lib/preflight.sh"
# shellcheck source=lib/steps.sh
source "$SCRIPT_DIR/lib/steps.sh"
# shellcheck source=lib/plan.sh
source "$SCRIPT_DIR/lib/plan.sh"
# shellcheck source=lib/repositories.sh
source "$SCRIPT_DIR/lib/repositories.sh"
# shellcheck source=lib/mirror.sh
source "$SCRIPT_DIR/lib/mirror.sh"
# shellcheck source=lib/git.sh
source "$SCRIPT_DIR/lib/git.sh"
# shellcheck source=lib/localproject.sh
source "$SCRIPT_DIR/lib/localproject.sh"
# shellcheck source=lib/framework.sh
source "$SCRIPT_DIR/lib/framework.sh"
# shellcheck source=lib/apply.sh
source "$SCRIPT_DIR/lib/apply.sh"
# shellcheck source=lib/cli.sh
source "$SCRIPT_DIR/lib/cli.sh"
# finish runs on every exit: it reports what a run that started creating
# things did or did not do, then removes the temporary files. It keeps the
# exit status of the run.
finish() {
local code=$?
if ((IS_CREATION_STARTED)); then
report_outcome "$code"
fi
cleanup
}
main() {
trap 'on_error "$LINENO"' ERR
trap finish EXIT
is_valid_repo_name "$PROJECT_NAME" ||
die "REPOFOUNDRY_NAME is not a valid project name"
parse_args "$@"
check_tools
setup_temp_dir
load_configuration
collect_project_details
if ((PROJECT[has_github])); then
require_github_credentials
fi
init_steps
print_summary
run_preflight
print_plan
if ((IS_APPLY)); then
apply_plan
else
say ""
say "Dry run: nothing was created. Run again with --apply to create it."
fi
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi