diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74010bd2a0..8295ac0cbc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -306,7 +306,8 @@ jobs: with: build_script_name: empty github_token: ${{ secrets.github_token }} - release: false # Don't publish unsigned; we sign first, then publish + release: false + args: "--publish never" # Explicit: don't auto-publish unsigned exe files # --- Code signing (SignPath failure is tolerated; post-signing verification failures will block the release) --- @@ -346,8 +347,8 @@ jobs: exit 1 } # Build a map of original filenames for matching - # SignPath may replace spaces with dots in filenames, so we normalize - # spaces, dots, and hyphens to find the corresponding original file + # SignPath may alter separators in filenames, so we normalize + # dots, hyphens, and spaces to find the corresponding original file $originals = Get-ChildItem -Path .tmp/app-builds -Filter *.exe $signed | ForEach-Object { $signedName = $_.Name @@ -382,21 +383,26 @@ jobs: } } - - name: Regenerate blockmaps and update latest.yml for signed executables + - name: Regenerate blockmaps and generate latest.yml for signed executables if: steps.signpath.outcome == 'success' shell: pwsh run: | $buildDir = ".tmp/app-builds" - $exeFiles = Get-ChildItem -Path $buildDir -Filter "*.exe" + # Only regenerate blockmaps for NSIS setup files (portables don't use them) + $setupExes = Get-ChildItem -Path $buildDir -Filter "Super-Productivity-Setup*.exe" - foreach ($exe in $exeFiles) { + # Resolve the platform-specific app-builder binary from the lockfile-pinned package + $appBuilder = node -e "console.log(require('app-builder-bin').appBuilderPath)" + + foreach ($exe in $setupExes) { $blockmapPath = "$($exe.FullName).blockmap" Write-Host "Regenerating blockmap for $($exe.Name)..." - $output = npx --yes app-builder-bin blockmap --input="$($exe.FullName)" --output="$blockmapPath" --compression=gzip + $output = & $appBuilder blockmap --input="$($exe.FullName)" --output="$blockmapPath" --compression=gzip Write-Host " blockmap output: $output" } - # Update latest.yml with new sha512 hashes and sizes from signed executables + # Update latest.yml with hashes from signed executables + # (electron-builder generates it during build, but with hashes of unsigned files) node -e " const fs = require('fs'); const path = require('path'); @@ -404,36 +410,72 @@ jobs: const crypto = require('crypto'); const buildDir = '.tmp/app-builds'; - const ymlFiles = fs.readdirSync(buildDir).filter(f => f.startsWith('latest') && f.endsWith('.yml')); + const ymlPath = path.join(buildDir, 'latest.yml'); - for (const ymlFile of ymlFiles) { - const ymlPath = path.join(buildDir, ymlFile); + function getFileInfo(filePath) { + const fileBuffer = fs.readFileSync(filePath); + const info = { + sha512: crypto.createHash('sha512').update(fileBuffer).digest('base64'), + size: fileBuffer.length, + }; + const blockmapPath = filePath + '.blockmap'; + if (fs.existsSync(blockmapPath)) { + info.blockMapSize = fs.statSync(blockmapPath).size; + } + return info; + } + + if (fs.existsSync(ymlPath)) { + // Update existing latest.yml (preserves all electron-builder fields) const ymlData = yaml.load(fs.readFileSync(ymlPath, 'utf8')); - - if (ymlData.files) { - for (const fileEntry of ymlData.files) { - const filePath = path.join(buildDir, fileEntry.url); - if (fs.existsSync(filePath)) { - const fileBuffer = fs.readFileSync(filePath); - const sha512 = crypto.createHash('sha512').update(fileBuffer).digest('base64'); - const size = fileBuffer.length; - console.log(ymlFile + ': Updated ' + fileEntry.url + ' sha512=' + sha512.substring(0, 20) + '... size=' + size); - fileEntry.sha512 = sha512; - fileEntry.size = size; - } - } + if (!ymlData.files || ymlData.files.length === 0) { + console.error('ERROR: latest.yml has no file entries'); + process.exit(1); + } + for (const fileEntry of ymlData.files) { + const filePath = path.join(buildDir, fileEntry.url); + if (!fs.existsSync(filePath)) { + console.error('ERROR: File referenced in latest.yml not found: ' + fileEntry.url); + process.exit(1); + } + const info = getFileInfo(filePath); + Object.assign(fileEntry, info); + console.log(' Updated ' + fileEntry.url + ': sha512=' + info.sha512.substring(0, 20) + '... size=' + info.size); } - - // Update top-level sha512 to match primary file if (ymlData.path) { - const primary = ymlData.files && ymlData.files.find(f => f.url === ymlData.path); - if (primary) { - ymlData.sha512 = primary.sha512; - } + const primary = ymlData.files.find(f => f.url === ymlData.path); + if (primary) { ymlData.sha512 = primary.sha512; } } - fs.writeFileSync(ymlPath, yaml.dump(ymlData, { lineWidth: -1 })); - console.log(ymlFile + ' updated successfully'); + console.log('latest.yml updated with signed file hashes'); + } else { + // Fallback: generate from scratch if latest.yml was not created + console.log('No existing latest.yml found, generating from scratch'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + const setupExes = fs.readdirSync(buildDir) + .filter(f => f.startsWith('Super-Productivity-Setup') && f.endsWith('.exe')); + if (setupExes.length === 0) { + console.error('ERROR: No setup executables found in ' + buildDir); + process.exit(1); + } + const files = setupExes.map(filename => { + const filePath = path.join(buildDir, filename); + const info = getFileInfo(filePath); + console.log(' ' + filename + ': sha512=' + info.sha512.substring(0, 20) + '... size=' + info.size); + return { url: filename, ...info }; + }); + const primary = files.find(f => f.url === 'Super-Productivity-Setup.exe') + || files.find(f => f.url === 'Super-Productivity-Setup-x64.exe') + || files[0]; + const latestYml = { + version: pkg.version, + files, + path: primary.url, + sha512: primary.sha512, + releaseDate: new Date().toISOString(), + }; + fs.writeFileSync(ymlPath, yaml.dump(latestYml, { lineWidth: -1 })); + console.log('Generated latest.yml for ' + files.length + ' signed executables'); } " @@ -445,7 +487,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} files: | .tmp/app-builds/*.exe - .tmp/app-builds/*.blockmap + .tmp/app-builds/Super-Productivity-Setup*.blockmap .tmp/app-builds/latest*.yml draft: true prerelease: ${{ contains(github.ref, 'RC') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }} diff --git a/docs/build-and-publish-notes.md b/docs/build-and-publish-notes.md index 35a85efc36..390a005604 100644 --- a/docs/build-and-publish-notes.md +++ b/docs/build-and-publish-notes.md @@ -162,7 +162,7 @@ After a release build completes: 2. **Verify signature** on Windows using PowerShell: ```powershell -Get-AuthenticodeSignature ".\Super Productivity Setup-x64.exe" | Format-List +Get-AuthenticodeSignature ".\Super-Productivity-Setup-x64.exe" | Format-List ``` 3. **Check output** shows: diff --git a/electron-builder.yaml b/electron-builder.yaml index 9f19321525..0da8aa36f7 100644 --- a/electron-builder.yaml +++ b/electron-builder.yaml @@ -30,7 +30,8 @@ win: publish: - github nsis: - artifactName: ${productName} Setup-${arch}.${ext} + # Hardcoded to avoid spaces (SignPath converts spaces to dots). Also referenced in build.yml. + artifactName: Super-Productivity-Setup-${arch}.${ext} # allow to install to custom location oneClick: false allowToChangeInstallationDirectory: true