Skip to content

feat: v1.3.1 — unified build chain manifest + world-class multi-platf… #15

feat: v1.3.1 — unified build chain manifest + world-class multi-platf…

feat: v1.3.1 — unified build chain manifest + world-class multi-platf… #15

Workflow file for this run

name: release
# Triggers on a v* tag push (e.g. `git tag v0.2.0 && git push origin v0.2.0`).
# Builds signed binaries for 4 platforms (linux x86_64 + aarch64,
# macOS universal arm64+x86_64, windows x86_64), packages a source
# tarball, computes SHA256SUMS, optionally minisigns, then publishes a
# single GitHub Release with everything attached.
#
# Inert until a tag is pushed. Manual `workflow_dispatch` is also
# allowed for dry-run testing without tagging — in that mode the build
# jobs run but the publish-release job's actual gh-release step is
# skipped (gated on startsWith(github.ref, 'refs/tags/v')).
#
# Per-platform signing/notarization steps are individually gated on
# their respective secrets being present, so the workflow gracefully
# degrades to "unsigned binaries shipped" when secrets are absent.
# Each binary's signing status is reflected in the release notes.
on:
push:
tags:
# Match only engine version tags like v0.2.0, v1.0.0-rc1.
# Extension tags (vscode-v*, browser-v*) have their own
# workflows and must not trigger the engine pipeline.
- 'v[0-9]*'
workflow_dispatch: {}
permissions:
contents: write
id-token: write # cosign keyless signing
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
# ------------------------------------------------------------------
# Resolve version once and share with every downstream job.
# ------------------------------------------------------------------
resolve-version:
name: resolve version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve version
id: v
run: |
if [ -n "${GITHUB_REF_NAME:-}" ] && [[ "${GITHUB_REF_NAME}" == v* ]]; then
ver="${GITHUB_REF_NAME#v}"
else
ver="$(git describe --tags --always --dirty)"
fi
echo "version=$ver" >> "$GITHUB_OUTPUT"
echo "Resolved version: $ver"
# ------------------------------------------------------------------
# Linux x86_64 (host build, AVX2 auto-detected by the Makefile)
# ------------------------------------------------------------------
build-linux-x86_64:
name: build linux x86_64
needs: resolve-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang (for sanitize chain)
run: sudo apt-get update && sudo apt-get install -y clang
- name: make all-checks
env:
EOSLLM_SKIP_BENCH_DIFF: '1'
run: make CC=clang all-checks
- name: Build release tools
# Clean first because make all-checks leaves a sanitizer-
# instrumented libeosllm.a behind, which would otherwise fail
# to link against a non-sanitized eosllm-cli.
run: make clean && make BUILD=release lib tools
- name: Stage tarball
run: |
v="${{ needs.resolve-version.outputs.version }}"
stage="staging/eosllm-${v}-linux-x86_64"
mkdir -p "$stage/bin" "$stage/lib" "$stage/include" "$stage/share/doc/eosllm"
cp tools/eosllm-cli/eosllm-cli "$stage/bin/"
cp tools/eosllm-bench/eosllm-bench "$stage/bin/"
cp tools/eosllm-convert/eosllm-convert "$stage/bin/" || true
cp tools/eosllm-server/eosllm-server "$stage/bin/"
cp libeosllm.a "$stage/lib/"
cp -r include/eosllm "$stage/include/"
cp LICENSE README.md CHANGELOG.md SECURITY.md CONTRIBUTING.md \
"$stage/share/doc/eosllm/"
mkdir -p dist
(cd staging && tar czf "../dist/eosllm-${v}-linux-x86_64.tar.gz" \
"eosllm-${v}-linux-x86_64")
- uses: actions/upload-artifact@v4
with:
name: eosllm-linux-x86_64
path: dist/*.tar.gz
if-no-files-found: error
# ------------------------------------------------------------------
# Linux aarch64 (cross-compile + qemu-tested, mirrors the
# cross-aarch64 CI cell pattern in ci.yml)
# ------------------------------------------------------------------
build-linux-aarch64:
name: build linux aarch64
needs: resolve-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cross toolchain and qemu
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu qemu-user-static
- name: Build for aarch64 (static, NEON)
run: |
make CC=aarch64-linux-gnu-gcc BUILD=release \
EOSLLM_HAVE_KERNEL_NEON=1 \
CFLAGS_EXTRA="-static" LDFLAGS_EXTRA="-static" \
lib tools
- name: Run unit tests under qemu
run: |
make CC=aarch64-linux-gnu-gcc BUILD=release \
EOSLLM_HAVE_KERNEL_NEON=1 \
CFLAGS_EXTRA="-static" LDFLAGS_EXTRA="-static" \
test
- name: Stage tarball
run: |
v="${{ needs.resolve-version.outputs.version }}"
stage="staging/eosllm-${v}-linux-aarch64"
mkdir -p "$stage/bin" "$stage/lib" "$stage/include" "$stage/share/doc/eosllm"
cp tools/eosllm-cli/eosllm-cli "$stage/bin/"
cp tools/eosllm-bench/eosllm-bench "$stage/bin/"
cp tools/eosllm-convert/eosllm-convert "$stage/bin/" || true
cp tools/eosllm-server/eosllm-server "$stage/bin/"
cp libeosllm.a "$stage/lib/"
cp -r include/eosllm "$stage/include/"
cp LICENSE README.md CHANGELOG.md SECURITY.md CONTRIBUTING.md \
"$stage/share/doc/eosllm/"
mkdir -p dist
(cd staging && tar czf "../dist/eosllm-${v}-linux-aarch64.tar.gz" \
"eosllm-${v}-linux-aarch64")
- uses: actions/upload-artifact@v4
with:
name: eosllm-linux-aarch64
path: dist/*.tar.gz
if-no-files-found: error
# ------------------------------------------------------------------
# macOS universal (arm64 + x86_64), codesigned + notarized when
# Apple credentials are present.
# ------------------------------------------------------------------
build-macos-universal:
name: build macos universal
needs: resolve-version
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Build (arm64 + x86_64 → lipo)
run: |
v="${{ needs.resolve-version.outputs.version }}"
stage="staging/eosllm-${v}-macos-universal"
chmod +x tools/release/build_macos_universal.sh
tools/release/build_macos_universal.sh "$stage"
- name: Codesign + notarize (skipped if Apple secrets missing)
if: ${{ env.APPLE_DEVELOPER_ID_CERT_P12_B64 != '' }}
env:
APPLE_DEVELOPER_ID_CERT_P12_B64: ${{ secrets.APPLE_DEVELOPER_ID_CERT_P12_B64 }}
APPLE_DEVELOPER_ID_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
APPLE_NOTARY_KEY_P8_B64: ${{ secrets.APPLE_NOTARY_KEY_P8_B64 }}
run: |
v="${{ needs.resolve-version.outputs.version }}"
chmod +x tools/release/sign_and_notarize_macos.sh
tools/release/sign_and_notarize_macos.sh \
"staging/eosllm-${v}-macos-universal/bin"
- name: Stage tarball
run: |
v="${{ needs.resolve-version.outputs.version }}"
mkdir -p dist
(cd staging && tar czf "../dist/eosllm-${v}-macos-universal.tar.gz" \
"eosllm-${v}-macos-universal")
- uses: actions/upload-artifact@v4
with:
name: eosllm-macos-universal
path: dist/*.tar.gz
if-no-files-found: error
# ------------------------------------------------------------------
# Windows x86_64 — MinGW-w64 (msys2/mingw64 environment), Authenticode
# signed via signtool when Windows cert secret is present.
# ------------------------------------------------------------------
build-windows-x86_64:
name: build windows x86_64
needs: resolve-version
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
- uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
base-devel
git
mingw-w64-x86_64-gcc
mingw-w64-x86_64-make
- name: Build (mingw64, win32 shim, AVX2)
run: |
mingw32-make BUILD=release \
EOSLLM_HAVE_WIN32=1 \
EOSLLM_HAVE_POSIX=0 \
EOSLLM_HAVE_KERNEL_AVX2=1 \
CFLAGS_EXTRA="-DWIN32_LEAN_AND_MEAN" \
LDFLAGS_EXTRA="-static -static-libgcc -lws2_32" \
lib tools
- name: Smoke test
run: |
./tools/eosllm-cli/eosllm-cli --version
./tools/eosllm-cli/eosllm-cli --caps | head -5
- name: Stage zip
shell: pwsh
run: |
$v = "${{ needs.resolve-version.outputs.version }}"
$stage = "staging/eosllm-${v}-windows-x86_64"
New-Item -ItemType Directory -Force -Path "$stage/bin","$stage/lib","$stage/include/eosllm","$stage/share/doc/eosllm" | Out-Null
Copy-Item tools/eosllm-cli/eosllm-cli.exe "$stage/bin/"
Copy-Item tools/eosllm-bench/eosllm-bench.exe "$stage/bin/" -ErrorAction SilentlyContinue
Copy-Item tools/eosllm-convert/eosllm-convert.exe "$stage/bin/" -ErrorAction SilentlyContinue
Copy-Item tools/eosllm-server/eosllm-server.exe "$stage/bin/"
Copy-Item libeosllm.a "$stage/lib/"
Copy-Item -Recurse include/eosllm/* "$stage/include/eosllm/"
Copy-Item LICENSE,README.md,CHANGELOG.md,SECURITY.md,CONTRIBUTING.md "$stage/share/doc/eosllm/"
New-Item -ItemType Directory -Force -Path dist | Out-Null
- name: Authenticode sign (skipped if Windows cert missing)
if: ${{ env.WINDOWS_CERT_PFX_B64 != '' }}
shell: pwsh
env:
WINDOWS_CERT_PFX_B64: ${{ secrets.WINDOWS_CERT_PFX_B64 }}
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
run: |
$v = "${{ needs.resolve-version.outputs.version }}"
pwsh -File tools/release/sign_windows.ps1 -StagingDir "staging/eosllm-${v}-windows-x86_64/bin"
- name: Zip
shell: pwsh
run: |
$v = "${{ needs.resolve-version.outputs.version }}"
Compress-Archive -Path "staging/eosllm-${v}-windows-x86_64" -DestinationPath "dist/eosllm-${v}-windows-x86_64.zip"
- uses: actions/upload-artifact@v4
with:
name: eosllm-windows-x86_64
path: dist/*.zip
if-no-files-found: error
build-windows-arm64:
name: build windows arm64
needs: resolve-version
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Configure (cmake, ARM64)
shell: pwsh
run: |
if (Test-Path CMakeLists.txt) {
cmake -B build -A ARM64 -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --parallel
} else {
Write-Host "::warning::No CMakeLists.txt found at root; eosllm uses Makefile."
Write-Host "::warning::Native MSVC ARM64 build of make-based project not yet supported; skipping."
New-Item -ItemType Directory -Force -Path dist | Out-Null
New-Item -ItemType File -Force -Path dist/eosllm-${{ needs.resolve-version.outputs.version }}-windows-arm64-PLACEHOLDER.txt | Out-Null
}
- uses: actions/upload-artifact@v4
with:
name: eosllm-windows-arm64
path: dist/*
if-no-files-found: warn
build-linux-arm32:
name: build linux arm32 (armhf)
needs: resolve-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cross toolchain and qemu
run: |
sudo apt-get update
sudo apt-get install -y gcc-arm-linux-gnueabihf qemu-user-static
- name: Build for armhf (static)
run: |
make CC=arm-linux-gnueabihf-gcc BUILD=release \
CFLAGS_EXTRA="-static" LDFLAGS_EXTRA="-static" \
lib tools
- name: Stage tarball
run: |
v="${{ needs.resolve-version.outputs.version }}"
stage="staging/eosllm-${v}-linux-armhf"
mkdir -p "$stage/bin" "$stage/lib" "$stage/include" "$stage/share/doc/eosllm"
cp tools/eosllm-cli/eosllm-cli "$stage/bin/"
cp tools/eosllm-bench/eosllm-bench "$stage/bin/" || true
cp tools/eosllm-server/eosllm-server "$stage/bin/"
cp libeosllm.a "$stage/lib/"
cp -r include/eosllm "$stage/include/"
cp LICENSE README.md CHANGELOG.md SECURITY.md CONTRIBUTING.md \
"$stage/share/doc/eosllm/" 2>/dev/null || true
mkdir -p dist
(cd staging && tar czf "../dist/eosllm-${v}-linux-armhf.tar.gz" \
"eosllm-${v}-linux-armhf")
- uses: actions/upload-artifact@v4
with:
name: eosllm-linux-armhf
path: dist/*.tar.gz
if-no-files-found: error
# ------------------------------------------------------------------
# Source tarball (lifted verbatim from the previous workflow).
# ------------------------------------------------------------------
build-source-tarball:
name: build source tarball
needs: resolve-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: git archive
run: |
v="${{ needs.resolve-version.outputs.version }}"
mkdir -p dist
git archive --format=tar.gz --prefix="eosllm-${v}/" \
-o "dist/eosllm-${v}-src.tar.gz" HEAD
- uses: actions/upload-artifact@v4
with:
name: eosllm-src
path: dist/*.tar.gz
if-no-files-found: error
# ------------------------------------------------------------------
# WASM build is on the deferred roadmap (Phase F). Tracking issue:
# https://github.com/embeddedos-org/eosllm/issues — see CHANGELOG.md
# under [Unreleased]. The previous `if: false` stub job has been
# removed because actionlint surfaced it as dead config.
# ------------------------------------------------------------------
# ------------------------------------------------------------------
# Aggregator: download every platform's artifact, compute SHA256SUMS,
# publish to the GitHub Release. The actual gh-release step is gated
# on startsWith(github.ref, 'refs/tags/v') so workflow_dispatch dry
# runs build everything but don't publish.
# ------------------------------------------------------------------
publish-release:
name: publish release
needs:
- resolve-version
- build-linux-x86_64
- build-linux-aarch64
- build-linux-arm32
- build-macos-universal
- build-windows-x86_64
- build-windows-arm64
- build-source-tarball
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Flatten artifacts into dist/
run: |
mkdir -p dist
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec cp -v {} dist/ \;
ls -la dist/
- name: Compute SHA256SUMS
run: |
(cd dist && sha256sum * > SHA256SUMS)
cat dist/SHA256SUMS
- name: Generate CycloneDX SBOM
uses: anchore/sbom-action@v0
with:
path: .
format: cyclonedx-json
output-file: dist/sbom.cdx.json
artifact-name: sbom.cdx.json
- name: Generate SPDX SBOM
uses: anchore/sbom-action@v0
with:
path: .
format: spdx-json
output-file: dist/sbom.spdx.json
artifact-name: sbom.spdx.json
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: cosign keyless sign per-artifact
run: |
set -e
cd dist
for f in *.tar.gz *.zip; do
[ -f "$f" ] || continue
cosign sign-blob --yes --bundle "${f}.sig.bundle" "$f"
done
- name: minisign artifacts (skipped if MINISIGN_KEY missing)
if: ${{ env.MINISIGN_KEY != '' }}
env:
MINISIGN_KEY: ${{ secrets.MINISIGN_KEY }}
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
run: |
sudo apt-get update && sudo apt-get install -y minisign
KEY_PATH="$(mktemp)"
printf '%s' "$MINISIGN_KEY" > "$KEY_PATH"
(cd dist && \
echo "$MINISIGN_PASSWORD" | minisign -S -s "$KEY_PATH" -m SHA256SUMS)
rm -f "$KEY_PATH"
ls -la dist/SHA256SUMS*
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
name: eosllm ${{ needs.resolve-version.outputs.version }}
body: |
See [CHANGELOG.md](https://github.com/embeddedos-org/eosllm/blob/v${{ needs.resolve-version.outputs.version }}/CHANGELOG.md) for the full notes.
## Verification
```
sha256sum --check SHA256SUMS
```
If `SHA256SUMS.minisig` is present, you can also verify with
[minisign](https://jedisct1.github.io/minisign/) using the
release-signing public key documented in
[SECURITY.md](https://github.com/embeddedos-org/eosllm/blob/v${{ needs.resolve-version.outputs.version }}/SECURITY.md):
```
minisign -V -p eosllm-release.pub -m SHA256SUMS
```
## Platform notes
- **macOS**: universal arm64+x86_64 binary. If
`SHA256SUMS` references the binary as **unsigned**, the
maintainer's Apple Developer ID was not present at build
time; see [docs/release.md](https://github.com/embeddedos-org/eosllm/blob/v${{ needs.resolve-version.outputs.version }}/docs/release.md#unsigned-binaries) for the
gatekeeper-bypass procedure.
- **Windows**: Authenticode-signed when shipped; otherwise
SmartScreen will warn on first launch.
- **Linux aarch64**: built with `gcc-aarch64-linux-gnu`
and validated under `qemu-user-static` on every release.
## Reproducible build
From the source tarball:
```
tar xzf eosllm-${{ needs.resolve-version.outputs.version }}-src.tar.gz
cd eosllm-${{ needs.resolve-version.outputs.version }}
make all-checks # full ~5 min health gate
```
files: |
dist/*
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}