diff --git a/.github/actions/set-version/action.yml b/.github/actions/set-version/action.yml index c5d07c0..c5d1ebf 100644 --- a/.github/actions/set-version/action.yml +++ b/.github/actions/set-version/action.yml @@ -11,4 +11,14 @@ runs: steps: - name: Set version shell: bash - run: bash scripts/ci-set-version.sh "${{ inputs.version }}" + run: | + VERSION="${{ inputs.version }}" + VERSION_CODE="$(printf '%s' "$VERSION" | sed 's/[^0-9]//g')" + if [[ -z "$VERSION_CODE" ]]; then + VERSION_CODE=1 + fi + GRADLE_FILE="build.gradle.kts" + sed -i.bak "s/extra\\[\"versionName\"\\] = \"[^\"]*\"/extra[\"versionName\"] = \"$VERSION\"/" "$GRADLE_FILE" + sed -i.bak "s/extra\\[\"versionCode\"\\] = [0-9]*/extra[\"versionCode\"] = $VERSION_CODE/" "$GRADLE_FILE" + rm -f "$GRADLE_FILE.bak" + echo "versionName=$VERSION versionCode=$VERSION_CODE" diff --git a/.github/workflows/desktop-windows-x64.yml b/.github/workflows/desktop-windows-x64.yml index e864065..cd7fc9c 100644 --- a/.github/workflows/desktop-windows-x64.yml +++ b/.github/workflows/desktop-windows-x64.yml @@ -29,7 +29,8 @@ jobs: with: targets: x86_64-pc-windows-msvc - uses: ./.github/actions/cache-cargo-windows - - name: Set version + - id: ver + name: Resolve version shell: bash run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then @@ -37,7 +38,10 @@ jobs: else V="${GITHUB_REF_NAME#win-x64-}" fi - bash scripts/ci-set-version.sh "$V" + echo "version=$V" >> "$GITHUB_OUTPUT" + - uses: ./.github/actions/set-version + with: + version: ${{ steps.ver.outputs.version }} - name: Package Windows x64 installer shell: bash run: | diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml deleted file mode 100644 index ca095fd..0000000 --- a/.github/workflows/release-smoke.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release smoke - -on: - pull_request: - paths: - - ".github/workflows/release.yml" - - ".github/workflows/release-smoke.yml" - - ".github/actions/**" - - "scripts/ci-*.sh" - - "app/desktop/build.gradle.kts" - workflow_dispatch: - -concurrency: - group: release-smoke-${{ github.ref }} - cancel-in-progress: true - -env: - GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -Dkotlin.daemon.jvm.options=-Xmx6g - -jobs: - test-compile: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-java-gradle - - run: bash scripts/ci-test-compile.sh - - test-rust: - runs-on: windows-latest - timeout-minutes: 25 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: x86_64-pc-windows-msvc - - uses: ./.github/actions/cache-cargo-windows - - name: Test Windows installer Rust workspace - shell: bash - working-directory: app/desktop/windows-setup - run: cargo test --workspace --release - - verify-script: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Syntax-check CI scripts - run: | - set -euo pipefail - for script in scripts/ci-*.sh; do - bash -n "$script" - echo "OK $script" - done - - name: Dry-run asset verification patterns - run: | - V="9.9.9" - bash scripts/ci-create-smoke-fixtures.sh /tmp/release-assets "$V" - sudo apt-get update && sudo apt-get install -y rpm file unzip - CI_SMOKE=1 bash scripts/ci-verify-release-assets.sh "$V" /tmp/release-assets diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 636a3d4..0e59e21 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ on: required: true type: string skip_publish: - description: "Build and verify only; skip GitHub Release upload" + description: "Build only; skip GitHub Release upload" required: false type: boolean default: false @@ -45,36 +45,8 @@ jobs: echo "version=$V" >> "$GITHUB_OUTPUT" echo "tag=v$V" >> "$GITHUB_OUTPUT" - test-compile: - needs: resolve-version - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-java-gradle - - uses: ./.github/actions/set-version - with: - version: ${{ needs.resolve-version.outputs.version }} - - name: Compile JVM release sources - run: bash scripts/ci-test-compile.sh - - test-rust: - needs: resolve-version - runs-on: windows-latest - timeout-minutes: 25 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: x86_64-pc-windows-msvc - - uses: ./.github/actions/cache-cargo-windows - - name: Test Windows installer Rust workspace - shell: bash - working-directory: app/desktop/windows-setup - run: cargo test --workspace --release - windows-common: - needs: [resolve-version, test-compile, test-rust] + needs: resolve-version runs-on: windows-latest timeout-minutes: 45 steps: @@ -87,7 +59,7 @@ jobs: with: targets: x86_64-pc-windows-msvc - uses: ./.github/actions/cache-cargo-windows - - name: Build arch-independent Windows tooling and JVM artifacts + - name: Build arch-independent Windows tooling and compile JVM shell: bash run: | export FROMCHAT_PACKAGING_JDK="$JAVA_HOME" @@ -115,17 +87,13 @@ jobs: include: - arch: x64 runner: windows-latest - java-architecture: "" - arch: arm64 runner: windows-11-arm - java-architecture: "" runs-on: ${{ matrix.runner }} timeout-minutes: 60 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-java-gradle - with: - java-architecture: ${{ matrix.java-architecture }} - uses: ./.github/actions/set-version with: version: ${{ needs.resolve-version.outputs.version }} @@ -140,16 +108,30 @@ jobs: path: app/desktop/windows-setup/target/release - name: Package Windows ${{ matrix.arch }} installer shell: bash - run: bash scripts/ci-package-windows-release.sh "${{ matrix.arch }}" - - name: Upload Windows ${{ matrix.arch }} installer - uses: actions/upload-artifact@v4 + env: + FROMCHAT_SKIP_RUST_BUILD: "1" + run: | + export FROMCHAT_PACKAGING_JDK="$JAVA_HOME" + args=( + :app:desktop:createReleaseDistributable + :app:desktop:patchWindowsJpackageReleaseIcon + :app:desktop:packSetupOnly + "-PdesktopArch=${{ matrix.arch }}" + --no-daemon + --console=plain + ) + if [ "${{ matrix.arch }}" = "arm64" ]; then + args+=("-PwindowsArm64") + fi + ./gradlew "${args[@]}" + - uses: actions/upload-artifact@v4 with: name: windows-${{ matrix.arch }} path: app/desktop/build/distributions/release/*-windows-${{ matrix.arch }}.exe if-no-files-found: error macos: - needs: [resolve-version, test-compile, test-rust] + needs: resolve-version strategy: fail-fast: false matrix: @@ -177,7 +159,15 @@ jobs: key: dmg-tools-${{ hashFiles('app/desktop/dmg-background/package-lock.json') }} restore-keys: dmg-tools- - name: Install macOS packaging tools - run: bash scripts/ci-install-macos-dmg.sh + run: | + if ! command -v create-dmg >/dev/null 2>&1; then + brew install create-dmg + fi + DMG_DIR="app/desktop/dmg-background" + if [[ ! -f "$DMG_DIR/node_modules/playwright/package.json" ]]; then + npm --prefix "$DMG_DIR" install --no-fund --no-audit + npm --prefix "$DMG_DIR" exec playwright install chromium + fi - name: Package macOS ${{ matrix.arch }} run: | export FROMCHAT_PACKAGING_JDK="$JAVA_HOME" @@ -189,15 +179,17 @@ jobs: if-no-files-found: error linux: - needs: [resolve-version, test-compile, test-rust] + needs: resolve-version strategy: fail-fast: false matrix: include: - arch: x64 runner: ubuntu-latest + appimage: appimagetool-x86_64.AppImage - arch: arm64 runner: ubuntu-24.04-arm + appimage: appimagetool-aarch64.AppImage runs-on: ${{ matrix.runner }} timeout-minutes: 75 steps: @@ -207,7 +199,17 @@ jobs: with: version: ${{ needs.resolve-version.outputs.version }} - name: Install Linux packaging tools - run: bash scripts/ci-install-linux-packaging.sh "${{ matrix.arch }}" + run: | + sudo apt-get update + sudo apt-get install -y fakeroot rpm binutils libfuse2 wget + CACHE_DIR="${RUNNER_TOOL_CACHE}/appimagetool" + mkdir -p "$CACHE_DIR" + TOOL="$CACHE_DIR/appimagetool-${{ matrix.arch }}.AppImage" + if [[ ! -f "$TOOL" ]]; then + wget -q "https://github.com/AppImage/appimagetool/releases/download/continuous/${{ matrix.appimage }}" -O "$TOOL" + chmod +x "$TOOL" + fi + echo "APPIMAGETOOL=$TOOL" >> "$GITHUB_ENV" - name: Package Linux ${{ matrix.arch }} run: | export FROMCHAT_PACKAGING_JDK="$JAVA_HOME" @@ -222,7 +224,7 @@ jobs: if-no-files-found: error android: - needs: [resolve-version, test-compile, test-rust] + needs: resolve-version runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -278,7 +280,8 @@ EOF path: release-staging/*.apk if-no-files-found: error - verify: + publish: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.skip_publish == false }} needs: - resolve-version - windows-package @@ -286,10 +289,9 @@ EOF - linux - android runs-on: ubuntu-latest + permissions: + contents: write steps: - - uses: actions/checkout@v4 - - name: Install verification tools - run: sudo apt-get update && sudo apt-get install -y rpm file unzip - uses: actions/download-artifact@v4 with: path: artifacts @@ -305,27 +307,25 @@ EOF -name '*.apk' \ \) -exec cp {} release-assets/ \; ls -la release-assets - - name: Verify release assets - run: bash scripts/ci-verify-release-assets.sh "${{ needs.resolve-version.outputs.version }}" release-assets - - uses: actions/upload-artifact@v4 - with: - name: release-assets - path: release-assets/* - if-no-files-found: error - - publish: - if: ${{ github.event_name != 'workflow_dispatch' || inputs.skip_publish == false }} - needs: - - resolve-version - - verify - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v4 - with: - name: release-assets - path: release-assets + VERSION="${{ needs.resolve-version.outputs.version }}" + for pattern in \ + "*-windows-x64.exe" \ + "*-windows-arm64.exe" \ + "*-macOS-x64.dmg" \ + "*-macOS-arm64.dmg" \ + "*-linux-x64.deb" \ + "*-linux-arm64.deb" \ + "*-linux-x64.rpm" \ + "*-linux-arm64.rpm" \ + "*-linux-x64.AppImage" \ + "*-linux-arm64.AppImage" \ + "*-android-universal.apk" + do + if ! compgen -G "release-assets/$pattern" > /dev/null; then + echo "Missing release asset: $pattern" >&2 + exit 1 + fi + done - name: Publish GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/scripts/ci-create-smoke-fixtures.sh b/scripts/ci-create-smoke-fixtures.sh deleted file mode 100644 index 07d1163..0000000 --- a/scripts/ci-create-smoke-fixtures.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ASSETS_DIR="${1:?usage: ci-create-smoke-fixtures.sh }" -VERSION="${2:?usage: ci-create-smoke-fixtures.sh }" - -mkdir -p "$ASSETS_DIR" - -for name in \ - "FromChat-Setup-${VERSION}-windows-x64.exe" \ - "FromChat-Setup-${VERSION}-windows-arm64.exe" \ - "FromChat-${VERSION}-macOS-x64.dmg" \ - "FromChat-${VERSION}-macOS-arm64.dmg" \ - "FromChat-${VERSION}-linux-x64.deb" \ - "FromChat-${VERSION}-linux-arm64.deb" \ - "FromChat-${VERSION}-linux-x64.rpm" \ - "FromChat-${VERSION}-linux-arm64.rpm" \ - "FromChat-${VERSION}-linux-x64.AppImage" \ - "FromChat-${VERSION}-linux-arm64.AppImage" \ - "FromChat-${VERSION}-android-universal.apk" -do - echo "placeholder" > "$ASSETS_DIR/$name" -done - -python3 - "$ASSETS_DIR" <<'PY' -import struct -import pathlib -import sys -import zipfile - -root = pathlib.Path(sys.argv[1]) -for name, machine in [ - ("FromChat-Setup-9.9.9-windows-x64.exe", 0x8664), - ("FromChat-Setup-9.9.9-windows-arm64.exe", 0xAA64), -]: - path = root / name - data = bytearray(0x80) - struct.pack_into("\n") -for rpm in root.glob("*.rpm"): - rpm.write_bytes(b"\xed\xab\xee\xdb" + b"\x00" * 96) -for appimage in root.glob("*.AppImage"): - appimage.write_bytes(b"\x7fELF" + b"\x00" * 16) -for dmg in root.glob("*.dmg"): - dmg.write_bytes(b"UDIF" + b"\x00" * 32) -PY diff --git a/scripts/ci-install-linux-packaging.sh b/scripts/ci-install-linux-packaging.sh deleted file mode 100644 index edcef83..0000000 --- a/scripts/ci-install-linux-packaging.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ARCH="${1:?usage: ci-install-linux-packaging.sh }" - -sudo apt-get update -sudo apt-get install -y fakeroot rpm binutils libfuse2 wget file - -case "$ARCH" in - x64) - TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" - ;; - arm64) - TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-aarch64.AppImage" - ;; - *) - echo "Unsupported arch: $ARCH" >&2 - exit 1 - ;; -esac - -CACHE_DIR="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}/appimagetool" -mkdir -p "$CACHE_DIR" -TOOL="$CACHE_DIR/appimagetool-${ARCH}.AppImage" -if [[ ! -f "$TOOL" ]]; then - wget -q "$TOOL_URL" -O "$TOOL" - chmod +x "$TOOL" -fi -echo "APPIMAGETOOL=$TOOL" >> "$GITHUB_ENV" diff --git a/scripts/ci-install-macos-dmg.sh b/scripts/ci-install-macos-dmg.sh deleted file mode 100644 index 88dfe82..0000000 --- a/scripts/ci-install-macos-dmg.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if ! command -v create-dmg >/dev/null 2>&1; then - brew install create-dmg -fi - -DMG_DIR="app/desktop/dmg-background" -if [[ ! -f "$DMG_DIR/node_modules/playwright/package.json" ]]; then - npm --prefix "$DMG_DIR" install --no-fund --no-audit - npm --prefix "$DMG_DIR" exec playwright install chromium -fi diff --git a/scripts/ci-package-windows-release.sh b/scripts/ci-package-windows-release.sh deleted file mode 100644 index 8b904e6..0000000 --- a/scripts/ci-package-windows-release.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ARCH="${1:?usage: ci-package-windows-release.sh }" - -export FROMCHAT_PACKAGING_JDK="${JAVA_HOME}" -export FROMCHAT_SKIP_RUST_BUILD=1 - -GRADLE_ARGS=( - :app:desktop:createReleaseDistributable - :app:desktop:patchWindowsJpackageReleaseIcon - :app:desktop:packSetupOnly - "-PdesktopArch=$ARCH" - --no-daemon - --console=plain -) - -if [[ "$ARCH" == "arm64" ]]; then - GRADLE_ARGS+=("-PwindowsArm64") -fi - -./gradlew "${GRADLE_ARGS[@]}" diff --git a/scripts/ci-set-version.sh b/scripts/ci-set-version.sh deleted file mode 100755 index 6d80858..0000000 --- a/scripts/ci-set-version.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -VERSION="${1:?usage: ci-set-version.sh }" -VERSION_CODE="$(printf '%s' "$VERSION" | sed 's/[^0-9]//g')" -if [[ -z "$VERSION_CODE" ]]; then - VERSION_CODE=1 -fi - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -GRADLE_FILE="$ROOT/build.gradle.kts" - -sed -i.bak "s/extra\\[\"versionName\"\\] = \"[^\"]*\"/extra[\"versionName\"] = \"$VERSION\"/" "$GRADLE_FILE" -sed -i.bak "s/extra\\[\"versionCode\"\\] = [0-9]*/extra[\"versionCode\"] = $VERSION_CODE/" "$GRADLE_FILE" -rm -f "$GRADLE_FILE.bak" - -echo "versionName=$VERSION versionCode=$VERSION_CODE" diff --git a/scripts/ci-test-compile.sh b/scripts/ci-test-compile.sh deleted file mode 100644 index 4f83ef3..0000000 --- a/scripts/ci-test-compile.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -./gradlew \ - :app:shared:compileKotlinJvm \ - :utils:shared:compileKotlinJvm \ - :app:desktop:compileKotlin \ - --no-daemon \ - --console=plain diff --git a/scripts/ci-verify-release-assets.sh b/scripts/ci-verify-release-assets.sh deleted file mode 100644 index 9635304..0000000 --- a/scripts/ci-verify-release-assets.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -VERSION="${1:?usage: ci-verify-release-assets.sh }" -ASSETS_DIR="${2:?usage: ci-verify-release-assets.sh }" - -if [[ ! -d "$ASSETS_DIR" ]]; then - echo "Assets directory not found: $ASSETS_DIR" >&2 - exit 1 -fi - -mapfile -t FILES < <(find "$ASSETS_DIR" -maxdepth 1 -type f | sort) -if [[ "${#FILES[@]}" -eq 0 ]]; then - echo "No release assets in $ASSETS_DIR" >&2 - exit 1 -fi - -require_file() { - local pattern="$1" - shopt -s nullglob - local matches=("$ASSETS_DIR"/$pattern) - shopt -u nullglob - if [[ "${#matches[@]}" -eq 0 ]]; then - echo "Missing asset matching: $pattern" >&2 - exit 1 - fi - for file in "${matches[@]}"; do - if [[ ! -s "$file" ]]; then - echo "Empty asset: $file" >&2 - exit 1 - fi - echo "OK $file ($(wc -c <"$file") bytes)" - done -} - -pe_machine() { - python3 - "$1" <<'PY' -import struct, sys -path = sys.argv[1] -with open(path, "rb") as f: - data = f.read(0x80) -pe_off = struct.unpack_from("&2 - exit 1 - fi - echo "OK PE arch $file" -} - -require_file "FromChat-Setup-${VERSION}-windows-x64.exe" -require_file "FromChat-Setup-${VERSION}-windows-arm64.exe" -require_file "FromChat-${VERSION}-macOS-x64.dmg" -require_file "FromChat-${VERSION}-macOS-arm64.dmg" -require_file "FromChat-${VERSION}-linux-x64.deb" -require_file "FromChat-${VERSION}-linux-arm64.deb" -require_file "FromChat-${VERSION}-linux-x64.rpm" -require_file "FromChat-${VERSION}-linux-arm64.rpm" -require_file "FromChat-${VERSION}-linux-x64.AppImage" -require_file "FromChat-${VERSION}-linux-arm64.AppImage" -require_file "FromChat-${VERSION}-android-universal.apk" - -shopt -s nullglob -for exe in "$ASSETS_DIR"/FromChat-Setup-"${VERSION}"-windows-x64.exe; do - assert_pe "$exe" 34404 -done -for exe in "$ASSETS_DIR"/FromChat-Setup-"${VERSION}"-windows-arm64.exe; do - assert_pe "$exe" 43620 -done -shopt -u nullglob - -for dmg in "$ASSETS_DIR"/*.dmg; do - if [[ "${CI_SMOKE:-}" == "1" ]]; then - echo "OK DMG (smoke) $dmg" - continue - fi - if command -v hdiutil >/dev/null 2>&1; then - hdiutil verify "$dmg" >/dev/null - elif ! file "$dmg" | grep -qiE 'zlib compressed data|bzip2 compressed data|Apple Disk Image|UDIF'; then - echo "DMG does not look valid: $dmg" >&2 - file "$dmg" || true - exit 1 - fi - echo "OK DMG $dmg" -done - -for deb in "$ASSETS_DIR"/*.deb; do - if [[ "${CI_SMOKE:-}" == "1" ]]; then - echo "OK deb (smoke) $deb" - continue - fi - if ! dpkg-deb -I "$deb" >/dev/null 2>&1; then - echo "Invalid .deb: $deb" >&2 - exit 1 - fi - echo "OK deb $deb" -done - -for rpm in "$ASSETS_DIR"/*.rpm; do - if [[ "${CI_SMOKE:-}" == "1" ]]; then - echo "OK rpm (smoke) $rpm" - continue - fi - if ! rpm -qp "$rpm" >/dev/null 2>&1; then - echo "Invalid .rpm: $rpm" >&2 - exit 1 - fi - echo "OK rpm $rpm" -done - -for appimage in "$ASSETS_DIR"/*.AppImage; do - if [[ "${CI_SMOKE:-}" == "1" ]]; then - echo "OK AppImage (smoke) $appimage" - continue - fi - if ! file "$appimage" | grep -qi 'ELF'; then - echo "AppImage is not ELF: $appimage" >&2 - exit 1 - fi - chmod +x "$appimage" - echo "OK AppImage $appimage" -done - -APK="$(find "$ASSETS_DIR" -maxdepth 1 -name '*android-universal.apk' | head -n 1)" -if [[ "${CI_SMOKE:-}" == "1" ]]; then - echo "OK APK (smoke) $APK" -elif command -v unzip >/dev/null 2>&1; then - if ! unzip -t "$APK" >/dev/null 2>&1; then - echo "APK zip integrity failed: $APK" >&2 - exit 1 - fi - libs="$(unzip -l "$APK" 'lib/*/*.so' | awk '/\.so$/ {print $4}' || true)" - echo "$libs" | grep -q 'lib/arm64-v8a/' || { echo "APK missing arm64-v8a libs" >&2; exit 1; } - echo "$libs" | grep -q 'lib/x86_64/' || { echo "APK missing x86_64 libs" >&2; exit 1; } - echo "OK APK ABIs $APK" -fi - -echo "All release assets verified for v$VERSION"