feat: mbs update takes a mirror url, backs up first and survives manual edits
Manual edits on the server are stashed (and saved as a patch) instead of aborting the update, a full backup is taken before every update, mbs mirror remembers a custom source, unsafe urls are refused, rollback restores the stashed edits. Covered by tests/test_mbs_update.sh.
This commit is contained in:
parent
2c5ec8b06c
commit
264991593a
3 changed files with 332 additions and 19 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,3 +7,5 @@ __pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
venv/
|
venv/
|
||||||
.claude/
|
.claude/
|
||||||
|
.update_mirror
|
||||||
|
local-changes/
|
||||||
|
|
|
||||||
194
mbs
194
mbs
|
|
@ -13,7 +13,10 @@ mbs — управление MBS Panel
|
||||||
mbs restart перезапустить всё (bot, api, xray, reload nginx)
|
mbs restart перезапустить всё (bot, api, xray, reload nginx)
|
||||||
mbs logs [bot|api|xray] последние строки лога (по умолчанию api)
|
mbs logs [bot|api|xray] последние строки лога (по умолчанию api)
|
||||||
mbs domain показать текущий домен панели
|
mbs domain показать текущий домен панели
|
||||||
mbs update обновить код (зеркало api.savsis.xyz, потом GitHub) и перезапустить (не трогает .env и базу)
|
mbs backup полная копия панели (база, .env, ручные правки, конфиг Xray) в /root/mbs-backups
|
||||||
|
mbs update [ссылка] обновить код и перезапустить (не трогает .env и базу, перед этим сам делает копию). Без ссылки: своё зеркало -> api.savsis.xyz -> GitHub.
|
||||||
|
Со ссылкой на git-репозиторий (зеркало) — берёт обновление оттуда, один раз
|
||||||
|
mbs mirror [ссылка|off] показать / запомнить / убрать своё зеркало, которое mbs update проверяет первым
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,34 +58,172 @@ cmd_domain() {
|
||||||
grep "^PANEL_DOMAIN=" "$ENV_FILE"
|
grep "^PANEL_DOMAIN=" "$ENV_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_update() {
|
MIRROR_FILE="$APP_DIR/.update_mirror"
|
||||||
cd "$APP_DIR"
|
BACKUP_DIR="${MBS_BACKUP_DIR:-/root/mbs-backups}"
|
||||||
echo "проверяю обновления..."
|
XRAY_CONFIG="/usr/local/etc/xray/config.json"
|
||||||
local remote="origin"
|
BACKUP_FILE=""
|
||||||
if ! git fetch --quiet origin main 2>/dev/null; then
|
UPDATE_STASHED=0
|
||||||
if git remote | grep -q '^github$'; then
|
|
||||||
echo "зеркало недоступно, пробую github..."
|
snapshot_backup() {
|
||||||
if ! git fetch --quiet github main 2>/dev/null; then
|
local stamp dbsnap file
|
||||||
echo "не удалось получить обновления ни с зеркала, ни с github"
|
local -a targs=(--exclude=venv --exclude=__pycache__)
|
||||||
|
stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
file="$BACKUP_DIR/mbs-$1-$stamp.tar.gz"
|
||||||
|
dbsnap="$APP_DIR/.mbs.db.snapshot"
|
||||||
|
mkdir -p "$BACKUP_DIR" || return 1
|
||||||
|
chmod 700 "$BACKUP_DIR"
|
||||||
|
rm -f "$dbsnap"
|
||||||
|
if [ -f "$APP_DIR/mbs.db" ] && [ -x "$APP_DIR/venv/bin/python" ] && \
|
||||||
|
"$APP_DIR/venv/bin/python" -c "import sqlite3,sys; s=sqlite3.connect(sys.argv[1]); d=sqlite3.connect(sys.argv[2]); s.backup(d); d.close(); s.close()" "$APP_DIR/mbs.db" "$dbsnap" 2>/dev/null; then
|
||||||
|
targs+=(--exclude=mbs.db --exclude=mbs.db-wal --exclude=mbs.db-shm "--transform=s#\.mbs\.db\.snapshot#mbs.db#")
|
||||||
|
fi
|
||||||
|
if [ -f "$XRAY_CONFIG" ]; then
|
||||||
|
tar czf "$file" "${targs[@]}" "$APP_DIR" "$XRAY_CONFIG" 2>/dev/null
|
||||||
|
else
|
||||||
|
tar czf "$file" "${targs[@]}" "$APP_DIR" 2>/dev/null
|
||||||
|
fi
|
||||||
|
local rc=$?
|
||||||
|
rm -f "$dbsnap"
|
||||||
|
if [ $rc -ne 0 ] || [ ! -s "$file" ]; then
|
||||||
|
rm -f "$file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
chmod 600 "$file"
|
||||||
|
ls -1t "$BACKUP_DIR"/mbs-*.tar.gz 2>/dev/null | tail -n +6 | while read -r old; do rm -f "$old"; done
|
||||||
|
BACKUP_FILE="$file"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_backup() {
|
||||||
|
if snapshot_backup manual; then
|
||||||
|
echo "копия сохранена: $BACKUP_FILE"
|
||||||
|
echo "внутри: база (консистентный снапшот), .env, код с твоими правками, конфиг Xray. Хранится 5 последних."
|
||||||
|
else
|
||||||
|
echo "не удалось сделать копию в $BACKUP_DIR (место на диске?)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_url() {
|
||||||
|
case "$1" in
|
||||||
|
https://*|http://*|ssh://*|git@*:*) ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
case "$1" in
|
||||||
|
*[[:space:]]*) return 1 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_url() {
|
||||||
|
git -c protocol.ext.allow=never fetch --quiet -- "$1" main 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_stash() {
|
||||||
|
if [ "$UPDATE_STASHED" = "1" ]; then
|
||||||
|
UPDATE_STASHED=0
|
||||||
|
if git stash pop --quiet 2>/dev/null; then
|
||||||
|
echo "ручные правки вернул на место"
|
||||||
|
else
|
||||||
|
echo "ручные правки остались в git stash (git stash list)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_mirror() {
|
||||||
|
local url="$1"
|
||||||
|
case "$url" in
|
||||||
|
"")
|
||||||
|
if [ -f "$MIRROR_FILE" ]; then
|
||||||
|
echo "своё зеркало для обновлений: $(head -n 1 "$MIRROR_FILE")"
|
||||||
|
else
|
||||||
|
echo "своё зеркало не задано — mbs update берёт api.savsis.xyz, потом GitHub"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
off|clear)
|
||||||
|
rm -f "$MIRROR_FILE"
|
||||||
|
echo "своё зеркало убрано"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if ! valid_url "$url"; then
|
||||||
|
echo "это не похоже на ссылку на git-репозиторий (нужна https://..., ssh://... или git@хост:путь)"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
remote="github"
|
printf '%s\n' "$url" > "$MIRROR_FILE"
|
||||||
|
echo "зеркало запомнил: $url — mbs update теперь проверяет его первым"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_update() {
|
||||||
|
local arg_url="$1" target="" saved="" before="" stamp="" patch=""
|
||||||
|
cd "$APP_DIR"
|
||||||
|
echo "проверяю обновления..."
|
||||||
|
if [ -n "$arg_url" ]; then
|
||||||
|
if ! valid_url "$arg_url"; then
|
||||||
|
echo "это не похоже на ссылку на git-репозиторий (нужна https://..., ssh://... или git@хост:путь)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! fetch_url "$arg_url"; then
|
||||||
|
echo "не удалось получить обновления по ссылке: $arg_url"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
target=$(git rev-parse FETCH_HEAD)
|
||||||
|
echo "источник: $arg_url"
|
||||||
|
else
|
||||||
|
if [ -f "$MIRROR_FILE" ]; then
|
||||||
|
saved=$(head -n 1 "$MIRROR_FILE" | tr -d '[:space:]')
|
||||||
|
fi
|
||||||
|
if [ -n "$saved" ] && valid_url "$saved" && fetch_url "$saved"; then
|
||||||
|
target=$(git rev-parse FETCH_HEAD)
|
||||||
|
echo "источник: своё зеркало $saved"
|
||||||
|
elif git fetch --quiet origin main 2>/dev/null; then
|
||||||
|
target=$(git rev-parse origin/main)
|
||||||
|
elif git remote | grep -q '^github$' && git fetch --quiet github main 2>/dev/null; then
|
||||||
|
echo "зеркало недоступно, взял с github..."
|
||||||
|
target=$(git rev-parse github/main)
|
||||||
else
|
else
|
||||||
echo "не удалось получить обновления"
|
echo "не удалось получить обновления ни с зеркала, ни с github"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
local before after
|
|
||||||
before=$(git rev-parse HEAD)
|
before=$(git rev-parse HEAD)
|
||||||
after=$(git rev-parse "$remote/main")
|
if [ "$before" = "$target" ]; then
|
||||||
if [ "$before" = "$after" ]; then
|
|
||||||
echo "уже последняя версия ($before)."
|
echo "уже последняя версия ($before)."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
if git merge-base --is-ancestor "$target" "$before" 2>/dev/null; then
|
||||||
|
echo "на сервере версия новее, чем в источнике ($before) — ничего не делаю."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
echo "текущая: $before"
|
echo "текущая: $before"
|
||||||
echo "новая: $after"
|
echo "новая: $target"
|
||||||
if ! git merge --ff-only "$remote/main" --quiet; then
|
|
||||||
echo "не вышло быстро обновиться (похоже, файлы правились вручную на сервере) — разберись руками: git status"
|
if ! snapshot_backup before-update; then
|
||||||
|
echo "не вышло сделать резервную копию в $BACKUP_DIR — обновление не начинаю, чтобы ничего не потерять (место на диске?)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "резервная копия перед обновлением: $BACKUP_FILE"
|
||||||
|
|
||||||
|
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
||||||
|
stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
mkdir -p "$APP_DIR/local-changes"
|
||||||
|
patch="$APP_DIR/local-changes/local-changes-$stamp.patch"
|
||||||
|
git diff HEAD > "$patch"
|
||||||
|
if GIT_AUTHOR_NAME=mbs GIT_AUTHOR_EMAIL=mbs@localhost GIT_COMMITTER_NAME=mbs GIT_COMMITTER_EMAIL=mbs@localhost git stash push --quiet -m "mbs-update-$stamp"; then
|
||||||
|
UPDATE_STASHED=1
|
||||||
|
echo "на сервере были ручные правки — убрал в сторону, ничего не потеряно:"
|
||||||
|
echo " патч: $patch"
|
||||||
|
echo " stash: git stash list (вернуть обратно: git stash pop)"
|
||||||
|
else
|
||||||
|
echo "не вышло спрятать ручные правки, остановился — разберись руками: git status"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git merge --ff-only "$target" --quiet 2>/dev/null; then
|
||||||
|
echo "не вышло быстро обновиться (на сервере есть свои коммиты, которых нет в источнике) — разберись руками: git log"
|
||||||
|
restore_stash
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
echo "обновляю зависимости..."
|
echo "обновляю зависимости..."
|
||||||
|
|
@ -92,6 +233,7 @@ cmd_update() {
|
||||||
echo "новый код не проходит проверку, откатываюсь на $before..."
|
echo "новый код не проходит проверку, откатываюсь на $before..."
|
||||||
git reset --hard "$before" --quiet
|
git reset --hard "$before" --quiet
|
||||||
venv/bin/pip install --quiet -r requirements.txt
|
venv/bin/pip install --quiet -r requirements.txt
|
||||||
|
restore_stash
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
echo "обновляю сам CLI..."
|
echo "обновляю сам CLI..."
|
||||||
|
|
@ -115,22 +257,36 @@ cmd_update() {
|
||||||
sleep 2
|
sleep 2
|
||||||
if systemctl is-active --quiet mbs-bot && systemctl is-active --quiet mbs-api; then
|
if systemctl is-active --quiet mbs-bot && systemctl is-active --quiet mbs-api; then
|
||||||
echo "обновлено: $before -> $(git rev-parse --short HEAD)"
|
echo "обновлено: $before -> $(git rev-parse --short HEAD)"
|
||||||
|
if [ "$UPDATE_STASHED" = "1" ]; then
|
||||||
|
echo "твои ручные правки лежат в git stash и в $patch — если они нужны, посмотри git stash show -p"
|
||||||
|
fi
|
||||||
|
echo "если что-то пошло не так: копия $BACKUP_FILE (распаковать: tar xzf файл -C /)"
|
||||||
|
if [ -n "$arg_url" ]; then
|
||||||
|
echo "чтобы всегда обновляться с этого зеркала: mbs mirror $arg_url"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "сервисы не поднялись после обновления, откатываюсь на $before..."
|
echo "сервисы не поднялись после обновления, откатываюсь на $before..."
|
||||||
git reset --hard "$before" --quiet
|
git reset --hard "$before" --quiet
|
||||||
venv/bin/pip install --quiet -r requirements.txt
|
venv/bin/pip install --quiet -r requirements.txt
|
||||||
systemctl restart mbs-bot mbs-api
|
systemctl restart mbs-bot mbs-api
|
||||||
|
restore_stash
|
||||||
echo "откачено обратно на $before"
|
echo "откачено обратно на $before"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
pass) cmd_pass "$2" ;;
|
pass) cmd_pass "$2" ;;
|
||||||
status) cmd_status ;;
|
status) cmd_status ;;
|
||||||
restart) cmd_restart ;;
|
restart) cmd_restart ;;
|
||||||
logs) cmd_logs "$2" ;;
|
logs) cmd_logs "$2" ;;
|
||||||
domain) cmd_domain ;;
|
domain) cmd_domain ;;
|
||||||
update) cmd_update ;;
|
update) cmd_update "$2" ;;
|
||||||
|
mirror) cmd_mirror "$2" ;;
|
||||||
|
backup) cmd_backup ;;
|
||||||
*) usage ;;
|
*) usage ;;
|
||||||
esac
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"; exit $?
|
||||||
|
|
|
||||||
155
tests/test_mbs_update.sh
Normal file
155
tests/test_mbs_update.sh
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
#!/bin/bash
|
||||||
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
T=$(mktemp -d)
|
||||||
|
cd "$T"
|
||||||
|
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.autocrlf GIT_CONFIG_VALUE_0=false
|
||||||
|
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
||||||
|
mkdir -p stub bin sysd
|
||||||
|
cat > stub/systemctl << 'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x stub/systemctl
|
||||||
|
export PATH="$T/stub:$PATH"
|
||||||
|
export MBS_BACKUP_DIR="$T/backups"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
ok() { echo "PASS $1"; PASS=$((PASS+1)); }
|
||||||
|
bad() { echo "FAIL $1 :: $2"; FAIL=$((FAIL+1)); }
|
||||||
|
check() { if eval "$2"; then ok "$1"; else bad "$1" "$3"; fi; }
|
||||||
|
|
||||||
|
sed -e "s#APP_DIR=\"/opt/mbs-panel\"#APP_DIR=\"$T/app\"#" -e "s#/usr/local/bin/mbs#$T/bin/mbs#g" -e "s#/etc/systemd/system#$T/sysd#g" "$REPO/mbs" > "$T/mbs-run"
|
||||||
|
|
||||||
|
git init -q --bare -b main origin.git
|
||||||
|
git clone -q origin.git seed 2>/dev/null
|
||||||
|
cd seed
|
||||||
|
git checkout -q -b main 2>/dev/null || true
|
||||||
|
mkdir -p systemd
|
||||||
|
cp "$REPO/mbs" mbs
|
||||||
|
echo "v1" > bot.py
|
||||||
|
echo "v1" > config.py
|
||||||
|
echo "v1" > db.py
|
||||||
|
echo "v1" > settings.py
|
||||||
|
echo "x" > requirements.txt
|
||||||
|
echo "[Service]" > systemd/mbs-bot.service
|
||||||
|
echo "ExecStart=x --workers __WORKERS__" > systemd/mbs-api.service
|
||||||
|
cp "$REPO/.gitignore" .gitignore
|
||||||
|
git add -A && git commit -q -m A && git push -q origin main
|
||||||
|
cd "$T"
|
||||||
|
git clone -q origin.git app
|
||||||
|
mkdir -p app/venv/bin
|
||||||
|
printf '#!/bin/sh\nexit 0\n' > app/venv/bin/pip
|
||||||
|
printf '#!/bin/sh\nexec python "$@"\n' > app/venv/bin/python
|
||||||
|
chmod +x app/venv/bin/pip app/venv/bin/python
|
||||||
|
echo "SECRET=1" > app/.env
|
||||||
|
python -c "import sqlite3,sys; c=sqlite3.connect(sys.argv[1]); c.execute('pragma journal_mode=wal'); c.execute('create table t(x)'); c.execute('insert into t values (42)'); c.commit(); c.close()" app/mbs.db
|
||||||
|
|
||||||
|
cd seed && echo "v2" > bot.py && echo "v2" > db.py && git commit -qam B && git push -q origin main && cd "$T"
|
||||||
|
|
||||||
|
echo "manual edit" >> app/bot.py
|
||||||
|
echo "manual edit" >> app/config.py
|
||||||
|
echo "manual edit" >> app/db.py
|
||||||
|
echo "manual edit" >> app/settings.py
|
||||||
|
OUT=$(bash "$T/mbs-run" update 2>&1); RC=$?
|
||||||
|
echo "$OUT" > out1.txt
|
||||||
|
check "update succeeds despite manual edits on the server (the reported bug)" "[ $RC -eq 0 ]" "rc=$RC $OUT"
|
||||||
|
check "bot.py is the new version" "[ \"\$(cat app/bot.py)\" = v2 ]" "$(cat app/bot.py)"
|
||||||
|
check "a stash with the manual edits exists" "[ \$(git -C app stash list | wc -l) -eq 1 ]"
|
||||||
|
check "patch with the manual edits saved" "ls app/local-changes/*.patch >/dev/null 2>&1 && grep -q 'manual edit' app/local-changes/*.patch"
|
||||||
|
check "user is told where the edits went" "grep -q 'ручные правки' out1.txt && grep -q 'патч' out1.txt"
|
||||||
|
check "working tree clean after update" "[ -z \"\$(git -C app status --porcelain --untracked-files=no)\" ]"
|
||||||
|
check "cli copied" "[ -f bin/mbs ]"
|
||||||
|
check "a pre-update backup was made" "[ \$(ls backups/mbs-before-update-*.tar.gz 2>/dev/null | wc -l) -eq 1 ]" "$(ls backups 2>&1)"
|
||||||
|
check "update output points at the backup" "grep -q 'резервная копия перед обновлением' out1.txt"
|
||||||
|
mkdir -p unpack && tar xzf backups/mbs-before-update-*.tar.gz -C unpack
|
||||||
|
APPREL="${T#/}/app"
|
||||||
|
check "backup keeps .env" "grep -q SECRET=1 unpack/$APPREL/.env"
|
||||||
|
check "backup keeps the manual edits as they were before the update" "grep -q 'manual edit' unpack/$APPREL/bot.py && grep -q 'manual edit' unpack/$APPREL/config.py"
|
||||||
|
check "backup database is a consistent sqlite copy" "[ \"\$(python -c \"import sqlite3,sys; print(sqlite3.connect(sys.argv[1]).execute('select x from t').fetchone()[0])\" unpack/$APPREL/mbs.db)\" = 42 ]"
|
||||||
|
check "backup does not drag the venv along" "[ ! -d unpack/$APPREL/venv ]"
|
||||||
|
check "no snapshot temp file is left behind" "[ ! -e app/.mbs.db.snapshot ]"
|
||||||
|
OUT=$(bash "$T/mbs-run" backup 2>&1); RC=$?
|
||||||
|
check "mbs backup makes a manual copy" "[ $RC -eq 0 ] && ls backups/mbs-manual-*.tar.gz >/dev/null 2>&1" "$OUT"
|
||||||
|
for i in 1 2 3 4 5 6 7; do sleep 1.1; bash "$T/mbs-run" backup >/dev/null 2>&1; done
|
||||||
|
check "only the 5 newest backups are kept" "[ \$(ls backups/mbs-*.tar.gz | wc -l) -eq 5 ]" "$(ls backups | wc -l)"
|
||||||
|
|
||||||
|
OUT=$(bash "$T/mbs-run" update 2>&1); echo "$OUT" > out2.txt
|
||||||
|
check "second run says already latest" "grep -q 'уже последняя' out2.txt" "$OUT"
|
||||||
|
|
||||||
|
cd "$T/app" && git stash drop -q && git reset -q --hard HEAD; cd "$T"
|
||||||
|
|
||||||
|
git clone -q --bare origin.git mirror2.git
|
||||||
|
cd seed && git pull -q origin main 2>/dev/null; echo "v3" > bot.py && git commit -qam C && git push -q "$T/mirror2.git" main && cd "$T"
|
||||||
|
git -C mirror2.git update-server-info
|
||||||
|
python -m http.server 8799 --directory "$T" > http.log 2>&1 &
|
||||||
|
HTTP_PID=$!
|
||||||
|
sleep 1.5
|
||||||
|
OUT=$(bash "$T/mbs-run" update "http://127.0.0.1:8799/mirror2.git" 2>&1); RC=$?
|
||||||
|
echo "$OUT" > out3.txt
|
||||||
|
check "update by mirror url works" "[ $RC -eq 0 ] && [ \"\$(cat app/bot.py)\" = v3 ]" "rc=$RC $OUT"
|
||||||
|
check "url run mentions the source and how to save it" "grep -q 'источник: http://127.0.0.1:8799/mirror2.git' out3.txt && grep -q 'mbs mirror http' out3.txt" "$OUT"
|
||||||
|
check "url alone is not saved automatically" "[ ! -f app/.update_mirror ]"
|
||||||
|
|
||||||
|
bash "$T/mbs-run" mirror "http://127.0.0.1:8799/mirror2.git" > out4.txt 2>&1
|
||||||
|
check "mirror command saves the url" "grep -q 'http://127.0.0.1:8799/mirror2.git' app/.update_mirror"
|
||||||
|
check "mirror file is git-ignored" "[ -z \"\$(git -C app status --porcelain)\" ]" "$(git -C app status --porcelain)"
|
||||||
|
bash "$T/mbs-run" mirror > out5.txt 2>&1
|
||||||
|
check "mirror shows the saved url" "grep -q 'своё зеркало для обновлений: http://127.0.0.1:8799' out5.txt"
|
||||||
|
|
||||||
|
cd seed && echo "v4" > bot.py && git commit -qam D && git push -q "$T/mirror2.git" main && cd "$T"
|
||||||
|
git -C mirror2.git update-server-info
|
||||||
|
OUT=$(bash "$T/mbs-run" update 2>&1); RC=$?
|
||||||
|
echo "$OUT" > out6.txt
|
||||||
|
check "plain update prefers the saved mirror" "[ $RC -eq 0 ] && grep -q 'источник: своё зеркало' out6.txt && [ \"\$(cat app/bot.py)\" = v4 ]" "rc=$RC $OUT"
|
||||||
|
|
||||||
|
bash "$T/mbs-run" mirror off > out7.txt 2>&1
|
||||||
|
check "mirror off removes it" "[ ! -f app/.update_mirror ]"
|
||||||
|
|
||||||
|
for bad_url in "ext::sh -c 'touch $T/pwned'" "-uHEAD" "file:///etc" "ftp://x/y" "https://a b/c" "--upload-pack=touch $T/pwned2"; do
|
||||||
|
OUT=$(bash "$T/mbs-run" update "$bad_url" 2>&1); RC=$?
|
||||||
|
check "rejects unsafe source '$bad_url'" "[ $RC -ne 0 ] && grep -q 'не похоже на ссылку' <<< \"\$OUT\"" "rc=$RC $OUT"
|
||||||
|
done
|
||||||
|
check "no command was executed via a crafted url" "[ ! -e pwned ] && [ ! -e pwned2 ]"
|
||||||
|
OUT=$(bash "$T/mbs-run" mirror "file:///etc" 2>&1); RC=$?
|
||||||
|
check "mirror refuses unsafe urls too" "[ $RC -ne 0 ] && [ ! -f app/.update_mirror ]"
|
||||||
|
|
||||||
|
OUT=$(bash "$T/mbs-run" update "http://127.0.0.1:1/nope.git" 2>&1); RC=$?
|
||||||
|
check "unreachable mirror fails cleanly" "[ $RC -ne 0 ] && grep -q 'не удалось получить обновления по ссылке' <<< \"\$OUT\"" "rc=$RC $OUT"
|
||||||
|
|
||||||
|
cd seed && echo "v5" > bot.py && echo "broken(" > broken.py && git add -A && git commit -qm E && git push -q origin main && cd "$T"
|
||||||
|
git -C app fetch -q origin main
|
||||||
|
git -C app merge -q --ff-only origin/main~1 2>/dev/null || true
|
||||||
|
cd app && git reset -q --hard origin/main~1 2>/dev/null; cd "$T"
|
||||||
|
echo "local tweak" >> app/settings.py
|
||||||
|
BEFORE=$(git -C app rev-parse HEAD)
|
||||||
|
OUT=$(bash "$T/mbs-run" update 2>&1); RC=$?
|
||||||
|
echo "$OUT" > out8.txt
|
||||||
|
check "broken new code is rolled back" "[ $RC -ne 0 ] && grep -q 'откатываюсь' out8.txt && [ \"\$(git -C app rev-parse HEAD)\" = \"$BEFORE\" ]" "rc=$RC $OUT"
|
||||||
|
check "manual edits are restored after the rollback" "grep -q 'local tweak' app/settings.py && [ \$(git -C app stash list | wc -l) -eq 0 ]" "$(git -C app stash list)"
|
||||||
|
|
||||||
|
cd app && git checkout -q -- . && git reset -q --hard origin/main~1 && cd "$T"
|
||||||
|
cd app && echo "own" > own.txt && git add own.txt && git commit -qm "local commit" && cd "$T"
|
||||||
|
cd seed && git rm -q broken.py && echo "v6" > bot.py && git commit -qam F && git push -q origin main && cd "$T"
|
||||||
|
OUT=$(bash "$T/mbs-run" update 2>&1); RC=$?
|
||||||
|
echo "$OUT" > out9.txt
|
||||||
|
check "diverged server history is refused, not merged" "[ $RC -ne 0 ] && grep -q 'свои коммиты' out9.txt" "rc=$RC $OUT"
|
||||||
|
check "refused update leaves the local commit alone" "git -C app log --oneline | grep -q 'local commit'"
|
||||||
|
|
||||||
|
cd app && git reset -q --hard origin/main && echo "ahead" > ahead.txt && git add ahead.txt && git commit -qm ahead && cd "$T"
|
||||||
|
OUT=$(bash "$T/mbs-run" update 2>&1); RC=$?
|
||||||
|
check "server newer than the source is left alone" "[ $RC -eq 0 ] && grep -q 'версия новее' <<< \"\$OUT\"" "rc=$RC $OUT"
|
||||||
|
|
||||||
|
cd "$T/app" && git reset -q --hard origin/main && cd "$T"
|
||||||
|
cd seed && echo "v7" > bot.py && git commit -qam G && git push -q origin main && cd "$T"
|
||||||
|
BEFORE=$(git -C app rev-parse HEAD)
|
||||||
|
echo "x" > notadir
|
||||||
|
OUT=$(MBS_BACKUP_DIR="$T/notadir/x" bash "$T/mbs-run" update 2>&1); RC=$?
|
||||||
|
check "update refuses to start when no backup can be made" "[ $RC -ne 0 ] && grep -q 'не начинаю' <<< \"\$OUT\" && [ \"\$(git -C app rev-parse HEAD)\" = \"$BEFORE\" ]" "rc=$RC $OUT"
|
||||||
|
check "refused update leaves the code untouched" "[ \"\$(cat app/bot.py)\" != v7 ]"
|
||||||
|
|
||||||
|
kill $HTTP_PID 2>/dev/null
|
||||||
|
cd /
|
||||||
|
rm -rf "$T"
|
||||||
|
echo "RESULT pass=$PASS fail=$FAIL"
|
||||||
|
[ "$FAIL" -eq 0 ]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue