Skip to content
Merged
140 changes: 140 additions & 0 deletions .github/workflows/actions/sign-files/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: "Signing file"
description: "Install and configure the environment for signing of files."
inputs:
paths:
description: "Paths to sign"
required: true
signtools-extra-args:
description: "Additional arguments to pass to signtool"
outputs:
cert_path:
description: "certificate path"
value: ${{ steps.setup-cert.outputs.SM_CLIENT_CERT_FILE }}

runs:
using: "composite"
steps:
- name: Setup for SMCTL authentication
id: setup-cert
shell: pwsh
run: |
Write-Output "::group::Check for required environment variable"
if (-not $env:SM_CLIENT_CERT_FILE_B64) {
Write-Output "::error title=Environment Variable Error::SM_CLIENT_CERT_FILE_B64 is not set"
exit 1
} else {
Write-Output "SM_CLIENT_CERT_FILE_B64 is set correctly"
}
Write-Output "::endgroup::"
Write-Output "::group::Retrieve client certificate for auth"
if (!(Test-Path ".\.build\certificates\codesign.pfx")) {
# Get certificates
New-Item -ItemType Directory -Force -Path .\.build\certificates
Set-Content -Path ".\.build\certificates\codesign.txt" -Value $env:SM_CLIENT_CERT_FILE_B64
& certutil -decode ".\.build\certificates\codesign.txt" ".\.build\certificates\codesign.pfx"
} else {
Write-Output "Certificate already exists"
}
# Configure environment for next step
"SM_CLIENT_CERT_FILE=.\.build\certificates\codesign.pfx" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Output "::endgroup::"

- name: Install SMCTL
shell: pwsh
run: |
Write-Output "::group::Install smctl if needed"
if (!(Get-Command smctl -ErrorAction SilentlyContinue)) {
# Download with retry (transient S3 failures cause silent install failures)
$maxRetries = 3
$downloaded = $false
for ($i = 1; $i -le $maxRetries; $i++) {
Write-Output "Downloading smtools MSI (attempt $i/$maxRetries)..."
curl -o smtools-windows-x64.msi "https://rstudio-buildtools.s3.amazonaws.com/posit-dev/smtools-windows-x64.msi"
if ($LASTEXITCODE -ne 0) {
Write-Output "::warning::curl failed with exit code $LASTEXITCODE"
continue
}
$fileSize = (Get-Item smtools-windows-x64.msi).Length
if ($fileSize -lt 1MB) {
Write-Output "::warning::Downloaded file is only $fileSize bytes, expected ~90MB"
continue
}
$downloaded = $true
Write-Output "Download successful ($fileSize bytes)"
break
}
if (-not $downloaded) {
Write-Output "::error title=Download Error::Failed to download smtools MSI after $maxRetries attempts"
exit 1
}
# Install synchronously (msiexec can return before install completes without -Wait)
$process = Start-Process msiexec -ArgumentList '/i', 'smtools-windows-x64.msi', '/quiet', '/qn', '/log', 'smtools-windows-x64.log' -Wait -PassThru
if ($process.ExitCode -ne 0) {
Write-Output "::error title=Install Error::msiexec failed with exit code $($process.ExitCode)"
if (Test-Path smtools-windows-x64.log) { Get-Content smtools-windows-x64.log -Tail 50 }
exit 1
}
# Verify smctl is actually on disk before declaring success
$smctlPath = "C:/Program Files/DigiCert/DigiCert One Signing Manager Tools"
if (!(Test-Path "$smctlPath/smctl.exe")) {
Write-Output "::error title=Install Error::smctl.exe not found at $smctlPath after install"
exit 1
}
$smctlPath | Out-File -FilePath $env:GITHUB_PATH -Append
Write-Output "SMCTL installed and added on PATH"
} else {
Write-Output "SMCTL already installed and on PATH"
}
Write-Output "::endgroup::"
Write-Output "::group::Add signtools in PATH"
if (!(Get-Command signtool -ErrorAction SilentlyContinue)) {
"C:/Program Files (x86)/Windows Kits/10/App Certification Kit" | Out-File -FilePath $env:GITHUB_PATH -Append
Write-Output "signtool added on PATH"
} else {
Write-Output "signtool already installed and on PATH"
}
Write-Output "::endgroup::"

- name: Sign files with signtool
shell: pwsh
env:
SM_CLIENT_CERT_FILE: ${{ steps.setup-cert.outputs.SM_CLIENT_CERT_FILE }}
run: |
Write-Output "::group::Check for required environment variables"
$requiredEnvVars = @('SM_HOST', 'SM_API_KEY', 'SM_CLIENT_CERT_FILE', 'SM_CLIENT_CERT_PASSWORD', 'SM_CLIENT_CERT_FINGERPRINT')
foreach ($envVar in $requiredEnvVars) {
if (-not $(Get-Item -Path "Env:$envVar" -ErrorAction SilentlyContinue)) {
Write-Output "::error title=Missing environment variable::Environment variable $envVar is not set."
exit 1
}
Write-Output "All env var correctly set."
}
Write-Output "::endgroup::"
Write-Output "::group::Sync certificates"
smctl windows certsync
Write-Output "::endgroup::"
# Sign each file that will be bundled in the installer
$rawPaths = "${{ inputs.paths }}" -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
$paths = @()
foreach ($raw in $rawPaths) {
$resolved = Resolve-Path -Path $raw -ErrorAction SilentlyContinue
if (-not $resolved) {
Write-Output "::error title=Signing error::No files matched pattern: ${raw}"
exit 1
}
$paths += $resolved.Path
}
foreach ($path in $paths) {
Write-Output "::group::Signing ${path}"
signtool.exe sign /sha1 $env:SM_CLIENT_CERT_FINGERPRINT /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 ${{ inputs.signtools-extra-args }} $path
if ($LASTEXITCODE -ne 0) {
Write-Output "::error title=Signing error::Error while signing ${path}"
exit 1
}
signtool.exe verify /v /pa $path
if ($LASTEXITCODE -ne 0) {
Write-Output "::error title=Verify signature error::Error while verifying ${path}"
exit 1
}
Write-Output "::endgroup::"
}
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: build-test.yaml
name: Build and Test

on:
push:
Expand Down
71 changes: 62 additions & 9 deletions .github/workflows/release-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,42 @@ jobs:
run: cargo install cargo-packager --locked

- name: Build ggsql binary
run: cargo build --release --package ggsql --bin ggsql
run: cargo build --release --bin ggsql --bin ggsql-jupyter

- name: Sign files before making NSIS and MSI installer
id: sign-files
uses: ./.github/workflows/actions/sign-files
with:
paths: |
./target/release/ggsql.exe
./target/release/ggsql-jupyter.exe
env:
# environment variables required to sign with signtool
SM_HOST: ${{ secrets.SM_HOST }}
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
SM_CLIENT_CERT_FINGERPRINT: ${{ secrets.SM_CLIENT_CERT_FINGERPRINT }}

- name: Build NSIS installer
run: cargo packager --release --formats nsis
working-directory: src

- name: Build MSI installer
run: cargo packager --release --formats wix
working-directory: src

- name: Sign installers
uses: ./.github/workflows/actions/sign-files
with:
paths: |
./src/target/release/packager/*.exe
./src/target/release/packager/*.msi
env:
# environment variables required to sign with signtool
SM_HOST: ${{ secrets.SM_HOST }}
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
SM_CLIENT_CERT_FINGERPRINT: ${{ secrets.SM_CLIENT_CERT_FINGERPRINT }}

- name: Upload NSIS installer
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -80,22 +107,49 @@ jobs:
with:
targets: x86_64-apple-darwin, aarch64-apple-darwin

- name: Set up Apple notarization key
run: |
mkdir -p ~/.private_keys
echo -n "$APPLE_API_KEY_BASE64" | base64 --decode -o ~/.private_keys/AuthKey_${APPLE_API_KEY}.p8
chmod 600 ~/.private_keys/AuthKey_${APPLE_API_KEY}.p8
env:
APPLE_API_KEY_BASE64: ${{ secrets.GWS_APPLE_API_KEY_BASE64 }}
APPLE_API_KEY: ${{ secrets.GWS_APPLE_API_KEY }}

- name: Configure macOS installer signing
env:
APPLE_SIGN_IDENTITY: ${{ secrets.GWS_APPLE_SIGN_IDENTITY }}
run: |
cat <<EOF >> src/Cargo.toml

[package.metadata.packager.macos]
signing-identity = "${APPLE_SIGN_IDENTITY}"
EOF

- name: Install cargo-packager
run: cargo install cargo-packager --locked

- name: Build ggsql binary (x86_64)
run: cargo build --release --package ggsql --bin ggsql
run: cargo build --release --bin ggsql --bin ggsql-jupyter

- name: Build DMG installer (x86_64)
run: cargo packager --release --formats dmg
working-directory: src
env:
APPLE_CERTIFICATE: ${{ secrets.GWS_APPLE_SIGN_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.GWS_APPLE_SIGN_PW }}
APPLE_API_KEY: ${{ secrets.GWS_APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.GWS_APPLE_API_ISSUER }}

- name: Build ggsql binary (aarch64)
run: cargo build --release --package ggsql --bin ggsql --target aarch64-apple-darwin
run: cargo build --release --bin ggsql --bin ggsql-jupyter --target aarch64-apple-darwin

- name: Build DMG installer (aarch64)
run: cargo packager --release --target aarch64-apple-darwin --formats dmg
working-directory: src
env:
APPLE_CERTIFICATE: ${{ secrets.GWS_APPLE_SIGN_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.GWS_APPLE_SIGN_PW }}
APPLE_API_KEY: ${{ secrets.GWS_APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.GWS_APPLE_API_ISSUER }}

- name: Upload DMG installers
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -139,11 +193,10 @@ jobs:
run: cargo install cargo-packager --locked

- name: Build ggsql binary
run: cargo build --release --package ggsql --bin ggsql
run: cargo build --release --bin ggsql --bin ggsql-jupyter

- name: Build Debian package
run: cargo packager --release --formats deb
working-directory: src

- name: Upload Debian package
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ copyright = "Copyright (c) 2026 ggsql Team"
# Binaries to include in the package
binaries = [
{ path = "ggsql", main = true },
{ path = "ggsql-jupyter", main = false },
]

# Resources to bundle (optional)
Expand Down
Loading