chore: prepare release 20260415.01 #138
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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})" |