Repository navigation
337 lines (317 loc) · 15.3 KB
/
Copy pathrelease.yml
File metadata and controls
337 lines (317 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
name: Build and Release CloudClient
# The release is AUTOMATIC: pushing master with IsPrerelease=false in Cloud/Cloud.csproj
# (in the pushed commit) triggers the wait for the dependency NuGet packages, the build of the
# platform archives, the Windows MSI installer and the GitHub release — the tag v1.yy.MM.dd is
# created on the fly. No tag push is needed. With IsPrerelease=true, or when today's tag already
# exists, the run is skipped. CloudClient has no Microsoft Store channel (unlike AgentBridge):
# the app needs administrator privileges (clock sync, hard-link versioning, VHDX mount, service
# registration), which the Store's AppContainer sandbox forbids. Instead the Windows installer is
# a per-machine MSI published on GitHub Releases, shipping the AGPL-3.0 license and an honest
# admin/privacy notice (tools/installer/NOTICE.txt).
on:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: write
# Serializes runs of this workflow per branch so a quick double push never races the release
# run (AgentBridge 2026-08-26: runs created in inverted order, the release run failed while
# queued). The gate-restore commit carries [skip ci] so it creates no run at all.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
# Release gate: when IsPrerelease=true in Cloud/Cloud.csproj the version carries a
# "-prerelease" suffix and no GitHub release is created. See docs-dev/RELEASE-PIPELINE.md.
check-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
do_release: ${{ steps.gate.outputs.do_release }}
steps:
- uses: actions/checkout@v4
with:
path: CloudClient
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Detect version + release gate
id: ver
shell: bash
run: |
VER=$(dotnet msbuild CloudClient/Cloud/Cloud.csproj -getProperty:Version -nologo | grep -oE '[0-9]+(\.[0-9]+)*(-[a-z0-9]+)?' | head -n1)
PREREL=$(dotnet msbuild CloudClient/Cloud/Cloud.csproj -getProperty:IsPrerelease -nologo | grep -oE 'true|false' | head -n1)
echo "version=$VER"
echo "prerelease=${PREREL:-false}" >> "$GITHUB_OUTPUT"
echo "version=$VER" >> "$GITHUB_OUTPUT"
- name: Decide whether to release
id: gate
working-directory: CloudClient
shell: bash
env:
VERSION: ${{ steps.ver.outputs.version }}
PRERELEASE: ${{ steps.ver.outputs.prerelease }}
run: |
if [ "$PRERELEASE" = "true" ]; then
echo "IsPrerelease=true -> prerelease build, no GitHub release"
echo "do_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then
echo "Tag v$VERSION already exists -> no second release for this version"
echo "do_release=false" >> "$GITHUB_OUTPUT"
else
echo "do_release=true" >> "$GITHUB_OUTPUT"
fi
build:
needs: check-version
if: needs.check-version.outputs.do_release == 'true'
name: Build ${{ matrix.rid }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
rid: win-x64
- os: ubuntu-latest
rid: linux-x64
- os: ubuntu-latest
rid: linux-arm64
- os: macos-latest
rid: osx-x64
- os: macos-latest
rid: osx-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
path: CloudClient
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
# The dependency NuGet packages are date-versioned (1.yy.MM.dd) and published by each
# repo's publish.yml when a v* tag is pushed. CloudClient restores them with 1.*
# floating, so the release must NOT silently restore a package older than the release
# date when its repo changed today. This step only waits for the packages whose GitHub
# repo carries today's tag (v1.26.09.09): a repo tagged today publishes today's
# version and nuget.org propagation is not instant (up to 30 min). Repos without
# today's tag did not change — their latest published version is exactly what the
# release should restore, so they never enter the wait.
- name: Wait for changed dependency packages on NuGet
shell: bash
env:
VERSION: ${{ needs.check-version.outputs.version }}
run: |
RAW="$VERSION"
NVER=$(printf '%s' "$VERSION" | awk -F. '{for(i=1;i<=NF;i++){gsub(/^0+/,"",$i); if($i=="")$i=0; printf "%s%s",(i>1?".":""),$i} print ""}')
# package -> repo that publishes it (same Graphene release convention).
# Plain text lines (no bash associative arrays: macOS runners ship bash 3.2,
# which does not support declare -A).
MAP="cloudbox:CloudLibraries cloudsynclibrary:CloudLibraries encryptedmessaging:EncryptedMessaging communicationchannel:EncryptedMessaging securestorage:SecureStorage fullduplexstreamsupport:FullDuplexStreamSupport bytesextension:BytesExtension githubappsync:GitHubAppSync antigitlibrary:AntiGithub backuplibrary:AntiGithub dataredundancy:AntiGithub systemextra:SystemExtra uisupportblazor:UISupportBlazor"
WAIT=""
for entry in $MAP; do
pkg="${entry%%:*}"
repo="${entry##*:}"
# A today-tag on the repo means it publishes today's version and the wait applies.
# gh api (authenticated via GITHUB_TOKEN) — anonymous curl would hit the 60/h
# rate limit because this step runs once per matrix job (5×13 requests).
if gh api "repos/Graphene-Lab/$repo/git/refs/tags/v$RAW" >/dev/null 2>&1; then
WAIT="$WAIT $pkg"
fi
done
if [ -z "$WAIT" ]; then
echo "no dependency repo carries today's tag v$RAW — nothing changed, continuing with the latest published packages"
exit 0
fi
echo "Waiting for today's packages at version $NVER:$WAIT"
for i in $(seq 1 60); do
missing=""
for pkg in $WAIT; do
if curl -fsS --max-time 20 "https://api.nuget.org/v3-flatcontainer/$pkg/index.json" | grep -q "\"$NVER\""; then
echo "$pkg $NVER available (attempt $i)"
else
missing="$missing $pkg"
fi
done
if [ -z "$missing" ]; then echo "all changed dependency packages available"; exit 0; fi
sleep 30
done
echo "::error::after 30 min these packages are still not at $NVER:$missing"
echo "::error::their repos carry today's tag v$RAW, so they SHOULD be published — check the failed 'Publish NuGet Package' run of the repo(s) above, fix it, then re-run. Proceeding would restore yesterday's package (misleading build crash or silent stale release)."
exit 1
# Builds against the published NuGet packages (CloudBox 1.* closure, GitHubAppSync,
# AntiGitLibrary, SystemExtra, UISupportBlazor) — the sibling repos are never checked
# out here. Self-contained single-file per RID, like AgentBridge: the archives run on a
# machine without a system .NET runtime (the PWA/web client and install scripts ship
# next to the executable).
- name: Publish
shell: bash
run: |
dotnet publish CloudClient/Cloud/Cloud.csproj -c Release -r ${{ matrix.rid }} \
--self-contained true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:DebugType=None -p:DebugSymbols=false \
-o ./publish
- name: Verify folder icons in publish payload
shell: pwsh
run: ./CloudClient/tools/Verify-CloudClientIcons.ps1 -PayloadDir ./publish
- name: Package
shell: bash
run: |
cd publish
tar -czf ../cloudclient-${{ matrix.rid }}.tar.gz .
cd ..
ls -lh cloudclient-${{ matrix.rid }}.tar.gz
- name: Upload archive
uses: actions/upload-artifact@v4
with:
name: cloudclient-${{ matrix.rid }}
path: cloudclient-${{ matrix.rid }}.tar.gz
# Windows MSI installer (per-machine). Built from the win-x64 self-contained publish and
# harvested into a WiX v5 MSI by tools/installer/New-CloudClientInstaller.ps1. The app
# requires administrator privileges (see tools/installer/NOTICE.txt), so this is NOT a
# Store package — it is published on GitHub Releases. Unsigned unless SIGN_* secrets are
# configured; the AGPL-3.0 project is eligible for the free SignPath open-source signing
# program. It runs after the build jobs so the dependency NuGet packages are already
# published/available (the build jobs performed the wait), avoiding a duplicated wait step.
store-msi:
needs: [check-version, build]
if: needs.check-version.outputs.do_release == 'true'
name: Build Windows MSI installer
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
path: CloudClient
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore WiX tool
working-directory: CloudClient/tools/installer
run: dotnet tool restore
- name: Publish win-x64 payload
run: |
dotnet publish CloudClient/Cloud/Cloud.csproj -c Release -r win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:DebugType=None -p:DebugSymbols=false `
-o ./msi-payload
- name: Verify folder icons in MSI payload
shell: pwsh
run: ./CloudClient/tools/Verify-CloudClientIcons.ps1 -PayloadDir ./msi-payload
- name: Build MSI
run: |
pwsh -File CloudClient/tools/installer/New-CloudClientInstaller.ps1 `
-PayloadDir ./msi-payload `
-Version '${{ needs.check-version.outputs.version }}' `
-OutDir ./msi-out `
-UseUi 0
env:
SIGN_PFX: ${{ secrets.SIGN_PFX }}
SIGN_PFX_PASSWORD: ${{ secrets.SIGN_PFX_PASSWORD }}
SIGN_THUMBPRINT: ${{ secrets.SIGN_THUMBPRINT }}
SIGN_TIMESTAMP_URL: ${{ secrets.SIGN_TIMESTAMP_URL }}
- name: Upload MSI
uses: actions/upload-artifact@v4
with:
name: cloudclient-msi
path: msi-out/*.msi
# Portable update package: the framework-dependent build (Cloud.dll sits next to the
# executable) that the website ships and that self-updates from the "portable" channel.
# Built as a Windows framework-dependent publish (-r win-x64 --self-contained false) so the
# archive carries the Windows apphost Cloud.exe that install.bat launches. A plain
# framework-dependent publish with no RID would bake the apphost for the BUILD host (Linux),
# shipping a Linux ELF launcher named "Cloud" and NO Cloud.exe — the Windows download then
# fails with "Cloud.exe not found" (issue #9). The win-x64 apphost template is restored from
# NuGet, so the Windows launcher is produced correctly even on the Linux runner; the managed
# Cloud.dll and static assets are platform-agnostic and Linux/macOS still run them with
# "dotnet Cloud.dll" (see install.sh). Then zipped and manifested by GitHubAppSync's helper.
# The helper is taken from the GitHubAppSync repo (checked out here) so the asset format
# stays identical to what the client parser reads — one source of truth.
# NOTE: this publish restores GitHubAppSync from NuGet (the sibling is not on the runner),
# so GitHubAppSync must already be published (v* tag) before a CloudClient release runs.
portable:
needs: check-version
if: needs.check-version.outputs.do_release == 'true'
name: Build portable update package
runs-on: ubuntu-latest
steps:
- name: Checkout CloudClient
uses: actions/checkout@v4
with:
path: CloudClient
- name: Checkout GitHubAppSync (asset helper)
uses: actions/checkout@v4
with:
repository: Graphene-Lab/GitHubAppSync
path: GitHubAppSync
ref: master
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Publish framework-dependent (portable, Windows apphost)
working-directory: CloudClient
run: dotnet publish Cloud/Cloud.csproj -c Release -r win-x64 --self-contained false -o ./portable
- name: Verify Cloud.exe is present in the portable payload
working-directory: CloudClient
shell: bash
run: |
# Guard against re-introducing the issue #9 regression: the Windows download must
# carry the Cloud.exe that install.bat launches. Fail the build if it is missing.
if [ ! -f ./portable/Cloud.exe ]; then
echo "::error::portable payload is missing Cloud.exe (Windows launcher). The portable publish must target win-x64 so install.bat can launch Cloud.exe."
exit 1
fi
if [ ! -f ./portable/Cloud.dll ]; then
echo "::error::portable payload is missing Cloud.dll (framework-dependent core)."
exit 1
fi
echo "portable payload OK: Cloud.exe + Cloud.dll present"
- name: Verify folder icons in portable payload
shell: pwsh
run: ./CloudClient/tools/Verify-CloudClientIcons.ps1 -PayloadDir ./CloudClient/portable
- name: Build update assets (portable.zip + portable-manifest.json)
run: pwsh -File GitHubAppSync/tools/New-UpdateAsset.ps1 -PublishDir CloudClient/portable -Channel portable -Version '${{ needs.check-version.outputs.version }}' -OutDir ./portable-assets
- name: Upload portable assets
uses: actions/upload-artifact@v4
with:
name: portable-update
path: portable-assets/*
release:
needs: [check-version, build, store-msi, portable]
if: needs.check-version.outputs.do_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download archives
uses: actions/download-artifact@v4
with:
path: artifacts
# Pin the tag to the commit that triggered this run (github.sha = the gate-off commit) so
# a later master push (e.g. the IsPrerelease restore after a button release) cannot move
# it. The GITHUB_TOKEN push does not re-trigger workflows. The tag is informational:
# nothing listens on it (auto-update reads the GitHub release, see docs).
- name: Tag the triggering commit
shell: bash
run: |
git tag "v${{ needs.check-version.outputs.version }}" "$GITHUB_SHA"
git push origin "v${{ needs.check-version.outputs.version }}"
- name: Create GitHub Release v${{ needs.check-version.outputs.version }}
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.check-version.outputs.version }}
files: |
artifacts/**/*.tar.gz
artifacts/**/*.msi
artifacts/portable-update/*
generate_release_notes: true