From bbe8d1eef5291af0b42f3a6a04cea460eccaa9bf Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 17 Sep 2026 10:11:23 -0700 Subject: [PATCH] fix: retry release uploads and retain artifacts for seven days (#690) --- .../scripts/preserve-release-artifacts.mjs | 79 +++++++++++++++++++ .github/workflows/release-app.yml | 54 ++++++++++++- 2 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 .github/scripts/preserve-release-artifacts.mjs diff --git a/.github/scripts/preserve-release-artifacts.mjs b/.github/scripts/preserve-release-artifacts.mjs new file mode 100644 index 00000000..e64cfe91 --- /dev/null +++ b/.github/scripts/preserve-release-artifacts.mjs @@ -0,0 +1,79 @@ +// Retain only distributable bundle files, never the target tree or signing material. +import { createHash } from "node:crypto"; +import { + copyFileSync, + createReadStream, + existsSync, + mkdirSync, + readdirSync, + writeFileSync, +} from "node:fs"; +import { join, relative } from "node:path"; + +const env = process.env; +const bundle = join("target", env.RELEASE_TARGET || "", "release", "bundle"); +const destination = join(env.RUNNER_TEMP, "yaak-release-artifacts"); +const extensions = + /\.(dmg|deb|rpm|AppImage|exe|msi|app\.tar\.gz|AppImage\.tar\.gz|nsis\.zip|msi\.zip)(\.sig)?$/; +const files = []; + +function snapshot(directory) { + if (!existsSync(directory)) return; + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const source = join(directory, entry.name); + // Only descend through the bundler's immediate output folders. This excludes + // .app trees and the extracted CEF deb contents (which contain executables). + if (entry.isDirectory() && directory === bundle) { + snapshot(source); + } else if ( + entry.isFile() && + (extensions.test(entry.name) || /^yaak-cef_.*_linux_.*\.tar\.gz$/.test(entry.name)) + ) { + // Match tauri-action's default macOS updater asset naming exactly. + const arch = env.RELEASE_ARCH === "arm64" ? "aarch64" : "x64"; + const assetName = entry.name.replace(/\.app\.tar\.gz(\.sig)?$/, `_${arch}.app.tar.gz$1`); + mkdirSync(destination, { recursive: true }); + const retained = join(destination, assetName); + // The second snapshot must not overwrite the original user installer/signature. + if (!existsSync(retained)) copyFileSync(source, retained); + files.push({ source: relative(".", source), assetName }); + } + } +} + +snapshot(bundle); +if (existsSync(destination)) { + const assets = readdirSync(destination) + .filter((name) => name !== "provenance.json") + .sort(); + const checksums = []; + for (const assetName of assets) { + const hash = createHash("sha256"); + for await (const chunk of createReadStream(join(destination, assetName))) hash.update(chunk); + checksums.push({ assetName, sha256: hash.digest("hex") }); + } + writeFileSync( + join(destination, "provenance.json"), + JSON.stringify( + { + repository: env.GITHUB_REPOSITORY, + tag: env.GITHUB_REF_NAME, + sha: env.GITHUB_SHA, + runId: env.GITHUB_RUN_ID, + runAttempt: env.GITHUB_RUN_ATTEMPT, + workflowRef: env.GITHUB_WORKFLOW_REF, + workflowSha: env.GITHUB_WORKFLOW_SHA, + tauriAction: "tauri-apps/tauri-action@v0", + platform: env.RELEASE_PLATFORM, + runtime: env.RELEASE_RUNTIME, + arch: env.RELEASE_ARCH, + target: env.RELEASE_TARGET, + args: env.RELEASE_ARGS, + assets: checksums, + }, + null, + 2, + ) + "\n", + ); +} +console.log(`Found ${files.length} distributable bundle files in ${bundle}`); diff --git a/.github/workflows/release-app.yml b/.github/workflows/release-app.yml index 6c519594..384b0843 100644 --- a/.github/workflows/release-app.yml +++ b/.github/workflows/release-app.yml @@ -172,7 +172,8 @@ jobs: codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/protoc/yaakprotoc || true codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/node/yaaknode || true - - uses: tauri-apps/tauri-action@v0 + - name: Build and upload Tauri bundles + uses: tauri-apps/tauri-action@v0 env: YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }} @@ -194,6 +195,8 @@ jobs: AZURE_CLIENT_SECRET: ${{ matrix.os == 'windows' && secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ matrix.os == 'windows' && secrets.AZURE_TENANT_ID }} with: + # Also retries builds; upload retries alone cannot clean up starter assets. + retryAttempts: 3 tauriScript: "node ../../node_modules/@tauri-apps/cli/tauri.js" tagName: "v__VERSION__" releaseName: "Release __VERSION__" @@ -203,6 +206,17 @@ jobs: projectPath: ./crates-tauri/yaak-app-client args: "${{ matrix.args }}" + # Snapshot before the per-machine build overwrites the regular NSIS bundle/signature. + - name: Snapshot Tauri bundles + if: ${{ !cancelled() }} + run: node .github/scripts/preserve-release-artifacts.mjs + env: + RELEASE_TARGET: ${{ matrix.targets }} + RELEASE_PLATFORM: ${{ matrix.platform }} + RELEASE_RUNTIME: ${{ matrix.runtime }} + RELEASE_ARCH: ${{ matrix.yaak_arch }} + RELEASE_ARGS: ${{ matrix.args }} + - name: Build and upload CEF tarball from deb (Linux only) if: matrix.os == 'ubuntu' && matrix.runtime == 'cef' env: @@ -217,7 +231,13 @@ jobs: mkdir -p "$extract_dir" dpkg-deb -x "$deb" "$extract_dir" tar -C "$extract_dir" -czf "$tarball" . - gh release upload "${{ github.ref_name }}" "$tarball" --clobber + for attempt in 1 2 3 4; do + if gh release upload "$GITHUB_REF_NAME" "$tarball" --clobber; then + break + fi + if [ "$attempt" -eq 4 ]; then exit 1; fi + sleep 10 + done # Build a per-machine NSIS installer for enterprise deployment (PDQ, SCCM, Intune) - name: Build and upload machine-wide installer (Windows only) @@ -235,6 +255,7 @@ jobs: Get-ChildItem -Recurse -Path target -File -Filter "*.exe.sig" | Remove-Item -Force Push-Location crates-tauri/yaak-app-client npx tauri bundle ${{ matrix.args }} --bundles nsis --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}' + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Pop-Location $setup = Get-ChildItem -Recurse -Path target -Filter "*setup*.exe" | Select-Object -First 1 $setupSig = "$($setup.FullName).sig" @@ -242,5 +263,30 @@ jobs: $destSig = "$dest.sig" Copy-Item $setup.FullName $dest Copy-Item $setupSig $destSig - gh release upload "${{ github.ref_name }}" "$dest" --clobber - gh release upload "${{ github.ref_name }}" "$destSig" --clobber + foreach ($asset in @($dest, $destSig)) { + for ($attempt = 1; $attempt -le 4; $attempt++) { + gh release upload "$env:GITHUB_REF_NAME" "$asset" --clobber + if ($LASTEXITCODE -eq 0) { break } + if ($attempt -eq 4) { exit $LASTEXITCODE } + Start-Sleep -Seconds 10 + } + } + + - name: Snapshot additional release bundles + if: ${{ !cancelled() }} + run: node .github/scripts/preserve-release-artifacts.mjs + env: + RELEASE_TARGET: ${{ matrix.targets }} + RELEASE_PLATFORM: ${{ matrix.platform }} + RELEASE_RUNTIME: ${{ matrix.runtime }} + RELEASE_ARCH: ${{ matrix.yaak_arch }} + RELEASE_ARGS: ${{ matrix.args }} + + - name: Retain release bundles for recovery + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: release-${{ github.ref_name }}-${{ matrix.platform }}-${{ matrix.runtime }}-${{ matrix.yaak_arch }}-${{ github.run_id }}-attempt-${{ github.run_attempt }} + path: ${{ runner.temp }}/yaak-release-artifacts/ + retention-days: 7 + if-no-files-found: warn