mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-12 14:11:46 +00:00
* ci: fall back to develop when frappe has no matching branch The frappe branch to install is taken from the pull request's base branch. A stacked pull request targets another erpnext branch, so the clone fails with "couldn't find remote ref", no bench is installed, and every job that needs one fails with it. Fall back to develop when the base branch does not exist in frappe. An explicit FRAPPE_BRANCH is left alone, since it can be a commit sha rather than a branch. * ci: only fall back when frappe is known to lack the branch git ls-remote --exit-code reports 2 for a branch that is not there and 128 for a remote it could not reach. Treating both as absence let a transient network or DNS failure install develop over the branch the pull request was built against. Fall back on 2 alone and log anything else, so a flaky probe leaves the branch as it was.
380 lines
14 KiB
Bash
380 lines
14 KiB
Bash
#!/bin/bash
|
|
|
|
set -e
|
|
|
|
cd ~ || exit
|
|
|
|
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
|
frappeuser=${FRAPPE_USER:-"frappe"}
|
|
frappecommitish=${FRAPPE_BRANCH:-}
|
|
|
|
# A stacked pull request targets another erpnext branch, which has no counterpart in frappe.
|
|
# Fall back to develop so the bench is still installed. An explicit FRAPPE_BRANCH is trusted as
|
|
# given, since it can be a commit sha rather than a branch.
|
|
if [ -z "$frappecommitish" ]; then
|
|
frappecommitish=$githubbranch
|
|
|
|
# git ls-remote --exit-code reports 2 for a branch that is not there and 128 for a remote it
|
|
# could not reach. Only the first one is proof of absence; keep the branch on anything else so
|
|
# a flaky probe cannot install an unrelated frappe.
|
|
probe=0
|
|
git ls-remote --exit-code --heads "https://github.com/${frappeuser}/frappe" "$frappecommitish" >/dev/null 2>&1 || probe=$?
|
|
|
|
if [ "$probe" -eq 2 ]; then
|
|
echo "frappe has no branch ${frappecommitish}, falling back to develop"
|
|
frappecommitish=develop
|
|
elif [ "$probe" -ne 0 ]; then
|
|
echo "could not reach frappe to check for branch ${frappecommitish} (git ls-remote exited ${probe}), keeping it"
|
|
fi
|
|
fi
|
|
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
|