Repository navigation
522 lines (463 loc) · 24 KB
/
Copy pathbuild.yml
File metadata and controls
522 lines (463 loc) · 24 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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
name: Build TablePro
on:
workflow_dispatch:
# No paths filter here. GitHub does not evaluate one for a tag push, and a tag push is the
# only thing that triggers this workflow, so the paths-ignore list that used to sit here
# never excluded anything.
push:
tags: ["v*"]
# A release must never be cancelled part-way through: the build jobs hold notarization
# submissions open with Apple, and the release job pushes a commit and publishes artifacts.
# Re-running the same tag queues behind the run already in flight rather than racing it.
concurrency:
group: build-tablepro-${{ github.ref }}
cancel-in-progress: false
# The release job raises this to write for itself. Everything else here builds and notarizes and
# needs nothing beyond the checkout.
permissions:
contents: read
jobs:
# Runs no suite. A release commit changes only release files, so its own push skips both suites.
# For each suite the verdict is the newest first-parent commit on main where its job ran, waited
# for when still running, and it holds only if nothing the suite reads changed since.
verdict:
name: Tested on main
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 240
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Find the passing runs that cover this commit
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
# The walk below follows the tag's own parents, so a tag off main would borrow main's verdict.
if ! git merge-base --is-ancestor HEAD origin/main; then
echo "::error::$GITHUB_SHA is not on main"
exit 1
fi
covered() {
local workflow="$1" job="$2" suite="$3" sha run conclusion changed
for sha in $(git rev-list --first-parent --max-count=50 HEAD); do
run=$(gh run list --workflow "$workflow" --commit "$sha" --event push --limit 1 \
--json databaseId --jq '.[0].databaseId // empty') || return 1
[ -n "$run" ] || continue
gh run watch "$run" --interval 60 > /dev/null || return 1
conclusion=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run/jobs" \
--jq ".jobs[] | select(.name == \"$job\") | .conclusion") || return 1
case "$conclusion" in
skipped) continue ;;
success) ;;
*) echo "::error::$job ended '$conclusion' on $sha (run $run)"; return 1 ;;
esac
changed=$(scripts/ci/detect-changed-paths.sh --base "$sha" "$suite") || return 1
if [ "$changed" != false ]; then
echo "::error::$job passed on $sha, but files it reads changed before $GITHUB_SHA"
return 1
fi
echo "$job: run $run on $sha" >> "$GITHUB_STEP_SUMMARY"
return 0
done
echo "::error::no $job run among the last 50 commits on main"
return 1
}
status=0
covered macos-tests.yml "macOS Tests" macos || status=1
covered ios-tests.yml "iOS and Package Tests" ios || status=1
exit "$status"
build:
name: Build ${{ matrix.arch }}
runs-on: xcode-27
timeout-minutes: ${{ matrix.timeout }}
strategy:
# No fail-fast. A failing arch would otherwise cancel its sibling mid-notarization, which is
# the same thing the concurrency block above exists to prevent.
fail-fast: false
matrix:
include:
# One budget for both arms. The arms run on separate runners at the same time, so a
# tighter budget on one buys nothing and only decides which arm dies first. v0.73.0 is
# what that cost: ARM64 reached "Build complete" at 30:01 and was cancelled 13 seconds
# into notarization, while x86_64 finished the whole job in 31:15. The two arms are not
# reliably ordered (over v0.62 to v0.67 x86_64 was the slower one, once by 22 minutes),
# and the notary queue is not something a build budget can predict, so the budget has to
# clear the slowest arm plus a notary wait rather than the arm's own past average.
- arch: arm64
timeout: 45
- arch: x86_64
timeout: 45
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Select Xcode
uses: ./.github/actions/setup-xcode
- name: Download static libraries
env:
GH_TOKEN: ${{ github.token }}
run: scripts/download-libs.sh --force
# build-release.sh clones packages into ~/.spm-cache via -clonedSourcePackagesDirPath,
# so this restores the checkouts instead of re-cloning 30 repositories on every release.
# Package.resolved pins every revision and is tracked, which is why it can key the cache.
# macos-tests.yml saves it from main; a save here would land in this tag's scope, which no
# other tag can read.
- name: Restore Swift package checkouts
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.spm-cache
key: spm-${{ runner.os }}-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-${{ runner.os }}-
# create-dmg is the only Homebrew formula either arm needs. The MySQL plugin links
# Libs/libmariadb.a, which download-libs.sh vendors and prepare-libs.sh selects per
# architecture, and its headers live in Plugins/MySQLDriverPlugin/CMariaDB/include.
# LIBRARY_SEARCH_PATHS names $(SRCROOT)/Libs and no Homebrew prefix, so nothing in the
# build ever read mariadb-connector-c. macos-tests.yml proves it: its build job links all
# 31 plugin bundles with no Homebrew mariadb installed at all.
#
# The x86_64 arm is a cross-compile on an arm64 runner: every tool that runs during the
# build is native and the x86_64 slices of the static libraries are vendored in Libs.
# Bootstrapping a second Homebrew prefix cost five minutes a release to install a
# mariadb-connector-c the build never opened. create-dmg is a shell script, so the native
# prefix serves both arms.
- name: Install create-dmg
run: brew list create-dmg &>/dev/null || brew install create-dmg
- name: Prepare libraries
run: scripts/ci/prepare-libs.sh ${{ matrix.arch }}
- name: Create Secrets.xcconfig
env:
ANALYTICS_HMAC_SECRET: ${{ secrets.ANALYTICS_HMAC_SECRET }}
run: echo "ANALYTICS_HMAC_SECRET = ${ANALYTICS_HMAC_SECRET}" > Configs/Secrets.xcconfig
- name: Setup XcodeGen
uses: ./.github/actions/setup-xcodegen
- name: Set up Apple signing
uses: ./.github/actions/apple-signing
with:
certificate-p12: ${{ secrets.CERTIFICATES_P12 }}
certificate-password: ${{ secrets.CERTIFICATES_PASSWORD }}
apple-id: ${{ secrets.APPLE_ID }}
team-id: ${{ secrets.APPLE_TEAM_ID }}
notary-password: ${{ secrets.NOTARY_PASSWORD }}
provisioning-profile: ${{ secrets.PROVISIONING_PROFILE }}
- name: Build ${{ matrix.arch }}
env:
ANALYTICS_HMAC_SECRET: ${{ secrets.ANALYTICS_HMAC_SECRET }}
NOTARIZE: "true"
run: scripts/build-release.sh ${{ matrix.arch }}
- name: Verify build
run: scripts/ci/verify-build.sh ${{ matrix.arch }}
- name: Package artifacts
env:
NOTARIZE: "true"
run: scripts/ci/package-artifacts.sh ${{ matrix.arch }}
- name: Verify the update archive
run: scripts/ci/verify-update-archive.sh "build/Release/TablePro-${{ matrix.arch }}.tar.xz"
- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: artifacts-${{ matrix.arch }}
path: |
build/Release/TablePro-*.dmg
build/Release/TablePro-*.tar.xz
# Reads two integers out of a Swift file and fetches a JSON manifest, so it wants nothing
# from macOS and does not need to spend a macOS runner minute (billed at ten times Linux).
registry-readiness:
name: Registry Readiness
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Verify registry has compatible plugin binaries
run: |
MANAGER="TablePro/Core/Plugins/PluginManager.swift"
CURRENT=$(grep -E 'static let currentPluginKitVersion = ' "$MANAGER" | grep -oE '[0-9]+' | head -1)
FLOOR=$(grep -E 'static let minimumCompatiblePluginKitVersion = ' "$MANAGER" | grep -oE '[0-9]+' | head -1)
echo "PluginKit floor=$FLOOR current=$CURRENT"
python3 scripts/check-registry-readiness.py --floor "$FLOOR" --current "$CURRENT"
release:
name: Create GitHub Release
runs-on: xcode-27
needs: [verdict, build, registry-readiness]
if: startsWith(github.ref, 'refs/tags/v')
# Holds APPCAST_DEPLOY_KEY, released only to v* tags.
environment: release
# Ten deltas at about a minute of CPU each, built two at a time.
timeout-minutes: 30
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Select Xcode
uses: ./.github/actions/setup-xcode
# Only the two build artifacts. Unfiltered, this also pulled the four test-result bundles and
# test-products, which is 874MB fetched to use 106MB of it, and every megabyte is another
# chance for the blob store to drop the transfer: v0.69.0 failed here twice in a row with
# "Artifact download failed after 5 retries", both times inside two minutes. Flatten takes any
# *.dmg or *.tar.xz it finds, so one inside a test bundle could also reach the release.
- name: Download build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: artifacts-*
path: artifacts-raw/
merge-multiple: true
- name: Flatten artifacts
run: |
mkdir -p artifacts/
find artifacts-raw/ -type f \( -name "*.dmg" -o -name "*.tar.xz" \) -exec mv {} artifacts/ \;
rm -rf artifacts-raw/
echo "Artifacts:"
ls -lh artifacts/
- name: Verify the tag matches the declared version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
XCCONFIG_VERSION=$(sed -n 's/^MARKETING_VERSION[[:space:]]*=[[:space:]]*//p' Configs/Version.xcconfig | tr -d ' ')
if [ "$TAG_VERSION" != "$XCCONFIG_VERSION" ]; then
echo "::error::tag v$TAG_VERSION does not match MARKETING_VERSION $XCCONFIG_VERSION"
exit 1
fi
echo "tag and MARKETING_VERSION agree on $TAG_VERSION"
# CURRENT_PROJECT_VERSION is what Sparkle compares, as <sparkle:version>. A build number
# that did not increase means no installed app is offered the release, and until now only
# a sentence in the release runbook stopped that. Compared against the newest PUBLISHED
# release rather than the newest tag, because re-cutting a release that failed leaves a
# tag at the same build and that is a legitimate state to be in.
BUILD=$(sed -n 's/^CURRENT_PROJECT_VERSION[[:space:]]*=[[:space:]]*//p' Configs/Version.xcconfig | tr -d ' ')
case "$BUILD" in
''|*[!0-9]*)
echo "::error::CURRENT_PROJECT_VERSION '$BUILD' is not a plain integer"
exit 1
;;
esac
# No `|| true`. A gh outage used to turn this gate into "no earlier published release"
# and exit 0, which is the same output a genuine first release produces.
PREVIOUS_TAG=$(gh release list --limit 100 --json tagName,isPrerelease,publishedAt \
-q '[.[] | select(.tagName|startswith("v")) | select(.isPrerelease==false)]
| sort_by(.publishedAt) | last | .tagName')
if [ "$PREVIOUS_TAG" = "null" ]; then
PREVIOUS_TAG=""
fi
if [ -z "$PREVIOUS_TAG" ] || [ "$PREVIOUS_TAG" = "v$TAG_VERSION" ]; then
echo "no earlier published release to compare the build number against"
exit 0
fi
PREVIOUS_BUILD=$(git show "${PREVIOUS_TAG}:Configs/Version.xcconfig" \
| sed -n 's/^CURRENT_PROJECT_VERSION[[:space:]]*=[[:space:]]*//p' | tr -d ' ')
if [ -n "$PREVIOUS_BUILD" ] && [ "$BUILD" -le "$PREVIOUS_BUILD" ]; then
echo "::error::CURRENT_PROJECT_VERSION $BUILD is not above $PREVIOUS_BUILD from $PREVIOUS_TAG; no install would be offered this update"
exit 1
fi
echo "build $BUILD is above $PREVIOUS_BUILD from $PREVIOUS_TAG"
- name: Verify and organize artifacts for release
run: |
VERSION=${GITHUB_REF#refs/tags/v}
if [ -z "$VERSION" ]; then
echo "❌ ERROR: Failed to extract version from ref: $GITHUB_REF"
exit 1
fi
echo "Preparing artifacts for version: $VERSION"
echo "Contents of artifacts directory:"
ls -la artifacts/
# The DMGs are already versioned by package-artifacts.sh; the update archives are not.
# Both renames used to be guarded by `if [ -f ... ]` with no else, so a missing or
# differently named archive printed "organized successfully" and the release published
# whatever the upload glob happened to match.
for ARCH in arm64 x86_64; do
if [ ! -f "artifacts/TablePro-${ARCH}.tar.xz" ]; then
echo "::error::artifacts/TablePro-${ARCH}.tar.xz is missing; the ${ARCH} build did not produce an update archive"
ls -la artifacts/ >&2
exit 1
fi
mv "artifacts/TablePro-${ARCH}.tar.xz" "artifacts/TablePro-${VERSION}-${ARCH}.tar.xz"
if [ ! -f "artifacts/TablePro-${VERSION}-${ARCH}.dmg" ]; then
echo "::error::artifacts/TablePro-${VERSION}-${ARCH}.dmg is missing"
ls -la artifacts/ >&2
exit 1
fi
done
echo "✅ Artifacts organized successfully"
echo "Final artifacts:"
ls -lh artifacts/
# No `if: env.SPARKLE_PRIVATE_KEY != ''` on these steps. This job only runs for a v* tag,
# so the key is always meant to be present, and gating on it meant a missing or rotated
# secret published a release that no existing install could ever see. A signing key that
# is not there is now a red job, not a silent skip.
# The tag's own appcast.xml is the feed as of that commit, which is missing every version
# published since. The merge base has to be the feed that is actually live.
- name: Fetch the published feed to merge into
run: |
git fetch origin main
mkdir -p appcast
git show origin/main:appcast.xml > appcast/base.xml
# A flag left true from the previous release would mark an ordinary build critical and
# interrupt every user, so it has to have been touched in this release's own diff.
- name: Read the release flags
id: flags
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CRITICAL=$(python3 -c "
import json
print('1' if json.load(open('.github/release-flags.json')).get('criticalUpdate') is True else '0')
")
echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT"
if [ "$CRITICAL" != "1" ]; then
echo "Ordinary release, staged over 36 hours"
exit 0
fi
RELEASES=$(gh release list --limit 100 --json tagName,isPrerelease,publishedAt \
-q '[.[] | select(.tagName|startswith("v")) | select(.isPrerelease==false)]
| sort_by(.publishedAt) | .[].tagName')
# A re-run after this tag's release went out finds it among the published ones.
PREVIOUS_TAG=$(printf '%s\n' "$RELEASES" | grep -vxF "${GITHUB_REF#refs/tags/}" | tail -1)
if [ -n "$PREVIOUS_TAG" ] && [ "$PREVIOUS_TAG" != "null" ] \
&& git diff --quiet "$PREVIOUS_TAG" HEAD -- .github/release-flags.json; then
echo "::error::criticalUpdate is true but .github/release-flags.json has not changed since $PREVIOUS_TAG, which is what a flag left over from the previous release looks like"
exit 1
fi
echo "::warning::This release is marked critical and will interrupt every user"
# A step of its own, so the token that reads releases never shares an environment with the
# signing key.
- name: Fetch earlier update archives for deltas
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: scripts/ci/fetch-delta-bases.sh "${GITHUB_REF#refs/tags/}" 5 delta-bases
- name: Sign update archives with Sparkle
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
BASE_APPCAST: appcast/base.xml
CRITICAL_UPDATE: ${{ steps.flags.outputs.critical }}
run: scripts/ci/sign-and-appcast.sh "${GITHUB_REF#refs/tags/v}"
- name: Upload appcast artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: appcast-${{ github.sha }}
path: appcast/appcast.xml
retention-days: 90
# Fails before anything is published when the secret does not match a deploy key.
- name: Check the release deploy key
env:
APPCAST_DEPLOY_KEY: ${{ secrets.APPCAST_DEPLOY_KEY }}
run: |
eval "$(ssh-agent -s)" > /dev/null
trap 'ssh-agent -k > /dev/null' EXIT
ssh-add - <<< "$APPCAST_DEPLOY_KEY"
git ls-remote "git@github.com:${GITHUB_REPOSITORY}.git" HEAD > /dev/null
# A published release is immutable, so every asset goes on the draft first.
- name: Create the draft release
id: draft
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PRERELEASE: ${{ contains(github.ref, '-beta') || contains(github.ref, '-alpha') || contains(github.ref, '-rc') }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
# A first release has nothing to build deltas from.
shopt -s nullglob
scripts/ci/draft-release.sh --prerelease "$PRERELEASE" "$TAG" "$TAG" release_notes.md \
artifacts/*.dmg artifacts/*.tar.xz artifacts/*.delta
- name: Publish the release
if: steps.draft.outputs.id != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft.outputs.id }}
run: |
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" -F draft=false \
--jq '"Published \(.tag_name): \(.html_url)"'
# Publishing the feed comes after publishing the artifacts it points at, so there is no
# window where Sparkle advertises a download URL that still 404s. A draft's URLs 404 too.
#
# This step used to be continue-on-error, which made a failed push indistinguishable from
# a release that went out clean: every user's updater would keep reporting "up to date"
# for a version that shipped. A rejected push now rebases and retries, and a push that
# still will not land fails the job.
# Pushed with the release deploy key, the ruleset's only bypass actor: a fresh commit on main
# has no checks of its own yet.
- name: Commit appcast.xml to repo
env:
APPCAST_DEPLOY_KEY: ${{ secrets.APPCAST_DEPLOY_KEY }}
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
if [ ! -f appcast/items/arm64.xml ] || [ ! -f appcast/items/x86_64.xml ]; then
echo "❌ ERROR: the signed per-architecture items are missing, the Sparkle step produced no feed" >&2
exit 1
fi
# The merge below runs after the tree has been reset to main, so the scripts are taken
# from the tag first. Otherwise a release would publish its feed with whatever
# scripts/ci main happened to hold, rather than the code the tag was tested with.
TOOLS=$(mktemp -d)
cp -R scripts/ci "$TOOLS/"
eval "$(ssh-agent -s)" > /dev/null
trap 'ssh-agent -k > /dev/null' EXIT
ssh-add - <<< "$APPCAST_DEPLOY_KEY"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -B main
# The whole attempt is inside the retry, because the merge is what has to be redone when
# main moves. It used to run once against a snapshot taken before the build and then be
# copied over main, which reverted anything pushed in between: a pull-release.py
# withdrawal landing inside that window was undone and the withdrawn build went back on
# offer. The rewind guard could not see it, because a restoration adds versions rather
# than dropping them. Rebasing was no answer either, since the merge rewrites the file
# wholesale and every retry conflicted.
for attempt in 1 2 3; do
git fetch origin main
git reset --hard origin/main
python3 "$TOOLS/ci/merge-appcast.py" \
--base appcast.xml \
--version "$VERSION" \
--arm64 appcast/items/arm64.xml \
--x86-64 appcast/items/x86_64.xml \
--download-prefix "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/v${VERSION}/" \
--out appcast.xml
# The feed is pruned to the newest releases, so an old version leaving it is the policy
# working. What must never leave is the newest item, the only one any install can be
# offered, or the release being published.
# An empty newest_before is a feed with no items yet, which is a legitimate first
# release rather than something to refuse.
newest_before=$(git show origin/main:appcast.xml \
| grep -o '<sparkle:shortVersionString>[^<]*' | sed 's/.*>//' | head -1)
for required in "$newest_before" "$VERSION"; do
if [ -z "$required" ]; then
continue
fi
if ! grep -q "<sparkle:shortVersionString>${required}</sparkle:shortVersionString>" appcast.xml; then
echo "❌ ERROR: the generated feed no longer advertises $required" >&2
exit 1
fi
done
first=$(grep -o '<sparkle:shortVersionString>[^<]*' appcast.xml | sed 's/.*>//' | head -1)
if [ "$first" != "$VERSION" ]; then
echo "❌ ERROR: the feed leads with $first, not the release being published ($VERSION)" >&2
exit 1
fi
git add appcast.xml
if git diff --cached --quiet; then
echo "appcast.xml already advertises v${VERSION}"
exit 0
fi
git commit -m "Update appcast.xml for v${VERSION}"
if git push "git@github.com:${GITHUB_REPOSITORY}.git" HEAD:main; then
exit 0
fi
echo "Push rejected, re-merging onto the current origin/main (attempt ${attempt})"
done
echo "❌ ERROR: could not push appcast.xml after 3 attempts" >&2
exit 1
- name: Notify Telegram
if: success() && env.TELEGRAM_BOT_TOKEN != ''
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
TELEGRAM_TOPIC_ID: ${{ secrets.TELEGRAM_TOPIC_ID }}
run: scripts/ci/notify-telegram.sh "${GITHUB_REF#refs/tags/v}"