ci: sign the Windows release executable with SignPath (#394)
SignPath's OSS tier requires every job leading up to a signing request to run on a GitHub-hosted agent, but the Windows release runs entirely on the self-hosted runner. Split the workflow instead of moving the whole build: - New `sign-exe` job on `windows-latest` builds only StemDeck.exe, uploads it as a workflow artifact, submits it to SignPath, and republishes the signed binary as an artifact. Version files are stamped before the build because SignPath restricts Foundation projects on PE product name and version. - `build-and-upload` now depends on it, downloads the signed executable, and packages both variants around it via a new `-PrebuiltExe` flag on make-portable.ps1. That also removes the redundant second Rust build the CPU package used to trigger. - make-portable.ps1 rejects an unsigned prebuilt binary before it reaches the zip, and the scan step reports the Authenticode status of what was packaged. - workflow_dispatch entry point plus a release-only guard on the upload step so the integration can be exercised against a test-signing policy without cutting a tag. Requires repo secret SIGNPATH_API_TOKEN and repo variable SIGNPATH_ORGANIZATION_ID. Adds the code signing policy and attribution required by the SignPath Foundation terms.
This commit is contained in:
@@ -3,6 +3,14 @@ name: Windows Release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
# Dry-run entry point: builds and signs without publishing anything, so the SignPath
|
||||
# integration can be exercised against the test-signing policy before a real tag is cut.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
signing_policy:
|
||||
description: SignPath signing policy slug to submit against
|
||||
required: false
|
||||
default: test-signing
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -10,33 +18,111 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Source the tag from the github context (evaluated by Actions) rather than
|
||||
# $env:GITHUB_REF_NAME, which is only injected by runner >= 2.290. Keeps the
|
||||
# build working on older self-hosted runners. (#212 follow-up)
|
||||
# workflow_dispatch runs off a branch, so there is no vX.Y.Z tag to derive a version from.
|
||||
PACKAGE_VERSION: ${{ github.event_name == 'release' && github.ref_name || '0.0.0-dev' }}
|
||||
|
||||
jobs:
|
||||
# SignPath's OSS tier requires every job leading up to a signing request to run on a
|
||||
# GitHub-hosted agent, so the executable is built here rather than on the self-hosted runner
|
||||
# that packages it. This job builds nothing but StemDeck.exe; the heavy Python/torch bundling
|
||||
# stays downstream.
|
||||
sign-exe:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # the SignPath action reads job details and downloads the artifact
|
||||
defaults:
|
||||
run:
|
||||
shell: powershell
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# Must run before the build: SignPath restricts Foundation projects on PE metadata, and
|
||||
# Tauri stamps ProductName/FileVersion into the executable from these files.
|
||||
- name: write version files
|
||||
run: |
|
||||
$version = $env:PACKAGE_VERSION -replace '^v', ''
|
||||
if (-not $version) { throw "PACKAGE_VERSION is not set" }
|
||||
(Get-Content "desktop/src-tauri/Cargo.toml") -replace '^version = ".*"', "version = `"$version`"" |
|
||||
Set-Content "desktop/src-tauri/Cargo.toml"
|
||||
(Get-Content "desktop/src-tauri/tauri.conf.json") -replace '"version": "[^"]*"', "`"version`": `"$version`"" |
|
||||
Set-Content "desktop/src-tauri/tauri.conf.json"
|
||||
Write-Host "Building StemDeck.exe version $version"
|
||||
|
||||
- name: build executable
|
||||
working-directory: desktop
|
||||
env:
|
||||
CI: "true" # Tauri only accepts true/false here
|
||||
run: |
|
||||
rustup default stable
|
||||
npm ci --include=dev
|
||||
node node_modules/@tauri-apps/cli/tauri.js build
|
||||
|
||||
# The SignPath connector only accepts artifacts stored on the GitHub server.
|
||||
- name: stage unsigned executable
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force unsigned | Out-Null
|
||||
Copy-Item -Force desktop/src-tauri/target/release/stemdeck.exe unsigned/
|
||||
|
||||
- name: upload unsigned executable
|
||||
id: upload-unsigned
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: stemdeck-exe-unsigned
|
||||
path: unsigned/stemdeck.exe
|
||||
|
||||
- name: submit signing request
|
||||
uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: stemdeck
|
||||
signing-policy-slug: ${{ github.event_name == 'release' && 'release-signing' || inputs.signing_policy }}
|
||||
github-artifact-id: ${{ steps.upload-unsigned.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
# Release signing needs a human approval in the SignPath console; the 600s default
|
||||
# expires long before an approver is likely to see the mail.
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: signed
|
||||
|
||||
- name: upload signed executable
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: stemdeck-exe-signed
|
||||
path: signed/stemdeck.exe
|
||||
|
||||
build-and-upload:
|
||||
needs: sign-exe
|
||||
# Runner must be windows/x64 with PowerShell, Docker, and rustup.
|
||||
runs-on: [self-hosted, windows, x64]
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
contents: write
|
||||
# Source the tag from the github context (evaluated by Actions) rather than
|
||||
# $env:GITHUB_REF_NAME, which is only injected by runner >= 2.290. Keeps the
|
||||
# build working on older self-hosted runners. (#212 follow-up)
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
defaults:
|
||||
run:
|
||||
shell: powershell
|
||||
steps:
|
||||
- name: clean workspace
|
||||
run: |
|
||||
Remove-Item -Recurse -Force .build, dist -ErrorAction SilentlyContinue
|
||||
Remove-Item -Recurse -Force .build, dist, signed -ErrorAction SilentlyContinue
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: download signed executable
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: stemdeck-exe-signed
|
||||
path: signed
|
||||
|
||||
- name: write version files
|
||||
run: |
|
||||
$tag = $env:REF_NAME
|
||||
if (-not $tag) { throw "REF_NAME is not set" }
|
||||
$version = $tag -replace '^v', ''
|
||||
$version = $env:PACKAGE_VERSION -replace '^v', ''
|
||||
if (-not $version) { throw "PACKAGE_VERSION is not set" }
|
||||
$json = "{`"version`": `"$version`"}"
|
||||
Set-Content -Path "static/version.json" -Value $json -Encoding UTF8
|
||||
(Get-Content "desktop/src-tauri/Cargo.toml") -replace '^version = ".*"', "version = `"$version`"" |
|
||||
@@ -49,18 +135,22 @@ jobs:
|
||||
Set-Content "desktop/package.json"
|
||||
Write-Host "Wrote version $version to all version files"
|
||||
|
||||
# Both variants package the one signed executable from sign-exe, so the Rust build is not
|
||||
# repeated here.
|
||||
- name: build Windows NVIDIA
|
||||
run: |
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/windows/make-portable.ps1 `
|
||||
-PackageName StemDeck-Windows-x64.NVIDIA `
|
||||
-PackageVersion "$env:REF_NAME" `
|
||||
-PackageVersion "$env:PACKAGE_VERSION" `
|
||||
-PrebuiltExe "$PWD\signed\stemdeck.exe" `
|
||||
-StripVenv
|
||||
|
||||
- name: build Windows CPU
|
||||
run: |
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/windows/make-portable.ps1 `
|
||||
-PackageName StemDeck-Windows-x64 `
|
||||
-PackageVersion "$env:REF_NAME" `
|
||||
-PackageVersion "$env:PACKAGE_VERSION" `
|
||||
-PrebuiltExe "$PWD\signed\stemdeck.exe" `
|
||||
-CpuOnly `
|
||||
-StripVenv
|
||||
|
||||
@@ -82,6 +172,13 @@ jobs:
|
||||
Write-Host " $($hash.Hash) $($_.Name)"
|
||||
}
|
||||
|
||||
Write-Host "Authenticode signatures on the packaged executables:"
|
||||
Get-ChildItem -Path "dist" -Filter "StemDeck.exe" -Recurse -File |
|
||||
ForEach-Object {
|
||||
$sig = Get-AuthenticodeSignature -LiteralPath $_.FullName
|
||||
Write-Host " $($_.FullName): $($sig.Status) $($sig.SignerCertificate.Subject)"
|
||||
}
|
||||
|
||||
Write-Host "Pulling latest ClamAV scanner image..."
|
||||
docker pull clamav/clamav:latest
|
||||
|
||||
@@ -94,6 +191,7 @@ jobs:
|
||||
Write-Host "ClamAV scan completed successfully. No infected files reported."
|
||||
|
||||
- name: upload artifacts
|
||||
if: github.event_name == 'release'
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
with:
|
||||
files: |
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Build policy enforced by SignPath on every signing request submitted against the
|
||||
# "release-signing" policy of the "stemdeck" project.
|
||||
# Reference: https://docs.signpath.io/trusted-build-systems/github
|
||||
github-policies:
|
||||
build:
|
||||
# A failed build must not be re-run just to obtain a signature.
|
||||
disallow_reruns: true
|
||||
# Branch rulesets are intentionally left out until the repository has matching rulesets
|
||||
# configured; SignPath rejects requests whose repository does not satisfy them.
|
||||
@@ -391,6 +391,39 @@ The author(s) of StemDeck provide this software "as is", without warranty of any
|
||||
|
||||
---
|
||||
|
||||
## Code signing policy
|
||||
|
||||
Windows releases of StemDeck are Authenticode-signed. Free code signing is provided by
|
||||
[SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
|
||||
|
||||
The signed binary is `StemDeck.exe` inside `StemDeck-Windows-x64.zip` and
|
||||
`StemDeck-Windows-x64.NVIDIA.zip`. The zip archives themselves are not signed; verify them with
|
||||
the `.sha256` file published alongside each release asset. macOS and Linux builds are not
|
||||
currently signed.
|
||||
|
||||
**Team roles**
|
||||
|
||||
| Role | Members |
|
||||
|---|---|
|
||||
| Committers | Thales Pereira ([@thcp](https://github.com/thcp)) |
|
||||
| Reviewers | Thales Pereira ([@thcp](https://github.com/thcp)) |
|
||||
| Approvers | Thales Pereira ([@thcp](https://github.com/thcp)) |
|
||||
|
||||
**How signing works**
|
||||
|
||||
The executable is built from this repository by the
|
||||
[Windows Release workflow](.github/workflows/windows-release.yml) on a GitHub-hosted runner,
|
||||
uploaded as a workflow artifact, and submitted to SignPath for signing. SignPath verifies the
|
||||
build's origin (repository, branch, commit, and build job) before the certificate is applied.
|
||||
Every release signing request is approved manually by an approver listed above.
|
||||
|
||||
**Privacy**
|
||||
|
||||
StemDeck does not transmit any personal data. All audio processing happens on the user's own
|
||||
machine. See the [Disclaimer](#disclaimer) for details.
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
| Platform | Link |
|
||||
|
||||
@@ -8,6 +8,13 @@ Run
|
||||
2. Double-click StemDeck.exe.
|
||||
3. Let first-run setup prepare local runtime assets.
|
||||
|
||||
Code signing
|
||||
------------
|
||||
|
||||
StemDeck.exe is Authenticode-signed. Free code signing provided by SignPath.io,
|
||||
certificate by SignPath Foundation. The zip itself is not signed; verify it with
|
||||
the .sha256 file published next to it on the release page.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ param(
|
||||
[string]$PackageName = "StemDeck-Windows-x64",
|
||||
[string]$PackageVersion,
|
||||
[switch]$SkipTauriBuild,
|
||||
[string]$PrebuiltExe,
|
||||
[switch]$CpuOnly,
|
||||
[switch]$StripVenv
|
||||
)
|
||||
@@ -27,6 +28,19 @@ $DesktopDir = Join-Path $Root "desktop"
|
||||
$TauriDir = Join-Path $DesktopDir "src-tauri"
|
||||
$TargetExe = Join-Path $TauriDir "target\$Configuration\stemdeck.exe"
|
||||
|
||||
# -PrebuiltExe packages an executable produced (and signed) by an earlier job instead of
|
||||
# building one here. The release pipeline uses it so both the CPU and NVIDIA packages ship the
|
||||
# one SignPath-signed binary rather than rebuilding Rust twice.
|
||||
if ($PrebuiltExe) {
|
||||
if ($SkipTauriBuild) {
|
||||
throw "-PrebuiltExe and -SkipTauriBuild are mutually exclusive."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $PrebuiltExe)) {
|
||||
throw "Prebuilt executable not found: $PrebuiltExe"
|
||||
}
|
||||
$TargetExe = (Resolve-Path -LiteralPath $PrebuiltExe).Path
|
||||
}
|
||||
|
||||
function Require-Command([string]$Name) {
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Required command not found on PATH: $Name"
|
||||
@@ -144,9 +158,11 @@ Remove -SkipTauriBuild or run the NVIDIA package build first so the CPU package
|
||||
}
|
||||
}
|
||||
|
||||
Require-Command "node"
|
||||
Require-Command "npm"
|
||||
Require-Command "cargo"
|
||||
if (-not $PrebuiltExe) {
|
||||
Require-Command "node"
|
||||
Require-Command "npm"
|
||||
Require-Command "cargo"
|
||||
}
|
||||
|
||||
if (-not (Get-Command "py" -ErrorAction SilentlyContinue) -and -not (Get-Command "python" -ErrorAction SilentlyContinue)) {
|
||||
throw "Python launcher not found. Install Python 3.12 on the Windows build agent."
|
||||
@@ -237,29 +253,47 @@ if ($StripVenv) {
|
||||
Remove-Item -Force
|
||||
}
|
||||
|
||||
Push-Location $DesktopDir
|
||||
try {
|
||||
if (Test-Path "package-lock.json") {
|
||||
npm ci --include=dev
|
||||
} else {
|
||||
npm install --include=dev
|
||||
}
|
||||
if (-not $PrebuiltExe) {
|
||||
Push-Location $DesktopDir
|
||||
try {
|
||||
if (Test-Path "package-lock.json") {
|
||||
npm ci --include=dev
|
||||
} else {
|
||||
npm install --include=dev
|
||||
}
|
||||
|
||||
if (-not $SkipTauriBuild) {
|
||||
$env:CI = "true" # Woodpecker sets CI=woodpecker; Tauri only accepts true/false
|
||||
rustup default stable
|
||||
Invoke-TauriBuild
|
||||
} else {
|
||||
Assert-Fresh-TauriBuild
|
||||
if (-not $SkipTauriBuild) {
|
||||
$env:CI = "true" # Woodpecker sets CI=woodpecker; Tauri only accepts true/false
|
||||
rustup default stable
|
||||
Invoke-TauriBuild
|
||||
} else {
|
||||
Assert-Fresh-TauriBuild
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
if (-not (Test-Path $TargetExe)) {
|
||||
throw "Tauri executable not found at $TargetExe"
|
||||
}
|
||||
|
||||
if ($PrebuiltExe) {
|
||||
# Fail loudly if the signing job handed back an unsigned binary. A test-signing policy uses a
|
||||
# self-signed certificate, so anything other than NotSigned is accepted here and only warned
|
||||
# about; the release certificate yields Valid.
|
||||
$Signature = Get-AuthenticodeSignature -LiteralPath $TargetExe
|
||||
if ($Signature.Status -eq "NotSigned") {
|
||||
throw "Prebuilt executable carries no Authenticode signature: $TargetExe"
|
||||
}
|
||||
if ($Signature.Status -ne "Valid") {
|
||||
Write-Warning "Authenticode status is $($Signature.Status) (expected for a test-signing certificate)."
|
||||
}
|
||||
if ($Signature.SignerCertificate) {
|
||||
Write-Host "Signed by : $($Signature.SignerCertificate.Subject)"
|
||||
}
|
||||
}
|
||||
|
||||
Copy-Item -Force $TargetExe (Join-Path $Stage "StemDeck.exe")
|
||||
|
||||
Compress-Archive -Path (Join-Path $Stage "*") -DestinationPath $ZipPath -Force
|
||||
|
||||
Reference in New Issue
Block a user