import { readFileSync, writeFileSync } from 'node:fs'; import { basename, extname, resolve } from 'node:path'; import { getOctokit } from '@actions/github'; import { uploadAssets } from './upload-release-assets'; import { deleteGiteaReleaseAsset, getAssetName } from './utils'; import type { Artifact, TargetInfo } from './types'; import { createArtifact } from './utils'; type Platform = { signature: string; url: string; }; type VersionContent = { version: string; notes: string; pub_date: string; platforms: { [key: string]: Platform; }; }; export async function uploadVersionJSON( owner: string, repo: string, version: string, notes: string, tagName: string, releaseId: number, artifacts: Artifact[], targetInfo: TargetInfo, unzippedSig: boolean, updaterJsonPreferNsis: boolean, updaterJsonKeepUniversal: boolean, retryAttempts: number, githubBaseUrl: string, isGitea: boolean, assetNamePattern?: string, ) { if (process.env.GITHUB_TOKEN === undefined) { throw new Error('GITHUB_TOKEN is required'); } const github = getOctokit(process.env.GITHUB_TOKEN, { baseUrl: githubBaseUrl, }); const versionFilename = 'latest.json'; const versionFile = resolve(process.cwd(), versionFilename); const versionContent: VersionContent = { version, notes, pub_date: new Date().toISOString(), platforms: {}, }; const assets = await github.rest.repos.listReleaseAssets({ owner: owner, repo: repo, release_id: releaseId, per_page: 50, }); const asset = assets.data.find((e) => e.name === versionFilename); if (asset) { if (isGitea) { const info = ( await github.request( 'GET /repos/{owner}/{repo}/releases/{release_id}/assets/{asset_id}', { owner, repo, release_id: releaseId, asset_id: asset.id, }, ) ).data as { browser_download_url: string }; const data = (await github.request(`GET ${info.browser_download_url}`)) .data as string; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment versionContent.platforms = JSON.parse( data, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access ).platforms; } else { const assetData = ( await github.request( `GET /repos/{owner}/{repo}/releases/assets/{asset_id}`, { owner: owner, repo: repo, release_id: releaseId, asset_id: asset.id, headers: { accept: 'application/octet-stream', }, }, ) ).data as unknown as ArrayBuffer; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment versionContent.platforms = JSON.parse( Buffer.from(assetData).toString(), // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access ).platforms; } } const downloadUrls = new Map(); for (const data of assets.data) { downloadUrls.set(data.name, data.browser_download_url); } // Assets matching artifacts generated by this action const filteredAssets = []; for (const artifact of artifacts) { const assetName = getAssetName(artifact, assetNamePattern) .trim() .replace(/[ ()[\]{}]/g, '.') .replace(/\.\./g, '.') .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); const downloadUrl = downloadUrls.get(assetName); if (downloadUrl) { filteredAssets.push({ downloadUrl, assetName, path: artifact.path, arch: artifact.arch, bundle: artifact.bundle, }); } } const signatureFiles = filteredAssets.filter((asset) => { return asset.assetName.endsWith('.sig'); }); function signaturePriority(signaturePath: string) { if ( (unzippedSig && signaturePath.endsWith('.AppImage.sig')) || (!unzippedSig && signaturePath.endsWith('.AppImage.tar.gz.sig')) ) { return 100; } const priorities = updaterJsonPreferNsis ? unzippedSig ? ['.exe.sig', '.msi.sig'] : ['.nsis.zip.sig', '.msi.zip.sig'] : unzippedSig ? ['.msi.sig', '.exe.sig'] : ['.msi.zip.sig', '.nsis.zip.sig']; for (const [index, extension] of priorities.entries()) { if (signaturePath.endsWith(extension)) { return 100 - index; } } return 0; } signatureFiles.sort((a, b) => { return signaturePriority(b.path) - signaturePriority(a.path); }); if (!signatureFiles[0]) { console.warn( 'Signature not found for the updater JSON. Skipping upload...', ); return; } for (const [idx, signatureFile] of signatureFiles.entries()) { const updaterFileName = basename( signatureFile.assetName, extname(signatureFile.assetName), ); let updaterFileDownloadUrl = filteredAssets.find( (asset) => asset.assetName === updaterFileName, )?.downloadUrl; if (!updaterFileDownloadUrl) { console.warn( `Updater asset belonging to signature file "${signatureFile.assetName}" not found.`, ); continue; } // Untagged release downloads won't work after the release was published updaterFileDownloadUrl = updaterFileDownloadUrl.replace( /\/download\/(untagged-[^/]+)\//, tagName ? `/download/${encodeURIComponent(tagName)}/` : '/latest/download/', ); let os = targetInfo.platform as string; if (os === 'macos') { os = 'darwin'; } let arch = signatureFile.arch; arch = arch === 'amd64' || arch === 'x86_64' || arch === 'x64' ? 'x86_64' : arch === 'x86' || arch === 'i386' ? 'i686' : arch === 'arm' ? 'armv7' : arch === 'arm64' ? 'aarch64' : arch; // This is our primary updater type we use for `{os}-{arch}` if (idx === 0) { if (os === 'darwin' && arch === 'universal') { // Don't overwrite native builds if (!versionContent.platforms['darwin-aarch64']) { (versionContent.platforms['darwin-aarch64'] as unknown) = { signature: readFileSync(signatureFile.path).toString(), url: updaterFileDownloadUrl, }; } if (!versionContent.platforms['darwin-x86_64']) { (versionContent.platforms['darwin-x86_64'] as unknown) = { signature: readFileSync(signatureFile.path).toString(), url: updaterFileDownloadUrl, }; } } if (updaterJsonKeepUniversal || os !== 'darwin' || arch !== 'universal') { (versionContent.platforms[`${os}-${arch}`] as unknown) = { signature: readFileSync(signatureFile.path).toString(), url: updaterFileDownloadUrl, }; } } // This is for the new `{os}-{arch}-{installer}` format if (os === 'darwin' && arch === 'universal') { // Don't overwrite native builds if (!versionContent.platforms['darwin-aarch64-app']) { (versionContent.platforms['darwin-aarch64-app'] as unknown) = { signature: readFileSync(signatureFile.path).toString(), url: updaterFileDownloadUrl, }; } if (!versionContent.platforms['darwin-x86_64-app']) { (versionContent.platforms['darwin-x86_64-app'] as unknown) = { signature: readFileSync(signatureFile.path).toString(), url: updaterFileDownloadUrl, }; } } if (updaterJsonKeepUniversal || os !== 'darwin' || arch !== 'universal') { (versionContent.platforms[ `${os}-${arch}-${signatureFile.bundle}` ] as unknown) = { signature: readFileSync(signatureFile.path).toString(), url: updaterFileDownloadUrl, }; } } writeFileSync(versionFile, JSON.stringify(versionContent, null, 2)); if (asset) { if (isGitea) { await deleteGiteaReleaseAsset(github, owner, repo, releaseId, asset.id); } else { // https://docs.github.com/en/rest/releases/assets#update-a-release-asset await github.rest.repos.deleteReleaseAsset({ owner: owner, repo: repo, release_id: releaseId, asset_id: asset.id, }); } } const artifact = createArtifact({ path: versionFile, name: versionFilename, debug: false, platform: targetInfo.platform, arch: '', bundle: '', version, }); await uploadAssets( owner, repo, releaseId, [artifact], retryAttempts, githubBaseUrl, isGitea, ); }