Skip to content

Include platforms in release asset names #140

Include platforms in release asset names

Include platforms in release asset names #140

Workflow file for this run

name: CI
on:
push:
branches: [master]
pull_request:
workflow_dispatch:
workflow_call:
inputs:
version:
description: Version to stamp and verify on built binaries; defaults to Cargo.toml
type: string
default: ''
signing_environment:
description: GitHub environment for signing; empty produces unsigned CI artifacts
type: string
default: ''
require_macos_notarization:
description: Require macOS signing and notarization credentials before building
type: boolean
default: false
permissions:
contents: read
concurrency:
group: build-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
name: ${{ matrix.rid }}
environment:
name: ${{ inputs.signing_environment || '' }}
permissions:
contents: read
id-token: write
# win-arm64 is cross-built on x64 (Visual Studio's ARM64 tools are on the
# image), so it is built but not run.
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- rid: win-x64
target: x86_64-pc-windows-msvc
test: true
- rid: win-arm64
target: aarch64-pc-windows-msvc
test: false
env:
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v7
- name: Set up Rust
shell: pwsh
run: |
# rust-toolchain.toml selects the channel; rustup installs it here.
rustup show
rustup target add ${{ matrix.target }}
cargo --version
rustc --version
- name: Cache Cargo and the Rustolonia host
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/AppData/Local/rustolonia/host
target
key: ${{ matrix.rid }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: ${{ matrix.rid }}-
- name: Build
env:
JIFFSHOT_VERSION: ${{ inputs.version }}
run: cargo build --release --locked -p jiffshot -p jiffshot-cli --target ${{ matrix.target }}
- name: Test
if: matrix.test
env:
JIFFSHOT_VERSION: ${{ inputs.version }}
run: cargo test --release --locked --workspace --target ${{ matrix.target }}
- name: Agent CLI artifact workflow
if: matrix.test
shell: pwsh
run: |
$env:JIFFSHOT_CLI_E2E = Join-Path (Get-Location) 'target\${{ matrix.target }}\release\jiffshot-cli.exe'
cargo test --release --locked -p jiffshot-workflow --target ${{ matrix.target }} cli_edits_artifacts_over_real_ipc_without_desktop_input -- --ignored --test-threads=1
- name: Package
shell: pwsh
env:
EXPECTED_VERSION: ${{ inputs.version }}
run: |
$ErrorActionPreference = 'Stop'
# The build script puts the Rustolonia host next to jiffshot.exe.
$out = "target/${{ matrix.target }}/release"
$dir = "jiffshot-${{ matrix.rid }}"
New-Item -ItemType Directory -Path $dir | Out-Null
Copy-Item "$out/jiffshot.exe", "$out/jiffshot-cli.exe", "$out/*.dll", "app/README-portable.txt" -Destination $dir
$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
}
$expectedFileVersion = "$expectedVersion.0"
$exeInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path "$dir/jiffshot.exe"))
if ($exeInfo.CompanyName -ne 'Devolutions Inc.') {
throw "jiffshot.exe CompanyName '$($exeInfo.CompanyName)' does not match 'Devolutions Inc.'"
}
if ($exeInfo.ProductName -ne 'Devolutions Jiffshot') {
throw "jiffshot.exe ProductName '$($exeInfo.ProductName)' does not match 'Devolutions Jiffshot'"
}
if ([string]::IsNullOrWhiteSpace($exeInfo.FileDescription)) {
throw 'jiffshot.exe is missing FileDescription'
}
if ($exeInfo.OriginalFilename -ne 'jiffshot.exe') {
throw "jiffshot.exe OriginalFilename '$($exeInfo.OriginalFilename)' does not match 'jiffshot.exe'"
}
if ($exeInfo.FileVersion -ne $expectedFileVersion) {
throw "jiffshot.exe FileVersion '$($exeInfo.FileVersion)' does not match '$expectedFileVersion'"
}
if ($exeInfo.ProductVersion -ne $expectedVersion) {
throw "jiffshot.exe ProductVersion '$($exeInfo.ProductVersion)' does not match '$expectedVersion'"
}
if ($cargoToml -notmatch 'rustolonia\s*=\s*\{[^}]*tag\s*=\s*"v(?<rv>[0-9]+\.[0-9]+\.[0-9]+)"') {
throw 'Could not read rustolonia tag from Cargo.toml'
}
$rustoloniaVersion = $Matches['rv']
$hostInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path "$dir/rustolonia_host.dll"))
if ($hostInfo.FileVersion -ne "$rustoloniaVersion.0") {
throw "rustolonia_host.dll FileVersion '$($hostInfo.FileVersion)' does not match '$rustoloniaVersion.0'"
}
if (-not $hostInfo.ProductVersion.StartsWith($rustoloniaVersion)) {
throw "rustolonia_host.dll ProductVersion '$($hostInfo.ProductVersion)' does not start with '$rustoloniaVersion'"
}
Write-Host "Version OK: jiffshot.exe FileVersion=$($exeInfo.FileVersion) ProductVersion=$($exeInfo.ProductVersion); rustolonia_host.dll FileVersion=$($hostInfo.FileVersion)"
Get-ChildItem $dir | Format-Table Name, Length
- name: Sign portable binaries
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: |
jiffshot-${{ matrix.rid }}\jiffshot.exe
jiffshot-${{ matrix.rid }}\jiffshot-cli.exe
jiffshot-${{ matrix.rid }}\rustolonia_host.dll
- uses: actions/upload-artifact@v7
with:
name: jiffshot-${{ matrix.rid }}
path: jiffshot-${{ matrix.rid }}
if-no-files-found: error
linux:
name: linux-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: ubuntu-24.04
machine: x86_64
- arch: arm64
runner: ubuntu-24.04-arm
machine: aarch64
env:
CARGO_TERM_COLOR: always
JIFFSHOT_VERSION: ${{ inputs.version }}
JIFFSHOT_TEST_ARCH: ${{ matrix.arch }}
steps:
- uses: actions/checkout@v7
- name: Assert native runner architecture
run: test "$(uname -m)" = "${{ matrix.machine }}"
- name: Install Linux build and media dependencies
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y \
clang \
gcc \
libclang-dev \
libleptonica-dev \
libtesseract-dev \
tesseract-ocr-eng \
pkg-config \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
gstreamer1.0-tools \
gstreamer1.0-pipewire \
gstreamer1.0-pulseaudio \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-ugly \
gstreamer1.0-libav \
dpkg-dev \
desktop-file-utils \
xvfb \
x11-utils \
xclip \
libxtst6 \
libgtk-3-0t64 \
dbus
- name: Set up PowerShell and Pester
uses: ./.github/actions/setup-pwsh
- name: Set up Rust
run: |
rustup show
rustup component add clippy
cargo --version
rustc --version
- name: Cache Cargo and the Rustolonia host
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cache/rustolonia/host
target
key: ubuntu2404-linux-${{ matrix.arch }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: ubuntu2404-linux-${{ matrix.arch }}-
- name: Test
run: cargo test --release --locked --workspace
- name: Lint Linux app
run: cargo clippy --locked --no-deps -p jiffshot --all-targets -- -D warnings
- name: Build Linux app
run: cargo build --release --locked -p jiffshot
- name: Package Ubuntu tar.gz and DEB
run: pwsh -NoProfile -File package/linux/build.ps1 -Arch "${{ matrix.arch }}" -Format tar.gz,deb
- name: Test Linux package builder and contents
env:
JIFFSHOT_TEST_FORMATS: tar.gz,deb
run: pwsh -NoProfile -File package/linux/run-tests.ps1
- uses: actions/upload-artifact@v7
with:
name: jiffshot-linux-${{ matrix.arch }}
path: package/out/linux/*
if-no-files-found: error
linux-rpm:
name: linux-${{ matrix.arch }} Fedora RPM
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: ubuntu-24.04
machine: x86_64
- arch: arm64
runner: ubuntu-24.04-arm
machine: aarch64
container: fedora:43
env:
CARGO_TERM_COLOR: always
JIFFSHOT_VERSION: ${{ inputs.version }}
JIFFSHOT_TEST_ARCH: ${{ matrix.arch }}
steps:
- name: Assert native Fedora container architecture
run: test "$(uname -m)" = "${{ matrix.machine }}"
- name: Install native Fedora build and packaging dependencies
run: |
dnf install --setopt=install_weak_deps=False -y \
git tar gzip curl ca-certificates gcc-c++ clang-devel dnf5-plugins \
leptonica-devel tesseract-devel pkgconf-pkg-config \
gstreamer1-devel gstreamer1-plugins-base-devel \
libicu binutils rpm-build cpio desktop-file-utils
# Enable this before installing libav/FFmpeg, which can require OpenH264.
dnf config-manager setopt fedora-cisco-openh264.enabled=1
dnf install --setopt=install_weak_deps=False -y \
gstreamer1-plugins-base gstreamer1-plugins-good \
gstreamer1-plugins-bad-free gstreamer1-plugin-libav ffmpeg-free \
pipewire-gstreamer tesseract tesseract-langpack-eng \
fontconfig dejavu-sans-fonts
- name: Enable Fedora 43 RPM Fusion media codecs
run: |
test "$(rpm -E %fedora)" = 43
dnf install --setopt=install_weak_deps=False -y \
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-43.noarch.rpm
# Follow RPM Fusion's supported full-codec swap, not Fedora's limited build.
dnf swap --setopt=install_weak_deps=False -y ffmpeg-free ffmpeg --allowerasing
dnf install --setopt=install_weak_deps=False -y \
gstreamer1-plugins-ugly gstreamer1-plugins-bad-freeworld
- name: Verify Fedora media and OCR dependency providers
run: |
# Fedora 43 supplies fdkaacenc; the shared app selector keeps voaacenc
# first on Ubuntu. Require the real Fedora provider, never an encoder alias.
# Workspace media tests also encode, parse and decode the selected AAC.
for element in jpegenc pngenc mp4mux x264enc h264parse aacparse \
fdkaacenc avdec_h264 avdec_aac pipewiresrc pulsesrc audiomixer; do
gst-inspect-1.0 --exists "$element"
done
languages=$(tesseract --list-langs)
printf '%s\n' "$languages" | grep -Fx eng
family=$(fc-match -f '%{family}\n' 'DejaVu Sans')
printf '%s\n' "$family" | grep -Fx 'DejaVu Sans'
- uses: actions/checkout@v7
- name: Set up PowerShell and Pester
uses: ./.github/actions/setup-pwsh
- name: Set up Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs |
sh -s -- -y --profile minimal --default-toolchain none
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
"$HOME/.cargo/bin/rustup" show
- name: Cache Cargo and the Rustolonia host
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cache/rustolonia/host
target
key: fedora43-linux-${{ matrix.arch }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: fedora43-linux-${{ matrix.arch }}-
- name: Test against native Fedora runtime libraries
run: cargo test --release --locked --workspace
- name: Build against Fedora runtime libraries
run: cargo build --release --locked -p jiffshot
- name: Package native Fedora RPM
run: pwsh -NoProfile -File package/linux/build.ps1 -Arch "${{ matrix.arch }}" -Format rpm -OutputDir package/out/linux-rpm
- name: Test RPM builder and contents
env:
JIFFSHOT_TEST_FORMATS: rpm
JIFFSHOT_PACKAGE_DIR: package/out/linux-rpm
run: pwsh -NoProfile -File package/linux/run-tests.ps1
- uses: actions/upload-artifact@v7
with:
name: jiffshot-linux-${{ matrix.arch }}-rpm
path: package/out/linux-rpm/*
if-no-files-found: error
linux-package-install:
name: Install Linux packages (${{ matrix.arch }}, ${{ matrix.format }})
needs: [linux, linux-rpm]
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- format: deb
arch: x64
runner: ubuntu-24.04
machine: x86_64
package_arch: amd64
image: ubuntu:24.04
artifact: jiffshot-linux-x64
- format: rpm
arch: x64
runner: ubuntu-24.04
machine: x86_64
package_arch: x86_64
image: fedora:43
artifact: jiffshot-linux-x64-rpm
- format: deb
arch: arm64
runner: ubuntu-24.04-arm
machine: aarch64
package_arch: arm64
image: ubuntu:24.04
artifact: jiffshot-linux-arm64
- format: rpm
arch: arm64
runner: ubuntu-24.04-arm
machine: aarch64
package_arch: aarch64
image: fedora:43
artifact: jiffshot-linux-arm64-rpm
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: ${{ matrix.artifact }}
path: package/out/install
- name: Download PowerShell for the clean install container
uses: ./.github/actions/setup-pwsh
with:
install-dir: ${{ runner.temp }}/powershell
force: 'true'
link: 'false'
pester: 'false'
- name: Clean install, desktop registration, and uninstall
run: |
test "$(uname -m)" = "${{ matrix.machine }}"
docker run --rm --volume "$PWD:/work:ro" \
--volume "$RUNNER_TEMP/powershell:/opt/microsoft/powershell/7:ro" "${{ matrix.image }}" \
bash -ceu '
test "$(uname -m)" = "$2"
cd /work/package/out/install
sha256sum --check "SHA256SUMS-linux-$4.txt"
if [ "$1" = deb ]; then
apt-get update -qq
apt-get install --no-install-recommends -y \
/work/package/out/install/*.deb libicu74
test "$(dpkg-query -W -f=\${Architecture} jiffshot)" = "$3"
else
dnf install --setopt=install_weak_deps=False -y \
/work/package/out/install/*.rpm libicu
test "$(rpm -q --queryformat "%{ARCH}" jiffshot)" = "$3"
fi
ln -s /opt/microsoft/powershell/7/pwsh /usr/local/bin/pwsh
pwsh -NoProfile -File /work/package/linux/validate-installed.ps1 -Arch "$4"
grep -F "application/x-jiffshot=" /usr/share/applications/mimeinfo.cache
grep -F "application/x-jiffshot" /usr/share/mime/globs2
mkdir -p /root/.config/Jiffshot
touch /root/.config/Jiffshot/package-removal-sentinel
if [ "$1" = deb ]; then
apt-get remove -y jiffshot
else
rpm -V jiffshot
dnf remove -y jiffshot
fi
test ! -e /usr/bin/jiffshot
test ! -e /usr/share/applications/com.devolutions.Jiffshot.desktop
test -e /root/.config/Jiffshot/package-removal-sentinel
' -- "${{ matrix.format }}" "${{ matrix.machine }}" "${{ matrix.package_arch }}" "${{ matrix.arch }}"
macos:
name: macOS ${{ matrix.arch }}
runs-on: macos-15
environment:
name: ${{ inputs.signing_environment || '' }}
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
target: aarch64-apple-darwin
- arch: x86_64
target: x86_64-apple-darwin
env:
CARGO_TERM_COLOR: always
MACOSX_DEPLOYMENT_TARGET: '13.0'
JIFFSHOT_VERSION: ${{ inputs.version }}
RUSTOLONIA_HOST_LIB: ${{ github.workspace }}/target/${{ matrix.target }}/release/librustolonia_host.dylib
steps:
- uses: actions/checkout@v7
- name: Select macOS signing
id: signing
if: github.event_name != 'pull_request' && inputs.signing_environment != ''
shell: pwsh
env:
REQUIRE_MACOS_NOTARIZATION: ${{ inputs.require_macos_notarization }}
APPLE_APP_DEV_ID_APP_CERTIFICATE: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }}
APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }}
APPLE_BOT_PASSWORD: ${{ secrets.APPLE_BOT_PASSWORD }}
DEVOLUTIONSBOT_TOKEN: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_DEVELOPER_ID: ${{ secrets.APPLE_DEVELOPER_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: pwsh -NoLogo -NoProfile -File package/macos/select-signing.ps1
- name: Set up Rust
run: |
rustup show
rustup target add ${{ matrix.target }}
- name: Cache Cargo and the Rustolonia host
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/Library/Caches/rustolonia/host
target
key: macos-${{ matrix.arch }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: macos-${{ matrix.arch }}-
- name: Build
run: cargo build --release --locked -p jiffshot -p jiffshot-cli --target ${{ matrix.target }}
- name: Test
if: matrix.arch == 'arm64'
run: |
cargo test --release --locked --workspace --target ${{ matrix.target }}
- name: Test macOS PowerShell scripts
if: matrix.arch == 'arm64'
shell: pwsh
env:
PESTER_VERSION: 5.9.1
PESTER_SHA256: 8dd4060fc3bc895f05bd655e4ab82abe346de54a4e6415dfb727cc8378672b69
run: |
$ErrorActionPreference = 'Stop'
$version = $env:PESTER_VERSION
$package = Join-Path $env:RUNNER_TEMP "pester.$version.nupkg"
Invoke-WebRequest -Uri "https://www.powershellgallery.com/api/v2/package/Pester/$version" -OutFile $package
$actual = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -cne $env:PESTER_SHA256) { throw "Pester $version package hash mismatch: $actual" }
$moduleRoot = Join-Path $env:RUNNER_TEMP 'PesterModules'
$destination = Join-Path $moduleRoot "Pester/$version"
[IO.Compression.ZipFile]::ExtractToDirectory($package, $destination)
$env:PSModulePath = "$moduleRoot$([IO.Path]::PathSeparator)$env:PSModulePath"
Import-Module Pester -RequiredVersion $version
& ./package/macos/run-tests.ps1
- name: Checkout shared Apple signing actions
if: steps.signing.outputs.kind == 'unigetui'
uses: actions/checkout@v7
with:
repository: Devolutions/actions
ref: v1
token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }}
path: .ci/devolutions-actions
persist-credentials: false
- name: Configure shared Apple signing
id: shared_signing
if: steps.signing.outputs.kind == 'unigetui'
uses: ./.ci/devolutions-actions/macos-code-sign-setup
with:
base64_certificate: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }}
certificate_password: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }}
- name: Extend shared signing keychain timeout
if: steps.signing.outputs.kind == 'unigetui'
run: security set-keychain-settings -lut 21600 build.keychain
- name: Configure legacy Apple signing
id: legacy_signing
if: steps.signing.outputs.kind == 'legacy'
env:
CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
IDENTITY: ${{ secrets.APPLE_DEVELOPER_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
shell: bash
run: |
set -euo pipefail
if [[ -z "$CERTIFICATE$CERTIFICATE_PASSWORD$IDENTITY$APPLE_ID$TEAM_ID$APP_PASSWORD" ]]; then
echo 'enabled=false' >> "$GITHUB_OUTPUT"
echo 'notarize=false' >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ -z "$CERTIFICATE" || -z "$CERTIFICATE_PASSWORD" || -z "$IDENTITY" ]]; then
echo 'Apple signing requires APPLE_CERTIFICATE_BASE64, APPLE_CERTIFICATE_PASSWORD, and APPLE_DEVELOPER_ID.' >&2
exit 1
fi
if [[ -n "$APPLE_ID$TEAM_ID$APP_PASSWORD" && ( -z "$APPLE_ID" || -z "$TEAM_ID" || -z "$APP_PASSWORD" ) ]]; then
echo 'Notarization requires APPLE_ID, APPLE_TEAM_ID, and APPLE_APP_PASSWORD.' >&2
exit 1
fi
keychain="$RUNNER_TEMP/jiffshot-signing.keychain-db"
certificate="$RUNNER_TEMP/jiffshot-signing.p12"
trap 'rm -f "$certificate"' EXIT
password="$(openssl rand -base64 32)"
echo "::add-mask::$password"
printf '%s' "$CERTIFICATE" | base64 --decode > "$certificate"
security create-keychain -p "$password" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$password" "$keychain"
security import "$certificate" -k "$keychain" -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$password" "$keychain"
echo 'enabled=true' >> "$GITHUB_OUTPUT"
if [[ -n "$APPLE_ID" ]]; then
xcrun notarytool store-credentials jiffshot-ci --keychain "$keychain" --apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$APP_PASSWORD"
echo 'notarize=true' >> "$GITHUB_OUTPUT"
else
echo 'notarize=false' >> "$GITHUB_OUTPUT"
fi
- name: Package and verify
env:
IDENTITY: "${{ steps.signing.outputs.enabled == 'true' && (secrets.APPLE_DEVELOPER_ID || vars.APPLE_DEVELOPER_ID || 'Developer ID Application: Devolutions inc.') || '' }}"
SIGNING_KIND: ${{ steps.signing.outputs.kind }}
SIGNING_ENABLED: ${{ steps.signing.outputs.enabled }}
NOTARIZE: ${{ steps.signing.outputs.notarize }}
shell: bash
run: |
set -euo pipefail
args=(-Binary "target/${{ matrix.target }}/release/jiffshot" -Output macos-package -Arch "${{ matrix.arch }}" -RequireNativePopupGate -RequireNativeTermination -RequireNativeFileDrag)
if [[ "$SIGNING_ENABLED" == true ]]; then
args+=(-Identity "$IDENTITY")
if [[ "$SIGNING_KIND" == legacy ]]; then
args+=(-Keychain "$RUNNER_TEMP/jiffshot-signing.keychain-db")
fi
fi
if [[ "$NOTARIZE" == true ]]; then
if [[ "$SIGNING_KIND" == legacy ]]; then
args+=(-NotaryProfile jiffshot-ci)
elif [[ "$SIGNING_KIND" == unigetui ]]; then
args+=(-PrepareForExternalNotarization)
else
echo "Unknown macOS signing mode: $SIGNING_KIND" >&2
exit 1
fi
fi
pwsh -NoLogo -NoProfile -File package/macos/build.ps1 "${args[@]}"
codesign --verify --deep --strict macos-package/Jiffshot.app
if [[ "$SIGNING_KIND" == unigetui && "$NOTARIZE" == true ]]; then
ditto -c -k --keepParent macos-package/Jiffshot.app "$RUNNER_TEMP/Jiffshot-app.zip"
fi
- name: Notarize the macOS app
if: steps.signing.outputs.kind == 'unigetui' && steps.signing.outputs.notarize == 'true'
uses: ./.ci/devolutions-actions/macos-notarize
with:
apple_password: ${{ secrets.APPLE_BOT_PASSWORD }}
package_path: ${{ runner.temp }}/Jiffshot-app.zip
skip_checksum_validation: 'true'
skip_stapling: 'true'
- name: Build disk image from notarized app
id: disk_image
if: steps.signing.outputs.kind == 'unigetui' && steps.signing.outputs.notarize == 'true'
env:
IDENTITY: "${{ secrets.APPLE_DEVELOPER_ID || vars.APPLE_DEVELOPER_ID || 'Developer ID Application: Devolutions inc.' }}"
shell: bash
run: |
set -euo pipefail
pwsh -NoLogo -NoProfile -File package/macos/staple.ps1 -Path macos-package/Jiffshot.app
pwsh -NoLogo -NoProfile -File package/macos/build-notarized-dmg.ps1 \
-App macos-package/Jiffshot.app -Output macos-package -Arch "${{ matrix.arch }}" -Identity "$IDENTITY"
shopt -s nullglob
images=(macos-package/*.dmg)
if [[ ${#images[@]} -ne 1 ]]; then
echo 'Expected exactly one macOS disk image.' >&2
exit 1
fi
printf 'path=%s\n' "${images[0]}" >> "$GITHUB_OUTPUT"
- name: Notarize the macOS disk image
if: steps.signing.outputs.kind == 'unigetui' && steps.signing.outputs.notarize == 'true'
uses: ./.ci/devolutions-actions/macos-notarize
with:
apple_password: ${{ secrets.APPLE_BOT_PASSWORD }}
package_path: ${{ steps.disk_image.outputs.path }}
skip_stapling: 'true'
- name: Verify macOS package
env:
SIGNING_ENABLED: ${{ steps.signing.outputs.enabled }}
NOTARIZE: ${{ steps.signing.outputs.notarize }}
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
images=(macos-package/*.dmg)
if [[ ${#images[@]} -ne 1 ]]; then
echo 'Expected exactly one macOS disk image.' >&2
exit 1
fi
codesign --verify --deep --strict macos-package/Jiffshot.app
hdiutil verify "${images[0]}"
status=adhoc
if [[ "$SIGNING_ENABLED" == true ]]; then
codesign --verify --strict "${images[0]}"
status=signed
fi
if [[ "$NOTARIZE" == true ]]; then
pwsh -NoLogo -NoProfile -File package/macos/staple.ps1 -Path "${images[0]}"
pwsh -NoLogo -NoProfile -File package/macos/staple.ps1 -Validate -Path macos-package/Jiffshot.app
pwsh -NoLogo -NoProfile -File package/macos/staple.ps1 -Validate -Path "${images[0]}"
status=notarized
fi
mountpoint="$RUNNER_TEMP/jiffshot-dmg-${{ matrix.arch }}"
mkdir "$mountpoint"
hdiutil attach -nobrowse -readonly -mountpoint "$mountpoint" "${images[0]}"
trap 'hdiutil detach "$mountpoint"' EXIT
codesign --verify --deep --strict "$mountpoint/Jiffshot.app"
if [[ "$NOTARIZE" == true ]]; then
pwsh -NoLogo -NoProfile -File package/macos/staple.ps1 -Validate -Path "$mountpoint/Jiffshot.app"
fi
hdiutil detach "$mountpoint"
trap - EXIT
rmdir "$mountpoint"
printf '%s\n' "$status" > macos-package/signing.txt
(cd "target/${{ matrix.target }}/release" && shasum -a 256 \
librustolonia_host.dylib libAvaloniaNative.dylib libSkiaSharp.dylib libHarfBuzzSharp.dylib) \
> macos-package/host-SHA256SUMS.txt
- uses: actions/upload-artifact@v7
with:
name: jiffshot-macos-${{ matrix.arch }}
path: |
macos-package/*.dmg
macos-package/signing.txt
macos-package/host-SHA256SUMS.txt
if-no-files-found: error
- name: Remove temporary legacy signing keychain
if: always() && steps.signing.outputs.kind == 'legacy'
run: |
if [[ -f "$RUNNER_TEMP/jiffshot-signing.keychain-db" ]]; then
security delete-keychain "$RUNNER_TEMP/jiffshot-signing.keychain-db"
fi
- name: Remove shared signing keychain
if: always() && steps.signing.outputs.kind == 'unigetui' && steps.shared_signing.outcome != 'skipped'
run: |
if [[ -f certificate.p12 ]]; then
rm -f -- certificate.p12
fi
keychain="$HOME/Library/Keychains/build.keychain-db"
if [[ -f "$keychain" ]]; then
security delete-keychain "$keychain"
fi
msi:
name: ${{ matrix.rid }} MSI
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
platform: x64
test: true
- rid: win-arm64
platform: arm64
test: false
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: jiffshot-${{ matrix.rid }}
path: bin
- name: Install WiX Toolset
shell: pwsh
run: |
winget install --id WiXToolset.WiXCLI --version 5.0.2.0 --exact --source winget `
--accept-source-agreements --accept-package-agreements
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$wixDir = Join-Path $env:ProgramFiles 'WiX Toolset v5.0\bin'
$wixExe = Join-Path $wixDir 'wix.exe'
if (-not (Test-Path -LiteralPath $wixExe -PathType Leaf)) {
throw "WiX CLI was not installed at $wixExe"
}
$wixVersion = (& $wixExe --version).Trim()
if ($LASTEXITCODE -ne 0 -or $wixVersion -notmatch '^5\.0\.2(?:\b|[+-])') {
throw "Expected WiX CLI 5.0.2, got '$wixVersion'"
}
$wixDir >> $env:GITHUB_PATH
- name: Build the MSI
shell: pwsh
env:
EXPECTED_VERSION: ${{ inputs.version }}
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'
}
$expectedVersion = if ([string]::IsNullOrWhiteSpace($env:EXPECTED_VERSION)) {
$Matches['v']
} elseif ($env:EXPECTED_VERSION -ne $Matches['v']) {
throw "Input version '$env:EXPECTED_VERSION' does not match Cargo.toml '$($Matches['v'])'"
} else {
$env:EXPECTED_VERSION
}
dotnet build package -c Release -p:Version=$expectedVersion
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$pkgExe = Resolve-Path 'package/bin/Release/net472/Jiffshot.Package.exe'
$pkgInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($pkgExe)
if ($pkgInfo.CompanyName -ne 'Devolutions Inc.') {
throw "Jiffshot.Package.exe CompanyName '$($pkgInfo.CompanyName)' does not match 'Devolutions Inc.'"
}
if ($pkgInfo.ProductName -ne 'Devolutions Jiffshot') {
throw "Jiffshot.Package.exe ProductName '$($pkgInfo.ProductName)' does not match 'Devolutions Jiffshot'"
}
if ($pkgInfo.FileVersion -ne "$expectedVersion.0") {
throw "Jiffshot.Package.exe FileVersion '$($pkgInfo.FileVersion)' does not match '$expectedVersion.0'"
}
if (-not $pkgInfo.ProductVersion.StartsWith($expectedVersion)) {
throw "Jiffshot.Package.exe ProductVersion '$($pkgInfo.ProductVersion)' does not start with '$expectedVersion'"
}
& $pkgExe -bin bin -platform ${{ matrix.platform }} -version $expectedVersion -out msi
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Get-ChildItem msi | Format-Table Name, Length
- name: Sign MSI
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: msi\*.msi
- name: Install and uninstall
if: matrix.test
shell: pwsh
env:
SIGNING_ENVIRONMENT: ${{ inputs.signing_environment }}
run: |
$msi = (Get-ChildItem msi/*.msi).FullName
$exe = "$env:ProgramFiles\Devolutions\Jiffshot\jiffshot.exe"
$hostDll = "$env:ProgramFiles\Devolutions\Jiffshot\rustolonia_host.dll"
$cli = "$env:ProgramFiles\Devolutions\Jiffshot\jiffshot-cli.exe"
function Invoke-Msiexec($arguments) {
$p = Start-Process msiexec.exe -ArgumentList $arguments -Wait -PassThru
if ($p.ExitCode -ne 0) { Get-Content msiexec.log -Tail 50; throw "msiexec $arguments exited with $($p.ExitCode)" }
}
Invoke-Msiexec "/i `"$msi`" /qn /l*v msiexec.log"
if (-not (Test-Path $exe)) { throw "$exe is missing" }
if (-not (Test-Path $hostDll)) { throw "the Rustolonia host is missing" }
if (-not (Test-Path $cli)) { throw "$cli is missing" }
# With no Jiffshot running, the CLI reports it as unavailable (exit code 3).
$reply = & $cli status --no-launch | ConvertFrom-Json
if ($LASTEXITCODE -ne 3 -or $reply.error.code -ne 'unavailable') { throw "jiffshot-cli status: exit $LASTEXITCODE, $($reply | ConvertTo-Json -Compress)" }
$global:LASTEXITCODE = 0
if ('${{ github.event_name }}' -ne 'pull_request' -and -not [string]::IsNullOrWhiteSpace($env:SIGNING_ENVIRONMENT)) {
foreach ($installedPath in @($exe, $cli, $hostDll)) {
$sig = Get-AuthenticodeSignature -LiteralPath $installedPath
if ($sig.Status -ne 'Valid' -or $null -eq $sig.SignerCertificate -or $null -eq $sig.TimeStamperCertificate) {
throw "Installed binary signature verification failed for ${installedPath}: $($sig.StatusMessage)"
}
}
}
$product = @(Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' |
Where-Object { $_.DisplayName -eq 'Devolutions Jiffshot' })
if ($product.Count -ne 1) { throw 'Expected one Devolutions Jiffshot entry in Programs and Features' }
if ($product[0].Publisher -ne 'Devolutions Inc.') { throw "Expected Publisher 'Devolutions Inc.', got '$($product[0].Publisher)'" }
$installedVer = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($exe)
if ($installedVer.ProductVersion -ne $product[0].DisplayVersion -or $installedVer.FileVersion -ne "$($product[0].DisplayVersion).0") {
throw "Installed jiffshot.exe version ($($installedVer.FileVersion) / $($installedVer.ProductVersion)) does not match MSI DisplayVersion '$($product[0].DisplayVersion)'"
}
$open = (Get-ItemProperty "HKLM:\Software\Classes\jiffshot.Document\shell\open\command").'(default)'
if ($open -notlike "*jiffshot.exe*") { throw "the .jsd association is wrong: $open" }
Get-ChildItem "$env:ProgramFiles\Devolutions\Jiffshot" | Format-Table Name, Length
Invoke-Msiexec "/x `"$msi`" /qn /l*v msiexec.log"
if (Test-Path $exe) { throw "$exe is still there after uninstalling" }
- uses: actions/upload-artifact@v7
with:
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 -in @('jiffshot.exe', 'jiffshot-cli.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 'jiffshot-cli.exe' })) { throw 'jiffshot-cli.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: Sign a temporary MSIX for installation testing
if: matrix.test && (github.event_name == 'pull_request' || inputs.signing_environment == '')
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$packages = @(Get-ChildItem -LiteralPath msix -Filter '*.msix' -File)
if ($packages.Count -ne 1) { throw "Expected one MSIX, found $($packages.Count)" }
$testPackage = Join-Path $env:RUNNER_TEMP 'jiffshot-install-test.msix'
Copy-Item -LiteralPath $packages[0].FullName -Destination $testPackage
[xml]$manifest = Get-Content -LiteralPath msix\stage\AppxManifest.xml -Raw
$publisher = $manifest.Package.Identity.Publisher
$certificate = New-SelfSignedCertificate -Type CodeSigningCert -Subject $publisher `
-CertStoreLocation 'Cert:\CurrentUser\My' -KeyExportPolicy NonExportable -NotAfter (Get-Date).AddDays(1)
"JIFFSHOT_MSIX_TEST_CERT=$($certificate.Thumbprint)" >> $env:GITHUB_ENV
$publicCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
$certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('TrustedPeople', 'LocalMachine')
try {
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$store.Add($publicCertificate)
} finally {
$store.Dispose()
$publicCertificate.Dispose()
}
$sdkBin = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
$signTool = Get-ChildItem -LiteralPath $sdkBin -Directory |
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'x64\signtool.exe') } |
Sort-Object { [version]$_.Name } -Descending |
Select-Object -First 1 |
ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' }
if (-not $signTool) { throw "Windows SDK SignTool.exe was not found under $sdkBin" }
& $signTool sign /fd SHA256 /s My /sha1 $certificate.Thumbprint $testPackage
if ($LASTEXITCODE -ne 0) { throw "SignTool failed with exit code $LASTEXITCODE" }
$signature = Get-AuthenticodeSignature -LiteralPath $testPackage
if ($signature.Status -ne 'Valid') { throw "Test MSIX signature verification failed: $($signature.StatusMessage)" }
"JIFFSHOT_MSIX_TEST_PATH=$testPackage" >> $env:GITHUB_ENV
- name: Install and uninstall
if: matrix.test
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 = if ([string]::IsNullOrWhiteSpace($env:JIFFSHOT_MSIX_TEST_PATH)) {
"msix\Devolutions.Jiffshot-x64-$version.msix"
} else {
$env:JIFFSHOT_MSIX_TEST_PATH
}
if (Get-AppxPackage -Name Devolutions.Jiffshot) { throw 'Jiffshot MSIX is already installed on the test runner' }
$added = $false
try {
Add-AppxPackage -Path $msix -ErrorAction Stop
$added = $true
$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'
}
$cli = Join-Path $installed.InstallLocation 'jiffshot-cli.exe'
$alias = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\jiffshot-cli.exe'
foreach ($cliPath in @($cli, $alias)) {
if (-not (Test-Path -LiteralPath $cliPath -PathType Leaf)) {
throw "The installed Jiffshot CLI is missing: $cliPath"
}
$help = & $cliPath --help-agent | Out-String
if ($LASTEXITCODE -ne 0 -or $help -notmatch '(?m)^jiffshot-cli for AI agents\r?$') {
throw "The installed Jiffshot CLI failed to run at ${cliPath} (exit $LASTEXITCODE)"
}
}
} finally {
if ($added) {
$installed = Get-AppxPackage -Name Devolutions.Jiffshot
if ($null -ne $installed) { Remove-AppxPackage -Package $installed.PackageFullName -ErrorAction Stop }
}
}
if (Get-AppxPackage -Name Devolutions.Jiffshot) { throw 'Jiffshot MSIX is still installed after removal' }
- name: Remove MSIX test certificate
if: always() && matrix.test && env.JIFFSHOT_MSIX_TEST_CERT != ''
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$trusted = "Cert:\LocalMachine\TrustedPeople\$env:JIFFSHOT_MSIX_TEST_CERT"
$personal = "Cert:\CurrentUser\My\$env:JIFFSHOT_MSIX_TEST_CERT"
if (Test-Path -LiteralPath $trusted) { Remove-Item -LiteralPath $trusted -ErrorAction Stop }
if (Test-Path -LiteralPath $personal) { Remove-Item -LiteralPath $personal -DeleteKey -ErrorAction Stop }
- uses: actions/upload-artifact@v7
with:
name: jiffshot-${{ matrix.rid }}-msix
path: msix/*.msix
if-no-files-found: error