The A3Guard update system uses a two-way architecture:
- Release Side (GitHub): Automatic build and packaging
- Client Side (App): Detect new versions and download updates
This document explains the complete flow from release to user update.
When you create a release, the workflow is triggered by pushing a git tag:
# Current version in Common.h: 1.0.0
./scripts/update-version.sh 1.0.0 1.1.0 # Update all version numbers
git add .
git commit -m "Bump version: 1.0.0 → 1.1.0"
git tag -a v1.1.0 -m "Release v1.1.0"
git push origin main && git push origin v1.1.0 # TRIGGERS workflowFile: .github/workflows/release.yml (primary) or build-deb.yml (triggered on tag)
When tag v1.1.0 is pushed, GitHub Actions automatically:
┌─────────────────────────────────────────────────────────┐
│ GitHub Actions - Release Workflow Triggered │
└─────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ JOB 1: validate-tag │
│ ├─ Extract version from tag name │
│ │ (v1.1.0 → 1.1.0) │
│ └─ Output: version=1.1.0 │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ JOB 2: build-release │
│ ├─ Checkout code on Ubuntu 20.04 │
│ ├─ Install dependencies │
│ │ (Qt5, OpenSSL, cmake, etc.) │
│ ├─ Run CMake with version 1.1.0 │
│ ├─ Build optimized Release binary │
│ ├─ Create DEB package │
│ ├─ Verify DEB integrity │
│ ├─ Create GitHub Release │
│ └─ Attach DEB file to Release │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ JOB 3: security-scan │
│ ├─ Run Trivy vulnerability scanner │
│ ├─ Upload to GitHub Security │
│ └─ Non-blocking (continues if fails) │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ JOB 4: notify-success │
│ ├─ Print success message │
│ └─ Show release URL │
└──────────────────────────────────────┘
After build completes, GitHub Release contains:
URL: https://github.com/iamthemag/A3Guard/releases/tag/v1.1.0
Assets:
📦 a3guard_1.1.0_amd64.deb (≈50-100 MB)
├─ Binary: /opt/a3guard/bin/A3Guard
├─ Config: /etc/a3guard/a3guard.conf
├─ Service: /etc/systemd/system/a3guard.service
└─ PolicyKit: /usr/share/polkit-1/actions/com.a3guard.policy
Release Info Available:
- Tag name:
v1.1.0 - Release date
- DEB download URL
- Release notes
- Build information
The UpdateChecker will query GitHub API:
GET https://api.github.com/repos/iamthemag/A3Guard/releases/latest
{
"url": "https://api.github.com/repos/iamthemag/A3Guard/releases/1234567",
"tag_name": "v1.1.0",
"name": "A3Guard v1.1.0",
"draft": false,
"prerelease": false,
"created_at": "2024-10-22T10:00:00Z",
"published_at": "2024-10-22T10:15:00Z",
"body": "# A3Guard v1.1.0 Release\n\nBug fixes and improvements...",
"assets": [
{
"name": "a3guard_1.1.0_amd64.deb",
"size": 75000000,
"download_count": 42,
"created_at": "2024-10-22T10:15:00Z",
"browser_download_url": "https://github.com/iamthemag/A3Guard/releases/download/v1.1.0/a3guard_1.1.0_amd64.deb"
}
]
}QString latestVersion = "1.1.0"; // From tag_name (remove 'v')
QString downloadUrl = "<browser_download_url>"; // Direct DEB link
QString releaseNotes = "<body>"; // Release descriptionA3Guard Application (Current Version: 1.0.0)
│
├─ MainWindow
│ ├─ Help Menu
│ │ └─ "Check for Updates" ← New menu item
│ │ └─ Triggers UpdateChecker::checkForUpdates()
│ │
│ └─ UpdateChecker (NEW COMPONENT)
│ ├─ Network Manager (QNetworkAccessManager)
│ ├─ Version Comparator
│ ├─ Download Manager
│ └─ Signals/Slots for UI feedback
│
└─ Configuration
└─ Common.h: A3GUARD_VERSION = "1.0.0"
User clicks Help → "Check for Updates"
UpdateChecker::checkForUpdates()
{
// 1. Rate limiting (max once per hour)
if (lastCheckTime < 1 hour ago) {
emit checkFailed("Already checked recently");
return;
}
// 2. Make HTTP request to GitHub API
QUrl url("https://api.github.com/repos/iamthemag/A3Guard/releases/latest");
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::UserAgentHeader, "A3Guard/1.0.0");
m_networkManager->get(request);
}
// 3. Response received (callback)
void UpdateChecker::onGitHubResponseReceived()
{
// Parse JSON response
QJsonDocument doc = QJsonDocument::fromJson(responseData);
QJsonObject obj = doc.object();
// Extract version and download URL
m_latestVersion = obj["tag_name"].toString().remove("v"); // "1.1.0"
// Find DEB asset
QJsonArray assets = obj["assets"].toArray();
for (const auto& asset : assets) {
if (asset["name"].toString().endsWith(".deb")) {
m_downloadUrl = asset["browser_download_url"].toString();
break;
}
}
// 4. Version comparison
if (isNewerVersion(getCurrentVersion(), m_latestVersion)) {
// "1.1.0" > "1.0.0" ✓
emit updateAvailable(m_latestVersion, m_downloadUrl);
} else {
emit noUpdateAvailable();
}
}bool UpdateChecker::isNewerVersion(
const QString ¤t, // "1.0.0"
const QString &latest) // "1.1.0"
{
// Parse semantic versions
QStringList currParts = current.split('.'); // ["1", "0", "0"]
QStringList latestParts = latest.split('.'); // ["1", "1", "0"]
// Compare major.minor.patch
for (int i = 0; i < 3; ++i) {
int currNum = currParts[i].toInt();
int latestNum = latestParts[i].toInt();
if (latestNum > currNum) return true; // 1 > 1? No, continue
// 1 > 0? Yes, return true ✓
if (latestNum < currNum) return false;
}
return false; // Same version
}
// Examples:
// isNewerVersion("1.0.0", "1.1.0") → true ✓
// isNewerVersion("1.0.0", "2.0.0") → true ✓
// isNewerVersion("1.0.0", "1.0.1") → true ✓
// isNewerVersion("1.1.0", "1.0.0") → false ✗
// isNewerVersion("1.0.0", "1.0.0") → false ✗┌─────────────────────────────────────────────────────┐
│ MainWindow │
│ │
│ Help Menu │
│ ├─ About │
│ ├─ Settings │
│ └─ Check for Updates ← User clicks [1]│
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Progress Dialog │
│ "Checking for updates from GitHub..." [2] │
│ [Cancel] │
└─────────────────────────────────────────────────────┘
↓
(Network request sent)
(Waiting for response...)
↓
┌─────────────────────────────────────────────────────┐
│ Update Available Dialog [3] │
│ │
│ ✓ New version available! │
│ │
│ Current version: 1.0.0 │
│ Latest version: 1.1.0 │
│ │
│ Release Notes: │
│ ┌─────────────────────────────────────────────┐ │
│ │ • Bug fixes and improvements │ │
│ │ • Performance optimization │ │
│ │ • Enhanced monitoring features │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [Download & Install] [Later] │
└─────────────────────────────────────────────────────┘
↓
User clicks "Download"
↓
┌─────────────────────────────────────────────────────┐
│ Download Progress Dialog [4] │
│ │
│ Downloading a3guard_1.1.0_amd64.deb... │
│ ████████████████░░░░░░░░░░░░░░ 65% │
│ 48.5 MB / 75 MB (2.3 MB/s) │
│ Time remaining: ~12 seconds │
│ │
│ [Cancel] │
└─────────────────────────────────────────────────────┘
↓
Download completes
↓
┌─────────────────────────────────────────────────────┐
│ Installation Instructions Dialog [5] │
│ │
│ ✓ Download Complete! │
│ │
│ File: a3guard_1.1.0_amd64.deb │
│ Location: ~/.cache/a3guard/a3guard_1.1.0_amd64.deb │
│ Size: 75 MB │
│ │
│ To install, run: │
│ ┌─────────────────────────────────────────────┐ │
│ │ sudo dpkg -i ~/.cache/a3guard/...deb [📋]│ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [Open Installer] [Copy Command] [OK] │
└─────────────────────────────────────────────────────┘
Progress Dialog → GitHub API Response → Version Check
(1.0.0 = 1.0.0)
↓
┌────────────────────────────────┐
│ Up to Date Dialog │
│ │
│ ✓ Already up to date! │
│ │
│ You are running the latest │
│ version (v1.0.0) │
│ │
│ [OK] │
└────────────────────────────────┘
Network Request → Connection Failed
↓
┌──────────────────────────────┐
│ Check Failed Dialog │
│ │
│ ✗ Unable to check for │
│ updates │
│ │
│ Check your internet │
│ connection and try again. │
│ │
│ Error: Connection timeout │
│ │
│ [Retry] [Cancel] │
└──────────────────────────────┘
#ifndef UPDATECHECKER_H
#define UPDATECHECKER_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QString>
#include <QDateTime>
class UpdateChecker : public QObject {
Q_OBJECT
public:
explicit UpdateChecker(QObject *parent = nullptr);
void checkForUpdates();
QString getCurrentVersion() const;
QString getLatestVersion() const;
bool isUpdateAvailable() const;
signals:
void updateAvailable(QString latestVersion, QString downloadUrl, QString releaseNotes);
void noUpdateAvailable();
void checkFailed(QString errorMessage);
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void downloadFinished(QString filePath);
void checkStarted();
private slots:
void onGitHubResponseReceived(QNetworkReply *reply);
void onNetworkError(QNetworkReply::NetworkError error);
void onDownloadFinished(QNetworkReply *reply);
private:
void parseGitHubResponse(const QByteArray &data);
bool isNewerVersion(const QString ¤t, const QString &latest);
QNetworkAccessManager *m_networkManager;
QNetworkReply *m_currentReply;
QString m_latestVersion;
QString m_downloadUrl;
QString m_releaseNotes;
QDateTime m_lastCheckTime;
};
#endifAdd Qt5::Network dependency:
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Network) # Added Network
# In add_executable section:
target_link_libraries(${PROJECT_NAME}
...existing...
Qt5::Network # NEW
)
# Add UpdateChecker to sources:
set(SOURCES
...existing...
src/UpdateChecker.cpp # NEW
)
set(HEADERS
...existing...
include/UpdateChecker.h # NEW
)include/MainWindow.h:
private:
UpdateChecker *m_updateChecker; // NEW
void checkForUpdates(); // NEW
void onUpdateAvailable(QString version, QString url, QString notes); // NEW
void onUpdateCheckFailed(QString error); // NEW
void onUpdateCheckStarted(); // NEWsrc/MainWindow.cpp setupMenuBar():
QMenu *helpMenu = menuBar()->addMenu(tr("Help"));
// NEW: Add Check for Updates
QAction *checkUpdatesAction = helpMenu->addAction(tr("Check for Updates..."));
connect(checkUpdatesAction, &QAction::triggered,
this, &MainWindow::checkForUpdates);
helpMenu->addSeparator();
QAction *aboutAction = helpMenu->addAction(tr("About"));
// existing connections...src/MainWindow.cpp constructor:
m_updateChecker = new UpdateChecker(this);
connect(m_updateChecker, &UpdateChecker::checkStarted,
this, &MainWindow::onUpdateCheckStarted);
connect(m_updateChecker, &UpdateChecker::updateAvailable,
this, &MainWindow::onUpdateAvailable);
connect(m_updateChecker, &UpdateChecker::noUpdateAvailable,
this, [this]() {
QMessageBox::information(this, tr("Check for Updates"),
tr("You are running the latest version (v%1)").arg(m_updateChecker->getCurrentVersion()));
});
connect(m_updateChecker, &UpdateChecker::checkFailed,
this, &MainWindow::onUpdateCheckFailed);┌─────────────────────────────────────┐
│ Your Local Machine │
│ A3Guard v1.0.0 │
│ ┌──────────────────────────────┐ │
│ │ Help Menu │ │
│ │ "Check for Updates" │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
↓ (HTTPS)
┌─────────────────────────────────────┐
│ GitHub API │
│ /repos/iamthemag/A3Guard/... │
│ /releases/latest │
└─────────────────────────────────────┘
↓ (JSON Response)
Tag: v1.1.0
Asset: a3guard_1.1.0_amd64.deb
URL: github.com/.../releases/download/...
↓
┌─────────────────────────────────────┐
│ Local Machine │
│ Version Comparison: │
│ 1.0.0 < 1.1.0 → Update Available │
└─────────────────────────────────────┘
↓ (User confirms)
Download URL invoked
↓
┌─────────────────────────────────────┐
│ GitHub Release Server │
│ /releases/download/v1.1.0/ │
│ a3guard_1.1.0_amd64.deb │
└─────────────────────────────────────┘
↓ (Binary stream)
┌─────────────────────────────────────┐
│ Local Machine │
│ ~/.cache/a3guard/ │
│ a3guard_1.1.0_amd64.deb (75 MB) │
└─────────────────────────────────────┘
↓ (User installs manually)
sudo dpkg -i a3guard_*.deb
↓
Installation complete
↓
A3Guard v1.1.0 installed
✅ HTTPS Only
- All GitHub API requests use HTTPS
- SSL certificate verification enforced
- No plaintext data transmission
✅ Authenticated Requests
- User-Agent header identifies requests
- Rate limiting prevents abuse (1 check/hour)
- GitHub API public (no token needed)
✅ File Integrity
- DEB downloaded from official GitHub releases only
- Debian package manager validates contents
- Future: GPG signature verification
✅ User Consent
- Only checks on explicit user action
- No background auto-checking (default)
- User confirms before downloading
- User controls installation
// Network errors caught:
- Connection timeout
- DNS resolution failure
- SSL certificate errors
- HTTP errors (404, 500, etc.)
- JSON parse errors
- Invalid version format
// User-friendly messages shown:
- "Unable to check for updates. Check internet connection."
- "Failed to retrieve update information."
- "Unexpected error. Try again later."- Caching: Store last check time (prevents API spam)
- Retry Logic: User can click "Retry" on failure
- Graceful Degradation: App continues even if update check fails
- Timeout Protection: Network request times out after 30 seconds
# 1. Update version
./scripts/update-version.sh 1.0.0 1.1.0
# 2. Review changes
git diff
# 3. Commit
git add .
git commit -m "Bump version: 1.0.0 → 1.1.0"
# 4. Tag
git tag -a v1.1.0 -m "Release v1.1.0"
# 5. Push (TRIGGERS workflow)
git push origin main
git push origin v1.1.0[validate-tag] → Extract version (1.1.0) ✓
[build-release] → Build DEB, create release ✓
[security-scan] → Run Trivy ✓
[notify-success] → Print success ✓
GitHub Release: https://github.com/iamthemag/A3Guard/releases/tag/v1.1.0
DEB Available: a3guard_1.1.0_amd64.deb
1. User clicks: Help → "Check for Updates"
2. Progress dialog shows
3. UpdateChecker queries GitHub API
4. Gets: v1.1.0 available
5. Version comparison: 1.1.0 > 1.0.0
6. Dialog: "Update Available" appears
7. User clicks: "Download"
8. DEB downloads: ~/.cache/a3guard/a3guard_1.1.0_amd64.deb
9. Instructions shown
10. User installs: sudo dpkg -i ~/.cache/a3guard/a3guard_1.1.0_amd64.deb
11. A3Guard updated to v1.1.0
# Build with current version
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
# Run app
sudo ./A3Guard # or pkexec ./A3Guard
# Click: Help → Check for Updates
# Should query GitHub API for releases/latest
# Show current version vs latestTo test without real GitHub:
// In UpdateChecker_test.cpp
void TestUpdateChecker::testVersionComparison() {
UpdateChecker checker;
ASSERT_TRUE(checker.isNewerVersion("1.0.0", "1.1.0"));
ASSERT_TRUE(checker.isNewerVersion("1.0.0", "2.0.0"));
ASSERT_TRUE(checker.isNewerVersion("1.0.5", "1.1.0"));
ASSERT_FALSE(checker.isNewerVersion("1.1.0", "1.0.0"));
ASSERT_FALSE(checker.isNewerVersion("1.0.0", "1.0.0"));
}
void TestUpdateChecker::testJSONParsing() {
// Mock GitHub response
QString mockResponse = R"({
"tag_name": "v1.1.0",
"assets": [{
"name": "a3guard_1.1.0_amd64.deb",
"browser_download_url": "https://..."
}]
})";
checker.parseGitHubResponse(mockResponse.toUtf8());
ASSERT_EQ(checker.getLatestVersion(), "1.1.0");
}Phase 1: ✅ Basic update checking (Current Design)
├─ Check GitHub releases API
├─ Version comparison
├─ Download DEB
└─ Installation instructions
Phase 2: 🔄 Auto-checking (Optional)
├─ Background update checks on startup
├─ Configurable check intervals
├─ Notification system
└─ Tray notifications
Phase 3: 🔄 Advanced features
├─ In-app DEB installation (pkexec)
├─ Changelog display
├─ Update history
├─ Rollback support
└─ Beta version opt-in
Phase 4: 🔄 Security enhancements
├─ GPG signature verification
├─ Checksum validation
├─ Binary fingerprint checking
└─ Signed releases
The update system works through:
- Release: Tag pushed → GitHub Actions builds DEB → Release created
- Discovery: User clicks "Check for Updates" → API queries GitHub
- Comparison: Local version (1.0.0) vs GitHub version (1.1.0)
- Download: If newer, user downloads DEB from GitHub
- Installation: User manually installs via dpkg
- Complete: App updated to latest version
This approach is:
- ✅ Simple and reliable
- ✅ Secure (HTTPS only)
- ✅ User-controlled (explicit consent)
- ✅ Professional (clean UI)
- ✅ Maintainable (minimal code)
- ✅ Scalable (future enhancements possible)