Files
nyaterm/.github/workflows/build-release.yml
T

901 lines
30 KiB
YAML

name: Build and Release
on:
push:
tags:
- "v*"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: build-release-${{ github.ref }}
cancel-in-progress: false
env:
APP_NAME: NyaTerm
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.platform }}
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest
target: aarch64-apple-darwin
args: --target aarch64-apple-darwin
- platform: macos-latest
target: x86_64-apple-darwin
args: --target x86_64-apple-darwin
- platform: ubuntu-22.04
target: x86_64-unknown-linux-gnu
args: ""
- platform: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
args: ""
- platform: windows-latest
target: x86_64-pc-windows-msvc
args: --bundles nsis
- platform: windows-latest
target: aarch64-pc-windows-msvc
args: --target aarch64-pc-windows-msvc --bundles nsis
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Add Rust target
if: matrix.target != 'x86_64-unknown-linux-gnu' && matrix.target != 'x86_64-pc-windows-msvc'
run: rustup target add ${{ matrix.target }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: |
./src-tauri -> target
key: ${{ matrix.platform }}-${{ matrix.target }}
cache-on-failure: false
env:
RUST_CACHE_COMPRESS: true
- name: Install pnpm
uses: pnpm/action-setup@v5
with:
version: 9
run_install: false
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install Linux dependencies
if: contains(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libjavascriptcoregtk-4.1-dev \
libsoup-3.0-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev \
libudev-dev \
pkg-config \
build-essential \
curl \
wget \
file \
xdg-utils
if [ "${{ matrix.platform }}" = "ubuntu-22.04-arm" ]; then
sudo apt-get install -y libfuse2
fi
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Generate release notes
if: startsWith(github.ref, 'refs/tags/v')
id: release_notes
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
NOTES=""
if [ -f CHANGELOG.md ]; then
NOTES=$(awk -v version="$VERSION" '
BEGIN { found = 0 }
/^## / {
if (found == 1) exit
if ($0 ~ version) found = 1
}
found == 1 { print }
' CHANGELOG.md)
fi
if [ -z "$NOTES" ]; then
PREV_TAG=$(git tag --sort=-v:refname | grep '^v' | sed -n '2p' || true)
if [ -n "$PREV_TAG" ]; then
NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="${GITHUB_REF_NAME}" \
-f previous_tag_name="$PREV_TAG" \
--jq '.body' 2>/dev/null || true)
else
NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="${GITHUB_REF_NAME}" \
--jq '.body' 2>/dev/null || true)
fi
fi
if [ -z "$NOTES" ]; then
NOTES="See the assets below to download the installer for your platform."
fi
{
echo "body<<EOF"
echo "$NOTES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Prepare Tauri updater signing key
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY_B64: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_B64 }}
run: |
set -euo pipefail
if [ -z "${TAURI_SIGNING_PRIVATE_KEY_B64:-}" ]; then
echo "TAURI_SIGNING_PRIVATE_KEY_B64 is empty"
exit 1
fi
node <<'NODE'
const fs = require('fs');
const os = require('os');
const path = require('path');
const keyB64 = process.env.TAURI_SIGNING_PRIVATE_KEY_B64 || '';
const runnerTemp = process.env.RUNNER_TEMP || os.tmpdir();
const keyPath = path.join(runnerTemp, 'tauri-updater.key');
fs.writeFileSync(keyPath, Buffer.from(keyB64, 'base64'));
fs.appendFileSync(process.env.GITHUB_ENV, `TAURI_SIGNING_PRIVATE_KEY=${keyPath}\n`);
console.log(`Tauri updater signing key prepared at: ${keyPath}`);
NODE
- name: Verify GitHub Gist OAuth Client ID
shell: bash
env:
NYATERM_GITHUB_GIST_CLIENT_ID: ${{ vars.NYATERM_GITHUB_GIST_CLIENT_ID }}
run: |
set -euo pipefail
if [ -z "${NYATERM_GITHUB_GIST_CLIENT_ID:-}" ]; then
echo "NYATERM_GITHUB_GIST_CLIENT_ID repository variable is required for release builds."
exit 1
fi
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPIMAGE_EXTRACT_AND_RUN: ${{ matrix.target == 'aarch64-unknown-linux-gnu' && '1' || '' }}
NYATERM_GITHUB_GIST_CLIENT_ID: ${{ vars.NYATERM_GITHUB_GIST_CLIENT_ID }}
with:
tagName: ${{ github.ref_name }}
releaseName: NyaTerm ${{ github.ref_name }}
releaseBody: ${{ steps.release_notes.outputs.body }}
releaseDraft: false
prerelease: ${{ contains(github.ref_name, '-') }}
includeUpdaterJson: false
args: ${{ matrix.args }}
- name: Package Windows portable zip
if: startsWith(github.ref, 'refs/tags/v') && matrix.platform == 'windows-latest'
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
$ErrorActionPreference = "Stop"
$version = $env:GITHUB_REF_NAME -replace '^v', ''
$target = "${{ matrix.target }}"
$arch = if ($target -eq "aarch64-pc-windows-msvc") { "arm64" } else { "x64" }
$releaseDir = if ($target -eq "aarch64-pc-windows-msvc") {
Join-Path "src-tauri/target" "$target/release"
} else {
"src-tauri/target/release"
}
$exe = Join-Path $releaseDir "nyaterm.exe"
if (-not (Test-Path $exe)) {
$exe = Get-ChildItem -Path $releaseDir -Filter "*.exe" |
Where-Object { $_.Name -notmatch 'build[-_]script' } |
Select-Object -First 1 -ExpandProperty FullName
}
if (-not $exe -or -not (Test-Path $exe)) {
throw "Release executable not found in $releaseDir"
}
$assetName = "NyaTerm_${version}_windows_${arch}_portable.zip"
$portableRoot = Join-Path $env:RUNNER_TEMP "NyaTerm-portable-package"
$portableDir = Join-Path $portableRoot "NyaTerm-portable"
$dataDir = Join-Path $portableDir "data"
$assetPath = Join-Path (Get-Location) $assetName
Remove-Item -LiteralPath $portableRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $dataDir -Force | Out-Null
Copy-Item -LiteralPath $exe -Destination (Join-Path $portableDir "NyaTerm.exe")
New-Item -ItemType File -Path (Join-Path $portableDir "portable.flag") -Force | Out-Null
New-Item -ItemType File -Path (Join-Path $dataDir ".keep") -Force | Out-Null
Compress-Archive -Path $portableDir -DestinationPath $assetPath -Force
pnpm tauri signer sign --private-key-path "$env:TAURI_SIGNING_PRIVATE_KEY" "$assetPath"
$signaturePath = "$assetPath.sig"
if (-not (Test-Path $signaturePath)) {
throw "Portable updater signature was not generated: $signaturePath"
}
gh release upload "$env:GITHUB_REF_NAME" "$assetPath" "$signaturePath" --repo "$env:GITHUB_REPOSITORY" --clobber
rename-assets:
name: Rename release assets
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Rename release assets
uses: actions/github-script@v8
env:
APP_NAME: ${{ env.APP_NAME }}
with:
script: |
const tag = context.ref.replace('refs/tags/', '');
const version = tag.replace(/^v/, '');
const appName = process.env.APP_NAME || 'NyaTerm';
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag,
});
const assets = release.assets || [];
function normalizeArch(name) {
const lower = name.toLowerCase();
if (
lower.includes('aarch64') ||
lower.includes('arm64') ||
lower.includes('arm_64')
) {
return 'arm64';
}
if (
lower.includes('x86_64') ||
lower.includes('x64') ||
lower.includes('amd64')
) {
return 'x64';
}
return null;
}
function getNewAssetName(assetName) {
if (assetName === 'latest.json') return null;
const isSig = assetName.endsWith('.sig');
const baseName = isSig ? assetName.slice(0, -4) : assetName;
const lower = baseName.toLowerCase();
const arch = normalizeArch(baseName);
if (!arch) return null;
let newName = null;
if (lower.endsWith('.dmg')) {
newName = `${appName}_${version}_macos_${arch}.dmg`;
} else if (lower.endsWith('.app.tar.gz')) {
newName = `${appName}_${version}_macos_${arch}.app.tar.gz`;
} else if (lower.endsWith('.appimage')) {
newName = `${appName}_${version}_linux_${arch}.AppImage`;
} else if (lower.endsWith('.deb')) {
newName = `${appName}_${version}_linux_${arch}.deb`;
} else if (lower.endsWith('.rpm')) {
newName = `${appName}_${version}_linux_${arch}.rpm`;
} else if (lower.endsWith('-setup.exe')) {
newName = `${appName}_${version}_windows_${arch}-setup.exe`;
} else if (lower.endsWith('-setup.nsis.zip')) {
newName = `${appName}_${version}_windows_${arch}-setup.nsis.zip`;
}
if (!newName) return null;
return isSig ? `${newName}.sig` : newName;
}
const existingNames = new Set(assets.map(asset => asset.name));
const renames = [];
for (const asset of assets) {
const newName = getNewAssetName(asset.name);
if (!newName) {
console.log(`Skip unmatched asset: ${asset.name}`);
continue;
}
if (newName === asset.name) {
console.log(`Already normalized: ${asset.name}`);
continue;
}
if (existingNames.has(newName)) {
console.log(`Skip because target already exists: ${asset.name} -> ${newName}`);
continue;
}
renames.push({
id: asset.id,
oldName: asset.name,
newName,
});
existingNames.add(newName);
}
for (const item of renames) {
console.log(`Renaming: ${item.oldName} -> ${item.newName}`);
await github.rest.repos.updateReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
asset_id: item.id,
name: item.newName,
});
}
console.log(`Renamed ${renames.length} asset(s).`);
publish-updater:
name: Generate updater manifest
needs: rename-assets
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Build and upload latest.json
uses: actions/github-script@v8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
script: |
const tag = context.ref.replace('refs/tags/', '');
const version = tag.replace(/^v/, '');
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: release } = await github.rest.repos.getReleaseByTag({
owner,
repo,
tag,
});
const assets = release.assets || [];
function mapSigToPlatforms(name) {
const baseName = name.replace(/\.sig$/, '');
const lower = name.toLowerCase();
const keys = [];
if (lower.endsWith('.app.tar.gz.sig')) {
if (lower.includes('macos_x64') || lower.includes('x64') || lower.includes('x86_64')) {
keys.push('darwin-x86_64');
} else if (lower.includes('macos_arm64') || lower.includes('arm64') || lower.includes('aarch64')) {
keys.push('darwin-aarch64');
}
} else if (lower.endsWith('.appimage.sig')) {
if (lower.includes('linux_x64') || lower.includes('x64') || lower.includes('x86_64') || lower.includes('amd64')) {
keys.push('linux-x86_64');
} else if (lower.includes('linux_arm64') || lower.includes('arm64') || lower.includes('aarch64')) {
keys.push('linux-aarch64');
}
} else if (lower.endsWith('_portable.zip.sig')) {
if (lower.includes('windows_x64') || lower.includes('x64') || lower.includes('x86_64') || lower.includes('amd64')) {
keys.push('windows-x86_64-portable');
} else if (lower.includes('windows_arm64') || lower.includes('arm64') || lower.includes('aarch64')) {
keys.push('windows-aarch64-portable');
}
} else if (lower.endsWith('-setup.exe.sig') || lower.endsWith('-setup.nsis.zip.sig')) {
if (lower.includes('windows_x64') || lower.includes('x64') || lower.includes('x86_64') || lower.includes('amd64')) {
keys.push('windows-x86_64');
} else if (lower.includes('windows_arm64') || lower.includes('arm64') || lower.includes('aarch64')) {
keys.push('windows-aarch64');
}
}
return { keys, baseName };
}
async function downloadAssetText(asset) {
const response = await fetch(asset.url, {
headers: {
Accept: 'application/octet-stream',
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'actions/github-script',
},
redirect: 'follow',
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(
`Failed to download ${asset.name}: HTTP ${response.status} ${response.statusText} ${body.slice(0, 300)}`
);
}
return (await response.text()).trim();
}
const platforms = {};
const sigAssets = assets.filter(asset => asset.name.endsWith('.sig'));
for (const sigAsset of sigAssets) {
const { keys, baseName } = mapSigToPlatforms(sigAsset.name);
if (keys.length === 0) {
console.log(`Skip non-updater signature asset: ${sigAsset.name}`);
continue;
}
const binaryAsset = assets.find(asset => asset.name === baseName);
if (!binaryAsset) {
console.log(`Warning: no binary asset found for ${baseName}`);
continue;
}
let signature;
try {
signature = await downloadAssetText(sigAsset);
} catch (error) {
console.log(`Warning: ${error.message}`);
continue;
}
for (const key of keys) {
platforms[key] = {
signature,
url: binaryAsset.browser_download_url,
};
}
}
if (Object.keys(platforms).length === 0) {
core.setFailed('No updater platforms generated. Check release asset names and .sig download permissions.');
return;
}
const latestJson = {
version,
notes: release.body || '',
pub_date: release.published_at || new Date().toISOString(),
platforms,
};
const existing = assets.find(asset => asset.name === 'latest.json');
if (existing) {
console.log('Deleting existing latest.json');
await github.rest.repos.deleteReleaseAsset({
owner,
repo,
asset_id: existing.id,
});
}
const content = JSON.stringify(latestJson, null, 2);
await github.rest.repos.uploadReleaseAsset({
owner,
repo,
release_id: release.id,
name: 'latest.json',
data: content,
headers: {
'content-type': 'application/json',
'content-length': Buffer.byteLength(content),
},
});
console.log(`Uploaded latest.json with ${Object.keys(platforms).length} platform key(s).`);
cleanup-sigs:
name: Remove signature assets
needs: publish-updater
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Delete .sig assets from release
uses: actions/github-script@v8
with:
script: |
const tag = context.ref.replace('refs/tags/', '');
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag,
});
const sigAssets = release.assets.filter(asset => asset.name.endsWith('.sig'));
for (const asset of sigAssets) {
console.log(`Deleting signature asset: ${asset.name}`);
await github.rest.repos.deleteReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
asset_id: asset.id,
});
}
console.log(`Deleted ${sigAssets.length} .sig file(s).`);
publish-beta-updater:
name: Publish beta updater manifest
needs: cleanup-sigs
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Upload latest.json to updater-beta release
uses: actions/github-script@v8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
script: |
const tag = context.ref.replace('refs/tags/', '');
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag,
});
const latestJsonAsset = release.assets.find(asset => asset.name === 'latest.json');
if (!latestJsonAsset) {
core.setFailed('No latest.json found on current release.');
return;
}
async function downloadAssetText(asset) {
const response = await fetch(asset.url, {
headers: {
Accept: 'application/octet-stream',
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'actions/github-script',
},
redirect: 'follow',
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(
`Failed to download ${asset.name}: HTTP ${response.status} ${response.statusText} ${body.slice(0, 300)}`
);
}
return await response.text();
}
const content = await downloadAssetText(latestJsonAsset);
let betaRelease;
try {
const { data } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: 'updater-beta',
});
betaRelease = data;
} catch (error) {
if (error.status !== 404) {
throw error;
}
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: 'updater-beta',
name: 'Beta Updater Manifest',
body: 'This release hosts latest.json for the beta update channel. Do not delete.',
prerelease: true,
draft: false,
});
betaRelease = data;
}
const existing = betaRelease.assets.find(asset => asset.name === 'latest.json');
if (existing) {
console.log('Deleting existing updater-beta latest.json');
await github.rest.repos.deleteReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
asset_id: existing.id,
});
}
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: betaRelease.id,
name: 'latest.json',
data: content,
headers: {
'content-type': 'application/json',
'content-length': Buffer.byteLength(content),
},
});
console.log(`Uploaded latest.json to updater-beta release for ${tag}.`);
publish-r2:
name: Publish release assets to Cloudflare R2
needs: publish-beta-updater
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
env:
TAG: ${{ github.ref_name }}
R2_BUCKET: nyaterm
DOWNLOAD_BASE_URL: https://downloads.nyaterm.app
R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
steps:
- name: Download GitHub release assets
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
rm -rf release-assets
mkdir -p release-assets
gh release download "${TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--dir release-assets \
--clobber
echo "Downloaded release assets:"
find release-assets -maxdepth 1 -type f -print
- name: Build R2 latest.json
shell: bash
run: |
set -euo pipefail
node <<'NODE'
const fs = require('fs');
const path = require('path');
const tag = process.env.GITHUB_REF_NAME;
const baseUrl = (process.env.DOWNLOAD_BASE_URL || '').replace(/\/$/, '');
const inputPath = 'release-assets/latest.json';
const outputPath = 'release-assets/r2-latest.json';
if (!fs.existsSync(inputPath)) {
throw new Error(`Missing ${inputPath}`);
}
const manifest = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
if (!manifest.platforms || typeof manifest.platforms !== 'object') {
throw new Error('latest.json does not contain platforms');
}
for (const [platform, item] of Object.entries(manifest.platforms)) {
if (!item.url) {
throw new Error(`Missing url for platforms.${platform}`);
}
const oldUrl = new URL(item.url);
const filename = decodeURIComponent(oldUrl.pathname.split('/').pop());
const localFile = path.join('release-assets', filename);
if (!fs.existsSync(localFile)) {
throw new Error(`Installer referenced by latest.json not found: ${filename}`);
}
item.url = `${baseUrl}/releases/${tag}/${encodeURIComponent(filename)}`;
}
fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2) + '\n');
console.log(`Wrote ${outputPath}`);
console.log(`Version: ${manifest.version}`);
console.log(`Platforms: ${Object.keys(manifest.platforms).join(', ')}`);
NODE
- name: Upload installers to R2
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
for file_path in release-assets/*; do
file="$(basename "$file_path")"
case "$file" in
latest.json|r2-latest.json|*.sig)
continue
;;
esac
content_type="application/octet-stream"
case "$file" in
*.dmg)
content_type="application/x-apple-diskimage"
;;
*.exe)
content_type="application/vnd.microsoft.portable-executable"
;;
*.AppImage)
content_type="application/octet-stream"
;;
*.deb)
content_type="application/vnd.debian.binary-package"
;;
*.rpm)
content_type="application/x-rpm"
;;
*.tar.gz|*.gz)
content_type="application/gzip"
;;
*.zip)
content_type="application/zip"
;;
esac
echo "Uploading installer: $file"
aws s3 cp "$file_path" "s3://${R2_BUCKET}/releases/${GITHUB_REF_NAME}/${file}" \
--endpoint-url "$R2_ENDPOINT" \
--content-type "$content_type" \
--cache-control "public, max-age=31536000, immutable" \
--content-disposition "attachment; filename=\"${file}\"" \
--no-progress
done
- name: Upload latest.json to R2
shell: bash
run: |
set -euo pipefail
echo "Uploading latest.json"
aws s3 cp release-assets/r2-latest.json "s3://${R2_BUCKET}/latest.json" \
--endpoint-url "$R2_ENDPOINT" \
--content-type "application/json; charset=utf-8" \
--cache-control "public, max-age=60, must-revalidate" \
--no-progress
echo "Uploading versioned latest.json"
aws s3 cp release-assets/r2-latest.json "s3://${R2_BUCKET}/releases/${GITHUB_REF_NAME}/latest.json" \
--endpoint-url "$R2_ENDPOINT" \
--content-type "application/json; charset=utf-8" \
--cache-control "public, max-age=31536000, immutable" \
--no-progress
- name: Verify R2 latest.json
shell: bash
run: |
set -euo pipefail
curl -fsSL "${DOWNLOAD_BASE_URL}/latest.json" | node -e "
let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
const json = JSON.parse(input);
const requiredPortablePlatforms = [
'windows-x86_64-portable',
'windows-aarch64-portable',
];
for (const platform of requiredPortablePlatforms) {
const item = json.platforms?.[platform];
if (!item?.url || !item?.signature) {
throw new Error(
'R2 latest.json is missing a signed portable target: ' + platform
);
}
const portableUrlPrefix = process.env.DOWNLOAD_BASE_URL +
'/releases/' + process.env.GITHUB_REF_NAME + '/';
if (!item.url.startsWith(portableUrlPrefix)) {
throw new Error(
'R2 latest.json portable target does not use the versioned R2 URL: ' + platform
);
}
}
console.log('R2 latest.json version:', json.version);
console.log('Platforms:', Object.keys(json.platforms || {}).join(', '));
});
"