Skip to content

Commit 9abdd9c

Browse files
authored
Sign Windows and macOS binaries and installers (#268)
* Sign releases and include ggsql-jupyter * Add name for build.yaml * Tweak signing script * Upload paths * Update Packager.toml * Fixup packager config * Add macOS signing and notorisation env vars * Configure apple sign identity in src/Cargo.toml
1 parent 48489f7 commit 9abdd9c

4 files changed

Lines changed: 204 additions & 10 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
name: "Signing file"
2+
description: "Install and configure the environment for signing of files."
3+
inputs:
4+
paths:
5+
description: "Paths to sign"
6+
required: true
7+
signtools-extra-args:
8+
description: "Additional arguments to pass to signtool"
9+
outputs:
10+
cert_path:
11+
description: "certificate path"
12+
value: ${{ steps.setup-cert.outputs.SM_CLIENT_CERT_FILE }}
13+
14+
runs:
15+
using: "composite"
16+
steps:
17+
- name: Setup for SMCTL authentication
18+
id: setup-cert
19+
shell: pwsh
20+
run: |
21+
Write-Output "::group::Check for required environment variable"
22+
if (-not $env:SM_CLIENT_CERT_FILE_B64) {
23+
Write-Output "::error title=Environment Variable Error::SM_CLIENT_CERT_FILE_B64 is not set"
24+
exit 1
25+
} else {
26+
Write-Output "SM_CLIENT_CERT_FILE_B64 is set correctly"
27+
}
28+
Write-Output "::endgroup::"
29+
Write-Output "::group::Retrieve client certificate for auth"
30+
if (!(Test-Path ".\.build\certificates\codesign.pfx")) {
31+
# Get certificates
32+
New-Item -ItemType Directory -Force -Path .\.build\certificates
33+
Set-Content -Path ".\.build\certificates\codesign.txt" -Value $env:SM_CLIENT_CERT_FILE_B64
34+
& certutil -decode ".\.build\certificates\codesign.txt" ".\.build\certificates\codesign.pfx"
35+
} else {
36+
Write-Output "Certificate already exists"
37+
}
38+
# Configure environment for next step
39+
"SM_CLIENT_CERT_FILE=.\.build\certificates\codesign.pfx" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
40+
Write-Output "::endgroup::"
41+
42+
- name: Install SMCTL
43+
shell: pwsh
44+
run: |
45+
Write-Output "::group::Install smctl if needed"
46+
if (!(Get-Command smctl -ErrorAction SilentlyContinue)) {
47+
# Download with retry (transient S3 failures cause silent install failures)
48+
$maxRetries = 3
49+
$downloaded = $false
50+
for ($i = 1; $i -le $maxRetries; $i++) {
51+
Write-Output "Downloading smtools MSI (attempt $i/$maxRetries)..."
52+
curl -o smtools-windows-x64.msi "https://rstudio-buildtools.s3.amazonaws.com/posit-dev/smtools-windows-x64.msi"
53+
if ($LASTEXITCODE -ne 0) {
54+
Write-Output "::warning::curl failed with exit code $LASTEXITCODE"
55+
continue
56+
}
57+
$fileSize = (Get-Item smtools-windows-x64.msi).Length
58+
if ($fileSize -lt 1MB) {
59+
Write-Output "::warning::Downloaded file is only $fileSize bytes, expected ~90MB"
60+
continue
61+
}
62+
$downloaded = $true
63+
Write-Output "Download successful ($fileSize bytes)"
64+
break
65+
}
66+
if (-not $downloaded) {
67+
Write-Output "::error title=Download Error::Failed to download smtools MSI after $maxRetries attempts"
68+
exit 1
69+
}
70+
# Install synchronously (msiexec can return before install completes without -Wait)
71+
$process = Start-Process msiexec -ArgumentList '/i', 'smtools-windows-x64.msi', '/quiet', '/qn', '/log', 'smtools-windows-x64.log' -Wait -PassThru
72+
if ($process.ExitCode -ne 0) {
73+
Write-Output "::error title=Install Error::msiexec failed with exit code $($process.ExitCode)"
74+
if (Test-Path smtools-windows-x64.log) { Get-Content smtools-windows-x64.log -Tail 50 }
75+
exit 1
76+
}
77+
# Verify smctl is actually on disk before declaring success
78+
$smctlPath = "C:/Program Files/DigiCert/DigiCert One Signing Manager Tools"
79+
if (!(Test-Path "$smctlPath/smctl.exe")) {
80+
Write-Output "::error title=Install Error::smctl.exe not found at $smctlPath after install"
81+
exit 1
82+
}
83+
$smctlPath | Out-File -FilePath $env:GITHUB_PATH -Append
84+
Write-Output "SMCTL installed and added on PATH"
85+
} else {
86+
Write-Output "SMCTL already installed and on PATH"
87+
}
88+
Write-Output "::endgroup::"
89+
Write-Output "::group::Add signtools in PATH"
90+
if (!(Get-Command signtool -ErrorAction SilentlyContinue)) {
91+
"C:/Program Files (x86)/Windows Kits/10/App Certification Kit" | Out-File -FilePath $env:GITHUB_PATH -Append
92+
Write-Output "signtool added on PATH"
93+
} else {
94+
Write-Output "signtool already installed and on PATH"
95+
}
96+
Write-Output "::endgroup::"
97+
98+
- name: Sign files with signtool
99+
shell: pwsh
100+
env:
101+
SM_CLIENT_CERT_FILE: ${{ steps.setup-cert.outputs.SM_CLIENT_CERT_FILE }}
102+
run: |
103+
Write-Output "::group::Check for required environment variables"
104+
$requiredEnvVars = @('SM_HOST', 'SM_API_KEY', 'SM_CLIENT_CERT_FILE', 'SM_CLIENT_CERT_PASSWORD', 'SM_CLIENT_CERT_FINGERPRINT')
105+
foreach ($envVar in $requiredEnvVars) {
106+
if (-not $(Get-Item -Path "Env:$envVar" -ErrorAction SilentlyContinue)) {
107+
Write-Output "::error title=Missing environment variable::Environment variable $envVar is not set."
108+
exit 1
109+
}
110+
Write-Output "All env var correctly set."
111+
}
112+
Write-Output "::endgroup::"
113+
Write-Output "::group::Sync certificates"
114+
smctl windows certsync
115+
Write-Output "::endgroup::"
116+
# Sign each file that will be bundled in the installer
117+
$rawPaths = "${{ inputs.paths }}" -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
118+
$paths = @()
119+
foreach ($raw in $rawPaths) {
120+
$resolved = Resolve-Path -Path $raw -ErrorAction SilentlyContinue
121+
if (-not $resolved) {
122+
Write-Output "::error title=Signing error::No files matched pattern: ${raw}"
123+
exit 1
124+
}
125+
$paths += $resolved.Path
126+
}
127+
foreach ($path in $paths) {
128+
Write-Output "::group::Signing ${path}"
129+
signtool.exe sign /sha1 $env:SM_CLIENT_CERT_FINGERPRINT /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ${{ inputs.signtools-extra-args }} $path
130+
if ($LASTEXITCODE -ne 0) {
131+
Write-Output "::error title=Signing error::Error while signing ${path}"
132+
exit 1
133+
}
134+
signtool.exe verify /v /pa $path
135+
if ($LASTEXITCODE -ne 0) {
136+
Write-Output "::error title=Verify signature error::Error while verifying ${path}"
137+
exit 1
138+
}
139+
Write-Output "::endgroup::"
140+
}

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: build-test.yaml
1+
name: Build and Test
22

33
on:
44
push:

.github/workflows/release-packages.yml

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,42 @@ jobs:
3535
run: cargo install cargo-packager --locked
3636

3737
- name: Build ggsql binary
38-
run: cargo build --release --package ggsql --bin ggsql
38+
run: cargo build --release --bin ggsql --bin ggsql-jupyter
39+
40+
- name: Sign files before making NSIS and MSI installer
41+
id: sign-files
42+
uses: ./.github/workflows/actions/sign-files
43+
with:
44+
paths: |
45+
./target/release/ggsql.exe
46+
./target/release/ggsql-jupyter.exe
47+
env:
48+
# environment variables required to sign with signtool
49+
SM_HOST: ${{ secrets.SM_HOST }}
50+
SM_API_KEY: ${{ secrets.SM_API_KEY }}
51+
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
52+
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
53+
SM_CLIENT_CERT_FINGERPRINT: ${{ secrets.SM_CLIENT_CERT_FINGERPRINT }}
3954

4055
- name: Build NSIS installer
4156
run: cargo packager --release --formats nsis
42-
working-directory: src
4357

4458
- name: Build MSI installer
4559
run: cargo packager --release --formats wix
46-
working-directory: src
60+
61+
- name: Sign installers
62+
uses: ./.github/workflows/actions/sign-files
63+
with:
64+
paths: |
65+
./src/target/release/packager/*.exe
66+
./src/target/release/packager/*.msi
67+
env:
68+
# environment variables required to sign with signtool
69+
SM_HOST: ${{ secrets.SM_HOST }}
70+
SM_API_KEY: ${{ secrets.SM_API_KEY }}
71+
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
72+
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
73+
SM_CLIENT_CERT_FINGERPRINT: ${{ secrets.SM_CLIENT_CERT_FINGERPRINT }}
4774

4875
- name: Upload NSIS installer
4976
uses: actions/upload-artifact@v4
@@ -80,22 +107,49 @@ jobs:
80107
with:
81108
targets: x86_64-apple-darwin, aarch64-apple-darwin
82109

110+
- name: Set up Apple notarization key
111+
run: |
112+
mkdir -p ~/.private_keys
113+
echo -n "$APPLE_API_KEY_BASE64" | base64 --decode -o ~/.private_keys/AuthKey_${APPLE_API_KEY}.p8
114+
chmod 600 ~/.private_keys/AuthKey_${APPLE_API_KEY}.p8
115+
env:
116+
APPLE_API_KEY_BASE64: ${{ secrets.GWS_APPLE_API_KEY_BASE64 }}
117+
APPLE_API_KEY: ${{ secrets.GWS_APPLE_API_KEY }}
118+
119+
- name: Configure macOS installer signing
120+
env:
121+
APPLE_SIGN_IDENTITY: ${{ secrets.GWS_APPLE_SIGN_IDENTITY }}
122+
run: |
123+
cat <<EOF >> src/Cargo.toml
124+
125+
[package.metadata.packager.macos]
126+
signing-identity = "${APPLE_SIGN_IDENTITY}"
127+
EOF
128+
83129
- name: Install cargo-packager
84130
run: cargo install cargo-packager --locked
85131

86132
- name: Build ggsql binary (x86_64)
87-
run: cargo build --release --package ggsql --bin ggsql
133+
run: cargo build --release --bin ggsql --bin ggsql-jupyter
88134

89135
- name: Build DMG installer (x86_64)
90136
run: cargo packager --release --formats dmg
91-
working-directory: src
137+
env:
138+
APPLE_CERTIFICATE: ${{ secrets.GWS_APPLE_SIGN_P12 }}
139+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.GWS_APPLE_SIGN_PW }}
140+
APPLE_API_KEY: ${{ secrets.GWS_APPLE_API_KEY }}
141+
APPLE_API_ISSUER: ${{ secrets.GWS_APPLE_API_ISSUER }}
92142

93143
- name: Build ggsql binary (aarch64)
94-
run: cargo build --release --package ggsql --bin ggsql --target aarch64-apple-darwin
144+
run: cargo build --release --bin ggsql --bin ggsql-jupyter --target aarch64-apple-darwin
95145

96146
- name: Build DMG installer (aarch64)
97147
run: cargo packager --release --target aarch64-apple-darwin --formats dmg
98-
working-directory: src
148+
env:
149+
APPLE_CERTIFICATE: ${{ secrets.GWS_APPLE_SIGN_P12 }}
150+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.GWS_APPLE_SIGN_PW }}
151+
APPLE_API_KEY: ${{ secrets.GWS_APPLE_API_KEY }}
152+
APPLE_API_ISSUER: ${{ secrets.GWS_APPLE_API_ISSUER }}
99153

100154
- name: Upload DMG installers
101155
uses: actions/upload-artifact@v4
@@ -139,11 +193,10 @@ jobs:
139193
run: cargo install cargo-packager --locked
140194

141195
- name: Build ggsql binary
142-
run: cargo build --release --package ggsql --bin ggsql
196+
run: cargo build --release --bin ggsql --bin ggsql-jupyter
143197

144198
- name: Build Debian package
145199
run: cargo packager --release --formats deb
146-
working-directory: src
147200

148201
- name: Upload Debian package
149202
uses: actions/upload-artifact@v4

src/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ copyright = "Copyright (c) 2026 ggsql Team"
108108
# Binaries to include in the package
109109
binaries = [
110110
{ path = "ggsql", main = true },
111+
{ path = "ggsql-jupyter", main = false },
111112
]
112113

113114
# Resources to bundle (optional)

0 commit comments

Comments
 (0)