minigraf 1.0.0

Zero-config, single-file, embedded graph database with bi-temporal Datalog queries
Documentation
name: Mobile Release Artifacts

on:
  push:
    tags:
      - '**[0-9]+.[0-9]+.[0-9]+*'
  workflow_dispatch:
    inputs:
      tag:
        description: 'Release tag to upload artifacts to (e.g. v0.21.0)'
        required: true

permissions:
  contents: write
  packages: write

jobs:
  # ── Android ──────────────────────────────────────────────────────────────────

  mobile-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android

      - name: Install cargo-ndk
        run: cargo install cargo-ndk --locked

      - name: Set ANDROID_NDK_HOME
        run: echo "ANDROID_NDK_HOME=$ANDROID_NDK_LATEST_HOME" >> $GITHUB_ENV

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Cross-compile Android targets
        run: |
          cargo ndk \
            -t arm64-v8a \
            -t armeabi-v7a \
            -t x86_64 \
            -o minigraf-ffi/android/jniLibs \
            build --release -p minigraf-ffi

      - name: Generate Kotlin bindings
        run: |
          cargo run -p minigraf-ffi --bin uniffi-bindgen -- generate \
            --library target/aarch64-linux-android/release/libminigraf_ffi.so \
            --language kotlin \
            --out-dir minigraf-ffi/android/src/main/java/

      - name: Assemble AAR
        run: |
          cd minigraf-ffi/android
          ./gradlew assembleRelease

      - name: Rename artifact
        run: |
          TAG="${{ inputs.tag || github.ref_name }}"
          cp minigraf-ffi/android/build/outputs/aar/minigraf-android-release.aar \
             "minigraf-android-${TAG}.aar"

      - uses: actions/upload-artifact@v4
        with:
          name: minigraf-android
          path: "minigraf-android-*.aar"

  # ── iOS ──────────────────────────────────────────────────────────────────────

  mobile-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-apple-ios,aarch64-apple-ios-sim

      - name: Build iOS device library
        run: cargo build --target aarch64-apple-ios --release -p minigraf-ffi

      - name: Build iOS simulator library
        run: cargo build --target aarch64-apple-ios-sim --release -p minigraf-ffi

      - name: Generate Swift bindings
        run: |
          mkdir -p minigraf-swift/Sources/MinigrafKit
          cargo run -p minigraf-ffi --bin uniffi-bindgen -- generate \
            --library target/aarch64-apple-ios/release/libminigraf_ffi.a \
            --language swift \
            --out-dir minigraf-swift/Sources/MinigrafKit/

      - name: Prepare headers for xcframework
        run: |
          mkdir -p minigraf-swift/includes
          cp minigraf-swift/Sources/MinigrafKit/*.h minigraf-swift/includes/ 2>/dev/null || true
          cp minigraf-swift/Sources/MinigrafKit/*.modulemap minigraf-swift/includes/ 2>/dev/null || true

      - name: Assemble xcframework
        run: |
          TAG="${{ inputs.tag || github.ref_name }}"
          xcodebuild -create-xcframework \
            -library target/aarch64-apple-ios/release/libminigraf_ffi.a \
            -headers minigraf-swift/includes/ \
            -library target/aarch64-apple-ios-sim/release/libminigraf_ffi.a \
            -headers minigraf-swift/includes/ \
            -output "MinigrafKit.xcframework"
          zip -r "MinigrafKit-${TAG}.xcframework.zip" MinigrafKit.xcframework

      - uses: actions/upload-artifact@v4
        with:
          name: minigraf-ios
          path: "MinigrafKit-*.xcframework.zip"

      - uses: actions/upload-artifact@v4
        with:
          name: swift-sources
          path: minigraf-swift/Sources/MinigrafKit/

  # ── Upload to release + GitHub Packages ──────────────────────────────────────

  release-upload-mobile:
    needs: [mobile-android, mobile-ios]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          fetch-depth: 0

      - uses: actions/download-artifact@v4
        with:
          name: minigraf-android
          path: artifacts/

      - uses: actions/download-artifact@v4
        with:
          name: minigraf-ios
          path: artifacts/

      - uses: actions/download-artifact@v4
        with:
          name: swift-sources
          path: minigraf-swift/Sources/MinigrafKit/

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Wait for GitHub Release to exist
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ inputs.tag || github.ref_name }}
        run: |
          echo "Waiting for release $TAG..."
          for i in $(seq 1 40); do
            if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
              echo "Release found on attempt $i"
              break
            fi
            echo "Attempt $i/40: not yet, waiting 15s..."
            sleep 15
          done
          if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
            echo "ERROR: Release $TAG not found after 40 attempts (10 minutes). Aborting."
            exit 1
          fi

      - name: Upload artifacts to GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ inputs.tag || github.ref_name }}
        run: |
          gh release upload "$TAG" artifacts/* \
            --repo "$GITHUB_REPOSITORY" \
            --clobber

      - name: Compute xcframework checksum and update Package.swift
        env:
          TAG: ${{ inputs.tag || github.ref_name }}
        run: |
          XCFW_ZIP=$(ls artifacts/MinigrafKit-*.xcframework.zip)
          CHECKSUM=$(shasum -a 256 "$XCFW_ZIP" | awk '{print $1}')
          # The xcframework zip lives on the main release (TAG).
          # Package.swift itself lives on the swift-releases branch tagged swift-TAG,
          # so SPM consumers use the swift-TAG reference for the Swift package.
          URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/MinigrafKit-${TAG}.xcframework.zip"
          sed -i \
            -e "s|url: \"https://github.com/.*/releases/download/.*/MinigrafKit-.*\\.xcframework\\.zip\"|url: \"${URL}\"|" \
            -e "s|checksum: \"[0-9a-f]*\"|checksum: \"${CHECKSUM}\"|" \
            Package.swift

      - name: Publish AAR to GitHub Packages
        env:
          GITHUB_ACTOR: ${{ github.actor }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${{ inputs.tag || github.ref_name }}"
          export VERSION="${TAG#v}"
          mkdir -p minigraf-ffi/android/build/outputs/aar/
          cp artifacts/minigraf-android-*.aar \
             minigraf-ffi/android/build/outputs/aar/minigraf-android-release.aar
          cd minigraf-ffi/android
          ./gradlew publishReleasePublicationToGitHubPackagesRepository

      - name: Commit Package.swift update and Swift sources, then create swift-TAG
        env:
          TAG: ${{ inputs.tag || github.ref_name }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git config user.name "github-actions[bot]"

          # Save the files we intend to push (Package.swift already updated by sed,
          # swift sources already downloaded by actions/download-artifact above).
          TMPDIR_SWIFT=$(mktemp -d)
          cp Package.swift "$TMPDIR_SWIFT/Package.swift"
          cp -r minigraf-swift/Sources/MinigrafKit/ "$TMPDIR_SWIFT/MinigrafKit"

          # Switch to swift-releases branch so we only push Package.swift and Swift
          # sources — not the full main HEAD (which includes workflow files, requiring
          # `workflows` scope that GITHUB_TOKEN cannot be granted via permissions:).
          if git fetch origin swift-releases 2>/dev/null; then
            # -f: discard local changes (Package.swift modified by sed, swift sources
            # untracked from artifact download) — we've already saved them to TMPDIR.
            git checkout -fB swift-releases origin/swift-releases
          else
            # First-ever release: start a clean orphan branch with no history.
            git checkout --orphan swift-releases
            git rm -rf . --quiet 2>/dev/null || true
          fi

          # Apply only the intended changes.
          cp "$TMPDIR_SWIFT/Package.swift" Package.swift
          mkdir -p minigraf-swift/Sources/MinigrafKit/
          cp -r "$TMPDIR_SWIFT/MinigrafKit/." minigraf-swift/Sources/MinigrafKit/
          rm -rf "$TMPDIR_SWIFT"

          git add Package.swift minigraf-swift/Sources/MinigrafKit/
          git commit -m "chore(release): update Package.swift and Swift bindings for ${TAG}" \
            || echo "Nothing to commit"
          # Push to the unprotected swift-releases branch instead of main.
          # SPM resolves packages by the swift-TAG (e.g. swift-v0.25.0) which points
          # to this commit — the original TAG is left on the main branch so that all
          # other language workflows (Python, Java, C, Node.js) can check out the
          # full source tree.
          git push origin swift-releases --force
          NEW_SHA=$(git rev-parse HEAD)
          # Create (or update) the swift-TAG reference on the swift-releases commit.
          SWIFT_TAG="swift-${TAG}"
          gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
            -X POST \
            -f ref="refs/tags/${SWIFT_TAG}" \
            -f sha="$NEW_SHA" 2>/dev/null \
          || gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/${SWIFT_TAG}" \
            -X PATCH \
            -f sha="$NEW_SHA" \
            -F force=true