mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(ci): prevent duplicate unsigned exe files in GitHub Releases
Root cause: electron-builder auto-published unsigned files via implicit tag-based publishing (despite release:false), and spaces in NSIS artifact names caused naming mismatches — electron-builder sanitized spaces to hyphens while softprops/action-gh-release converted them to dots, resulting in three sets of exe files per architecture. Changes: - Hardcode NSIS artifactName with hyphens to eliminate spaces - Add --publish never to explicitly prevent auto-publishing - Update latest.yml with signed file hashes (preserving EB fields) - Use lockfile-pinned app-builder-bin for blockmap regeneration - Scope blockmap publishing to NSIS setup files only - Fail hard if latest.yml references missing files
This commit is contained in:
parent
29441b7ae2
commit
757b7d4f52
3 changed files with 78 additions and 35 deletions
108
.github/workflows/build.yml
vendored
108
.github/workflows/build.yml
vendored
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue