Skip to content

chore: prepare release 20260415.01 #138

chore: prepare release 20260415.01

chore: prepare release 20260415.01 #138

Workflow file for this run

name: Build Packages
on:
push:
tags:
- "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].[0-9][0-9]"
workflow_dispatch:
inputs:
dry_run:
description: "Build release packages without publishing artifacts, images, or release assets"
required: false
default: false
type: boolean
pre_release:
description: "Publish as pre-release (builds and uploads artifacts but skips Homebrew tap, deploy templates, and releases.json updates)"
required: false
default: false
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
env:
NIGHTLY_TOOLCHAIN: nightly-2025-11-30
RELEASE_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && 'true' || 'false' }}
RELEASE_PRE_RELEASE: ${{ github.event_name == 'workflow_dispatch' && inputs.pre_release && 'true' || 'false' }}
jobs:
zizmor:
name: Workflow Security
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Dry-run short-circuit
if: ${{ env.RELEASE_DRY_RUN == 'true' }}
run: echo "Dry run enabled, skipping workflow security checks"
- uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
advanced-security: false
online-audits: false
validate-tag:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
name: Validate Tag Format
permissions:
contents: read
steps:
- name: Check tag matches YYYYMMDD.NN
env:
TAG: ${{ github.ref_name }}
run: |
if [[ ! "$TAG" =~ ^[0-9]{8}\.[0-9]{2}$ ]]; then
echo "::error::Tag '$TAG' must match YYYYMMDD.NN format"
exit 1
fi
biome:
needs: zizmor
runs-on: ubuntu-latest
name: Biome
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Dry-run short-circuit
if: ${{ env.RELEASE_DRY_RUN == 'true' }}
run: echo "Dry run enabled, skipping biome checks"
- uses: biomejs/setup-biome@29711cbb52afee00eb13aeb30636592f9edc0088 # v2.7.0
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
version: "2.4.6"
- if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: biome ci crates/web/src/assets/js/
- if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: ./scripts/i18n-check.sh
fmt:
needs: zizmor
runs-on: ubuntu-latest
name: Format
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Dry-run short-circuit
if: ${{ env.RELEASE_DRY_RUN == 'true' }}
run: echo "Dry run enabled, skipping rustfmt check"
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
toolchain: ${{ env.NIGHTLY_TOOLCHAIN }}
components: rustfmt
- if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: cargo fmt --all -- --check
clippy:
needs: [fmt, biome]
runs-on: [self-hosted, Linux, X64]
container:
image: nvidia/cuda:12.4.1-devel-ubuntu22.04
env:
LD_LIBRARY_PATH: /usr/local/cuda/compat:/usr/local/nvidia/lib:/usr/local/nvidia/lib64
name: Clippy
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Dry-run short-circuit
if: ${{ env.RELEASE_DRY_RUN == 'true' }}
run: echo "Dry run enabled, skipping clippy job"
- name: Clean up corrupted cargo config
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: rm -f ~/.cargo/config.toml
- name: Install build dependencies
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: |
apt-get update
apt-get install -y curl git cmake build-essential clang libclang-dev pkg-config ca-certificates wget gpg
wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc | tee /etc/apt/trusted.gpg.d/lunarg.asc >/dev/null
echo "deb https://packages.lunarg.com/vulkan jammy main" | tee /etc/apt/sources.list.d/lunarg-vulkan-jammy.list
apt-get update
apt-get install -y vulkan-sdk
nvcc --version
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
toolchain: ${{ env.NIGHTLY_TOOLCHAIN }}
components: clippy, rustfmt
- name: Initialize git repo in llama-cpp source to satisfy cmake
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: |
cargo fetch --locked
LLAMA_SRC=$(find ~/.cargo/registry/src -name "llama-cpp-sys-2-*" -type d 2>/dev/null | head -1)
if [ -n "$LLAMA_SRC" ] && [ ! -d "$LLAMA_SRC/.git" ]; then
cd "$LLAMA_SRC"
git init
git config user.email "ci@example.com"
git config user.name "CI"
git add -A
git commit -m "init" --allow-empty
git tag v0.0.0
fi
- name: Build Tailwind CSS
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: |
./scripts/download-tailwindcss-cli.sh tailwindcss-linux-x64
cd crates/web/ui && TAILWINDCSS=../../../tailwindcss-linux-x64 ./build.sh
- if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: cargo clippy -Z unstable-options --workspace --all-features --all-targets --timings -- -D warnings
test:
needs: [fmt, biome]
runs-on: ubuntu-latest
name: Test
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Dry-run short-circuit
if: ${{ env.RELEASE_DRY_RUN == 'true' }}
run: echo "Dry run enabled, skipping unit test job"
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
toolchain: stable
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
node-version: "22"
package-manager-cache: false
- name: Install QMD CLI
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: |
npm install -g @tobilu/qmd
qmd --version
- uses: taiki-e/install-action@f176c07a0a40cbfdd08ee9aa8bf1655701d11e69 # v2.67.25
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
tool: cargo-nextest
- name: Build Tailwind CSS
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: |
./scripts/download-tailwindcss-cli.sh tailwindcss-linux-x64
cd crates/web/ui && TAILWINDCSS=../../../tailwindcss-linux-x64 ./build.sh
- if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: cargo nextest run --profile ci
e2e:
name: E2E Tests
needs: [fmt, biome]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Dry-run short-circuit
if: ${{ env.RELEASE_DRY_RUN == 'true' }}
run: echo "Dry run enabled, skipping E2E job"
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
toolchain: stable
- name: Build Tailwind CSS
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: |
./scripts/download-tailwindcss-cli.sh tailwindcss-linux-x64
cd crates/web/ui && TAILWINDCSS=../../../tailwindcss-linux-x64 ./build.sh
- name: Build moltis binary
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
run: cargo build --bin moltis
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
with:
node-version: "22"
package-manager-cache: false
- name: Install npm dependencies
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
working-directory: crates/web/ui
run: npm ci
- name: Install Playwright browsers
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
working-directory: crates/web/ui
run: npx playwright install --with-deps chromium
- name: Run E2E tests
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
working-directory: crates/web/ui
env:
CI: "true"
run: npx playwright test
- name: Upload test results
if: ${{ !cancelled() && env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: e2e-test-results-${{ github.run_id }}-${{ github.run_attempt }}
path: |
crates/web/ui/playwright-report/
crates/web/ui/test-results/
if-no-files-found: ignore
retention-days: 14
build-deb:
needs: [clippy, test, e2e]
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
arch: amd64
os: ubuntu-22.04
- target: aarch64-unknown-linux-gnu
arch: arm64
os: ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
name: Build .deb (${{ matrix.arch }})
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Resolve release version
id: release_version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: stable
targets: ${{ matrix.target }}, wasm32-wasip2
- name: Install cargo-deb
run: cargo install cargo-deb
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Build Tailwind CSS
run: |
ARCH=$(uname -m)
case "$ARCH" in x86_64) TW="tailwindcss-linux-x64";; aarch64) TW="tailwindcss-linux-arm64";; esac
./scripts/download-tailwindcss-cli.sh "$TW"
cd crates/web/ui && TAILWINDCSS="../../../$TW" ./build.sh
- name: Build WASM components
run: |
cargo build --target wasm32-wasip2 -p moltis-wasm-calc -p moltis-wasm-web-fetch -p moltis-wasm-web-search --release
cargo run -p moltis-wasm-precompile --release
- name: Build release binary
env:
BUILD_TARGET: ${{ matrix.target }}
MOLTIS_VERSION: ${{ steps.release_version.outputs.version }}
run: cargo build --release --target "$BUILD_TARGET"
- name: Stage WASM assets for cargo-deb
env:
BUILD_TARGET: ${{ matrix.target }}
run: bash ./scripts/stage-wasm-package-assets.sh "target/$BUILD_TARGET/release"
- name: Build .deb package
env:
BUILD_TARGET: ${{ matrix.target }}
MOLTIS_VERSION: ${{ steps.release_version.outputs.version }}
run: cargo deb -p moltis --no-build --target "$BUILD_TARGET" --deb-version "$MOLTIS_VERSION"
- name: Sign with Sigstore and generate checksums
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: ./.github/actions/sign-artifacts
with:
files: '*.deb'
working-directory: target/${{ matrix.target }}/debian
- name: Upload .deb artifact
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: moltis-${{ matrix.arch }}.deb
path: |
target/${{ matrix.target }}/debian/*.deb
target/${{ matrix.target }}/debian/*.sha256
target/${{ matrix.target }}/debian/*.sha512
target/${{ matrix.target }}/debian/*.sig
target/${{ matrix.target }}/debian/*.crt
build-rpm:
needs: [clippy, test, e2e]
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
arch: x86_64
os: ubuntu-22.04
- target: aarch64-unknown-linux-gnu
arch: aarch64
os: ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
name: Build .rpm (${{ matrix.arch }})
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Resolve release version
id: release_version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: stable
targets: ${{ matrix.target }}, wasm32-wasip2
- name: Install cargo-generate-rpm
run: cargo install cargo-generate-rpm
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Build Tailwind CSS
run: |
ARCH=$(uname -m)
case "$ARCH" in x86_64) TW="tailwindcss-linux-x64";; aarch64) TW="tailwindcss-linux-arm64";; esac
./scripts/download-tailwindcss-cli.sh "$TW"
cd crates/web/ui && TAILWINDCSS="../../../$TW" ./build.sh
- name: Build WASM components
run: |
cargo build --target wasm32-wasip2 -p moltis-wasm-calc -p moltis-wasm-web-fetch -p moltis-wasm-web-search --release
cargo run -p moltis-wasm-precompile --release
- name: Build release binary
env:
BUILD_TARGET: ${{ matrix.target }}
MOLTIS_VERSION: ${{ steps.release_version.outputs.version }}
run: cargo build --release --target "$BUILD_TARGET"
- name: Build .rpm package
env:
BUILD_TARGET: ${{ matrix.target }}
MOLTIS_VERSION: ${{ steps.release_version.outputs.version }}
run: cargo generate-rpm -p crates/cli --target "$BUILD_TARGET" --set-metadata="version=\"$MOLTIS_VERSION\""
- name: Sign with Sigstore and generate checksums
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: ./.github/actions/sign-artifacts
with:
files: '*.rpm'
working-directory: target/${{ matrix.target }}/generate-rpm
- name: Upload .rpm artifact
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: moltis-${{ matrix.arch }}.rpm
path: |
target/${{ matrix.target }}/generate-rpm/*.rpm
target/${{ matrix.target }}/generate-rpm/*.sha256
target/${{ matrix.target }}/generate-rpm/*.sha512
target/${{ matrix.target }}/generate-rpm/*.sig
target/${{ matrix.target }}/generate-rpm/*.crt
build-arch:
needs: [clippy, test, e2e]
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
arch: x86_64
os: ubuntu-22.04
- target: aarch64-unknown-linux-gnu
arch: aarch64
os: ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
name: Build .pkg.tar.zst (${{ matrix.arch }})
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: stable
targets: ${{ matrix.target }}, wasm32-wasip2
- name: Install packaging tools
run: sudo apt-get update && sudo apt-get install -y fakeroot zstd
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Build Tailwind CSS
run: |
ARCH=$(uname -m)
case "$ARCH" in x86_64) TW="tailwindcss-linux-x64";; aarch64) TW="tailwindcss-linux-arm64";; esac
./scripts/download-tailwindcss-cli.sh "$TW"
cd crates/web/ui && TAILWINDCSS="../../../$TW" ./build.sh
- name: Build WASM components
run: |
cargo build --target wasm32-wasip2 -p moltis-wasm-calc -p moltis-wasm-web-fetch -p moltis-wasm-web-search --release
cargo run -p moltis-wasm-precompile --release
- name: Determine package version
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build release binary
env:
BUILD_TARGET: ${{ matrix.target }}
MOLTIS_VERSION: ${{ steps.version.outputs.version }}
run: cargo build --release --target "$BUILD_TARGET"
- name: Build .pkg.tar.zst
env:
VERSION: ${{ steps.version.outputs.version }}
BUILD_TARGET: ${{ matrix.target }}
MATRIX_ARCH: ${{ matrix.arch }}
run: |
PKG_DIR="pkg-root"
mkdir -p "$PKG_DIR/usr/bin" "$PKG_DIR/usr/share/moltis/web" "$PKG_DIR/usr/share/moltis/wasm"
cp "target/$BUILD_TARGET/release/moltis" "$PKG_DIR/usr/bin/moltis"
chmod 755 "$PKG_DIR/usr/bin/moltis"
cp -R crates/web/src/assets/. "$PKG_DIR/usr/share/moltis/web/"
cp "target/wasm32-wasip2/release/moltis_wasm_calc.wasm" "$PKG_DIR/usr/share/moltis/wasm/"
cp "target/wasm32-wasip2/release/moltis_wasm_web_fetch.wasm" "$PKG_DIR/usr/share/moltis/wasm/"
cp "target/wasm32-wasip2/release/moltis_wasm_web_search.wasm" "$PKG_DIR/usr/share/moltis/wasm/"
cat > "$PKG_DIR/.PKGINFO" <<PKGINFO
pkgname = moltis
pkgver = ${VERSION}-1
pkgdesc = Personal AI gateway inspired by OpenClaw
url = https://www.moltis.org/
arch = $MATRIX_ARCH
license = MIT
PKGINFO
cd "$PKG_DIR"
fakeroot -- tar --zstd -cf "../moltis-${VERSION}-1-${MATRIX_ARCH}.pkg.tar.zst" .PKGINFO usr/
- name: Sign with Sigstore and generate checksums
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: ./.github/actions/sign-artifacts
with:
files: moltis-${{ steps.version.outputs.version }}-1-${{ matrix.arch }}.pkg.tar.zst
- name: Upload .pkg.tar.zst artifact
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: moltis-${{ matrix.arch }}.pkg.tar.zst
path: |
*.pkg.tar.zst
*.pkg.tar.zst.sha256
*.pkg.tar.zst.sha512
*.pkg.tar.zst.sig
*.pkg.tar.zst.crt
build-appimage:
needs: [clippy, test, e2e]
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
arch: x86_64
os: ubuntu-22.04
appimagetool_arch: x86_64
- target: aarch64-unknown-linux-gnu
arch: aarch64
os: ubuntu-24.04-arm
appimagetool_arch: aarch64
runs-on: ${{ matrix.os }}
name: Build AppImage (${{ matrix.arch }})
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: stable
targets: ${{ matrix.target }}, wasm32-wasip2
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Build Tailwind CSS
run: |
ARCH=$(uname -m)
case "$ARCH" in x86_64) TW="tailwindcss-linux-x64";; aarch64) TW="tailwindcss-linux-arm64";; esac
./scripts/download-tailwindcss-cli.sh "$TW"
cd crates/web/ui && TAILWINDCSS="../../../$TW" ./build.sh
- name: Build WASM components
run: |
cargo build --target wasm32-wasip2 -p moltis-wasm-calc -p moltis-wasm-web-fetch -p moltis-wasm-web-search --release
cargo run -p moltis-wasm-precompile --release
- name: Determine package version
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build release binary
env:
BUILD_TARGET: ${{ matrix.target }}
MOLTIS_VERSION: ${{ steps.version.outputs.version }}
run: cargo build --release --target "$BUILD_TARGET"
- name: Build AppImage
env:
VERSION: ${{ steps.version.outputs.version }}
BUILD_TARGET: ${{ matrix.target }}
MATRIX_ARCH: ${{ matrix.arch }}
APPIMAGETOOL_ARCH: ${{ matrix.appimagetool_arch }}
run: |
# Download appimagetool matching the runner architecture
wget -q "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${APPIMAGETOOL_ARCH}.AppImage" -O appimagetool
chmod +x appimagetool
# Build AppDir structure
APP_DIR="moltis.AppDir"
mkdir -p "$APP_DIR/usr/bin" "$APP_DIR/usr/share/moltis/web" "$APP_DIR/usr/share/moltis/wasm"
cp "target/$BUILD_TARGET/release/moltis" "$APP_DIR/usr/bin/moltis"
chmod 755 "$APP_DIR/usr/bin/moltis"
cp -R crates/web/src/assets/. "$APP_DIR/usr/share/moltis/web/"
cp "target/wasm32-wasip2/release/moltis_wasm_calc.wasm" "$APP_DIR/usr/share/moltis/wasm/"
cp "target/wasm32-wasip2/release/moltis_wasm_web_fetch.wasm" "$APP_DIR/usr/share/moltis/wasm/"
cp "target/wasm32-wasip2/release/moltis_wasm_web_search.wasm" "$APP_DIR/usr/share/moltis/wasm/"
# Create .desktop file
cat > "$APP_DIR/moltis.desktop" <<DESKTOP
[Desktop Entry]
Type=Application
Name=Moltis
Exec=moltis
Icon=moltis
Categories=Network;
Terminal=true
DESKTOP
# Create minimal icon
cat > "$APP_DIR/moltis.svg" <<SVG
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256"><rect width="256" height="256" fill="#333"/><text x="128" y="140" font-size="120" text-anchor="middle" fill="white">M</text></svg>
SVG
ln -s moltis.svg "$APP_DIR/.DirIcon"
# Create AppRun
cat > "$APP_DIR/AppRun" <<'APPRUN'
#!/bin/sh
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export MOLTIS_SHARE_DIR="$HERE/usr/share/moltis"
exec "$HERE/usr/bin/moltis" "$@"
APPRUN
chmod +x "$APP_DIR/AppRun"
# Package - use --appimage-extract-and-run to avoid FUSE requirement in CI
ARCH="$MATRIX_ARCH" ./appimagetool --appimage-extract-and-run "$APP_DIR" "moltis-${VERSION}-${MATRIX_ARCH}.AppImage"
- name: Sign with Sigstore and generate checksums
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: ./.github/actions/sign-artifacts
with:
files: moltis-${{ steps.version.outputs.version }}-${{ matrix.arch }}.AppImage
- name: Upload AppImage artifact
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: moltis-${{ matrix.arch }}.AppImage
path: |
*.AppImage
*.AppImage.sha256
*.AppImage.sha512
*.AppImage.sig
*.AppImage.crt
build-snap:
needs: [clippy, test, e2e]
runs-on: ubuntu-latest
name: Build Snap
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Resolve release version
id: release_version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Sync snap version to release version
env:
VERSION: ${{ steps.release_version.outputs.version }}
run: |
sed -Ei "s/^version:[[:space:]]*'.*'/version: '${VERSION}'/" snap/snapcraft.yaml
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1
id: build-snap
- name: Sign with Sigstore and generate checksums
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: ./.github/actions/sign-artifacts
with:
files: ${{ steps.build-snap.outputs.snap }}
- name: Upload Snap artifact
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: moltis.snap
path: |
${{ steps.build-snap.outputs.snap }}
${{ steps.build-snap.outputs.snap }}.sha256
${{ steps.build-snap.outputs.snap }}.sha512
${{ steps.build-snap.outputs.snap }}.sig
${{ steps.build-snap.outputs.snap }}.crt
build-homebrew-binaries:
needs: [clippy, test, e2e]
strategy:
matrix:
include:
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-unknown-linux-gnu
os: ubuntu-22.04
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
name: Build binary (${{ matrix.target }})
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: ${{ env.NIGHTLY_TOOLCHAIN }}
targets: ${{ matrix.target }}, wasm32-wasip2
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Build Tailwind CSS
run: |
OS=$(uname -s)
ARCH=$(uname -m)
case "${OS}-${ARCH}" in
Linux-x86_64) TW="tailwindcss-linux-x64";;
Linux-aarch64) TW="tailwindcss-linux-arm64";;
Darwin-arm64) TW="tailwindcss-macos-arm64";;
Darwin-x86_64) TW="tailwindcss-macos-x64";;
esac
./scripts/download-tailwindcss-cli.sh "$TW"
cd crates/web/ui && TAILWINDCSS="../../../$TW" ./build.sh
- name: Build WASM components
run: |
cargo build --target wasm32-wasip2 -p moltis-wasm-calc -p moltis-wasm-web-fetch -p moltis-wasm-web-search --release
cargo run -p moltis-wasm-precompile --release
- name: Determine package version
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build release binary
env:
BUILD_TARGET: ${{ matrix.target }}
MOLTIS_VERSION: ${{ steps.version.outputs.version }}
run: cargo build --release --target "$BUILD_TARGET"
- name: Package binary
env:
VERSION: ${{ steps.version.outputs.version }}
BUILD_TARGET: ${{ matrix.target }}
run: |
PKG_DIR="moltis-package"
mkdir -p "$PKG_DIR/share/moltis/web" "$PKG_DIR/share/moltis/wasm"
cp "target/$BUILD_TARGET/release/moltis" "$PKG_DIR/moltis"
cp -R crates/web/src/assets/. "$PKG_DIR/share/moltis/web/"
cp "target/wasm32-wasip2/release/moltis_wasm_calc.wasm" "$PKG_DIR/share/moltis/wasm/"
cp "target/wasm32-wasip2/release/moltis_wasm_web_fetch.wasm" "$PKG_DIR/share/moltis/wasm/"
cp "target/wasm32-wasip2/release/moltis_wasm_web_search.wasm" "$PKG_DIR/share/moltis/wasm/"
tar czf "moltis-${VERSION}-${BUILD_TARGET}.tar.gz" -C "$PKG_DIR" moltis share
- name: Sign with Sigstore and generate checksums
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: ./.github/actions/sign-artifacts
with:
files: moltis-${{ steps.version.outputs.version }}-${{ matrix.target }}.tar.gz
- name: Upload binary artifact
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: moltis-${{ matrix.target }}
path: |
*.tar.gz
*.tar.gz.sha256
*.tar.gz.sha512
*.tar.gz.sig
*.tar.gz.crt
build-macos-app:
needs: [clippy, test, e2e]
runs-on: macos-latest
name: Build macOS app
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: stable
- name: Install Swift build dependencies
run: brew install xcodegen cbindgen
- name: Build Tailwind CSS
run: |
./scripts/download-tailwindcss-cli.sh tailwindcss-macos-arm64
cd crates/web/ui && TAILWINDCSS=../../../tailwindcss-macos-arm64 ./build.sh
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Determine package version
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build Swift bridge artifacts and generate Xcode project
env:
MOLTIS_VERSION: ${{ steps.version.outputs.version }}
run: |
./scripts/build-swift-bridge.sh
./scripts/generate-swift-project.sh
- name: Build macOS app
env:
DERIVED_DATA_DIR: apps/macos/.derivedData-ci
run: |
xcodebuild \
-project apps/macos/Moltis.xcodeproj \
-scheme Moltis \
-configuration Release \
-destination "platform=macOS" \
-derivedDataPath "$DERIVED_DATA_DIR" \
build
- name: Package macOS app
env:
VERSION: ${{ steps.version.outputs.version }}
APP_PATH: apps/macos/.derivedData-ci/Build/Products/Release/Moltis.app
run: |
if [ ! -d "$APP_PATH" ]; then
echo "expected app bundle not found at $APP_PATH" >&2
exit 1
fi
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "moltis-${VERSION}-macos.app.zip"
- name: Sign with Sigstore and generate checksums
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: ./.github/actions/sign-artifacts
with:
files: moltis-${{ steps.version.outputs.version }}-macos.app.zip
- name: Upload macOS app artifact
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: moltis-macos-app
path: |
*.app.zip
*.app.zip.sha256
*.app.zip.sha512
*.app.zip.sig
*.app.zip.crt
build-windows-exe:
needs: [clippy, test, e2e]
runs-on: windows-latest
name: Build .exe (x86_64)
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
env:
BUILD_TARGET: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: stable
targets: ${{ env.BUILD_TARGET }}, wasm32-wasip2
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Configure Perl for vendored OpenSSL
shell: pwsh
run: |
$strawberryPerl = "C:\Strawberry\perl\bin\perl.exe"
if (-not (Test-Path $strawberryPerl)) {
Write-Error "Strawberry Perl not found at $strawberryPerl"
}
Add-Content -Path $env:GITHUB_ENV -Value "OPENSSL_SRC_PERL=$strawberryPerl"
Add-Content -Path $env:GITHUB_ENV -Value "PERL=$strawberryPerl"
& $strawberryPerl -v
- name: Build Tailwind CSS
shell: bash
run: |
./scripts/download-tailwindcss-cli.sh tailwindcss-windows-x64.exe
cd crates/web/ui && TAILWINDCSS=../../../tailwindcss-windows-x64.exe ./build.sh
- name: Build WASM components
shell: bash
run: |
cargo build --target wasm32-wasip2 -p moltis-wasm-calc -p moltis-wasm-web-fetch -p moltis-wasm-web-search --release
cargo run -p moltis-wasm-precompile --release
- name: Determine package version
id: version
shell: bash
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build release binary
shell: bash
env:
MOLTIS_VERSION: ${{ steps.version.outputs.version }}
run: cargo build --release --target "$BUILD_TARGET" --features embedded-assets,embedded-wasm
- name: Package .exe
shell: bash
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
cp "target/$BUILD_TARGET/release/moltis.exe" "moltis-${VERSION}-${BUILD_TARGET}.exe"
- name: Sign with Sigstore and generate checksums
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: ./.github/actions/sign-artifacts
with:
files: moltis-${{ steps.version.outputs.version }}-${{ env.BUILD_TARGET }}.exe
- name: Upload .exe artifact
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: moltis-${{ env.BUILD_TARGET }}.exe
path: |
*.exe
*.exe.sha256
*.exe.sha512
*.exe.sig
*.exe.crt
build-docker:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
needs: [clippy, test, e2e]
strategy:
matrix:
include:
- platform: linux/amd64
os: ubuntu-latest
- platform: linux/arm64
os: ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
name: Build Docker (${{ matrix.platform }})
permissions:
contents: read
packages: write
id-token: write # Required for Sigstore keyless signing
attestations: write # Required for provenance attestations
outputs:
# Each matrix leg overwrites, but merge job reads digests from artifacts
image: ghcr.io/${{ github.repository }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Resolve release version
id: release_version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6
env:
DOCKER_BUILD_RECORD_UPLOAD: "false"
with:
context: .
platforms: ${{ matrix.platform }}
build-args: MOLTIS_VERSION=${{ steps.release_version.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,"name=ghcr.io/${{ github.repository }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,scope=${{ matrix.platform }},mode=max
sbom: true
provenance: mode=max
- name: Smoke test Docker image startup
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
IMAGE="ghcr.io/${{ github.repository }}@${DIGEST}"
docker run --rm "${IMAGE}" --help > /dev/null
DOCKER_CLI_VERSION="$(docker run --rm --entrypoint docker "${IMAGE}" --version | awk '{print $3}' | tr -d ',')"
DOCKER_CLI_MAJOR="${DOCKER_CLI_VERSION%%.*}"
if [[ -z "${DOCKER_CLI_MAJOR}" || "${DOCKER_CLI_MAJOR}" -lt 25 ]]; then
echo "Docker CLI in image is too old: ${DOCKER_CLI_VERSION} (require >= 25.x; API >= 1.44)" >&2
exit 1
fi
- name: Export digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p /tmp/digests
digest="${DIGEST#sha256:}"
touch "/tmp/digests/${digest}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docker-digests-${{ strategy.job-index }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge-docker:
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
needs: build-docker
runs-on: ubuntu-latest
name: Merge Docker manifest
permissions:
contents: read
packages: write
id-token: write # Required for Sigstore keyless signing
attestations: write # Required for artifact attestations
steps:
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: /tmp/digests
pattern: docker-digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Log in to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Check if this is the highest release tag
id: check_latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CURRENT="${GITHUB_REF_NAME}"
LATEST=$(gh api "repos/${{ github.repository }}/tags" --paginate --jq '.[].name' \
| grep -E '^[0-9]{8}\.[0-9]{2}$' \
| sort | tail -1)
if [ "$CURRENT" = "$LATEST" ]; then
echo "is_latest=true" >> "$GITHUB_OUTPUT"
else
echo "is_latest=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping floating tags: ${CURRENT} is not the highest version (${LATEST} is)"
fi
- name: Extract metadata
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=match,pattern=\d{8}\.\d{2}
type=sha
type=raw,value=latest,enable=${{ steps.check_latest.outputs.is_latest }}
- name: Create multi-arch manifest and push
env:
TAGS: ${{ steps.meta.outputs.tags }}
working-directory: /tmp/digests
run: |
mapfile -t tags < <(jq -r '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
tag_args=()
for tag in "${tags[@]}"; do
tag_args+=("-t" "$tag")
done
image_refs=()
for digest in *; do
image_refs+=("ghcr.io/${{ github.repository }}@sha256:${digest}")
done
docker buildx imagetools create "${tag_args[@]}" "${image_refs[@]}"
- name: Inspect manifest
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
tag=$(echo "$TAGS" | head -1)
docker buildx imagetools inspect "$tag"
- name: Get manifest digest
id: manifest
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
tag=$(echo "$TAGS" | head -1)
digest=$(docker buildx imagetools inspect "$tag" --format '{{json .Manifest}}' | jq -r '.digest')
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
- name: Sign container image with cosign (keyless)
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.manifest.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
mapfile -t tags < <(printf '%s\n' "$TAGS")
images=()
for tag in "${tags[@]}"; do
[ -n "$tag" ] || continue
images+=("${tag}@${DIGEST}")
done
cosign sign --yes "${images[@]}"
- name: Verify container signature
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.manifest.outputs.digest }}
run: |
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}/*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
"ghcr.io/${{ github.repository }}@${DIGEST}"
- name: Attest build provenance for container image
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.manifest.outputs.digest }}
push-to-registry: true
upload-release:
needs:
- build-deb
- build-rpm
- build-arch
- build-appimage
- build-snap
- build-homebrew-binaries
- build-macos-app
- build-windows-exe
if: startsWith(github.ref, 'refs/tags/') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
runs-on: ubuntu-latest
name: Upload release assets
permissions:
contents: write
id-token: write # Required for artifact attestations
attestations: write # Required for artifact attestations
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
sparse-checkout: CHANGELOG.md
sparse-checkout-cone-mode: false
- name: Download all build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: artifacts
pattern: moltis-*
- name: Download snap artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: moltis.snap
path: artifacts/moltis.snap
- name: Collect all release files
run: |
mkdir -p release-files
find artifacts -type f \( \
-name '*.deb' -o -name '*.rpm' -o -name '*.pkg.tar.zst' \
-o -name '*.AppImage' -o -name '*.snap' -o -name '*.tar.gz' -o -name '*.zip' -o -name '*.exe' \
-o -name '*.sha256' -o -name '*.sha512' -o -name '*.sig' -o -name '*.crt' \
\) -exec cp {} release-files/ \;
cp CHANGELOG.md release-files/
echo "Files to upload:"
ls -lh release-files/
- name: Upload to release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
with:
files: release-files/*
prerelease: ${{ env.RELEASE_PRE_RELEASE == 'true' }}
- name: Attest build provenance for release artifacts
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4
with:
subject-path: |
release-files/*.tar.gz
release-files/*.deb
release-files/*.rpm
release-files/*.pkg.tar.zst
release-files/*.AppImage
release-files/*.snap
release-files/*.zip
release-files/*.exe
# Generate SBOM for the entire release
generate-sbom:
needs:
- upload-release
- merge-docker
if: startsWith(github.ref, 'refs/tags/') && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
runs-on: ubuntu-latest
name: Generate Release SBOM
permissions:
contents: write
id-token: write # Required for Sigstore keyless signing
attestations: write # Required for artifact attestations
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install cosign
if: ${{ env.RELEASE_DRY_RUN != 'true' }}
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
- name: Install cargo-sbom
run: cargo install cargo-sbom
- name: Generate SBOM (CycloneDX and SPDX)
run: |
cargo sbom --output-format cyclone_dx_json_1_4 > moltis-sbom.cdx.json
cargo sbom --output-format spdx_json_2_3 > moltis-sbom.spdx.json
- name: Sign SBOMs with Sigstore and generate checksums
uses: ./.github/actions/sign-artifacts
with:
files: moltis-sbom.cdx.json moltis-sbom.spdx.json
skip-sha512: 'true'
- name: Upload SBOM to release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
with:
files: |
moltis-sbom.cdx.json
moltis-sbom.cdx.json.sha256
moltis-sbom.cdx.json.sig
moltis-sbom.cdx.json.crt
moltis-sbom.spdx.json
moltis-sbom.spdx.json.sha256
moltis-sbom.spdx.json.sig
moltis-sbom.spdx.json.crt
- name: Attest build provenance for SBOMs
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4
with:
subject-path: |
moltis-sbom.cdx.json
moltis-sbom.spdx.json
update-homebrew-tap:
needs:
- upload-release
- merge-docker
if: startsWith(github.ref, 'refs/tags/') && !(github.event_name == 'workflow_dispatch' && (inputs.dry_run || inputs.pre_release))
runs-on: ubuntu-latest
name: Update Homebrew tap
permissions:
contents: read
steps:
- name: Check Homebrew tap token
id: token_check
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
set -euo pipefail
if [ -z "${HOMEBREW_TAP_TOKEN}" ]; then
echo "has_token=false" >> "$GITHUB_OUTPUT"
echo "HOMEBREW_TAP_TOKEN is not set; skipping Homebrew tap update"
else
echo "has_token=true" >> "$GITHUB_OUTPUT"
fi
- name: Download release assets and compute SHAs
if: steps.token_check.outputs.has_token == 'true'
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
VERSION="${TAG}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
for target in aarch64-apple-darwin x86_64-apple-darwin aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu; do
ASSET="moltis-${VERSION}-${target}.tar.gz"
URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${ASSET}"
curl --silent --show-error --fail --location "$URL" -o "$ASSET"
SHA=$(sha256sum "$ASSET" | cut -d' ' -f1)
VAR="SHA_$(echo "$target" | tr '-' '_')"
echo "${VAR}=${SHA}" >> "$GITHUB_ENV"
echo "$target: $SHA"
done
- name: Checkout homebrew-tap
if: steps.token_check.outputs.has_token == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: moltis-org/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
persist-credentials: true
- name: Update formula
if: steps.token_check.outputs.has_token == 'true'
run: |
cat > Formula/moltis.rb <<RUBY
class Moltis < Formula
desc "Personal AI gateway - one binary, multiple LLM providers"
homepage "https://www.moltis.org/"
license "MIT"
version "${VERSION}"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/moltis-org/moltis/releases/download/#{version}/moltis-#{version}-aarch64-apple-darwin.tar.gz"
sha256 "${SHA_aarch64_apple_darwin}"
else
url "https://github.com/moltis-org/moltis/releases/download/#{version}/moltis-#{version}-x86_64-apple-darwin.tar.gz"
sha256 "${SHA_x86_64_apple_darwin}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/moltis-org/moltis/releases/download/#{version}/moltis-#{version}-aarch64-unknown-linux-gnu.tar.gz"
sha256 "${SHA_aarch64_unknown_linux_gnu}"
else
url "https://github.com/moltis-org/moltis/releases/download/#{version}/moltis-#{version}-x86_64-unknown-linux-gnu.tar.gz"
sha256 "${SHA_x86_64_unknown_linux_gnu}"
end
end
def install
libexec.install "moltis"
share.install "share/moltis"
(bin/"moltis").write_env_script libexec/"moltis", MOLTIS_SHARE_DIR: share/"moltis"
end
test do
assert_match "moltis", shell_output("#{bin}/moltis --version")
end
end
RUBY
- name: Commit and push
if: steps.token_check.outputs.has_token == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/moltis.rb
if git diff --cached --quiet; then
echo "No Homebrew formula changes to commit"
exit 0
fi
git commit -m "moltis ${VERSION}"
git push
update-deploy-tags:
needs: merge-docker
if: startsWith(github.ref, 'refs/tags/') && !(github.event_name == 'workflow_dispatch' && (inputs.dry_run || inputs.pre_release))
runs-on: ubuntu-latest
name: Update deploy template tags
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
ref: main
- name: Update image tags in deploy templates
env:
TAG: ${{ github.ref_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${TAG}"
echo "Updating deploy templates to version ${VERSION}"
# DigitalOcean App Platform (pattern matches both semver 0.10.18 and date 20260311.01)
sed -i "s/tag: \"[0-9][0-9.]*\"/tag: \"${VERSION}\"/" .do/deploy.template.yaml
# Render
sed -i "s|ghcr.io/moltis-org/moltis:[0-9][0-9.]*|ghcr.io/moltis-org/moltis:${VERSION}|" render.yaml
# Fly.io
sed -i "s|ghcr.io/moltis-org/moltis:[0-9][0-9.]*|ghcr.io/moltis-org/moltis:${VERSION}|" fly.toml
# Website releases.json
jq --arg v "$VERSION" \
'.stable.version = $v | .stable.release_url = "https://github.com/moltis-org/moltis/releases/tag/" + $v' \
website/releases.json > website/releases.json.tmp \
&& mv website/releases.json.tmp website/releases.json
# Check if any files changed
if git diff --quiet .do/deploy.template.yaml render.yaml fly.toml website/releases.json; then
echo "No deploy template changes needed"
exit 0
fi
# Commit via GitHub API so the commit is signed/verified
REPO="${{ github.repository }}"
MAIN_SHA=$(gh api "repos/${REPO}/git/ref/heads/main" --jq '.object.sha')
BASE_TREE=$(gh api "repos/${REPO}/git/commits/${MAIN_SHA}" --jq '.tree.sha')
# Create blobs for each updated file
DO_BLOB=$(gh api "repos/${REPO}/git/blobs" \
-f content="$(base64 -w0 .do/deploy.template.yaml)" \
-f encoding=base64 --jq '.sha')
RENDER_BLOB=$(gh api "repos/${REPO}/git/blobs" \
-f content="$(base64 -w0 render.yaml)" \
-f encoding=base64 --jq '.sha')
FLY_BLOB=$(gh api "repos/${REPO}/git/blobs" \
-f content="$(base64 -w0 fly.toml)" \
-f encoding=base64 --jq '.sha')
RELEASES_BLOB=$(gh api "repos/${REPO}/git/blobs" \
-f content="$(base64 -w0 website/releases.json)" \
-f encoding=base64 --jq '.sha')
# Create a new tree with the updated files (JSON input for proper array structure)
TREE_SHA=$(jq -n \
--arg base "$BASE_TREE" \
--arg do_sha "$DO_BLOB" \
--arg render_sha "$RENDER_BLOB" \
--arg fly_sha "$FLY_BLOB" \
--arg releases_sha "$RELEASES_BLOB" \
'{
base_tree: $base,
tree: [
{path: ".do/deploy.template.yaml", mode: "100644", type: "blob", sha: $do_sha},
{path: "render.yaml", mode: "100644", type: "blob", sha: $render_sha},
{path: "fly.toml", mode: "100644", type: "blob", sha: $fly_sha},
{path: "website/releases.json", mode: "100644", type: "blob", sha: $releases_sha}
]
}' | gh api "repos/${REPO}/git/trees" --input - --jq '.sha')
# Create a signed commit
COMMIT_SHA=$(jq -n \
--arg msg "chore: update deploy templates and releases to ${VERSION}" \
--arg tree "$TREE_SHA" \
--arg parent "$MAIN_SHA" \
'{message: $msg, tree: $tree, parents: [$parent]}' \
| gh api "repos/${REPO}/git/commits" --input - --jq '.sha')
# Update main to point to the new commit
gh api "repos/${REPO}/git/refs/heads/main" \
-X PATCH -f sha="${COMMIT_SHA}"
echo "Updated deploy templates to ${VERSION} (commit ${COMMIT_SHA})"