Skip to content

Build and upload to latest release #18

Build and upload to latest release

Build and upload to latest release #18

name: Build and upload to latest release
on:
workflow_dispatch:
permissions:
contents: write
concurrency:
group: upload-to-latest-release-${{ github.repository }}
cancel-in-progress: false
jobs:
test:
uses: ./.github/workflows/tests.yml
prepare:
needs: test
runs-on: ubuntu-latest
outputs:
name: ${{ steps.metadata.outputs.name }}
version: ${{ steps.metadata.outputs.version }}
release_tag: ${{ steps.metadata.outputs.release_tag }}
revision: ${{ steps.metadata.outputs.revision }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Prepare hotfix metadata
id: metadata
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$info = Get-Content version.json -Raw | ConvertFrom-Json
if (-not $info.name -or -not $info.version) {
Write-Error 'version.json must contain name and version.'
exit 1
}
$tag = gh release view --repo $env:GITHUB_REPOSITORY --json tagName --jq '.tagName'
if (-not $tag) {
Write-Error 'Latest release was not found.'
exit 1
}
if ($tag.TrimStart('v') -ne [string]$info.version) {
Write-Error "version.json version '$($info.version)' does not match latest release '$tag'."
exit 1
}
$revision = (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmmss')
Add-Content -Path $env:GITHUB_OUTPUT -Value "name=$($info.name)"
Add-Content -Path $env:GITHUB_OUTPUT -Value "version=$($info.version)"
Add-Content -Path $env:GITHUB_OUTPUT -Value "release_tag=$tag"
Add-Content -Path $env:GITHUB_OUTPUT -Value "revision=$revision"
build:
needs: prepare
env:
name: ${{ needs.prepare.outputs.name }}
version: ${{ needs.prepare.outputs.version }}
revision: ${{ needs.prepare.outputs.revision }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
platform: Windows
- os: macos-latest
platform: macOS
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
update-environment: true
cache: 'pipenv'
- name: Install dependencies with pipenv
shell: bash
run: |
python -m pip install --upgrade pip pipenv
pipenv --python 3.14
pipenv install --dev
- name: Embed hotfix metadata
shell: bash
run: |
python - <<'PY'
import json
import os
from datetime import datetime, timezone
with open("version.json", encoding="utf-8") as file:
info = json.load(file)
info["build_revision"] = int(os.environ["revision"])
info["build_time"] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
info["source_sha"] = os.environ["GITHUB_SHA"]
with open("version.json", "w", encoding="utf-8") as file:
json.dump(info, file, ensure_ascii=False, indent=2)
file.write("\n")
PY
- name: Build the primary GUI application
run: pipenv run ui_build
- name: Package Windows application
if: runner.os == 'Windows'
shell: pwsh
run: |
$bundle = Join-Path $PWD "dist\$env:name-GUI-v$env:version"
if (-not (Test-Path $bundle)) {
Write-Error "Windows application directory not found: $bundle"
exit 1
}
New-Item -ItemType Directory -Path artifacts -Force | Out-Null
$asset = "$env:name-GUI-Windows-$env:RUNNER_ARCH-v$env:version-r$env:revision.zip"
Compress-Archive -Path $bundle -DestinationPath (Join-Path $PWD "artifacts\$asset")
- name: Package macOS application
if: runner.os == 'macOS'
shell: bash
run: |
app_path="dist/${name}-GUI-v${version}.app"
if [[ ! -d "$app_path" ]]; then
echo "macOS application bundle not found: $app_path" >&2
exit 1
fi
mkdir -p artifacts
asset="${name}-GUI-macOS-${RUNNER_ARCH}-v${version}-r${revision}.zip"
ditto -c -k --sequesterRsrc --keepParent "$app_path" "artifacts/$asset"
- name: Upload GUI artifact
uses: actions/upload-artifact@v4
with:
name: gui-${{ matrix.platform }}-${{ runner.arch }}
path: artifacts/*.zip
if-no-files-found: error
upload:
needs: [prepare, build]
runs-on: ubuntu-latest
env:
name: ${{ needs.prepare.outputs.name }}
version: ${{ needs.prepare.outputs.version }}
release_tag: ${{ needs.prepare.outputs.release_tag }}
revision: ${{ needs.prepare.outputs.revision }}
steps:
- name: Download GUI artifacts
uses: actions/download-artifact@v4
with:
pattern: gui-*
path: release-assets
merge-multiple: true
- name: Upload assets to latest release
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$ErrorActionPreference = 'Stop'
$tag = $env:release_tag
$assets = @(Get-ChildItem release-assets -Filter '*.zip' | Select-Object -ExpandProperty FullName)
if ($assets.Count -ne 2) {
Write-Error "Expected two desktop assets, found $($assets.Count)."
exit 1
}
$assetNames = @($assets | ForEach-Object { Split-Path $_ -Leaf })
$windowsAsset = @($assetNames | Where-Object { $_ -match '-Windows-X64-' })
$macosAsset = @($assetNames | Where-Object { $_ -match '-macOS-ARM64-' })
if ($windowsAsset.Count -ne 1 -or $macosAsset.Count -ne 1) {
Write-Error 'Expected one Windows X64 asset and one macOS ARM64 asset.'
exit 1
}
$stateDirectory = Join-Path $PWD 'release-state'
New-Item -ItemType Directory -Path $stateDirectory -Force | Out-Null
$manifestName = 'UPDATE-MANIFEST.json'
$manifestPath = Join-Path $stateDirectory $manifestName
$releaseBefore = gh api "repos/$env:GITHUB_REPOSITORY/releases/tags/$tag" | ConvertFrom-Json
$existingManifest = @($releaseBefore.assets | Where-Object { $_.name -eq $manifestName })
if ($existingManifest.Count -gt 0) {
gh release download $tag --pattern $manifestName --dir $stateDirectory --repo $env:GITHUB_REPOSITORY
if ($LASTEXITCODE -ne 0) {
Write-Error 'Unable to download the existing update manifest.'
exit $LASTEXITCODE
}
}
foreach ($asset in $assets) {
gh release upload $tag $asset --repo $env:GITHUB_REPOSITORY
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
$releaseAfter = gh api "repos/$env:GITHUB_REPOSITORY/releases/tags/$tag" | ConvertFrom-Json
$remoteNames = @($releaseAfter.assets | Select-Object -ExpandProperty name)
foreach ($assetName in $assetNames) {
if ($assetName -notin $remoteNames) {
Write-Error "Uploaded asset was not found in the release: $assetName"
exit 1
}
}
$builds = @()
if (Test-Path $manifestPath) {
$existing = Get-Content $manifestPath -Raw | ConvertFrom-Json
if ($existing.schema_version -ne 1) {
Write-Error 'The existing update manifest has an unsupported schema.'
exit 1
}
if ([string]$existing.version -ne $env:version) {
Write-Error 'The existing update manifest version does not match version.json.'
exit 1
}
$builds = @($existing.builds | Where-Object { [string]$_.revision -ne $env:revision })
}
$builds += [ordered]@{
revision = [long]$env:revision
status = 'active'
source_sha = $env:GITHUB_SHA
workflow_run_id = $env:GITHUB_RUN_ID
created_at = (Get-Date).ToUniversalTime().ToString('o')
assets = [ordered]@{
'windows-x64' = $windowsAsset[0]
'macos-arm64' = $macosAsset[0]
}
}
[ordered]@{
schema_version = 1
version = $env:version
builds = @($builds)
} | ConvertTo-Json -Depth 8 | Set-Content -Path $manifestPath -Encoding utf8NoBOM
$checksums = Join-Path $stateDirectory 'SHA256SUMS.txt'
$checksumLines = @($releaseAfter.assets | Where-Object {
$_.name -like '*.zip'
} | Sort-Object name | ForEach-Object {
$digest = [string]$_.digest
if (-not $digest.StartsWith('sha256:')) {
Write-Error "Release asset is missing a SHA-256 digest: $($_.name)"
exit 1
}
"$($digest.Substring(7).ToLower()) $($_.name)"
})
if ($checksumLines.Count -eq 0) {
Write-Error 'No ZIP assets were found when generating checksums.'
exit 1
}
$checksumLines | Set-Content -Path $checksums -Encoding utf8NoBOM
gh release upload $tag $checksums --clobber --repo $env:GITHUB_REPOSITORY
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
gh release upload $tag $manifestPath --clobber --repo $env:GITHUB_REPOSITORY
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
- name: Confirm upload
shell: pwsh
run: Write-Host "Uploaded revision $env:revision to release $env:release_tag."