Repository navigation
599 lines (513 loc) · 23.3 KB
/
Copy pathrelease.yml
File metadata and controls
599 lines (513 loc) · 23.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
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
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing tag to (re)build a release for, e.g. v0.1.0"
required: true
targets:
description: "Which platform to build (the tag decides *what* is built — move it first if it's behind)"
type: choice
required: true
default: all
options:
- all
- macos
- linux
- windows
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
# Owns the release metadata. Runs once, before any build, so the body is
# written exactly once instead of being raced (and then skipped) by the
# build matrix: tauri-action never edits the title/body of a release that
# already exists.
create-release:
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.meta.outputs.tag }}
version: ${{ steps.meta.outputs.version }}
prerelease: ${{ steps.meta.outputs.prerelease }}
release_id: ${{ steps.release.outputs.release_id }}
steps:
# Full history and every ref. The default shallow single-ref clone can
# resolve neither the tag being built nor how far it trails the default
# branch, which is what the report step below needs.
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve tag and version
id: meta
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
VERSION="${TAG#v}"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Tag '$TAG' is not vMAJOR.MINOR.PATCH[-prerelease]"
exit 1
fi
# Anything with a pre-release suffix (v1.0.0-beta.1) ships as a prerelease.
if [[ "$VERSION" == *-* ]]; then PRERELEASE=true; else PRERELEASE=false; fi
{
echo "tag=$TAG"
echo "version=$VERSION"
echo "prerelease=$PRERELEASE"
} >> "$GITHUB_OUTPUT"
# The build jobs check out the *tag*, not the branch this was dispatched
# from. Those are easy to confuse on a manual re-run — you can dispatch a
# workflow that contains a fix and still build tagged code that doesn't —
# so state the commit up front rather than leaving it to be inferred from
# a compiler error's line numbers.
- name: Report the commit that will be built
env:
TAG: ${{ steps.meta.outputs.tag }}
run: |
if ! SHA=$(git rev-parse --verify "refs/tags/$TAG^{commit}" 2>/dev/null); then
echo "::error::Tag $TAG does not exist. Create it first, or fix the input."
exit 1
fi
SUBJECT=$(git log -1 --format=%s "$SHA")
# A tag that trails the default branch is the single most likely reason
# for a build failing on something already fixed. Say so loudly, but
# don't fail: rebuilding a deliberately old tag is legitimate.
DEFAULT_BRANCH="origin/${{ github.event.repository.default_branch }}"
BEHIND=""
if git rev-parse --verify --quiet "$DEFAULT_BRANCH" >/dev/null; then
COUNT=$(git rev-list --count "$SHA..$DEFAULT_BRANCH" 2>/dev/null || echo 0)
if [ "$COUNT" -gt 0 ]; then
BEHIND="$COUNT"
echo "::warning title=Tag is behind $DEFAULT_BRANCH::$TAG points at ${SHA:0:12}, which is $COUNT commit(s) behind $DEFAULT_BRANCH. The builds compile the tag, so anything fixed since then is NOT in this build. Move the tag if that isn't what you want."
fi
fi
{
echo "### Building \`$TAG\`"
echo
echo "| | |"
echo "|---|---|"
echo "| Commit | \`${SHA:0:12}\` |"
echo "| Subject | $SUBJECT |"
echo "| Workflow from | \`${{ github.ref_name }}\` |"
if [ -n "$BEHIND" ]; then
echo "| :warning: Behind \`$DEFAULT_BRANCH\` by | **$BEHIND commit(s)** |"
fi
echo
echo "The builds compile **the tag**, not the branch this was dispatched from."
if [ -n "$BEHIND" ]; then
echo
echo "This tag is behind, so fixes merged since \`${SHA:0:12}\` are **not**"
echo "in this build. To move it:"
echo
echo '```'
echo "git tag -f $TAG $DEFAULT_BRANCH && git push -f origin $TAG"
echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
echo "Building $TAG at $SHA — $SUBJECT"
- name: Render release notes
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
cat > "$RUNNER_TEMP/notes.md" <<'NOTES'
## FigyTerm v__VERSION__
### Updating
FigyTerm updates itself from here on — it checks for new versions and
installs them in-app, with no Gatekeeper prompt and no `xattr` step.
If you're coming from **v0.0.6 or earlier**, this one install is manual;
after it, updates are automatic.
### Installation (macOS)
Two ways to install. Both give you the same app.
#### Option 1 — install script (recommended)
```
curl -fsSL https://raw.githubusercontent.com/code4mk/figyterm/main/install.sh | sh
```
One line, no security dialog, no `xattr` step. It picks the right build for
your Mac and installs to `/Applications`.
Why it's simpler: FigyTerm isn't code-signed with an Apple Developer ID, and
macOS quarantines anything downloaded by a **browser**. `curl` isn't a
browser, so this route avoids the problem rather than working around it.
[Read the script](https://github.com/code4mk/figyterm/blob/main/install.sh)
before running it.
#### Option 2 — download the .dmg manually
1. Download `FigyTerm___VERSION___aarch64.dmg` (Apple Silicon) or
`FigyTerm___VERSION___x64.dmg` (Intel)
2. Open the `.dmg` and drag **FigyTerm** into **Applications**
3. Clear the quarantine flag macOS added during the download:
```
xattr -cr /Applications/FigyTerm.app
```
4. Launch FigyTerm
Step 3 isn't optional — without it macOS reports *"FigyTerm is damaged and
can't be opened"*, which only means the app is unsigned.
### Installation (Linux)
Three formats, with one difference worth knowing.
**AppImage (recommended)** — runs on any distro, and updates itself:
```
chmod +x FigyTerm___VERSION___amd64.AppImage
./FigyTerm___VERSION___amd64.AppImage
```
**Debian / Ubuntu:** `sudo apt install ./FigyTerm___VERSION___amd64.deb`
**Fedora / RHEL:** `sudo dnf install ./FigyTerm-__VERSION__-1.x86_64.rpm`
A `.deb` or `.rpm` install belongs to your package manager, so FigyTerm
won't replace its own files — it tells you a new version exists and leaves
the install to you. The AppImage is the one that updates in place.
If the window comes up blank — some Nvidia and older Mesa drivers do this
to WebKitGTK — start it with:
```
WEBKIT_DISABLE_DMABUF_RENDERER=1 figyterm
```
### Installation (Windows)
Download `FigyTerm___VERSION___x64-setup.exe` and run it. It installs for
the current user, needs no administrator rights, and updates itself from
then on.
Windows will show **"Windows protected your PC"** the first time, because
the installer isn't code-signed. Click **More info**, then **Run anyway**.
There's no way around this yet: unlike macOS there's no flag to clear,
and reputation accrues per binary as downloads accumulate.
`FigyTerm___VERSION___x64_en-US.msi` is also published for deployment via
Group Policy or Intune. It does *not* self-update — a managed install
stays managed by whatever pushed it.
Full details: [Installation guide](https://github.com/code4mk/figyterm/blob/main/docs/INSTALLATION.md)
### Troubleshooting
**"FigyTerm can't be opened because it is from an unidentified developer"**
Open **System Settings > Privacy & Security**, scroll down, and click
**Open Anyway**. On macOS 15 and later this is the only route — Apple removed
the old right-click > Open shortcut.
**"FigyTerm is damaged and can't be opened"**
The quarantine flag is still set. Run:
```
xattr -cr /Applications/FigyTerm.app
```
**Keychain access popup**
macOS may ask to access your keychain. Click **Allow** or **Always Allow**. This is normal for apps that interact with system credentials.
### What's New
See [CHANGELOG](https://github.com/code4mk/figyterm/blob/main/CHANGELOG.md) for details.
NOTES
perl -pi -e 's/__VERSION__/$ENV{VERSION}/g' "$RUNNER_TEMP/notes.md"
- name: Create or update draft release
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.meta.outputs.tag }}
PRERELEASE: ${{ steps.meta.outputs.prerelease }}
run: |
TITLE="FigyTerm $TAG"
# GET /releases/tags/{tag} only returns *published* releases, so it
# 404s on the draft we create here. The list endpoint does include
# drafts, so match on tag_name there instead.
find_release_id() {
gh api --paginate "repos/$GITHUB_REPOSITORY/releases?per_page=100" \
--jq '.[] | select(.tag_name == env.TAG) | .id' | head -n1
}
ID=$(find_release_id)
if [ -n "$ID" ]; then
# Re-run: force the body back in sync instead of leaving it stale.
gh api --method PATCH "repos/$GITHUB_REPOSITORY/releases/$ID" \
-F name="$TITLE" \
-F body=@"$RUNNER_TEMP/notes.md" \
-F draft=true \
-F prerelease="$PRERELEASE" >/dev/null
else
gh release create "$TAG" \
--title "$TITLE" \
--notes-file "$RUNNER_TEMP/notes.md" \
--draft \
--prerelease="$PRERELEASE" \
--target "$GITHUB_SHA"
ID=$(find_release_id)
fi
if [ -z "$ID" ]; then
echo "::error::Could not resolve a release id for tag $TAG"
exit 1
fi
echo "Release id for $TAG: $ID"
echo "release_id=$ID" >> "$GITHUB_OUTPUT"
build-macos:
needs: create-release
# Skipped when a dispatch asked for a different platform. A tag push has no
# inputs at all, so that case falls through to building everything.
if: >-
github.event_name != 'workflow_dispatch'
|| inputs.targets == 'all'
|| inputs.targets == 'macos'
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- args: "--target aarch64-apple-darwin"
rust_target: aarch64-apple-darwin
- args: "--target x86_64-apple-darwin"
rust_target: x86_64-apple-darwin
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.create-release.outputs.tag }}
# The bundle version comes from tauri.conf.json, not from the tag, so
# without this every release would ship artifacts named after whatever
# version happens to be committed.
- name: Sync app version to ${{ needs.create-release.outputs.version }}
env:
VERSION: ${{ needs.create-release.outputs.version }}
run: node scripts/sync-version.mjs
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_target }}
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
key: ${{ matrix.rust_target }}
- name: Install frontend dependencies
run: npm ci
- name: Build Tauri app
uses: tauri-apps/tauri-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Minisign keypair for the updater. Unrelated to Apple code signing —
# it only lets installed clients verify an update came from us.
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# No Apple signing vars here on purpose. A missing secret still sets
# the variable to an empty string, and the bundler treats a *present*
# APPLE_CERTIFICATE as "sign this", then fails on `security import`.
# To enable notarization later, add the block documented in
# docs/RELEASING.md — don't pre-wire it. See that doc before editing.
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.args }}
# The manifest is written by the updater-manifest job instead. With a
# two-target matrix, letting both builds upload latest.json races:
# whichever finishes last overwrites the other's entry, producing an
# updater that silently 404s for one architecture.
uploadUpdaterJson: false
# Ubuntu 22.04 on purpose, not ubuntu-latest: the binary links against the
# builder's glibc, so building on 24.04 (glibc 2.39) would refuse to start on
# Debian 12 and Ubuntu 22.04. 22.04 ships glibc 2.35, which covers every
# currently supported distro.
build-linux:
needs: create-release
# Skipped when a dispatch asked for a different platform. A tag push has no
# inputs at all, so that case falls through to building everything.
if: >-
github.event_name != 'workflow_dispatch'
|| inputs.targets == 'all'
|| inputs.targets == 'linux'
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.create-release.outputs.tag }}
- name: Sync app version to ${{ needs.create-release.outputs.version }}
env:
VERSION: ${{ needs.create-release.outputs.version }}
run: node scripts/sync-version.mjs
# WebKitGTK is the webview on Linux, and none of this is preinstalled.
# libfuse2 is for the bundler, not the app: appimagetool ships as an
# AppImage itself and needs FUSE to run.
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libssl-dev \
libxdo-dev \
build-essential \
curl wget file patchelf libfuse2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
key: x86_64-unknown-linux-gnu
- name: Install frontend dependencies
run: npm ci
- name: Build Tauri app
uses: tauri-apps/tauri-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# Runners have no FUSE device, so appimagetool has to unpack itself
# instead of mounting. Without this the AppImage step fails outright.
APPIMAGE_EXTRACT_AND_RUN: 1
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
args: --target x86_64-unknown-linux-gnu
# Same reason as the macOS job: the manifest is composed once, later.
uploadUpdaterJson: false
build-windows:
needs: create-release
# Skipped when a dispatch asked for a different platform. A tag push has no
# inputs at all, so that case falls through to building everything.
if: >-
github.event_name != 'workflow_dispatch'
|| inputs.targets == 'all'
|| inputs.targets == 'windows'
permissions:
contents: write
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.create-release.outputs.tag }}
- name: Sync app version to ${{ needs.create-release.outputs.version }}
env:
VERSION: ${{ needs.create-release.outputs.version }}
run: node scripts/sync-version.mjs
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"
key: x86_64-pc-windows-msvc
- name: Install frontend dependencies
run: npm ci
- name: Build Tauri app
uses: tauri-apps/tauri-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# No Windows code-signing vars here, for the same reason there are none
# for Apple: an empty secret still sets the variable, and the bundler
# treats a *present* certificate as "sign this" and then fails. An
# unsigned installer works — SmartScreen warns, it doesn't refuse.
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
args: --target x86_64-pc-windows-msvc
# Same reason as the other two jobs: the manifest is composed once.
uploadUpdaterJson: false
# Composes latest.json once, after every build has uploaded its updater
# artifacts, so it always describes all of them.
updater-manifest:
needs: [create-release, build-macos, build-linux, build-windows]
# Runs after a partial build too: `needs` on a skipped job would
# otherwise skip this as well, and a one-platform rebuild still has to
# refresh the manifest and undraft. A genuine build failure still stops it.
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }}
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Compose and upload latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.create-release.outputs.tag }}
VERSION: ${{ needs.create-release.outputs.version }}
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
run: |
set -euo pipefail
# The release is still a draft here, so it must be addressed by id.
gh api --paginate "repos/$GH_REPO/releases/$RELEASE_ID/assets?per_page=100" \
> assets.json
PLATFORMS='{}'
# Adds one updater platform entry from the artifact's detached
# signature. Runs in the current shell, not a subshell, so it can
# accumulate into $PLATFORMS.
add_platform() {
PLATFORM="$1"
ARTIFACT="$2"
SIG_ID=$(jq -r --arg n "${ARTIFACT}.sig" \
'.[] | select(.name == $n) | .id' assets.json | head -n1)
if [ -z "$SIG_ID" ]; then
echo "::error::${ARTIFACT}.sig is missing from the release. Is TAURI_SIGNING_PRIVATE_KEY set?"
exit 1
fi
SIG=$(gh api -H "Accept: application/octet-stream" \
"repos/$GH_REPO/releases/assets/$SIG_ID")
# Public download URL, valid once the release is undrafted.
URL="https://github.com/$GH_REPO/releases/download/$TAG/$ARTIFACT"
PLATFORMS=$(jq \
--arg k "$PLATFORM" --arg s "$SIG" --arg u "$URL" \
'. + {($k): {signature: $s, url: $u}}' <<<"$PLATFORMS")
}
add_platform darwin-aarch64 "FigyTerm_${VERSION}_aarch64.app.tar.gz"
add_platform darwin-x86_64 "FigyTerm_${VERSION}_x64.app.tar.gz"
# Linux updates go through the AppImage, which the updater replaces in
# place. The .deb and .rpm are downloads only: their files belong to
# the package manager, so the app must never rewrite them.
add_platform linux-x86_64 "FigyTerm_${VERSION}_amd64.AppImage"
# Windows updates go through the NSIS installer, which can install
# per-user without administrator rights. The .msi ships alongside for
# enterprise deployment and is left to whatever pushed it.
add_platform windows-x86_64 "FigyTerm_${VERSION}_x64-setup.exe"
jq -n \
--arg v "$VERSION" \
--arg d "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--argjson p "$PLATFORMS" \
'{version: $v, notes: "See the release notes on GitHub.", pub_date: $d, platforms: $p}' \
> latest.json
cat latest.json
# Re-runs: drop the previous manifest so the upload doesn't collide.
OLD_ID=$(jq -r '.[] | select(.name == "latest.json") | .id' assets.json | head -n1)
if [ -n "$OLD_ID" ]; then
gh api --method DELETE "repos/$GH_REPO/releases/assets/$OLD_ID" >/dev/null
fi
curl -sSf -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @latest.json \
"https://uploads.github.com/repos/$GH_REPO/releases/$RELEASE_ID/assets?name=latest.json" \
>/dev/null
echo "Uploaded latest.json for $TAG"
publish-release:
needs: [create-release, build-macos, build-linux, build-windows, updater-manifest]
# Runs after a partial build too: `needs` on a skipped job would
# otherwise skip this as well, and a one-platform rebuild still has to
# refresh the manifest and undraft. A genuine build failure still stops it.
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }}
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Undraft release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.create-release.outputs.tag }}
PRERELEASE: ${{ needs.create-release.outputs.prerelease }}
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
run: |
# Address the release by id: it is still a draft at this point, and
# tag-based lookups do not resolve drafts.
if [ "$PRERELEASE" = "true" ]; then MAKE_LATEST=false; else MAKE_LATEST=true; fi
gh api --method PATCH "repos/$GH_REPO/releases/$RELEASE_ID" \
-F draft=false \
-f make_latest="$MAKE_LATEST" >/dev/null
echo "Published $TAG"