From fdbbd0516acdfe55786f5a5e9a2effb9c1a4af7d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 8 Jul 2026 20:42:03 +0200 Subject: [PATCH] added `setup-gittally-instance` script: migrates legacy `.gitTally` configurations to YAML, sets up a GitTally instance on a Docker host, and prompts for Gitea secrets --- tools/setup-gittally-instance | 278 ++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100755 tools/setup-gittally-instance diff --git a/tools/setup-gittally-instance b/tools/setup-gittally-instance new file mode 100755 index 0000000..780a443 --- /dev/null +++ b/tools/setup-gittally-instance @@ -0,0 +1,278 @@ +#!/usr/bin/env bash +# +# Set up a fresh GitTally instance on a new Docker host by converting a legacy +# .gitTally env configuration to the new YAML format. +# +# The legacy config comes either from a file passed as the optional third +# argument, or — when omitted — from the embedded snapshot of the hs.hsadmin.ng +# .gitTally on vm2176. Either way the script sources it, converts it to YAML +# (mapping per docs/migration-from-legacy.md), substitutes the public hostname +# from the command line, and prompts for the Gitea secrets. +# +# Run this ON the new Docker host. The repository must already be cloned at the +# given repo-dir. Builds run in Docker; GitTally itself runs from the jar via +# java, so a JRE and the jar are required (see checks below). +# +# Usage: setup-gittally-instance [--force] [legacy-config] +# FQDN this instance is reachable at, e.g. vm2180.hostsharing.net +# (used for server.publicBaseUrl) +# path to the cloned git repository to configure, +# e.g. ~/hs.hsadmin.ng +# [legacy-config] optional path to a legacy .gitTally file to convert; +# if omitted, the embedded vm2176 snapshot is used +# --force overwrite .gittally.yml files that already exist + +set -euo pipefail + +# ---------------------------------------------------------------- settings -- + +JAR_PATH="$HOME/bin/gittally.jar" # where the GitTally jar is expected + +die() { echo "ERROR: $*" >&2; exit 1; } +warn() { echo "WARNING: $*" >&2; } + +usage() { + echo "usage: setup-gittally-instance [--force] [legacy-config]" >&2 + echo "example: setup-gittally-instance vm2180.hostsharing.net ~/hs.hsadmin.ng" >&2 + exit 1 +} + +# --------------------------------------------------------------- arguments -- + +force=false +positional=() +for arg in "$@"; do + case "$arg" in + --force) force=true ;; + -*) die "unknown option: $arg" ;; + *) positional+=("$arg") ;; + esac +done +case ${#positional[@]} in + 2) legacy_file="" ;; + 3) legacy_file="${positional[2]}" ;; + *) usage ;; +esac + +hostname="${positional[0]}" +REPO_DIR="${positional[1]}" +[ -z "$legacy_file" ] || [ -f "$legacy_file" ] || die "legacy config not found: $legacy_file" + +# ---------------------------------------------------- legacy config source -- +# load_legacy_config populates the GITTALLY_* variables, either by sourcing the +# file passed on the command line, or from the embedded vm2176 snapshot below. +# Secrets (git username/token) are intentionally never read here — they are +# prompted for, so a token in a legacy file is not echoed or reused implicitly. + +load_legacy_config() { + if [ -n "$legacy_file" ]; then + # shellcheck source=/dev/null + . "$legacy_file" + return + fi + # embedded, non-secret snapshot of tallyman@vm2176:~/hs.hsadmin.ng/.gitTally + export GITTALLY_BUILD_COMMAND='./gradlew --console=plain --no-daemon --no-build-cache jacocoTestReport check generateDocumentation -x pitest -x dependencyCheckAnalyze' + export GITTALLY_BUILD_CLEAN_COMMAND='rm -rf build' + export GITTALLY_BUILD_ARTEFACT_DIRS='build/reports:build/doc' + export GITTALLY_BUILD_STDOUT_LOG='build.stdout.log' + export GITTALLY_BUILD_STDERR_LOG='build.stderr.log' + export GITTALLY_BUILD_DOCKER_IMAGE='hsadmin-ng-build-env:latest' + export GITTALLY_BUILD_DOCKERFILE='Jenkins/jenkins-agent/Dockerfile' + export GITTALLY_BUILD_DOCKER_CONTEXT='Jenkins/jenkins-agent' + export GITTALLY_BUILD_DOCKER_NETWORK='host' + export GITTALLY_BUILD_DOCKER_ENV='TESTCONTAINERS_RYUK_DISABLED=true' + # legacy hsadmin-ng-specific JAVA_TOOL_OPTIONS — carried into docker.env so the + # Testcontainers builds keep finding the docker socket (see migration doc). + export GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS='-Ddocker.client.strategy=org.testcontainers.dockerclient.UnixSocketClientProviderStrategy -Dtestcontainers.docker.socket.override=/var/run/docker.sock' + export GITTALLY_ARTIFACT_SERVER_PORT='18080' + export GITTALLY_ARTIFACT_SERVER_BIND_ADDRESS='0.0.0.0' + export GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH='3' + export GITTALLY_IMPRESSUM_URL='https://michael.hoennig.de/imprint.html' + export GITTALLY_GITEA_BASE_URL='https://dev.hostsharing.net' + export GITTALLY_GITEA_OWNER='hostsharing' + export GITTALLY_GITEA_REPO='hs.hsadmin.ng' + export GITTALLY_GITEA_STATUS_CONTEXT='GitTally' +} + +# ----------------------------------------------------------------- helpers -- + +# quote a scalar for double-quoted YAML +yaml_quote() { + local s=${1//\\/\\\\} + s=${s//\"/\\\"} + printf '"%s"' "$s" +} + +# ------------------------------------------------------------ preconditions -- + +[ -d "$REPO_DIR/.git" ] || die "$REPO_DIR is not a git clone — clone the repository there first" +command -v docker >/dev/null 2>&1 || die "docker not found — this setup targets a Docker host" + +have_jar=true +[ -f "$JAR_PATH" ] || { have_jar=false; warn "jar not found at $JAR_PATH — config will be written, but copy the jar there before running init"; } + +have_java=true +if ! command -v java >/dev/null 2>&1; then + have_java=false + warn "no java in PATH — install a JRE (e.g. Temurin 21 into ~/opt) before running init/server" +fi + +# ----------------------------------------------------------- prompt secrets -- + +# suggest the account from the legacy config's git username, if it sets one +suggested_account=$( set +u; load_legacy_config >/dev/null 2>&1; printf '%s' "${GITTALLY_GITEA_GIT_USERNAME:-jenkins-ci}" ) + +echo "== Gitea credentials for this instance (used for git HTTPS auth + status API)" +read -r -p " Gitea git account [$suggested_account]: " git_account +git_account=${git_account:-$suggested_account} +read -r -s -p " Gitea API token: " git_token; echo +[ -n "$git_token" ] || die "the Gitea token must not be empty" + +# -------------------------------------------------------------- convert cfg -- + +project_yml="$REPO_DIR/.gittally.yml" +machine_yml="$REPO_DIR/.git/gittally/.gittally.yml" +public_base_url="https://$hostname/" + +if ! $force; then + for f in "$project_yml" "$machine_yml"; do + [ -e "$f" ] && die "$f already exists — re-run with --force to overwrite" + done +fi + +config_source=${legacy_file:-"embedded vm2176 snapshot"} +echo "== writing $project_yml (public host: $hostname, source: $config_source)" + +( + set +u + load_legacy_config + + # warn about legacy keys that have no counterpart in the new config + for var in \ + GITTALLY_ARTIFACT_NGINX_SERVER_NAME GITTALLY_ARTIFACT_NGINX_HTTP_PORT \ + GITTALLY_ARTIFACT_NGINX_HTTPS_PORT GITTALLY_ARTIFACT_LETSENCRYPT_EMAIL \ + GITTALLY_ARTIFACT_AUTH_MODE GITTALLY_BUILD_DOCKER_PREFLIGHT_COMMAND \ + GITTALLY_INSTALL_DIR + do + [ -n "${!var}" ] && warn "$var is set but has no counterpart in the new config — skipped (see docs/migration-from-legacy.md)" + done + + # retention: strip a legacy age suffix (h/d), only the build count is supported + retention="$GITTALLY_ARTIFACT_BUILD_RETENTION_PER_BRANCH" + if [ -n "$retention" ] && [[ ! "$retention" =~ ^[0-9]+$ ]]; then + warn "retention '$retention' has an age suffix — only the build count is supported, using '${retention%%[!0-9]*}'" + retention=${retention%%[!0-9]*} + fi + + emit() { printf '%s\n' "$1" >>"$project_yml"; } + + : >"$project_yml" + emit "# GitTally configuration, converted from $config_source" + emit "# by tools/setup-gittally-instance; public host set to $hostname." + emit "# Omitted keys fall back to the application defaults — see docs/configuration.md." + emit "" + + emit "server:" + emit " publicBaseUrl: $(yaml_quote "$public_base_url")" + [ -n "$GITTALLY_ARTIFACT_SERVER_PORT" ] && emit " port: $GITTALLY_ARTIFACT_SERVER_PORT" + [ -n "$GITTALLY_ARTIFACT_SERVER_BIND_ADDRESS" ] && emit " bindAddress: $GITTALLY_ARTIFACT_SERVER_BIND_ADDRESS" + [ -n "$GITTALLY_IMPRESSUM_URL" ] && emit " impressumUrl: $(yaml_quote "$GITTALLY_IMPRESSUM_URL")" + emit "" + + emit "gitea:" + [ -n "$GITTALLY_GITEA_BASE_URL" ] && emit " baseUrl: $(yaml_quote "$GITTALLY_GITEA_BASE_URL")" + [ -n "$GITTALLY_GITEA_OWNER" ] && emit " owner: $(yaml_quote "$GITTALLY_GITEA_OWNER")" + [ -n "$GITTALLY_GITEA_REPO" ] && emit " repo: $(yaml_quote "$GITTALLY_GITEA_REPO")" + [ -n "$GITTALLY_GITEA_STATUS_CONTEXT" ] && emit " statusContext: $(yaml_quote "$GITTALLY_GITEA_STATUS_CONTEXT")" + emit "" + + [ -n "$retention" ] && { emit "artifacts:"; emit " retentionPerBranch: $retention"; emit ""; } + + [ -n "$GITTALLY_NEW_BRANCH_COMMIT_MAX_AGE" ] && { emit "watcher:"; emit " newBranchMaxAge: $GITTALLY_NEW_BRANCH_COMMIT_MAX_AGE"; emit ""; } + + emit "branches:" + emit " default:" + [ -n "$GITTALLY_BUILD_CLEAN_COMMAND" ] && emit " cleanCommand: $(yaml_quote "$GITTALLY_BUILD_CLEAN_COMMAND")" + [ -n "$GITTALLY_BUILD_COMMAND" ] && emit " buildCommand: $(yaml_quote "$GITTALLY_BUILD_COMMAND")" + if [ -n "$GITTALLY_BUILD_ARTEFACT_DIRS" ]; then + emit " artifactDirs:" + # legacy separators: ';' (documented) and ':' (as used on vm2176) + IFS=';:' read -r -a dirs <<<"$GITTALLY_BUILD_ARTEFACT_DIRS" + for dir in "${dirs[@]}"; do + [ -n "$dir" ] && emit " - $(yaml_quote "$dir")" + done + fi + [ -n "$GITTALLY_BUILD_STDOUT_LOG" ] && emit " stdoutLog: $(yaml_quote "$GITTALLY_BUILD_STDOUT_LOG")" + [ -n "$GITTALLY_BUILD_STDERR_LOG" ] && emit " stderrLog: $(yaml_quote "$GITTALLY_BUILD_STDERR_LOG")" + + if [ -n "$GITTALLY_BUILD_DOCKER_IMAGE" ] || [ -n "$GITTALLY_BUILD_DOCKERFILE" ]; then + emit " docker:" + emit " enabled: true" + [ -n "$GITTALLY_BUILD_DOCKER_IMAGE" ] && emit " image: $(yaml_quote "$GITTALLY_BUILD_DOCKER_IMAGE")" + [ -n "$GITTALLY_BUILD_DOCKERFILE" ] && emit " dockerfile: $(yaml_quote "$GITTALLY_BUILD_DOCKERFILE")" + [ -n "$GITTALLY_BUILD_DOCKER_CONTEXT" ] && emit " context: $(yaml_quote "$GITTALLY_BUILD_DOCKER_CONTEXT")" + # legacy default network was host; new default is Docker's default + emit " network: $(yaml_quote "${GITTALLY_BUILD_DOCKER_NETWORK:-host}")" + if [ -n "$GITTALLY_BUILD_DOCKER_ENV" ] || [ -n "$GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS" ]; then + emit " env:" + for assignment in $GITTALLY_BUILD_DOCKER_ENV; do + emit " ${assignment%%=*}: $(yaml_quote "${assignment#*=}")" + done + # hsadmin-ng-specific JAVA_TOOL_OPTIONS carried into docker.env so the + # Testcontainers builds keep finding the docker socket (see migration doc) + [ -n "$GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS" ] && \ + emit " JAVA_TOOL_OPTIONS: $(yaml_quote "$GITTALLY_BUILD_DOCKER_JAVA_TOOL_OPTIONS")" + fi + fi + + # legacy auto-build: a branch list plus global times -> per-branch autoBuild + if [ -n "$GITTALLY_AUTO_BUILD_BRANCHES" ]; then + times_yaml="" + for t in ${GITTALLY_AUTO_BUILD_TIMES//,/ }; do + times_yaml+="${times_yaml:+, }$(yaml_quote "$t")" + done + for branch in ${GITTALLY_AUTO_BUILD_BRANCHES//,/ }; do + emit " $(yaml_quote "$branch"):" + emit " autoBuild:" + emit " enabled: true" + [ -n "$times_yaml" ] && emit " times: [$times_yaml]" + done + fi +) + +# -------------------------------------------------------- machine + secrets -- + +echo "== writing $machine_yml (secrets, mode 600)" +mkdir -p "$(dirname "$machine_yml")" +{ + echo "# Machine-specific overrides and secrets. Keys here win over .gittally.yml." + echo "git:" + echo " account: $(yaml_quote "$git_account")" + echo " token: $(yaml_quote "$git_token")" +} >"$machine_yml" +chmod 600 "$machine_yml" + +echo "== converted project config:" +sed 's/^/ /' "$project_yml" + +# ------------------------------------------------------------------- finish -- + +if $have_jar && $have_java; then + echo "== running init (keeps the config files just written)" + ( cd "$REPO_DIR" && java -jar "$JAR_PATH" init ) +fi + +cat <