-
-
Notifications
You must be signed in to change notification settings - Fork 388
348 lines (297 loc) · 14.5 KB
/
Copy pathbuild.yml
File metadata and controls
348 lines (297 loc) · 14.5 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
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:
lint:
name: SwiftLint
runs-on: macos-26
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install SwiftLint
run: brew list swiftlint &>/dev/null || brew install swiftlint
- name: Run SwiftLint
run: swiftlint lint --strict
# No `secrets: inherit`. That handed the suite CERTIFICATES_P12, CERTIFICATES_PASSWORD,
# NOTARY_PASSWORD, PROVISIONING_PROFILE, SPARKLE_PRIVATE_KEY, TELEGRAM_BOT_TOKEN and
# REGISTRY_DEPLOY_KEY, none of which it reads.
test:
name: macOS Tests
uses: ./.github/workflows/macos-tests.yml
build:
name: Build ${{ matrix.arch }}
runs-on: macos-26
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:
# Separate timeouts because the two arms genuinely differ: measured over v0.62 to v0.67,
# x86_64 was never the faster of the two and once ran 22 minutes longer than an ARM64
# sibling that started in the same second, so this is the architecture rather than the
# notary queue.
- arch: arm64
timeout: 30
- 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.
- name: Cache Swift package checkouts
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.spm-cache
key: ${{ runner.os }}-spm-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: ${{ runner.os }}-spm-
# 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: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: artifacts-${{ matrix.arch }}
path: |
build/Release/TablePro-*.dmg
build/Release/TablePro-*.zip
# 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: macos-26
needs: [lint, test, build, registry-readiness]
if: startsWith(github.ref, 'refs/tags/v')
timeout-minutes: 10
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 *.zip it finds, so a zip 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 "*.zip" \) -exec mv {} artifacts/ \;
rm -rf artifacts-raw/
echo "Artifacts:"
ls -lh artifacts/
- name: Verify the tag matches the declared version
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"
- 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 ZIPs are not.
# Both renames used to be guarded by `if [ -f ... ]` with no else, so a missing or
# differently named ZIP 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}.zip" ]; then
echo "::error::artifacts/TablePro-${ARCH}.zip is missing; the ${ARCH} build did not produce an update archive"
ls -la artifacts/ >&2
exit 1
fi
mv "artifacts/TablePro-${ARCH}.zip" "artifacts/TablePro-${VERSION}-${ARCH}.zip"
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. Seeding from it drops every
# version published since, and generate_appcast keeps only what it is handed, so the
# committed feed would silently rewind and those releases would stop being offered.
- name: Seed the appcast from the published feed
run: |
git fetch origin main
mkdir -p appcast
git show origin/main:appcast.xml > appcast/seed.xml
- name: Sign update archives with Sparkle
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
SEED_APPCAST: appcast/seed.xml
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
- name: Extract release notes from CHANGELOG.md
run: scripts/ci/extract-release-notes.sh "${GITHUB_REF#refs/tags/v}"
- name: Create GitHub Release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
files: |
artifacts/*.dmg
artifacts/*.zip
body_path: release_notes.md
draft: false
prerelease: ${{ contains(github.ref, '-beta') || contains(github.ref, '-alpha') || contains(github.ref, '-rc') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 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.
#
# 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.
- name: Commit appcast.xml to repo
run: |
if [ ! -f appcast/appcast.xml ]; then
echo "❌ ERROR: appcast/appcast.xml is missing, the Sparkle step produced no feed" >&2
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin main
git checkout main
# Refuse to publish a feed that drops a version main already advertises. Sparkle serves
# this file to every install, so a rewind stops offering updates that were shipped.
missing=$(comm -23 \
<(git show origin/main:appcast.xml | grep -o '<sparkle:shortVersionString>[^<]*' | sed 's/.*>//' | sort -u) \
<(grep -o '<sparkle:shortVersionString>[^<]*' appcast/appcast.xml | sed 's/.*>//' | sort -u))
if [ -n "$missing" ]; then
echo "❌ ERROR: the generated feed drops versions already published on main: $missing" >&2
exit 1
fi
cp appcast/appcast.xml appcast.xml
git add appcast.xml
if git diff --cached --quiet; then
echo "appcast.xml is already up to date"
exit 0
fi
git commit -m "Update appcast.xml for v${GITHUB_REF#refs/tags/v}"
for attempt in 1 2 3; do
if git push origin main; then
exit 0
fi
echo "Push rejected, rebasing onto origin/main (attempt ${attempt})"
# A conflicting rebase leaves the repo mid-rebase with main still at the pre-rebase
# commit, so the next attempt pushed a stale ref and failed differently every time.
git pull --rebase origin main || git rebase --abort || true
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}"