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/actions/sign/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Sign Windows artifacts
description: Sign and verify binaries or MSIs using psign and Azure Trusted Signing.
description: Sign and verify Windows binaries and packages using psign and Azure Trusted Signing.
inputs:
files:
description: Newline-separated file paths or patterns to sign.
Expand Down
150 changes: 150 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,153 @@ jobs:
name: jiffshot-${{ matrix.rid }}-msi
path: msi/*.msi
if-no-files-found: error

msix:
name: ${{ matrix.rid }} MSIX
environment:
name: ${{ inputs.signing_environment || '' }}
permissions:
contents: read
id-token: write
needs: build
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- rid: win-x64
architecture: x64
test: true
- rid: win-arm64
architecture: arm64
test: false
steps:
- uses: actions/checkout@v7

- uses: actions/download-artifact@v8
with:
name: jiffshot-${{ matrix.rid }}
path: bin

- uses: actions/download-artifact@v8
with:
name: jiffshot-win-x64
path: logo-bin

- name: Build the MSIX
shell: pwsh
env:
EXPECTED_VERSION: ${{ inputs.version }}
PACKAGE_ARCHITECTURE: ${{ matrix.architecture }}
run: |
$ErrorActionPreference = 'Stop'
$cargoToml = Get-Content -LiteralPath Cargo.toml -Raw
if ($cargoToml -notmatch '(?ms)^\[workspace\.package\].*?^\s*version\s*=\s*"(?<v>[0-9]+\.[0-9]+\.[0-9]+)"') {
throw 'Could not read [workspace.package] version from Cargo.toml'
}
$workspaceVersion = $Matches['v']
$expectedVersion = if ([string]::IsNullOrWhiteSpace($env:EXPECTED_VERSION)) {
$workspaceVersion
} elseif ($env:EXPECTED_VERSION -ne $workspaceVersion) {
throw "Input version '$env:EXPECTED_VERSION' does not match Cargo.toml '$workspaceVersion'"
} else {
$env:EXPECTED_VERSION
}
$stage = 'msix\stage'
$assets = Join-Path $stage 'Assets'
New-Item -ItemType Directory -Force -Path $assets | Out-Null
$binaries = @(Get-ChildItem -LiteralPath bin -File |
Where-Object { $_.Name -eq 'jiffshot.exe' -or $_.Extension -eq '.dll' })
if (-not ($binaries | Where-Object { $_.Name -eq 'jiffshot.exe' })) { throw 'jiffshot.exe is missing from the build artifact' }
if (-not ($binaries | Where-Object { $_.Name -eq 'rustolonia_host.dll' })) { throw 'rustolonia_host.dll is missing from the build artifact' }
$binaries | Copy-Item -Destination $stage
# Logo rendering is architecture-independent, but the ARM64 executable
# cannot run on the x64 GitHub-hosted runner.
$exe = (Resolve-Path 'logo-bin\jiffshot.exe').Path
foreach ($logo in @(
@{ Size = 150; Name = 'Square150x150Logo.png' },
@{ Size = 44; Name = 'Square44x44Logo.png' },
@{ Size = 50; Name = 'StoreLogo.png' }
)) {
$logoPath = Join-Path $assets $logo.Name
$process = Start-Process -FilePath $exe `
-ArgumentList @('--write-logo', [string]$logo.Size, "`"$logoPath`"") `
-Wait -PassThru
if ($process.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $logoPath -PathType Leaf)) {
throw "Failed to generate MSIX logo $($logo.Name)"
}
}
$signature = Get-AuthenticodeSignature -LiteralPath $exe
$publisher = if ($signature.Status -eq 'Valid' -and $null -ne $signature.SignerCertificate) {
$signature.SignerCertificate.Subject
} else {
'CN=Devolutions Jiffshot CI'
}
[xml]$manifest = Get-Content -LiteralPath package\AppxManifest.xml -Raw
$namespace = [System.Xml.XmlNamespaceManager]::new($manifest.NameTable)
$namespace.AddNamespace('appx', 'http://schemas.microsoft.com/appx/manifest/foundation/windows10')
$identity = $manifest.SelectSingleNode('/appx:Package/appx:Identity', $namespace)
if ($null -eq $identity) { throw 'Package identity is missing from package\AppxManifest.xml' }
$identity.SetAttribute('Version', "$expectedVersion.0")
$identity.SetAttribute('ProcessorArchitecture', $env:PACKAGE_ARCHITECTURE)
$identity.SetAttribute('Publisher', $publisher)
$manifest.Save((Join-Path $stage 'AppxManifest.xml'))
$sdkBin = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
$makeAppx = Get-ChildItem -LiteralPath $sdkBin -Directory |
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'x64\makeappx.exe') } |
Sort-Object { [version]$_.Name } -Descending |
Select-Object -First 1 |
ForEach-Object { Join-Path $_.FullName 'x64\makeappx.exe' }
if (-not $makeAppx) { throw "Windows SDK MakeAppx.exe was not found under $sdkBin" }
$output = "msix\Devolutions.Jiffshot-$env:PACKAGE_ARCHITECTURE-$expectedVersion.msix"
& $makeAppx pack /d $stage /p $output /o
if ($LASTEXITCODE -ne 0) { throw "MakeAppx failed with exit code $LASTEXITCODE" }
Get-Item -LiteralPath $output | Format-Table Name, Length

- name: Sign MSIX
if: github.event_name != 'pull_request' && inputs.signing_environment != ''
uses: ./.github/actions/sign
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }}
TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }}
TRUSTED_SIGNING_TIMESTAMP_SERVER: ${{ vars.TRUSTED_SIGNING_TIMESTAMP_SERVER || 'http://timestamp.acs.microsoft.com/' }}
with:
install_test_signing_root: ${{ inputs.signing_environment == 'publish-test' }}
files: msix/*.msix

- name: Install and uninstall
if: matrix.test && github.event_name != 'pull_request' && inputs.signing_environment != ''
shell: pwsh
env:
EXPECTED_VERSION: ${{ inputs.version }}
run: |
$ErrorActionPreference = 'Stop'
$version = if ([string]::IsNullOrWhiteSpace($env:EXPECTED_VERSION)) {
if ((Get-Content -LiteralPath Cargo.toml -Raw) -notmatch '(?ms)^\[workspace\.package\].*?^\s*version\s*=\s*"(?<v>[0-9]+\.[0-9]+\.[0-9]+)"') {
throw 'Could not read [workspace.package] version from Cargo.toml'
}
$Matches['v']
} else {
$env:EXPECTED_VERSION
}
$msix = "msix\Devolutions.Jiffshot-x64-$version.msix"
Add-AppxPackage -Path $msix -ErrorAction Stop
$installed = Get-AppxPackage -Name Devolutions.Jiffshot
if ($null -eq $installed -or $installed.Version.ToString() -ne "$version.0") {
throw "The installed Jiffshot MSIX does not match version $version"
}
if (-not (Test-Path -LiteralPath (Join-Path $installed.InstallLocation 'jiffshot.exe'))) {
throw 'The installed MSIX does not contain jiffshot.exe'
}
Remove-AppxPackage -Package $installed.PackageFullName -ErrorAction Stop
if (Get-AppxPackage -Name Devolutions.Jiffshot) { throw 'Jiffshot MSIX is still installed after removal' }

- uses: actions/upload-artifact@v7
with:
name: jiffshot-${{ matrix.rid }}-msix
path: msix/*.msix
if-no-files-found: error
9 changes: 6 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,16 @@ jobs:
$portable = "artifacts\jiffshot-win-$arch"
$msiName = "Devolutions.Jiffshot-$arch-$env:RELEASE_VERSION.msi"
$msi = "artifacts\jiffshot-win-$arch-msi\$msiName"
foreach ($required in @("$portable\jiffshot.exe", "$portable\rustolonia_host.dll", "$portable\README-portable.txt", $msi)) {
$msixName = "Devolutions.Jiffshot-$arch-$env:RELEASE_VERSION.msix"
$msix = "artifacts\jiffshot-win-$arch-msix\$msixName"
foreach ($required in @("$portable\jiffshot.exe", "$portable\rustolonia_host.dll", "$portable\README-portable.txt", $msi, $msix)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing release file: $required" }
}
$signedTargets = @(
Get-ChildItem -Path $portable -File |
Where-Object { $_.Extension -in @('.exe', '.dll') } |
ForEach-Object { $_.FullName }
) + @($msi)
) + @($msi, $msix)
foreach ($path in $signedTargets) {
$signature = Get-AuthenticodeSignature -LiteralPath $path
if ($signature.Status -ne 'Valid' `
Expand All @@ -215,8 +217,9 @@ jobs:
throw "Release jiffshot.exe ($arch) version ($($exeInfo.FileVersion) / $($exeInfo.ProductVersion)) does not match $env:RELEASE_VERSION"
}
Copy-Item -LiteralPath $msi -Destination "release\$msiName"
Copy-Item -LiteralPath $msix -Destination "release\$msixName"
Compress-Archive -Path "$portable\*" `
-DestinationPath "release\Devolutions.Jiffshot-$arch-$env:RELEASE_VERSION-portable.zip"
-DestinationPath "release\Devolutions.Jiffshot-$arch-$env:RELEASE_VERSION.zip"
}
Get-ChildItem release -File | Sort-Object Name | ForEach-Object {
'{0} {1}' -f (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant(), $_.Name
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ Prebuilt, Authenticode-signed packages for **Windows 10 and Windows 11** (`x64`
|---|---|---|
| **`Devolutions.Jiffshot-x64-<version>.msi`** | Windows x64 (Intel / AMD) | Standard Windows Installer. Installs to `C:\Program Files\Devolutions\Jiffshot`, adds a Start Menu shortcut, and associates `.jsd` Jiffshot Document files. |
| **`Devolutions.Jiffshot-arm64-<version>.msi`** | Windows ARM64 | Standard Windows Installer for Windows on ARM devices. |
| **`Devolutions.Jiffshot-x64-<version>-portable.zip`** | Windows x64 (Intel / AMD) | Portable archive—unzip anywhere and run `jiffshot.exe`. Nothing is installed in the system. |
| **`Devolutions.Jiffshot-arm64-<version>-portable.zip`** | Windows ARM64 | Portable archive for Windows on ARM devices. |
| **`Devolutions.Jiffshot-x64-<version>.msix`** | Windows x64 (Intel / AMD) | Signed MSIX package. Install it to add Jiffshot to Windows; uninstall it from Installed apps. |
| **`Devolutions.Jiffshot-arm64-<version>.msix`** | Windows ARM64 | Signed MSIX package for Windows on ARM devices. |
| **`Devolutions.Jiffshot-x64-<version>.zip`** | Windows x64 (Intel / AMD) | Portable archive—unzip anywhere and run `jiffshot.exe`. Nothing is installed in the system. |
| **`Devolutions.Jiffshot-arm64-<version>.zip`** | Windows ARM64 | Portable archive for Windows on ARM devices. |

> **Note for Windows "N" editions:** Video recording uses Windows Media Foundation, which is omitted from Windows "N" SKUs by default. Install Microsoft's *Media Feature Pack* in Windows Optional Features to enable MP4 recording and playback.

Expand Down Expand Up @@ -266,7 +268,7 @@ Jiffshot is written in Rust with its UI built on [Rustolonia](https://github.com
| `crates/jiffshot-doc/` | Documents: annotation objects, rendering, editing and undo, effects, smart tools, `.jsd` and image files |
| `crates/jiffshot-workflow/` | The capture side: tray, hotkeys, selection overlay, recording, capture panels, post-capture workflow, command-line modes |
| `app/` | `jiffshot.exe`: the editor's model (`src/app.rs`, view models in `src/vm.rs`), every window (`src/ui/`) and the capture side, in one process |
| `package/` | The MSI installer, built with WixSharp |
| `package/` | The MSI installer (WixSharp) and MSIX manifest |

### Compiling

Expand All @@ -285,7 +287,7 @@ rustup target add aarch64-pc-windows-msvc
cargo build --release -p jiffshot --target aarch64-pc-windows-msvc
```

CI (`.github/workflows/ci.yml`) builds both and uploads them as portable zips (`jiffshot-win-x64`, `jiffshot-win-arm64`). To move to a newer Rustolonia, change the tag on the two lines in `Cargo.toml`'s `[workspace.dependencies]`.
CI (`.github/workflows/ci.yml`) builds both architectures and uploads portable ZIP, MSI, and MSIX artifacts (`jiffshot-win-x64`, `jiffshot-win-arm64`, and their `-msi` / `-msix` variants). To move to a newer Rustolonia, change the tag on the two lines in `Cargo.toml`'s `[workspace.dependencies]`.

### Logo Assets

Expand Down Expand Up @@ -316,11 +318,11 @@ The MSI lands in `package/out` as `Devolutions.Jiffshot-x64-0.1.0.msi` (with the

### Releases & Code Signing

`ci.yml` builds and tests unsigned portable packages and MSIs for pushes to master, pull requests, and manual runs. `release.yml` reuses those jobs with signing enabled: it signs the portable executable and Rustolonia host DLL before building the MSI, then signs the MSI. The x64 MSI is installed and uninstalled before the release can proceed.
`ci.yml` builds and tests unsigned portable packages, MSIs, and MSIX packages for pushes to master, pull requests, and manual runs. `release.yml` reuses those jobs with signing enabled: it signs the portable executable and Rustolonia host DLL before building the installers, then signs the MSI and MSIX. The x64 MSI is installed and uninstalled before the release can proceed; signed MSIX release builds are also installed and uninstalled.

Run **Release** from GitHub Actions with a version matching `Cargo.toml` (for example, `0.1.0`, without `v`). Update and commit the workspace version before releasing a new version. Dry runs are enabled by default and upload signed packages without creating a tag or release. The `github_env` input selects the signing environment independently of `dry_run`: on master, `auto` (the default) and `prod` use `publish-prod`, while `test` uses `publish-test`. Other branches always use `publish-test` and only permit dry runs, even if `prod` is requested. For test signing, the workflow retrieves the `publish-test` profile root with psign and trusts it only in each ephemeral runner's current user certificate store, so Authenticode validation remains enabled. A non-dry-run dispatch with `github_env=test` publishes test-signed artifacts that are not publicly trusted. The workflow rejects non-dry-run releases from other branches, version mismatches, and existing tags.

Both modes retain the portable and MSI Actions artifacts plus a `release-assets` artifact containing x64/ARM64 MSIs, portable ZIPs, and `SHA256SUMS.txt`. After all builds, signing, and validation succeed, a non-dry-run creates `v<version>` at the dispatched commit and publishes a GitHub Release with those same files attached. Do not create the tag manually.
Both modes retain the portable, MSI, and MSIX Actions artifacts plus a `release-assets` artifact containing x64/ARM64 MSIs, MSIX packages, ZIP archives, and `SHA256SUMS.txt`. After all builds, signing, and validation succeed, a non-dry-run creates `v<version>` at the dispatched commit and publishes a GitHub Release with those same files attached. Do not create the tag manually.

```powershell
# Validate with test signing without publishing (defaults to dry_run=true).
Expand Down
36 changes: 36 additions & 0 deletions package/AppxManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="Devolutions.Jiffshot"
Publisher="CN=Devolutions Jiffshot"
Version="0.1.0.0"
ProcessorArchitecture="neutral" />
<Properties>
<DisplayName>Devolutions Jiffshot</DisplayName>
<PublisherDisplayName>Devolutions Inc.</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
</Resources>
<Applications>
<Application Id="Jiffshot" Executable="jiffshot.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="Devolutions Jiffshot"
Description="Screen capture, video recording, and image and video editing."
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png" />
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>