Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 37 additions & 36 deletions .github/workflows/nightly-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ jobs:
- os: windows-latest
config: buildScripts/electron-builder-win.json
artifact_name: windows-x64-build
win_arch: x64
win_unpacked_dir: win-unpacked
- os: windows-latest
config: buildScripts/electron-builder-win-arm64.json
artifact_name: windows-arm64-build
win_arch: arm64
win_unpacked_dir: win-arm64-unpacked
- os: macos-latest
config: buildScripts/electron-builder-mac.json
artifact_name: macos-build
Expand Down Expand Up @@ -107,9 +111,9 @@ jobs:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

- name: Package application (Windows)
- name: Build application directory (Windows)
if: matrix.os == 'windows-latest'
run: node ./buildScripts/package.js --config=${{ matrix.config }}
run: pnpm exec electron-builder --config ${{ matrix.config }} --dir --${{ matrix.win_arch }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
Expand All @@ -120,7 +124,7 @@ jobs:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

- name: Sign Windows artifacts with Azure Trusted Signing
- name: Sign application binaries with Azure Trusted Signing (Windows)
if: matrix.os == 'windows-latest'
uses: azure/artifact-signing-action@v1
with:
Expand All @@ -130,47 +134,44 @@ jobs:
endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
files-folder: ${{ github.workspace }}/build
files-folder-filter: exe,msi
files-folder: ${{ github.workspace }}/build/${{ matrix.win_unpacked_dir }}
files-folder-filter: exe,dll
files-folder-recurse: true
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
description: Power Platform ToolBox (Insider)
description-url: https://github.com/PowerPlatformToolBox/desktop-app

- name: Repackage portable ZIP with signed EXE (Windows)
- name: Package installers from signed directory (Windows)
if: matrix.os == 'windows-latest'
shell: powershell
run: |
$buildDir = "${{ github.workspace }}/build"
$zipFiles = @(Get-ChildItem "$buildDir/*.zip" -ErrorAction SilentlyContinue)

if ($zipFiles.Count -eq 0) {
Write-Host "No ZIP artifacts found; skipping repack."
exit 0
}

foreach ($zip in $zipFiles) {
Write-Host "Repacking ZIP: $($zip.Name)"
$tempDir = Join-Path $env:RUNNER_TEMP ([Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $tempDir | Out-Null

Expand-Archive -Path $zip.FullName -DestinationPath $tempDir -Force

$zipExeFiles = @(Get-ChildItem $tempDir -Recurse -Filter *.exe -ErrorAction SilentlyContinue)
foreach ($zipExe in $zipExeFiles) {
$signedExe = Get-ChildItem $buildDir -Recurse -Filter $zipExe.Name -ErrorAction SilentlyContinue | Select-Object -First 1
if ($signedExe) {
Copy-Item $signedExe.FullName $zipExe.FullName -Force
Write-Host " Replaced $($zipExe.Name) with signed binary."
} else {
Write-Host " No signed match found for $($zipExe.Name)."
}
}
run: pnpm exec electron-builder --config ${{ matrix.config }} --prepackaged build/${{ matrix.win_unpacked_dir }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

Remove-Item $zip.FullName -Force
Compress-Archive -Path (Join-Path $tempDir '*') -DestinationPath $zip.FullName -Force
}
- name: Sign Windows installers with Azure Trusted Signing
if: matrix.os == 'windows-latest'
uses: azure/artifact-signing-action@v1
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
files-folder: ${{ github.workspace }}/build
files-folder-filter: exe,msi
files-folder-recurse: false
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
description: Power Platform ToolBox (Insider)
description-url: https://github.com/PowerPlatformToolBox/desktop-app

- name: Regenerate latest.yml with correct SHA256 hashes (Windows)
if: matrix.os == 'windows-latest'
Expand Down
73 changes: 37 additions & 36 deletions .github/workflows/prod-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ jobs:
- os: windows-latest
config: buildScripts/electron-builder-win.json
artifact_name: windows-x64-release
win_arch: x64
win_unpacked_dir: win-unpacked
- os: windows-latest
config: buildScripts/electron-builder-win-arm64.json
artifact_name: windows-arm64-release
win_arch: arm64
win_unpacked_dir: win-arm64-unpacked
- os: macos-latest
config: buildScripts/electron-builder-mac.json
artifact_name: macos-release
Expand Down Expand Up @@ -133,9 +137,9 @@ jobs:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

- name: Package application (Windows)
- name: Build application directory (Windows)
if: matrix.os == 'windows-latest'
run: node ./buildScripts/package.js --config=${{ matrix.config }}
run: pnpm exec electron-builder --config ${{ matrix.config }} --dir --${{ matrix.win_arch }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
Expand All @@ -146,7 +150,7 @@ jobs:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

- name: Sign Windows artifacts with Azure Trusted Signing
- name: Sign application binaries with Azure Trusted Signing (Windows)
if: matrix.os == 'windows-latest'
uses: azure/artifact-signing-action@v1
with:
Expand All @@ -156,47 +160,44 @@ jobs:
endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
files-folder: ${{ github.workspace }}/build
files-folder-filter: exe,msi
files-folder: ${{ github.workspace }}/build/${{ matrix.win_unpacked_dir }}
files-folder-filter: exe,dll
files-folder-recurse: true
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
description: Power Platform ToolBox
description-url: https://github.com/PowerPlatformToolBox/desktop-app

- name: Repackage portable ZIP with signed EXE (Windows)
- name: Package installers from signed directory (Windows)
if: matrix.os == 'windows-latest'
shell: powershell
run: |
$buildDir = "${{ github.workspace }}/build"
$zipFiles = @(Get-ChildItem "$buildDir/*.zip" -ErrorAction SilentlyContinue)

if ($zipFiles.Count -eq 0) {
Write-Host "No ZIP artifacts found; skipping repack."
exit 0
}

foreach ($zip in $zipFiles) {
Write-Host "Repacking ZIP: $($zip.Name)"
$tempDir = Join-Path $env:RUNNER_TEMP ([Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $tempDir | Out-Null

Expand-Archive -Path $zip.FullName -DestinationPath $tempDir -Force

$zipExeFiles = @(Get-ChildItem $tempDir -Recurse -Filter *.exe -ErrorAction SilentlyContinue)
foreach ($zipExe in $zipExeFiles) {
$signedExe = Get-ChildItem $buildDir -Recurse -Filter $zipExe.Name -ErrorAction SilentlyContinue | Select-Object -First 1
if ($signedExe) {
Copy-Item $signedExe.FullName $zipExe.FullName -Force
Write-Host " Replaced $($zipExe.Name) with signed binary."
} else {
Write-Host " No signed match found for $($zipExe.Name)."
}
}
run: pnpm exec electron-builder --config ${{ matrix.config }} --prepackaged build/${{ matrix.win_unpacked_dir }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

Remove-Item $zip.FullName -Force
Compress-Archive -Path (Join-Path $tempDir '*') -DestinationPath $zip.FullName -Force
}
- name: Sign Windows installers with Azure Trusted Signing
if: matrix.os == 'windows-latest'
uses: azure/artifact-signing-action@v1
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
files-folder: ${{ github.workspace }}/build
files-folder-filter: exe,msi
files-folder-recurse: false
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
description: Power Platform ToolBox
description-url: https://github.com/PowerPlatformToolBox/desktop-app

- name: Regenerate latest.yml with correct SHA256 hashes (Windows)
if: matrix.os == 'windows-latest'
Expand Down
31 changes: 26 additions & 5 deletions docs/azure-trusted-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,37 @@ This document explains how Windows artifacts generated by the production and nig

## Workflow behavior

- The jobs running on `windows-latest` in both `prod-release.yml` and `nightly-release.yml` execute the [`azure/artifact-signing-action@v1`](https://github.com/Azure/artifact-signing-action) step immediately after Electron Builder packages the installers.
- Every `.exe` and `.msi` produced under `build/` is signed in place. Portable `.zip` artifacts remain unsigned because the action does not yet support signing archives. The action handles hashing, timestamping, and certificate management by requesting a short-lived certificate from Azure Trusted Signing.
- The action fails if any secret is missing or Azure denies the signing request, which prevents unsigned installers from being uploaded or released.
The Windows signing process uses a **three-phase signing approach** to ensure every binary—both the application itself and the installer wrappers—carries a valid signature:

### Phase 1 – Sign application binaries

After building only the unpacked application directory (`build/win-unpacked/` for x64, `build/win-arm64-unpacked/` for arm64), the `azure/artifact-signing-action@v1` step signs all `.exe` and `.dll` files found recursively inside that directory. This includes:

- `Power Platform ToolBox.exe` (the main application executable)
- All `.dll` files bundled with the application

### Phase 2 – Package installers from signed binaries

With the application binaries now signed, Electron Builder is invoked with `--prepackaged` pointing at the already-signed directory. This creates the NSIS installer (`.exe`), MSI (`.msi`), MSI-wrapped EXE (`msiWrapped`), and portable ZIP, all of which embed the **signed** application binaries.

### Phase 3 – Sign installer packages

A second `azure/artifact-signing-action@v1` step signs the top-level installer files in `build/` (non-recursive) so that the outer wrappers are also authenticated:

- `Power-Platform-ToolBox-*-win.exe` (NSIS setup installer)
- `Power-Platform-ToolBox-*-win.msi` (MSI installer)
- `Power-Platform-ToolBox-*-Setup.exe` (msiWrapped EXE bootstrapper, x64 only)

Portable `.zip` archives are not signed as packages (the action does not support archive signing), but they contain the signed application binaries from Phase 1 because the ZIP is assembled from the `--prepackaged` directory.

The action fails if any secret is missing or Azure denies the signing request, which prevents unsigned installers from being uploaded or released.

## Testing the signing step

1. Configure the secrets above in the repository or organization settings.
2. Trigger the `Stable Release` or `Insider Pre-Release` workflow via the `workflow_dispatch` entry point (you can do this on a throwaway branch after updating the version and release notes).
3. Inspect the Windows job logs for the "Sign Windows artifacts with Azure Trusted Signing" step to verify that Azure returned a certificate and that all `.exe`/`.msi` files were processed.
4. Download the Windows artifacts and run `Get-AuthenticodeSignature` locally to validate the signature chain.
3. Inspect the Windows job logs for the "Sign application binaries with Azure Trusted Signing (Windows)" and "Sign Windows installers with Azure Trusted Signing" steps to verify that Azure returned a certificate and that all files were processed.
4. Download the Windows artifacts and run `Get-AuthenticodeSignature` locally to validate the signature chain on both the installer and the installed application executable.

## Operational guidance

Expand Down
Loading