Ctrl K
ComunidadGuías…

script/fresh-checkout-check

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/sh
# SPDX-FileCopyrightText: 2026 GPForum Contributors
# SPDX-License-Identifier: BSD-3-Clause
#
# Prove that a fresh checkout reaches a green `make check` from the README
# quick start alone.
#
# Nothing else checks the quick start. Every gate runs in a checkout that is
# already set up, so a step that works only because local/ is populated, a
# file is present but untracked, or the shell still carries a PERL5LIB passes
# there and fails for the next person to clone the repository. This clones a
# commit into a temporary directory, clears what a working shell carries over,
# and runs the quick start's commands there in the README's order, then every
# prerequisite of `make check`, timing each one.
#
# It writes nothing to this checkout or to its local/: the clone installs its
# own local/, cpm builds inside that local/, and the directory is
# removed afterwards unless --keep is given. Uncommitted changes are not part
# of the run, because a fresh checkout does not have them either.

set -eu
# Nothing here is meant as a file pattern: a --skip of `*` must stay `*`, not
# become the names of the files in this checkout.
set -f

root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
cd "$root"

ref=HEAD
keep=0
keep_going=0
plan_only=0
skip=' '
make_command=${MAKE:-make}

usage() {
    cat <<'USAGE'
Usage: script/fresh-checkout-check [--ref REV] [--skip STEP]... [--keep-going] [--keep] [--plan]

Clones REV of this repository into a temporary directory and runs the README
quick start there, then every prerequisite of `make check`, reporting each
step's result and duration.

When GPFORUM_DATABASE_DSN is set (with GPFORUM_DATABASE_USER and
GPFORUM_DATABASE_PASSWORD as the README describes), the quick start's gpforum
commands run too, as bin/gpforum in the clone, reading an environment file
of the run's own: gpforum setup, on a throwaway plain role and a database it
owns, made on that server first and dropped afterwards; gpforum admin create;
and gpforum start --foreground, until the forum answers /health/ready. The
PostgreSQL integration tier (`make integration`) then runs last, as the DSN's
role. That role must be allowed to create roles and databases, and the server
must let the new role sign in with a password, or trust it, where the DSN
connects. Without a DSN all of this is left out, and `make check` needs no
database.

  --ref REV    the commit to check out (default HEAD); uncommitted changes in
               this checkout are never part of the run
  --skip STEP  leave a step out (repeatable), e.g. --skip critic; the run then
               does not prove a green `make check`, and says so
  --keep-going run every step even after one fails, and report them all; the
               run still fails if any step did
  --keep       keep the temporary directory: the clone, its local/, and one
               log per step under logs/ (the throwaway role and database
               are dropped either way)
  --plan       print the steps for REV and exit without cloning anything
  --help       show this help

The steps install every dependency from MetaCPAN into the clone's own local/,
so the run needs the network and takes a while. The host prerequisites the
README lists first (selected Perl, cpm, Carton, the PostgreSQL client files) must
already be installed.

Exit status: 0 every step passed, 1 a step failed, 2 usage error.
USAGE
}

while [ $# -gt 0 ]; do
    case "$1" in
        --keep) keep=1 ;;
        --keep-going) keep_going=1 ;;
        --plan) plan_only=1 ;;
        --ref|--skip)
            if [ $# -lt 2 ]; then
                echo "fresh-checkout-check: $1 needs a value" >&2
                exit 2
            fi
            if [ "$1" = --ref ]; then ref=$2; else skip="$skip$2 "; fi
            shift
            ;;
        --ref=*) ref=${1#--ref=} ;;
        --skip=*) skip="$skip${1#--skip=} " ;;
        --help|-h)
            usage
            exit 0
            ;;
        *)
            echo "fresh-checkout-check: unknown option $1" >&2
            echo "Usage: script/fresh-checkout-check [--ref REV] [--skip STEP]... [--keep-going] [--keep] [--plan]" >&2
            exit 2
            ;;
    esac
    shift
done

if ! command -v git >/dev/null 2>&1 \
    || ! git -C "$root" rev-parse --git-dir >/dev/null 2>&1; then
    echo "fresh-checkout-check: this is not a git checkout, so there is no commit to clone" >&2
    exit 2
fi
if ! commit=$(git -C "$root" rev-parse --verify --quiet "$ref^{commit}"); then
    echo "fresh-checkout-check: $ref is not a commit in this repository" >&2
    exit 2
fi
short=$(git -C "$root" rev-parse --short "$commit")

# The database settings are read once, here, and handed only to the steps that
# use them; everything else runs the way `make check` does, without a database.
db_dsn=${GPFORUM_DATABASE_DSN:-}
db_user=${GPFORUM_DATABASE_USER:-}
db_password=${GPFORUM_DATABASE_PASSWORD:-}

# Read a finite amount of entropy. An unbounded tr | head pipeline can keep
# reading forever when its parent ignores SIGPIPE (notably BSD tr).
random_password() {
    random_value=$(LC_ALL=C od -An -v -N24 -tx1 /dev/urandom | tr -d '[:space:]')
    if [ "${#random_value}" -ne 48 ]; then
        echo 'fresh-checkout-check: cannot read 24 random bytes' >&2
        return 1
    fi
    printf '%s' "$random_value"
}

# The throwaway role and database the quick start's commands create and
# migrate. The README's `createuser gpforum` makes a plain role, and the
# application connects as it, so the run does too: migrating as the DSN's
# role, often a superuser, passes where the README's role is refused. The role
# gets a generated password, because createuser asks for one only on a
# terminal and a server that does not trust 127.0.0.1 wants one.
fresh_name="gpforum_fresh_$(date +%s)_$$"
fresh_db=$fresh_name
fresh_role=$fresh_name
fresh_password=$(random_password)
maintenance_db=''
fresh_dsn=''

# What the quick start's gpforum commands read and answer: an environment
# file of the run's own, never the host's; the owner's password, which admin
# create asks for twice at a terminal and reads once with --password-stdin;
# and the address the forum is started on, a port of the run's own.
fresh_env=''
# Expanded when the quick-start command string is evaluated.
# shellcheck disable=SC2034
fresh_admin_password=$(random_password)
fresh_listen="http://127.0.0.1:$((20000 + $$ % 10000))"
if [ -n "$db_dsn" ]; then
    case "$db_dsn" in
        [dD][bB][iI]:Pg:*|[dD][bB][iI]:Pg\(*) ;;
        *)
            echo "fresh-checkout-check: GPFORUM_DATABASE_DSN is not a dbi:Pg: DSN" >&2
            exit 2
            ;;
    esac
    # DBD::Pg takes libpq's keywords separated by semicolons, and accepts db=
    # and database= for dbname=; psql, createdb and dropdb want libpq's own.
    dsn_pairs=$(printf '%s\n' "$db_dsn" \
        | sed -E 's/^[dD][bB][iI]:Pg(\([^)]*\))?://' | tr ';' '\n' \
        | sed -E 's/^[[:space:]]+//; s/^(db|database)=/dbname=/; /^$/d')
    maintenance_db=$(printf '%s\n' "$dsn_pairs" | paste -sd ' ' -)
    server_pairs=$(printf '%s\n' "$dsn_pairs" | sed '/^dbname=/d' | paste -sd ';' -)
    # Expanded when the quick-start command string is evaluated.
    # shellcheck disable=SC2034
    fresh_dsn="dbi:Pg:${server_pairs:+$server_pairs;}dbname=$fresh_db"
fi

# The quick start's commands for a host that already has the prerequisites,
# in the README's order. Each line is "name command"; a command is shown as it
# is written here, so the database's connection details and password are
# expanded only when it runs. Each gpforum command is bin/gpforum in the
# clone, typed with the README's own arguments and what answers its
# questions; setup's link onto the PATH is left out, since the clone is
# removed afterwards -- not run as root, and with a Homebrew prefix of the
# run's own, setup finds nowhere to make it. setup finds the throwaway role
# and database the two steps before it made -- as a superuser setup would
# make them itself -- and the role's password in the file it is given.
quick_start_names='install-deps-postgres'
quick_start_steps() {
    printf '%s\n' \
        "install-deps-postgres $make_command install-deps-postgres"
    if [ -n "$db_dsn" ]; then
        printf '%s\n' "createuser createuser_with_password \"\$fresh_role\""
        if [ -n "$db_user" ]; then
            printf '%s\n' "createdb createdb --maintenance-db=\"\$maintenance_db\" --username=\"\$db_user\" --owner=\"\$fresh_role\" \"\$fresh_db\""
        else
            printf '%s\n' "createdb createdb --maintenance-db=\"\$maintenance_db\" --owner=\"\$fresh_role\" \"\$fresh_db\""
        fi
        printf '%s\n' \
            "setup write_fresh_env && HOMEBREW_PREFIX=\"\$workdir/homebrew\" script/gpforum-carton exec bin/gpforum --env-file \"\$fresh_env\" setup --environment development --yes --database \"\$fresh_dsn\" --database-user \"\$fresh_role\" --mail log" \
            "admin-create printf '%s\\n' \"\$fresh_admin_password\" | script/gpforum-carton exec bin/gpforum --env-file \"\$fresh_env\" admin create --email you@example.com --username you --password-stdin" \
            "start start_and_probe script/gpforum-carton exec bin/gpforum --env-file \"\$fresh_env\" start --foreground --listen \"\$fresh_listen\""
    fi
}

# `make check` is read from the commit's own Makefile rather than restated
# here, so the run cannot drift from the gate. Its prerequisites run one at a
# time, which is what `make check` does, but each gets its own timing.
check_prerequisites() {
    sed -n 's/^check:\([^#]*\).*/\1/p' | tr -s '[:blank:]' '\n' | sed '/^$/d'
}

# Writes one "name command" line per step.
plan_steps() {
    makefile_text=$1
    quick_start_steps
    targets=$(printf '%s\n' "$makefile_text" | check_prerequisites)
    if [ -z "$targets" ]; then
        echo "fresh-checkout-check: the Makefile at $short has no check target" >&2
        return 1
    fi
    for target in $targets; do
        case " $quick_start_names " in
            *" $target "*) continue ;;
        esac
        printf '%s %s %s\n' "$target" "$make_command" "$target"
    done
    if [ -n "$db_dsn" ]; then
        printf 'integration %s integration\n' "$make_command"
    fi
}

if ! makefile_text=$(git -C "$root" show "$commit:Makefile" 2>/dev/null); then
    echo "fresh-checkout-check: there is no Makefile at $short" >&2
    exit 1
fi
steps=$(plan_steps "$makefile_text") || exit 1

# A misspelt --skip would otherwise run the step it meant to leave out, after
# the minutes the dependency install takes. The name must be a step's exactly:
# matched as a pattern, `te.t` passed here and then left nothing out.
for name in $skip; do
    if ! printf '%s\n' "$steps" | sed 's/ .*//' | grep -qxF -e "$name"; then
        echo "fresh-checkout-check: --skip $name names no step; the steps are:" >&2
        printf '%s\n' "$steps" | sed 's/ .*//; s/^/  /' >&2
        exit 2
    fi
done

if [ "$plan_only" -eq 1 ]; then
    printf 'fresh-checkout-check: plan for commit %s\n' "$short"
    printf '  %-22s %s\n' clone "git clone --no-hardlinks, then check out $short"
    while read -r name command; do
        case "$skip" in
            *" $name "*) command="(skipped) $command" ;;
        esac
        printf '  %-22s %s\n' "$name" "$command"
    done <<STEPS
$steps
STEPS
    if [ -n "$db_dsn" ]; then
        printf '  %-22s %s\n' dropdb "the throwaway database and role $fresh_name, always"
    else
        printf '  GPFORUM_DATABASE_DSN is not set: the database commands and the integration tier are left out\n'
    fi
    exit 0
fi

tmp_base=${TMPDIR:-/tmp}
workdir=$(mktemp -d "${tmp_base%/}/gpforum-fresh-checkout.XXXXXX")
checkout="$workdir/gpforum"
logs="$workdir/logs"
fresh_env="$workdir/gpforum.env"
mkdir -p "$logs"
database_made=0

# psql on the server the DSN names, as the DSN's role. Its password, when there
# is one, is exported only inside the steps and the cleanup that need it.
dsn_psql() {
    if [ -n "$db_user" ]; then
        set -- --username="$db_user" "$@"
    fi
    psql --no-psqlrc --quiet --set=ON_ERROR_STOP=1 --dbname="$maintenance_db" "$@"
}

# The README's createuser, with a password. A role that may create roles but is
# no superuser must also be a member of the new one before it may give it a
# database (PostgreSQL 16 and later), hence the GRANT.
createuser_with_password() {
    dsn_psql <<SQL
CREATE ROLE "$1" LOGIN PASSWORD '$fresh_password';
GRANT "$1" TO CURRENT_USER;
SQL
}

# The file setup is given: the throwaway role's password, which setup keeps,
# and nothing else, readable by this account alone.
write_fresh_env() {
    (umask 077 && printf 'GPFORUM_DATABASE_PASSWORD=%s\n' "$fresh_password" >"$fresh_env")
}

# The README's `gpforum start --foreground`, then "open http://127.0.0.1:3000":
# the server started in the background, asked for /health/ready until it
# answers 200 or a minute has passed, then stopped.
start_and_probe() {
    "$@" &
    server=$!
    answered=1
    tries=0
    while [ "$tries" -lt 60 ]; do
        if ! kill -0 "$server" 2>/dev/null; then
            break
        fi
        if curl -fsS "$fresh_listen/health/ready"; then
            echo
            answered=0
            break
        fi
        tries=$((tries + 1))
        sleep 1
    done
    kill "$server" 2>/dev/null || true
    wait "$server" 2>/dev/null || true
    return "$answered"
}

use_dsn_password() {
    if [ -n "$db_password" ]; then
        PGPASSWORD=$db_password
        export PGPASSWORD
    fi
}

drop_database() {
    [ "$database_made" -eq 1 ] || return 0
    if (
        use_dsn_password
        if [ -n "$db_user" ]; then
            set -- --username="$db_user"
        fi
        dropdb --maintenance-db="$maintenance_db" "$@" --if-exists "$fresh_db" \
            && dsn_psql --command="DROP ROLE IF EXISTS \"$fresh_role\""
    ) >>"$logs/dropdb.log" 2>&1; then
        database_made=0
    else
        printf 'fresh-checkout-check: could not drop the throwaway database and role %s; drop them by hand\n' \
            "$fresh_name" >&2
    fi
}

cleanup() {
    drop_database
    if [ "$keep" -eq 1 ]; then
        printf 'fresh-checkout-check: kept %s\n' "$workdir"
        return
    fi
    chmod -R u+w "$workdir" 2>/dev/null || true
    rm -rf "$workdir"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

# A working shell carries state a fresh one does not. PERL5LIB or a
# local::lib can hand the clone this checkout's modules, and PERL_CARTON_PATH
# would make Carton maintain another local/ altogether, so the clone
# would pass on dependencies it never installed. A leftover GPFORUM_* setting
# would make it pass or fail on configuration the README never mentions; only
# GPFORUM_CARTON and GPFORUM_CPM, which locate the host tools, are kept. The
# database settings were read above, and PGPASSWORD goes only to the steps
# that sign in as the DSN's role.
#
# Run as `make fresh-checkout`, the script inherits make's own flags, and every
# inner make would honour them: under `make -i` each would ignore its errors,
# and a failing test tier would pass; a VAR=value on that command line would
# reach every step. HARNESS_PERL_SWITCHES can hand prove another -I as
# PERL5LIB can, and PERL_CARTON_MIRROR would install from somewhere other than
# the MetaCPAN the README's commands use.
unset PERL5LIB PERL5OPT PERL_LOCAL_LIB_ROOT PERL_MB_OPT PERL_MM_OPT \
    PERL_CARTON_PATH PERL_CARTON_CPANFILE PERL_CARTON_MIRROR PGPASSWORD \
    MAKEFLAGS MFLAGS MAKELEVEL MAKEOVERRIDES GNUMAKEFLAGS \
    HARNESS_PERL_SWITCHES
for variable in $(env | sed -n 's/^\(GPFORUM_[A-Za-z0-9_]*\)=.*/\1/p'); do
    case "$variable" in
        GPFORUM_CARTON|GPFORUM_CPM) ;;
        *) unset "$variable" ;;
    esac
done
PERL_CPANM_HOME="$workdir/cpanm"
export PERL_CPANM_HOME

total=0
steps_run=0
skipped=''
failed=''

# One line a log can be searched for: the result, and which steps failed or
# were left out.
finish() {
    printf '\nfresh-checkout-check status=%s commit=%s steps=%s failed=%s skipped=%s total=%ss\n' \
        "$1" "$short" "$steps_run" \
        "$(printf '%s' "${failed:- none}" | sed 's/^ //; s/ /,/g')" \
        "$(printf '%s' "${skipped:- none}" | sed 's/^ //; s/ /,/g')" "$total"
}

# The lines worth keeping from a step's log: the harness's and the gates' own
# totals, the last of what setup and admin create did (the schema, the
# owner), and what /health/ready answered.
summarise() {
    indent='                                        '
    case "$1" in
        setup|admin-create)
            grep '^✓' "$2" 2>/dev/null | tail -n 1 | sed "s/^/$indent/" || true
            ;;
        start)
            grep '"check"' "$2" 2>/dev/null | tail -n 1 | sed "s/^/$indent/" || true
            ;;
        *)
            grep -E '^(Files=|Result: |[a-z-]+ status=)' "$2" 2>/dev/null \
                | sed "s/^/$indent/" || true
            ;;
    esac
}

run_step() {
    name=$1
    command=$2
    case "$skip" in
        *" $name "*)
            printf '  %-22s %-6s %6s  %s\n' "$name" skip - "$command"
            skipped="$skipped $name"
            return 0
            ;;
    esac
    log="$logs/$name.log"
    # These two leave something outside the directory, so the cleanup must
    # know to drop it even when the step itself fails.
    case "$name" in
        createuser|createdb) database_made=1 ;;
    esac
    started=$(date +%s)
    # `set -e` does not apply inside an `if` condition, so a failed cd would
    # carry on and run the step here, in the working checkout, where
    # install-deps-postgres writes its local/.
    if (
        cd "$checkout" || exit 1
        case "$name" in
            createuser|createdb)
                use_dsn_password
                ;;
            integration)
                GPFORUM_DATABASE_DSN=$db_dsn
                GPFORUM_DATABASE_USER=$db_user
                GPFORUM_DATABASE_PASSWORD=$db_password
                export GPFORUM_DATABASE_DSN GPFORUM_DATABASE_USER \
                    GPFORUM_DATABASE_PASSWORD
                ;;
        esac
        eval "$command"
    ) >"$log" 2>&1 </dev/null; then
        result=pass
    else
        result=fail
    fi
    elapsed=$(($(date +%s) - started))
    total=$((total + elapsed))
    steps_run=$((steps_run + 1))
    printf '  %-22s %-6s %5ss  %s\n' "$name" "$result" "$elapsed" "$command"
    summarise "$name" "$log"
    if [ "$result" = fail ]; then
        failed="$failed $name"
        printf '\n--- the last 40 lines of the %s log ---\n' "$name" >&2
        tail -n 40 "$log" >&2
        printf -- '--- end of the %s log ---\n\n' "$name" >&2
        if [ "$keep_going" -eq 0 ]; then
            [ "$keep" -eq 1 ] || printf 'rerun with --keep to keep every log\n' >&2
            finish fail
            exit 1
        fi
    fi
}

clone_step() {
    started=$(date +%s)
    if git clone --quiet --no-hardlinks --no-checkout "$root" "$checkout" \
        >"$logs/clone.log" 2>&1 \
        && git -C "$checkout" checkout --quiet --detach "$commit" \
        >>"$logs/clone.log" 2>&1; then
        result=pass
    else
        result=fail
    fi
    elapsed=$(($(date +%s) - started))
    total=$((total + elapsed))
    steps_run=$((steps_run + 1))
    printf '  %-22s %-6s %5ss  %s\n' clone "$result" "$elapsed" \
        "git clone --no-hardlinks, then check out $short"
    if [ "$result" = fail ]; then
        cat "$logs/clone.log" >&2
        finish fail
        exit 1
    fi
}

printf 'fresh-checkout-check: commit %s (%s)\n' "$short" \
    "$(git -C "$root" log -1 --format=%s "$commit")"
if [ "$keep" -eq 1 ]; then
    printf '  working directory %s\n' "$workdir"
else
    printf '  working directory: a temporary one, removed afterwards (--keep keeps it)\n'
fi
uncommitted=$(git -C "$root" --no-optional-locks status --porcelain 2>/dev/null | wc -l | tr -d ' ')
if [ "$uncommitted" -gt 0 ]; then
    printf '  %s uncommitted paths in this checkout are not part of the run\n' "$uncommitted"
fi
if [ -n "$db_dsn" ]; then
    printf '  GPFORUM_DATABASE_DSN is set: the quick start makes and migrates the throwaway role and\n'
    printf '  database %s, dropped afterwards, and the integration tier runs last\n' "$fresh_name"
else
    printf '  GPFORUM_DATABASE_DSN is not set: the database commands and the integration tier are left out\n'
fi
printf '\n  %-22s %-6s %6s  %s\n' step result time command

clone_step

# The README's macOS line: Homebrew's PostgreSQL is keg-only, so its client
# tools are not on PATH until this puts them there. A no-op elsewhere.
eval "$(cd "$checkout" && script/gpforum-homebrew-env 2>/dev/null || true)"

while read -r name command; do
    run_step "$name" "$command"
done <<STEPS
$steps
STEPS

perl_version=$(cd "$checkout" \
    && "$(script/gpforum-system-perl --require)" -e 'printf q{v%vd}, $^V' 2>/dev/null \
    || echo unknown)
printf '\n  host %s, perl %s' "$(uname -sm)" "$perl_version"
if command -v pg_config >/dev/null 2>&1; then
    printf ', %s client' "$(pg_config --version)"
fi
printf '\n'
if [ -n "$failed" ]; then
    finish fail
    exit 1
fi
if [ -n "$skipped" ]; then
    printf '  steps were left out, so this run does not prove a green make check\n'
fi
finish pass