Стабильный канал обновлений был добавлен; клон и увеличение диска защищены от спящих машин
- stable (по умолчанию): обновление до последнего тега vX.Y.Z; пока релизов нет — ветка main - main: ранний доступ, все изменения; канал берётся из web.env (Настройки → Обновления) - scripts/update_target.sh общий для автообновления и ручного обновления, ZIP-запасной путь для тегов - страница «Обновления» показывает канал и целевую версию - клонирование и увеличение диска недоступны, пока машина в спящем режиме Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
@@ -7,8 +7,11 @@ STATE_FILE="${STATE_DIR}/state.json"
|
|||||||
LOG_FILE="/var/log/virtuality/update.log"
|
LOG_FILE="/var/log/virtuality/update.log"
|
||||||
REMOTE="${VIRTUALITY_UPDATE_REMOTE:-origin}"
|
REMOTE="${VIRTUALITY_UPDATE_REMOTE:-origin}"
|
||||||
BRANCH="${VIRTUALITY_UPDATE_BRANCH:-main}"
|
BRANCH="${VIRTUALITY_UPDATE_BRANCH:-main}"
|
||||||
REPO_ZIP_URL="${VIRTUALITY_UPDATE_ZIP_URL:-https://github.com/viktor138irk/virtuality/archive/refs/heads/${BRANCH}.zip}"
|
|
||||||
TMP_DIR="${STATE_DIR}/tmp"
|
TMP_DIR="${STATE_DIR}/tmp"
|
||||||
|
# shellcheck source=scripts/update_target.sh
|
||||||
|
. "$(dirname "${BASH_SOURCE[0]}")/update_target.sh"
|
||||||
|
resolve_update_target
|
||||||
|
REPO_ZIP_URL="${VIRTUALITY_UPDATE_ZIP_URL:-$TARGET_ZIP_URL}"
|
||||||
|
|
||||||
mkdir -p "$STATE_DIR" "$TMP_DIR" "$(dirname "$LOG_FILE")"
|
mkdir -p "$STATE_DIR" "$TMP_DIR" "$(dirname "$LOG_FILE")"
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ try_run() {
|
|||||||
|
|
||||||
fetch_zip_update() {
|
fetch_zip_update() {
|
||||||
local zip_file extract_dir extracted_root
|
local zip_file extract_dir extracted_root
|
||||||
zip_file="${TMP_DIR}/virtuality-${BRANCH}.zip"
|
zip_file="${TMP_DIR}/virtuality-${TARGET_LABEL}.zip"
|
||||||
extract_dir="${TMP_DIR}/zip-update"
|
extract_dir="${TMP_DIR}/zip-update"
|
||||||
rm -rf "$zip_file" "$extract_dir"
|
rm -rf "$zip_file" "$extract_dir"
|
||||||
mkdir -p "$extract_dir"
|
mkdir -p "$extract_dir"
|
||||||
@@ -127,9 +130,10 @@ GIT_UPDATED=0
|
|||||||
if [ -d "$SOURCE_DIR/.git" ]; then
|
if [ -d "$SOURCE_DIR/.git" ]; then
|
||||||
cd "$SOURCE_DIR" || exit 1
|
cd "$SOURCE_DIR" || exit 1
|
||||||
write_state "running" "Получаем изменения из GitHub через git"
|
write_state "running" "Получаем изменения из GitHub через git"
|
||||||
if try_run git fetch "$REMOTE" "$BRANCH"; then
|
if try_run git fetch --tags --force "$REMOTE" "$BRANCH"; then
|
||||||
TARGET_REF="${REMOTE}/${BRANCH}"
|
resolve_update_target
|
||||||
TARGET_COMMIT="$(git rev-parse "$TARGET_REF" 2>/dev/null || true)"
|
log "channel: ${UPDATE_CHANNEL}, target: ${TARGET_LABEL}"
|
||||||
|
TARGET_COMMIT="$(git rev-parse "${TARGET_REF}^{commit}" 2>/dev/null || true)"
|
||||||
if [ -n "$TARGET_COMMIT" ]; then
|
if [ -n "$TARGET_COMMIT" ]; then
|
||||||
LOCAL_STATUS="$(git status --porcelain 2>/dev/null || true)"
|
LOCAL_STATUS="$(git status --porcelain 2>/dev/null || true)"
|
||||||
if [ -n "$LOCAL_STATUS" ]; then
|
if [ -n "$LOCAL_STATUS" ]; then
|
||||||
@@ -137,8 +141,8 @@ if [ -d "$SOURCE_DIR/.git" ]; then
|
|||||||
printf '%s\n' "$LOCAL_STATUS" >> "$LOG_FILE"
|
printf '%s\n' "$LOCAL_STATUS" >> "$LOG_FILE"
|
||||||
fi
|
fi
|
||||||
write_state "running" "Синхронизируем исходники с GitHub через git reset"
|
write_state "running" "Синхронизируем исходники с GitHub через git reset"
|
||||||
log "Deploy mode: git reset --hard $TARGET_REF"
|
log "Deploy mode: git checkout -B ${BRANCH} ${TARGET_LABEL}"
|
||||||
if try_run git reset --hard "$TARGET_REF" && try_run git clean -fd; then
|
if try_run git checkout -q -f -B "$BRANCH" "$TARGET_COMMIT" && try_run git clean -fd; then
|
||||||
GIT_UPDATED=1
|
GIT_UPDATED=1
|
||||||
else
|
else
|
||||||
log "git reset/clean failed, ZIP fallback will be used"
|
log "git reset/clean failed, ZIP fallback will be used"
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ LOG_FILE="/var/log/virtuality/update.log"
|
|||||||
REMOTE="${VIRTUALITY_UPDATE_REMOTE:-origin}"
|
REMOTE="${VIRTUALITY_UPDATE_REMOTE:-origin}"
|
||||||
BRANCH="${VIRTUALITY_UPDATE_BRANCH:-main}"
|
BRANCH="${VIRTUALITY_UPDATE_BRANCH:-main}"
|
||||||
APPLY_SCRIPT="${SOURCE_DIR}/scripts/apply_github_update.sh"
|
APPLY_SCRIPT="${SOURCE_DIR}/scripts/apply_github_update.sh"
|
||||||
ZIP_URL="${VIRTUALITY_UPDATE_ZIP_URL:-https://github.com/viktor138irk/virtuality/archive/refs/heads/${BRANCH}.zip}"
|
# shellcheck source=scripts/update_target.sh
|
||||||
|
. "${SOURCE_DIR}/scripts/update_target.sh"
|
||||||
|
|
||||||
mkdir -p "$STATE_DIR" "$(dirname "$LOG_FILE")"
|
mkdir -p "$STATE_DIR" "$(dirname "$LOG_FILE")"
|
||||||
|
|
||||||
@@ -89,16 +90,18 @@ diag_network() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d "$SOURCE_DIR/.git" ]; then
|
if [ -d "$SOURCE_DIR/.git" ]; then
|
||||||
if git fetch --quiet "$REMOTE" "$BRANCH" >> "$LOG_FILE" 2>&1; then
|
if git fetch --quiet --tags --force "$REMOTE" "$BRANCH" >> "$LOG_FILE" 2>&1; then
|
||||||
latest="$(git rev-parse "${REMOTE}/${BRANCH}" 2>/dev/null || true)"
|
resolve_update_target
|
||||||
|
log "channel: ${UPDATE_CHANNEL}, target: ${TARGET_LABEL}"
|
||||||
|
latest="$(git rev-parse "${TARGET_REF}^{commit}" 2>/dev/null || true)"
|
||||||
if [ -n "$latest" ] && [ "$current" = "$latest" ]; then
|
if [ -n "$latest" ] && [ "$current" = "$latest" ]; then
|
||||||
log "no updates: ${current:0:12} / version $current_version"
|
log "no updates: ${current:0:12} / version $current_version"
|
||||||
write_state "idle" "Автообновление: новых обновлений нет"
|
write_state "idle" "Автообновление: новых обновлений нет"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
if [ -n "$latest" ] && [ "$current" != "unknown" ] && ! git merge-base --is-ancestor "$current" "$latest" 2>/dev/null; then
|
if [ -n "$latest" ] && [ "$current" != "unknown" ] && ! git merge-base --is-ancestor "$current" "$latest" 2>/dev/null; then
|
||||||
log "local build ${current:0:12} is not behind ${REMOTE}/${BRANCH} (${latest:0:12}); skip to avoid a downgrade"
|
log "local build ${current:0:12} is not behind ${TARGET_LABEL} (${latest:0:12}); skip to avoid a downgrade"
|
||||||
write_state "idle" "Автообновление: установленная сборка новее или отличается от ${BRANCH}, обновление пропущено"
|
write_state "idle" "Автообновление: установленная версия новее ${TARGET_LABEL}, обновление пропущено"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
log "update may be available or current commit unknown: ${current:0:12} -> ${latest:0:12}"
|
log "update may be available or current commit unknown: ${current:0:12} -> ${latest:0:12}"
|
||||||
@@ -117,6 +120,6 @@ diag_network() {
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
write_state "running" "Автообновление: проверяем и устанавливаем обновление через устойчивый механизм"
|
write_state "running" "Автообновление: устанавливаем обновление"
|
||||||
VIRTUALITY_SOURCE_DIR="$SOURCE_DIR" VIRTUALITY_UPDATE_REMOTE="$REMOTE" VIRTUALITY_UPDATE_BRANCH="$BRANCH" VIRTUALITY_UPDATE_ZIP_URL="$ZIP_URL" bash "$APPLY_SCRIPT"
|
VIRTUALITY_SOURCE_DIR="$SOURCE_DIR" VIRTUALITY_UPDATE_REMOTE="$REMOTE" VIRTUALITY_UPDATE_BRANCH="$BRANCH" bash "$APPLY_SCRIPT"
|
||||||
) 9>"$LOCK_FILE"
|
) 9>"$LOCK_FILE"
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sourced by auto_update_check.sh and apply_github_update.sh: which git ref a node updates to.
|
||||||
|
# stable (default) — the latest release tag vX.Y.Z; main until the first release is tagged
|
||||||
|
# main — every change on the main branch
|
||||||
|
# Needs SOURCE_DIR, REMOTE and BRANCH; sets UPDATE_CHANNEL, TARGET_REF, TARGET_LABEL, TARGET_ZIP_URL.
|
||||||
|
|
||||||
|
update_channel() {
|
||||||
|
local value="${VIRTUALITY_UPDATE_CHANNEL:-}"
|
||||||
|
if [[ -z "$value" && -f /var/lib/virtuality/config/web.env ]]; then
|
||||||
|
value="$(sed -n 's/^VIRTUALITY_UPDATE_CHANNEL=//p' /var/lib/virtuality/config/web.env | tail -n1 | tr -d "\"'")"
|
||||||
|
fi
|
||||||
|
[[ "$value" == "main" ]] && echo main || echo stable
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_update_target() {
|
||||||
|
local tag=""
|
||||||
|
UPDATE_CHANNEL="$(update_channel)"
|
||||||
|
TARGET_REF="${REMOTE}/${BRANCH}"
|
||||||
|
TARGET_LABEL="${BRANCH}"
|
||||||
|
TARGET_ZIP_URL="https://github.com/viktor138irk/virtuality/archive/refs/heads/${BRANCH}.zip"
|
||||||
|
if [[ "$UPDATE_CHANNEL" == "stable" && -d "${SOURCE_DIR}/.git" ]]; then
|
||||||
|
tag="$(git -C "$SOURCE_DIR" tag --list 'v[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+(\.[0-9]+)*$' | head -n1 || true)"
|
||||||
|
if [[ -n "$tag" ]]; then
|
||||||
|
TARGET_REF="refs/tags/${tag}"
|
||||||
|
TARGET_LABEL="$tag"
|
||||||
|
TARGET_ZIP_URL="https://github.com/viktor138irk/virtuality/archive/refs/tags/${tag}.zip"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -102,3 +102,41 @@ def test_update_not_offered_when_local_is_newer(tmp_path, monkeypatch, data_dirs
|
|||||||
git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", "upstream", cwd=remote)
|
git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", "upstream", cwd=remote)
|
||||||
git("reset", "-q", "--hard", "origin/main", cwd=local)
|
git("reset", "-q", "--hard", "origin/main", cwd=local)
|
||||||
assert update_core.check_updates(fetch=True)["has_update"] is True
|
assert update_core.check_updates(fetch=True)["has_update"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_stable_channel_follows_release_tags(tmp_path, monkeypatch, data_dirs):
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def git(*args, cwd):
|
||||||
|
subprocess.run(["git", "-c", "user.name=t", "-c", "user.email=t@t", *args], cwd=cwd, check=True, capture_output=True)
|
||||||
|
|
||||||
|
remote, local = tmp_path / "remote", tmp_path / "local"
|
||||||
|
remote.mkdir()
|
||||||
|
git("init", "-q", "-b", "main", cwd=remote)
|
||||||
|
(remote / "VERSION").write_text("0.10.0\n")
|
||||||
|
git("add", "VERSION", cwd=remote)
|
||||||
|
git("commit", "-q", "-m", "0.10.0", cwd=remote)
|
||||||
|
git("clone", "-q", str(remote), str(local), cwd=tmp_path)
|
||||||
|
monkeypatch.setattr(update_core, "SOURCE_DIR", local)
|
||||||
|
monkeypatch.setattr(update_core, "NODE_ENV", tmp_path / "web.env")
|
||||||
|
monkeypatch.delenv("VIRTUALITY_UPDATE_CHANNEL", raising=False)
|
||||||
|
assert update_core.update_channel() == "stable"
|
||||||
|
|
||||||
|
# Work on main without a release: stable follows main until the first tag exists.
|
||||||
|
(remote / "VERSION").write_text("0.11.0\n")
|
||||||
|
git("commit", "-q", "-am", "0.11.0", cwd=remote)
|
||||||
|
data = update_core.check_updates(fetch=True)
|
||||||
|
assert data["has_update"] and data["target"] == "main"
|
||||||
|
|
||||||
|
# With a release tag, stable offers the tag and ignores newer commits on main.
|
||||||
|
git("tag", "v0.11.0", cwd=remote)
|
||||||
|
git("commit", "-q", "--allow-empty", "-m", "work in progress", cwd=remote)
|
||||||
|
data = update_core.check_updates(fetch=True)
|
||||||
|
assert data["channel"] == "stable" and data["target"] == "v0.11.0" and data["latest_version"] == "0.11.0" and data["has_update"]
|
||||||
|
git("checkout", "-q", "v0.11.0", cwd=local)
|
||||||
|
assert update_core.check_updates(fetch=True)["has_update"] is False
|
||||||
|
|
||||||
|
# The early-access channel sees every change on main.
|
||||||
|
(tmp_path / "web.env").write_text("VIRTUALITY_UPDATE_CHANNEL=main\n")
|
||||||
|
data = update_core.check_updates(fetch=True)
|
||||||
|
assert data["channel"] == "main" and data["target"] == "main" and data["has_update"]
|
||||||
|
|||||||
@@ -174,6 +174,11 @@ def parse_desc_output(result: dict[str, Any]) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def saved_state(name: str) -> bool:
|
||||||
|
"""The machine is hibernated (virsh managedsave): its disks must not change until it is resumed."""
|
||||||
|
return has_managed_save(run(["virsh", "dominfo", name], timeout=10)["stdout"])
|
||||||
|
|
||||||
|
|
||||||
def has_managed_save(dominfo: str) -> bool:
|
def has_managed_save(dominfo: str) -> bool:
|
||||||
"""Строка «Managed save: yes» в `virsh dominfo` — машина в спящем режиме."""
|
"""Строка «Managed save: yes» в `virsh dominfo` — машина в спящем режиме."""
|
||||||
match = re.search(r"^Managed save:\s*(\S+)", dominfo or "", re.MULTILINE | re.IGNORECASE)
|
match = re.search(r"^Managed save:\s*(\S+)", dominfo or "", re.MULTILINE | re.IGNORECASE)
|
||||||
@@ -372,6 +377,8 @@ def disk_resize(request: Request, name: str, target: str, size_gb: int = Form(..
|
|||||||
free = free_space(disk["source"])
|
free = free_space(disk["source"])
|
||||||
if growth > free:
|
if growth > free:
|
||||||
return redirect_tab(name, "storage", "disk_error", f"На сервере свободно только {presenters.format_bytes(free)}: столько добавить диску не получится")
|
return redirect_tab(name, "storage", "disk_error", f"На сервере свободно только {presenters.format_bytes(free)}: столько добавить диску не получится")
|
||||||
|
if saved_state(name):
|
||||||
|
return redirect_tab(name, "storage", "disk_error", "Машина в спящем режиме: сохранённая память рассчитана на старый размер диска. Сначала запустите её и выключите обычным образом.")
|
||||||
if is_shut_off(vm_state(name)):
|
if is_shut_off(vm_state(name)):
|
||||||
result = run(["qemu-img", "resize", disk["source"], f"{size_gb}G"], timeout=120)
|
result = run(["qemu-img", "resize", disk["source"], f"{size_gb}G"], timeout=120)
|
||||||
else:
|
else:
|
||||||
@@ -413,6 +420,8 @@ def clone_context(name: str) -> dict[str, Any]:
|
|||||||
blockers = []
|
blockers = []
|
||||||
if not is_shut_off(state):
|
if not is_shut_off(state):
|
||||||
blockers.append("running")
|
blockers.append("running")
|
||||||
|
elif saved_state(name):
|
||||||
|
blockers.append("saved")
|
||||||
if not cloneable:
|
if not cloneable:
|
||||||
blockers.append("no_disks")
|
blockers.append("no_disks")
|
||||||
if needed > free:
|
if needed > free:
|
||||||
|
|||||||
@@ -66,7 +66,9 @@
|
|||||||
<div class="card-body" style="padding-top:6px;padding-bottom:6px">
|
<div class="card-body" style="padding-top:6px;padding-bottom:6px">
|
||||||
<div class="summary-row"><span>Установлена</span><strong>{{ info.current_version }}</strong></div>
|
<div class="summary-row"><span>Установлена</span><strong>{{ info.current_version }}</strong></div>
|
||||||
{% if info.has_update %}<div class="summary-row"><span>Доступна</span><strong>{{ info.latest_version }}</strong></div>{% endif %}
|
{% if info.has_update %}<div class="summary-row"><span>Доступна</span><strong>{{ info.latest_version }}</strong></div>{% endif %}
|
||||||
<div class="summary-row"><span>Канал</span><strong>{{ info.branch }}</strong></div>
|
<div class="summary-row"><span>Канал</span><strong>{{ info.channel_label or info.branch }}</strong></div>
|
||||||
|
{% if info.target %}<div class="summary-row"><span>Обновляемся до</span><strong class="mono">{{ info.target }}</strong></div>{% endif %}
|
||||||
|
<div class="summary-row"><span></span><a class="small" href="/settings#updates">Сменить канал</a></div>
|
||||||
<div class="summary-row"><span>Сборка</span><strong class="mono">{{ info.current_commit[:8] if info.current_commit else '—' }}</strong></div>
|
<div class="summary-row"><span>Сборка</span><strong class="mono">{{ info.current_commit[:8] if info.current_commit else '—' }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
{% if st.running %}<form method="post" action="/vm/{{ vm_name }}/shutdown" class="alert-action" data-toast="Машина выключается — это может занять до минуты"><button class="btn btn-sm">{{ icon('power', 'icon-sm') }}Выключить</button></form>{% endif %}
|
{% if st.running %}<form method="post" action="/vm/{{ vm_name }}/shutdown" class="alert-action" data-toast="Машина выключается — это может занять до минуты"><button class="btn btn-sm">{{ icon('power', 'icon-sm') }}Выключить</button></form>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if 'saved' in blockers %}{{ alert('warning', 'Машина в спящем режиме, её память сохранена на диске. Запустите машину и выключите её обычным образом — тогда копия будет целой.', 'Сначала разбудите машину') }}{% endif %}
|
||||||
{% if 'space' in blockers %}{{ alert('danger', 'Копии нужно ' ~ needed_text ~ ', а свободно только ' ~ free_text ~ '. Удалите ненужные машины или образы.', 'Не хватает места на сервере') }}{% endif %}
|
{% if 'space' in blockers %}{{ alert('danger', 'Копии нужно ' ~ needed_text ~ ', а свободно только ' ~ free_text ~ '. Удалите ненужные машины или образы.', 'Не хватает места на сервере') }}{% endif %}
|
||||||
{% if 'no_disks' in blockers %}{{ alert('danger', 'У машины нет дисков, которые можно скопировать.') }}{% endif %}
|
{% if 'no_disks' in blockers %}{{ alert('danger', 'У машины нет дисков, которые можно скопировать.') }}{% endif %}
|
||||||
{% if 'busy' in blockers %}{{ alert('info', 'Эта машина уже клонируется. Дождитесь окончания в разделе «Задачи».') }}{% endif %}
|
{% if 'busy' in blockers %}{{ alert('info', 'Эта машина уже клонируется. Дождитесь окончания в разделе «Задачи».') }}{% endif %}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ STATE_FILE = STATE_DIR / 'state.json'
|
|||||||
LOG_FILE = Path('/var/log/virtuality/update.log')
|
LOG_FILE = Path('/var/log/virtuality/update.log')
|
||||||
DEFAULT_BRANCH = os.environ.get('VIRTUALITY_UPDATE_BRANCH', 'main')
|
DEFAULT_BRANCH = os.environ.get('VIRTUALITY_UPDATE_BRANCH', 'main')
|
||||||
REMOTE = os.environ.get('VIRTUALITY_UPDATE_REMOTE', 'origin')
|
REMOTE = os.environ.get('VIRTUALITY_UPDATE_REMOTE', 'origin')
|
||||||
|
NODE_ENV = Path(os.environ.get('VIRTUALITY_NODE_ENV', '/var/lib/virtuality/config/web.env'))
|
||||||
|
CHANNELS = {'stable': 'Стабильный — выпущенные версии', 'main': 'Ранний доступ — все изменения'}
|
||||||
|
|
||||||
|
|
||||||
class UpdateError(Exception):
|
class UpdateError(Exception):
|
||||||
@@ -69,8 +71,37 @@ def current_commit() -> str:
|
|||||||
return result['stdout'] if result['ok'] else ''
|
return result['stdout'] if result['ok'] else ''
|
||||||
|
|
||||||
|
|
||||||
def latest_commit() -> str:
|
def update_channel() -> str:
|
||||||
result = run_cmd(['git', 'rev-parse', f'{REMOTE}/{DEFAULT_BRANCH}'], cwd=SOURCE_DIR, timeout=10)
|
"""stable: the latest release tag vX.Y.Z; main: every change on the main branch."""
|
||||||
|
value = os.environ.get('VIRTUALITY_UPDATE_CHANNEL', '')
|
||||||
|
if not value:
|
||||||
|
try:
|
||||||
|
for line in NODE_ENV.read_text().splitlines():
|
||||||
|
if line.startswith('VIRTUALITY_UPDATE_CHANNEL='):
|
||||||
|
value = line.split('=', 1)[1].strip().strip('"').strip("'")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return value if value in CHANNELS else 'stable'
|
||||||
|
|
||||||
|
|
||||||
|
def latest_release_tag() -> str:
|
||||||
|
result = run_cmd(['git', 'tag', '--list', 'v[0-9]*', '--sort=-v:refname'], cwd=SOURCE_DIR, timeout=10)
|
||||||
|
tags = [tag for tag in result['stdout'].split() if all(part.isdigit() for part in tag[1:].split('.'))] if result['ok'] else []
|
||||||
|
return tags[0] if tags else ''
|
||||||
|
|
||||||
|
|
||||||
|
def target_ref(channel: str | None = None) -> str:
|
||||||
|
"""Git ref the node updates to. Until the first release is tagged, stable follows the main branch."""
|
||||||
|
channel = channel or update_channel()
|
||||||
|
if channel == 'stable':
|
||||||
|
tag = latest_release_tag()
|
||||||
|
if tag:
|
||||||
|
return f'refs/tags/{tag}'
|
||||||
|
return f'{REMOTE}/{DEFAULT_BRANCH}'
|
||||||
|
|
||||||
|
|
||||||
|
def latest_commit(ref: str | None = None) -> str:
|
||||||
|
result = run_cmd(['git', 'rev-parse', f'{ref or target_ref()}^{{commit}}'], cwd=SOURCE_DIR, timeout=10)
|
||||||
return result['stdout'] if result['ok'] else ''
|
return result['stdout'] if result['ok'] else ''
|
||||||
|
|
||||||
|
|
||||||
@@ -91,8 +122,8 @@ def current_version() -> str:
|
|||||||
return local_file_text('VERSION') or '0.0.0'
|
return local_file_text('VERSION') or '0.0.0'
|
||||||
|
|
||||||
|
|
||||||
def latest_version() -> str:
|
def latest_version(ref: str | None = None) -> str:
|
||||||
value = file_from_git(f'{REMOTE}/{DEFAULT_BRANCH}', 'VERSION').strip()
|
value = file_from_git(ref or target_ref(), 'VERSION').strip()
|
||||||
return value or current_version()
|
return value or current_version()
|
||||||
|
|
||||||
|
|
||||||
@@ -169,13 +200,15 @@ def check_updates(fetch: bool = True) -> dict[str, Any]:
|
|||||||
|
|
||||||
fetch_result = {'ok': True, 'stdout': '', 'stderr': '', 'cmd': 'skip'}
|
fetch_result = {'ok': True, 'stdout': '', 'stderr': '', 'cmd': 'skip'}
|
||||||
if fetch:
|
if fetch:
|
||||||
fetch_result = run_cmd(['git', 'fetch', '--quiet', REMOTE, DEFAULT_BRANCH], cwd=SOURCE_DIR, timeout=60)
|
fetch_result = run_cmd(['git', 'fetch', '--quiet', '--tags', '--force', REMOTE, DEFAULT_BRANCH], cwd=SOURCE_DIR, timeout=60)
|
||||||
|
|
||||||
|
channel = update_channel()
|
||||||
|
ref = target_ref(channel)
|
||||||
local_commit = current_commit()
|
local_commit = current_commit()
|
||||||
remote_commit = latest_commit()
|
remote_commit = latest_commit(ref)
|
||||||
local_version = current_version()
|
local_version = current_version()
|
||||||
remote_version = latest_version()
|
remote_version = latest_version(ref)
|
||||||
manifest = load_manifest(f'{REMOTE}/{DEFAULT_BRANCH}')
|
manifest = load_manifest(ref)
|
||||||
missed = missing_versions(local_version, remote_version, manifest)
|
missed = missing_versions(local_version, remote_version, manifest)
|
||||||
commits = git_log_between(local_commit, remote_commit)
|
commits = git_log_between(local_commit, remote_commit)
|
||||||
has_update = bool(local_commit and remote_commit and local_commit != remote_commit and is_ancestor(local_commit, remote_commit))
|
has_update = bool(local_commit and remote_commit and local_commit != remote_commit and is_ancestor(local_commit, remote_commit))
|
||||||
@@ -185,6 +218,9 @@ def check_updates(fetch: bool = True) -> dict[str, Any]:
|
|||||||
'source_dir': str(SOURCE_DIR),
|
'source_dir': str(SOURCE_DIR),
|
||||||
'remote': REMOTE,
|
'remote': REMOTE,
|
||||||
'branch': DEFAULT_BRANCH,
|
'branch': DEFAULT_BRANCH,
|
||||||
|
'channel': channel,
|
||||||
|
'channel_label': CHANNELS[channel],
|
||||||
|
'target': ref.removeprefix('refs/tags/').removeprefix(f'{REMOTE}/'),
|
||||||
'fetch_ok': fetch_result['ok'],
|
'fetch_ok': fetch_result['ok'],
|
||||||
'fetch_error': fetch_result['stderr'] or fetch_result['stdout'],
|
'fetch_error': fetch_result['stderr'] or fetch_result['stdout'],
|
||||||
'current_commit': local_commit,
|
'current_commit': local_commit,
|
||||||
@@ -230,6 +266,6 @@ def start_update() -> dict[str, Any]:
|
|||||||
# Run outside virtuality-web's cgroup: the update restarts virtuality-web,
|
# Run outside virtuality-web's cgroup: the update restarts virtuality-web,
|
||||||
# which would otherwise kill the update script before it finishes.
|
# which would otherwise kill the update script before it finishes.
|
||||||
unit = f"virtuality-update-{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
unit = f"virtuality-update-{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
cmd = ['systemd-run', '--unit', unit, '--collect', '--quiet', f'--setenv=VIRTUALITY_SOURCE_DIR={SOURCE_DIR}', f'--working-directory={SOURCE_DIR}'] + cmd
|
cmd = ['systemd-run', '--unit', unit, '--collect', '--quiet', f'--setenv=VIRTUALITY_SOURCE_DIR={SOURCE_DIR}', f'--setenv=VIRTUALITY_UPDATE_CHANNEL={update_channel()}', f'--working-directory={SOURCE_DIR}'] + cmd
|
||||||
subprocess.Popen(cmd, cwd=str(SOURCE_DIR), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
|
subprocess.Popen(cmd, cwd=str(SOURCE_DIR), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
|
||||||
return data
|
return data
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user