From 156445ed166f243bc3be8b63ef50274973940b36 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Tue, 23 Jun 2026 03:29:17 -0400 Subject: [PATCH] fix(desktop): publish mac updater metadata (#9945) Co-authored-by: Douwe M Osinga --- .github/workflows/release.yml | 8 ++ Cargo.lock | 2 +- .../scripts/generate-mac-update-manifest.js | 112 ++++++++++++++++++ .../components/settings/app/UpdateSection.tsx | 32 +++-- ui/desktop/src/i18n/messages/en.json | 8 +- 5 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 ui/desktop/scripts/generate-mac-update-manifest.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41a191e38..ccf2bcfea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -108,11 +108,16 @@ jobs: id-token: write # Required for Sigstore OIDC signing attestations: write # Required for SLSA build provenance attestations steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Download all artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: merge-multiple: true + - name: Generate macOS update manifest + run: node ui/desktop/scripts/generate-mac-update-manifest.js --version "${GITHUB_REF_NAME}" --directory . + - name: Attest build provenance uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: @@ -124,6 +129,7 @@ jobs: *.deb *.rpm *.flatpak + latest-mac.yml download_cli.sh # Create/update the versioned release @@ -139,6 +145,7 @@ jobs: *.deb *.rpm *.flatpak + latest-mac.yml download_cli.sh allowUpdates: true omitBody: true @@ -159,6 +166,7 @@ jobs: *.deb *.rpm *.flatpak + latest-mac.yml download_cli.sh allowUpdates: true omitBody: true diff --git a/Cargo.lock b/Cargo.lock index b37d68180..bb70e2087 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4995,7 +4995,7 @@ dependencies = [ "pem", "pkcs1", "pkcs8 0.11.0", - "rand 0.8.6", + "rand 0.10.1", "regex", "reqwest 0.13.4", "rmcp", diff --git a/ui/desktop/scripts/generate-mac-update-manifest.js b/ui/desktop/scripts/generate-mac-update-manifest.js new file mode 100644 index 000000000..a909802c2 --- /dev/null +++ b/ui/desktop/scripts/generate-mac-update-manifest.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +function usage() { + console.error( + 'Usage: node scripts/generate-mac-update-manifest.js --version [--directory ]' + ); +} + +function parseArgs(argv) { + const args = { + directory: process.cwd(), + version: '', + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--version') { + args.version = argv[++i] || ''; + } else if (arg === '--directory') { + args.directory = argv[++i] || ''; + } else { + usage(); + process.exit(1); + } + } + + if (!args.version || !args.directory) { + usage(); + process.exit(1); + } + + args.version = args.version.replace(/^v/, ''); + args.directory = path.resolve(args.directory); + return args; +} + +function ensureFile(filePath) { + if (!fs.existsSync(filePath)) { + throw new Error(`Missing required file: ${filePath}`); + } +} + +function copyIfDifferent(source, target) { + ensureFile(source); + if (path.resolve(source) === path.resolve(target)) { + return; + } + fs.copyFileSync(source, target); +} + +function sha512(filePath) { + const hash = crypto.createHash('sha512'); + hash.update(fs.readFileSync(filePath)); + return hash.digest('base64'); +} + +function yamlString(value) { + return JSON.stringify(value); +} + +function writeManifest({ directory, version }) { + const files = [ + { + sourceName: 'Goose.zip', + updateName: 'Goose-darwin-arm64.zip', + }, + { + sourceName: 'Goose_intel_mac.zip', + updateName: 'Goose-darwin-x64.zip', + }, + ]; + + const entries = files.map(({ sourceName, updateName }) => { + const sourcePath = path.join(directory, sourceName); + const updatePath = path.join(directory, updateName); + copyIfDifferent(sourcePath, updatePath); + + const stats = fs.statSync(updatePath); + return { + url: updateName, + sha512: sha512(updatePath), + size: stats.size, + }; + }); + + const manifest = [ + `version: ${yamlString(version)}`, + 'files:', + ...entries.flatMap((entry) => [ + ` - url: ${yamlString(entry.url)}`, + ` sha512: ${yamlString(entry.sha512)}`, + ` size: ${entry.size}`, + ]), + `path: ${yamlString(entries[0].url)}`, + `sha512: ${yamlString(entries[0].sha512)}`, + `releaseDate: ${yamlString(new Date().toISOString())}`, + '', + ].join('\n'); + + fs.writeFileSync(path.join(directory, 'latest-mac.yml'), manifest); +} + +try { + writeManifest(parseArgs(process.argv.slice(2))); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +} diff --git a/ui/desktop/src/components/settings/app/UpdateSection.tsx b/ui/desktop/src/components/settings/app/UpdateSection.tsx index d295f1f9d..d69045e7c 100644 --- a/ui/desktop/src/components/settings/app/UpdateSection.tsx +++ b/ui/desktop/src/components/settings/app/UpdateSection.tsx @@ -81,7 +81,8 @@ const i18n = defineMessages({ }, autoDownload: { id: 'updateSection.autoDownload', - defaultMessage: 'Update will be downloaded automatically in the background.', + defaultMessage: + 'Goose will download the update in the background and install it the next time you quit or restart.', }, manualInstallNote: { id: 'updateSection.manualInstallNote', @@ -89,7 +90,7 @@ const i18n = defineMessages({ }, autoInstallNote: { id: 'updateSection.autoInstallNote', - defaultMessage: 'The update will be installed automatically when you quit the app.', + defaultMessage: 'No manual install is needed.', }, readyInstallManual: { id: 'updateSection.readyInstallManual', @@ -101,11 +102,12 @@ const i18n = defineMessages({ }, readyInstallAuto: { id: 'updateSection.readyInstallAuto', - defaultMessage: '✓ Update is ready! It will be installed when you quit Goose.', + defaultMessage: + "✓ Update is ready. Restart Goose to finish installing it, or quit when you're done.", }, installNowHint: { id: 'updateSection.installNowHint', - defaultMessage: 'Or click "Install & Restart" to update now.', + defaultMessage: 'Click "Install & Restart" to update now.', }, }); @@ -176,7 +178,6 @@ export default function UpdateSection() { // Listen for updater events window.electron.onUpdaterEvent((event) => { - switch (event.event) { case 'checking-for-update': setUpdateStatus('checking'); @@ -354,10 +355,15 @@ export default function UpdateSection() {
{updateInfo.currentVersion || intl.formatMessage(i18n.loading)}
-
{intl.formatMessage(i18n.currentVersion)}
+
+ {intl.formatMessage(i18n.currentVersion)} +
{updateInfo.latestVersion && updateInfo.isUpdateAvailable && ( - {intl.formatMessage(i18n.versionAvailable, { version: updateInfo.latestVersion })} + + {' '} + {intl.formatMessage(i18n.versionAvailable, { version: updateInfo.latestVersion })} + )} {updateInfo.currentVersion && updateInfo.isUpdateAvailable === false && ( {intl.formatMessage(i18n.upToDate)} @@ -375,11 +381,13 @@ export default function UpdateSection() { {intl.formatMessage(i18n.checkForUpdates)} - {updateInfo.isUpdateAvailable && updateStatus === 'idle' && autoDownloadEffectivelyDisabled && ( - - )} + {updateInfo.isUpdateAvailable && + updateStatus === 'idle' && + autoDownloadEffectivelyDisabled && ( + + )} {updateStatus === 'ready' && (