From bda4ee1290ad51d8f249c813a76310a72728535c Mon Sep 17 00:00:00 2001 From: Max Novich Date: Tue, 17 Jun 2025 14:26:07 -0700 Subject: [PATCH] enabling windows builds with code signing (#2968) --- .github/workflows/bundle-desktop-windows.yml | 309 +++++++++++++++--- .github/workflows/canary.yml | 17 +- .../workflows/pr-comment-bundle-windows.yml | 13 +- .github/workflows/release.yml | 25 +- ui/desktop/forge.config.ts | 2 +- 5 files changed, 302 insertions(+), 64 deletions(-) diff --git a/.github/workflows/bundle-desktop-windows.yml b/.github/workflows/bundle-desktop-windows.yml index 3a796096..7eac44dd 100644 --- a/.github/workflows/bundle-desktop-windows.yml +++ b/.github/workflows/bundle-desktop-windows.yml @@ -12,15 +12,22 @@ on: required: false type: boolean default: false + ref: + description: 'Git ref to checkout' + required: false + type: string + default: 'refs/heads/main' secrets: - WINDOWS_CERTIFICATE: + WINDOWS_CODESIGN_CERTIFICATE: required: false - WINDOWS_CERTIFICATE_PASSWORD: + WINDOW_SIGNING_ROLE: required: false - ref: - type: string - required: false - default: 'refs/heads/main' + +# Permissions required for OIDC authentication with AWS +permissions: + id-token: write # Required to fetch the OIDC token + contents: read # Required by actions/checkout + actions: read # May be needed for some workflows jobs: build-desktop-windows: @@ -35,11 +42,19 @@ jobs: ref: ${{ inputs.ref }} fetch-depth: 0 + # 2) Configure AWS credentials for code signing + - name: Configure AWS credentials + if: inputs.signing && inputs.signing == true + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # ratchet:aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.WINDOW_SIGNING_ROLE }} + aws-region: us-west-2 + # 2) Set up Node.js - name: Set up Node.js uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # pin@v3 with: - node-version: 18 + node-version: 22 # 3) Cache dependencies - name: Cache node_modules @@ -48,36 +63,132 @@ jobs: path: | node_modules ui/desktop/node_modules - key: ${{ runner.os }}-build-desktop-windows-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-build-desktop-windows-node22-${{ hashFiles('**/package-lock.json') }} restore-keys: | - ${{ runner.os }}-build-desktop-windows- + ${{ runner.os }}-build-desktop-windows-node22- - # 4) Build Rust for Windows using Docker (cross-compilation) - - name: Build Windows executable using Docker + # Cache Cargo registry and git dependencies + - name: Cache Cargo registry + uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-registry- + + # Cache compiled dependencies (target/release/deps) + - name: Cache Cargo build + uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f + with: + path: target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }} + restore-keys: | + ${{ runner.os }}-cargo-build-${{ hashFiles('Cargo.lock') }}- + ${{ runner.os }}-cargo-build- + + # 4) Build Rust for Windows using Docker (cross-compilation with enhanced caching) + - name: Build Windows executable using Docker cross-compilation with enhanced caching run: | - echo "Building Windows executable using Docker cross-compilation..." - docker volume create goose-windows-cache || true + echo "🚀 Building Windows executable with enhanced GitHub Actions caching..." + + # Create cache directories + mkdir -p ~/.cargo/registry ~/.cargo/git + + # Use enhanced caching with GitHub Actions cache mounts docker run --rm \ -v "$(pwd)":/usr/src/myapp \ - -v goose-windows-cache:/usr/local/cargo/registry \ + -v "$HOME/.cargo/registry":/usr/local/cargo/registry \ + -v "$HOME/.cargo/git":/usr/local/cargo/git \ -w /usr/src/myapp \ rust:latest \ - sh -c "rustup target add x86_64-pc-windows-gnu && \ - apt-get update && \ - apt-get install -y mingw-w64 protobuf-compiler cmake && \ - export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc && \ - export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++ && \ - export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar && \ - export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc && \ - export PKG_CONFIG_ALLOW_CROSS=1 && \ - export PROTOC=/usr/bin/protoc && \ - export PATH=/usr/bin:\$PATH && \ - protoc --version && \ - cargo build --release --target x86_64-pc-windows-gnu && \ - GCC_DIR=\$(ls -d /usr/lib/gcc/x86_64-w64-mingw32/*/ | head -n 1) && \ - cp \$GCC_DIR/libstdc++-6.dll /usr/src/myapp/target/x86_64-pc-windows-gnu/release/ && \ - cp \$GCC_DIR/libgcc_s_seh-1.dll /usr/src/myapp/target/x86_64-pc-windows-gnu/release/ && \ - cp /usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll /usr/src/myapp/target/x86_64-pc-windows-gnu/release/" + bash -c " + set -e + echo '=== Setting up Rust environment with caching ===' + export CARGO_HOME=/usr/local/cargo + export PATH=/usr/local/cargo/bin:\$PATH + + # Check if Windows target is already installed in cache + if rustup target list --installed | grep -q x86_64-pc-windows-gnu; then + echo '✅ Windows cross-compilation target already installed' + else + echo '📦 Installing Windows cross-compilation target...' + rustup target add x86_64-pc-windows-gnu + fi + + echo '=== Setting up build dependencies ===' + apt-get update + apt-get install -y mingw-w64 protobuf-compiler cmake time + + echo '=== Setting up cross-compilation environment ===' + export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc + export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++ + export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar + export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc + export PKG_CONFIG_ALLOW_CROSS=1 + export PROTOC=/usr/bin/protoc + + echo '=== Optimized Cargo configuration ===' + mkdir -p .cargo + echo '[build]' > .cargo/config.toml + echo 'jobs = 4' >> .cargo/config.toml + echo '' >> .cargo/config.toml + echo '[target.x86_64-pc-windows-gnu]' >> .cargo/config.toml + echo 'linker = \"x86_64-w64-mingw32-gcc\"' >> .cargo/config.toml + echo '' >> .cargo/config.toml + echo '[net]' >> .cargo/config.toml + echo 'git-fetch-with-cli = true' >> .cargo/config.toml + echo 'retry = 3' >> .cargo/config.toml + echo '' >> .cargo/config.toml + echo '[profile.release]' >> .cargo/config.toml + echo 'codegen-units = 1' >> .cargo/config.toml + echo 'lto = false' >> .cargo/config.toml + echo 'panic = \"abort\"' >> .cargo/config.toml + echo 'debug = false' >> .cargo/config.toml + echo 'opt-level = 2' >> .cargo/config.toml + echo '' >> .cargo/config.toml + echo '[registries.crates-io]' >> .cargo/config.toml + echo 'protocol = \"sparse\"' >> .cargo/config.toml + + echo '=== Building with cached dependencies ===' + # Check if we have cached build artifacts + if [ -d target/x86_64-pc-windows-gnu/release/deps ] && [ \"\$(ls -A target/x86_64-pc-windows-gnu/release/deps)\" ]; then + echo '✅ Found cached build artifacts, performing incremental build...' + CARGO_INCREMENTAL=1 + else + echo '🔨 No cached artifacts found, performing full build...' + CARGO_INCREMENTAL=0 + fi + + echo '🔨 Building Windows executable...' + CARGO_INCREMENTAL=\$CARGO_INCREMENTAL \ + CARGO_NET_RETRY=3 \ + CARGO_HTTP_TIMEOUT=60 \ + RUST_BACKTRACE=1 \ + cargo build --release --target x86_64-pc-windows-gnu --jobs 4 + + echo '=== Copying Windows runtime DLLs ===' + GCC_DIR=\$(ls -d /usr/lib/gcc/x86_64-w64-mingw32/*/ | head -n 1) + cp \"\$GCC_DIR/libstdc++-6.dll\" target/x86_64-pc-windows-gnu/release/ + cp \"\$GCC_DIR/libgcc_s_seh-1.dll\" target/x86_64-pc-windows-gnu/release/ + cp /usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll target/x86_64-pc-windows-gnu/release/ + + echo '✅ Build completed successfully!' + ls -la target/x86_64-pc-windows-gnu/release/ + " + + # Verify build succeeded + if [ ! -f "./target/x86_64-pc-windows-gnu/release/goosed.exe" ]; then + echo "❌ Windows binary not found." + ls -la ./target/x86_64-pc-windows-gnu/release/ || echo "Release directory doesn't exist" + exit 1 + fi + + echo "✅ Windows binary found!" + ls -la ./target/x86_64-pc-windows-gnu/release/goosed.exe + ls -la ./target/x86_64-pc-windows-gnu/release/*.dll # 4.5) Build temporal-service for Windows - name: Build temporal-service for Windows @@ -96,7 +207,7 @@ jobs: echo "Downloading temporal CLI for Windows..." TEMPORAL_VERSION="1.3.0" curl -L "https://github.com/temporalio/cli/releases/download/v${TEMPORAL_VERSION}/temporal_cli_${TEMPORAL_VERSION}_windows_amd64.zip" -o temporal-cli-windows.zip - unzip temporal-cli-windows.zip + unzip -o temporal-cli-windows.zip chmod +x temporal.exe echo "temporal CLI downloaded successfully" @@ -152,31 +263,139 @@ jobs: - name: Build desktop UI with npm run: | cd ui/desktop + + # Fix for rollup native module issue (npm optional dependencies bug) + echo "🔧 Fixing npm optional dependencies issue..." + rm -rf node_modules package-lock.json npm install + + # Verify rollup native module is installed + if [ ! -d "node_modules/@rollup/rollup-linux-x64-gnu" ]; then + echo "⚠️ Rollup native module missing, installing manually..." + npm install @rollup/rollup-linux-x64-gnu --save-optional + fi + npm run bundle:windows - # 7) Copy exe/dll to final out/Goose-win32-x64/resources/bin - - name: Copy exe/dll to out folder + # 7) Copy exe/dll to final out folder and prepare flat distribution + - name: Copy exe/dll to final out folder and prepare flat distribution run: | cd ui/desktop mkdir -p ./out/Goose-win32-x64/resources/bin rsync -av src/bin/ out/Goose-win32-x64/resources/bin/ + + # Create flat distribution structure + mkdir -p ./dist-windows + cp -r ./out/Goose-win32-x64/* ./dist-windows/ + + # Verify the final structure + echo "📋 Final flat distribution structure:" + ls -la ./dist-windows/ + echo "📋 Binary files in resources/bin:" + ls -la ./dist-windows/resources/bin/ - # 8) Code signing (if enabled) - - name: Sign Windows executable + # 8) Sign Windows executables with jsign + AWS KMS + - name: Sign Windows executables with jsign + AWS KMS if: inputs.signing && inputs.signing == true - env: - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} - WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} run: | - # Note: This would need to be adapted for Linux-based signing - # or moved to a Windows runner for the signing step only - echo "Code signing would be implemented here" - echo "Currently skipped as we're running on Ubuntu" + set -exuo pipefail + echo "🔐 Starting Windows code signing with jsign + AWS KMS..." + + # Create certificate file from secret + echo "📝 Creating certificate file from GitHub secret..." + echo "${{ secrets.WINDOWS_CODESIGN_CERTIFICATE }}" > block-codesign-cert.pem + + # Install Java (required for jsign) + echo "☕ Installing Java runtime..." + sudo apt-get update + sudo apt-get install -y openjdk-11-jre-headless osslsigncode + + # Download jsign + echo "📥 Downloading jsign..." + wget -q https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar -O jsign.jar + echo "05ca18d4ab7b8c2183289b5378d32860f0ea0f3bdab1f1b8cae5894fb225fa8a jsign.jar" | sha256sum -c + + # Sign the main Electron executable (Goose.exe) + echo "🔐 Signing main Electron executable: Goose.exe" + cd ui/desktop/dist-windows/ + + java -jar ${GITHUB_WORKSPACE}/jsign.jar \ + --storetype AWS \ + --keystore us-west-2 \ + --storepass "${AWS_ACCESS_KEY_ID}|${AWS_SECRET_ACCESS_KEY}|${AWS_SESSION_TOKEN}" \ + --alias windows-codesign \ + --certfile "${GITHUB_WORKSPACE}/block-codesign-cert.pem" \ + --tsaurl "http://timestamp.digicert.com" \ + --name "Goose" \ + --url "https://github.com/block/goose" \ + "Goose.exe" - # 9) Upload the final Windows build + osslsigncode verify Goose.exe + echo "✅ Main executable Goose.exe signed successfully" + + # Sign the backend executable (goosed.exe) + echo "🔐 Signing backend executable: goosed.exe" + cd resources/bin/ + + java -jar ${GITHUB_WORKSPACE}/jsign.jar \ + --storetype AWS \ + --keystore us-west-2 \ + --storepass "${AWS_ACCESS_KEY_ID}|${AWS_SECRET_ACCESS_KEY}|${AWS_SESSION_TOKEN}" \ + --alias windows-codesign \ + --certfile "${GITHUB_WORKSPACE}/block-codesign-cert.pem" \ + --tsaurl "http://timestamp.digicert.com" \ + --name "Goose Backend" \ + --url "https://github.com/block/goose" \ + "goosed.exe" + + osslsigncode verify goosed.exe + echo "✅ Backend executable goosed.exe signed successfully" + + # Show final file status + echo "📋 Final signed files:" + cd ../../ + ls -la Goose.exe + sha256sum Goose.exe + ls -la resources/bin/goosed.exe + sha256sum resources/bin/goosed.exe + + # Clean up certificate file + rm -f ${GITHUB_WORKSPACE}/block-codesign-cert.pem + + # 9) Verify signed executables are in final distribution + - name: Verify signed executables are in final distribution + if: inputs.signing && inputs.signing == true + run: | + echo "📋 Verifying both signed executables in final distribution:" + echo "Main executable:" + ls -la ui/desktop/dist-windows/Goose.exe + osslsigncode verify ui/desktop/dist-windows/Goose.exe + echo "✅ Main executable signature verification passed" + + echo "Backend executable:" + ls -la ui/desktop/dist-windows/resources/bin/goosed.exe + osslsigncode verify ui/desktop/dist-windows/resources/bin/goosed.exe + echo "✅ Backend executable signature verification passed" + + # 10) Create Windows zip package + - name: Create Windows zip package + run: | + cd ui/desktop + echo "📦 Creating Windows zip package..." + + # Create a zip file from the dist-windows directory + zip -r "Goose-win32-x64.zip" dist-windows/ + + echo "✅ Windows zip package created:" + ls -la Goose-win32-x64.zip + + # Also create the zip in the expected output structure for consistency + mkdir -p out/Goose-win32-x64/ + cp Goose-win32-x64.zip out/Goose-win32-x64/ + + # 11) Upload the final Windows build - name: Upload Windows build artifacts uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # pin@v4 with: - name: desktop-windows-dist - path: ui/desktop/out/Goose-win32-x64/ + name: Goose-win32-x64 + path: ui/desktop/out/Goose-win32-x64/Goose-win32-x64.zip diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 7cad5b10..27ffd00a 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -84,13 +84,26 @@ jobs: with: version: ${{ needs.prepare-version.outputs.version }} + # ------------------------------------------------------------ + # 6) Bundle Desktop App (Windows) - builds goosed and Electron app + # ------------------------------------------------------------ + bundle-desktop-windows: + needs: [prepare-version] + uses: ./.github/workflows/bundle-desktop-windows.yml + with: + version: ${{ needs.prepare-version.outputs.version }} + signing: true + secrets: + WINDOWS_CODESIGN_CERTIFICATE: ${{ secrets.WINDOWS_CODESIGN_CERTIFICATE }} + WINDOW_SIGNING_ROLE: ${{ secrets.WINDOW_SIGNING_ROLE }} + # ------------------------------------ - # 6) Create/Update GitHub Release + # 7) Create/Update GitHub Release # ------------------------------------ release: name: Release runs-on: ubuntu-latest - needs: [build-cli, install-script, bundle-desktop, bundle-desktop-linux] + needs: [build-cli, install-script, bundle-desktop, bundle-desktop-linux, bundle-desktop-windows] permissions: contents: write diff --git a/.github/workflows/pr-comment-bundle-windows.yml b/.github/workflows/pr-comment-bundle-windows.yml index a394818e..b5481589 100644 --- a/.github/workflows/pr-comment-bundle-windows.yml +++ b/.github/workflows/pr-comment-bundle-windows.yml @@ -11,10 +11,13 @@ on: required: true type: string -# permissions needed for reacting to IssueOps commands on PRs +# permissions needed for reacting to IssueOps commands on PRs and AWS OIDC authentication permissions: pull-requests: write checks: read + id-token: write # Required for AWS OIDC authentication in called workflow + contents: read # Required by actions/checkout in called workflow + actions: read # May be needed for some workflows name: Bundle Windows Desktop App @@ -61,11 +64,9 @@ jobs: if: ${{ needs.trigger-on-command.outputs.continue == 'true' }} uses: ./.github/workflows/bundle-desktop-windows.yml with: - signing: false # false for now as we don't have a cert yet + signing: false ref: ${{ needs.trigger-on-command.outputs.head_sha }} - secrets: - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} - WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + pr-comment-windows: name: PR Comment with Windows App @@ -93,4 +94,4 @@ jobs: **Instructions:** After downloading, unzip the file and run Goose.exe. The app is signed for Windows. - This link is provided by nightly.link and will work even if you're not logged into GitHub. \ No newline at end of file + This link is provided by nightly.link and will work even if you're not logged into GitHub. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b997fe7b..bb03de72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,13 @@ on: - "v1.*" name: Release + +# Permissions needed for AWS OIDC authentication in called workflows +permissions: + id-token: write # Required for AWS OIDC authentication in called workflow + contents: write # Required for creating releases and by actions/checkout + actions: read # May be needed for some workflows + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -69,15 +76,13 @@ jobs: # # ------------------------------------------------------------ # # 6) Bundle Desktop App (Windows) # # ------------------------------------------------------------ -# bundle-desktop-windows: -# uses: ./.github/workflows/bundle-desktop-windows.yml -# # Signing is disabled by default until we have a certificate -# with: -# signing: false -# # Uncomment and configure these when we have a certificate: -# # secrets: -# # WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} -# # WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + bundle-desktop-windows: + uses: ./.github/workflows/bundle-desktop-windows.yml + with: + signing: true + secrets: + WINDOWS_CODESIGN_CERTIFICATE: ${{ secrets.WINDOWS_CODESIGN_CERTIFICATE }} + WINDOW_SIGNING_ROLE: ${{ secrets.WINDOW_SIGNING_ROLE }} # ------------------------------------ # 7) Create/Update GitHub Release @@ -85,7 +90,7 @@ jobs: release: name: Release runs-on: ubuntu-latest - needs: [build-cli, install-script, bundle-desktop, bundle-desktop-intel, bundle-desktop-linux] + needs: [build-cli, install-script, bundle-desktop, bundle-desktop-intel, bundle-desktop-linux, bundle-desktop-windows] permissions: contents: write steps: diff --git a/ui/desktop/forge.config.ts b/ui/desktop/forge.config.ts index 5dc22339..21ff9221 100644 --- a/ui/desktop/forge.config.ts +++ b/ui/desktop/forge.config.ts @@ -10,7 +10,7 @@ let cfg = { win32: { icon: 'src/images/icon.ico', certificateFile: process.env.WINDOWS_CERTIFICATE_FILE, - certificatePassword: process.env.WINDOWS_CERTIFICATE_PASSWORD, + signingRole: process.env.WINDOW_SIGNING_ROLE, rfc3161TimeStampServer: 'http://timestamp.digicert.com', signWithParams: '/fd sha256 /tr http://timestamp.digicert.com /td sha256' },