diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8f4ff619..7f181f63 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,9 +1,7 @@ name: "CI" on: + # TODO: re-enable pull_request trigger after Windows signing is verified e2e workflow_dispatch: - # NOTE: disable before merge - # FIXME: FYI - # pull_request: jobs: quality: @@ -86,28 +84,18 @@ jobs: runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 - - uses: dorny/paths-filter@v3 - id: changes - with: - filters: | - desktop: - - 'apps/desktop/**' - name: Setup node - if: steps.changes.outputs.desktop == 'true' uses: actions/setup-node@v4 with: node-version: 20 - uses: pnpm/action-setup@v3 - if: steps.changes.outputs.desktop == 'true' with: version: 9 - name: Get pnpm store directory - if: steps.changes.outputs.desktop == 'true' shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - uses: actions/cache@v4 - if: steps.changes.outputs.desktop == 'true' name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -115,24 +103,20 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install Rust stable - if: steps.changes.outputs.desktop == 'true' uses: dtolnay/rust-toolchain@stable with: target: "${{ matrix.settings.toolchain }}" - uses: Swatinem/rust-cache@v2 - if: steps.changes.outputs.desktop == 'true' with: workspaces: "apps/desktop/src-tauri/target" - name: Install dependencies (ubuntu only) - if: matrix.settings.host == 'ubuntu-latest' && steps.changes.outputs.desktop == 'true' + if: matrix.settings.host == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - name: install frontend dependencies - if: steps.changes.outputs.desktop == 'true' run: pnpm install - uses: tauri-apps/tauri-action@dev - if: steps.changes.outputs.desktop == 'true' with: projectPath: apps/desktop args: --config "src-tauri/tauri.conf.ci.json" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 413e2dc3..bf3589ba 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,8 +6,8 @@ on: push: branches: # TODO: REMOVE ME BEFORE MERGE - # FIXME: fr fr ong pls remove - migrate/codesign + - copilot/sub-pr-253 concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -22,6 +22,7 @@ jobs: fail-fast: false matrix: settings: + # TODO: re-enable after Windows signing is verified e2e # - host: macos-latest # target: universal-apple-darwin # toolchain: aarch64-apple-darwin,x86_64-apple-darwin @@ -32,6 +33,7 @@ jobs: toolchain: x86_64-pc-windows-msvc bundles: msi,nsis os: windows + # TODO: re-enable after Windows signing is verified e2e # - host: ubuntu-latest # target: x86_64-unknown-linux-gnu # toolchain: x86_64-unknown-linux-gnu @@ -78,76 +80,82 @@ jobs: VITE_AXIOM_TOKEN: "${{ secrets.VITE_AXIOM_TOKEN }}" VITE_SENTRY_AUTH_TOKEN: "${{ secrets.VITE_SENTRY_AUTH_TOKEN }}" with: - # NOTE: we only use this action to build the project bins for each platform - # because we need to do code signing for windows we will upload manually after the signing is completed projectPath: "${{ env.APP_DIR }}" args: --target ${{ matrix.settings.target }} --bundles ${{ matrix.settings.bundles }},updater - # TODO: figure out where the binary are in the fs from the above action - - name: Presign - run: | - # Create binaries directory if it doesn't exist - New-Item -ItemType Directory -Force -Path "./binaries" - # Parse artifactPaths to extract .exe and .msi files - $artifactPaths = '${{ steps.tauri.outputs.artifactPaths }}' - - # Parse as JSON array - $paths = $artifactPaths | ConvertFrom-Json - - # Extract .exe and .msi files (excluding .sig and .zip files) - $exeFiles = $paths | Where-Object { $_ -match '\.exe$' } | Where-Object { $_ -notmatch '\.(sig|zip)$' } - $msiFiles = $paths | Where-Object { $_ -match '\.msi$' } | Where-Object { $_ -notmatch '\.(sig|zip)$' } - - Write-Host "Found EXE files:" - $exeFiles | ForEach-Object { Write-Host " $_" } - - Write-Host "Found MSI files:" - $msiFiles | ForEach-Object { Write-Host " $_" } - - # Move the files to binaries folder - $exeFiles | ForEach-Object { - $fileName = Split-Path $_ -Leaf - Copy-Item $_ -Destination ".\binaries\$fileName" - } - - $msiFiles | ForEach-Object { - $fileName = Split-Path $_ -Leaf - Copy-Item $_ -Destination ".\binaries\$fileName" - } + # Collect ALL Tauri artifacts (cross-platform, single source of truth) + - name: Collect release artifacts + shell: bash + env: + ARTIFACT_PATHS: ${{ steps.tauri.outputs.artifactPaths }} + run: | + mkdir -p ./release-artifacts + node -e " + const fs = require('fs'); + const path = require('path'); + const paths = JSON.parse(process.env.ARTIFACT_PATHS); + paths.forEach(p => { + const dest = path.join('./release-artifacts', path.basename(p)); + fs.copyFileSync(p, dest); + console.log('Collected:', path.basename(p)); + }); + " - # sha sum the files in binaries - Get-ChildItem -Path ".\binaries" -File -Recurse | ForEach-Object { Get-FileHash -Path $_.FullName -Algorithm SHA256 } + # Windows only: extract .exe/.msi for signing, sign, then replace in release-artifacts + - name: Extract Windows binaries for signing + if: matrix.settings.os == 'windows' + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path "./binaries-to-sign" + Get-ChildItem -Path "./release-artifacts" -File | + Where-Object { $_.Extension -in '.exe', '.msi' } | + ForEach-Object { + Copy-Item $_.FullName -Destination "./binaries-to-sign/$($_.Name)" + Write-Host "To sign: $($_.Name)" + } + Get-ChildItem -Path "./binaries-to-sign" -File | ForEach-Object { Get-FileHash -Path $_.FullName -Algorithm SHA256 } - - name: Upload Unsigned Windows Binaries + - name: Upload unsigned Windows binaries + if: matrix.settings.os == 'windows' id: upload-unsigned-artifact uses: actions/upload-artifact@v4 - with: - path: ./binaries - - name: Sign Windows Binaries + with: + name: windows-unsigned + path: ./binaries-to-sign + + - name: Sign Windows binaries + if: matrix.settings.os == 'windows' uses: signpath/github-action-submit-signing-request@v1.1 with: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: 'b2cc34a4-3b75-4753-82e4-b755351770ea' project-slug: 'overlayed' - # TODO: when we have the production signing policy update this + # TODO: switch to the production signing policy when available signing-policy-slug: 'test-signing' github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}' wait-for-completion: true - output-artifact-directory: 'binaries/signed' - - name: Postsign + output-artifact-directory: 'binaries-signed' + + - name: Replace unsigned binaries with signed + if: matrix.settings.os == 'windows' + shell: pwsh run: | - # using powershell lets list out the sha sum of the signed binaries - Get-ChildItem -Path binaries/signed -File -Recurse | ForEach-Object { Get-FileHash -Path $_.FullName -Algorithm SHA256 } - - name: Upload signed windows binaries - uses: actions/github-script@v7 + Get-ChildItem -Path "./binaries-signed" -File | ForEach-Object { + Copy-Item $_.FullName -Destination "./release-artifacts/$($_.Name)" -Force + Write-Host "Replaced with signed: $($_.Name)" + } + Get-ChildItem -Path "./release-artifacts" -File | ForEach-Object { Get-FileHash -Path $_.FullName -Algorithm SHA256 } + + # Upload release artifact — same step for all platforms (single source of truth) + - name: Upload ${{ matrix.settings.os }} release artifacts + uses: actions/upload-artifact@v4 with: - script: | - const { script } = await import('${{ github.workspace }}/scripts/actions/upload-signed-bins.js'); - const id = "${{ needs.create-release.outputs.release_id }}"; - await script({ github, context }, id); + name: ${{ matrix.settings.os }}-release + path: ./release-artifacts + create-release: needs: build-tauri - name: Create or Update + name: Create Release permissions: contents: write runs-on: ubuntu-latest @@ -155,7 +163,7 @@ jobs: release_id: ${{ steps.create-release.outputs.result }} steps: - uses: actions/checkout@v4 - - name: setup node + - name: Setup node uses: actions/setup-node@v4 with: node-version: 20 @@ -169,3 +177,29 @@ jobs: const { script } = await import('${{ github.workspace }}/scripts/actions/create-release.js') return await script({ github, context }); + upload-release: + needs: create-release + name: Upload Release Artifacts + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Download all release artifacts + uses: actions/download-artifact@v4.1.3 + with: + pattern: '*-release' + path: ./release-artifacts + merge-multiple: true + - name: Upload artifacts to release + uses: actions/github-script@v7 + with: + script: | + const { script } = await import('${{ github.workspace }}/scripts/actions/upload-release-artifacts.js') + const id = "${{ needs.create-release.outputs.release_id }}"; + await script({ github, context }, id); + diff --git a/scripts/actions/upload-release-artifacts.js b/scripts/actions/upload-release-artifacts.js new file mode 100644 index 00000000..a0233d8c --- /dev/null +++ b/scripts/actions/upload-release-artifacts.js @@ -0,0 +1,71 @@ +import fs from "fs"; +import path from "path"; + +const RELEASE_ARTIFACTS_DIR = "./release-artifacts"; + +/** @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments */ +export const script = async ({ context, github }, releaseId) => { + console.log("📦 Uploading release artifacts for release id", releaseId); + console.log({ cwd: process.cwd() }); + + if (!fs.existsSync(RELEASE_ARTIFACTS_DIR)) { + console.log("No release artifacts found"); + return; + } + + const files = fs.readdirSync(RELEASE_ARTIFACTS_DIR); + console.log("Files to upload:", files); + + const errors = []; + + // Fetch existing assets so we can replace them if a re-run uploads the same names + const { data: existingAssets } = await github.rest.repos.listReleaseAssets({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + per_page: 100, + }); + console.log("Existing assets:", existingAssets.map((a) => a.name)); + + for (const file of files) { + const filePath = path.join(RELEASE_ARTIFACTS_DIR, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + continue; + } + + const fileData = fs.readFileSync(filePath); + console.log("uploading asset", file, filePath); + + try { + // Delete any existing asset with the same name so re-runs don't fail + const existingAsset = existingAssets.find((a) => a.name === file); + if (existingAsset) { + console.log("deleting existing asset", existingAsset.name, existingAsset.id); + await github.rest.repos.deleteReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + asset_id: existingAsset.id, + }); + } + + const { data: uploadResponse } = await github.rest.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: releaseId, + data: fileData, + name: file, + }); + + console.log("uploaded asset", uploadResponse.name, uploadResponse.id); + } catch (error) { + console.error("error uploading asset", file, error.message); + errors.push(`${file}: ${error.message}`); + } + } + + if (errors.length > 0) { + throw new Error(`Failed to upload ${errors.length} asset(s):\n${errors.join("\n")}`); + } +};