Skip to content
20 changes: 2 additions & 18 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -86,53 +84,39 @@ 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 }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
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"
Expand Down
140 changes: 87 additions & 53 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -78,84 +80,90 @@ 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
outputs:
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
Expand All @@ -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);

71 changes: 71 additions & 0 deletions scripts/actions/upload-release-artifacts.js
Original file line number Diff line number Diff line change
@@ -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")}`);
}
};
Loading