1 name: CLI Release winget
2
3 # Dispatched by release-github.yml once the release and its checksums exist.
4 #
5 # winget manifests live in microsoft/winget-pkgs, and `wingetcreate update`
6 # only works on a package that is already there. So this workflow branches:
7 #
8 # - package already in winget-pkgs -> `wingetcreate update`, which rewrites
9 # the URLs and re-hashes the installers for us.
10 # - package not there yet -> render the manifests in packaging/winget/ and
11 # `wingetcreate submit` them as a new package.
12 #
13 # The second path only runs once, but keeping it here means a first release
14 # does not need someone to sit at a Windows box running `wingetcreate new`
15 # interactively.
16 on:
17 workflow_dispatch:
18 inputs:
19 tag:
20 description: "Release tag (e.g. v0.6.0)"
21 required: true
22
23 concurrency:
24 # A re-pushed or re-dispatched tag fires this a second time. Queue the
25 # duplicate behind the original instead of cancelling it: cancelling a run
26 # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
27 # just hits the "already published" check and exits clean.
28 group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
29 cancel-in-progress: false
30
31 permissions:
32 contents: read
33
34 env:
35 REPO: smbcloudXYZ/smbcloud-cli
36 PACKAGE_IDENTIFIER: smbCloud.CLI
37
38 jobs:
39 submit-winget-manifest:
40 name: Submit winget manifest
41 # wingetcreate is a Windows-only tool.
42 runs-on: windows-latest
43
44 steps:
45 - name: Check out repository
46 uses: actions/checkout@v6
47
48 - name: Resolve tag, version and release date
49 id: release
50 shell: bash
51 env:
52 GH_TOKEN: ${{ github.token }}
53 run: |
54 TAG="${{ github.event.inputs.tag }}"
55 VERSION="${TAG#v}"
56 # wingetcreate wants ReleaseDate as YYYY-MM-DD.
57 DATE=$(gh release view "$TAG" --repo "$REPO" --json publishedAt --jq '.publishedAt[0:10]')
58
59 echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
60 echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
61 echo "date=${DATE}" >> "$GITHUB_OUTPUT"
62
63 - name: Check whether the package is already in winget-pkgs
64 id: exists
65 shell: bash
66 env:
67 GH_TOKEN: ${{ github.token }}
68 run: |
69 # manifests are filed under the lowercased first letter of the
70 # publisher, e.g. manifests/s/smbCloud/CLI.
71 PUBLISHER="${PACKAGE_IDENTIFIER%%.*}"
72 NAME="${PACKAGE_IDENTIFIER#*.}"
73 FIRST=$(echo "${PUBLISHER:0:1}" | tr '[:upper:]' '[:lower:]')
74 PATH_IN_REPO="manifests/${FIRST}/${PUBLISHER}/${NAME}"
75
76 if gh api "repos/microsoft/winget-pkgs/contents/${PATH_IN_REPO}" >/dev/null 2>&1; then
77 echo "Found ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}; submitting an update."
78 echo "found=true" >> "$GITHUB_OUTPUT"
79 echo "pending=false" >> "$GITHUB_OUTPUT"
80 exit 0
81 fi
82
83 echo "found=false" >> "$GITHUB_OUTPUT"
84
85 # Until the first New-package PR merges the folder check above is
86 # always false, so without this every release would open another
87 # PR and Package Request issue for the same package.
88 OPEN_PR=$(gh pr list --repo microsoft/winget-pkgs --state open \
89 --search "New package: ${PACKAGE_IDENTIFIER} in:title" \
90 --json number,url --jq '.[0].url // empty')
91
92 if [ -n "$OPEN_PR" ]; then
93 echo "No ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}, but ${OPEN_PR} is already open; not submitting again."
94 echo "pending=true" >> "$GITHUB_OUTPUT"
95 else
96 echo "No ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}; submitting it as a new package."
97 echo "pending=false" >> "$GITHUB_OUTPUT"
98 fi
99
100 - name: Render manifests for a first submission
101 if: steps.exists.outputs.found == 'false' && steps.exists.outputs.pending != 'true'
102 shell: bash
103 env:
104 GH_TOKEN: ${{ github.token }}
105 run: |
106 TAG="${{ steps.release.outputs.tag }}"
107 VERSION="${{ steps.release.outputs.version }}"
108
109 mkdir -p artifacts manifests
110
111 gh release download "$TAG" \
112 --repo "$REPO" \
113 --pattern "smb-win-*.exe.sha256" \
114 --dir artifacts/
115
116 # The schema wants 64 hex characters; the community validation
117 # pipeline writes them uppercase, so match that.
118 SHA_AMD64=$(tr -d '[:space:]' < artifacts/smb-win-amd64.exe.sha256 | tr '[:lower:]' '[:upper:]')
119 SHA_ARM64=$(tr -d '[:space:]' < artifacts/smb-win-arm64.exe.sha256 | tr '[:lower:]' '[:upper:]')
120
121 RELEASE_DATE="${{ steps.release.outputs.date }}"
122
123 for template in packaging/winget/*.yaml.in; do
124 out="manifests/$(basename "$template" .in)"
125 sed \
126 -e "s|@VERSION@|${VERSION}|g" \
127 -e "s|@TAG@|${TAG}|g" \
128 -e "s|@REPO@|${REPO}|g" \
129 -e "s|@SHA_AMD64@|${SHA_AMD64}|g" \
130 -e "s|@SHA_ARM64@|${SHA_ARM64}|g" \
131 -e "s|@RELEASE_DATE@|${RELEASE_DATE}|g" \
132 "$template" > "$out"
133
134 echo "--- ${out} ---"
135 cat "$out"
136 done
137
138 - name: Download wingetcreate
139 shell: pwsh
140 run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe
141
142 # File the Package Request issue *first*, then reference it from the PR
143 # with "Resolves #N" below. winget-pkgs' Package Request is an issue
144 # *form* (.github/ISSUE_TEMPLATE/package_request.yml): the title must be
145 # "[Package Request]: ..." and the body has to carry the labelled fields
146 # below (Installer Url especially — without it a maintainer just replies
147 # "please provide the installer link"). A free-text body lands as
148 # Needs-Triage and can't be acted on.
149 #
150 # The issue and the PR are authored by two separate PATs, configured as
151 # the repo secrets WINGET_ISSUE_TOKEN (the Package Request issue) and
152 # WINGET_PR_TOKEN (the manifest PR). Both need public_repo scope on
153 # microsoft/winget-pkgs (fork + open issues/PRs). WINGET_PR_TOKEN also
154 # needs read:org and read:discussion: the "Set the winget PR body" step
155 # below calls `gh pr edit`, whose GraphQL query resolves org/team and
156 # discussion fields and fails with a scope error without them.
157 # WINGET_PR_TOKEN replaces the old single WINGET_TOKEN.
158 - name: File the winget Package Request issue
159 id: issue
160 if: steps.exists.outputs.pending != 'true'
161 shell: bash
162 env:
163 # Authored by the issue account for this repo.
164 GH_TOKEN: ${{ secrets.WINGET_ISSUE_TOKEN }}
165 run: |
166 set -euo pipefail
167
168 if [ -z "${GH_TOKEN:-}" ]; then
169 echo "WINGET_ISSUE_TOKEN is not set. It needs a PAT (public_repo) for the account that should author the Package Request issue." >&2
170 exit 1
171 fi
172
173 VERSION="${{ steps.release.outputs.version }}"
174 TAG="${{ steps.release.outputs.tag }}"
175
176 # The release assets, same URLs the update step feeds wingetcreate.
177 BASE="https://github.com/${REPO}/releases/download/${TAG}"
178 INSTALLER_AMD64="${BASE}/smb-win-amd64.exe"
179 INSTALLER_ARM64="${BASE}/smb-win-arm64.exe"
180
181 ISSUE_BODY=$(cat <<EOF
182 Download Page Url: https://github.com/${REPO}/releases/tag/${TAG}
183 Publisher: smbCloud
184 Package Name: smbCloud CLI
185 Description: smbCloud command line interface.
186 Package Version: ${VERSION}
187 Installer Url: ${INSTALLER_AMD64}
188 Installer Url: ${INSTALLER_ARM64}
189 EOF
190 )
191
192 ISSUE_URL=$(gh issue create \
193 --repo microsoft/winget-pkgs \
194 --title "[Package Request]: ${PACKAGE_IDENTIFIER} ${VERSION}" \
195 --body "$ISSUE_BODY")
196
197 echo "Opened issue ${ISSUE_URL}"
198 echo "issue_url=${ISSUE_URL}" >> "$GITHUB_OUTPUT"
199 echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT"
200
201 - name: Sebat dulu ...
202 if: steps.issue.outputs.issue_number != ''
203 shell: bash
204 run: sleep $(( RANDOM % 61 + 150 ))
205
206 - name: Submit new package
207 id: submit_new
208 if: steps.exists.outputs.found == 'false' && steps.exists.outputs.pending != 'true'
209 shell: pwsh
210 env:
211 # wingetcreate reads the token from this variable. Passing it as
212 # --token instead makes the tool warn that the token can end up in
213 # logs, which on a shared runner is not a warning worth ignoring.
214 WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
215 run: |
216 if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
217 Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org, and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR, and gh can edit the PR body."
218 }
219
220 $version = "${{ steps.release.outputs.version }}"
221
222 # Tee stdout so we can scrape the PR URL afterwards. wingetcreate
223 # prints "Pull request can be found here: <url>" on success, and
224 # that line is the only handle we get on the PR it just opened.
225 # --no-open because the runner has no browser to open the PR in.
226 .\wingetcreate.exe submit manifests `
227 --prtitle "New package: $env:PACKAGE_IDENTIFIER version $version" `
228 --no-open 2>&1 | Tee-Object -Variable output
229
230 if ($LASTEXITCODE -ne 0) {
231 Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE"
232 }
233
234 $match = [regex]::Match(($output -join "`n"), 'https://github\.com/microsoft/winget-pkgs/pull/(\d+)')
235 if ($match.Success) {
236 "pr_url=$($match.Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
237 "pr_number=$($match.Groups[1].Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
238 } else {
239 Write-Warning "Could not find the PR URL in wingetcreate output; skipping issue linking."
240 }
241
242 - name: Submit manifest update
243 id: submit_update
244 if: steps.exists.outputs.found == 'true'
245 shell: pwsh
246 env:
247 WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
248 run: |
249 if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
250 Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org, and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR, and gh can edit the PR body."
251 }
252
253 $tag = "${{ steps.release.outputs.tag }}"
254 $version = "${{ steps.release.outputs.version }}"
255 $base = "https://github.com/${env:REPO}/releases/download/$tag"
256
257 # The trailing |x64 and |arm64 tell wingetcreate which installer
258 # entry each URL replaces. Without them it guesses from the file
259 # name, and "smb-win-amd64.exe" is not a spelling it recognises.
260 .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER `
261 --version $version `
262 --urls "$base/smb-win-amd64.exe|x64" "$base/smb-win-arm64.exe|arm64" `
263 --release-notes-url "https://github.com/${env:REPO}/releases/tag/$tag" `
264 --release-date "${{ steps.release.outputs.date }}" `
265 --prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" `
266 --submit `
267 --no-open 2>&1 | Tee-Object -Variable output
268
269 if ($LASTEXITCODE -ne 0) {
270 Write-Error "wingetcreate failed with exit code $LASTEXITCODE"
271 }
272
273 $match = [regex]::Match(($output -join "`n"), 'https://github\.com/microsoft/winget-pkgs/pull/(\d+)')
274 if ($match.Success) {
275 "pr_url=$($match.Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
276 "pr_number=$($match.Groups[1].Value)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
277 } else {
278 Write-Warning "Could not find the PR URL in wingetcreate output; skipping issue linking."
279 }
280
281 # wingetcreate has no --prbody flag (only --prtitle), so it always opens
282 # the PR with the winget-pkgs template: an empty Description and an
283 # unticked "Linked to an issue" box next to a "Resolves #[Issue Number]"
284 # placeholder. The issue already exists (filed first, above), so this
285 # step just rewrites the body to point the PR at it with "Resolves #N".
286 #
287 # This must run as the PR author (WINGET_PR_TOKEN) — the account that
288 # opened the PR via wingetcreate — since only the author (or a repo
289 # maintainer) can edit it.
290 - name: Set the winget PR body and link the issue
291 id: link
292 if: >-
293 (steps.submit_new.outputs.pr_number != '') ||
294 (steps.submit_update.outputs.pr_number != '')
295 shell: bash
296 env:
297 GH_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
298 run: |
299 set -euo pipefail
300
301 PR_URL="${{ steps.submit_new.outputs.pr_url || steps.submit_update.outputs.pr_url }}"
302 ISSUE_NUMBER="${{ steps.issue.outputs.issue_number }}"
303 VERSION="${{ steps.release.outputs.version }}"
304 TAG="${{ steps.release.outputs.tag }}"
305
306 if [ -n "$ISSUE_NUMBER" ]; then
307 CHECKBOX="- [x] Linked to an issue (if applicable)"
308 RESOLVES="- Resolves #${ISSUE_NUMBER}"
309 else
310 CHECKBOX="- [ ] Linked to an issue (if applicable)"
311 RESOLVES=""
312 fi
313
314 if [ "${{ steps.exists.outputs.found }}" = "false" ]; then
315 DESC="Adds ${PACKAGE_IDENTIFIER} version ${VERSION} from ${TAG}."
316 else
317 DESC="Updates ${PACKAGE_IDENTIFIER} to version ${VERSION} from ${TAG}."
318 fi
319
320 # Mirrors the current .github/PULL_REQUEST_TEMPLATE.md; keep the
321 # checklist items in sync with it (e.g. the 1.12 schema line).
322 BODY=$(cat <<EOF
323 ## 📖 Description
324 ${DESC}
325 ## ✅ Checklist
326 - [x] Signed the [Contributor License Agreement](https://cla.opensource.microsoft.com)
327 ${CHECKBOX}
328 ${RESOLVES}
329 ## 📦 Manifest Checklist
330 - [x] Checked that there aren't other open [pull requests](https://github.com/microsoft/winget-pkgs/pulls) for the same manifest update/change
331 - [x] This PR only modifies one (1) manifest
332 - [x] Validated manifest locally with \`winget validate --manifest <directory>\`
333 - [x] Tested manifest locally with \`winget install --manifest <directory>\`
334 - [x] Manifest conforms to the [1.12 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.12.0)
335 EOF
336 )
337
338 gh pr edit "$PR_URL" --body "$BODY"
339 echo "Linked ${PR_URL} to issue #${ISSUE_NUMBER:-<none>}"