From 285b19ccc37b52711fa2671654f6be80e793f1fd Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 23 Sep 2026 01:43:40 -0700 Subject: [PATCH] fix(ci): download AUR assets without release metadata --- .github/workflows/release.yml | 25 +++++--- docs/RELEASING.md | 2 +- scripts/aur-release-download.test.ts | 93 ++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 scripts/aur-release-download.test.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a608aaf6..6271d1d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -255,22 +255,31 @@ jobs: echo "skip=true" >> "$GITHUB_OUTPUT" - name: Download release assets for AUR + id: aur_assets if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ steps.version.outputs.VERSION }} run: | set -euo pipefail version="$RELEASE_VERSION" install -dm755 .tmp/aur-release-assets - gh release download "$version" \ - --dir .tmp/aur-release-assets \ - --pattern "SubMiner-${version#v}.AppImage" \ - --pattern "subminer" \ - --pattern "subminer-assets.tar.gz" + for asset in "SubMiner-${version#v}.AppImage" subminer subminer-assets.tar.gz; do + destination=".tmp/aur-release-assets/$asset" + if ! curl --fail --silent --show-error --location \ + --retry 3 --retry-delay 1 --retry-all-errors \ + --connect-timeout 30 --max-time 600 \ + --output "$destination.partial" \ + "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$version/$asset"; then + echo "::warning::Unable to download $asset after retries; skipping automated AUR publish." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + mv "$destination.partial" "$destination" + done + echo "skip=false" >> "$GITHUB_OUTPUT" - name: Update AUR packaging metadata - if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' + if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true' env: RELEASE_VERSION: ${{ steps.version.outputs.VERSION }} run: | @@ -287,7 +296,7 @@ jobs: --assets ".tmp/aur-release-assets/subminer-assets.tar.gz" - name: Commit and push AUR update - if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' + if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true' working-directory: aur-subminer-bin env: GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes diff --git a/docs/RELEASING.md b/docs/RELEASING.md index ae4a1cd8..5fb21b66 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -152,7 +152,7 @@ Notes: - Tagged release workflow now also attempts to update `subminer-bin` on the AUR after GitHub Release publication. - Stable release tags update `https://docs.subminer.moe/` and `https://docs.subminer.moe/v//` through `.github/workflows/docs-pages.yml`; `/main/` continues to show development docs from `main`. - Keep Cloudflare Pages Git auto-deploy disabled for `docs.subminer.moe`. Production docs are direct-uploaded by Wrangler from GitHub Actions with `--branch main`. -- AUR publish is best-effort: the workflow retries transient SSH clone/push failures, then warns and leaves the GitHub Release green if AUR still fails. Follow up with a manual `git push aur master` from the AUR checkout when needed. +- AUR publish is best-effort: the workflow downloads the three known assets directly from the tagged release URLs, avoiding GitHub's sometimes-stale release asset listing. Downloads and SSH clone/push operations retry transient failures, then warn and skip AUR publication if retries are exhausted. Follow up with a manual `git push aur master` from the AUR checkout when needed. - Required GitHub Actions secret: `AUR_SSH_PRIVATE_KEY`. Add the matching public key to your AUR account before relying on the automation. - Release and prerelease workflows upload updater metadata (`latest*.yml`) and blockmaps (`*.blockmap`) alongside platform artifacts. Do not remove those files while `electron-updater` is enabled. - Release and prerelease workflows publish `subminer` for POSIX systems and `subminer.cmd` for Windows. Both locate a packaged app and use its private Bun runtime. Keep the corresponding-source archive named `bun-v1.3.5-source.tar.gz`. diff --git a/scripts/aur-release-download.test.ts b/scripts/aur-release-download.test.ts new file mode 100644 index 00000000..4dfaa76a --- /dev/null +++ b/scripts/aur-release-download.test.ts @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { test } from 'bun:test'; + +test.each([false, true])( + 'AUR downloads handle empty release metadata, unavailable=%s', + async (unavailable) => { + const workflow = await readFile( + new URL('../.github/workflows/release.yml', import.meta.url), + 'utf8', + ); + const step = workflow + .split(' - name: Download release assets for AUR\n')[1] + ?.split('\n - name:')[0]; + const script = step?.split(' run: |\n')[1]?.replace(/^ /gm, ''); + assert.ok(script, 'AUR download step must have a shell script'); + + const workspace = await mkdtemp(path.join(os.tmpdir(), 'subminer-aur-download-')); + const requests: string[] = []; + const files = new Map([ + ['SubMiner-0.20.0.AppImage', 'appimage bytes'], + ['subminer', 'launcher bytes'], + ['subminer-assets.tar.gz', 'optional assets bytes'], + ]); + const server = Bun.serve({ + hostname: '127.0.0.1', + port: 0, + fetch(request) { + const pathname = new URL(request.url).pathname; + requests.push(pathname); + if (unavailable || requests.length === 1) return new Response('try again', { status: 503 }); + const name = pathname.split('/').at(-1); + const body = name ? files.get(name) : undefined; + return new Response(body ?? 'not found', { status: body ? 200 : 404 }); + }, + }); + + try { + const bin = path.join(workspace, 'bin'); + await mkdir(bin); + await writeFile( + path.join(bin, 'gh'), + '#!/bin/sh\necho "no assets to download" >&2\nexit 1\n', + { mode: 0o755 }, + ); + const output = path.join(workspace, 'output'); + const proc = Bun.spawn(['bash', '-c', script], { + cwd: workspace, + env: { + ...process.env, + PATH: `${bin}${path.delimiter}${process.env.PATH}`, + RELEASE_VERSION: 'v0.20.0', + GITHUB_SERVER_URL: server.url.origin, + GITHUB_REPOSITORY: 'ksyasuda/SubMiner', + GITHUB_OUTPUT: output, + }, + stdout: 'pipe', + stderr: 'pipe', + }); + const [status, stderr, stdout] = await Promise.all([ + proc.exited, + new Response(proc.stderr).text(), + new Response(proc.stdout).text(), + ]); + assert.equal(status, 0, stderr); + if (unavailable) { + assert.equal(requests.length, 4, 'failed downloads stop after three retries'); + assert.match(await readFile(output, 'utf8'), /^skip=true$/m); + assert.match(stdout, /::warning::Unable to download/); + await assert.rejects( + readFile(path.join(workspace, '.tmp/aur-release-assets/SubMiner-0.20.0.AppImage')), + { code: 'ENOENT' }, + ); + return; + } + for (const [name, body] of files) { + assert.equal( + await readFile(path.join(workspace, '.tmp/aur-release-assets', name), 'utf8'), + body, + ); + assert.ok(requests.includes(`/ksyasuda/SubMiner/releases/download/v0.20.0/${name}`)); + } + assert.equal(requests.length, 4, 'the first failed download must be retried'); + assert.match(await readFile(output, 'utf8'), /^skip=false$/m); + } finally { + server.stop(true); + await rm(workspace, { recursive: true, force: true }); + } + }, + 15_000, +);