個人的共用 CI/CD 流程庫,用來統一所有 .NET 專案的建置、測試、發佈與文件流程。
| 類型 | 路徑 | 用途 |
|---|---|---|
| Reusable Workflow | .github/workflows/reusable-dotnet-ci.yml |
Restore → Build → Test(→ Pack),PR 與發版前驗證 |
| Reusable Workflow | .github/workflows/reusable-create-release.yml |
從 CHANGELOG 建立 GitHub Release,可附上 artifact |
| Reusable Workflow | .github/workflows/reusable-docfx-publish.yml |
建置 DocFX 文件並部署至 GitHub Pages |
| Reusable Workflow | .github/workflows/reusable-winget-submit.yml |
以現成的 WinGet manifest 向 microsoft/winget-pkgs 提交 PR |
| Composite Action | .github/actions/nuget-publish |
以 Trusted Publishing 打包並發佈至 nuget.org |
| Composite Action | .github/actions/velopack-pack |
以 Velopack 打包桌面應用程式安裝檔 |
| Reusable Workflow | .github/workflows/reusable-nuget-publish.yml |
nuget-publish |
- 依 SemVer 發佈
vX.Y.Ztag,並自動將 major tag(如v1)移到最新的相容版本。 - Caller 請以
@v1引用,不要用@main:main上的任何變更都會立刻影響所有 caller。 - 有破壞性變更時才會升 major(
v2),既有@v1不受影響。 - 搭配 Dependabot(
package-ecosystem: github-actions)可自動收到升版 PR。
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
ci:
uses: CloudyWing/devops-templates/.github/workflows/reusable-dotnet-ci.yml@v1
with:
solution: MyLib.slnx
warnaserror: true| 參數 | 預設值 | 說明 |
|---|---|---|
solution |
'' |
方案或專案路徑;留空時於根目錄執行 |
dotnet-version |
10.0.x |
SDK 版本,可多行指定多個版本 |
global-json-file |
'' |
設定後依 global.json 安裝 SDK,忽略 dotnet-version |
runs-on |
windows-latest |
含 .NET Framework 目標時請維持 Windows |
configuration |
Release |
建置組態 |
warnaserror |
false |
以 -warnaserror 建置 |
build-args / test-args |
'' |
額外傳給 dotnet build / dotnet test 的參數 |
test |
true |
是否執行測試;測試結果(trx)會上傳為 test-results artifact |
pack |
false |
是否打包並上傳為 packages artifact |
artifact-suffix |
'' |
同一 workflow 呼叫多次時,避免 artifact 名稱衝突 |
- 至 nuget.org 建立 Trusted Publishing policy:
- Repository Owner:
CloudyWing - Repository:套件 repo 名稱
- Workflow File:
release.yml(caller 自己的 workflow 檔名)
- Repository Owner:
- 不需要
NUGET_API_KEYsecret,已存在的可以刪除。
on:
push:
tags: ['v*.*.*']
workflow_dispatch: # 手動觸發時只執行 probe,驗證 Trusted Publishing 綁定
permissions:
contents: read
jobs:
verify:
if: github.event_name == 'push'
uses: CloudyWing/devops-templates/.github/workflows/reusable-dotnet-ci.yml@v1
with:
solution: MyLib.slnx
create-release:
needs: verify
permissions:
contents: write
uses: CloudyWing/devops-templates/.github/workflows/reusable-create-release.yml@v1
publish-nuget:
needs: create-release
runs-on: windows-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- uses: CloudyWing/devops-templates/.github/actions/nuget-publish@v1
with:
nuget-user: CloudyWing
expected-version: ${{ github.ref_name }}
probe:
if: github.event_name == 'workflow_dispatch'
runs-on: windows-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- uses: CloudyWing/devops-templates/.github/actions/nuget-publish@v1
with:
nuget-user: CloudyWing
dry-run: 'true'
nuget-publish刻意做成 composite action:nuget.org 會驗證 OIDC token 的job_workflow_ref,必須指向套件 repo 自己的 workflow。若改成 reusable workflow,claim 會指向本 repo,policy 就得授權給本 repo,等於所有 caller 都能發佈。
| 參數 | 預設值 | 說明 |
|---|---|---|
changelog-path |
CHANGELOG.md |
CHANGELOG 路徑 |
tag |
'' |
留空時使用觸發的 tag |
artifact-name |
'' |
同一 run 內上傳的 artifact,會成為 Release 附件 |
draft |
false |
保留為草稿 |
dry-run |
false |
只解析 Release Notes,不建立 Release |
- 支援的 CHANGELOG 標題:
## v1.2.3 (2026-01-01)、## 1.2.3、## [v1.2.3] - 2026-01-01(Keep a Changelog)。找不到對應段落時改用自動產生的 Release Notes。 - 版號含
-(如v1.0.0-beta.1)時自動標為 prerelease。 - Release 已存在(例如重跑 workflow)時會更新內容並覆蓋附件,不會失敗。
- Outputs:
tag、prerelease、notes-source(changelog/generated)。
| 參數 | 預設值 | 說明 |
|---|---|---|
nuget-user |
'' |
nuget.org profile name;pack-only 時可省略 |
project-path |
./src |
打包對象的搜尋根目錄 |
artifacts-path |
./artifacts |
nupkg 輸出目錄 |
expected-version |
'' |
檢查每個 nupkg 版號是否等於此值,避免 tag 與 csproj 不一致 |
dry-run |
'false' |
取得憑證後即結束,用來驗證 policy 綁定 |
pack-only |
'false' |
只打包,不登入也不 push |
- Outputs:
packages(產出的 nupkg 檔名,以換行分隔)。
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- run: dotnet publish src/MyApp/MyApp.csproj -c Release -r win-x64 --self-contained -o artifacts/publish
- uses: CloudyWing/devops-templates/.github/actions/velopack-pack@v1
with:
version: ${{ github.ref_name }}
pack-id: CloudyWing.MyApp
main-exe: MyApp.exe
pack-dir: artifacts/publish
output-dir: artifacts/velopack
vpk-version: '1.2.161' # 必須與專案引用的 Velopack 套件版本一致
delta: 'true'
github-token: ${{ github.token }}
# 每行一個參數;旗標與其值各佔一行,值含空白時直接寫原文,不加引號
extra-args: |
--packTitle
My Desktop App
--packAuthors
My Company
--framework
webview2
--runtime
win-x64
--icon
assets/app icon.ico
--instReadme
docs/install readme.md
- uses: actions/upload-artifact@v7
with:
name: velopack
path: artifacts/velopack/*
release:
needs: build
permissions:
contents: write
uses: CloudyWing/devops-templates/.github/workflows/reusable-create-release.yml@v1
with:
artifact-name: velopack| 參數 | 預設值 | 說明 |
|---|---|---|
version / pack-id / main-exe / pack-dir / output-dir |
— | 必填 |
vpk-version |
'' |
留空時使用 .config/dotnet-tools.json 的 local tool |
channel |
'' |
Velopack channel |
delta |
'false' |
先下載前一版 Release,產生 delta 更新包;找不到前一版時只會警告 |
github-token |
'' |
下載前一版時使用(private repo 必填) |
extra-args |
'' |
額外傳給 vpk pack 的參數,每行一個參數(規則見下方說明) |
extra-args以換行切分,每個非空白行是一個獨立的vpk參數,原樣傳入。旗標與其值各佔一行,例如--packTitle換行My Desktop App。- 值含空白(如標題或含空白的路徑)時獨立一行,直接寫原文。引號字元會成為參數內容,因此不加引號。
- 單行寫法如
--icon app.ico會被視為單一參數,由vpk以非零 exit code 回報失敗。 - Outputs:
vpk-command(實際使用的 vpk 指令,vpk或dotnet vpk)。
- Settings > Pages > Source 設為 GitHub Actions(不再使用
gh-pagesbranch)。 - 呼叫的 job 需授予
pages: write、id-token: write。
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
docs:
permissions:
contents: read
pages: write
id-token: write
uses: CloudyWing/devops-templates/.github/workflows/reusable-docfx-publish.yml@v1
with:
docfx-config: docs/docfx.json| 參數 | 預設值 | 說明 |
|---|---|---|
docfx-config |
docfx.json |
docfx.json 路徑 |
site-dir |
_site |
DocFX 輸出目錄,需與 docfx.json 的 build.output 一致 |
dotnet-version |
10.0.x |
SDK 版本 |
docfx-version |
2.81.0 |
固定 DocFX 版本,確保建置可重現 |
runs-on |
windows-latest |
Runner label |
deploy |
true |
為 false 時只建置不部署,適合 PR 驗證 |
- Outputs:
page-url(部署後的網址,deploy為true時)。
以 caller 提供的現成 WinGet manifest 向 microsoft/winget-pkgs 提交 PR。本範本不重新產生 manifest,PackageIdentifier、PackageVersion、InstallerUrl 與各 locale 內容都以 caller 的檔案為準。
- 建立 classic Personal Access Token 並勾選
public_repo(fine-grained token 不支援),存成 caller repo 的 secret,例如WINGET_TOKEN。 - token 擁有者帳號下沒有
winget-pkgsfork 時,wingetcreate會自動建立,不需要事先 fork。 - 在建置 job 將 manifest 上傳為 artifact,內容須已是最終版號。檔案可直接位於 artifact 根目錄,或位於一層版本子目錄(如
1.2.3/),且恰有一個*.installer.yaml。 - 呼叫的 job 以
needs相依於建立 Release 的 job,確保InstallerUrl指向的 Release 資產已公開。
on:
push:
tags: ['v*.*.*']
permissions:
contents: read
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
# 建置、打包,並產生含最終版號的 WinGet manifest 到 artifacts/winget/
- uses: actions/upload-artifact@v7
with:
name: release-assets
path: artifacts/release/*
- uses: actions/upload-artifact@v7
with:
name: winget-manifest
path: artifacts/winget/*
create-release:
needs: build
permissions:
contents: write
uses: CloudyWing/devops-templates/.github/workflows/reusable-create-release.yml@v1
with:
artifact-name: release-assets
submit-winget:
needs: create-release
permissions:
contents: read
uses: CloudyWing/devops-templates/.github/workflows/reusable-winget-submit.yml@v1
with:
manifest-artifact-name: winget-manifest
package-identifier: MyCompany.MyApp
secrets:
winget-token: ${{ secrets.WINGET_TOKEN }}| 參數 | 預設值 | 說明 |
|---|---|---|
manifest-artifact-name |
— | 必填。同一 workflow run 內已上傳的 artifact 名稱,內容為 WinGet manifest |
package-identifier |
— | 必填。WinGet PackageIdentifier,須與 installer manifest 的 PackageIdentifier 逐字相同 |
tag |
'' |
要提交的版本 tag;留空時,觸發的 ref 為 tag 才使用該 tag。明確指定時視為 tag 觸發,不論 ref 型別 |
dry-run |
false |
為 true 時執行到準備提交為止,不呼叫 wingetcreate submit,也不需要 winget-token |
url-check-timeout-seconds |
300 |
InstallerUrl 檢查的總逾時秒數,所有 URL 共用;下載 wingetcreate 時另以相同秒數作為重試期限 |
runs-on |
windows-latest |
Runner label,wingetcreate 為 Windows 執行檔,須為 Windows runner |
- Secret:
winget-token(選填,classic PAT;判定需提交且dry-run為false時必填,跳過或dry-run時不檢查)。 - Outputs:
result(submitted、dry-run或skipped)、skip-reason(result為skipped時為not-a-tag、prerelease或open-pr-exists)、pull-request-url(submitted時為新 PR 網址,open-pr-exists時為既有 PR 網址)、version(提交版號,tag 去除開頭的v)。 - 只在正式版 tag 提交。沒有 tag 時以
not-a-tag跳過,版號含-(如v1.0.0-beta.1)時以prerelease跳過,兩者都視為成功結束。 - manifest 的
PackageIdentifier須等於package-identifier,PackageVersion須等於 tag 去除v後的版號,不一致時失敗,避免提交來自其他版本的 manifest。 - 提交前確認 manifest 內每個
InstallerUrl都以https://開頭且可下載,在url-check-timeout-seconds內每 10 秒重試一次,逾時仍未通過則失敗。 microsoft/winget-pkgs已有同套件同版本的 open PR 時以open-pr-exists跳過,不重複提交。wingetcreate的版本與 SHA-256 固定於 workflow 內,下載失敗時在url-check-timeout-seconds內每 10 秒重試一次,下載後會校驗;升級時需修改 workflow 並發新版,不提供 input。- 本範本的
dry-run涵蓋版號判定、manifest 解析、去重查詢、InstallerUrl檢查與工具校驗,不涵蓋wingetcreate對 manifest 的解析與實際提交,這兩項在第一次真實提交時才會驗證。
self-test.yml 會在每次 push / PR 時,以 tests/fixtures 的範例專案實際執行所有範本,不發佈任何套件:
actionlint(含 shellcheck)檢查所有 workflowreusable-dotnet-ci:Ubuntu 與 Windows 各跑一次 build、test、packnuget-publish:pack-only模式,並驗證版號不符、前面專案編譯失敗時一定會失敗reusable-create-release:以dry-run驗證四種 CHANGELOG 情境reusable-docfx-publish:deploy: false只建置velopack-pack:實際打包範例程式reusable-winget-submit:以範例 manifest 執行dry-run,並驗證預發布版本與非 tag 的跳過輸出
手動觸發並勾選 e2e-release 時,會在本 repo 建立暫時的 prerelease(含附件)、重跑一次驗證更新流程,最後刪除該 Release 與 tag。
Trusted Publishing 的 OIDC 綁定只能在各套件 repo 驗證:手動觸發套件 repo 的 release.yml,probe job 會以 dry-run 取得臨時憑證但不 push。
This project is MIT licensed.