-
-
Notifications
You must be signed in to change notification settings - Fork 5
671 lines (632 loc) · 27.6 KB
/
Copy pathrelease.yml
File metadata and controls
671 lines (632 loc) · 27.6 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
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
name: Release
run-name: Release ${{ github.event.inputs.tag || github.ref_name }}
# Two parallel tag streams trigger this workflow:
# sss_cli/v0.2.1 → releases the `sss` binary (sss_cli)
# sss_code/v0.3.0 → releases the `sss_code` binary
#
# Library crates (sss_core, sss_lib, sss_ocr, sss_capture, sss_capture_ui)
# have `tag = false` in their per-crate release metadata so cargo-release
# bumps their version + commit but never tags them — only the two binary
# crates fan into this workflow.
#
# `workflow_dispatch` lets you force a release from a tag too.
on:
push:
tags:
- "sss_cli/v*"
- "sss_code/v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g. sss_cli/v0.2.1 or sss_code/v0.3.0)."
required: true
type: string
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
# Resolve which binary we're releasing + which version, once for all
# downstream jobs to consume via `needs.plan.outputs.*`.
plan:
name: Plan release
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.plan.outputs.tag }}
binary: ${{ steps.plan.outputs.binary }}
version: ${{ steps.plan.outputs.version }}
release_name: ${{ steps.plan.outputs.release_name }}
crate_dir: ${{ steps.plan.outputs.crate_dir }}
tag_pattern: ${{ steps.plan.outputs.tag_pattern }}
cliff_paths: ${{ steps.plan.outputs.cliff_paths }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}
fetch-depth: 0
- name: Resolve target
id: plan
run: |
tag="${{ github.event.inputs.tag || github.ref_name }}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
case "$tag" in
sss_code/v*)
binary=sss_code
version="${tag#sss_code/v}"
crate_dir=sss_code
tag_pattern='sss_code/v[0-9]+\.[0-9]+\.[0-9]+'
# sss_code depends on sss_lib (rendering pipeline) and
# sss_core (shared primitives). Changes in either land in
# the same binary, so include them in the changelog.
cliff_paths='--include-path crates/sss_code/** --include-path crates/sss_lib/** --include-path crates/sss_core/**'
crate=crates/sss_code/Cargo.toml ;;
sss_cli/v*)
binary=sss
version="${tag#sss_cli/v}"
crate_dir=sss_cli
tag_pattern='sss_cli/v[0-9]+\.[0-9]+\.[0-9]+'
# sss_cli pulls every workspace crate (sss_lib for render,
# sss_core primitives, sss_capture + sss_capture_ui for the
# selector/annotator, sss_ocr for text recognition). All of
# them ship inside the `sss` binary, so all are in scope.
cliff_paths='--include-path crates/sss_cli/** --include-path crates/sss_lib/** --include-path crates/sss_core/** --include-path crates/sss_capture/** --include-path crates/sss_capture_ui/** --include-path crates/sss_ocr/**'
crate=crates/sss_cli/Cargo.toml ;;
*)
echo "::error::tag '$tag' does not match sss_cli/v* or sss_code/v*"
exit 1 ;;
esac
echo "binary=$binary" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "release_name=$binary v$version" >> "$GITHUB_OUTPUT"
echo "crate_dir=$crate_dir" >> "$GITHUB_OUTPUT"
echo "tag_pattern=$tag_pattern" >> "$GITHUB_OUTPUT"
echo "cliff_paths=$cliff_paths" >> "$GITHUB_OUTPUT"
crate_ver=$(awk -F\" '/^version = /{print $2; exit}' "$crate")
if [ "$crate_ver" != "$version" ]; then
echo "::error::$crate version ($crate_ver) does not match tag ($version)"
exit 1
fi
# Per-runner slice builds. Each runner only emits what it can produce
# natively (or via pkgsCross on the linux host); the publish job stitches
# every artifact into a single release.
#
# `variant` axis: every `sss` (sss_cli) tag releases TWO sets of bundles
# in parallel — the full build (default features, ships libonnxruntime +
# CUDA stack inside the bundle) and a `no-ocr` build (`--no-default-features`,
# no OCR code, distro packages list `onnxruntime` as a recommendation).
# `sss_code` only ships in one flavour; the matrix is filtered below.
bundles:
name: Build bundles (${{ matrix.slice.label }} ${{ matrix.variant }})
needs: plan
runs-on: ${{ matrix.slice.runner }}
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
slice:
- label: linux-x86_64
runner: ubuntu-latest
slice: linux-x86_64
- label: linux-aarch64
runner: ubuntu-24.04-arm
slice: linux-aarch64
- label: macos-aarch64
runner: macos-14
slice: darwin-aarch64
variant: [system, nvidia, rocm, noocr]
# Static excludes drop combinations that cannot produce any
# artifact, so the matrix doesn't spawn a runner just to no-op.
# Per-variant slice restrictions:
# - any slice × `nvidia` on macOS → no CUDA stack on Apple
# - any slice × `rocm` except linux-x86_64 → ROCm only on Linux-x86_64
# `sss_code` × {nvidia, rocm, noocr} can't be expressed here (the
# binary name resolves only at job time from `needs.plan.outputs`);
# those are still gated via the `Plan slice` step below.
# x86_64-darwin slice dropped: nixpkgs deprecating it (26.05 is
# the last supported), GitHub macos-13 runners queue indefinitely
# on free-tier quotas. aarch64-darwin covers modern Mac users.
exclude:
- slice:
label: macos-aarch64
runner: macos-14
slice: darwin-aarch64
variant: nvidia
- slice:
label: macos-aarch64
runner: macos-14
slice: darwin-aarch64
variant: rocm
- slice:
label: linux-aarch64
runner: ubuntu-24.04-arm
slice: linux-aarch64
variant: rocm
steps:
- name: Plan slice
id: pin
shell: bash
run: |
# The matrix `exclude:` already prunes combinations that can't
# produce any artifact. This step handles the dynamic gate: if
# the tag belongs to `sss_code`, the only eligible variant is
# `system` — `nvidia`/`rocm`/`noocr` are sss-only.
eligible=true
variant="${{ matrix.variant }}"
binary="${{ needs.plan.outputs.binary }}"
slice="${{ matrix.slice.slice }}"
if [ "$binary" != "sss" ] && [ "$variant" != "system" ]; then
eligible=false
fi
echo "eligible=$eligible" >> "$GITHUB_OUTPUT"
echo "Plan: variant=$variant binary=$binary slice=$slice → eligible=$eligible"
- name: Checkout
if: steps.pin.outputs.eligible == 'true'
uses: actions/checkout@v4
with:
ref: ${{ needs.plan.outputs.tag }}
fetch-depth: 0
- name: Install Nix
if: steps.pin.outputs.eligible == 'true'
uses: DeterminateSystems/nix-installer-action@main
with:
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
# Attic-backed binary cache hosted at cache.sergioribera.rs (`main`).
# The action wires the cache up as a substituter (so the build can pull
# nixpkgs + workspace artifacts that were already pushed) and uploads
# any new store paths created during the job in its post step.
- name: Attic cache
if: steps.pin.outputs.eligible == 'true'
uses: ryanccn/attic-action@v0
with:
endpoint: https://cache.sergioribera.rs
cache: main
token: ${{ secrets.ATTIC_TOKEN }}
- name: Build release slice
if: steps.pin.outputs.eligible == 'true'
run: |
# `system` variant for `sss` maps to the bare `release-sss`
# flake output; all other variants suffix the variant name.
# `sss_code` only ever uses the bare `release-sss_code` output.
attr="release-${{ needs.plan.outputs.binary }}"
if [ "${{ needs.plan.outputs.binary }}" = "sss" ] && [ "${{ matrix.variant }}" != "system" ]; then
attr="release-sss-${{ matrix.variant }}"
fi
# `--fallback` rebuilds from source when a substituter starts
# serving a NAR but the stream breaks mid-transfer (we hit
# HTTP/2 framing errors against attic intermittently). Without
# this the build aborts even though Nix could just re-derive
# the path locally.
nix build ".#$attr" -L --fallback --print-out-paths
- name: Stage artifacts
if: steps.pin.outputs.eligible == 'true'
run: |
mkdir -p dist
# `cp -RL` portable across GNU + BSD. nix store paths are 555 —
# `chmod -R u+w` makes the staged copy writable so later steps
# (upload-artifact, mv) don't hit EACCES on macOS.
cp -RL result/* dist/
chmod -R u+w dist/
# Per-slice + per-variant INSTALL.md/install.sh fragments —
# the publish job merges them into a single unified release.
tag="${{ matrix.slice.slice }}-${{ matrix.variant }}"
mv dist/INSTALL.md "dist/INSTALL-${tag}.md" 2>/dev/null || true
mv dist/install.sh "dist/install-${tag}.sh" 2>/dev/null || true
mv dist/install.ps1 "dist/install-${tag}.ps1" 2>/dev/null || true
mv dist/SHA256SUMS "dist/SHA256SUMS-${tag}" 2>/dev/null || true
ls -la dist
- name: Upload slice
if: steps.pin.outputs.eligible == 'true'
uses: actions/upload-artifact@v4
with:
name: bundles-${{ matrix.slice.slice }}-${{ matrix.variant }}
path: dist/
if-no-files-found: error
retention-days: 1
publish:
name: Publish GitHub Release
needs: [plan, bundles]
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
has_aur: ${{ steps.aur_detect.outputs.has_aur }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.plan.outputs.tag }}
fetch-depth: 0
# Download each (slice, variant) artifact into its own subdir so the
# variant-specific `aur/` layouts don't clobber each other (their
# filenames inside `aur/` are the same `PKGBUILD` / `.SRCINFO`,
# only the content differs). Then flatten the non-AUR pieces into
# `dist/` — variant disambiguation lives in the bundle filename
# (`sss-*` vs `sss-no-ocr-*`) so no collisions there.
- name: Download all bundles
uses: actions/download-artifact@v4
with:
pattern: bundles-*
path: bundles_raw
- name: Flatten non-AUR artifacts into dist/
run: |
mkdir -p dist aur_system aur_nvidia aur_rocm aur_noocr
shopt -s nullglob
for sub in bundles_raw/*/; do
slice_dir="${sub%/}"
name=$(basename "$slice_dir")
variant=system
case "$name" in
*-system) variant=system ;;
*-nvidia) variant=nvidia ;;
*-rocm) variant=rocm ;;
*-noocr) variant=noocr ;;
esac
# Pull the AUR source layout aside per variant so the four
# PKGBUILD/.SRCINFO sets don't clobber each other (filenames
# are identical inside `aur/`, only content differs).
if [ -d "$slice_dir/aur" ]; then
cp -r "$slice_dir/aur/." "aur_${variant}/" || true
rm -rf "$slice_dir/aur"
fi
# Everything else flattens straight in. Brew formulas (.rb)
# stay per-slice because the homebrew job needs to merge
# them by arch — we copy them into dist/ keyed by source
# subdir name.
cp -r "$slice_dir/." "dist/"
done
ls -la dist | head -50
- name: Generate unified SHA256SUMS
working-directory: dist
run: |
rm -f SHA256SUMS-*
find . -maxdepth 1 -type f \
! -name 'SHA256SUMS*' \
! -name 'INSTALL-*.md' \
! -name 'install-*.sh' \
! -name 'install-*.ps1' \
-printf '%P\n' \
| LC_ALL=C sort \
| xargs sha256sum -- > SHA256SUMS
- name: Assemble unified INSTALL.md + install.sh
working-directory: dist
run: |
# Each per-slice bundle ships its own INSTALL-<slice>.md /
# install-<slice>.sh whose only slice-specific bit is the
# arch/format table (resp. file_for() cases + list_entries
# body). The static prose, env vars, help text etc. are
# identical across slices. So: take the first slice as the
# template, then splice in the union of per-slice table rows
# / cases / list lines from every slice. One unified
# INSTALL.md, one unified install.sh — both arch-agnostic.
shopt -s nullglob
md_slices=(INSTALL-*.md)
sh_slices=(install-*.sh)
if [ "${#md_slices[@]}" -gt 0 ]; then
template="${md_slices[0]}"
# Pull the union of "| arch | os | format | file |" rows from
# every slice's "Bundles in this release" table. Dedupe.
{
for f in "${md_slices[@]}"; do
awk '/^\| Arch \| OS \| Format \| File \|$/{t=1; next}
t && /^\|-/{next}
t && /^\|/{print; next}
t{exit}' "$f"
done
} | LC_ALL=C sort -u > .bundle-rows
# Splice the union back into the template's table location.
awk -v rows=.bundle-rows '
/^\| Arch \| OS \| Format \| File \|$/{
print
print "|------|----|--------|------|"
while ((getline line < rows) > 0) print line
close(rows)
in_table=1
next
}
in_table && /^\|-/{next}
in_table && /^\|/{next}
in_table{in_table=0}
{print}
' "$template" > INSTALL.md
rm -f .bundle-rows
fi
if [ "${#sh_slices[@]}" -gt 0 ]; then
template_sh="${sh_slices[0]}"
# Union of file_for() case bodies (between `case "$1" in` and
# `*) return 1 ;;`). Lines look like
# "x86_64:Linux:archlinux") echo "..."; return ;;
{
for f in "${sh_slices[@]}"; do
awk '/^file_for\(\) \{/{f=1; next}
f && /case "\$1" in/{c=1; next}
c && /\*\) return 1/{exit}
c{print}' "$f"
done
} | LC_ALL=C sort -u > .sh-cases
# Union of list_entries() body — the human-readable
# "Configured bundles" listing inside the heredoc.
{
for f in "${sh_slices[@]}"; do
awk '/^list_entries\(\) \{/{f=1; next}
f && /Configured bundles:/{c=1; next}
c && /^EOF$/{exit}
c{print}' "$f"
done
} | LC_ALL=C sort -u > .sh-list
awk -v cases=.sh-cases -v listing=.sh-list '
/^file_for\(\) \{/{print; in_ff=1; next}
in_ff && /case "\$1" in/{
print
while ((getline line < cases) > 0) print line
close(cases)
in_cases=1
next
}
in_cases && /\*\) return 1/{in_cases=0; in_ff=0}
in_cases{next}
/^list_entries\(\) \{/{print; in_le=1; next}
in_le && /Configured bundles:/{
print
while ((getline line < listing) > 0) print line
close(listing)
in_list=1
next
}
in_list && /^EOF$/{in_list=0; in_le=0}
in_list{next}
{print}
' "$template_sh" > install.sh
chmod +x install.sh
rm -f .sh-cases .sh-list
fi
rm -f INSTALL-*.md install-*.sh install-*.ps1
echo "--- INSTALL.md preview ---"
head -60 INSTALL.md
echo "--- install.sh file_for() preview ---"
awk '/^file_for/,/^\}/' install.sh
- name: Pack AUR layouts for downstream workflow
id: aur_detect
run: |
# The archlinux format emits the AUR layout in `aur/` next to
# the .pkg.tar.zst. The AUR push workflow expects one tarball
# per AUR package. For `sss` we ship four: `sss-bin`,
# `sss-nvidia-bin`, `sss-rocm-bin`, `sss-noocr-bin`.
# `sss_code` only ships one: `sss_code-bin`.
ver="${{ needs.plan.outputs.version }}"
binary="${{ needs.plan.outputs.binary }}"
has_any=false
pack() {
local dir=$1 pkg=$2
if [ -d "$dir" ] && [ -n "$(ls -A "$dir")" ]; then
# The archlinux format emits the AUR source tarball
# (`<pkg>-<ver>-<arch>.tar.gz`) inside `aur/` alongside
# PKGBUILD. PKGBUILD's `source=()` points to it as a
# release asset URL, so we lift it into `dist/` to be
# uploaded directly — otherwise makepkg/updpkgsums 404s.
for src_tar in "$dir"/${pkg}-*.tar.gz; do
[ -f "$src_tar" ] || continue
cp "$src_tar" "dist/$(basename "$src_tar")"
done
tar czf "dist/${pkg}-${ver}-aur.tar.gz" -C "$dir" .
has_any=true
fi
}
if [ "$binary" = "sss" ]; then
pack aur_system sss-bin
pack aur_nvidia sss-nvidia-bin
pack aur_rocm sss-rocm-bin
pack aur_noocr sss-noocr-bin
elif [ "$binary" = "sss_code" ]; then
pack aur_system sss_code-bin
fi
echo "has_aur=$has_any" >> "$GITHUB_OUTPUT"
- name: Generate changelog with git-cliff
uses: orhun/git-cliff-action@v4
id: cliff
with:
config: cliff.toml
# Scope the changelog to the binary being released:
# * `--tag-pattern` restricts the "previous tag" walk to the
# same release stream so a sss_cli release doesn't reach
# back to the most recent sss_code tag (or vice versa) and
# drag every workspace commit since then into the body.
# * `--include-path` keeps only commits touching the binary's
# own crate directory. Workspace-wide changes (shared libs,
# CI infra, docs) intentionally don't land here — they show
# up in whichever binary they actually affected.
# * `--tag` is explicit instead of `--current`: cargo-release
# can land cross-tag commits at the same SHA, so trusting
# the implicit HEAD lookup mis-attributes the release.
args: >
--latest
--tag-pattern '${{ needs.plan.outputs.tag_pattern }}'
${{ needs.plan.outputs.cliff_paths }}
--strip header
env:
OUTPUT: CHANGELOG.md
- name: Assemble release body
run: |
{
cat CHANGELOG.md 2>/dev/null || echo "_(no changelog generated)_"
echo
cat dist/INSTALL.md
} > RELEASE_BODY.md
echo "--- release body preview ---"
head -120 RELEASE_BODY.md
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.plan.outputs.tag }}
name: ${{ needs.plan.outputs.release_name }}
draft: false
prerelease: ${{ contains(needs.plan.outputs.tag, '-') }}
body_path: RELEASE_BODY.md
# The globs below cover every format nix-bundle-app may emit,
# but most releases only produce a subset (sss_code skips deb
# variants on some slices, darwin doesn't produce .deb / .rpm,
# etc). `fail_on_unmatched_files: false` lets the publish
# succeed against whichever shape this release happened to
# produce; the SHA256SUMS still pins the full asset list.
fail_on_unmatched_files: false
# Exclude the `aur/` subdir from the release upload — it's the
# source layout for the AUR repo, not a user-installable artifact.
# AppImage intentionally absent: dropped in release.nix because
# the binary expects a distro-installed libonnxruntime which
# breaks AppImage's portability promise. Listing the glob with
# `fail_on_unmatched_files: true` would abort the publish job.
files: |
dist/*.deb
dist/*.rpm
dist/*.pkg.tar.zst
dist/*.tar.gz
dist/*.tar.zst
dist/*.dmg
dist/*.pkg
dist/*.rb
dist/*-aur.tar.gz
dist/INSTALL.md
dist/SHA256SUMS
dist/install.sh
# Pushes Homebrew formula files to SergioRibera/homebrew-tap. Triggered
# by the AUR push workflow indirectly (both react to a successful
# Release run via workflow_run). AUR runs in aur.yml, brew runs here so
# the secrets stay isolated per ecosystem.
homebrew:
name: Publish to Homebrew tap
needs: [plan, publish]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# Keep per-slice subdirs — each runner's .rb has the SAME filename
# (`sss.rb` / `sss_code.rb`) but different content per arch, so
# `merge-multiple: true` would silently lose one arch.
- name: Download brew formulas
uses: actions/download-artifact@v4
with:
pattern: bundles-*
path: dist
# nix-bundle-app emits one single-arch formula per (binary, target),
# and its `url` field is the bare `meta.downloadUrl` (a directory).
# We merge per-arch into a universal `on_macos { on_arm / on_intel }`
# formula AND rewrite the url to the actual tarball asset for that
# arch. Output filename + class are remapped per binary so the tap
# keeps the legacy `sss_cli.rb` / `sss_code.rb` names (cli's crate
# is `sss_cli` but its binary is `sss` — Homebrew users typed
# `brew install sergioribera/tap/sss_cli` historically).
- name: Merge per-arch formulas into universal
id: merge
run: |
python3 - <<'PY'
import collections, glob, os, pathlib, re
# internal-name (from meta.name) -> (formula filename stem,
# ruby class, installed binary). The brew test stanza needs the
# ACTUAL binary that lands in $bin, not the formula stem.
# Only the `system` and `noocr` variants ship on macOS — nvidia
# and rocm are Linux-only and never produce a brew formula.
rename = {
"sss": ("sss_cli", "SssCli", "sss"),
"sss-noocr": ("sss_cli_noocr", "SssCliNoocr", "sss"),
"sss_code": ("sss_code", "SssCode", "sss_code"),
}
slices = collections.defaultdict(dict) # name -> {arch: (url, sha)}
fields = {} # name -> shared metadata
# Slice dir name is `bundles-darwin-<arch>-<variant>`. We only
# care about <arch> for the merge axis — the formula stem
# already disambiguates the variant via `meta.name`.
for slice_dir in sorted(glob.glob("dist/bundles-darwin-*")):
base = pathlib.Path(slice_dir).name
arch = "arm" if "aarch64" in base else "intel"
os_label = "darwin"
for rb in glob.glob(f"{slice_dir}/*.rb"):
text = pathlib.Path(rb).read_text()
name = pathlib.Path(rb).stem
base_url = re.search(r'url\s+"([^"]+)"', text).group(1)
sha = re.search(r'sha256\s+"([^"]+)"', text).group(1)
version = re.search(r'version\s+"([^"]+)"', text).group(1)
# Construct the actual tarball asset URL.
asset_arch = "x86_64" if arch == "intel" else "aarch64"
tarball = f"{name}-{version}-{asset_arch}-{os_label}.tar.gz"
url = base_url.rstrip("/") + "/" + tarball
slices[name][arch] = (url, sha)
fields.setdefault(name, {
"desc": re.search(r'desc\s+"([^"]+)"', text).group(1),
"homepage": re.search(r'homepage\s+"([^"]+)"', text).group(1),
"version": version,
"license": re.search(r'license\s+"([^"]+)"', text).group(1),
})
if not slices:
print("::notice::no darwin .rb formulas to publish")
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
fh.write("skip=true\n")
raise SystemExit(0)
os.makedirs("formulas", exist_ok=True)
for name, f in fields.items():
file_stem, klass, binary = rename.get(
name, (name, name[:1].upper() + name[1:], name)
)
arches = slices[name]
parts = [
f"class {klass} < Formula",
f' desc "{f["desc"]}"',
f' homepage "{f["homepage"]}"',
f' version "{f["version"]}"',
f' license "{f["license"]}"',
"",
" on_macos do",
]
for label, key in (("on_arm", "arm"), ("on_intel", "intel")):
if key in arches:
url, sha = arches[key]
parts += [
f" {label} do",
f' url "{url}"',
f' sha256 "{sha}"',
" end",
]
parts += [
" end",
"",
" def install",
' bin.install Dir["bin/*"]',
' lib.install Dir["lib/*"] if Dir.exist?("lib")',
' share.install Dir["share/*"] if Dir.exist?("share")',
" end",
"",
" test do",
f' system "#{{bin}}/{binary}", "--version"',
" end",
"end",
"",
]
pathlib.Path(f"formulas/{file_stem}.rb").write_text("\n".join(parts))
print(f"wrote formulas/{file_stem}.rb ({', '.join(arches)})")
PY
ls -la formulas || true
- name: Checkout homebrew-tap
if: steps.merge.outputs.skip != 'true'
uses: actions/checkout@v4
with:
repository: SergioRibera/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
- name: Commit + push formulas
if: steps.merge.outputs.skip != 'true'
working-directory: tap
env:
BINARY: ${{ needs.plan.outputs.binary }}
VERSION: ${{ needs.plan.outputs.version }}
run: |
mkdir -p Formula
cp -f ../formulas/*.rb Formula/
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/*.rb
if git diff --cached --quiet; then
echo "no formula changes"
exit 0
fi
git commit -m "$BINARY $VERSION"
git push