fix(release): retry uploads with backoff + drop Gitea workflows
Gitea/nginx intermittently returns 504 on large multipart uploads even when curl successfully streamed the body. Add up to 4 retries with exponential backoff (15s/45s/2m/5m). Before each retry, check whether the asset is actually present server-side at the expected size — Gitea sometimes accepts the body but times out the response, so the file is already there. Also drop .gitea/workflows/* — we use release.ps1 locally and Gitea Actions runners are not configured, so every push was leaving queued/ failed workflow runs in the Actions tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,65 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
quality:
|
|
||||||
name: Typecheck + Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Typecheck (main + preload + shared)
|
|
||||||
run: npm run typecheck:node
|
|
||||||
|
|
||||||
- name: Typecheck (renderer)
|
|
||||||
run: npm run typecheck:web
|
|
||||||
|
|
||||||
- name: Run unit tests
|
|
||||||
run: npm run test:run
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build (Windows)
|
|
||||||
runs-on: windows-latest
|
|
||||||
needs: quality
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build production bundle (no installer)
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Smoke-test unpacked build
|
|
||||||
run: npm run dist:dir
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload unpacked artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: exercise-reminder-unpacked
|
|
||||||
path: release/win-unpacked/
|
|
||||||
retention-days: 7
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*.*.*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Build installer + publish release
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Verify version matches tag
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$tag = "${{ gitea.ref_name }}"
|
|
||||||
$expected = $tag.TrimStart('v')
|
|
||||||
$actual = (Get-Content package.json | ConvertFrom-Json).version
|
|
||||||
if ($expected -ne $actual) {
|
|
||||||
Write-Error "Tag $tag does not match package.json version $actual"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
Write-Host "Version match: $actual"
|
|
||||||
|
|
||||||
- name: Typecheck
|
|
||||||
run: npm run typecheck
|
|
||||||
|
|
||||||
- name: Run unit tests
|
|
||||||
run: npm run test:run
|
|
||||||
|
|
||||||
- name: Build NSIS installer
|
|
||||||
run: npm run dist
|
|
||||||
env:
|
|
||||||
# electron-builder uses this when --publish flag is set; we publish
|
|
||||||
# to a Gitea release manually below to avoid hard-coupling.
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Generate release notes from commits
|
|
||||||
id: notes
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$tag = "${{ gitea.ref_name }}"
|
|
||||||
$prev = git describe --tags --abbrev=0 "$tag^" 2>$null
|
|
||||||
if ($prev) {
|
|
||||||
$log = git log --pretty=format:"- %s" "$prev..$tag"
|
|
||||||
} else {
|
|
||||||
$log = git log --pretty=format:"- %s" "$tag"
|
|
||||||
}
|
|
||||||
$notes = "### Изменения`n`n$log`n`n---`n`nУстановщик ниже — запустить и следовать мастеру. Если приложение уже стояло — обновится поверх с сохранением настроек."
|
|
||||||
$encoded = $notes -replace "`r?`n", "%0A"
|
|
||||||
"notes=$encoded" | Out-File -FilePath $env:GITEA_OUTPUT -Append
|
|
||||||
|
|
||||||
- name: Create Gitea release with artifacts
|
|
||||||
uses: akkuman/gitea-release-action@v1
|
|
||||||
with:
|
|
||||||
server_url: ${{ gitea.server_url }}
|
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
name: 'Exercise Reminder ${{ gitea.ref_name }}'
|
|
||||||
body: ${{ steps.notes.outputs.notes }}
|
|
||||||
files: |
|
|
||||||
release/Exercise-Reminder-Setup-*.exe
|
|
||||||
release/Exercise-Reminder-Setup-*.exe.blockmap
|
|
||||||
release/latest.yml
|
|
||||||
@@ -157,21 +157,66 @@ if ($curlCmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$maxRetries = 4
|
||||||
|
$backoffs = @(15, 45, 120, 300) # seconds between attempts
|
||||||
|
|
||||||
foreach ($asset in @($installer, $blockmap, $manifest)) {
|
foreach ($asset in @($installer, $blockmap, $manifest)) {
|
||||||
$name = Split-Path $asset -Leaf
|
$name = Split-Path $asset -Leaf
|
||||||
$size = (Get-Item $asset).Length
|
$size = (Get-Item $asset).Length
|
||||||
Write-Host ("Uploading {0} ({1:N1} MB)..." -f $name, ($size / 1MB)) -ForegroundColor Cyan
|
|
||||||
$uri = "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($name))"
|
$uri = "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($name))"
|
||||||
# -f: fail on HTTP errors; -s -S: silent but show errors; --data-binary @file
|
|
||||||
|
$attempt = 0
|
||||||
|
$uploaded = $false
|
||||||
|
while (-not $uploaded -and $attempt -le $maxRetries) {
|
||||||
|
if ($attempt -gt 0) {
|
||||||
|
$wait = $backoffs[[Math]::Min($attempt - 1, $backoffs.Length - 1)]
|
||||||
|
Write-Host (" Retrying in {0}s (attempt {1}/{2})..." -f $wait, ($attempt + 1), ($maxRetries + 1)) -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds $wait
|
||||||
|
|
||||||
|
# Re-check whether prior attempt actually succeeded server-side before
|
||||||
|
# 504-ing the client. If asset is already there, treat as success.
|
||||||
|
try {
|
||||||
|
$check = Invoke-RestMethod `
|
||||||
|
-Uri "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets" `
|
||||||
|
-Method Get -Headers $headers
|
||||||
|
$existing = $check | Where-Object { $_.name -eq $name }
|
||||||
|
if ($existing -and $existing.size -eq $size) {
|
||||||
|
Write-Host " Asset already present server-side ($($existing.size) bytes) — skipping retry." -ForegroundColor DarkGray
|
||||||
|
$uploaded = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
# If asset is present but with wrong size (half-uploaded), delete first.
|
||||||
|
if ($existing) {
|
||||||
|
Write-Host " Removing partial asset id=$($existing.id) ($($existing.size) bytes) before retry..." -ForegroundColor DarkGray
|
||||||
|
Invoke-RestMethod `
|
||||||
|
-Uri "$apiBase/repos/$repoOwner/$repoName/releases/$($release.id)/assets/$($existing.id)" `
|
||||||
|
-Method Delete -Headers $headers | Out-Null
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# If the list call itself fails, just proceed with the retry.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ("Uploading {0} ({1:N1} MB)..." -f $name, ($size / 1MB)) -ForegroundColor Cyan
|
||||||
& $curl `
|
& $curl `
|
||||||
--fail-with-body `
|
--fail-with-body `
|
||||||
--silent --show-error `
|
--silent --show-error `
|
||||||
|
--connect-timeout 30 `
|
||||||
|
--max-time 900 `
|
||||||
-H "Authorization: token $env:GITEA_TOKEN" `
|
-H "Authorization: token $env:GITEA_TOKEN" `
|
||||||
-H "Content-Type: application/octet-stream" `
|
-H "Content-Type: application/octet-stream" `
|
||||||
--data-binary "@$asset" `
|
--data-binary "@$asset" `
|
||||||
$uri
|
$uri
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
Write-Error "Upload failed for $name (curl exit $LASTEXITCODE)"
|
$uploaded = $true
|
||||||
|
} else {
|
||||||
|
Write-Host " curl exit $LASTEXITCODE — will retry." -ForegroundColor Yellow
|
||||||
|
$attempt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $uploaded) {
|
||||||
|
Write-Error "Upload failed for $name after $($maxRetries + 1) attempts."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user