人都是想偷懶的,本篇介紹怎麼程式化發 Pull Reqeust,解決手動上新的版本到 winget 的流程。
微軟 //build 2020 推出 winget 可讓開發人員在 Windows 10 電腦上探索、安裝、升級、移除和設定應用程式。
讓我想要把自己的程式也放到 winget 上,讓其他人也可以直接下載。
要怎麼送程式到 winget 呢?
根據 Submit packages to Windows Packages Manager 介紹,目前 winget 沒有上架的工具,要上程式到 winget 有兩個步驟:
- 建立一份 manifest,提供程式的說明(作者, 授權, 下載 URL, checksum, ... 等)
- 藉由 GitHub 來發 Pull Reqeust 到 winget-pkgs repo
Create your package manifest
manifest 是一份 YAML 描述程式的基本資訊,基本必要資訊如下:
Id: string # Publisher.package format. (必須是唯一的)
Publisher: string # The name of the publisher.
Name: string # The name of the application.
Version: string # Version numbering format.
License: string # The open source license or copyright.
InstallerType: string # Enumeration of supported installer types (exe, msi, msix, inno, wix, nullsoft, appx).
Installers:
- Arch: string # Enumeration of supported architectures.
- Url: string # Path to download installation file.
- Sha256: string # SHA256 calculated from installer.
ManifestVersion: 0.1.0
範例如:
Id: microsoft.teams
Publisher: Microsoft Corporation
Name: Microsoft Teams
Version: 1.3.0.4461
License: Copyright (c) Microsoft Corporation. All rights reserved.
InstallerType: exe
Installers:
- Arch: x64
Url: https://statics.teams.cdn.office.net/production-windows-x64/1.3.00.4461/Teams_windows_x64.exe
Sha256: 712f139d71e56bfb306e4a7b739b0e1109abb662dfa164192a5cfd6adb24a4e1
ManifestVersion: 0.1.0
介紹幾個比較特別的參數:
- Silent:可以強迫在安裝過程沒有出現任何需要人為操作的畫面,在 winget 安裝非常適合
- SilentWithProgress:功能類似 Silent
- Commands:可搭配在安裝時執行特定的指令
更多詳細的 schema 可以參考:Complete Shcema。
了解 manifest 的格式之後,下面介紹怎麼建立 manifest:
- 登入 GitHub 帳號
- fork winget-pkgs repo 到自己的帳號下
- clone 自己帳號下的 winget-pkgs repo 到本機
- 進入 clone 下來的 manifest 目錄,建立專屬的 {publisher} 目錄,例如:我是 pou lin。如果未來同一個 {publisher} 發多個 apps 都只能放在這個 {publisher} 目錄
- 在 {publisher} 目錄下,建立 {app} 目錄,例如:我的程式叫 just love radio,目錄名稱就是 just love radio。
- 在 {app} 目錄下,建立 {version}.yaml。{version}.yaml 每個不同版本都會有一份,例如:1.0.0.0.yaml,1.0.2.0.yaml
- 產生 yaml 檔案,可以 Tool 目錄中找到 YamlCreate.ps1,利用 Powershell 執行它來加以建立 yaml
- 目錄結構如圖範例:
- 準備好 {version}.yaml 可以發 git push 到自己的 repo
- 最後在自己的 winget-pkgs repo 發一個 PullRequest 到 microsoft/winget-pkgs 就完成了
建立 manifest 需要注意的地方,可以參考:Tips and best practices。
Custom Powershell script to auto create YAML and send Pull Request to GitHub
上面介紹是手動處理方式,雖然步驟不多,但每次有新版本發布都要再做一次。我很懶惰,身為開發人員能減少這種手動的流程是最基本的。
分析 {manifest}.yaml 的基本參數跟學習 GitHub 的 GraphQL API 後,我寫出了 Powershell 的 script 來自動化幫我完成手動的流程。
要自動化處理這些流程有幾個重要的組成元素:
1. 準備可以檢查是否有新版的地方
不管是用 Jenkins 或其他 CI,建議準備一個地方紀錄目前最新版本的資訊,讓 script 可以檢查是否有新的版本要發給 winget。
例如:我準備一個 json 檔案,再每次 Jenkins 建立新版本時,自動更新該份 json 留下最新的版本資訊。
內文如下:
{
"packages": [
{
"version" : "10.0.2",
"code" : 100002,
"url" : "https://poumason.internal.com/jlr/100002.msi"
},
...
]
}
2. 自動檢查更新並產生 {version}.yaml 的內容
根據 Minimal required schema 定義,準備需要的參數,如下:
Write-Host "1. 從 https://poumason.internal.com/jlr/version.json 檢查是否有新版本"
$response = Invoke-WebRequest -Uri "https://poumason.internal.com/jlr/version.json"
$jsonObj = ConvertFrom-Json $([String]::new($response.Content))
$lastestVer = $jsonObj.versions[0].version
$lastestUrl = $jsonObj.supports[0].url
$previousVer = $lastestVer
# 讀取本機的暫存檔,檢查上次抓到的版本跟最新的版本是否一致
$cacheFile = "..\.\winget_ver.txt"
if(![System.IO.File]::Exists($cacheFile)){ SET-Content -Path $cacheFile -Value $lastestVer } else { $previousVer = Get-Content -Path $cacheFile }
if ($previousVer -eq $lastestVer) {
Write-Host "*** The same version ***" -ForeGroundColor Blue
} else {
# 準備一個新的 branch
$branchName = "jlr-${lastestVer}"
git branch -D $branchName
git branch $branchName
git checkout $branchName
# 準備一個存安裝檔
$exeFile = ".\jlr-${lastestVer}.msi";
Write-Host "2. 下載 url 中的檔案 ${exeFile}"
Invoke-WebRequest $lastestUrl -OutFile $exeFile
Write-Host "3. 取得下載好檔案的 checksum ${exeFile}"
$checkSum = (Get-FileHash $exeFile -Algorithm sha256).hash
rm $exeFile
Write-Host "4. 產生新版本的 ${lastestVer}.yaml"
# publisherFolder 可根據自己的名稱與 App 名稱做改變
$publisherFolder = "poulin\jlr"
$fileName = ".\manifests\${publisherFolder}\${lastestVer}.yaml"
# 寫入 ID
$string = "Id: poulin.jlr"
write-output $string | out-file $filename
# 寫入 Version
$string = "Version: " + $lastestVer
write-output $string | out-file $filename -append
# 寫入 App 名稱
$string = "Name: Just Love Radio"
write-output $string | out-file $filename -append
# 寫入 Publisher
$string = "Publisher: Pou Lin"
write-output $string | out-file $filename -append
# 寫入 License
$string = "License: Copyright (c) Pou Lin All Rights Reserved."
write-output $string | out-file $filename -append
# 寫入 InstallerType
$string = "InstallerType: msi"
write-output $string | out-file $filename -append
$string = "Installers:"
write-output $string | out-file $filename -append
$string = " - Arch: x86"
write-output $string | out-file $filename -append
$string = " Url: " + $lastestUrl
write-output $string | out-file $filename -append
$string = " Sha256: " + $checkSum
write-output $string | out-file $filename -append
# 加入 Silent 與 SilentWithProgress
$string = " Switches:"
write-output $string | out-file $filename -append
$string = " Silent: /S"
write-output $string | out-file $filename -append
$string = " SilentWithProgress: /S"
write-output $string | out-file $filename -append
Write-Host "5. 更新暫存檔案到最新檢查的版本 ${cacheFile}"
SET-Content -Path $cacheFile -Value $lastestVer
Write-host GET-Content -Path $cacheFile
Write-Host "6. 寫入 git commit 並送到自己的 winget-pkgs repo"
git add $fileName
$comment = "Add JLR new version ${lastestVer}"
git commit -m $comment
git push --set-upstream origin $branchName
git push
}
這個 script 需要被放在自己 GitHub 帳號下 clone 到本機的 winget-pkgs 根目錄中,如下圖:
因為裡面會產生新的 {version}.yaml,它需要被 push 到自己的 repo 中,才能發 pull request 給 microsoft:winget-pkgs repo。
3. 利用 GraphQL API 從自己的 winget-pkgs repo 發 Pull Request 給 microsof:winget-pkgs repo
要程式化發送 Pull Request 需要使用 createPullRequest - GraphQL API v4,根據 API 參數說明,我們需要:
- Github Personal access token
- 點擊連結申請 Access Token
- microsoft:winget-pkgs repo 的 repo ID
- 利用 GraphQL API Explorer 輸入下面的指令得到 repo ID
{ repository(owner: "microsoft", name: "winget-pkgs") { id } }
- 利用 GraphQL API Explorer 輸入下面的指令得到 repo ID
最後準備如下的程式碼:
Write-Host "7. create pull request for GitHub"
$graphql = "https://api.github.com/graphql"
$accessToken = "{申請自己專用的 GitHub Access Token}"
# microsoft:winget-pkgs 的 repo id
$repoId = "{}"
# 準備發送 api 的 http request headers
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("User-Agent", 'Agent')
$headers.Add("Authorization", 'bearer ' + $accessToken)
# 把 $branchName 加入來源
$branchName = "{你的 github 帳號}:${branchName}"
$body = '{ "query": "mutation { createPullRequest(input: { baseRefName: \"master\", headRefName: \"' + $branchName + '\", repositoryId: \"' + $repoId + '\", title: \"' + $comment + '\" , body: \"\"}) { clientMutationId} }" }'
echo $body
$response = Invoke-WebRequest -Uri $graphql -Headers $headers -Method 'POST' -Body $body
echo $response
git checkout master
執行的效果如同 https://github.com/microsoft/winget-pkgs/pull/1940 我發送的結果。
重新整理整份的 script 如下:
Write-Host "1. 從 https://poumason.internal.com/jlr/version.json 檢查是否有新版本"
$response = Invoke-WebRequest -Uri "https://poumason.internal.com/jlr/version.json"
$jsonObj = ConvertFrom-Json $([String]::new($response.Content))
$lastestVer = $jsonObj.versions[0].version
$lastestUrl = $jsonObj.supports[0].url
$previousVer = $lastestVer
# 讀取本機的暫存檔,檢查上次抓到的版本跟最新的版本是否一致
$cacheFile = "..\.\winget_ver.txt"
if(![System.IO.File]::Exists($cacheFile)){ SET-Content -Path $cacheFile -Value $lastestVer } else { $previousVer = Get-Content -Path $cacheFile }
if ($previousVer -eq $lastestVer) {
Write-Host "*** The same version ***" -ForeGroundColor Blue
} else {
# 準備一個新的 branch
$branchName = "jlr-${lastestVer}"
git branch -D $branchName
git branch $branchName
git checkout $branchName
# 準備一個存安裝檔
$exeFile = ".\jlr-${lastestVer}.msi";
Write-Host "2. 下載 url 中的檔案 ${exeFile}"
Invoke-WebRequest $lastestUrl -OutFile $exeFile
Write-Host "3. 取得下載好檔案的 checksum ${exeFile}"
$checkSum = (Get-FileHash $exeFile -Algorithm sha256).hash
rm $exeFile
Write-Host "4. 產生新版本的 ${lastestVer}.yaml"
# publisherFolder 可根據自己的名稱與 App 名稱做改變
$publisherFolder = "poulin\jlr"
$fileName = ".\manifests\${publisherFolder}\${lastestVer}.yaml"
# 寫入 ID
$string = "Id: poulin.jlr"
write-output $string | out-file $filename
# 寫入 Version
$string = "Version: " + $lastestVer
write-output $string | out-file $filename -append
# 寫入 App 名稱
$string = "Name: Just Love Radio"
write-output $string | out-file $filename -append
# 寫入 Publisher
$string = "Publisher: Pou Lin"
write-output $string | out-file $filename -append
# 寫入 License
$string = "License: Copyright (c) Pou Lin All Rights Reserved."
write-output $string | out-file $filename -append
# 寫入 InstallerType
$string = "InstallerType: msi"
write-output $string | out-file $filename -append
$string = "Installers:"
write-output $string | out-file $filename -append
$string = " - Arch: x86"
write-output $string | out-file $filename -append
$string = " Url: " + $lastestUrl
write-output $string | out-file $filename -append
$string = " Sha256: " + $checkSum
write-output $string | out-file $filename -append
# 加入 Silent 與 SilentWithProgress
$string = " Switches:"
write-output $string | out-file $filename -append
$string = " Silent: /S"
write-output $string | out-file $filename -append
$string = " SilentWithProgress: /S"
write-output $string | out-file $filename -append
Write-Host "5. 更新暫存檔案到最新檢查的版本 ${cacheFile}"
SET-Content -Path $cacheFile -Value $lastestVer
Write-host GET-Content -Path $cacheFile
Write-Host "6. 寫入 git commit 並送到自己的 winget-pkgs repo"
git add $fileName
$comment = "Add JLR new version ${lastestVer}"
git commit -m $comment
git push --set-upstream origin $branchName
git push
Write-Host "7. create pull request for GitHub"
$graphql = "https://api.github.com/graphql"
$accessToken = "{申請自己專用的 GitHub Access Token}"
# microsoft:winget-pkgs 的 repo id
$repoId = "{}"
# 準備發送 api 的 http request headers
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("User-Agent", 'Agent')
$headers.Add("Authorization", 'bearer ' + $accessToken)
# 把 $branchName 加入來源
$branchName = "{你的 github 帳號}:${branchName}"
$body = '{ "query": "mutation { createPullRequest(input: { baseRefName: \"master\", headRefName: \"' + $branchName + '\", repositoryId: \"' + $repoId + '\", title: \"' + $comment + '\" , body: \"\"}) { clientMutationId} }" }'
echo $body
$response = Invoke-WebRequest -Uri $graphql -Headers $headers -Method 'POST' -Body $body
echo $response
git checkout master
}
再次強調這個 script 要放在跟自己 winget-pkgs 的目錄下,這樣才能正常執行。
===
在研究 GraphQL API 跟 Powershell 指令花了不少時間,感謝 Joe Wen 的幫忙讓我加快不少。
上面的 script 歡迎大家使用,如果有任何的 script 的問題歡迎跟我說,希望有幫忙到大家,謝謝。
參考資料
- createPullRequest - GraphQL API v4
- Invoke-WebRequest
- HTTP requests with PowerShell’s Invoke-WebRequest – by Example
- Send a JSON HTTP API Request in PowerShell
- Building JSON Payload in Powershell
- Creating a personal access token for the command line
- Is it possible to send additional HTTP headers to web services via New-WebServiceProxy