mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-04 10:14:39 +00:00
The "Stop DB and stage datadir" step swallowed a failed `pg_ctl -m fast -w stop` with `|| true`, then moved and tarred the PGDATA regardless. A stop that times out or errors would bake a still-running, crash-inconsistent cluster into the artifact every test shard consumes — and with full_page_writes off, crash recovery can't repair torn pages. Drop the `|| true` so a failed stop fails the job, mirroring the MariaDB sister's "don't bake a dirty datadir" guard. Also drop the redundant `ALTER SYSTEM SET fsync/synchronous_commit/ full_page_writes = off` block from install.sh. Its comment claimed the postgres workflow "runs a service-container DB and never calls start-db.sh", but it does call start-db.sh, which already applies those flags via `-o` on every postgres start (setup job and each shard). The block was a no-op and its justification was factually wrong. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
360 lines
13 KiB
Bash
360 lines
13 KiB
Bash
#!/bin/bash
|
|
|
|
set -e
|
|
|
|
cd ~ || exit
|
|
|
|
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
|
frappeuser=${FRAPPE_USER:-"frappe"}
|
|
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
|
|
db_host=${DB_HOST:-"127.0.0.1"}
|
|
db_user_host=${DB_USER_HOST:-"localhost"}
|
|
wkhtmltox_deb=${WKHTMLTOX_DEB:-"/tmp/wkhtmltox.deb"}
|
|
bench_cache_dir=${BENCH_CACHE_DIR:-}
|
|
|
|
run_as_ci_user_if_needed() {
|
|
if [ "$(id -u)" != "0" ] || [ "${SKIP_SYSTEM_SETUP:-0}" != "1" ] || [ "${ERPNEXT_CI_NON_ROOT:-0}" = "1" ]; then
|
|
return
|
|
fi
|
|
|
|
local missing_packages=()
|
|
if ! command -v pkg-config >/dev/null 2>&1; then
|
|
missing_packages+=("pkg-config")
|
|
fi
|
|
if ! command -v mariadb_config >/dev/null 2>&1 && ! command -v mysql_config >/dev/null 2>&1; then
|
|
missing_packages+=("libmariadb-dev")
|
|
fi
|
|
if ! command -v crontab >/dev/null 2>&1; then
|
|
missing_packages+=("cron")
|
|
fi
|
|
|
|
if [ "${#missing_packages[@]}" -gt 0 ]; then
|
|
apt-get update
|
|
apt-get install -y --no-install-recommends "${missing_packages[@]}"
|
|
fi
|
|
|
|
local ci_user="${ERPNEXT_CI_USER:-frappe}"
|
|
|
|
if ! id "$ci_user" >/dev/null 2>&1; then
|
|
useradd --home-dir "$HOME" --no-create-home --shell /bin/bash "$ci_user"
|
|
fi
|
|
|
|
rm -rf ~/frappe ~/frappe-bench
|
|
|
|
local ci_dirs=(
|
|
"$HOME"
|
|
"$GITHUB_WORKSPACE"
|
|
"$HOME/.cache"
|
|
"${PIP_CACHE_DIR:-$HOME/.cache/pip}"
|
|
"${npm_config_cache:-$HOME/.npm}"
|
|
"${YARN_CACHE_FOLDER:-$HOME/.cache/yarn}"
|
|
"$HOME/.yarn"
|
|
"${UV_CACHE_DIR:-$HOME/.cache/uv}"
|
|
"$(dirname "$wkhtmltox_deb")"
|
|
)
|
|
if [ -n "$bench_cache_dir" ]; then
|
|
ci_dirs+=("$bench_cache_dir")
|
|
fi
|
|
|
|
# Create + own (non-recursively) the home/cache/workspace dirs before dropping to
|
|
# the ci user. We deliberately do NOT wipe the yarn/uv caches here so a persistent
|
|
# cache (mounted volume or baked image layer) stays warm across runs.
|
|
mkdir -p "${ci_dirs[@]}" "$HOME/.yarn"
|
|
chown "$ci_user:$ci_user" "${ci_dirs[@]}" "$HOME/.yarn"
|
|
|
|
export ERPNEXT_CI_NON_ROOT=1
|
|
exec su -m "$ci_user" -s /bin/bash -c "cd '$HOME' && bash '$GITHUB_WORKSPACE/.github/helper/install.sh'"
|
|
}
|
|
|
|
run_as_ci_user_if_needed
|
|
|
|
run_ci_step() {
|
|
local label=$1
|
|
shift
|
|
|
|
echo "::group::${label}"
|
|
date -u
|
|
local exit_code=0
|
|
timeout --foreground "${CI_INSTALL_STEP_TIMEOUT:-1800}" "$@" || exit_code=$?
|
|
date -u
|
|
echo "::endgroup::"
|
|
return "$exit_code"
|
|
}
|
|
|
|
if [ -n "${GITHUB_WORKSPACE:-}" ]; then
|
|
git config --global --add safe.directory "$GITHUB_WORKSPACE" || true
|
|
git config --global --add safe.directory "$GITHUB_WORKSPACE/.git" || true
|
|
fi
|
|
|
|
rm -rf ~/frappe ~/frappe-bench
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 1 — parallelise the three slow, independent setup steps:
|
|
# a) system packages b) frappe-bench pip install c) frappe git fetch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if [ "${SKIP_SYSTEM_SETUP:-0}" != "1" ]; then
|
|
sudo apt-get update
|
|
|
|
# apt remove/install must run sequentially but can overlap with pip and git.
|
|
sudo apt-get remove -y mysql-server mysql-client
|
|
sudo apt-get install -y libcups2-dev redis-server mariadb-client libmariadb-dev &
|
|
apt_pid=$!
|
|
|
|
pip install frappe-bench &
|
|
pip_pid=$!
|
|
else
|
|
apt_pid=
|
|
pip_pid=
|
|
fi
|
|
|
|
mkdir frappe
|
|
(
|
|
cd frappe
|
|
git init
|
|
git remote add origin "https://github.com/${frappeuser}/frappe"
|
|
git fetch origin "${frappecommitish}" --depth 1
|
|
) &
|
|
clone_pid=$!
|
|
|
|
if [ -n "$apt_pid" ]; then wait $apt_pid; fi
|
|
if [ -n "$pip_pid" ]; then wait $pip_pid; fi
|
|
wait $clone_pid
|
|
|
|
pushd frappe
|
|
git checkout FETCH_HEAD
|
|
popd
|
|
frappe_sha=$(git -C frappe rev-parse HEAD)
|
|
|
|
get_bench_cache_archive() {
|
|
if [ -z "$bench_cache_dir" ]; then
|
|
return
|
|
fi
|
|
|
|
mkdir -p "$bench_cache_dir"
|
|
|
|
# Keyed on tool versions only (NOT the frappe SHA): any recent base bench works, because
|
|
# restore_warm_bench fast-forwards it to the exact live develop SHA. This is what lets a
|
|
# constantly-moving develop still hit the cache.
|
|
local cache_key
|
|
cache_key=$(
|
|
{
|
|
uname -m
|
|
python --version
|
|
node --version
|
|
bench --version
|
|
} | sha256sum | awk '{print $1}'
|
|
)
|
|
|
|
echo "${bench_cache_dir}/frappe-bench-base-${cache_key}.tar.zst"
|
|
}
|
|
|
|
restore_warm_bench() {
|
|
bench_cache_archive=$(get_bench_cache_archive)
|
|
[ -n "$bench_cache_archive" ] && [ -f "$bench_cache_archive" ] || return 1
|
|
|
|
echo "Restoring base bench from ${bench_cache_archive}"
|
|
tar --use-compress-program=unzstd -xf "$bench_cache_archive" -C ~ || return 1
|
|
[ -d ~/frappe-bench/apps/frappe/.git ] || return 1
|
|
mkdir -p ~/frappe-bench/sites ~/frappe-bench/logs
|
|
[ -f ~/frappe-bench/sites/apps.txt ] || printf "frappe\n" > ~/frappe-bench/sites/apps.txt
|
|
[ -f ~/frappe-bench/sites/common_site_config.json ] || printf "{}\n" > ~/frappe-bench/sites/common_site_config.json
|
|
|
|
# Fast-forward the restored frappe to the EXACT live develop SHA fetched in phase 1, then
|
|
# rebuild only what changed. The editable install means the venv tracks the new code with
|
|
# no reinstall. Any failure returns non-zero so the caller falls back to a full bench init.
|
|
if ! (
|
|
cd ~/frappe-bench/apps/frappe || exit 1
|
|
# Phase 1 already fetched ~/frappe to the exact live develop SHA. Fetch that commit
|
|
# straight from it (bench init names the remote 'upstream', not 'origin', and points
|
|
# it at this local clone — so a plain `git fetch origin` does not work).
|
|
git fetch --no-tags "$HOME/frappe" HEAD || exit 1
|
|
git checkout --force FETCH_HEAD || exit 1
|
|
); then
|
|
echo "Fast-forward to ${frappe_sha} failed; falling back to full init"
|
|
rm -rf ~/frappe-bench
|
|
return 1
|
|
fi
|
|
|
|
# Pick up any frappe dependency changes since the base was built (cached → fast if none),
|
|
# so a develop commit that bumped requirements doesn't leave a stale venv.
|
|
if ! ~/frappe-bench/env/bin/python -m pip install -q -e ~/frappe-bench/apps/frappe; then
|
|
echo "frappe dependency refresh failed; falling back to full init"
|
|
rm -rf ~/frappe-bench
|
|
return 1
|
|
fi
|
|
|
|
( cd ~/frappe-bench && CI=Yes bench build --app frappe ) || { rm -rf ~/frappe-bench; return 1; }
|
|
return 0
|
|
}
|
|
|
|
save_warm_bench() {
|
|
if [ -z "${bench_cache_archive:-}" ] || [ -f "$bench_cache_archive" ]; then
|
|
return
|
|
fi
|
|
|
|
if [ -n "$bench_cache_dir" ] && [ ! -w "$bench_cache_dir" ]; then
|
|
echo "Skipping warm bench save because ${bench_cache_dir} is not writable"
|
|
return
|
|
fi
|
|
|
|
local tmp_archive
|
|
tmp_archive="${bench_cache_archive}.${$}.tmp"
|
|
|
|
echo "Saving warm bench to ${bench_cache_archive}"
|
|
# Keep sites/common_site_config.json (the redis ports live there — dropping it makes the
|
|
# restore path fall back to a default redis port that bench start never bound, so reinstall
|
|
# fails with "redis ... connection refused"). Only the rebuildable sites/assets is excluded;
|
|
# restore_warm_bench runs `bench build` to regenerate it.
|
|
tar \
|
|
--use-compress-program="zstd -T0 -3" \
|
|
--exclude="frappe-bench/logs" \
|
|
--exclude="frappe-bench/sites/assets" \
|
|
-cf "$tmp_archive" \
|
|
-C ~ frappe-bench
|
|
mv "$tmp_archive" "$bench_cache_archive"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 2 — bench init and site setup
|
|
# ---------------------------------------------------------------------------
|
|
|
|
install_whktml() {
|
|
# Re-use the .deb if the wkhtmltopdf cache step already restored it.
|
|
if [ ! -f "$wkhtmltox_deb" ]; then
|
|
wget -O "$wkhtmltox_deb" https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
|
|
fi
|
|
sudo apt-get install -y "$wkhtmltox_deb"
|
|
}
|
|
if [ "${SKIP_WKHTMLTOX_SETUP:-0}" != "1" ]; then
|
|
install_whktml &
|
|
wkpid=$!
|
|
else
|
|
wkpid=
|
|
fi
|
|
|
|
if ! restore_warm_bench; then
|
|
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
|
|
|
|
cd ~/frappe-bench || exit
|
|
|
|
sed -i 's/watch:/# watch:/g' Procfile
|
|
sed -i 's/schedule:/# schedule:/g' Procfile
|
|
sed -i 's/socketio:/# socketio:/g' Procfile
|
|
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
|
|
|
CI=Yes bench build --app frappe
|
|
save_warm_bench
|
|
fi
|
|
|
|
if [ -n "$wkpid" ]; then wait $wkpid; fi
|
|
|
|
mkdir -p ~/frappe-bench/sites/test_site
|
|
|
|
if [ "$DB" == "mariadb" ];then
|
|
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/test_site/site_config.json
|
|
if [ "$db_host" != "127.0.0.1" ]; then
|
|
sed -i "s/\"db_host\": \"127.0.0.1\"/\"db_host\": \"${db_host}\"/" ~/frappe-bench/sites/test_site/site_config.json
|
|
fi
|
|
else
|
|
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_postgres.json" ~/frappe-bench/sites/test_site/site_config.json
|
|
fi
|
|
|
|
|
|
if [ "$DB" == "mariadb" ];then
|
|
for _ in {1..60}; do
|
|
if mariadb-admin ping --host "$db_host" --port 3306 -u root -proot --silent; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
mariadb-admin ping --host "$db_host" --port 3306 -u root -proot --silent
|
|
|
|
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
|
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
|
|
|
# Throwaway-DB durability tuning at runtime. (innodb_doublewrite is read-only on MariaDB
|
|
# 10.6, so it can't be disabled here — would need a server startup flag.)
|
|
mariadb --host "$db_host" --port 3306 -u root -proot \
|
|
-e "SET GLOBAL innodb_flush_log_at_trx_commit=0; SET GLOBAL sync_binlog=0;"
|
|
|
|
# Opt-in DDL speedup: a shared tablespace avoids a create+fsync per DocType table during
|
|
# reinstall — a big win under disk contention. But ROW_FORMAT=DYNAMIC must be accepted in
|
|
# the system tablespace on this MariaDB. Enable with CI_INNODB_SHARED_TABLESPACE=1; if
|
|
# reinstall then errors on table creation, unset it (off by default — zero risk).
|
|
if [ "${CI_INNODB_SHARED_TABLESPACE:-0}" = "1" ]; then
|
|
mariadb --host "$db_host" --port 3306 -u root -proot -e "SET GLOBAL innodb_file_per_table=0;"
|
|
fi
|
|
|
|
mariadb --host "$db_host" --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'${db_user_host}' IDENTIFIED BY 'test_frappe'"
|
|
mariadb --host "$db_host" --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
|
|
mariadb --host "$db_host" --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'${db_user_host}'"
|
|
|
|
mariadb --host "$db_host" --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
|
|
fi
|
|
|
|
if [ "$DB" == "postgres" ];then
|
|
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres;
|
|
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
|
|
|
|
# Durability-off for speed (no fsync/synchronous_commit/full_page_writes) is applied by
|
|
# start-db.sh's postgres `-o` flags on every start — setup job AND each test shard — so it is
|
|
# NOT repeated here. The postgres workflow runs in-runner via start-db.sh, not a service
|
|
# container.
|
|
fi
|
|
|
|
cd ~/frappe-bench || exit
|
|
|
|
run_ci_step "Get payments app" bench get-app payments --branch develop
|
|
|
|
# Opt-in: skip building erpnext's frontend assets. Server tests don't need them, but PDF
|
|
# tests (print formats) do — they pass only if the PDF renderer ignores missing assets.
|
|
# Enable with CI_SKIP_ERPNEXT_ASSETS=1 to test; if PDF tests fail, unset it.
|
|
erpnext_get_app_args=()
|
|
if [ "${CI_SKIP_ERPNEXT_ASSETS:-0}" = "1" ]; then erpnext_get_app_args=(--skip-assets); fi
|
|
run_ci_step "Get erpnext app" bench get-app erpnext "${GITHUB_WORKSPACE}" "${erpnext_get_app_args[@]}"
|
|
|
|
if [ "$TYPE" == "server" ]; then run_ci_step "Setup dev requirements" bench setup requirements --dev; fi
|
|
|
|
bench start >> ~/frappe-bench/bench_start.log 2>&1 &
|
|
|
|
# Under heavy concurrency, gunicorn's startup can delay redis coming up. reinstall and the
|
|
# tests need redis, so wait for it (best-effort, bounded) instead of racing — contention
|
|
# then slows the job rather than failing it.
|
|
wait_for_redis() {
|
|
local cfg=~/frappe-bench/sites/common_site_config.json
|
|
[ -f "$cfg" ] || return 0
|
|
local ports port
|
|
ports=$(python - "$cfg" <<'PY'
|
|
import json, re, sys
|
|
try:
|
|
cfg = json.load(open(sys.argv[1]))
|
|
except Exception:
|
|
sys.exit(0)
|
|
for key in ("redis_cache", "redis_queue"):
|
|
match = re.search(r":(\d+)", str(cfg.get(key, "")))
|
|
if match:
|
|
print(match.group(1))
|
|
PY
|
|
)
|
|
for port in $ports; do
|
|
local up=0
|
|
for _ in $(seq 1 120); do
|
|
if (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
|
|
exec 3>&- 3<&-; up=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
# Fail clearly instead of letting reinstall die later on a vague socket-connection error
|
|
# when redis never bound.
|
|
[ "$up" = "1" ] || { echo "redis did not come up on port $port"; return 1; }
|
|
done
|
|
}
|
|
wait_for_redis
|
|
|
|
# Site setup: build the schema (~1000 DocTypes) into the DB. This is the single-threaded-Python
|
|
# bottleneck, but the fan-out amortises it — it runs once here in the setup job, and the test
|
|
# shards start the DB on the baked datadir instead of repeating the reinstall.
|
|
run_ci_step "Reinstall test site" bench --site test_site reinstall --yes
|