-
Notifications
You must be signed in to change notification settings - Fork 0
166 lines (150 loc) · 7.66 KB
/
Copy pathpublish-winget.yml
File metadata and controls
166 lines (150 loc) · 7.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# Submits a released version to the Windows Package Manager repository.
#
# winget reaches the audience that never visits a landing page, and unlike the Microsoft
# Store it is normally available on managed corporate machines, where Store access is
# uneven (decision 10).
#
# This runs beside the release rather than inside it, for the same reason the Store
# submission does (decision 13): a submission is a pull request against
# microsoft/winget-pkgs, it is reviewed by people, and it can sit for days. Nothing about
# the download being available may depend on it. A failure here is visible as a failed run
# and changes nothing that has already shipped.
#
# Setup was done on 20 August 2026 and is recorded here because it has to be repeated if
# the token is ever rotated or the account changes.
#
# 1. The first submission was made by hand. `wingetcreate update` below reads the
# previous version's manifests out of winget-pkgs, so it cannot make the first one.
# From a Windows machine with this repository checked out:
#
# winget install Microsoft.WingetCreate
# winget validate installer\winget
# wingetcreate submit installer\winget
#
# With no --token, wingetcreate runs a browser OAuth flow, which is what the tool's
# own documentation recommends locally: a token passed on the command line ends up in
# shell history and in any transcript of the session.
#
# 2. WINGET_TOKEN is a repository secret holding a GitHub personal access token. A
# workflow has no browser, so this is the one place a token is unavoidable.
#
# It must be a **classic** token - wingetcreate does not support fine-grained tokens,
# which is now what GitHub offers first. The scope is public_repo, not full repo;
# winget-pkgs and the fork are both public. delete_repo is worth adding as well: it
# lets wingetcreate remove the fork it created when a submission fails, instead of
# leaving one behind on every failure.
#
# wingetcreate forks winget-pkgs and opens the pull request as whoever owns the
# token, so it is an identity rather than only a permission. GITHUB_TOKEN cannot
# stand in: it has no rights outside this repository.
name: Publish to winget
on:
release:
types: [published]
# Re-submitting a release whose first attempt failed, or catching one published before
# this workflow existed, without cutting a new release to do it.
workflow_dispatch:
inputs:
tag:
description: 'Release tag to submit (default: the latest released version)'
required: false
type: string
permissions:
contents: read
concurrency:
group: winget
cancel-in-progress: false
jobs:
submit:
name: Submit to winget-pkgs
# Pre-releases are the Dev channel, which installs as a separate product so a tester
# can run it beside a released copy. Only the released channel belongs in a package
# manager. github.event.release is absent on workflow_dispatch, so the guard has to
# allow that case through explicitly.
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- name: Resolve the release
id: release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
EVENT_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
run: |
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$tag = if ($env:INPUT_TAG) { $env:INPUT_TAG } elseif ($env:EVENT_TAG) { $env:EVENT_TAG } else { '' }
if ($tag) {
$release = gh release view $tag --json tagName,isPrerelease,assets | ConvertFrom-Json
}
else {
# --exclude-pre-releases is what makes "latest" mean the released channel.
$tag = (gh release list --limit 1 --exclude-pre-releases --json tagName |
ConvertFrom-Json).tagName
if (-not $tag) { throw 'No released version has been published yet.' }
$release = gh release view $tag --json tagName,isPrerelease,assets | ConvertFrom-Json
}
if ($release.isPrerelease) {
throw "$tag is a pre-release. Only the released channel is submitted to winget."
}
$version = $release.tagName -replace '^v', ''
if ($version -notmatch '^\d+\.\d+\.\d+$') {
throw "Expected a major.minor.patch tag, but '$($release.tagName)' gives '$version'."
}
# The two installers winget offers, machine scope first so the order matches the
# manifest. Anything framework-dependent is a pipeline artifact, not a download.
$perMachine = $release.assets |
Where-Object { $_.name -like '*.msi' -and $_.name -notlike '*userinstaller*' -and $_.name -notlike '*frameworkdependent*' -and $_.name -notlike '*-dev*' } |
Select-Object -First 1
$perUser = $release.assets |
Where-Object { $_.name -like '*userinstaller.msi' -and $_.name -notlike '*frameworkdependent*' -and $_.name -notlike '*-dev*' } |
Select-Object -First 1
if (-not $perMachine) { throw "$tag has no per-machine installer." }
if (-not $perUser) { throw "$tag has no per-user installer." }
# wingetcreate reads metadata appended to a URL as <url>|<architecture>|<scope>.
# Both are given rather than letting it infer: with one trailing field it has to
# guess which of the two was meant, and the two installers here differ only by
# scope.
$base = "https://github.com/$env:GITHUB_REPOSITORY/releases/download/$tag"
"version=$version" >> $env:GITHUB_OUTPUT
"tag=$tag" >> $env:GITHUB_OUTPUT
"urls=$base/$($perMachine.name)|x64|machine $base/$($perUser.name)|x64|user" >> $env:GITHUB_OUTPUT
Write-Host "Submitting $version from $tag"
Write-Host " $($perMachine.name)"
Write-Host " $($perUser.name)"
- name: Check the submission token is present
shell: pwsh
env:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
if (-not $env:WINGET_TOKEN) {
throw 'WINGET_TOKEN is not set. See the setup notes at the top of this workflow.'
}
# wingetcreate downloads each installer, computes its own SHA256, and reads the
# ProductCode out of the MSI. The hashes in installer/winget/ are the seed for the
# first submission only; from here they are derived from the bytes being submitted,
# so a mismatch is impossible rather than merely unlikely.
- name: Submit the update
shell: pwsh
env:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
VERSION: ${{ steps.release.outputs.version }}
URLS: ${{ steps.release.outputs.urls }}
run: |
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$exe = Join-Path $env:RUNNER_TEMP 'wingetcreate.exe'
Invoke-WebRequest -Uri 'https://aka.ms/wingetcreate/latest' -OutFile $exe
# $urls, not @urls: the @ prefix is splatting, which applies to cmdlets. A plain
# array passed to a native command already expands to one argument per element.
$urls = $env:URLS -split ' '
& $exe update SQLBI.Whiteboard `
--version $env:VERSION `
--urls $urls `
--submit `
--token $env:WINGET_TOKEN
if ($LASTEXITCODE -ne 0) {
throw "wingetcreate exited with $LASTEXITCODE. If this is the first submission, installer/winget/ has to be submitted by hand once before update can work."
}