Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 106 additions & 16 deletions .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,22 @@ concurrency:

jobs:
release:
name: Draft Desktop Release
runs-on: windows-latest
name: Build (${{ matrix.target }})
runs-on: ${{ matrix.runner }}

strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc
runner: windows-latest
rust_target: x86_64-pc-windows-msvc
- target: aarch64-apple-darwin
runner: macos-latest
rust_target: aarch64-apple-darwin
- target: x86_64-apple-darwin
runner: macos-13
rust_target: x86_64-apple-darwin

steps:
- name: Checkout repository
Expand All @@ -38,12 +52,13 @@ jobs:
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
targets: ${{ matrix.rust_target }}

- name: Cache Rust
uses: Swatinem/rust-cache@v2
with:
workspaces: desktop/src-tauri -> .codex-cargo-target/desktop-tauri
key: ${{ matrix.rust_target }}

- name: Install dependencies
run: pnpm install
Expand Down Expand Up @@ -101,27 +116,102 @@ jobs:
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_NOTES_OUTPUT: desktop/.codex-temp/release-assets/RELEASE_NOTES.md

- name: Create or update draft GitHub release
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.target }}
path: |
desktop/.codex-temp/release-assets/
.codex-cargo-target/desktop-tauri/release/bundle/

merge-and-publish:
name: Merge Manifest & Publish Release
needs: release
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
pattern: build-*
path: build-artifacts
merge-multiple: false

- name: Collect and merge updater manifests
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$tag = "${{ github.ref_name }}"
$repo = "${{ github.repository }}"
$notesPath = (Resolve-Path "desktop/.codex-temp/release-assets/RELEASE_NOTES.md").Path
$manifestPath = (Resolve-Path "desktop/.codex-temp/release-assets/latest.json").Path
$bundleRoot = (Resolve-Path ".codex-cargo-target/desktop-tauri/release/bundle").Path
$manifestDir = "merged-manifests"
New-Item -ItemType Directory -Path $manifestDir -Force

Get-ChildItem -Path "build-artifacts" -Recurse -Filter "latest.json" |
ForEach-Object {
Copy-Item $_.FullName -Destination (Join-Path $manifestDir "latest-$($_.Directory.Parent.Name).json") -Force
}

node scripts/merge-updater-manifests.mjs $manifestDir desktop/.codex-temp/release-assets/latest.json

- name: Collect release notes
shell: pwsh
run: |
$notesFile = Get-ChildItem -Path "build-artifacts" -Recurse -Filter "RELEASE_NOTES.md" |
Select-Object -First 1
if ($notesFile) {
New-Item -ItemType Directory -Path "desktop/.codex-temp/release-assets" -Force
Copy-Item $notesFile.FullName -Destination "desktop/.codex-temp/release-assets/RELEASE_NOTES.md" -Force
}

- name: Collect all platform assets
id: collect
shell: pwsh
run: |
$assets = @()

$assets = Get-ChildItem -Path $bundleRoot -Recurse -File |
# Windows assets
Get-ChildItem -Path "build-artifacts" -Recurse -File |
Where-Object { $_.Extension -in @('.exe', '.msi', '.zip', '.sig') } |
Select-Object -ExpandProperty FullName
ForEach-Object { $assets += $_.FullName }

# macOS assets
Get-ChildItem -Path "build-artifacts" -Recurse -File |
Where-Object { $_.Extension -in @('.dmg', '.app', '.sig') } |
ForEach-Object { $assets += $_.FullName }

# Merged manifest
$manifestPath = (Resolve-Path "desktop/.codex-temp/release-assets/latest.json").Path
$assets += $manifestPath

if ($assets.Count -eq 0) {
throw "No release assets were found under $bundleRoot."
throw "No release assets were found."
}

# Write asset list to file for next step
$assets -join "`n" | Out-File -FilePath "asset-list.txt" -Encoding utf8
Write-Output "Asset count: $($assets.Count)"

- name: Create or update draft GitHub release
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$tag = "${{ github.ref_name }}"
$repo = "${{ github.repository }}"
$notesPath = "desktop/.codex-temp/release-assets/RELEASE_NOTES.md"
$assetList = Get-Content "asset-list.txt" | Where-Object { $_.Trim().Length -gt 0 }

& gh release view $tag --repo $repo *> $null
$releaseExists = $LASTEXITCODE -eq 0

Expand All @@ -132,7 +222,7 @@ jobs:
"--title", $tag,
"--draft",
"--notes-file", $notesPath
) + $assets
) + $assetList
& gh @createArgs
exit $LASTEXITCODE
}
Expand All @@ -153,6 +243,6 @@ jobs:
"release", "upload", $tag,
"--repo", $repo,
"--clobber"
) + $assets
) + $assetList
& gh @uploadArgs
exit $LASTEXITCODE
13 changes: 12 additions & 1 deletion desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ tauri-plugin-updater = "2.0.0-rc.4"
uuid = { version = "1.18.1", features = ["v4"] }
pdf-extract = "0.7"

[target.'cfg(target_os = "macos")'.dependencies]
keyring = "3"

13 changes: 13 additions & 0 deletions desktop/src-tauri/entitlements/macOS.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
65 changes: 58 additions & 7 deletions desktop/src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -880,8 +880,12 @@ fn evaluate_runtime_vault_state(
));
}
if warnings.is_empty() && keyring_active_count > 0 {
warnings
.push("Active secret descriptors are backed by Windows Credential Manager.".into());
#[cfg(target_os = "windows")]
warnings.push("Active secret descriptors are backed by Windows Credential Manager.".into());
#[cfg(target_os = "macos")]
warnings.push("Active secret descriptors are backed by macOS Keychain.".into());
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
warnings.push("Active secret descriptors are backed by OS keyring.".into());
}
}

Expand Down Expand Up @@ -1141,7 +1145,7 @@ fn decode_legacy_entry_to_utf8(
}

fn os_keyring_backend_supported() -> bool {
cfg!(target_os = "windows")
cfg!(target_os = "windows") || cfg!(target_os = "macos")
}

fn secret_keyring_target(secret_key: &str) -> String {
Expand All @@ -1154,7 +1158,11 @@ fn read_secret_from_os_keyring(secret_key: &str) -> Result<Option<String>, Strin
{
windows_credential::read(&target)
}
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
{
macos_keychain::read(&target)
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
let _ = target;
Ok(None)
Expand All @@ -1167,11 +1175,15 @@ fn write_secret_to_os_keyring(secret_key: &str, value: &str) -> Result<(), Strin
{
windows_credential::write(&target, value)
}
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
{
macos_keychain::write(&target, value)
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
let _ = target;
let _ = value;
Err("OS keyring backend is only available on Windows in this PR6 slice.".into())
Err("OS keyring backend is only available on Windows and macOS.".into())
}
}

Expand All @@ -1181,7 +1193,11 @@ fn delete_secret_from_os_keyring(secret_key: &str) -> Result<(), String> {
{
windows_credential::delete(&target)
}
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
{
macos_keychain::delete(&target)
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
let _ = target;
Ok(())
Expand Down Expand Up @@ -1365,6 +1381,41 @@ mod windows_credential {
}
}

#[cfg(target_os = "macos")]
mod macos_keychain {
pub fn read(target: &str) -> Result<Option<String>, String> {
let entry = keyring::Entry::new("RoleRoverDesktop", target)
.map_err(|error| format!("failed to create keyring entry for read ({target}): {error}"))?;
match entry.get_password() {
Ok(password) => Ok(Some(password)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(error) => Err(format!(
"failed to read secret from macOS Keychain ({target}): {error}"
)),
}
}

pub fn write(target: &str, value: &str) -> Result<(), String> {
let entry = keyring::Entry::new("RoleRoverDesktop", target)
.map_err(|error| format!("failed to create keyring entry for write ({target}): {error}"))?;
entry.set_password(value).map_err(|error| {
format!("failed to write secret to macOS Keychain ({target}): {error}")
})
}

pub fn delete(target: &str) -> Result<(), String> {
let entry = keyring::Entry::new("RoleRoverDesktop", target)
.map_err(|error| format!("failed to create keyring entry for delete ({target}): {error}"))?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()),
Err(error) => Err(format!(
"failed to delete secret from macOS Keychain ({target}): {error}"
)),
}
}
}

fn normalize_provider_key(provider: &str) -> Option<&'static str> {
match provider.trim().to_ascii_lowercase().as_str() {
"openai" | "custom" | "azure" => Some("openai"),
Expand Down
4 changes: 4 additions & 0 deletions desktop/src-tauri/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,10 @@ fn resolve_pdf_browser_path() -> Result<PathBuf, String> {
let candidates = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/opt/homebrew/bin/google-chrome",
"/opt/homebrew/bin/microsoft-edge",
"/usr/local/bin/google-chrome",
"/usr/local/bin/microsoft-edge",
];

#[cfg(target_os = "linux")]
Expand Down
14 changes: 12 additions & 2 deletions desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,17 @@
"active": true,
"createUpdaterArtifacts": true,
"icon": [
"icons/icon.ico"
]
"icons/icon.ico",
"icons/icon.icns",
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.png"
],
"macOS": {
"minimumSystemVersion": "12.0",
"entitlements": "entitlements/macOS.entitlements",
"signingIdentity": null
}
}
}
10 changes: 8 additions & 2 deletions scripts/build-local-updater-feed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const bundleDirCandidates = [
path.join(ROOT, "desktop", "src-tauri", "target", "release", "bundle"),
].filter((value) => typeof value === "string" && value.length > 0);
const bundleDir = bundleDirCandidates.find((candidate) => fs.existsSync(candidate));
const supportedArtifactExtensions = [".zip", ".exe", ".msi"];
const supportedArtifactExtensions = [".zip", ".exe", ".msi", ".dmg"];

function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
Expand Down Expand Up @@ -67,6 +67,9 @@ const preferred = candidates.sort((left, right) => {
if (normalized.includes("nsis") && extension === ".exe") {
return 4;
}
if (extension === ".dmg") {
return 4;
}
if (normalized.includes("msi") && extension === ".msi") {
return 3;
}
Expand Down Expand Up @@ -95,7 +98,10 @@ fs.copyFileSync(preferred.sigPath, signatureTargetPath);
const signature = fs.readFileSync(preferred.sigPath, "utf8").trim();
const baseUrl =
process.env.DESKTOP_UPDATER_FEED_BASE_URL ?? "http://127.0.0.1:8765/artifacts";
const target = process.env.DESKTOP_UPDATER_TARGET ?? "windows-x86_64";
const target = process.env.DESKTOP_UPDATER_TARGET?.trim()
|| (process.platform === "darwin"
? (process.arch === "arm64" ? "darwin-aarch64" : "darwin-x86_64")
: "windows-x86_64");
const latestJson = {
version: tauriConfig.version,
notes: "Local updater smoke-test feed for RoleRover Desktop.",
Expand Down
Loading
Loading