Compare commits
No commits in common. "main" and "v0.8.4" have entirely different histories.
8 changed files with 20 additions and 184 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Application
|
||||
APP_NAME=WebDrop Bridge
|
||||
APP_VERSION=0.8.6
|
||||
APP_VERSION=0.8.4
|
||||
|
||||
# Web App
|
||||
WEBAPP_URL=file:///./webapp/index.html
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
{"timestamp": "2026-03-12T10:57:42.150570", "release": {"tag_name": "v0.8.4", "name": "WebDropBridge v0.8.4", "version": "0.8.4", "body": "Shared branded release for WebDrop Bridge v0.8.4", "assets": [{"id": 49, "name": "AgravityBridge-0.8.4-win-x64.msi", "size": 214445231, "download_count": 2, "created_at": "2026-03-12T08:25:03Z", "uuid": "7ffcd98a-99a9-4100-8e71-3ebe63534b8f", "browser_download_url": "https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.4/AgravityBridge-0.8.4-win-x64.msi", "type": "attachment"}, {"id": 50, "name": "AgravityBridge-0.8.4-win-x64.msi.sha256", "size": 64, "download_count": 2, "created_at": "2026-03-12T08:25:03Z", "uuid": "ddd00072-a5bc-422f-93c0-7cc3bc3408d3", "browser_download_url": "https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.4/AgravityBridge-0.8.4-win-x64.msi.sha256", "type": "attachment"}, {"id": 47, "name": "WebDropBridge-0.8.4-win-x64.msi", "size": 214445229, "download_count": 0, "created_at": "2026-03-12T08:24:20Z", "uuid": "5a20eef9-b77d-4e04-be06-d85c3ebd3f6e", "browser_download_url": "https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.4/WebDropBridge-0.8.4-win-x64.msi", "type": "attachment"}, {"id": 48, "name": "WebDropBridge-0.8.4-win-x64.msi.sha256", "size": 64, "download_count": 0, "created_at": "2026-03-12T08:24:21Z", "uuid": "9972b3bb-7c4b-4b26-951a-5a8dfc1a1f27", "browser_download_url": "https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.4/WebDropBridge-0.8.4-win-x64.msi.sha256", "type": "attachment"}, {"id": 51, "name": "release-manifest.json", "size": 931, "download_count": 0, "created_at": "2026-03-12T08:25:03Z", "uuid": "e3c13ccd-cbc6-4eb1-988e-7f465a75eca6", "browser_download_url": "https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.8.4/release-manifest.json", "type": "attachment"}], "published_at": "2026-03-12T08:23:40Z"}}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:ui="http://schemas.microsoft.com/wix/2010/ui"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
<Product Id="*" Name="{product_name_with_version}" Language="1033" Version="{version}"
|
||||
<Product Id="*" Name="{product_name}" Language="1033" Version="{version}"
|
||||
Manufacturer="{manufacturer}"
|
||||
UpgradeCode="{upgrade_code}">
|
||||
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ class WindowsBuilder:
|
|||
print(f" ✓ Marked components as 64-bit")
|
||||
|
||||
# Compile both WiX files
|
||||
wix_obj = self.build_dir / "WebDropBridge.generated.wixobj"
|
||||
wix_obj = self.build_dir / "WebDropBridge.wixobj"
|
||||
wix_files_obj = self.build_dir / "WebDropBridge_Files.wixobj"
|
||||
msi_output = self.dist_dir / self.brand.windows_installer_name(self.version)
|
||||
|
||||
|
|
@ -360,7 +360,7 @@ class WindowsBuilder:
|
|||
"-ext",
|
||||
"WixUtilExtension",
|
||||
f"-dDistDir={self.dist_dir}",
|
||||
f"-dSourceDir={self.dist_dir / self.brand.exe_name}", # Set SourceDir for Heat-generated files
|
||||
f"-dSourceDir={self.dist_dir}\{self.brand.exe_name}", # Set SourceDir for Heat-generated files
|
||||
f"-dResourcesDir={self.project_root}\\resources", # Set ResourcesDir for branding assets
|
||||
"-o",
|
||||
str(self.build_dir) + "\\",
|
||||
|
|
@ -455,7 +455,6 @@ class WindowsBuilder:
|
|||
wix_template = self.wix_template.read_text(encoding="utf-8")
|
||||
wix_content = wix_template.format(
|
||||
product_name=self.brand.display_name,
|
||||
product_name_with_version=f"{self.brand.display_name} v{self.version}",
|
||||
version=self.version,
|
||||
manufacturer=self.brand.manufacturer,
|
||||
upgrade_code=self.brand.msi_upgrade_code,
|
||||
|
|
|
|||
|
|
@ -80,8 +80,7 @@ if ($artifactPaths.Count -eq 0) {
|
|||
exit 1
|
||||
}
|
||||
|
||||
$localManifestJson = $localData.manifest | ConvertTo-Json -Depth 10
|
||||
[System.IO.File]::WriteAllText($localManifestPath, $localManifestJson, (New-Object System.Text.UTF8Encoding($false)))
|
||||
$localData.manifest | ConvertTo-Json -Depth 10 | Set-Content -Path $localManifestPath -Encoding utf8
|
||||
|
||||
if ($DryRun) {
|
||||
Copy-Item $localManifestPath $manifestOutput -Force
|
||||
|
|
@ -156,132 +155,27 @@ else {
|
|||
Copy-Item $localManifestPath $manifestOutput -Force
|
||||
}
|
||||
|
||||
# Ensure uploaded manifest is UTF-8 without BOM (for strict JSON parsers)
|
||||
if (Test-Path $manifestOutput) {
|
||||
$manifestText = Get-Content -Raw -Path $manifestOutput
|
||||
[System.IO.File]::WriteAllText($manifestOutput, $manifestText, (New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
$artifactPaths.Add($manifestOutput)
|
||||
$assetMap = Get-AssetMap -Assets $releaseInfo.assets
|
||||
|
||||
$artifactsToUpload = New-Object System.Collections.Generic.List[string]
|
||||
$curlAuth = "$ForgejoUser`:$ForgejoPW"
|
||||
$uploadUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets"
|
||||
|
||||
foreach ($artifact in $artifactPaths) {
|
||||
$assetName = [System.IO.Path]::GetFileName($artifact)
|
||||
$extension = [System.IO.Path]::GetExtension($artifact).ToLowerInvariant()
|
||||
|
||||
if ($extension -eq ".msi" -and $assetMap.ContainsKey($assetName)) {
|
||||
$localSize = (Get-Item $artifact).Length
|
||||
$remoteSize = [int64]$assetMap[$assetName].size
|
||||
if ($localSize -eq $remoteSize) {
|
||||
Write-Host "[OK] Skipping already uploaded MSI $assetName ($([math]::Round($localSize / 1MB, 2)) MB)" -ForegroundColor Cyan
|
||||
continue
|
||||
}
|
||||
if ($assetMap.ContainsKey($assetName)) {
|
||||
$existingAsset = $assetMap[$assetName]
|
||||
Invoke-RestMethod -Uri "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets/$($existingAsset.id)" -Method DELETE -Headers $headers -TimeoutSec 30 | Out-Null
|
||||
Write-Host "[OK] Replaced existing asset $assetName" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$artifactsToUpload.Add($artifact)
|
||||
$response = curl.exe -s -X POST -u $curlAuth -F "attachment=@$artifact" $uploadUrl
|
||||
if ($response -like "*error*" -or $response -like "*404*") {
|
||||
Write-Host "WARNING: Could not upload $artifact : $response" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($artifactsToUpload.Count -eq 0) {
|
||||
Write-Host "[OK] All release assets already uploaded." -ForegroundColor Green
|
||||
Write-Host "View at: $ForgejoUrl/$Repo/releases/tag/v$Version" -ForegroundColor Cyan
|
||||
exit 0
|
||||
else {
|
||||
Write-Host "[OK] Uploaded $assetName" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Use Python requests library for more reliable large file uploads
|
||||
$pythonUploadScript = @"
|
||||
import sys
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
upload_url = sys.argv[1]
|
||||
artifacts = sys.argv[2:]
|
||||
username = '$ForgejoUser'
|
||||
password = '$ForgejoPW'
|
||||
delete_url_template = '${ForgejoUrl}/api/v1/repos/${Repo}/releases/$releaseId/assets/{}'
|
||||
release_info_url = '${ForgejoUrl}/api/v1/repos/${Repo}/releases/$releaseId'
|
||||
|
||||
session = requests.Session()
|
||||
session.auth = HTTPBasicAuth(username, password)
|
||||
session.headers.update({'Connection': 'close'})
|
||||
|
||||
def upload_with_retry(artifact_path, max_retries=3):
|
||||
asset_name = Path(artifact_path).name
|
||||
|
||||
# Check if asset already exists and delete it
|
||||
try:
|
||||
release_response = session.get(release_info_url, timeout=30)
|
||||
release_response.raise_for_status()
|
||||
for asset in release_response.json().get('assets', []):
|
||||
if asset['name'] == asset_name:
|
||||
delete_resp = session.delete(delete_url_template.format(asset['id']), timeout=30)
|
||||
delete_resp.raise_for_status()
|
||||
print(f'[OK] Replaced existing asset {asset_name}', file=sys.stderr)
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'Warning checking existing assets: {e}', file=sys.stderr)
|
||||
|
||||
# Upload file with streaming and retries
|
||||
retryable_status_codes = {429, 502, 503, 504}
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
if attempt > 0:
|
||||
print(f' Retry {attempt} of {max_retries}...', file=sys.stderr)
|
||||
time.sleep(min(10, 2 * attempt))
|
||||
|
||||
with open(artifact_path, 'rb') as f:
|
||||
files = {'attachment': (asset_name, f)}
|
||||
response = session.post(
|
||||
upload_url,
|
||||
files=files,
|
||||
timeout=900, # 15 minute timeout
|
||||
stream=False
|
||||
)
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
print(f'[OK] Uploaded {asset_name}')
|
||||
return True
|
||||
|
||||
if response.status_code in retryable_status_codes:
|
||||
if attempt >= max_retries - 1:
|
||||
print(f'ERROR uploading {asset_name} (HTTP {response.status_code} after {max_retries} retries)')
|
||||
print(response.text)
|
||||
sys.exit(1)
|
||||
continue
|
||||
|
||||
print(f'ERROR uploading {asset_name} (HTTP {response.status_code})')
|
||||
print(response.text)
|
||||
sys.exit(1)
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
||||
if attempt >= max_retries - 1:
|
||||
print(f'ERROR uploading {asset_name}: Connection failed after {max_retries} retries')
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
time.sleep(min(10, 2 * (attempt + 1)))
|
||||
except Exception as e:
|
||||
print(f'ERROR uploading {asset_name}: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
for artifact_path in artifacts:
|
||||
upload_with_retry(artifact_path)
|
||||
|
||||
print(f'[OK] All files uploaded successfully')
|
||||
"@
|
||||
|
||||
$uploadScriptPath = ([System.IO.Path]::GetTempFileName() -replace 'tmp$', 'py')
|
||||
Set-Content -Path $uploadScriptPath -Value $pythonUploadScript -Encoding UTF8
|
||||
|
||||
try {
|
||||
$uploadUrl = "$ForgejoUrl/api/v1/repos/$Repo/releases/$releaseId/assets"
|
||||
& $pythonExe $uploadScriptPath $uploadUrl @artifactsToUpload
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item $uploadScriptPath -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "`n[OK] Release complete!" -ForegroundColor Green
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""WebDrop Bridge - Qt-based desktop application for intelligent drag-and-drop file handling."""
|
||||
|
||||
__version__ = "0.8.6"
|
||||
__version__ = "0.8.4"
|
||||
__author__ = "WebDrop Team"
|
||||
__license__ = "MIT"
|
||||
|
||||
|
|
|
|||
|
|
@ -93,9 +93,7 @@ class UpdateManager:
|
|||
"""Download and parse a JSON asset from a release."""
|
||||
try:
|
||||
with urlopen(url, timeout=10) as response:
|
||||
# Some release pipelines may upload JSON files with UTF-8 BOM.
|
||||
# Use utf-8-sig to transparently handle both BOM and non-BOM files.
|
||||
return json.loads(response.read().decode("utf-8-sig"))
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except (URLError, json.JSONDecodeError) as e:
|
||||
logger.error(f"Failed to download JSON asset: {e}")
|
||||
return None
|
||||
|
|
@ -150,28 +148,8 @@ class UpdateManager:
|
|||
brand_prefix = f"{self.brand_id}-*"
|
||||
|
||||
installer_asset = None
|
||||
|
||||
# Prefer brand-specific naming when possible.
|
||||
if self.brand_id == "webdrop_bridge":
|
||||
preferred_patterns = ["webdropbridge-*.msi", "webdropbridge*.msi"]
|
||||
else:
|
||||
preferred_patterns = [f"{self.brand_id.lower()}-*.msi", f"{self.brand_id.lower()}*.msi"]
|
||||
|
||||
# 1) Try strict brand-pattern match first
|
||||
for asset in release.assets:
|
||||
asset_name = asset.get("name", "")
|
||||
asset_name_lower = asset_name.lower()
|
||||
if not asset_name_lower.endswith(extension):
|
||||
continue
|
||||
if any(fnmatch.fnmatch(asset_name_lower, pattern) for pattern in preferred_patterns):
|
||||
installer_asset = asset
|
||||
break
|
||||
|
||||
# 2) Fallback: preserve previous behavior (first installer for platform)
|
||||
for asset in release.assets:
|
||||
if installer_asset:
|
||||
break
|
||||
asset_name = asset.get("name", "")
|
||||
if not asset_name.endswith(extension):
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -315,40 +315,6 @@ class TestDownloading:
|
|||
assert result.name == "AgravityBridge-0.0.2-win-x64.msi"
|
||||
mock_download.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_update_falls_back_to_brand_prefix_without_manifest(
|
||||
self, agravity_update_manager, tmp_path
|
||||
):
|
||||
"""Test branded download selection still works when the manifest is unavailable."""
|
||||
release = Release(
|
||||
tag_name="v0.0.2",
|
||||
name="WebDropBridge v0.0.2",
|
||||
version="0.0.2",
|
||||
body="Release notes",
|
||||
assets=[
|
||||
{
|
||||
"name": "WebDropBridge-0.0.2-win-x64.msi",
|
||||
"browser_download_url": "https://example.com/WebDropBridge-0.0.2-win-x64.msi",
|
||||
},
|
||||
{
|
||||
"name": "AgravityBridge-0.0.2-win-x64.msi",
|
||||
"browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi",
|
||||
},
|
||||
{
|
||||
"name": "AgravityBridge-0.0.2-win-x64.msi.sha256",
|
||||
"browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi.sha256",
|
||||
},
|
||||
],
|
||||
published_at="2026-01-29T10:00:00Z",
|
||||
)
|
||||
|
||||
with patch.object(UpdateManager, "_download_file", return_value=True) as mock_download:
|
||||
result = await agravity_update_manager.download_update(release, tmp_path)
|
||||
|
||||
assert result is not None
|
||||
assert result.name == "AgravityBridge-0.0.2-win-x64.msi"
|
||||
mock_download.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_checksum_uses_release_manifest(self, agravity_update_manager, tmp_path):
|
||||
"""Test branded checksum selection from a shared release manifest."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue