Repository navigation
499 lines (482 loc) · 23.4 KB
/
Copy pathbinaries.yaml
File metadata and controls
499 lines (482 loc) · 23.4 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
# Reusable: build self-contained flowR binaries for every platform and upload
# them to `release_tag`. Called by release.yaml and build-flowr-binaries.yaml.
#
# flowR itself is not modified: we compile the published npm CLI with `bun build
# --compile`, which inlines every module; the two tree-sitter .wasm files ship
# alongside and are loaded from disk.
#
# Four jobs:
# build - a single Linux runner cross-compiles the four non-Windows
# targets (Bun's --target does darwin/linux-arm from x64 Linux),
# signs them, and uploads. Keeping these off the scarce macos-13
# runners means a busy pool can never stall the actual publish.
# sigdb - the signature database, built once as ONE platform-independent
# signed asset shared by every target and engine (it is data, not
# code), so it is not duplicated into all five binaries and can be
# re-cut on its own when the database changes but flowR does not.
# build-win - win-x64 is built natively on windows-latest instead of being
# cross-compiled. Cross-compiling the Windows standalone .exe from
# Linux segfaults at launch (a Bun bug in the compiled runtime:
# first with --bytecode, then again after it was removed), so this
# target must be produced on a real Windows host. It `needs: build`
# so the release already exists when it uploads and the two
# action-gh-release calls can't race to create it.
# smoke - a per-platform matrix downloads each *uploaded* asset and boots
# it natively, as a post-publish safeguard. If a native runner is
# starved it only delays this check; the binaries are already live.
name: binaries
on:
workflow_call:
inputs:
flowr_version:
required: true
type: string
release_tag:
required: true
type: string
# Rebuild and overwrite even if this flowR version's binaries are already
# published. Needed to replace a bad asset (e.g. a segfaulting win-x64)
# without bumping the flowR version. Off by default so automated releases
# keep skipping already-published binaries.
force_rebuild:
required: false
default: false
type: boolean
permissions:
contents: write
jobs:
build:
name: build + upload all targets
runs-on: ubuntu-latest
steps:
- name: Validate inputs
shell: bash
run: |
set -euo pipefail
[ -n "${{ inputs.flowr_version }}" ] || { echo "::error::flowr_version input is empty"; exit 1; }
[ -n "${{ inputs.release_tag }}" ] || { echo "::error::release_tag input is empty"; exit 1; }
echo "building flowR ${{ inputs.flowr_version }} -> release ${{ inputs.release_tag }} (all targets)"
- uses: actions/checkout@v4
# Skip the (expensive) build + upload when this flowR version's binaries
# are already published: if flowR did not change, nothing needs rebuilding.
# The smoke job still runs and re-verifies the existing assets.
- name: Skip build if all binaries already published
id: preflight
shell: bash
env:
GH_TOKEN: ${{ github.token }}
FORCE: ${{ inputs.force_rebuild }}
run: |
set -euo pipefail
targets="linux-x64 linux-arm64 darwin-x64 darwin-arm64"
have=$(gh release view "${{ inputs.release_tag }}" --repo "$GITHUB_REPOSITORY" \
--json assets --jq '.assets[].name' 2>/dev/null || true)
if [ "$FORCE" = "true" ]; then
# Force: always rebuild, and delete any stale assets first so the
# re-upload is a clean replace (belt-and-suspenders — the upload step
# also overwrites same-named assets). Only delete what exists so a
# missing .sig/.sha256 can't fail the step.
for t in $targets; do
for f in "flowr-${{ inputs.flowr_version }}-$t.tar.gz" \
"flowr-${{ inputs.flowr_version }}-$t.tar.gz.sha256" \
"flowr-${{ inputs.flowr_version }}-$t.tar.gz.sig"; do
echo "$have" | grep -qx "$f" && \
gh release delete-asset "${{ inputs.release_tag }}" "$f" \
--repo "$GITHUB_REPOSITORY" --yes || true
done
done
echo "have_all=false" >> "$GITHUB_OUTPUT"
echo "force_rebuild set; rebuilding all non-Windows targets."
else
all=true
for t in $targets; do
echo "$have" | grep -qx "flowr-${{ inputs.flowr_version }}-$t.tar.gz" || all=false
done
echo "have_all=$all" >> "$GITHUB_OUTPUT"
[ "$all" = true ] && echo "flowR ${{ inputs.flowr_version }} binaries already published; skipping build."
fi
true
# bun both installs the npm package and cross-compiles the four
# non-Windows targets, so no separate Node/npm toolchain is needed. (The
# compiled binary is self-contained; the bundled engine that needs Node
# lives in the R package, not this workflow.)
#
# Pin the version: the compiled program's argv layout is Bun-version
# dependent on Windows (older Bun handed [exe, ...args], a leading entry
# short of Node's [exe, script, ...args], which broke flowR's argv.slice(2);
# 1.3.14 aligns Windows with the Node/Unix layout). Pinning keeps that
# layout deterministic so the binaries' arg handling can't silently regress.
- if: steps.preflight.outputs.have_all != 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- name: Install flowR (the published npm package)
if: steps.preflight.outputs.have_all != 'true'
shell: bash
run: |
# bun add needs a manifest to install into
printf '{}\n' > package.json
# `ignore` is a peer import of flowR's CLI that bun must resolve when
# compiling; install it alongside flowR.
bun add "@eagleoutice/flowr@${{ inputs.flowr_version }}" ignore
- name: Cross-compile, package + sign every target
if: steps.preflight.outputs.have_all != 'true'
shell: bash
env:
FLOWR_SIGNING_KEY: ${{ secrets.FLOWR_SIGNING_KEY }}
FLOWR_VERSION: ${{ inputs.flowr_version }}
run: |
set -euo pipefail
entry=./node_modules/@eagleoutice/flowr/cli/flowr.js
# the two tree-sitter wasm blobs are platform-independent; ship them
# unchanged next to every binary.
# -print -quit: stop at the first match in-process, so no SIGPIPE can
# trip `set -o pipefail` (a `find | head` would).
wr=$(find node_modules -name 'tree-sitter-r.wasm' -print -quit)
wt=$(find node_modules -name 'tree-sitter.wasm' -print -quit)
[ -n "$wr" ] && [ -n "$wt" ] || { echo "::error::tree-sitter wasm not found in node_modules"; exit 1; }
mkdir -p out
# release target -> bun --target triple
while read -r target buntarget; do
[ -n "$target" ] || continue
ext=""; [ "$target" = "win-x64" ] && ext=".exe"
echo "::group::compile $target ($buntarget)"
rm -rf dist; mkdir -p dist
# No --bytecode: it embeds JSC bytecode built for the *host*, which a
# cross-compiled target loads and segfaults on at launch when run on
# its native OS (Bun issue #18416). Every target in this job is
# cross-compiled from Linux, so plain JS bundling is the only portable
# choice; startup is marginally slower, but correct.
bun build "$entry" --compile --minify --sourcemap=none \
--target="$buntarget" --outfile "dist/flowr${ext}"
cp "$wr" "$wt" dist/
tgz="flowr-${FLOWR_VERSION}-${target}.tar.gz"
tar -czf "out/${tgz}" -C dist .
(
cd out
sha256sum "$tgz" > "${tgz}.sha256"
# openssl ECDSA/SHA-256 so the R `openssl` package verifies it, no gpg.
if [ -n "${FLOWR_SIGNING_KEY:-}" ]; then
printf '%s' "$FLOWR_SIGNING_KEY" > signing_key.pem
openssl dgst -sha256 -sign signing_key.pem -out "${tgz}.sig" "$tgz"
rm -f signing_key.pem
else
echo "No FLOWR_SIGNING_KEY secret; SHA-256 only for ${target}."
fi
)
echo "::endgroup::"
echo "- \`${tgz}\` built" >> "$GITHUB_STEP_SUMMARY"
done <<'TARGETS'
linux-x64 bun-linux-x64
linux-arm64 bun-linux-arm64
darwin-x64 bun-darwin-x64
darwin-arm64 bun-darwin-arm64
TARGETS
- name: Upload to the release
if: steps.preflight.outputs.have_all != 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.release_tag }}
# This release only hosts the downloadable engine binaries, keyed by
# flowR version (the R client builds its download URL from this tag).
# It is NOT the product release: keep it off "Latest" so the
# adapter-versioned release (v<adapter>) stays the prominent one, and
# label it clearly.
name: flowR engine binaries — ${{ inputs.flowr_version }}
make_latest: false
body: |
Prebuilt, self-contained **flowR ${{ inputs.flowr_version }}** engine
binaries (per platform), downloaded on demand by the flowr R adapter.
This is an asset store keyed by flowR version, **not** an adapter
release — see the [latest adapter release](https://github.com/flowr-analysis/flowr-r-adapter/releases/latest)
for the versioned package.
# false, not true: signing is optional (no FLOWR_SIGNING_KEY -> no .sig
# files), so the *.sig glob legitimately matches nothing. A genuinely
# missing binary is caught downstream — the smoke job's
# `gh release download --pattern` errors if its asset is absent.
fail_on_unmatched_files: false
files: |
out/*.tar.gz
out/*.tar.gz.sha256
out/*.tar.gz.sig
# win-x64 is built natively here instead of being cross-compiled in `build`.
# Cross-compiling the Windows standalone .exe from Linux segfaults at launch (a
# Bun bug in the compiled runtime — first with --bytecode, then again after it
# was removed), so this one target must be produced on a real Windows host.
# `needs: build` serializes the two uploads: by the time this runs the release
# already exists, so the two action-gh-release calls can't race to create it.
build-win:
name: build + upload win-x64 (native)
needs: build
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
# Same per-target skip as `build` (and same force_rebuild override).
- name: Skip build if win-x64 already published
id: preflight
shell: bash
env:
GH_TOKEN: ${{ github.token }}
FORCE: ${{ inputs.force_rebuild }}
run: |
set -euo pipefail
have=$(gh release view "${{ inputs.release_tag }}" --repo "$GITHUB_REPOSITORY" \
--json assets --jq '.assets[].name' 2>/dev/null || true)
if [ "$FORCE" = "true" ]; then
# Force: always rebuild, deleting any stale win-x64 assets first.
for f in "flowr-${{ inputs.flowr_version }}-win-x64.tar.gz" \
"flowr-${{ inputs.flowr_version }}-win-x64.tar.gz.sha256" \
"flowr-${{ inputs.flowr_version }}-win-x64.tar.gz.sig"; do
echo "$have" | grep -qx "$f" && \
gh release delete-asset "${{ inputs.release_tag }}" "$f" \
--repo "$GITHUB_REPOSITORY" --yes || true
done
echo "have=false" >> "$GITHUB_OUTPUT"
echo "force_rebuild set; rebuilding win-x64."
elif echo "$have" | grep -qx "flowr-${{ inputs.flowr_version }}-win-x64.tar.gz"; then
echo "have=true" >> "$GITHUB_OUTPUT"
echo "flowR ${{ inputs.flowr_version }} win-x64 binary already published; skipping build."
else
echo "have=false" >> "$GITHUB_OUTPUT"
fi
# Same pinned Bun as `build` (1.3.14): it is the version whose Windows argv
# layout matches Node/Unix ([exe, script, ...args]), which flowR's
# argv.slice(2) relies on. Building natively keeps that runtime unchanged.
- if: steps.preflight.outputs.have != 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- name: Install flowR (the published npm package)
if: steps.preflight.outputs.have != 'true'
shell: bash
run: |
# bun add needs a manifest to install into; `ignore` is a peer import
# of flowR's CLI that bun must resolve when compiling.
printf '{}\n' > package.json
bun add "@eagleoutice/flowr@${{ inputs.flowr_version }}" ignore
- name: Compile, package + sign win-x64 (native)
if: steps.preflight.outputs.have != 'true'
shell: bash
env:
FLOWR_SIGNING_KEY: ${{ secrets.FLOWR_SIGNING_KEY }}
FLOWR_VERSION: ${{ inputs.flowr_version }}
run: |
set -euo pipefail
entry=./node_modules/@eagleoutice/flowr/cli/flowr.js
wr=$(find node_modules -name 'tree-sitter-r.wasm' -print -quit)
wt=$(find node_modules -name 'tree-sitter.wasm' -print -quit)
[ -n "$wr" ] && [ -n "$wt" ] || { echo "::error::tree-sitter wasm not found in node_modules"; exit 1; }
mkdir -p out dist
# Native Windows build: same flags as the cross targets, but the real
# Windows toolchain produces the runtime, so no cross-compile segfault.
# No --bytecode (kept off for parity); plain minified JS bundle.
bun build "$entry" --compile --minify --sourcemap=none \
--target=bun-windows-x64 --outfile dist/flowr.exe
cp "$wr" "$wt" dist/
tgz="flowr-${FLOWR_VERSION}-win-x64.tar.gz"
tar -czf "out/${tgz}" -C dist .
(
cd out
sha256sum "$tgz" > "${tgz}.sha256"
if [ -n "${FLOWR_SIGNING_KEY:-}" ]; then
printf '%s' "$FLOWR_SIGNING_KEY" > signing_key.pem
openssl dgst -sha256 -sign signing_key.pem -out "${tgz}.sig" "$tgz"
rm -f signing_key.pem
else
echo "No FLOWR_SIGNING_KEY secret; SHA-256 only for win-x64."
fi
)
echo "- \`${tgz}\` built (native windows)" >> "$GITHUB_STEP_SUMMARY"
# Adds win-x64 assets to the release created by `build`. No name/body/
# make_latest here so this upload can't clobber that release's metadata.
- name: Upload to the release
if: steps.preflight.outputs.have != 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.release_tag }}
fail_on_unmatched_files: false
files: |
out/*.tar.gz
out/*.tar.gz.sha256
out/*.tar.gz.sig
# The signature database, as platform-independent signed assets shared by every
# target and every engine (binary/bundled/node) -- it is data, like the wasm
# blobs, so there is nothing per-platform about it. Publishing it beside the
# binaries (rather than inside all five) keeps it out of every binary and lets
# it be re-cut on its own when the database changes but flowR does not.
#
# tools/pack-sigdb.sh does the work and is the same script `make sigdb` runs,
# so what ships is exactly what a maintainer can build and inspect locally. It
# takes what to pack from the pointer flowR itself commits, verifies every
# shard against that pointer's hashes, and boots a real flowR against each
# finished archive -- a database that does not mount is never published.
#
# Three non-redundant sets are published (`base`, `current`, `history`); the R
# side downloads whichever the user selected (`flowr.sigdb`) and mounts them
# together.
#
# `needs: build` for the same reason as build-win: the release exists by then,
# so no two uploads race to create it.
sigdb:
name: build + upload the shared sigdb
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Skip build if the sigdb is already published
id: preflight
shell: bash
env:
GH_TOKEN: ${{ github.token }}
FORCE: ${{ inputs.force_rebuild }}
run: |
set -euo pipefail
have=$(gh release view "${{ inputs.release_tag }}" --repo "$GITHUB_REPOSITORY" \
--json assets --jq '.assets[].name' 2>/dev/null || true)
all=true
for s in base current history; do
asset="flowr-sigdb-$s-${{ inputs.flowr_version }}.tar.gz"
if [ "$FORCE" = "true" ]; then
for f in "$asset" "$asset.sha256" "$asset.sig"; do
echo "$have" | grep -qx "$f" && \
gh release delete-asset "${{ inputs.release_tag }}" "$f" \
--repo "$GITHUB_REPOSITORY" --yes || true
done
all=false
else
echo "$have" | grep -qx "$asset" || all=false
fi
done
if [ "$all" = true ]; then
echo "have=true" >> "$GITHUB_OUTPUT"
echo "sigdb for flowR ${{ inputs.flowr_version }} already published; skipping."
else
echo "have=false" >> "$GITHUB_OUTPUT"
fi
# node runs the mount check the pack script performs on every archive
- if: steps.preflight.outputs.have != 'true'
uses: actions/setup-node@v4
with:
node-version: 22
- name: Pack the sigdb (verifying it mounts)
if: steps.preflight.outputs.have != 'true'
shell: bash
run: tools/pack-sigdb.sh "${{ inputs.flowr_version }}" out
- name: Sign
if: steps.preflight.outputs.have != 'true'
shell: bash
env:
FLOWR_SIGNING_KEY: ${{ secrets.FLOWR_SIGNING_KEY }}
run: |
set -euo pipefail
cd out
if [ -z "${FLOWR_SIGNING_KEY:-}" ]; then
echo "No FLOWR_SIGNING_KEY secret; SHA-256 only for the sigdb."
else
printf '%s' "$FLOWR_SIGNING_KEY" > signing_key.pem
for tgz in *.tar.gz; do
openssl dgst -sha256 -sign signing_key.pem -out "${tgz}.sig" "$tgz"
done
rm -f signing_key.pem
fi
for tgz in *.tar.gz; do
echo "- \`${tgz}\` built ($(du -h "$tgz" | cut -f1), shared by all platforms)" \
>> "$GITHUB_STEP_SUMMARY"
done
- name: Upload to the release
if: steps.preflight.outputs.have != 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.release_tag }}
fail_on_unmatched_files: false
files: |
out/*.tar.gz
out/*.tar.gz.sha256
out/*.tar.gz.sig
# Post-publish safeguard: pull each uploaded asset and check it. Two modes:
# boot - run the binary natively and require flowR's REPL banner. Used for
# every target that has a GitHub-hosted runner able to execute it.
# inspect - macOS Mach-O binaries can't be executed off a Mac, and the Intel
# (macos-13) runners are what starved this pipeline for hours. So we
# don't boot darwin at all: on Linux we verify the artifact is a
# well-formed Mach-O of the right CPU type (via `file`) with the
# wasm alongside. This catches a wrong/corrupt/mis-targeted binary;
# it does not execute it. Boot confidence is indirect but real: the
# same cross-compile toolchain and flags produce linux-arm64 and
# win-x64, which ARE booted below, so only the OS wrapper (which
# `file` validates) differs for darwin.
smoke:
name: smoke ${{ matrix.target }}
needs: [build, build-win]
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, target: linux-x64, mode: boot }
- { os: ubuntu-24.04-arm, target: linux-arm64, mode: boot }
- { os: windows-latest, target: win-x64, mode: boot }
- { os: ubuntu-latest, target: darwin-x64, mode: inspect, macho: x86_64 }
- { os: ubuntu-latest, target: darwin-arm64, mode: inspect, macho: arm64 }
runs-on: ${{ matrix.os }}
steps:
- name: Download the uploaded binary for this platform
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
asset="flowr-${{ inputs.flowr_version }}-${{ matrix.target }}.tar.gz"
gh release download "${{ inputs.release_tag }}" \
--repo "$GITHUB_REPOSITORY" --pattern "$asset" --dir dl
mkdir -p run
tar -xzf "dl/$asset" -C run
- name: Boot the uploaded binary
if: ${{ matrix.mode == 'boot' }}
shell: bash
run: |
set -uo pipefail
ext=""; [ "${{ matrix.target }}" = "win-x64" ] && ext=".exe"
cd run
chmod +x "flowr${ext}" 2>/dev/null || true
wr=$(find . -name 'tree-sitter-r.wasm' | head -1)
wt=$(find . -name 'tree-sitter.wasm' | head -1)
printf ':quit\n' | "./flowr${ext}" --default-engine tree-sitter \
--engine.r-shell.disabled \
--engine.tree-sitter.wasm-path "$wr" \
--engine.tree-sitter.tree-sitter-wasm-path "$wt" > smoke.log 2>&1 || true
if grep -q "flowR repl" smoke.log; then
echo "✅ **${{ matrix.target }}** — binary boots flowR" >> "$GITHUB_STEP_SUMMARY"
echo "smoke OK for ${{ matrix.target }}"
else
{
echo "❌ **${{ matrix.target }}** — binary does NOT boot"
echo '```'
tail -n 40 smoke.log
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
echo "SMOKE TEST FAILED for ${{ matrix.target }} - the binary does not run:"
cat smoke.log
exit 1
fi
- name: Inspect the uploaded Mach-O binary (no execution)
if: ${{ matrix.mode == 'inspect' }}
shell: bash
run: |
set -euo pipefail
cd run
info=$(file -b flowr)
echo "file: $info"
# Require a Mach-O executable of the expected CPU type, a plausibly
# sized binary (a failed compile yields a tiny stub), and both wasm blobs.
bytes=$(wc -c < flowr)
ok=1
echo "$info" | grep -qi 'Mach-O' || { echo "::error::not a Mach-O binary"; ok=0; }
echo "$info" | grep -qi '${{ matrix.macho }}' || { echo "::error::wrong CPU type (want ${{ matrix.macho }})"; ok=0; }
[ "$bytes" -gt 1000000 ] || { echo "::error::binary implausibly small ($bytes bytes)"; ok=0; }
[ -f tree-sitter-r.wasm ] && [ -f tree-sitter.wasm ] || { echo "::error::missing tree-sitter wasm"; ok=0; }
if [ "$ok" = 1 ]; then
echo "✅ **${{ matrix.target }}** — Mach-O ${{ matrix.macho }}, $((bytes/1024/1024)) MB, wasm present (not executed: no macOS runner)" >> "$GITHUB_STEP_SUMMARY"
else
echo "❌ **${{ matrix.target }}** — Mach-O validation failed (\`$info\`, $bytes bytes)" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi