DevOps

Проверка версий и каналы обновлений (stable/beta)

Введение

Не все пользователи должны получать одно и то же обновление одновременно — часто нужно разделить аудиторию на каналы: стабильный (stable) канал для большинства пользователей, получающих только тщательно протестированные релизы, и бета-канал (beta) для энтузиастов или внутренней команды, готовых тестировать более свежую, потенциально менее стабильную функциональность раньше остальных. В статье разберём, как организовать несколько каналов обновлений поверх механизма QtIFW, разобранного в предыдущей статье.

Концепция

QtIFW не имеет встроенной «из коробки» концепции множественных каналов обновления — реализация строится на организации нескольких отдельных репозиториев (один на канал) и переключении между ними через настройку RemoteRepositories в конфигурации приложения, обновляемой самим приложением программно в зависимости от того, к какому каналу подписан конкретный пользователь. Переключение канала пользователем обычно реализуется через настройку в самом приложении, которая модифицирует список используемых репозиториев maintenance tool перед следующей проверкой обновлений.

Пример кода: Проверка версий и

# Генерация ОТДЕЛЬНЫХ репозиториев для каждого канала из соответствующего набора пакетов
repogen -p packages_stable --update-new-components repo_stable
repogen -p packages_beta --update-new-components repo_beta

# Каждый репозиторий размещается на сервере по своему собственному пути
rsync -avz repo_stable/ user@updates.mycompany.com:/var/www/myapp-updates/stable/
rsync -avz repo_beta/ user@updates.mycompany.com:/var/www/myapp-updates/beta/
// updatechannelmanager.h — переключение канала обновлений из самого приложения
#pragma once
#include <QSettings>
#include <QProcess>
#include <QCoreApplication>

class UpdateChannelManager
{
public:
    enum class Channel { Stable, Beta };

    static Channel currentChannel()
    {
        const QString saved = QSettings().value("updates/channel", "stable").toString();
        return (saved == "beta") ? Channel::Beta : Channel::Stable;
    }

    static void setChannel(Channel channel)
    {
        QSettings settings;
        settings.setValue("updates/channel", channel == Channel::Beta ? "beta" : "stable");

        // Перенастраиваем maintenance tool на использование нужного репозитория
        const QString repositoryUrl = (channel == Channel::Beta)
            ? "https://updates.mycompany.com/myapp-updates/beta"
            : "https://updates.mycompany.com/myapp-updates/stable";

        const QString maintenanceToolPath = QCoreApplication::applicationDirPath()
                                           + "/MyAppUpdater";

        QProcess::execute(maintenanceToolPath,
            {"--addRepository", repositoryUrl, "--removeRepository", oppositeRepositoryUrl(channel)});
    }

private:
    static QString oppositeRepositoryUrl(Channel channel)
    {
        return (channel == Channel::Beta)
            ? "https://updates.mycompany.com/myapp-updates/stable"
            : "https://updates.mycompany.com/myapp-updates/beta";
    }
};
// Использование в настройках приложения
void SettingsDialog::onChannelComboBoxChanged(int index)
{
    const auto newChannel = (index == 1) ? UpdateChannelManager::Channel::Beta
                                          : UpdateChannelManager::Channel::Stable;

    if (newChannel != UpdateChannelManager::currentChannel()) {
        UpdateChannelManager::setChannel(newChannel);
        QMessageBox::information(this, "Канал обновлений изменён",
            "Изменение вступит в силу при следующей проверке обновлений.");
    }
}

Пояснения к коду

Каждый канал обновлений генерируется как полностью независимый репозиторий через отдельный вызов repogen с собственным набором пакетов (packages_stable/packages_beta) — это позволяет, например, иметь в бета-репозитории компонент с более новой версией, чем в стабильном, при том что оба репозитория физически независимы и размещены по разным путям на сервере, не конфликтуя друг с другом.

UpdateChannelManager::setChannel() сохраняет выбор канала в QSettings для последующего отображения текущего состояния в интерфейсе настроек, и одновременно вызывает maintenance tool с флагами --addRepository/--removeRepository, программно изменяя список репозиториев, которые maintenance tool будет опрашивать при следующей проверке обновлений — этот вызов происходит через QProcess::execute() синхронно, поскольку операция достаточно быстрая (просто изменение локальной конфигурации, без сетевого обращения на этом этапе).

Подводные камни

  • Переключение с бета на стабильный канал не означает автоматический «откат» уже установленных бета-версий компонентов до стабильной версии. Если пользователь уже обновился до бета-версии 1.2.0-beta, а затем переключился на стабильный канал, где максимальная доступная версия — 1.1.0, maintenance tool, скорее всего, не предложит «понижение» версии автоматически (зависит от конкретной настройки версионирования) — для поддержки реального откат-сценария нужна отдельная, явно спроектированная логика, не предоставляемая автоматически базовым механизмом QtIFW.
  • Несогласованность номеров версий между каналами, если бета-версии не следуют чёткой, однозначно сравнимой схеме версионирования относительно стабильных релизов (например, использование суффикса -beta, который не все инструменты сравнения версий корректно интерпретируют как «более новую, но менее стабильную» версию) — нужна явная, продуманная схема версионирования (semver с пре-релизными суффиксами), совместимая с тем, как именно QtIFW сравнивает строки версий внутри Updates.xml.
  • Раздельное поддержание двух (или более) параллельных наборов пакетов для разных каналов увеличивает нагрузку на процесс релиза. Команде нужно явно решить, является ли бета-канал просто «более ранним» срезом того же кода (что технически проще поддерживать, синхронизируя пакеты автоматически из CI) или полноценной отдельной веткой с собственной независимой логикой возможных отличий — выбор архитектуры процесса релиза существенно влияет на сложность поддержки множественных каналов в долгосрочной перспективе.
  • Отсутствие явного предупреждения пользователю о потенциальной нестабильности бета-канала перед переключением — пользователь, случайно выбравший бета-канал из любопытства, может столкнуться с реальными багами и составить негативное мнение о качестве продукта в целом, не понимая, что находится на тестовом канале — UI переключения каналов должен явно и недвусмысленно предупреждать о характере и рисках выбора нестабильного канала, как частично показано во всплывающем сообщении в примере, но в реальном продукте формулировка должна быть заметно более явной о потенциальных рисках.