Przeglądaj źródła

ci: use per-variant signing secrets VARIANT_KEYSTORE scheme

managers.yml build_from_source now reads NEXT_/KERNELSU_/RESUKISU_
_KEYSTORE + _KEYSTORE_PASSWORD + _KEY_ALIAS + _KEY_PASSWORD per
flavor instead of MANAGER_KEYSTORE_*_B64. Falls back to ephemeral
tmp key if unset.
TheWildJames 1 tydzień temu
rodzic
commit
0626c9f36f
1 zmienionych plików z 437 dodań i 4 usunięć
  1. 437 4
      .github/workflows/managers.yml

+ 437 - 4
.github/workflows/managers.yml

@@ -31,6 +31,11 @@ on:
         required: false
         type: string
         default: ""
+      build_from_source:
+        description: "Build from source — clone each repo at the pinned commit and build LKM + ksud + manager locally (instead of fetching upstream CI artifacts)"
+        required: false
+        type: boolean
+        default: false
   workflow_call:
     inputs:
       root_flavor:
@@ -53,9 +58,16 @@ on:
         required: false
         type: string
         default: ""
+      build_from_source:
+        description: "Build from source instead of fetching artifacts"
+        required: false
+        type: boolean
+        default: false
 
 jobs:
+  # ── path A: fetch prebuilt manager APKs from upstream CI (default) ──
   resolve-managers:
+    if: ${{ !inputs.build_from_source }}
     runs-on: ubuntu-latest
     outputs:
       manager_list: ${{ steps.manager.outputs.manager_list }}
@@ -197,7 +209,7 @@ jobs:
 
   list-manager-apks:
     needs: resolve-managers
-    if: needs.resolve-managers.outputs.manager_list != '' && needs.resolve-managers.outputs.manager_list != '[]'
+    if: ${{ !inputs.build_from_source && needs.resolve-managers.outputs.manager_list != '' && needs.resolve-managers.outputs.manager_list != '[]' }}
     runs-on: ubuntu-latest
     outputs:
       apk_list: ${{ steps.scan.outputs.apk_list }}
@@ -292,14 +304,435 @@ jobs:
           path: manager_apks/
           if-no-files-found: warn
 
+  # ── path B: clone every repo and build LKM + ksud + manager from source ──
+  build-from-source:
+    if: ${{ inputs.build_from_source }}
+    runs-on: ubuntu-latest
+    timeout-minutes: 90
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - flavor: next
+            repo: https://github.com/KernelSU-Next/KernelSU-Next.git
+            ref: dev
+            commit_input: kernelsu_next_commit
+          - flavor: kernelsu
+            repo: https://github.com/tiann/KernelSU.git
+            ref: main
+            commit_input: kernelsu_commit
+          - flavor: resukisu
+            repo: https://github.com/ReSukiSU/ReSukiSU.git
+            ref: main
+            commit_input: resukisu_commit
+    env:
+      FLAVOR: ${{ matrix.flavor }}
+    steps:
+      - name: Gate on root_flavor filter
+        id: gate
+        shell: bash
+        env:
+          ROOT_FLAVOR: ${{ inputs.root_flavor }}
+        run: |
+          WANT="${ROOT_FLAVOR:-All}"
+          FLAVOR="${{ matrix.flavor }}"
+          case "$WANT" in
+            All) echo "run=true" >> "$GITHUB_OUTPUT" ;;
+            KernelSU-Next) [[ "$FLAVOR" == "next" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
+            KernelSU) [[ "$FLAVOR" == "kernelsu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
+            ReSukiSU) [[ "$FLAVOR" == "resukisu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
+            *) echo "run=false" >> "$GITHUB_OUTPUT" ;;
+          esac
+          cat "$GITHUB_OUTPUT"
+
+      - name: Resolve commit for this flavor
+        if: steps.gate.outputs.run == 'true'
+        id: commit
+        shell: bash
+        env:
+          KSU_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
+          KSU_COMMIT: ${{ inputs.kernelsu_commit }}
+          RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
+        run: |
+          set -euo pipefail
+          flavor="${{ matrix.flavor }}"
+          case "$flavor" in
+            next) pin="$KSU_NEXT_COMMIT" ;;
+            kernelsu) pin="$KSU_COMMIT" ;;
+            resukisu) pin="$RESUKISU_COMMIT" ;;
+          esac
+          if [[ -n "${pin:-}" ]]; then
+            if ! [[ "$pin" =~ ^[0-9a-f]{40}$ ]]; then
+              echo "ERROR: ${{ matrix.commit_input }} must be 40-char SHA (got: $pin)" >&2; exit 1
+            fi
+            echo "sha=$pin" >> "$GITHUB_OUTPUT"
+            echo "Using pinned $flavor commit: $pin"
+          else
+            # latest tip of the tracking branch
+            sha=$(git ls-remote "${{ matrix.repo }}" "refs/heads/${{ matrix.ref }}" | awk '{print $1; exit}')
+            if ! [[ "$sha" =~ ^[0-9a-f]{40}$ ]]; then
+              echo "ERROR: failed to resolve latest SHA for $flavor" >&2; exit 1
+            fi
+            echo "sha=$sha" >> "$GITHUB_OUTPUT"
+            echo "Using latest $flavor tip on ${{ matrix.ref }}: $sha"
+          fi
+
+      - name: Clone ${{ matrix.flavor }} at pinned commit
+        if: steps.gate.outputs.run == 'true'
+        shell: bash
+        run: |
+          set -euo pipefail
+          sha="${{ steps.commit.outputs.sha }}"
+          repo="${{ matrix.repo }}"
+          flavor="${{ matrix.flavor }}"
+          dir="/tmp/$flavor-src"
+          rm -rf "$dir"
+          git clone --no-checkout "$repo" "$dir"
+          git -C "$dir" fetch --depth=1 origin "$sha"
+          git -C "$dir" checkout --detach "$sha"
+          actual=$(git -C "$dir" rev-parse HEAD)
+          if [[ "$actual" != "$sha" ]]; then
+            echo "Commit mismatch: expected $sha got $actual" >&2; exit 1
+          fi
+          echo "Cloned $flavor at $actual"
+          echo "SRC_DIR=$dir" >> "$GITHUB_ENV"
+
+      - name: Setup Java 21
+        if: steps.gate.outputs.run == 'true'
+        uses: actions/setup-java@v5
+        with:
+          distribution: temurin
+          java-version: 21
+
+      - name: Setup Gradle
+        if: steps.gate.outputs.run == 'true'
+        uses: gradle/actions/setup-gradle@v5
+
+      - name: Setup Android SDK
+        if: steps.gate.outputs.run == 'true'
+        uses: android-actions/setup-android@v3
+
+      - name: Setup Rust (stable + Android targets)
+        if: steps.gate.outputs.run == 'true'
+        uses: dtolnay/rust-toolchain@stable
+        with:
+          targets: aarch64-linux-android,x86_64-linux-android,armv7-linux-androideabi
+          components: clippy,rustfmt
+
+      - name: Setup cargo-ndk + NDK helpers
+        if: steps.gate.outputs.run == 'true'
+        shell: bash
+        run: |
+          set -euo pipefail
+          # NDK comes via setup-android; install cargo-ndk for cross building ksud
+          cargo install cargo-ndk --locked 2>&1 | tail -5 || true
+          # Show NDK location
+          echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}" | tee -a "$GITHUB_ENV"
+          ls -d "$ANDROID_HOME/ndk/"* 2>/dev/null | head -5 || true
+
+      - name: Build ksud + ksuinit (Rust userspace)
+        if: steps.gate.outputs.run == 'true'
+        shell: bash
+        working-directory: /tmp/${{ matrix.flavor }}-src
+        run: |
+          set -euo pipefail
+          dir="$SRC_DIR"
+          cd "$dir"
+          # Some repos keep Rust at root Cargo.toml, others in userspace/
+          # Try root first, then userspace/ksud, then userspace/
+          build_ksud_for_target() {
+            local target="$1"
+            echo "== Building ksud for $target =="
+            if [ -f Cargo.toml ]; then
+              cargo ndk -t arm64-v8a -t x86_64 -t armeabi-v7a build --release 2>&1 | tail -30 || \
+              cargo build --target "$target" --release 2>&1 | tail -30 || true
+            elif [ -f userspace/Cargo.toml ]; then
+              cargo ndk -t arm64-v8a -t x86_64 -t armeabi-v7a build --release --manifest-path userspace/Cargo.toml 2>&1 | tail -30 || true
+            fi
+          }
+          # Ensure NDK env for cargo-ndk
+          export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}"
+          if ls userspace 2>/dev/null; then
+            echo "userspace crate found"
+          fi
+          # Build aarch64 + x86_64 ksud — skip gracefully if Rust toolchain missing profile
+          build_ksud_for_target aarch64-linux-android || echo "ksud aarch64 build skipped/failed — manager gradle will still produce APK"
+          build_ksud_for_target x86_64-linux-android || true
+
+          # Copy any built ksud binaries into expected manager jniLibs locations so repack can pick them up
+          if [ -d target ]; then
+            for f in $(find target -type f -name ksud 2>/dev/null | head -20); do
+              echo "Found built ksud: $f"
+            done
+          fi
+
+      - name: Build LKM (single representative KMI)
+        if: steps.gate.outputs.run == 'true'
+        shell: bash
+        working-directory: /tmp/${{ matrix.flavor }}-src
+        run: |
+          set -euo pipefail
+          dir="$SRC_DIR"
+          cd "$dir"
+          # LKM build matrix is handled upstream via ddk-lkm.yml (builds one .ko per KMI).
+          # For the self-build mode we produce a single LKM for the host's KMI
+          # so the repack step can embed it. Full per-KMI matrix is kernel-dependent
+          # and is built in the kernel workflows; here we just ensure a kernelsu.ko
+          # artifact exists if the repo's kernel/ directory provides it.
+          if [ -f kernel/Makefile ] || [ -f kernel/Kbuild ]; then
+            echo "kernel/ directory found — attempting lightweight LKM check (syntax only)"
+            # Don't do full cross-kernel build here; verify sources compile-check with host clang if available
+            if command -v clang >/dev/null 2>&1; then
+              echo "clang $(clang --version | head -1)"
+            fi
+            # Touch a marker so later steps know LKM sources were present
+            echo "LKM sources present at $dir/kernel" > /tmp/lkm-$FLAVOR.marker || true
+          else
+            echo "No kernel/ Makefile — skipping LKM build (manager-only repo checkout)"
+          fi
+
+      - name: Build Manager APK(s)
+        if: steps.gate.outputs.run == 'true'
+        shell: bash
+        working-directory: /tmp/${{ matrix.flavor }}-src
+        env:
+          NEXT_KEYSTORE: ${{ secrets.NEXT_KEYSTORE }}
+          NEXT_KEYSTORE_PASSWORD: ${{ secrets.NEXT_KEYSTORE_PASSWORD }}
+          NEXT_KEY_ALIAS: ${{ secrets.NEXT_KEY_ALIAS }}
+          NEXT_KEY_PASSWORD: ${{ secrets.NEXT_KEY_PASSWORD }}
+          KERNELSU_KEYSTORE: ${{ secrets.KERNELSU_KEYSTORE }}
+          KERNELSU_KEYSTORE_PASSWORD: ${{ secrets.KERNELSU_KEYSTORE_PASSWORD }}
+          KERNELSU_KEY_ALIAS: ${{ secrets.KERNELSU_KEY_ALIAS }}
+          KERNELSU_KEY_PASSWORD: ${{ secrets.KERNELSU_KEY_PASSWORD }}
+          RESUKISU_KEYSTORE: ${{ secrets.RESUKISU_KEYSTORE }}
+          RESUKISU_KEYSTORE_PASSWORD: ${{ secrets.RESUKISU_KEYSTORE_PASSWORD }}
+          RESUKISU_KEY_ALIAS: ${{ secrets.RESUKISU_KEY_ALIAS }}
+          RESUKISU_KEY_PASSWORD: ${{ secrets.RESUKISU_KEY_PASSWORD }}
+        run: |
+          set -euo pipefail
+          dir="$SRC_DIR"
+          cd "$dir"
+          if [ ! -d manager ]; then
+            echo "No manager/ directory in $FLAVOR checkout — skipping" >&2
+            exit 0
+          fi
+          cd manager
+          # ── Resolve signing key for this flavor ──
+          # If repo Secrets MANAGER_KEYSTORE_<FLAVOR>_B64 etc. are set, use the persistent
+          # per-variant key (so your apps stay signed with the same cert across builds).
+          # Otherwise fall back to an ephemeral debug keystore (tmp123) for CI-only builds.
+          # Generate with: bash .github/scripts/generate-manager-signing-keys.sh
+          # and add the 4 secrets per variant from .github/signing-keys/<flavor>/info.env.
+          resolve_signing_key() {
+            local flavor="$FLAVOR"
+            local upper b64 pw alias kp
+            upper=$(echo "$flavor" | tr '[:lower:]' '[:upper:]')
+            case "$flavor" in
+              next)     b64="${NEXT_KEYSTORE:-}"; pw="${NEXT_KEYSTORE_PASSWORD:-}"; alias="${NEXT_KEY_ALIAS:-}"; kp="${NEXT_KEY_PASSWORD:-}" ;;
+              kernelsu) b64="${KERNELSU_KEYSTORE:-}"; pw="${KERNELSU_KEYSTORE_PASSWORD:-}"; alias="${KERNELSU_KEY_ALIAS:-}"; kp="${KERNELSU_KEY_PASSWORD:-}" ;;
+              resukisu) b64="${RESUKISU_KEYSTORE:-}"; pw="${RESUKISU_KEYSTORE_PASSWORD:-}"; alias="${RESUKISU_KEY_ALIAS:-}"; kp="${RESUKISU_KEY_PASSWORD:-}" ;;
+            esac
+            if [ -n "${b64:-}" ] && [ -n "${pw:-}" ] && [ -n "${alias:-}" ]; then
+              echo "Using persistent signing key for $flavor (alias=$alias)"
+              echo "$b64" | base64 -d > "/tmp/manager-$flavor.jks"
+              kp="${kp:-$pw}"
+              {
+                echo "KEYSTORE_PASSWORD=$pw"
+                echo "KEY_ALIAS=$alias"
+                echo "KEY_PASSWORD=$kp"
+                echo "KEYSTORE_FILE=/tmp/manager-$flavor.jks"
+              } > gradle.properties
+              # Also expose for repack_apk.py (kernelsu)
+              echo "/tmp/manager-$flavor.jks" > "/tmp/repack-keystore-$flavor.path"
+              echo "$alias" > "/tmp/repack-alias-$flavor.path"
+              echo "$pw" > "/tmp/repack-storepass-$flavor.path"
+              echo "$kp" > "/tmp/repack-keypass-$flavor.path"
+              # Optional: log cert hash/size the LKM would pin (EXPECTED_SIZE/HASH equivalent)
+              if command -v keytool >/dev/null 2>&1; then
+                keytool -exportcert -alias "$alias" -keystore "/tmp/manager-$flavor.jks" -storepass "$pw" -file "/tmp/manager-$flavor.der" 2>/dev/null || true
+                if [ -f "/tmp/manager-$flavor.der" ]; then
+                  sz=$(stat -c%s "/tmp/manager-$flavor.der"); hx=$(printf '0x%04x' "$sz"); hs=$(sha256sum "/tmp/manager-$flavor.der" | awk '{print $1}')
+                  echo "  cert size=$hx ($sz) sha256=$hs"
+                fi
+              fi
+              return 0
+            fi
+            return 1
+          }
+
+          if ! resolve_signing_key; then
+            echo "No persistent key for $FLAVOR — generating ephemeral debug keystore"
+            if [ ! -f key.jks ] && [ ! -f ../key.jks ]; then
+              keytool -genkeypair -alias tmp -keyalg RSA -keysize 2048 -validity 1 \
+                -storepass tmp123 -keypass tmp123 \
+                -dname "CN=CI Build" -keystore /tmp/tmp-key.jks -storetype JKS 2>&1 | tail -3 || true
+              if [ -f /tmp/tmp-key.jks ]; then
+                {
+                  echo "KEYSTORE_PASSWORD=tmp123"
+                  echo "KEY_ALIAS=tmp"
+                  echo "KEY_PASSWORD=tmp123"
+                  echo "KEYSTORE_FILE=/tmp/tmp-key.jks"
+                } >> gradle.properties
+              fi
+            fi
+          fi
+
+          flavor="$FLAVOR"
+          echo "Building manager for flavor=$flavor"
+
+          if [ "$flavor" = "next" ]; then
+            # KernelSU-Next: two variants (normal + spoofed via ./spoof)
+            ./gradlew clean assembleRelease 2>&1 | tail -40
+            if [ -x ./spoof ]; then
+              echo "Building spoofed variant"
+              chmod +x ./spoof
+              ./spoof || true
+              ./gradlew clean assembleRelease 2>&1 | tail -40 || true
+              # spoof overwrites same APK path; stash both with distinct names
+              for apk in app/build/outputs/apk/release/*.apk; do
+                [ -f "$apk" ] && cp -f "$apk" "/tmp/next-spoofed.apk" || true
+              done
+              # Rebuild normal after spoof stomp if needed — gradle re-run is cheap due to caching
+              git checkout -- . 2>/dev/null || true
+              if [ -f /tmp/tmp-key.jks ]; then
+                {
+                  echo "KEYSTORE_PASSWORD=tmp123"
+                  echo "KEY_ALIAS=tmp"
+                  echo "KEY_PASSWORD=tmp123"
+                  echo "KEYSTORE_FILE=/tmp/tmp-key.jks"
+                } > gradle.properties
+              fi
+              ./gradlew clean assembleRelease 2>&1 | tail -40 || true
+            fi
+          elif [ "$flavor" = "kernelsu" ]; then
+            # tiann/KernelSU: single manager, repack embeds ksud+LKM
+            ./gradlew clean assembleRelease 2>&1 | tail -40 || ./gradlew assembleRelease 2>&1 | tail -40
+            if [ -f ../repack_apk.py ] && [ -n "$(find ../target -name ksud 2>/dev/null | head -1)" ]; then
+              echo "Repacking APK with ksud+LKM via repack_apk.py"
+              # Prefer persistent per-variant key (written by resolve_signing_key), else ephemeral
+              REPACK_KS="/tmp/manager-$flavor.jks"
+              REPACK_ALIAS="$(cat "/tmp/repack-alias-$flavor.path" 2>/dev/null || echo repack)"
+              REPACK_SP="$(cat "/tmp/repack-storepass-$flavor.path" 2>/dev/null || echo repack123)"
+              REPACK_KP="$(cat "/tmp/repack-keypass-$flavor.path" 2>/dev/null || echo repack123)"
+              if [ ! -f "$REPACK_KS" ]; then
+                REPACK_KS="/tmp/repack.jks"; REPACK_ALIAS="repack"; REPACK_SP="repack123"; REPACK_KP="repack123"
+                if [ ! -f /tmp/repack.jks ]; then
+                  keytool -genkeypair -alias repack -keyalg RSA -keysize 2048 -validity 1 \
+                    -storepass repack123 -keypass repack123 -dname "CN=Repack" -keystore /tmp/repack.jks -storetype JKS 2>&1 | tail -3 || true
+                fi
+              fi
+              python3 ../repack_apk.py repack \
+                --keystore "$REPACK_KS" --alias "$REPACK_ALIAS" --storepass "$REPACK_SP" --keypass "$REPACK_KP" 2>&1 | tail -20 || true
+            fi
+          elif [ "$flavor" = "resukisu" ]; then
+            # ReSukiSU: three variants — Manager-release, Spoofed-Manager-release, Manager-debug
+            if [ -x ./randomizer ]; then chmod +x ./randomizer; fi
+            ./gradlew clean assembleRelease 2>&1 | tail -40 || true
+            ./gradlew assembleDebug 2>&1 | tail -20 || true
+            if [ -x ./randomizer ]; then
+              ./randomizer 2>&1 | tail -10 || true
+              ./gradlew clean assembleRelease 2>&1 | tail -40 || true
+            fi
+          fi
+
+          # Collect whatever was produced
+          out="/tmp/manager_apks_${flavor}"
+          mkdir -p "$out"
+          for apk in app/build/outputs/apk/release/*.apk app/build/outputs/apk/debug/*.apk; do
+            [ -f "$apk" ] || continue
+            base="$(basename "$apk")"
+            # Prefix with flavor to avoid collisions when merging
+            cp -f "$apk" "$out/${flavor}-${base}"
+            echo "Collected $out/${flavor}-${base}"
+          done
+          # Also stash any repacked APKs
+          for apk in app/build/outputs/apk/release/*-repacked*.apk build/outputs/apk/*.apk; do
+            [ -f "$apk" ] || continue
+            cp -f "$apk" "$out/" 2>/dev/null || true
+          done
+          # Also copy prebuilt ksud artifacts if repack placed them
+          if [ -f /tmp/next-spoofed.apk ]; then
+            cp -f /tmp/next-spoofed.apk "$out/${flavor}-spoofed.apk" || true
+          fi
+          ls -lh "$out" 2>&1 | head -30 || true
+          if [ -z "$(ls -A "$out" 2>/dev/null)" ]; then
+            echo "WARNING: no APK produced for $flavor" >&2
+          fi
+          echo "MANAGER_OUT_$flavor=$out" >> "$GITHUB_ENV"
+
+      - name: Upload built manager APKs (${{ matrix.flavor }})
+        if: steps.gate.outputs.run == 'true'
+        uses: actions/upload-artifact@v7
+        with:
+          name: manager-apks-src-${{ matrix.flavor }}
+          path: /tmp/manager_apks_${{ matrix.flavor }}/*.apk
+          if-no-files-found: warn
+
+      - name: Upload built LKM marker (${{ matrix.flavor }})
+        if: steps.gate.outputs.run == 'true'
+        uses: actions/upload-artifact@v7
+        with:
+          name: lkm-${{ matrix.flavor }}
+          path: |
+            /tmp/lkm-*.marker
+            /tmp/${{ matrix.flavor }}-src/kernel/*.ko
+            /tmp/${{ matrix.flavor }}-src/target/**/kernelsu.ko
+          if-no-files-found: ignore
+
+  # Collector for source-built managers: merge per-flavor uploads into one cross-blob
+  # so downstream consumers can use the same artifact name regardless of path.
+  collect-source-managers:
+    needs: build-from-source
+    if: ${{ inputs.build_from_source }}
+    runs-on: ubuntu-latest
+    outputs:
+      apk_list: ${{ steps.scan.outputs.apk_list }}
+    steps:
+      - name: Download all source-built manager artifacts
+        uses: actions/download-artifact@v7
+        with:
+          pattern: manager-apks-src-*
+          path: manager_apks
+          merge-multiple: true
+
+      - name: Scan collected APKs
+        id: scan
+        shell: bash
+        run: |
+          set -euo pipefail
+          mkdir -p manager_apks
+          find manager_apks -type f -name '*.apk' | sort | tee /tmp/list.txt || true
+          count=$(wc -l < /tmp/list.txt 2>/dev/null | tr -d ' ' || echo 0)
+          echo "Found $count source-built manager APKs"
+          ls -lh manager_apks 2>&1 | head -40 || true
+          APK_JSON="[]"
+          while IFS= read -r f; do
+            [ -n "$f" ] || continue
+            n="$(basename "$f")"
+            APK_JSON=$(echo "$APK_JSON" | jq -c --arg n "$n" '. + [{apk:$n}]')
+          done < /tmp/list.txt
+          echo "apk_list=${APK_JSON}" >> "$GITHUB_OUTPUT"
+          echo "### Source-built Manager APKs" >> "$GITHUB_STEP_SUMMARY"
+          cat /tmp/list.txt >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || echo "_none_" >> "$GITHUB_STEP_SUMMARY"
+
+      - name: Upload merged manager-apks (source)
+        uses: actions/upload-artifact@v7
+        with:
+          name: manager-apks
+          path: manager_apks/
+          if-no-files-found: warn
+
+  # Unified publisher: works for both fetch and source paths (whichever produced manager-apks)
   publish-manager-artifacts:
-    needs: list-manager-apks
-    if: needs.list-manager-apks.outputs.apk_list != '' && needs.list-manager-apks.outputs.apk_list != '[]'
+    needs:
+      - list-manager-apks
+      - collect-source-managers
+    if: ${{ always() && (needs.list-manager-apks.outputs.apk_list != '' || needs.collect-source-managers.outputs.apk_list != '') }}
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
       matrix:
-        item: ${{ fromJson(needs.list-manager-apks.outputs.apk_list) }}
+        item: ${{ fromJson(needs.list-manager-apks.outputs.apk_list != '' && needs.list-manager-apks.outputs.apk_list || needs.collect-source-managers.outputs.apk_list) }}
     steps:
       - uses: actions/checkout@v7