Repository navigation
Include platforms in release asset names #140
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |