ci(desktop): sign Windows installers and publish production releases to winget (#6217)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-07-12 06:17:04 -05:00
committed by GitHub
parent 9005fb7129
commit b6c4acb768
4 changed files with 139 additions and 0 deletions

View File

@@ -132,6 +132,18 @@ jobs:
call-promote-workflow:
if: ${{ !inputs.dry_run && inputs.channel != 'internal' }}
needs: determine-tags
# promote.yml's token is capped by this job's grants, and a called
# workflow requesting more than its caller grants fails at startup —
# so this must cover promote.yml's declared workflow-level set, plus
# actions: write for its publish-workflow dispatch. Scoped to this job
# so call-release-workflow doesn't carry it.
permissions:
contents: write
pull-requests: write
statuses: write
id-token: write
attestations: write
actions: write
uses: ./.github/workflows/promote.yml
with:
tag_name: ${{ needs.determine-tags.outputs.tag_to_process }}

View File

@@ -143,6 +143,15 @@ jobs:
update-github-release:
runs-on: ubuntu-24.04-arm
needs: [ prepare-build-info, promote-release ]
# actions: write is scoped here — only this job's publish-workflow
# dispatch needs it, and the other jobs must not get it. Job-level
# permissions replace the workflow-level block, so the full set this
# job uses is listed.
permissions:
contents: write
pull-requests: write
statuses: write
actions: write
steps:
- name: Checkout code
uses: actions/checkout@v7.0.0
@@ -167,6 +176,19 @@ jobs:
--draft=false \
--prerelease=${{ inputs.channel != 'production' }}
# The edit above is made with GITHUB_TOKEN, and GITHUB_TOKEN-caused
# events never start workflow runs — winget-publish.yml's `released`
# trigger will not fire from it. workflow_dispatch is exempt from that
# suppression, so dispatch it explicitly. Soft-fail: a winget hiccup
# must not block the changelog stamp or Discord notification.
- name: Dispatch winget publish
if: ${{ inputs.channel == 'production' }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.final_tag }}
run: gh workflow run winget-publish.yml --ref main -f "tag=$TAG"
- name: Stamp CHANGELOG.md for release
if: ${{ inputs.channel == 'production' }}
env:

View File

@@ -62,6 +62,20 @@ on:
required: false
APPLE_TEAM_ID:
required: false
# Windows Authenticode signing via Azure Trusted Signing (now "Artifact Signing").
# All optional: the signing step skips cleanly when absent, like macOS above.
AZURE_TENANT_ID:
required: false
AZURE_CLIENT_ID:
required: false
AZURE_CLIENT_SECRET:
required: false
AZURE_TRUSTED_SIGNING_ENDPOINT:
required: false
AZURE_TRUSTED_SIGNING_ACCOUNT:
required: false
AZURE_TRUSTED_SIGNING_CERT_PROFILE:
required: false
concurrency:
group: ${{ github.workflow }}-${{ inputs.tag_name }}
@@ -269,6 +283,8 @@ jobs:
GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
# Secrets aren't readable in step `if:` expressions, so surface presence here.
SIGN_WINDOWS: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT != '' && 'true' || 'false' }}
steps:
- name: Checkout code
uses: actions/checkout@v7.0.0
@@ -308,6 +324,39 @@ jobs:
${{ contains(runner.os, 'macOS') && ':desktopApp:packageReleaseUberJarForCurrentOS' || '' }}
'-PaboutLibraries.release=true' '-Pdesktop.release=true' --no-daemon
# Authenticode-signs the MSI/EXE installers via Azure Trusted Signing
# (rebranded "Artifact Signing"). Skips cleanly when the secrets are
# absent, mirroring SIGN_MACOS. Required repository secrets:
# AZURE_TENANT_ID Entra tenant ID
# AZURE_CLIENT_ID app registration (service principal) client ID
# AZURE_CLIENT_SECRET client secret for that app registration
# AZURE_TRUSTED_SIGNING_ENDPOINT e.g. https://eus.codesigning.azure.net
# AZURE_TRUSTED_SIGNING_ACCOUNT Trusted Signing account name
# AZURE_TRUSTED_SIGNING_CERT_PROFILE public-trust certificate profile name
# The service principal needs the "Trusted Signing Certificate Profile
# Signer" role on the account. Presence is gated on the ENDPOINT secret;
# a half-configured set fails here loudly rather than shipping unsigned.
# Note: only the installers are signed, not the launcher .exe packaged
# inside them — SmartScreen reputation attaches to the downloaded file.
# Sign the inner .exe between createReleaseDistributable and packaging
# if that ever becomes necessary.
- name: Sign Windows installers (Azure Trusted Signing)
if: runner.os == 'Windows' && env.SIGN_WINDOWS == 'true'
uses: azure/artifact-signing-action@v2.0.0
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERT_PROFILE }}
files-folder: desktopApp/build/compose/binaries/main-release
files-folder-filter: msi,exe
files-folder-recurse: true
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
# jpackage computes the .deb Depends: line from the build host's package names.
# Ubuntu 24.04 records time_t64 transition names (libasound2t64, libpng16-16t64)
# that don't exist on Debian 12 Bookworm / Raspberry Pi OS, making the .deb

56
.github/workflows/winget-publish.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Publish to winget
# The `released` type fires when a release is published as a full release OR
# when an existing pre-release is flipped to a full release — the latter is
# what promote.yml's production promotion does (`gh release edit
# --prerelease=false` on the already-published release object). It never fires
# for drafts or pre-releases, so internal/closed/open promotions are ignored.
#
# CAVEAT: promote.yml performs that edit with the workflow's own GITHUB_TOKEN,
# and events caused by GITHUB_TOKEN never start workflow runs — so promote.yml
# also dispatches this workflow explicitly (workflow_dispatch is exempt from
# that suppression). The release trigger stays for manually-published releases;
# workflow_dispatch doubles as the manual retry/seed path.
on:
release:
types: [released]
workflow_dispatch:
inputs:
tag:
description: 'Production release tag to publish (e.g., v2.8.0)'
required: true
type: string
# The WINGET_TOKEN PAT does all the work against microsoft/winget-pkgs;
# nothing in this repo is written.
permissions: {}
jobs:
winget:
# Belt and braces for release events — `released` should already exclude
# these. workflow_dispatch has no release payload and passes through.
if: ${{ !github.event.release.prerelease && !github.event.release.draft }}
runs-on: ubuntu-latest
env:
# Secrets aren't readable in step `if:` expressions; skip cleanly until
# the token is configured.
HAS_WINGET_TOKEN: ${{ secrets.WINGET_TOKEN != '' && 'true' || 'false' }}
steps:
# Prerequisites (both manual, one-time):
# 1. Meshtastic.MeshtasticDesktop must already exist in
# microsoft/winget-pkgs — winget-releaser only updates existing
# packages. Submit the first version by hand with `wingetcreate new
# <MSI release asset URL>`.
# 2. WINGET_TOKEN secret: a *classic* PAT with the public_repo scope
# (fine-grained PATs are not supported), owned by an account that
# has (or can create) a fork of microsoft/winget-pkgs.
# The version is derived from the release tag with the leading `v`
# stripped (v2.8.0 -> 2.8.0), matching the MSI's ProductVersion.
- name: Submit MSI manifest to microsoft/winget-pkgs
if: env.HAS_WINGET_TOKEN == 'true'
uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: Meshtastic.MeshtasticDesktop
installers-regex: '\.msi$'
release-tag: ${{ inputs.tag || github.event.release.tag_name }}
token: ${{ secrets.WINGET_TOKEN }}