Repository navigation
feat(core,gui): allow middle click gesture mode #4038
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: Build | |
| # Reusable build workflow. `release.yml` calls it with `sign: true` to produce | |
| # the signed/notarized installers it publishes; PRs labeled `needs: build` and | |
| # manual dispatches build the SAME installers UNSIGNED as run artifacts. | |
| # Every signing step and the protected `release` environment are gated on | |
| # `inputs.sign`, so an unsigned run needs no secrets at all — fork PRs (which | |
| # get none) build fine. | |
| on: | |
| workflow_call: | |
| inputs: | |
| sign: | |
| description: Sign and notarize the installers (release only). | |
| type: boolean | |
| required: false | |
| default: false | |
| outputs: | |
| # The caller's publish gate ships the macOS DMG even when a Windows/Linux | |
| # leg failed, so it needs each leg's result — a single reusable-workflow | |
| # job result would be `failure` if any platform failed, collapsing that | |
| # graceful degradation. Sourced from the always()-running `results` job. | |
| macos_result: | |
| description: Result of the macOS build job. | |
| value: ${{ jobs.results.outputs.macos }} | |
| windows_result: | |
| description: Result of the Windows EXE build job. | |
| value: ${{ jobs.results.outputs.windows }} | |
| windows_msi_result: | |
| description: Result of the Windows MSI build job. | |
| value: ${{ jobs.results.outputs.windows_msi }} | |
| linux_result: | |
| description: Result of the Linux packages build job. | |
| value: ${{ jobs.results.outputs.linux }} | |
| # PR builds are opt-in: add the `needs: build` label to produce unsigned | |
| # installers for testing. `closed` / unlabel cancel an in-flight run via | |
| # concurrency (jobs below no-op on those actions). | |
| pull_request: | |
| types: [opened, synchronize, reopened, labeled, unlabeled, closed] | |
| workflow_dispatch: | |
| inputs: | |
| sign: | |
| description: Sign and notarize (needs the release-environment secrets). | |
| type: boolean | |
| default: false | |
| concurrency: | |
| # PR number keeps the group stable across open/sync/label/close — on a | |
| # merged close, github.ref becomes the base branch, which would not match | |
| # the in-flight PR run. Non-PR events fall back to ref+event. | |
| group: build-${{ github.event.pull_request.number || format('{0}-{1}', github.ref, github.event_name) }} | |
| # Cancel only when this event should supersede a PR build. Unrelated | |
| # label add/remove must not kill an in-flight installer build. | |
| # Keep this unquoted so the expression stays a boolean (a quoted "false" | |
| # string is truthy). format() avoids a bare `needs: build` colon that YAML | |
| # would parse as a mapping. | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' && (github.event.action == 'synchronize' || github.event.action == 'reopened' || github.event.action == 'closed' || (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) && github.event.label.name == format('needs{0} build', ':'))) }} | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_INCREMENTAL: 0 | |
| MACOSX_DEPLOYMENT_TARGET: "13.0" | |
| DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer | |
| RUSTC_WRAPPER: sccache | |
| SCCACHE_GHA_ENABLED: "true" | |
| # PR merge refs and release tags cannot warm another ref. Only dispatches | |
| # and other branch-scoped runs write reusable compiler objects. | |
| SCCACHE_GHA_RW_MODE: ${{ (github.event_name != 'pull_request' && github.ref_type != 'tag') && 'READ_WRITE' || 'READ_ONLY' }} | |
| jobs: | |
| # Opt-in gate for PR builds. Non-PR triggers (workflow_call / dispatch) | |
| # always pass. closed/unlabeled still start a run so concurrency can cancel | |
| # the previous one, but this job fails the gate and all build jobs skip. | |
| should-build: | |
| name: Should build | |
| runs-on: ubuntu-latest | |
| outputs: | |
| run: ${{ steps.gate.outputs.run }} | |
| steps: | |
| - id: gate | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| EVENT_ACTION: ${{ github.event.action }} | |
| # format() keeps `needs: build` out of bare YAML (colon). | |
| HAS_BUILD_LABEL: ${{ contains(github.event.pull_request.labels.*.name, format('needs{0} build', ':')) }} | |
| EVENT_LABEL: ${{ github.event.label.name }} | |
| BUILD_LABEL: ${{ format('needs{0} build', ':') }} | |
| run: | | |
| if [ "$EVENT_NAME" != "pull_request" ]; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$EVENT_ACTION" = "closed" ] || [ "$EVENT_ACTION" = "unlabeled" ]; then | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$EVENT_ACTION" = "labeled" ] && [ "$EVENT_LABEL" != "$BUILD_LABEL" ]; then | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ "$HAS_BUILD_LABEL" = "true" ]; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| macos: | |
| name: macOS DMG (${{ matrix.arch }}) | |
| needs: should-build | |
| if: ${{ needs.should-build.outputs.run == 'true' }} | |
| # Apple Silicon compiles both slices. Building the Intel slice on the arm64 | |
| # runner is supported by Rust and Xcode and avoids the much slower hosted | |
| # Intel runner; every bundled Mach-O is checked below before it can ship. | |
| runs-on: macos-latest | |
| # Bound a stuck brew/linker/notarize hang so it can't hold a PR build slot | |
| # for the 6-hour runner default (the signed path also notarizes). | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: arm64 | |
| target: aarch64-apple-darwin | |
| - arch: x86_64 | |
| target: x86_64-apple-darwin | |
| permissions: | |
| contents: read | |
| env: | |
| OPENLOGI_BUNDLE_ASSETS: ${{ vars.OPENLOGI_BUNDLE_ASSETS }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 | |
| with: | |
| version: "v0.16.0" | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| prefix-key: v2-cargo | |
| # Release profile (fat LTO) — never share with CI debug keys. | |
| shared-key: macos-stable-release | |
| cache-targets: false | |
| # PR and tag caches are isolated to that ref. Restore there, but only | |
| # save Cargo inputs from branch-scoped dispatches/callers. | |
| save-if: ${{ github.event_name != 'pull_request' && github.ref_type != 'tag' }} | |
| - name: Verify runner architecture | |
| run: test "$(uname -m)" = arm64 | |
| # Pin both target builds to an Icon Composer-capable Xcode instead of | |
| # relying on whatever `/Applications/Xcode.app` points at today. | |
| - name: Select the Xcode both legs build with | |
| run: | | |
| # An unmatched glob stays literal, which `-d` then rejects — where | |
| # `ls` would have failed the step under the shell's `-e -o pipefail` | |
| # before this check could say what is actually wrong. | |
| xcode=$(printf '%s\n' /Applications/Xcode_26*.app | sort -V | tail -1) | |
| if [ ! -d "$xcode" ]; then | |
| echo "No Xcode 26 on this runner; the app icon cannot be compiled." >&2 | |
| exit 1 | |
| fi | |
| echo "OPENLOGI_DEVELOPER_DIR=$xcode/Contents/Developer" >> "$GITHUB_ENV" | |
| echo "Building with $xcode" | |
| - name: Load release secrets from 1Password | |
| id: load_secrets | |
| if: ${{ inputs.sign }} | |
| uses: 1password/load-secrets-action@70062d7a876d3eb6334754fa26efd2fbd90c32f2 # v5.0.1 | |
| with: | |
| export-env: false | |
| env: | |
| OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_SIGNING_IDENTITY | |
| APPLE_CERTIFICATE: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_CERTIFICATE | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_CERTIFICATE_PASSWORD | |
| APPLE_ID: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_ID | |
| APPLE_PASSWORD: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_PASSWORD | |
| APPLE_TEAM_ID: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_TEAM_ID | |
| GITHUB_APP_ID: ${{ secrets.OP_GITHUB_APP_ITEM }}/GITHUB_APP_ID | |
| GITHUB_APP_PRIVATE_KEY: ${{ secrets.OP_GITHUB_APP_ITEM }}/GITHUB_APP_PRIVATE_KEY | |
| OPENLOGI_UPDATE_BASE_URL: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_BASE_URL | |
| OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY | |
| - name: Cache Homebrew downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| ~/Library/Caches/Homebrew | |
| ~/Library/Caches/Homebrew/downloads | |
| key: brew-macos-${{ runner.arch }}-librsvg-create-dmg-cargo-bundle-v2 | |
| restore-keys: | | |
| brew-macos-${{ runner.arch }}- | |
| - name: Install packaging tools | |
| run: | | |
| # cargo-bundle has bottles for both hosted macOS architectures. | |
| # Installing the bottle avoids compiling the tool from source in | |
| # every release leg before OpenLogi itself can start building. | |
| brew install librsvg create-dmg cargo-bundle | |
| - name: Validate release secrets | |
| if: ${{ inputs.sign }} | |
| env: | |
| APPLE_SIGNING_IDENTITY: ${{ steps.load_secrets.outputs.APPLE_SIGNING_IDENTITY }} | |
| APPLE_CERTIFICATE: ${{ steps.load_secrets.outputs.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ steps.load_secrets.outputs.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_ID: ${{ steps.load_secrets.outputs.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ steps.load_secrets.outputs.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ steps.load_secrets.outputs.APPLE_TEAM_ID }} | |
| GITHUB_APP_ID: ${{ steps.load_secrets.outputs.GITHUB_APP_ID }} | |
| GITHUB_APP_PRIVATE_KEY: ${{ steps.load_secrets.outputs.GITHUB_APP_PRIVATE_KEY }} | |
| OPENLOGI_UPDATE_BASE_URL: ${{ steps.load_secrets.outputs.OPENLOGI_UPDATE_BASE_URL }} | |
| OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.load_secrets.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }} | |
| run: | | |
| set -euo pipefail | |
| : "${APPLE_SIGNING_IDENTITY:?Configure APPLE_SIGNING_IDENTITY in 1Password}" | |
| : "${APPLE_CERTIFICATE:?Configure APPLE_CERTIFICATE in 1Password}" | |
| : "${APPLE_CERTIFICATE_PASSWORD:?Configure APPLE_CERTIFICATE_PASSWORD in 1Password}" | |
| : "${APPLE_ID:?Configure APPLE_ID in 1Password}" | |
| : "${APPLE_PASSWORD:?Configure APPLE_PASSWORD in 1Password}" | |
| : "${APPLE_TEAM_ID:?Configure APPLE_TEAM_ID in 1Password}" | |
| : "${GITHUB_APP_ID:?Configure GITHUB_APP_ID in 1Password}" | |
| : "${GITHUB_APP_PRIVATE_KEY:?Configure GITHUB_APP_PRIVATE_KEY in 1Password}" | |
| : "${OPENLOGI_UPDATE_BASE_URL:?Configure OPENLOGI_UPDATE_BASE_URL in 1Password}" | |
| : "${OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY:?Configure OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY in 1Password}" | |
| - name: Import Apple signing certificate | |
| if: ${{ inputs.sign }} | |
| uses: apple-actions/import-codesign-certs@v7 | |
| with: | |
| p12-file-base64: ${{ steps.load_secrets.outputs.APPLE_CERTIFICATE }} | |
| p12-password: ${{ steps.load_secrets.outputs.APPLE_CERTIFICATE_PASSWORD }} | |
| - name: Build and package OpenLogi | |
| env: | |
| APPLE_SIGNING_IDENTITY: ${{ steps.load_secrets.outputs.APPLE_SIGNING_IDENTITY }} | |
| OPENLOGI_UPDATE_BASE_URL: ${{ steps.load_secrets.outputs.OPENLOGI_UPDATE_BASE_URL }} | |
| OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.load_secrets.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }} | |
| run: | | |
| set -euo pipefail | |
| export OPENLOGI_UPDATE_MANIFEST_URL="${OPENLOGI_UPDATE_BASE_URL%/}/channels/stable/latest.json" | |
| if [ "${{ inputs.sign }}" = "true" ]; then | |
| cargo run -p xtask -- macos package --target ${{ matrix.target }} --sign-identity "$APPLE_SIGNING_IDENTITY" | |
| else | |
| cargo run -p xtask -- macos package --target ${{ matrix.target }} | |
| fi | |
| - name: Verify bundled architectures | |
| env: | |
| EXPECTED_ARCH: ${{ matrix.arch }} | |
| run: | | |
| set -euo pipefail | |
| app="target/release/bundle/osx/OpenLogi.app" | |
| binaries=( | |
| "$app/Contents/MacOS/openlogi-desktop" | |
| "$app/Contents/MacOS/openlogi" | |
| "$app/Contents/Library/LoginItems/OpenLogi Agent.app/Contents/MacOS/openlogi-agent" | |
| "$app/Contents/Library/LoginItems/OpenLogi Overlay.app/Contents/MacOS/openlogi-overlay" | |
| ) | |
| for binary in "${binaries[@]}"; do | |
| archs=$(lipo -archs "$binary") | |
| echo "$binary: $archs" | |
| test "$archs" = "$EXPECTED_ARCH" | |
| done | |
| # xtask stamps and verifies this itself; re-checking the artifact here is | |
| # deliberate redundancy, because a bundle that ships with the `.dev` | |
| # identity voids every user's permission grants and moves them to a | |
| # different config directory (releases 0.6.24–0.6.26). | |
| - name: Verify production bundle identities | |
| run: | | |
| set -euo pipefail | |
| app="target/release/bundle/osx/OpenLogi.app" | |
| plist() { /usr/libexec/PlistBuddy -c "Print :$2" "$1/Contents/Info.plist"; } | |
| check() { # bundle expected-id expected-name | |
| [ "$(plist "$1" CFBundleIdentifier)" = "$2" ] | |
| [ "$(plist "$1" CFBundleName)" = "$3" ] | |
| [ "$(plist "$1" CFBundleDisplayName)" = "$3" ] | |
| [ -f "$1/Contents/Resources/AppIcon.icns" ] | |
| } | |
| check "$app" "org.openlogi.openlogi" "OpenLogi" | |
| check "$app/Contents/Library/LoginItems/OpenLogi Agent.app" "org.openlogi.agent" "OpenLogi Agent" | |
| check "$app/Contents/Library/LoginItems/OpenLogi Overlay.app" "org.openlogi.overlay" "OpenLogi Overlay" | |
| - name: Verify signed app bundle | |
| if: ${{ inputs.sign }} | |
| run: | | |
| set -euo pipefail | |
| app="target/release/bundle/osx/OpenLogi.app" | |
| helper="$app/Contents/Library/LoginItems/OpenLogi Agent.app" | |
| overlay="$app/Contents/Library/LoginItems/OpenLogi Overlay.app" | |
| cli="$app/Contents/MacOS/openlogi" | |
| codesign --verify --strict --verbose=2 "$app" | |
| [ -d "$helper" ] && codesign --verify --strict --verbose=2 "$helper" | |
| [ -d "$overlay" ] && codesign --verify --strict --verbose=2 "$overlay" | |
| [ -f "$cli" ] && codesign --verify --strict --verbose=2 "$cli" | |
| # Fail the release if camera access would still be blocked at runtime. | |
| codesign -d --entitlements - "$app" 2>/dev/null \ | |
| | grep -q 'com.apple.security.device.camera' | |
| - name: Notarize DMG | |
| if: ${{ inputs.sign }} | |
| env: | |
| APPLE_ID: ${{ steps.load_secrets.outputs.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ steps.load_secrets.outputs.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ steps.load_secrets.outputs.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| xcrun notarytool submit target/release/OpenLogi.dmg \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --wait | |
| - name: Staple DMG | |
| if: ${{ inputs.sign }} | |
| run: | | |
| set -euo pipefail | |
| xcrun stapler staple target/release/OpenLogi.dmg | |
| xcrun stapler validate target/release/OpenLogi.dmg | |
| - name: Collect DMG artifact | |
| run: | | |
| mkdir -p dist | |
| ref_name="${GITHUB_REF_NAME:-dev}" | |
| ref_name="${ref_name//\//-}" | |
| cp target/release/OpenLogi.dmg "dist/OpenLogi-${ref_name}-macos-${{ matrix.arch }}.dmg" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: OpenLogi-macos-dmg-${{ matrix.arch }} | |
| path: dist/*.dmg | |
| # 'warn' (the default) would let an empty-dist job stay green while | |
| # `results.macos` reports success and the publish gate ships nothing — | |
| # same reasoning as the windows zip upload. | |
| if-no-files-found: error | |
| windows: | |
| name: Windows EXE (${{ matrix.arch }}) | |
| needs: should-build | |
| if: ${{ needs.should-build.outputs.run == 'true' }} | |
| # Both arches build on the x86_64 runner — arm64 as an MSVC cross-compile. | |
| # Native Arm runners are not an option: artifact-signing-action refuses to | |
| # run on them, while the signing itself is arch-agnostic over the PE file. | |
| runs-on: windows-2025 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: x86_64 | |
| target: x86_64-pc-windows-msvc | |
| - arch: arm64 | |
| target: aarch64-pc-windows-msvc | |
| # A hung signing/login call must become a normal failure (which publish | |
| # tolerates), not a 6-hour stall: publish waits for this job to reach a | |
| # terminal state before its own gate is even evaluated. The GUI release | |
| # build (thin LTO, codegen-units=1, the whole GPUI stack) dominates the | |
| # runtime on the 4-core runner, hence a looser budget than the ~8-minute | |
| # CLI-only runs needed. | |
| timeout-minutes: 60 | |
| env: | |
| # Static CRT: a bare exe must not depend on the VC++ Redistributable — | |
| # the v0.6.6 exe died on clean machines with "VCRUNTIME140.dll was not | |
| # found". The explicit --target below keeps the flag off host build | |
| # scripts, which RUSTFLAGS would otherwise also reach. | |
| RUSTFLAGS: -C target-feature=+crt-static | |
| permissions: | |
| # Mint an OIDC token so azure/login can exchange it for an Azure access token | |
| # via a federated credential — no client secret is stored in GitHub. | |
| id-token: write | |
| contents: read | |
| # The federated credential's subject is bound to this GitHub environment, so the | |
| # OIDC `sub` claim stays stable (repo:AprilNEA/OpenLogi:environment:release) | |
| # instead of changing with every tag ref. Only signed (release) runs enter it; | |
| # an unsigned run needs no secrets, so it takes no environment (fork PRs, which | |
| # can't access a protected environment at all, then build fine). | |
| environment: ${{ inputs.sign && 'release' || '' }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 | |
| with: | |
| version: "v0.16.0" | |
| # Both targets consume the same Cargo registry/git inputs. Compiler | |
| # outputs stay isolated by sccache's command and target-triple hashes. | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| prefix-key: v2-cargo | |
| shared-key: windows-stable-release | |
| cache-targets: false | |
| save-if: ${{ github.event_name != 'pull_request' && github.ref_type != 'tag' }} | |
| # Azure signing config lives in 1Password, matching the macOS job's secret flow. | |
| # load-secrets-action is a node action, so it runs on Windows runners too. The | |
| # client/tenant/subscription IDs are not secrets under OIDC, but are centralised | |
| # here so all release config has a single source of truth. | |
| - name: Load Azure signing config from 1Password | |
| id: azure_config | |
| if: ${{ inputs.sign }} | |
| uses: 1password/load-secrets-action@70062d7a876d3eb6334754fa26efd2fbd90c32f2 # v5.0.1 | |
| with: | |
| export-env: false | |
| env: | |
| OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} | |
| AZURE_CLIENT_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_CLIENT_ID | |
| AZURE_TENANT_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_TENANT_ID | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SUBSCRIPTION_ID | |
| AZURE_SIGNING_ENDPOINT: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SIGNING_ENDPOINT | |
| AZURE_SIGNING_ACCOUNT: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SIGNING_ACCOUNT | |
| AZURE_CERT_PROFILE: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_CERT_PROFILE | |
| # load-secrets-action silently skips values that aren't op:// references | |
| # (e.g. when OP_AZURE_SECRET_ITEM is unset) and emits empty outputs for | |
| # empty fields — without this gate the job only fails minutes later, in | |
| # azure/login, with an error that never names the real cause. | |
| - name: Validate Azure signing config | |
| if: ${{ inputs.sign }} | |
| shell: bash | |
| env: | |
| AZURE_CLIENT_ID: ${{ steps.azure_config.outputs.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ steps.azure_config.outputs.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ steps.azure_config.outputs.AZURE_SUBSCRIPTION_ID }} | |
| AZURE_SIGNING_ENDPOINT: ${{ steps.azure_config.outputs.AZURE_SIGNING_ENDPOINT }} | |
| AZURE_SIGNING_ACCOUNT: ${{ steps.azure_config.outputs.AZURE_SIGNING_ACCOUNT }} | |
| AZURE_CERT_PROFILE: ${{ steps.azure_config.outputs.AZURE_CERT_PROFILE }} | |
| run: | | |
| set -euo pipefail | |
| : "${AZURE_CLIENT_ID:?Configure AZURE_CLIENT_ID in the Azure 1Password item}" | |
| : "${AZURE_TENANT_ID:?Configure AZURE_TENANT_ID in the Azure 1Password item}" | |
| : "${AZURE_SUBSCRIPTION_ID:?Configure AZURE_SUBSCRIPTION_ID in the Azure 1Password item}" | |
| : "${AZURE_SIGNING_ENDPOINT:?Configure AZURE_SIGNING_ENDPOINT in the Azure 1Password item}" | |
| : "${AZURE_SIGNING_ACCOUNT:?Configure AZURE_SIGNING_ACCOUNT in the Azure 1Password item}" | |
| : "${AZURE_CERT_PROFILE:?Configure AZURE_CERT_PROFILE in the Azure 1Password item}" | |
| # Same R2 1Password item the macOS job reads: the minisign public key the | |
| # binary embeds and the base URL its manifest URL derives from. Without | |
| # this env the exe ships with no key, and the updater's Strict policy | |
| # fails every Windows check with "policy requires a minisign signature, | |
| # but no public key is configured" — auto-update never worked on Windows. | |
| - name: Load static updater config from 1Password | |
| id: update_config | |
| if: ${{ inputs.sign }} | |
| # Pinned to an immutable commit (matching the Crowdin workflow): this step runs | |
| # with the 1Password service-account token during signed release | |
| # builds, so a repointed mutable tag must not be able to swap the code. | |
| uses: 1password/load-secrets-action@70062d7a876d3eb6334754fa26efd2fbd90c32f2 # v5.0.1 | |
| with: | |
| export-env: false | |
| env: | |
| OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} | |
| OPENLOGI_UPDATE_BASE_URL: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_BASE_URL | |
| OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY | |
| # Same rationale as the Azure gate above: load-secrets emits empty | |
| # outputs on a missing item, and an empty key would rebuild the broken | |
| # no-key release instead of failing here with the cause named. | |
| - name: Validate updater config | |
| if: ${{ inputs.sign }} | |
| shell: bash | |
| env: | |
| OPENLOGI_UPDATE_BASE_URL: ${{ steps.update_config.outputs.OPENLOGI_UPDATE_BASE_URL }} | |
| OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.update_config.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }} | |
| run: | | |
| set -euo pipefail | |
| : "${OPENLOGI_UPDATE_BASE_URL:?Configure OPENLOGI_UPDATE_BASE_URL in the R2 1Password item}" | |
| : "${OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY:?Configure OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY in the R2 1Password item}" | |
| # The CLI ships inside the same zip and MSI rather than as a separate | |
| # artifact, so job and artifact names carry no GUI qualifier. The GUI is | |
| # a thin IPC client; the headless agent (ported in | |
| # #167) owns the device I/O, input hook, and IPC server, and the GUI | |
| # spawns a sibling `openlogi-agent.exe` when the pipe is absent — so the | |
| # agent MUST ship next to the GUI or the app connects to nothing (#347). | |
| # The updater env mirrors the macOS bundle step: openlogi-desktop reads the | |
| # key and manifest URL via option_env! at compile time. Unsigned builds | |
| # get empty values and keep the fail-closed dev behaviour, so the | |
| # manifest-URL export is guarded rather than derived from an empty base. | |
| - name: Build OpenLogi executables | |
| shell: bash | |
| env: | |
| OPENLOGI_UPDATE_BASE_URL: ${{ steps.update_config.outputs.OPENLOGI_UPDATE_BASE_URL }} | |
| OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.update_config.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }} | |
| run: | | |
| set -euo pipefail | |
| if [ -n "$OPENLOGI_UPDATE_BASE_URL" ]; then | |
| export OPENLOGI_UPDATE_MANIFEST_URL="${OPENLOGI_UPDATE_BASE_URL%/}/channels/stable/latest.json" | |
| fi | |
| cargo build --release -p openlogi-desktop -p openlogi-overlay -p openlogi-agent -p openlogi --target ${{ matrix.target }} | |
| # Federated (passwordless) login. Requires an app registration whose federated | |
| # credential subject is `repo:AprilNEA/OpenLogi:environment:release`, granted the | |
| # "Artifact Signing Certificate Profile Signer" role on the signing account. | |
| - name: Azure login (OIDC) | |
| if: ${{ inputs.sign }} | |
| uses: azure/login@v3 | |
| with: | |
| client-id: ${{ steps.azure_config.outputs.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ steps.azure_config.outputs.AZURE_TENANT_ID }} | |
| subscription-id: ${{ steps.azure_config.outputs.AZURE_SUBSCRIPTION_ID }} | |
| # artifact-signing-action installs its ArtifactSigning module from PSGallery, | |
| # which is intermittently missing on hosted windows runners (the action then | |
| # dies with "Unable to find repository 'PSGallery'" — actions/runner-images#13758). | |
| # That flake silently dropped the arm64 exe + msi from v0.6.14. Re-register the | |
| # default PSGallery so a mis-provisioned runner can't lose an artifact again. | |
| - name: Ensure PSGallery is registered | |
| if: ${{ inputs.sign }} | |
| shell: pwsh | |
| run: | | |
| if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { | |
| Register-PSRepository -Default | |
| } | |
| # Authenticode-signs via Artifact Signing (ex-Trusted Signing). The action only | |
| # runs on Windows runners. azure/login leaves an az CLI session that the action's | |
| # default AzureCliCredential picks up, so no azure-* secret inputs are needed. | |
| - name: Sign OpenLogi Windows executables with Artifact Signing | |
| if: ${{ inputs.sign }} | |
| uses: azure/artifact-signing-action@v2 | |
| with: | |
| endpoint: ${{ steps.azure_config.outputs.AZURE_SIGNING_ENDPOINT }} | |
| signing-account-name: ${{ steps.azure_config.outputs.AZURE_SIGNING_ACCOUNT }} | |
| certificate-profile-name: ${{ steps.azure_config.outputs.AZURE_CERT_PROFILE }} | |
| # `files` is a newline-separated list; one call signs every PE file. | |
| files: | | |
| ${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-desktop.exe | |
| ${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-overlay.exe | |
| ${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-agent.exe | |
| ${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi.exe | |
| # Timestamping is mandatory: without it the signature expires after | |
| # 3 days. The URL must stay http — SignTool rejects an https /tr up | |
| # front ("SignTool Error: Invalid Timestamp URL", v0.6.5) before | |
| # even contacting Azure. The RFC3161 response is itself signed, so | |
| # http transport does not weaken it. The file/timestamp digests stay | |
| # on the action's SHA256 defaults. | |
| timestamp-rfc3161: http://timestamp.acs.microsoft.com | |
| # Tag-only workflow: the action's four dependency caches save to | |
| # per-tag refs no future run can restore, so skip them. | |
| cache-dependencies: false | |
| - name: Verify the binaries are signed | |
| if: ${{ inputs.sign }} | |
| shell: pwsh | |
| run: | | |
| # The signing step above already fails on error; this guards against it | |
| # silently producing an unsigned or corrupted binary. NotSigned and | |
| # HashMismatch (bytes no longer match the signature) are trust-store | |
| # independent and always ship-blockers; NotTrusted/UnknownError depend | |
| # on the runner's trust store, so they stay log-only. | |
| foreach ($exe in 'openlogi-desktop.exe', 'openlogi-overlay.exe', 'openlogi-agent.exe', 'openlogi.exe') { | |
| $sig = Get-AuthenticodeSignature "target\${{ matrix.target }}\release\$exe" | |
| $sig | Format-List | |
| if ($null -eq $sig.SignerCertificate -or | |
| "$($sig.Status)" -in 'NotSigned', 'HashMismatch') { | |
| Write-Error "$exe Authenticode signature is invalid: $($sig.Status)" | |
| exit 1 | |
| } | |
| } | |
| # The portable artifact is a zip of every exe: a bare exe has nowhere to | |
| # put the agent, and GUI-only can never work (#347). Inside the zip the | |
| # GUI carries the product name (`OpenLogi.exe`, matching the MSI layout) | |
| # and the agent keeps the exact name the GUI's sibling lookup expects. | |
| # `openlogi.exe` is the CLI the docs and both issue templates ask users to | |
| # run; it is the same binary Linux installs to /usr/bin and macOS embeds | |
| # at Contents/MacOS/openlogi. It goes in `bin\` rather than beside the | |
| # GUI because Windows path comparison is case-insensitive: staging it as | |
| # `stage\openlogi.exe` would overwrite `stage\OpenLogi.exe` and ship a zip | |
| # with the CLI wearing the GUI's name. The MSI splits them the same way. | |
| - name: Collect signed portable zip artifact | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path dist, stage, stage\bin | Out-Null | |
| Copy-Item target\${{ matrix.target }}\release\openlogi-desktop.exe stage\OpenLogi.exe | |
| Copy-Item target\${{ matrix.target }}\release\openlogi-overlay.exe stage\openlogi-overlay.exe | |
| Copy-Item target\${{ matrix.target }}\release\openlogi-agent.exe stage\openlogi-agent.exe | |
| Copy-Item target\${{ matrix.target }}\release\openlogi.exe stage\bin\openlogi.exe | |
| # Guard the collision above: four inputs must produce four staged | |
| # files. A case-insensitive overwrite would silently leave three. | |
| $staged = (Get-ChildItem stage -Recurse -File).Count | |
| if ($staged -ne 4) { | |
| Write-Error "expected 4 staged files, found $staged — a name collision dropped one" | |
| exit 1 | |
| } | |
| # The MSI extracts this ZIP but only installs its explicitly listed | |
| # executables, so the portable marker never enters an MSI install. | |
| Copy-Item packaging\windows\openlogi-installation.json stage\openlogi-installation.json | |
| # Zipping after signing is safe: Authenticode lives in the PE files. | |
| $ref = $env:GITHUB_REF_NAME -replace '/', '-' | |
| Compress-Archive -Path stage\* -DestinationPath "dist\OpenLogi-$ref-windows-${{ matrix.arch }}.zip" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: OpenLogi-windows-${{ matrix.arch }} | |
| path: dist/*.zip | |
| # 'warn' (the default) would let a no-zip job stay green, and publish | |
| # would then die at the by-name download instead of shipping DMG-only. | |
| if-no-files-found: error | |
| windows-msi: | |
| name: Windows MSI (${{ matrix.arch }}) | |
| runs-on: windows-2025 | |
| needs: [should-build, windows] | |
| # `needs` only orders the jobs; this gate runs the MSI legs even when the | |
| # windows matrix AGGREGATE failed. The by-name artifact download below | |
| # then fails exactly the leg whose exe never shipped, and the other arch | |
| # keeps going. Skip when the PR gate is off; `!cancelled()` alone would | |
| # still run when `windows` was skipped. | |
| if: ${{ !cancelled() && needs.should-build.outputs.run == 'true' }} | |
| # No Rust build here — dotnet tool install + wix build + signing. | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: x86_64 | |
| msi_platform: x64 | |
| - arch: arm64 | |
| msi_platform: arm64 | |
| permissions: | |
| # Same OIDC setup as the windows job — the MSI is signed with the same | |
| # Artifact Signing profile as the exe it wraps. | |
| id-token: write | |
| contents: read | |
| environment: ${{ inputs.sign && 'release' || '' }} | |
| steps: | |
| # Needed for packaging/windows/OpenLogi.wxs. | |
| - uses: actions/checkout@v7 | |
| # The portable zip from this arch's windows leg, containing the | |
| # already-signed OpenLogi.exe, openlogi-agent.exe, openlogi-overlay.exe, | |
| # and openlogi.exe. The MSI packages exactly the bytes the portable | |
| # artifact ships. | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: OpenLogi-windows-${{ matrix.arch }} | |
| path: signed-zip | |
| - name: Extract the signed exes | |
| shell: pwsh | |
| run: | | |
| $zip = Get-ChildItem signed-zip\*.zip | Select-Object -First 1 | |
| if ($null -eq $zip) { | |
| Write-Error "No portable zip found in signed-zip\ — nothing to package" | |
| exit 1 | |
| } | |
| Expand-Archive -Path $zip.FullName -DestinationPath signed-exe | |
| - name: Load Azure signing config from 1Password | |
| id: azure_config | |
| if: ${{ inputs.sign }} | |
| uses: 1password/load-secrets-action@70062d7a876d3eb6334754fa26efd2fbd90c32f2 # v5.0.1 | |
| with: | |
| export-env: false | |
| env: | |
| OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} | |
| AZURE_CLIENT_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_CLIENT_ID | |
| AZURE_TENANT_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_TENANT_ID | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SUBSCRIPTION_ID | |
| AZURE_SIGNING_ENDPOINT: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SIGNING_ENDPOINT | |
| AZURE_SIGNING_ACCOUNT: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SIGNING_ACCOUNT | |
| AZURE_CERT_PROFILE: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_CERT_PROFILE | |
| - name: Validate Azure signing config | |
| if: ${{ inputs.sign }} | |
| shell: bash | |
| env: | |
| AZURE_CLIENT_ID: ${{ steps.azure_config.outputs.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ steps.azure_config.outputs.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ steps.azure_config.outputs.AZURE_SUBSCRIPTION_ID }} | |
| AZURE_SIGNING_ENDPOINT: ${{ steps.azure_config.outputs.AZURE_SIGNING_ENDPOINT }} | |
| AZURE_SIGNING_ACCOUNT: ${{ steps.azure_config.outputs.AZURE_SIGNING_ACCOUNT }} | |
| AZURE_CERT_PROFILE: ${{ steps.azure_config.outputs.AZURE_CERT_PROFILE }} | |
| run: | | |
| set -euo pipefail | |
| : "${AZURE_CLIENT_ID:?Configure AZURE_CLIENT_ID in the Azure 1Password item}" | |
| : "${AZURE_TENANT_ID:?Configure AZURE_TENANT_ID in the Azure 1Password item}" | |
| : "${AZURE_SUBSCRIPTION_ID:?Configure AZURE_SUBSCRIPTION_ID in the Azure 1Password item}" | |
| : "${AZURE_SIGNING_ENDPOINT:?Configure AZURE_SIGNING_ENDPOINT in the Azure 1Password item}" | |
| : "${AZURE_SIGNING_ACCOUNT:?Configure AZURE_SIGNING_ACCOUNT in the Azure 1Password item}" | |
| : "${AZURE_CERT_PROFILE:?Configure AZURE_CERT_PROFILE in the Azure 1Password item}" | |
| - name: Install the WiX toolset | |
| # Pinned exactly: a WiX point release can silently change the produced | |
| # MSI (validation, cab layout), and nothing in CI installs the MSI, so | |
| # drift would only surface on end-user machines. Bump deliberately. | |
| # The UI extension (the progress-only dialog flow) and Util extension | |
| # (util:CloseApplication in OpenLogi.wxs) are pinned to the same version | |
| # — WiX extensions release in lockstep with wix. | |
| run: | | |
| dotnet tool install --global wix --version 6.0.2 | |
| wix extension add --global WixToolset.UI.wixext/6.0.2 | |
| wix extension add --global WixToolset.Util.wixext/6.0.2 | |
| - name: Build the MSI | |
| shell: pwsh | |
| run: | | |
| # ProductVersion must be a numeric x.y.z; non-tag dispatches build a | |
| # throwaway 0.0.0 package. | |
| if ($env:GITHUB_REF_NAME -match '^v(\d+\.\d+\.\d+)$') { | |
| $version = $Matches[1] | |
| } else { | |
| $version = '0.0.0' | |
| } | |
| # Selected by exact name, never by glob order: the zip carries multiple | |
| # executables, and Get-ChildItem's alphabetical sort would put the | |
| # agent first. Upstream invariants (upload's if-no-files-found: error, the | |
| # by-name download, the extract step's guard) should make the misses | |
| # unreachable; the guards turn a confusing "$null.FullName -> empty | |
| # -d" wix error into a named cause if those invariants ever shift. | |
| $gui = Get-Item signed-exe\OpenLogi.exe -ErrorAction SilentlyContinue | |
| $overlay = Get-Item signed-exe\openlogi-overlay.exe -ErrorAction SilentlyContinue | |
| $agent = Get-Item signed-exe\openlogi-agent.exe -ErrorAction SilentlyContinue | |
| $cli = Get-Item signed-exe\bin\openlogi.exe -ErrorAction SilentlyContinue | |
| if ($null -eq $gui -or $null -eq $overlay -or $null -eq $agent -or $null -eq $cli) { | |
| Write-Error "signed-exe\ must contain OpenLogi.exe, openlogi-overlay.exe, openlogi-agent.exe, and bin\openlogi.exe — nothing to package" | |
| exit 1 | |
| } | |
| New-Item -ItemType Directory -Force -Path out | Out-Null | |
| wix build packaging\windows\OpenLogi.wxs ` | |
| -ext WixToolset.UI.wixext ` | |
| -ext WixToolset.Util.wixext ` | |
| -arch ${{ matrix.msi_platform }} ` | |
| -d Version=$version ` | |
| -d ExeFile="$($gui.FullName)" ` | |
| -d AgentExeFile="$($agent.FullName)" ` | |
| -d OverlayExeFile="$($overlay.FullName)" ` | |
| -d CliExeFile="$($cli.FullName)" ` | |
| -o out\OpenLogi.msi | |
| - name: Azure login (OIDC) | |
| if: ${{ inputs.sign }} | |
| uses: azure/login@v3 | |
| with: | |
| client-id: ${{ steps.azure_config.outputs.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ steps.azure_config.outputs.AZURE_TENANT_ID }} | |
| subscription-id: ${{ steps.azure_config.outputs.AZURE_SUBSCRIPTION_ID }} | |
| # Same PSGallery flake guard as the exe job — see that step's comment. | |
| - name: Ensure PSGallery is registered | |
| if: ${{ inputs.sign }} | |
| shell: pwsh | |
| run: | | |
| if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { | |
| Register-PSRepository -Default | |
| } | |
| - name: Sign OpenLogi.msi with Artifact Signing | |
| if: ${{ inputs.sign }} | |
| uses: azure/artifact-signing-action@v2 | |
| with: | |
| endpoint: ${{ steps.azure_config.outputs.AZURE_SIGNING_ENDPOINT }} | |
| signing-account-name: ${{ steps.azure_config.outputs.AZURE_SIGNING_ACCOUNT }} | |
| certificate-profile-name: ${{ steps.azure_config.outputs.AZURE_CERT_PROFILE }} | |
| files: ${{ github.workspace }}\out\OpenLogi.msi | |
| # http on purpose — see the exe signing step. | |
| timestamp-rfc3161: http://timestamp.acs.microsoft.com | |
| cache-dependencies: false | |
| - name: Verify the MSI is signed | |
| if: ${{ inputs.sign }} | |
| shell: pwsh | |
| run: | | |
| $sig = Get-AuthenticodeSignature out\OpenLogi.msi | |
| $sig | Format-List | |
| # Same gate as the exe: NotSigned/HashMismatch are trust-store | |
| # independent ship-blockers; NotTrusted/UnknownError stay log-only. | |
| if ($null -eq $sig.SignerCertificate -or | |
| "$($sig.Status)" -in 'NotSigned', 'HashMismatch') { | |
| Write-Error "OpenLogi.msi Authenticode signature is invalid: $($sig.Status)" | |
| exit 1 | |
| } | |
| - name: Collect signed MSI artifact | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path dist | Out-Null | |
| # Renaming after signing is safe: the signature lives in the MSI. | |
| $ref = $env:GITHUB_REF_NAME -replace '/', '-' | |
| Copy-Item out\OpenLogi.msi "dist\OpenLogi-$ref-windows-${{ matrix.arch }}.msi" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: OpenLogi-windows-msi-${{ matrix.arch }} | |
| path: dist/*.msi | |
| if-no-files-found: error | |
| linux-packages: | |
| name: Linux packages (${{ matrix.arch }}) | |
| needs: should-build | |
| if: ${{ needs.should-build.outputs.run == 'true' }} | |
| runs-on: ${{ matrix.runner }} | |
| # Bound a hang so it can't hold a PR build slot for the 6-hour runner | |
| # default; a cargo build + nfpm package is well under this. | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # The native build host defines the oldest GLIBC these binaries can | |
| # load against. Keep both architectures on the documented 2.35 floor. | |
| - arch: amd64 | |
| runner: ubuntu-22.04 | |
| nfpm_sha256: "d6417f99d5fa32bba7a4e007084615d3897651498c2e443118c26b9ec3b698a8" | |
| - arch: arm64 | |
| runner: ubuntu-22.04-arm | |
| nfpm_sha256: "dc63aa7228ec70490bae67ad3146883055a055639dcf0dffc82fa965bac75a31" | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 | |
| with: | |
| version: "v0.16.0" | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| prefix-key: v2-cargo | |
| # Cargo does not include the host GLIBC in its freshness checks. Do | |
| # not share Cargo inputs under the Ubuntu 24.04 CI key; sccache hashes | |
| # the compiler command and target environment independently. | |
| shared-key: linux-jammy-stable-release | |
| cache-targets: false | |
| save-if: ${{ github.event_name != 'pull_request' && github.ref_type != 'tag' }} | |
| # The nfpm `arch` and the runner's native arch must agree — the packages | |
| # are built natively, not cross-compiled, so a mismatched runner would | |
| # silently ship a wrongly-labelled binary. | |
| - name: Verify runner architecture | |
| env: | |
| EXPECTED_ARCH: ${{ matrix.arch }} | |
| run: | | |
| case "$(uname -m)" in | |
| x86_64) got=amd64 ;; | |
| aarch64) got=arm64 ;; | |
| *) echo "unexpected machine $(uname -m)" >&2; exit 1 ;; | |
| esac | |
| test "$got" = "$EXPECTED_ARCH" | |
| - name: Install system dependencies and nfpm | |
| env: | |
| NFPM_VERSION: "2.46.3" | |
| NFPM_ARCH: ${{ matrix.arch }} | |
| NFPM_SHA256: ${{ matrix.nfpm_sha256 }} | |
| run: | | |
| sudo apt-get update | |
| # The Linux packaging task builds openlogi-desktop, which links GPUI's full | |
| # wayland / x11 stack — keep this in sync with ci.yml's Linux deps | |
| # (the proven set). The previous short list was missing | |
| # libxkbcommon-x11-dev / libwayland-dev / libx11-xcb-dev, so every | |
| # tagged Linux build failed to link (`-lxkbcommon-x11`) and took the | |
| # whole GitHub Release down with it. | |
| sudo apt-get install -y \ | |
| libudev-dev \ | |
| gcc g++ clang libfontconfig-dev libwayland-dev \ | |
| libxkbcommon-x11-dev libx11-xcb-dev \ | |
| libssl-dev libzstd-dev pkg-config | |
| curl -fsSLo /tmp/nfpm.deb \ | |
| "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_${NFPM_ARCH}.deb" | |
| echo "${NFPM_SHA256} /tmp/nfpm.deb" | sha256sum -c | |
| sudo dpkg -i /tmp/nfpm.deb | |
| - name: Build and package | |
| run: cargo run -p xtask -- linux package | |
| - name: Verify GLIBC baseline | |
| env: | |
| MAX_GLIBC_VERSION: "2.35" | |
| run: | | |
| for binary in openlogi openlogi-desktop openlogi-overlay openlogi-agent; do | |
| required=$( | |
| readelf --version-info "target/release/$binary" \ | |
| | sed -n 's/.*Name: GLIBC_\([0-9][0-9.]*\).*/\1/p' \ | |
| | sort -Vu \ | |
| | tail -n 1 | |
| ) | |
| if [ -z "$required" ]; then | |
| echo "could not determine the GLIBC requirement for $binary" >&2 | |
| exit 1 | |
| fi | |
| echo "$binary requires GLIBC $required" | |
| newest=$(printf '%s\n%s\n' "$MAX_GLIBC_VERSION" "$required" | sort -V | tail -n 1) | |
| if [ "$newest" != "$MAX_GLIBC_VERSION" ]; then | |
| echo "$binary exceeds the GLIBC $MAX_GLIBC_VERSION release baseline" >&2 | |
| exit 1 | |
| fi | |
| done | |
| - name: Collect packages | |
| env: | |
| PKG_ARCH: ${{ matrix.arch }} | |
| run: | | |
| mkdir -p dist | |
| ref_name="${GITHUB_REF_NAME:-dev}" | |
| ref_name="${ref_name//\//-}" | |
| for pkg in target/release/*.deb target/release/*.rpm; do | |
| [ -f "$pkg" ] || continue | |
| ext="${pkg##*.}" | |
| cp "$pkg" "dist/openlogi-${ref_name}-linux-${PKG_ARCH}.${ext}" | |
| done | |
| # .pkg.tar.zst has a compound extension; handle it separately. | |
| for pkg in target/release/*.pkg.tar.zst; do | |
| [ -f "$pkg" ] || continue | |
| cp "$pkg" "dist/openlogi-${ref_name}-linux-${PKG_ARCH}.pkg.tar.zst" | |
| done | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: OpenLogi-linux-packages-${{ matrix.arch }} | |
| path: dist/* | |
| # Single owner of the GitHub Release. softprops/action-gh-release@v3 creates the | |
| # release as a draft, uploads every asset to that (mutable) draft, then publishes | |
| # it as its final step — so the release only becomes immutable once all DMGs and | |
| # checksums are attached. The static updater assets are pushed to R2 *before* that | |
| # softprops call, so the GitHub Release is the last thing published. | |
| # Surfaces each platform's result as a workflow output so the caller's publish | |
| # gate can ship a partial release (e.g. the macOS DMG even when a Windows leg | |
| # failed). The reusable workflow's own job result would be `failure` if any leg | |
| # failed, collapsing that graceful degradation. `always()` so the outputs are | |
| # set regardless of which legs failed. | |
| results: | |
| name: Build results | |
| if: ${{ always() }} | |
| needs: [should-build, macos, windows, windows-msi, linux-packages] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| macos: ${{ needs.macos.result }} | |
| windows: ${{ needs.windows.result }} | |
| windows_msi: ${{ needs['windows-msi'].result }} | |
| linux: ${{ needs['linux-packages'].result }} | |
| steps: | |
| - name: Summarize | |
| run: | | |
| echo "macos=${{ needs.macos.result }}" | |
| echo "windows=${{ needs.windows.result }}" | |
| echo "windows-msi=${{ needs['windows-msi'].result }}" | |
| echo "linux-packages=${{ needs['linux-packages'].result }}" |