Skip to content

Commit 31ecde8

Browse files
authored
Merge pull request #37 from nefarius/release-job
Adds release helper scripts
2 parents 9ab9ebf + f5b945c commit 31ecde8

File tree

3 files changed

+310
-0
lines changed

3 files changed

+310
-0
lines changed

.github/workflows/release.yml

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
8+
permissions:
9+
contents: write
10+
11+
env:
12+
SOLUTION_FILE_PATH: Injector.sln
13+
BUILD_CONFIGURATION: Release
14+
15+
jobs:
16+
build:
17+
runs-on: windows-latest
18+
strategy:
19+
fail-fast: false
20+
matrix:
21+
platform: [Win32, x64, ARM64]
22+
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Add MSBuild to PATH
28+
uses: microsoft/setup-msbuild@v2
29+
30+
- name: Build
31+
run: msbuild /m /p:Configuration="${{ env.BUILD_CONFIGURATION }}" /p:Platform=${{ matrix.platform }} ${{ env.SOLUTION_FILE_PATH }}
32+
33+
- name: Stage binary artifact
34+
shell: pwsh
35+
run: |
36+
$source = Join-Path "${{ github.workspace }}" "bin/${{ matrix.platform }}/Injector.exe"
37+
if (!(Test-Path $source)) {
38+
throw "Expected binary not found: $source"
39+
}
40+
41+
$targetDir = Join-Path "${{ github.workspace }}" "artifacts/${{ matrix.platform }}"
42+
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
43+
Copy-Item -Path $source -Destination (Join-Path $targetDir "Injector.exe") -Force
44+
45+
- name: Upload platform artifact
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: Injector-${{ matrix.platform }}
49+
path: artifacts/${{ matrix.platform }}/Injector.exe
50+
if-no-files-found: error
51+
retention-days: 1
52+
53+
package-and-release:
54+
runs-on: windows-latest
55+
needs: build
56+
57+
steps:
58+
- name: Download Win32 artifact
59+
uses: actions/download-artifact@v4
60+
with:
61+
name: Injector-Win32
62+
path: downloaded/Win32
63+
64+
- name: Download x64 artifact
65+
uses: actions/download-artifact@v4
66+
with:
67+
name: Injector-x64
68+
path: downloaded/x64
69+
70+
- name: Download ARM64 artifact
71+
uses: actions/download-artifact@v4
72+
with:
73+
name: Injector-ARM64
74+
path: downloaded/ARM64
75+
76+
- name: Assemble zip structure
77+
shell: pwsh
78+
run: |
79+
$releaseRoot = Join-Path "${{ github.workspace }}" "release-content"
80+
$zipPath = Join-Path "${{ github.workspace }}" "Injector_x86_amd64_arm64_unsigned.zip"
81+
82+
if (Test-Path $releaseRoot) {
83+
Remove-Item -Path $releaseRoot -Recurse -Force
84+
}
85+
if (Test-Path $zipPath) {
86+
Remove-Item -Path $zipPath -Force
87+
}
88+
89+
$arm64Dir = Join-Path $releaseRoot "ARM64"
90+
$win32Dir = Join-Path $releaseRoot "Win32"
91+
$x64Dir = Join-Path $releaseRoot "x64"
92+
New-Item -ItemType Directory -Force -Path $arm64Dir, $win32Dir, $x64Dir | Out-Null
93+
94+
Copy-Item -Path "downloaded/ARM64/Injector.exe" -Destination (Join-Path $arm64Dir "Injector.exe") -Force
95+
Copy-Item -Path "downloaded/Win32/Injector.exe" -Destination (Join-Path $win32Dir "Injector.exe") -Force
96+
Copy-Item -Path "downloaded/x64/Injector.exe" -Destination (Join-Path $x64Dir "Injector.exe") -Force
97+
98+
Compress-Archive -Path (Join-Path $releaseRoot "*") -DestinationPath $zipPath
99+
100+
- name: Upload unsigned release bundle artifact
101+
uses: actions/upload-artifact@v4
102+
with:
103+
name: unsigned-release-bundle-${{ github.ref_name }}
104+
path: Injector_x86_amd64_arm64_unsigned.zip
105+
if-no-files-found: error
106+
retention-days: 14
107+
108+
- name: Create draft release awaiting EV signing
109+
uses: softprops/action-gh-release@v2
110+
with:
111+
draft: true
112+
body: |
113+
Release draft created automatically.
114+
Final asset must be EV-signed on the maintainer local machine.
115+
116+
Expected final asset name:
117+
- Injector_x86_amd64_arm64.zip
118+
generate_release_notes: true

RELEASE.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Release process
2+
3+
## Overview
4+
5+
Pushing a tag like `v1.5.0` triggers `.github/workflows/release.yml`, which:
6+
7+
- Builds `Injector.exe` for `Win32`, `x64`, and `ARM64`
8+
- Creates an unsigned bundle artifact: `Injector_x86_amd64_arm64_unsigned.zip`
9+
- Creates a draft GitHub release for the tag
10+
11+
Final signing and publish are performed locally on the EV-capable machine.
12+
13+
## Prerequisites
14+
15+
1. `gh` CLI installed and authenticated (`gh auth status`)
16+
2. [`wdkwhere`](https://github.com/nefarius/wdkwhere) installed and available in `PATH`
17+
3. EV token/certificate available and unlocked
18+
19+
## Finalize a tagged release
20+
21+
Run from repository root:
22+
23+
```powershell
24+
.\scripts\finalize-release.ps1 -Tag v1.5.0 -CertificateSubjectName "Nefarius Software Solutions e.U."
25+
```
26+
27+
The script will:
28+
29+
- Download `unsigned-release-bundle-v1.5.0` automatically (unless `-UnsignedZipPath` is provided)
30+
- Sign:
31+
- `ARM64/Injector.exe`
32+
- `Win32/Injector.exe`
33+
- `x64/Injector.exe`
34+
- Create `Injector_x86_amd64_arm64.zip`
35+
- Upload it to the draft release and publish it
36+
37+
## Useful options
38+
39+
```powershell
40+
# Upload signed zip but keep release as draft
41+
.\scripts\finalize-release.ps1 -Tag v1.5.0 -CertificateSubjectName "Nefarius Software Solutions e.U." -NoPublish
42+
43+
# Use a manually downloaded unsigned zip
44+
.\scripts\finalize-release.ps1 -Tag v1.5.0 -CertificateSubjectName "Nefarius Software Solutions e.U." -UnsignedZipPath "C:\Temp\Injector_x86_amd64_arm64_unsigned.zip"
45+
```

scripts/finalize-release.ps1

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
param(
2+
[Parameter(Mandatory = $true)]
3+
[ValidatePattern("^v\d+\.\d+\.\d+$")]
4+
[string]$Tag,
5+
6+
[Parameter(Mandatory = $true)]
7+
[string]$CertificateSubjectName,
8+
9+
[string]$TimestampUrl = "http://timestamp.digicert.com",
10+
[string]$UnsignedZipPath,
11+
[string]$WorkspaceRoot = (Join-Path $PSScriptRoot ".."),
12+
[string]$OutputDir = (Join-Path $PSScriptRoot "../.release-local"),
13+
[switch]$NoPublish
14+
)
15+
16+
Set-StrictMode -Version Latest
17+
$ErrorActionPreference = "Stop"
18+
19+
function Ensure-WdkWhere {
20+
$command = Get-Command wdkwhere -ErrorAction SilentlyContinue
21+
if (!$command) {
22+
throw "wdkwhere was not found in PATH. Install it first (dotnet tool install --global Nefarius.Tools.WDKWhere)."
23+
}
24+
25+
return $command.Source
26+
}
27+
28+
function Resolve-UnsignedZip {
29+
param(
30+
[string]$TagValue,
31+
[string]$ExplicitZipPath,
32+
[string]$DestinationDir
33+
)
34+
35+
if ($ExplicitZipPath) {
36+
if (!(Test-Path $ExplicitZipPath)) {
37+
throw "Unsigned zip path not found: $ExplicitZipPath"
38+
}
39+
40+
return (Resolve-Path $ExplicitZipPath).Path
41+
}
42+
43+
$workflowName = "release.yml"
44+
$artifactName = "unsigned-release-bundle-$TagValue"
45+
$runRows = gh run list --workflow $workflowName --limit 100 --json databaseId,headBranch,displayTitle,status,conclusion,event | ConvertFrom-Json
46+
if (!$runRows) {
47+
throw "No workflow runs found for '$workflowName'."
48+
}
49+
50+
$run = $runRows |
51+
Where-Object {
52+
$_.event -eq "push" -and
53+
$_.status -eq "completed" -and
54+
$_.conclusion -eq "success" -and
55+
($_.headBranch -eq $TagValue -or $_.displayTitle -eq $TagValue)
56+
} |
57+
Select-Object -First 1
58+
59+
if (!$run) {
60+
throw "No successful '$workflowName' run found for tag '$TagValue'."
61+
}
62+
63+
$downloadDir = Join-Path $DestinationDir "downloaded"
64+
New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null
65+
66+
gh run download $run.databaseId -n $artifactName -D $downloadDir | Out-Null
67+
68+
$zip = Get-ChildItem -Path $downloadDir -Filter "Injector_x86_amd64_arm64_unsigned.zip" -File -Recurse | Select-Object -First 1
69+
if (!$zip) {
70+
throw "Downloaded artifact '$artifactName' did not contain Injector_x86_amd64_arm64_unsigned.zip."
71+
}
72+
73+
return $zip.FullName
74+
}
75+
76+
function Sign-Binary {
77+
param(
78+
[string]$WdkWherePath,
79+
[string]$CertSubjectName,
80+
[string]$Timestamp,
81+
[string]$FilePath
82+
)
83+
84+
if (!(Test-Path $FilePath)) {
85+
throw "Expected binary missing: $FilePath"
86+
}
87+
88+
& $WdkWherePath run signtool sign /n $CertSubjectName /a /fd SHA256 /td SHA256 /tr $Timestamp $FilePath
89+
if ($LASTEXITCODE -ne 0) {
90+
throw "signtool failed for '$FilePath' with exit code $LASTEXITCODE."
91+
}
92+
}
93+
94+
Push-Location $WorkspaceRoot
95+
try {
96+
# Validate GH auth early because this script relies on release + artifact APIs.
97+
gh auth status | Out-Null
98+
99+
$wdkWhere = Ensure-WdkWhere
100+
Write-Host "Using wdkwhere: $wdkWhere"
101+
102+
$resolvedOutputDir = Resolve-Path (New-Item -ItemType Directory -Path $OutputDir -Force)
103+
$workRoot = Join-Path $resolvedOutputDir ".work-$Tag"
104+
if (Test-Path $workRoot) {
105+
Remove-Item -Path $workRoot -Recurse -Force
106+
}
107+
New-Item -ItemType Directory -Path $workRoot -Force | Out-Null
108+
109+
$unsignedZip = Resolve-UnsignedZip -TagValue $Tag -ExplicitZipPath $UnsignedZipPath -DestinationDir $workRoot
110+
Write-Host "Using unsigned zip: $unsignedZip"
111+
112+
$unsignedExtract = Join-Path $workRoot "unsigned"
113+
Expand-Archive -Path $unsignedZip -DestinationPath $unsignedExtract -Force
114+
115+
$targets = @(
116+
(Join-Path $unsignedExtract "ARM64/Injector.exe"),
117+
(Join-Path $unsignedExtract "Win32/Injector.exe"),
118+
(Join-Path $unsignedExtract "x64/Injector.exe")
119+
)
120+
121+
foreach ($file in $targets) {
122+
Write-Host "Signing $file"
123+
Sign-Binary -WdkWherePath $wdkWhere -CertSubjectName $CertificateSubjectName -Timestamp $TimestampUrl -FilePath $file
124+
}
125+
126+
$finalZip = Join-Path $resolvedOutputDir "Injector_x86_amd64_arm64.zip"
127+
if (Test-Path $finalZip) {
128+
Remove-Item -Path $finalZip -Force
129+
}
130+
Compress-Archive -Path (Join-Path $unsignedExtract "*") -DestinationPath $finalZip
131+
Write-Host "Created signed zip: $finalZip"
132+
133+
gh release view $Tag --json tagName,isDraft | Out-Null
134+
gh release upload $Tag $finalZip --clobber | Out-Null
135+
Write-Host "Uploaded asset to release '$Tag'."
136+
137+
if (-not $NoPublish) {
138+
gh release edit $Tag --draft=false | Out-Null
139+
Write-Host "Published release '$Tag'."
140+
}
141+
else {
142+
Write-Host "Draft release left unpublished due to -NoPublish."
143+
}
144+
}
145+
finally {
146+
Pop-Location
147+
}

0 commit comments

Comments
 (0)