diff --git a/.github/workflows/create-or-promote-release.yml b/.github/workflows/create-or-promote-release.yml index 2af583a4ad..bbbef113cb 100644 --- a/.github/workflows/create-or-promote-release.yml +++ b/.github/workflows/create-or-promote-release.yml @@ -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 }} diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index da19153877..8ba75e1f43 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -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: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ae68689a8..def49311d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/winget-publish.yml new file mode 100644 index 0000000000..c5b31d99d9 --- /dev/null +++ b/.github/workflows/winget-publish.yml @@ -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 + # `. + # 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 }}