Skip to content

Commit 683e9bc

Browse files
Seggrclaude
andcommitted
ci: auto-publish plugins on push to their source folder
publish-plugins.yml now has a `push` trigger with `paths:` filters on each plugin folder, plus a `dorny/paths-filter` step that identifies which specific plugins changed in the commit. Only the changed plugin(s) get packed + pushed; unchanged plugins are skipped entirely so the workflow run is cheap. The existing `workflow_dispatch` selector still works for manual republish (post-NuGet-outage retry, etc.). `--skip-duplicate` keeps the auto-trigger safe: a code edit that forgets to bump the csproj `<Version>` runs the workflow but is a no-op on nuget.org, so the code doesn't ship until the version bumps. That's the enforcement against accidentally shipping the same version with different bits. CONTRIBUTING.md updated to document the auto-trigger. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7e3d3dc commit 683e9bc

2 files changed

Lines changed: 94 additions & 22 deletions

File tree

.github/workflows/publish-plugins.yml

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,27 @@ name: Publish plugins
33
# Plugin packages are released independently from the host:
44
# * The host's release workflow (release.yml) handles the Velopack
55
# installer + draft GitHub Release; it does NOT touch nuget.org.
6-
# * This workflow handles plugin .nupkg publish. Run it manually
7-
# whenever a plugin's <Version> in its csproj changes.
6+
# * This workflow handles plugin .nupkg publish. Two triggers:
7+
# - push to main with changes inside a plugin folder → auto-detect
8+
# which plugin changed and publish only that one
9+
# - workflow_dispatch → manual "publish this specific package now"
810
#
911
# Each plugin tracks its own version in its csproj. nuget.org's
10-
# `--skip-duplicate` makes pushing an unchanged version a no-op, so
11-
# selecting "all" never republishes versions that already exist.
12+
# `--skip-duplicate` makes pushing an unchanged version a no-op, so a
13+
# code edit that forgets to bump <Version> simply doesn't ship — author
14+
# has to bump the csproj to actually publish.
1215

1316
on:
17+
push:
18+
branches: [main]
19+
paths:
20+
- 'src/AzureTray.Plugin.Contracts/**'
21+
- 'src/AzureTray.Plugin.PIM/**'
22+
- 'src/AzureTray.Plugin.LAPS/**'
1423
workflow_dispatch:
1524
inputs:
1625
plugin:
17-
description: Which package to publish
26+
description: Which package to publish (used only on manual runs)
1827
type: choice
1928
required: true
2029
default: all
@@ -41,54 +50,110 @@ jobs:
4150
- name: Checkout
4251
uses: actions/checkout@v4
4352
with:
44-
fetch-depth: 0 # SourceLink needs commit history
53+
fetch-depth: 0 # SourceLink + paths-filter need history
4554

55+
# Per-plugin path filter. Outputs booleans we'll read below to
56+
# decide which projects to pack. On workflow_dispatch the action
57+
# is skipped (push event only); we fall back to the manual input.
58+
- name: Detect changed plugin folders
59+
id: filter
60+
if: github.event_name == 'push'
61+
uses: dorny/paths-filter@v3
62+
with:
63+
base: ${{ github.event.before }}
64+
filters: |
65+
contracts:
66+
- 'src/AzureTray.Plugin.Contracts/**'
67+
pim:
68+
- 'src/AzureTray.Plugin.PIM/**'
69+
laps:
70+
- 'src/AzureTray.Plugin.LAPS/**'
71+
72+
- name: Resolve plugin selection
73+
id: pick
74+
shell: pwsh
75+
env:
76+
IS_PUSH: ${{ github.event_name == 'push' }}
77+
MANUAL: ${{ github.event.inputs.plugin }}
78+
CHANGED_CONTRACTS: ${{ steps.filter.outputs.contracts }}
79+
CHANGED_PIM: ${{ steps.filter.outputs.pim }}
80+
CHANGED_LAPS: ${{ steps.filter.outputs.laps }}
81+
run: |
82+
$selected = New-Object System.Collections.Generic.List[string]
83+
if ($env:IS_PUSH -eq 'true') {
84+
# Auto mode — include every plugin folder that changed in this push.
85+
if ($env:CHANGED_CONTRACTS -eq 'true') { $selected.Add('Contracts') }
86+
if ($env:CHANGED_PIM -eq 'true') { $selected.Add('PIM') }
87+
if ($env:CHANGED_LAPS -eq 'true') { $selected.Add('LAPS') }
88+
}
89+
else {
90+
# Manual mode — honour the dropdown.
91+
switch ($env:MANUAL) {
92+
'all' { $selected.AddRange([string[]]@('Contracts','PIM','LAPS')) }
93+
'Contracts' { $selected.Add('Contracts') }
94+
'PIM' { $selected.Add('PIM') }
95+
'LAPS' { $selected.Add('LAPS') }
96+
}
97+
}
98+
99+
$list = ($selected -join ',')
100+
Write-Host "Selected for publish: [$list]"
101+
"selected=$list" >> $env:GITHUB_OUTPUT
102+
"any=$([bool]($selected.Count -gt 0)).ToString().ToLower()" >> $env:GITHUB_OUTPUT
103+
104+
# Everything below is a no-op when the selection is empty — saves
105+
# restore/build time on push events that didn't touch a plugin.
46106
- name: Setup .NET
107+
if: steps.pick.outputs.any == 'true'
47108
uses: actions/setup-dotnet@v4
48109
with:
49110
global-json-file: global.json
50111

51112
- name: Restore
113+
if: steps.pick.outputs.any == 'true'
52114
run: dotnet restore AzureTray.sln
53115

54116
- name: Build (Release)
117+
if: steps.pick.outputs.any == 'true'
55118
run: dotnet build AzureTray.sln --configuration Release --no-restore
56119

57-
# Pack only the selected projects. Each csproj declares its own
58-
# <Version> so no /p:Version override is passed — the package
59-
# version is whatever the csproj says.
60-
- name: Pack
120+
- name: Pack selected plugin(s)
121+
if: steps.pick.outputs.any == 'true'
61122
shell: pwsh
123+
env:
124+
SELECTED: ${{ steps.pick.outputs.selected }}
62125
run: |
63-
$plugin = '${{ github.event.inputs.plugin }}'
64126
$projects = @{
65127
'Contracts' = 'src/AzureTray.Plugin.Contracts/AzureTray.Plugin.Contracts.csproj'
66128
'PIM' = 'src/AzureTray.Plugin.PIM/AzureTray.Plugin.PIM.csproj'
67129
'LAPS' = 'src/AzureTray.Plugin.LAPS/AzureTray.Plugin.LAPS.csproj'
68130
}
69-
$selected = if ($plugin -eq 'all') { $projects.Values } else { @($projects[$plugin]) }
70-
131+
$picks = $env:SELECTED -split ',' | Where-Object { $_ }
71132
if (-not (Test-Path NuGet)) { New-Item -ItemType Directory -Path NuGet | Out-Null }
72-
foreach ($csproj in $selected) {
73-
Write-Host "Packing $csproj …" -ForegroundColor Cyan
133+
foreach ($pick in $picks) {
134+
$csproj = $projects[$pick]
135+
Write-Host "Packing $pick ($csproj) ..." -ForegroundColor Cyan
74136
dotnet pack $csproj --configuration Release --no-build --output NuGet
75137
if ($LASTEXITCODE -ne 0) { throw "dotnet pack failed for $csproj" }
76138
}
77139
78140
- name: Upload nupkgs as artifact
141+
if: steps.pick.outputs.any == 'true'
79142
uses: actions/upload-artifact@v4
80143
with:
81144
name: plugin-packages
82145
path: NuGet/*.nupkg
83146
if-no-files-found: error
84147

85148
- name: Attest build provenance
149+
if: steps.pick.outputs.any == 'true'
86150
uses: actions/attest-build-provenance@v2
87151
with:
88152
subject-path: NuGet/*.nupkg
89153

90154
- name: Check NuGet config
91155
id: nugetcfg
156+
if: steps.pick.outputs.any == 'true'
92157
shell: pwsh
93158
env:
94159
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
@@ -98,20 +163,25 @@ jobs:
98163
Write-Host "Push to nuget.org enabled: $enabled"
99164
100165
- name: Push to nuget.org
101-
if: steps.nugetcfg.outputs.enabled == 'true'
166+
if: steps.pick.outputs.any == 'true' && steps.nugetcfg.outputs.enabled == 'true'
102167
shell: pwsh
103168
env:
104169
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
105170
run: |
106-
# --skip-duplicate keeps re-runs idempotent: a plugin whose
107-
# csproj version hasn't been bumped since its last publish
108-
# is a no-op rather than a failure.
171+
# --skip-duplicate keeps the push idempotent: a code change
172+
# that didn't bump the csproj's <Version> is a no-op rather
173+
# than a workflow failure.
109174
$packages = Get-ChildItem NuGet\*.nupkg | Where-Object { $_.Name -notlike '*.symbols.nupkg' }
110175
foreach ($pkg in $packages) {
111-
Write-Host "Pushing $($pkg.Name)"
176+
Write-Host "Pushing $($pkg.Name)..."
112177
dotnet nuget push $pkg.FullName `
113178
--api-key $env:NUGET_API_KEY `
114179
--source https://api.nuget.org/v3/index.json `
115180
--skip-duplicate
116181
if ($LASTEXITCODE -ne 0) { throw "nuget push failed for $($pkg.Name)" }
117182
}
183+
184+
- name: Nothing to publish
185+
if: steps.pick.outputs.any != 'true'
186+
shell: pwsh
187+
run: Write-Host "No plugin folder changed in this push and no manual selection — skipping." -ForegroundColor Yellow

CONTRIBUTING.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ To publish a plugin:
106106

107107
1. Bump `<Version>` in the relevant csproj (`src/AzureTray.Plugin.Contracts/AzureTray.Plugin.Contracts.csproj`, etc.).
108108
2. Commit and push to `main`.
109-
3. Open the GitHub Actions UI → **Publish plugins** workflow → **Run workflow** → choose which package (`Contracts`, `PIM`, `LAPS`, or `all`).
110-
4. The workflow ([`publish-plugins.yml`](.github/workflows/publish-plugins.yml)) packs the selected csproj(s), attests build provenance, and pushes to nuget.org (when `NUGET_API_KEY` is set). `--skip-duplicate` makes pushes idempotent — selecting "all" without bumping any csproj is safe and produces no new versions.
109+
110+
The [`publish-plugins.yml`](.github/workflows/publish-plugins.yml) workflow fires automatically: it uses path filters to detect which plugin folder changed in your push, packs only that csproj, attests build provenance, and pushes to nuget.org (when `NUGET_API_KEY` is set). `--skip-duplicate` makes pushes idempotent — if you forget to bump `<Version>`, the push is a no-op rather than a failure, but **your code doesn't reach nuget.org until you bump**. That's the enforcement.
111+
112+
You can also trigger the workflow manually from the GitHub Actions UI → **Publish plugins****Run workflow** → pick `Contracts`, `PIM`, `LAPS`, or `all`. Useful for retrying after a transient failure or republishing after a NuGet-side outage.
111113

112114
For test-publishing prereleases from a developer machine, see [scripts/publish-plugins-prerelease.ps1](scripts/publish-plugins-prerelease.ps1) — it stamps a `-preview.YYYYMMDDHHMM` suffix on top of each csproj's declared `<Version>`.
113115

0 commit comments

Comments
 (0)