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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
name: Typecheck & test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
name: Build VitePress site
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
# Release events check out the release tag by default. The generated changelog is
# committed to the default branch, so always build and update main.
Expand All @@ -38,7 +38,7 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/configure-pages@v5
- uses: actions/configure-pages@v6
- run: bun install --frozen-lockfile
- name: Refresh changelog
env:
Expand Down Expand Up @@ -67,7 +67,7 @@ jobs:
# browser. Absent -> analytics stays a silent no-op. Same var name as
# the adc.co site, so one org value can feed both.
PUBLIC_POSTHOG_PROJECT_TOKEN: ${{ vars.PUBLIC_POSTHOG_PROJECT_TOKEN }}
- uses: actions/upload-pages-artifact@v3
- uses: actions/upload-pages-artifact@v5
with:
path: docs/.vitepress/dist

Expand All @@ -80,4 +80,4 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
220 changes: 199 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
name: Release

# Builds and (when the signing secrets are configured) signs the distributable artifacts:
# Builds and signs the distributable artifacts:
# - npm: per-OS prebuilt binaries published under @agentdeploymentco/argus*
# - macOS: a signed + notarized Argus.app / .dmg
# - Windows: NSIS installers (.exe) for x64 and ARM64 (Authenticode signing not wired up yet)
# - Windows: standalone CLI ZIPs plus Azure Artifact Signing-signed NSIS installers (.exe)
#
# Two modes, decided by what triggered the run:
# - Tag push: build everything, upload to a DRAFT GitHub Release, and publish it only after
# every platform leg has succeeded (the publish-release job below).
# - workflow_dispatch (any branch): build-only dry run — no tag, no release; the bundles are
# uploaded as workflow artifacts so they can be downloaded and tested on real machines.
# All signing is gated on secrets being present, so this workflow runs end-to-end (producing
# UNSIGNED artifacts) before any certificate exists, and starts signing the moment the secrets are
# added. No secret value ever lives in this file — only references to `secrets.*`.
# Windows signing uses GitHub Actions OIDC with Azure Artifact Signing. The Windows jobs fail
# clearly if the Azure identity or signing configuration is missing, so a tagged release cannot
# silently publish unsigned installers. No secret value ever lives in this file — only references
# to `secrets.*`.
#
# Required secrets to activate signing (set in repo/org settings; never commit their values):
# Required secrets (set in repo/org settings; never commit their values):
# NPM_TOKEN npm automation token (npm publish)
# APPLE_CERTIFICATE base64 of the Developer ID Application .p12
# APPLE_CERTIFICATE_PASSWORD its export password
# APPLE_SIGNING_IDENTITY e.g. "Developer ID Application: … (TEAMID)"
# APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID notarytool credentials (app-specific password)
# WINDOWS_CERTIFICATE / WINDOWS_CERTIFICATE_PASSWORD Authenticode signing cert (base64 + pwd;
# reserved — the Windows jobs don't sign yet)
# AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_SUBSCRIPTION_ID
# GitHub OIDC identity used by Azure Artifact Signing
# Required repository or organization secrets:
# AZURE_ARTIFACT_SIGNING_ENDPOINT Region-specific codesigning.azure.net endpoint
# AZURE_ARTIFACT_SIGNING_ACCOUNT Artifact Signing account name
# AZURE_ARTIFACT_SIGNING_PROFILE Certificate profile name
# TAURI_SIGNING_PRIVATE_KEY / TAURI_SIGNING_PRIVATE_KEY_PASSWORD
# updater signing key (separate from OS certs)

Expand Down Expand Up @@ -51,6 +56,7 @@ jobs:
# Windows x64, built natively on the x64 runner.
- name: Windows x64
slug: windows-x64
archive_arch: amd64
platform: "windows-latest"
rust-targets: ""
stage-args: "--build"
Expand All @@ -60,12 +66,14 @@ jobs:
# `bun build --compile --target=bun-windows-arm64` (stage-desktop-sidecar.sh --target).
- name: Windows ARM64
slug: windows-arm64
archive_arch: arm64
platform: "windows-latest"
rust-targets: "aarch64-pc-windows-msvc"
stage-args: "--target aarch64-pc-windows-msvc"
args: "--target aarch64-pc-windows-msvc --bundles nsis"
name: ${{ matrix.name }}
runs-on: ${{ matrix.platform }}
environment: production
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
Expand All @@ -75,7 +83,7 @@ jobs:
# cross-volume moves fail when `bun build --compile --target=bun-windows-arm64` populates it.
BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun-cache
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
Expand All @@ -97,6 +105,30 @@ jobs:
- name: Stage sidecar + web
run: bash scripts/stage-desktop-sidecar.sh ${{ matrix.stage-args }}

- name: Azure login for Artifact Signing
if: runner.os == 'Windows'
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

# Sign the CLI sidecar before Tauri embeds it in the installed application. The ARM64
# sidecar is signed on this x64 Windows runner because Artifact Signing does not support
# Windows ARM runners.
- name: Sign Windows sidecar
if: runner.os == 'Windows'
uses: azure/artifact-signing-action@v2
with:
endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_PROFILE }}
files-folder: ${{ github.workspace }}\desktop\src-tauri\binaries
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: import Apple Developer Certificate
if: runner.os == 'macOS'
# Prevents keychain from locking automatically for 3600 seconds.
Expand All @@ -122,7 +154,7 @@ jobs:
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported."

- name: Build (and sign, if secrets present) the desktop app
- name: Build the desktop app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -183,18 +215,164 @@ jobs:
exit 1
fi

# Dry-run mode only: expose the bundles on the workflow run so they can be
# downloaded and installed on real machines before any tag exists.
- name: Upload bundles as workflow artifacts
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: actions/upload-artifact@v4
- name: Locate Windows installers
if: runner.os == 'Windows'
id: windows_installers
shell: pwsh
run: |
$installers = @(Get-ChildItem -Path "$env:GITHUB_WORKSPACE\desktop\src-tauri\target" -Filter "*-setup.exe" -Recurse |
Select-Object -ExpandProperty FullName
)
if ($installers.Count -eq 0) {
throw "No Windows installer was found to sign."
}
"files<<EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
$installers | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append

- name: Sign Windows installers
if: runner.os == 'Windows'
uses: azure/artifact-signing-action@v2
with:
endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_PROFILE }}
files: ${{ steps.windows_installers.outputs.files }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: Verify Windows Authenticode signatures
if: runner.os == 'Windows'
shell: pwsh
run: |
$installers = @(Get-ChildItem -Path "$env:GITHUB_WORKSPACE\desktop\src-tauri\target" -Filter "*-setup.exe" -Recurse)
if ($installers.Count -eq 0) {
throw "No Windows installer was found to verify."
}
$artifacts = @(
Get-ChildItem -Path "$env:GITHUB_WORKSPACE\desktop\src-tauri\binaries" -Filter "*.exe"
$installers
)
foreach ($artifact in $artifacts) {
$signature = Get-AuthenticodeSignature -FilePath $artifact.FullName
if ($signature.Status -ne 'Valid') {
throw "Invalid Authenticode signature on $($artifact.FullName): $($signature.Status)"
}
Write-Host "$($artifact.Name): $($signature.Status) ($($signature.SignerCertificate.Subject))"
}

# Package the signed CLI sidecar with the web app so Windows users can run Argus without
# installing the desktop shell. Keep both files at the archive root for easy extraction.
- name: Package Windows CLI archive
if: runner.os == 'Windows'
shell: pwsh
run: |
$archiveDir = Join-Path $env:RUNNER_TEMP "argus-cli-archive"
$releaseDir = Join-Path $env:GITHUB_WORKSPACE "release-artifacts"
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $archiveDir
New-Item -ItemType Directory -Force -Path $archiveDir, $releaseDir | Out-Null

$binaries = @(Get-ChildItem -Path "$env:GITHUB_WORKSPACE\desktop\src-tauri\binaries" -Filter "*.exe")
if ($binaries.Count -ne 1) {
throw "Expected one Windows CLI binary, found $($binaries.Count)."
}
Copy-Item -LiteralPath $binaries[0].FullName -Destination (Join-Path $archiveDir "argus.exe")
Copy-Item -Recurse -LiteralPath "$env:GITHUB_WORKSPACE\desktop\src-tauri\web" -Destination (Join-Path $archiveDir "web")

$version = (Get-Content "$env:GITHUB_WORKSPACE\desktop\src-tauri\tauri.conf.json" -Raw | ConvertFrom-Json).version
$archive = Join-Path $releaseDir "argus_${version}_windows_${{ matrix.archive_arch }}.zip"
Compress-Archive -Path (Join-Path $archiveDir "argus.exe"), (Join-Path $archiveDir "web") -DestinationPath $archive -Force
Write-Host "Created $archive"

- name: Upload Windows CLI archive to release
if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Windows'
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$version = (Get-Content "desktop/src-tauri/tauri.conf.json" -Raw | ConvertFrom-Json).version
$tag = "argus-v$version"
$archive = Get-ChildItem -Path "$env:GITHUB_WORKSPACE\release-artifacts" -Filter "*.zip" |
Select-Object -First 1 -ExpandProperty FullName
if (-not $archive) {
throw "No Windows CLI archive was found to upload."
}
gh release upload $tag --repo $env:GITHUB_REPOSITORY --clobber $archive

# tauri-action uploads the unsigned bundle while creating the draft release. Replace that
# asset with the signed file before publish-release makes the draft public. workflow_dispatch
# has no release, so its upload-artifact step below receives the signed file directly.
- name: Replace Windows release assets with signed installers
if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Windows'
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$version = (Get-Content "desktop/src-tauri/tauri.conf.json" -Raw | ConvertFrom-Json).version
$tag = "argus-v$version"
$installers = @(Get-ChildItem -Path "$env:GITHUB_WORKSPACE\desktop\src-tauri\target" -Filter "*-setup.exe" -Recurse |
Select-Object -ExpandProperty FullName
)
if ($installers.Count -eq 0) {
throw "No Windows installer was found to upload."
}
gh release upload $tag --repo $env:GITHUB_REPOSITORY --clobber $installers

# Stage the Windows installer with a stable name so archive:false can upload the raw file.
- name: Stage Windows installer for workflow artifact
if: ${{ !startsWith(github.ref, 'refs/tags/') && runner.os == 'Windows' }}
shell: pwsh
run: |
$destination = Join-Path $env:GITHUB_WORKSPACE "release-artifacts"
$filename = "argus-${{ matrix.slug }}-setup.exe"
$installers = @(Get-ChildItem -Path "$env:GITHUB_WORKSPACE\desktop\src-tauri\target" -Filter "*-setup.exe" -Recurse)
if ($installers.Count -eq 0) {
throw "No Windows installer was found to package."
}
if ($installers.Count -ne 1) {
throw "Expected one Windows installer, found $($installers.Count)."
}
foreach ($installer in $installers) {
Copy-Item -LiteralPath $installer.FullName -Destination (Join-Path $destination $filename) -Force
}

# The artifact action can upload one file without wrapping it in a ZIP. Give the DMG a
# stable name because archive:false uses the file name as the artifact name.
- name: Stage macOS DMG for workflow artifact
if: ${{ !startsWith(github.ref, 'refs/tags/') && runner.os == 'macOS' }}
shell: bash
run: |
set -euo pipefail
destination="$GITHUB_WORKSPACE/release-artifacts/argus-macos-universal.dmg"
mkdir -p "$(dirname "$destination")"
dmg_count="$(find desktop/src-tauri/target -path '*/bundle/dmg/*.dmg' -type f | wc -l | tr -d '[:space:]')"
if [ "$dmg_count" -ne 1 ]; then
echo "Expected one macOS DMG, found $dmg_count." >&2
exit 1
fi
dmg="$(find desktop/src-tauri/target -path '*/bundle/dmg/*.dmg' -type f -print -quit)"
cp "$dmg" "$destination"
echo "Staged $destination"

# Dry-run mode only: expose the desktop installers on the workflow run so they can be
# downloaded and tested on real machines before any tag exists. Standalone Windows CLI
# ZIPs are published only on tagged releases, not as workflow artifacts.
- name: Upload macOS DMG as workflow artifact
if: ${{ !startsWith(github.ref, 'refs/tags/') && runner.os == 'macOS' }}
uses: actions/upload-artifact@v7
with:
if-no-files-found: error
path: release-artifacts/argus-macos-universal.dmg
archive: false

- name: Upload Windows installer as workflow artifact
if: ${{ !startsWith(github.ref, 'refs/tags/') && runner.os == 'Windows' }}
uses: actions/upload-artifact@v7
with:
name: argus-${{ matrix.slug }}
if-no-files-found: error
path: |
desktop/src-tauri/target/**/bundle/macos/*.app.tar.gz*
desktop/src-tauri/target/**/bundle/dmg/*.dmg
desktop/src-tauri/target/**/bundle/nsis/*-setup.exe*
path: release-artifacts/argus-${{ matrix.slug }}-setup.exe
archive: false

# Tag pushes create the release as a draft; flip it public only when every platform leg
# succeeded, so a failed leg can never leave a half-populated public release (and the
Expand All @@ -209,7 +387,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
HAS_UPDATER_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY != '' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Publish the draft release
run: |
set -euo pipefail
Expand Down
Loading