Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# 契约 SDK 的日常验证。发布不走这里(见 release.yml),但**发布会做的事这里基本都做一遍**。
# 契约 SDK 与插件构建支持包的日常验证。发布不走这里(见 release.yml),
# 但**发布会做的事这里基本都做一遍**。
#
# 本仓库产出三个包,共用一个版本号:
# VelaShell.PluginSdk 契约程序集
# VelaShell.PluginSdk.Testing 测试替身
# VelaShell.PluginSdk.Build 插件工程只需引用的那一个包
# (2026-09-11 从 velashell-plugin-cli 搬来)
#
# 与 release.yml 的差别只有三处:不推 nuget.org、不上传产物到 Release、
# 以及 fork PR 拿不到强名称密钥时关掉签名继续跑。
Expand Down Expand Up @@ -91,21 +98,27 @@ jobs:
run: |
$version = '${{ steps.version.outputs.version }}'
New-Item -ItemType Directory -Force artifacts/nuget | Out-Null
# VelaShell.PluginSdk.Build 也在这一批里:它与契约包同仓库、同版本、同一次发布
# (2026-09-11 从 velashell-plugin-cli 搬过来)。顺序无所谓 —— 它对契约包和打包器
# 都是 ProjectReference,dotnet pack 会自己先把那两个工程建出来。
# (VelaShell.PluginSdk.Packer 不在这个列表里:它 IsPackable=false,只以构建产物的
# 形式躺在 .Build 包的 tools/ 下。)
$projects = @(
'src/VelaShell.PluginSdk/VelaShell.PluginSdk.csproj',
'src/VelaShell.PluginSdk.Testing/VelaShell.PluginSdk.Testing.csproj'
'src/VelaShell.PluginSdk.Testing/VelaShell.PluginSdk.Testing.csproj',
'src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj'
)
foreach ($project in $projects) {
dotnet pack $project -c Release -o artifacts/nuget -p:VelaSdkVersion=$version --nologo $env:VELA_SIGN_ARGS
if ($LASTEXITCODE -ne 0) { exit 1 }
}
Get-ChildItem artifacts/nuget | Select-Object -ExpandProperty Name

# ── 拆库之后新增的一步 ────────────────────────────────────────────────
# 本仓库是 Avalonia 版本锁的**权威**,而这个事实只通过包里那一个 buildTransitive
# props 文件传达给下游(宿主的 VerifyAvaloniaMatchesSdk、cli 仓库的
# VerifyAvaloniaVersionPin 都读它)。单仓库时代那两处核对是直接读文件的,
# 漏打进包也没人发现;现在包就是唯一通道,所以拆完包核对一遍它真的在里面。
# 本仓库是 Avalonia 版本锁的**权威**,而这个事实传给**宿主**的唯一通道就是包里那一个
# buildTransitive props 文件(宿主的 VerifyAvaloniaMatchesSdk 读它)。它没打进包也不会
# 让 pack 失败,所以拆开核对一遍。
# (.Build 现在同仓库,直接读 $(VelaAvaloniaVersion),不走这条通道 —— 见其
# VerifyAvaloniaVersionPin。所以这一步守的是宿主那一侧。)
- name: Verify the pin props shipped in the package
shell: pwsh
run: |
Expand All @@ -121,7 +134,7 @@ jobs:
Expand-Archive -Path $zip -DestinationPath (Join-Path $work 'x')
$props = Join-Path $work 'x/buildTransitive/VelaShell.PluginSdk.props'
if (-not (Test-Path $props)) {
throw "buildTransitive/VelaShell.PluginSdk.props is missing from the package. Downstream repos read the pinned Avalonia version from it; without it the host and VelaShell.PluginSdk.Build lose their only cross-repo consistency check."
throw "buildTransitive/VelaShell.PluginSdk.props is missing from the package. The VelaShell host reads the pinned Avalonia version from it; without it the host loses its only consistency check against this SDK."
}
$text = Get-Content -Raw $props
$expected = [regex]::Match((Get-Content -Raw Directory.Build.props), '<VelaAvaloniaVersion>([^<]+)</VelaAvaloniaVersion>').Groups[1].Value
Expand All @@ -132,6 +145,24 @@ jobs:
Write-Host "Pinned Avalonia = $shipped, SDK package version = $version"
Write-Host $text

# 端到端冒烟:拿刚打出的包,站在插件作者的位置上走一遍
# (最小插件工程 → 还原 → 构建 → 出 .vpx → 读回容器 → 查共享程序集有没有漏)。
# 夹具在 tests/smoke/,细节见 scripts/Invoke-Smoke.ps1 的注释。
#
# 这一步不是形式,它挡两类东西:
# ① 插件工程是**仓库外环境**,仓库内的 Directory.Build.props 一条也吃不到 ——
# 历史上两个最难查的坑(CA2252 全线报错、AXAML 编译器到不了插件工程)
# 都只有在这个前提下才会显形;
# ② 打包器(VelaShell.PluginSdk.Packer)真的进了包、targets 真的调得动它 ——
# "随包分发、无需全局工具"是本包对插件作者的承诺,而漏打或调错了只有真跑一遍
# 才看得见(插件作者那边的现象是一句 VELA1003 或构建到一半 Exec 失败)。
# 改 targets 或改打包器命令面之后必跑。
- name: Smoke test (plugin project -> build -> .vpx)
shell: pwsh
run: |
& ./scripts/Invoke-Smoke.ps1 -Feed ./artifacts/nuget -Version '${{ steps.version.outputs.version }}'
if ($LASTEXITCODE) { exit $LASTEXITCODE }

- uses: actions/upload-artifact@v7
with:
name: nuget-packages
Expand Down
65 changes: 50 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
# 契约 SDK 的发布流水线:在 GitHub 页面**发布 Release** 时自动触发。
# 插件契约 SDK 与构建支持包的发布流水线:在 GitHub 页面**发布 Release** 时自动触发。
#
# 一次发布产出两个 NuGet 包,共用 Release 标签里的那个版本号:
# 一次发布产出三个 NuGet 包,共用 Release 标签里的那个版本号:
#
# VelaShell.PluginSdk 契约程序集(插件与宿主唯一共享的类型)
# VelaShell.PluginSdk.Testing 测试替身(TestPluginContext 与各能力内存实现)
# VelaShell.PluginSdk.Build 插件工程**只需引用的那一个包**(targets + 打包器 + 依赖锁)
#
# **其余三个包不在这里发**(2026-08-27 拆库起):
# VelaShell.Plugin.Cli / VelaShell.PluginSdk.Build … VelaShellLabs/velashell-plugin-cli
# VelaShell.Plugin.Templates ………………………………… VelaShellLabs/velashell-plugin-templates
# 那两个仓库有自己的版本号与自己的 Release 流水线。本仓库发 1.6.0 **不要求**它们跟着发;
# 它们只在想吃到新契约时才把自己引用的 VelaShell.PluginSdk 版本抬上来。
# .Build 于 2026-09-11 从 velashell-plugin-cli 搬来。它对契约包是 ProjectReference,
# 因此传给插件工程的那份契约永远就是本仓库这一版 —— 原先"引用哪一版契约"那个跨仓库
# 旋钮就此消失,连带它漂移的可能。插件工程出包用的打包器同样在本仓库
# (src/VelaShell.PluginSdk.Packer,IsPackable=false,只随 .Build 的 tools/ 分发)。
#
# **另外两个包不在这里发**:
# VelaShell.Plugin.Cli ……………… VelaShellLabs/velashell-plugin-cli
# VelaShell.Plugin.Templates …… VelaShellLabs/velashell-plugin-templates
# 那两个仓库有自己的版本号与自己的 Release 流水线,本仓库发版**不要求**它们跟着发,
# 本仓库也**不引用**它们的任何东西 —— 依赖方向是单向的,没有环。
#
# 📌 版本号:**发版前在本地落好、随功能改动一起合进 main**。
# 跑一次 `pwsh scripts/Set-Version.ps1 <版本>`,它会把该版本写进 Directory.Build.props、
Expand All @@ -22,13 +28,16 @@
# Release 标签,与仓库里当时提交了什么无关。忘了第 ② 步的兜底是 CI 的版本同步体检。
#
# ⚠️ Avalonia 版本锁:本仓库是整个插件生态里这个版本号的**权威**。改 VelaAvaloniaVersion
# 等于改插件生态的 Avalonia 版本 —— 必须与宿主同一波发布,并且发完之后让
# velashell-plugin-cli 抬一次它引用的 SDK 版本,否则它那道 VerifyAvaloniaVersionPin 会红。
# 等于改插件生态的 Avalonia 版本 —— 必须与宿主同一波发布,并且**同一个提交里**把
# src/VelaShell.PluginSdk.Build 的两处副本(csproj 上那条精确区间、build/*.props 里的
# 同名默认值)一起改掉,否则构建期的 VELA1000 / VELA1006 会红。
#
# 推送用 **NuGet Trusted Publishing(OIDC)**,不存 API Key:
# NuGet/login 拿本次运行的 GitHub OIDC 令牌去 nuget.org 换一把**短时效**的推送密钥,
# 用完即弃。密钥不落仓库机密,也就没有"泄漏了要轮换"这件事。
# ⚠️ 首次发版前必须在 nuget.org 上为**这两个包**各建一条可信发布策略,三项都填新值:
# ⚠️ 首次发版前必须在 nuget.org 上为**这三个包**各建一条可信发布策略,三项都填新值:
# (VelaShell.PluginSdk.Build 原先那条指向 velashell-plugin-cli,搬库之后**必须重建**,
# 否则推它的时候会被 nuget.org 拒掉 —— 而那时前两个包已经推上去了。)
# Repository owner VelaShellLabs
# Repository velashell-plugin-sdk
# Workflow file release.yml
Expand Down Expand Up @@ -126,19 +135,27 @@ jobs:
run: |
$version = '${{ steps.version.outputs.version }}'
New-Item -ItemType Directory -Force artifacts/nuget | Out-Null
# VelaShell.PluginSdk.Build 也在这一批里:它与契约包同仓库、同版本、同一次发布
# (2026-09-11 从 velashell-plugin-cli 搬过来)。顺序无所谓 —— 它对契约包和打包器
# 都是 ProjectReference,dotnet pack 会自己先把那两个工程建出来。
# (VelaShell.PluginSdk.Packer 不在这个列表里:它 IsPackable=false,只以构建产物的
# 形式躺在 .Build 包的 tools/ 下。)
$projects = @(
'src/VelaShell.PluginSdk/VelaShell.PluginSdk.csproj',
'src/VelaShell.PluginSdk.Testing/VelaShell.PluginSdk.Testing.csproj'
'src/VelaShell.PluginSdk.Testing/VelaShell.PluginSdk.Testing.csproj',
'src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj'
)
foreach ($project in $projects) {
dotnet pack $project -c Release -o artifacts/nuget -p:VelaSdkVersion=$version --nologo
if ($LASTEXITCODE -ne 0) { exit 1 }
}
Get-ChildItem artifacts/nuget | Select-Object -ExpandProperty Name

# 拆库之后,包里的 buildTransitive props 是 Avalonia 版本锁传给下游的**唯一通道**
# (宿主的 VerifyAvaloniaMatchesSdk、cli 仓库的 VerifyAvaloniaVersionPin 都读它)。
# 它没打进包也不会让 pack 失败,所以在推之前拆开核对一遍。
# 本仓库是 Avalonia 版本锁的**权威**,而这个事实传给**宿主**的唯一通道就是包里那一个
# buildTransitive props 文件(宿主的 VerifyAvaloniaMatchesSdk 读它)。它没打进包也不会
# 让 pack 失败,所以拆开核对一遍。
# (.Build 现在同仓库,直接读 $(VelaAvaloniaVersion),不走这条通道 —— 见其
# VerifyAvaloniaVersionPin。所以这一步守的是宿主那一侧。)
- name: Verify the pin props shipped in the package
shell: pwsh
run: |
Expand All @@ -154,7 +171,7 @@ jobs:
Expand-Archive -Path $zip -DestinationPath (Join-Path $work 'x')
$props = Join-Path $work 'x/buildTransitive/VelaShell.PluginSdk.props'
if (-not (Test-Path $props)) {
throw "buildTransitive/VelaShell.PluginSdk.props is missing from the package. Downstream repos read the pinned Avalonia version from it; without it the host and VelaShell.PluginSdk.Build lose their only cross-repo consistency check."
throw "buildTransitive/VelaShell.PluginSdk.props is missing from the package. The VelaShell host reads the pinned Avalonia version from it; without it the host loses its only consistency check against this SDK."
}
$text = Get-Content -Raw $props
$expected = [regex]::Match((Get-Content -Raw Directory.Build.props), '<VelaAvaloniaVersion>([^<]+)</VelaAvaloniaVersion>').Groups[1].Value
Expand All @@ -165,6 +182,24 @@ jobs:
Write-Host "Pinned Avalonia = $shipped, SDK package version = $version"
Write-Host $text

# 端到端冒烟:拿刚打出的包,站在插件作者的位置上走一遍
# (最小插件工程 → 还原 → 构建 → 出 .vpx → 读回容器 → 查共享程序集有没有漏)。
# 夹具在 tests/smoke/,细节见 scripts/Invoke-Smoke.ps1 的注释。
#
# 这一步不是形式,它挡两类东西:
# ① 插件工程是**仓库外环境**,仓库内的 Directory.Build.props 一条也吃不到 ——
# 历史上两个最难查的坑(CA2252 全线报错、AXAML 编译器到不了插件工程)
# 都只有在这个前提下才会显形;
# ② 打包器(VelaShell.PluginSdk.Packer)真的进了包、targets 真的调得动它 ——
# "随包分发、无需全局工具"是本包对插件作者的承诺,而漏打或调错了只有真跑一遍
# 才看得见(插件作者那边的现象是一句 VELA1003 或构建到一半 Exec 失败)。
# 改 targets 或改打包器命令面之后必跑。
- name: Smoke test (plugin project -> build -> .vpx)
shell: pwsh
run: |
& ./scripts/Invoke-Smoke.ps1 -Feed ./artifacts/nuget -Version '${{ steps.version.outputs.version }}'
if ($LASTEXITCODE) { exit $LASTEXITCODE }

- uses: actions/upload-artifact@v7
with:
name: nuget-packages
Expand Down
54 changes: 47 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,67 @@ VelaShell 生态的**全部文档**集中在一个仓库:
- **例外**:留在代码仓库里的少数几份文件不适用上述规则,因为它们服务的是「在这个仓库里写代码」
这件事,搬走只会离使用场景更远。各仓库的例外清单见下面第三节。

## 三、本仓库:velashell-plugin-sdk(插件契约 SDK)
## 三、本仓库:velashell-plugin-sdk(插件契约 SDK + 构建支持包)

产出 `VelaShell.PluginSdk`(契约程序集)与 `VelaShell.PluginSdk.Testing`(测试替身)两个 NuGet 包。
这是插件与宿主**唯一共享的那批类型**。
产出三个**同版本、同一次发布**的 NuGet 包:

| 包 | 是什么 |
| --- | --- |
| `VelaShell.PluginSdk` | 契约程序集 —— 插件与宿主**唯一共享的那批类型** |
| `VelaShell.PluginSdk.Testing` | 测试替身 |
| `VelaShell.PluginSdk.Build` | 插件工程**只需引用的那一个包**:targets + 随包分发的打包器 + 依赖锁 |

外加一个**不发包**的工程:`src/VelaShell.PluginSdk.Packer`(`IsPackable=false`)——
`.vpx` 打包器,只以构建产物的形式躺在 `.Build` 包的 `tools/` 下。

`.Build` 于 2026-09-11 从 `velashell-plugin-cli` 搬来,打包器随后也做进了本仓库。
两者都走 `ProjectReference`,所以发给插件作者的契约与打包器永远就是本仓库这一版 ——
这正是搬过来要的效果:没有任何跨仓库的版本需要对齐。

### 构建与测试

```bash
dotnet build VelaShell.PluginSdk.slnx
dotnet test VelaShell.PluginSdk.slnx -c Debug

# 端到端冒烟:拿刚打出的包当插件作者走一遍
dotnet pack src/VelaShell.PluginSdk/VelaShell.PluginSdk.csproj -c Release -o artifacts/nuget
dotnet pack src/VelaShell.PluginSdk.Build/VelaShell.PluginSdk.Build.csproj -c Release -o artifacts/nuget
pwsh scripts/Invoke-Smoke.ps1 -Feed ./artifacts/nuget -Version <版本>
```

`-c Debug` 不是随口一说:Release 会打开强名称签名,而测试程序集不是签名友元,
Release 下 `dotnet test` 编不过。签名密钥不入库,CI 从 `STRONG_NAME_KEY` 机密还原。
Release 下 `dotnet test` 编不过。签名密钥不入库,CI 从 `STRONG_NAME_KEY` 机密还原;
本地想打 Release 包又没有密钥时加 `-p:SignAssembly=false`。

冒烟夹具在 `tests/smoke/`:一个手写的最小插件工程,刻意带两个空的
`Directory.Build.props`/`.targets` 来切断向上查找 —— **插件工程是仓库外环境,仓库内的
构建约定一条也吃不到**。**改了 `.Build` 的 targets、或抬了它引用的 vela-plugin 版本,必须跑它。**

### 只有本仓库能动的两件事

1. **`apiLevel` 纪律** —— `VelaPluginApi.Level` 是插件兼容性的整数代际,宿主拒载高于自身代际的插件。
纪律是「SDK 主版本 == apiLevel」,由 `scripts/Set-Version.ps1` 在发版前硬核对。
**破坏性变更要先手工把 `Level` +1**:脚本会核对但不代改,因为「契约破没破」是人的判断。
2. **Avalonia 版本锁的权威** —— 值写在 `Directory.Build.props` 的 `VelaAvaloniaVersion`,
由本包经 `buildTransitive` 导出成 `$(VelaSdkPinnedAvaloniaVersion)`,宿主与 CLI 仓库在
各自构建期核对。改它 = 改整个插件生态的 Avalonia 版本,必须与宿主同一波发布。
由 `VelaShell.PluginSdk` 包经 `buildTransitive` 导出成 `$(VelaSdkPinnedAvaloniaVersion)`
供宿主核对;`.Build` 现在同仓库,直接读这个属性(`VerifyAvaloniaVersionPin`)。
改它 = 改整个插件生态的 Avalonia 版本,必须与宿主同一波发布,并且在**同一个提交里**
把 `.Build` 的两处副本(csproj 上那条精确区间、`build/*.props` 里的同名默认值)一起改掉。

### 打包器为什么在本仓库、为什么是独立进程

`.vpx` 的定义(`VpxContainer`)与清单规则(`PluginManifestReader`)本来就在
`VelaShell.PluginSdk` 里 —— `vela-plugin` 只是它的另一个调用方。让 `.Build` 绕一趟 cli 仓库
去取打包器,换来的只是一个要人盯着的跨仓库版本旋钮(2026-09-11 短暂存在过,已去掉)。

**刻意不做成 MSBuild 任务**:VS 的 MSBuild 跑在 .NET Framework 上,而本仓库是 net11.0。
做成 `<UsingTask>` 的话,插件作者在 VS 里一按生成就会因为加载不了任务程序集而失败 ——
而清单校验是 `AfterTargets="Build"` 的,每次生成都跑。`dotnet exec` 一个独立进程则与调用方
的 MSBuild 是哪种运行时完全无关。

改了打包器的命令面(`validate` / `pack` 的参数或输出),`.Build` 的 `build/*.targets`
要跟着改,**并跑一次 `scripts/Invoke-Smoke.ps1`** —— 那个冒烟是这条链唯一的守门人。

### 版本号不归你定 —— 也不要为了自证能编译去造本地包

Expand All @@ -68,8 +106,10 @@ Release 下 `dotnet test` 编不过。签名密钥不入库,CI 从 `STRONG_NAME_
- **不要**跑 `scripts/Set-Version.ps1`、不要动 `Directory.Build.props` 的 `VelaSdkVersion`、
不要动 `VelaPluginApi.SdkVersion`。下一版是 1.6.0 还是 1.5.2、还是先发个 preview,
取决于当时排了什么、要不要跟宿主同波发 —— 这些你不知道。
- **不要** `dotnet pack` 出本地包、不要在任何 `nuget.config` 里加本地源、
- **不要** `dotnet pack` 出本地包给**别的仓库**用、不要在任何 `nuget.config` 里加本地源、
不要把下游仓库的 `Directory.Packages.props` 指到一个还不存在的版本。
(例外只有一个:`scripts/Invoke-Smoke.ps1` 会把包打进本仓库的临时源自己验自己,
那是 CI 的常规步骤,产物不出本仓库、也不进任何人的 `nuget.config`。)
自己造一个包来让编译通过,等于把「这套东西还没发布」这个事实从构建结果里抹掉;
而且本地包必然**未签名**(`VelaShell.snk` 不在仓库里),下游一编译就是一片
`CS0012` 强名称不匹配 —— 那是自己制造的噪声,不是真问题。
Expand Down
Loading