diff --git a/.github/workflows/actions/sign-files/action.yml b/.github/workflows/actions/sign-files/action.yml new file mode 100644 index 00000000..917680fb --- /dev/null +++ b/.github/workflows/actions/sign-files/action.yml @@ -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::" + } diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8dd8f192..0937dc62 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -name: build-test.yaml +name: Build and Test on: push: diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 55ddace0..cf01e0d4 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -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 @@ -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 <> 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 @@ -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 diff --git a/src/Cargo.toml b/src/Cargo.toml index ef5e9ba8..4f45fea2 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -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)