| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902 |
- name: Fetch Root Managers
- permissions:
- contents: read
- actions: read
- on:
- workflow_dispatch:
- inputs:
- root_flavor:
- description: "Root flavor to fetch managers for"
- type: choice
- options:
- - All
- - KernelSU-Next
- - KernelSU
- - ReSukiSU
- - KernelSU-Next-SUSFS
- default: All
- kernelsu_next_commit:
- description: "Pin KernelSU-Next manager to this commit (40-char SHA, empty = latest dev tip)"
- required: false
- type: string
- default: ""
- kernelsu_commit:
- description: "Pin KernelSU manager to this commit (40-char SHA, empty = latest main tip)"
- required: false
- type: string
- default: ""
- resukisu_commit:
- description: "Pin ReSukiSU manager to this commit (40-char SHA, empty = latest main tip)"
- required: false
- type: string
- default: ""
- kernelsu_next_susfs_commit:
- description: "Pin KernelSU-Next SUSFS (pershoot dev-susfs) manager to this commit (40-char SHA, empty = latest dev-susfs tip)"
- 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:
- description: "Root flavor to fetch managers for"
- required: false
- type: string
- default: All
- kernelsu_next_commit:
- description: "Pin KernelSU-Next manager commit"
- required: false
- type: string
- default: ""
- kernelsu_commit:
- description: "Pin KernelSU manager commit"
- required: false
- type: string
- default: ""
- resukisu_commit:
- description: "Pin ReSukiSU manager commit"
- required: false
- type: string
- default: ""
- kernelsu_next_susfs_commit:
- description: "Pin KernelSU-Next SUSFS manager commit"
- 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 }}
- steps:
- - name: Resolve manager builds for selected root flavor(s)
- id: manager
- shell: bash
- env:
- GH_TOKEN: ${{ github.token }}
- INPUT_ROOT_FLAVOR: ${{ inputs.root_flavor }}
- INPUT_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
- INPUT_KSU_COMMIT: ${{ inputs.kernelsu_commit }}
- INPUT_RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
- INPUT_NEXT_SUSFS_COMMIT: ${{ inputs.kernelsu_next_susfs_commit }}
- run: |
- set -euo pipefail
- # Exact extraction of main.yml#resolve-managers / manager step.
- # Each selected root must ship ALL manager APK builds. KernelSU-Next publishes
- # its manager APKs (manager + manager-spoofed) from the official dev-tip
- # build-manager-ci run; kernelsu (KernelSU) and ReSukiSU expose theirs as
- # CI-run artifacts. All three are pinned to an exact commit.
- retry() {
- local n=0
- until [ "$n" -ge 5 ]; do
- "$@" && return 0
- n=$((n+1))
- echo "retry $n/5 failed for: $*" >&2
- sleep 5
- done
- return 1
- }
- resolve_sha() { # repo ref
- retry git ls-remote "$1" "$2" | awk '{print $1; exit}'
- }
- resolve_one() { # flavor
- local flavor="$1"
- local owner repo wf stock
- case "$flavor" in
- next)
- owner="KernelSU-Next"; repo="KernelSU-Next"; wf="build-manager-ci.yml"
- if [[ -n "${INPUT_NEXT_COMMIT:-}" ]]; then
- stock="$INPUT_NEXT_COMMIT"
- if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
- echo "ERROR: kernelsu_next_commit must be a 40-char SHA (got: $stock)" >&2
- exit 1
- fi
- else
- stock=$(resolve_sha https://github.com/KernelSU-Next/KernelSU-Next.git refs/heads/dev)
- fi
- run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
- -H "Authorization: Bearer $GH_TOKEN" \
- "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
- | jq -r '.workflow_runs[0].id // empty')
- if [[ -z "${run_id}" ]]; then
- if [[ -z "${INPUT_NEXT_COMMIT:-}" ]]; then
- echo "No manager build for dev tip ${stock}, falling back to latest successful run" >&2
- fb=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?branch=dev&status=success&per_page=1")
- run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
- stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
- if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
- fi
- if [[ -z "${run_id}" ]]; then
- echo "ERROR: no ${repo} manager build exists for dev commit ${stock}" >&2
- exit 1
- fi
- fi
- printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager,manager-spoofed"}' \
- "$flavor" "$owner" "$repo" "$run_id" "$stock"
- ;;
- kernelsu)
- owner="tiann"; repo="KernelSU"; wf="build-manager.yml"
- if [[ -n "${INPUT_KSU_COMMIT:-}" ]]; then
- stock="$INPUT_KSU_COMMIT"
- if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
- echo "ERROR: kernelsu_commit must be a 40-char SHA (got: $stock)" >&2
- exit 1
- fi
- else
- stock=$(resolve_sha https://github.com/tiann/KernelSU.git refs/heads/main)
- fi
- run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
- "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
- | jq -r '.workflow_runs[0].id // empty')
- if [[ -z "${run_id}" ]]; then
- if [[ -z "${INPUT_KSU_COMMIT:-}" ]]; then
- echo "No manager build for stock ${stock}, falling back to latest successful run" >&2
- fb=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?branch=main&status=success&per_page=1")
- run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
- stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
- if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
- fi
- if [[ -z "${run_id}" ]]; then
- echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
- exit 1
- fi
- fi
- printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager"}' \
- "$flavor" "$owner" "$repo" "$run_id" "$stock"
- ;;
- next-susfs)
- owner="pershoot"; repo="KernelSU-Next"; wf="build-manager-ci.yml"
- if [[ -n "${INPUT_NEXT_SUSFS_COMMIT:-}" ]]; then
- stock="$INPUT_NEXT_SUSFS_COMMIT"
- if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
- echo "ERROR: kernelsu_next_susfs_commit must be a 40-char SHA (got: $stock)" >&2
- exit 1
- fi
- else
- stock=$(resolve_sha https://github.com/pershoot/KernelSU-Next.git refs/heads/dev-susfs)
- # fallback to upstream KernelSU-Next dev-susfs if pershoot fork tip not found
- if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
- stock=$(resolve_sha https://github.com/KernelSU-Next/KernelSU-Next.git refs/heads/dev-susfs)
- fi
- fi
- run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
- -H "Authorization: Bearer ***" \
- "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
- | jq -r '.workflow_runs[0].id // empty')
- if [[ -z "${run_id}" ]]; then
- if [[ -z "${INPUT_NEXT_SUSFS_COMMIT:-}" ]]; then
- echo "No manager build for dev-susfs tip ${stock}, falling back to latest successful run" >&2
- fb=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors -H "Authorization: Bearer ***" "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?branch=dev-susfs&status=success&per_page=1")
- run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
- stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
- if [[ -z "$run_id" ]]; then
- # dev-susfs has no CI on this fork — try dev fallback
- echo "No dev-susfs CI run, trying dev branch fallback" >&2
- fb=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors -H "Authorization: Bearer ***" "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?branch=dev&status=success&per_page=1")
- run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
- stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
- fi
- if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
- fi
- if [[ -z "${run_id}" ]]; then
- echo "WARNING: no ${owner}/${repo} manager build for dev-susfs commit ${stock} — will need build_from_source" >&2
- # emit empty want so fetch path skips gracefully; source build will handle it
- printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":null,"stock":"%s","want":"manager,manager-spoofed","note":"no-ci-run"}' \
- "$flavor" "$owner" "$repo" "$stock"
- return 0
- fi
- fi
- printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager,manager-spoofed"}' \
- "$flavor" "$owner" "$repo" "$run_id" "$stock"
- ;;
- resukisu)
- owner="ReSukiSU"; repo="ReSukiSU"; wf="build-manager.yml"
- if [[ -n "${INPUT_RESUKISU_COMMIT:-}" ]]; then
- stock="$INPUT_RESUKISU_COMMIT"
- if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
- echo "ERROR: resukisu_commit must be a 40-char SHA (got: $stock)" >&2
- exit 1
- fi
- else
- stock=$(resolve_sha https://github.com/ReSukiSU/ReSukiSU.git refs/heads/main)
- fi
- run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
- "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
- | jq -r '.workflow_runs[0].id // empty')
- if [[ -z "${run_id}" ]]; then
- if [[ -z "${INPUT_RESUKISU_COMMIT:-}" ]]; then
- echo "No manager build for stock ${stock}, falling back to latest successful run" >&2
- fb=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?branch=main&status=success&per_page=1")
- run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
- stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
- if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
- fi
- if [[ -z "${run_id}" ]]; then
- echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
- exit 1
- fi
- fi
- printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"Manager-release,Spoofed-Manager-release,Manager-debug"}' \
- "$flavor" "$owner" "$repo" "$run_id" "$stock"
- ;;
- *)
- echo "skipping unknown flavor: $flavor" >&2
- return 0
- ;;
- esac
- }
- FLAVORS="${INPUT_ROOT_FLAVOR:-All}"
- echo "Root flavor: ${FLAVORS}"
- LIST=""
- case "${FLAVORS}" in
- All) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next)"$'\n'"$(resolve_one next-susfs)"$'\n'"$(resolve_one kernelsu)"$'\n'"$(resolve_one resukisu)") ;;
- KernelSU-Next) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next)") ;;
- KernelSU-Next-SUSFS) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next-susfs)") ;;
- KernelSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one kernelsu)") ;;
- ReSukiSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one resukisu)") ;;
- *) echo "Skipping manager resolution (no root flavor / none)." ;;
- esac
- if [ -n "$LIST" ]; then
- echo "manager_list=$LIST" >> "$GITHUB_OUTPUT"
- echo "Manager sources resolved: $LIST"
- else
- echo "manager_list=[]" >> "$GITHUB_OUTPUT"
- fi
- list-manager-apks:
- needs: resolve-managers
- 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 }}
- steps:
- - uses: actions/checkout@v7
- - name: Download and scan all manager APKs
- id: scan
- shell: bash
- env:
- MANAGER_LIST: ${{ needs.resolve-managers.outputs.manager_list }}
- GH_TOKEN: ${{ github.token }}
- run: |
- set -euo pipefail
- MANAGER_LIST=$(echo "$MANAGER_LIST" | sed 's/^\[,/[/')
- if ! echo "$MANAGER_LIST" | jq empty 2>/dev/null; then
- echo "ERROR: Invalid MANAGER_LIST JSON"
- echo "Raw: $MANAGER_LIST"
- exit 0
- fi
- OUT_APKS="${GITHUB_WORKSPACE:-$PWD}/manager_apks"
- MANIFEST="${GITHUB_WORKSPACE:-$PWD}/manifest.txt"
- rm -rf "$OUT_APKS" "$MANIFEST"
- mkdir -p "$OUT_APKS"
- : > "$MANIFEST"
- echo "$MANAGER_LIST" | jq -c '.[] | select(.run_id != null and .run_id != "null")' | while read -r M; do
- OWNER=$(echo "$M" | jq -r .owner)
- REPO=$(echo "$M" | jq -r .repo)
- SRC=$(echo "$M" | jq -r .source)
- RUN_ID=$(echo "$M" | jq -r .run_id)
- WANT=$(echo "$M" | jq -r '.want // ""')
- DLDIR="dl_${REPO}"
- mkdir -p "$DLDIR"
- (
- cd "$DLDIR"
- if [ "$SRC" = "release" ]; then
- curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
- "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" > rel.json
- for URL in $(jq -r '.assets[] | select(.name | endswith(".apk")) | .browser_download_url' rel.json); do
- NAME="${URL##*/}"
- curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors -o "$NAME" "$URL"
- cp -f "$NAME" "$OUT_APKS/$NAME"
- echo "$OWNER|$REPO|$NAME" >> "$MANIFEST"
- done
- else
- AJSON=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
- -H "Authorization: Bearer $GH_TOKEN" \
- "https://api.github.com/repos/${OWNER}/${REPO}/actions/runs/${RUN_ID}/artifacts")
- if [ -z "$WANT" ]; then
- ART_IDS=$(echo "$AJSON" | jq -r '.artifacts[].id')
- else
- ART_IDS=$(echo "$AJSON" | jq -r --arg want "$WANT" '
- ($want | split(",")) as $wl |
- .artifacts[] | select(.name as $n | any($wl[]; $n == .)) | .id')
- fi
- echo "$ART_IDS" | while read -r AID; do
- [ -n "$AID" ] || continue
- curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
- -H "Authorization: Bearer $GH_TOKEN" \
- -o "_${AID}.zip" \
- "https://api.github.com/repos/${OWNER}/${REPO}/actions/artifacts/${AID}/zip"
- mkdir -p "_${AID}"
- unzip -o -q "_${AID}.zip" -d "_${AID}" || true
- rm -f "_${AID}.zip"
- for apk in _${AID}/*.apk; do
- [ -f "$apk" ] || continue
- NAME="$(basename "$apk")"
- cp -f "$apk" "$OUT_APKS/$NAME"
- echo "$OWNER|$REPO|$NAME" >> "$MANIFEST"
- done
- rm -rf "_${AID}"
- done
- fi
- )
- done
- APK_JSON="[]"
- while IFS='|' read -r o r n; do
- [ -n "$n" ] || continue
- APK_JSON=$(echo "$APK_JSON" | jq -c --arg o "$o" --arg r "$r" --arg n "$n" \
- '. + [{owner:$o,repo:$r,apk:$n}]')
- done < "$MANIFEST"
- echo "apk_list=${APK_JSON}" >> "$GITHUB_OUTPUT"
- count=$(wc -l < "$MANIFEST" 2>/dev/null | tr -d ' ' || echo 0)
- echo "Found $count manager APKs"
- ls -lh "$OUT_APKS" || true
- {
- echo "## Managers"
- echo ""
- echo "### Resolved Sources"
- echo ""
- echo "| Flavor | Upstream | Commit | Run | Artifacts |"
- echo "|---|---|---|---|---|"
- echo "$MANAGER_LIST" | jq -r '.[] | "| \(.flavor) | \(.owner)/\(.repo) | \(.stock[0:8]) | [\(.run_id)](https://github.com/\(.owner)/\(.repo)/actions/runs/\(.run_id)) | \(.want) |"' 2>/dev/null || echo "| _error parsing manager list_ | | | | |"
- echo ""
- echo "### Fetched APKs ($count)"
- echo ""
- echo "| APK | Source | Size |"
- echo "|---|---|---|"
- while IFS='|' read -r o r n; do
- [ -n "$n" ] || continue
- sz=$(stat -c%s "$OUT_APKS/$n" 2>/dev/null || stat -f%z "$OUT_APKS/$n" 2>/dev/null || echo "?")
- hr=$(numfmt --to=iec-i --suffix=B "$sz" 2>/dev/null || echo "${sz} B")
- echo "| \`$n\` | $o/$r | $hr |"
- done < "$MANIFEST"
- } >> "$GITHUB_STEP_SUMMARY"
- - name: Upload all manager APKs as one cross-blob
- uses: actions/upload-artifact@v7
- with:
- name: manager-apks
- 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
- - flavor: next-susfs
- repo: https://github.com/pershoot/KernelSU-Next.git
- ref: dev-susfs
- commit_input: kernelsu_next_susfs_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-Next-SUSFS) [[ "$FLAVOR" == "next-susfs" ]] && 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_NEXT_SUSFS_COMMIT: ${{ inputs.kernelsu_next_susfs_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" ;;
- next-susfs) pin="$KSU_NEXT_SUSFS_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:-}" ;;
- next-susfs) 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"
- # Helper to run gradle with full log on failure (tail hides root cause)
- gradle_build() {
- local task="$1"
- echo ">> ./gradlew $task"
- if ! ./gradlew $task 2>&1 | tee /tmp/gradle-$flavor.log; then
- echo "--- gradle $task FAILED for $flavor ---"
- # show full log tail for diagnosis but keep beginning
- echo "=== tail 200 ==="; tail -n 200 /tmp/gradle-$flavor.log || true
- return 1
- fi
- }
- # Copy any cargo-built ksud into jniLibs so manager bundles it (mirrors stock CI)
- copy_ksud_if_built() {
- local built
- built=$(find ../target /tmp -type f -name ksud 2>/dev/null | head -5 || true)
- if [ -n "$built" ]; then
- for arch in arm64-v8a x86_64 armeabi-v7a; do echo "ksud map $arch -> $built" | head -1; done
- mkdir -p app/src/main/jniLibs/arm64-v8a app/src/main/jniLibs/x86_64 app/src/main/jniLibs/armeabi-v7a 2>/dev/null || true
- for f in $(find .. /tmp -type f -name ksud 2>/dev/null | head -10); do echo " ksud candidate: $f"; done
- # try to place best match; gradle will still build even if absent
- cp -f $(find .. /tmp -type f -name ksud 2>/dev/null | head -1) app/src/main/jniLibs/arm64-v8a/libksud.so 2>/dev/null || true
- fi
- }
- copy_ksud_if_built || true
- if [ "$flavor" = "next" ] || [ "$flavor" = "next-susfs" ]; then
- # KernelSU-Next (+ SUSFS): two variants (normal + spoofed via ./spoof)
- # Save signing config so spoof stomp can restore it
- cp -f gradle.properties /tmp/gradle-$flavor.properties.bak 2>/dev/null || true
- cp -f /tmp/manager-$flavor.jks /tmp/manager-$flavor.jks.bak 2>/dev/null || true
- if ! gradle_build "clean assembleRelease"; then
- echo "Next normal build failed — see log above" >&2
- cat /tmp/gradle-$flavor.log 2>/dev/null | grep -i -E "error|failed|exception" | head -n 50 || true
- fi
- if [ -x ./spoof ]; then
- echo "Building spoofed variant"
- chmod +x ./spoof
- ./spoof || true
- if gradle_build "clean assembleRelease"; then
- for apk in app/build/outputs/apk/release/*.apk; do
- [ -f "$apk" ] && cp -f "$apk" "/tmp/next-spoofed.apk" || true
- done
- fi
- # Restore normal sources + signing (spoof overwrites files, don't git checkout -- .)
- git checkout -- app/src/main/java 2>/dev/null || git checkout -- . 2>/dev/null || true
- cp -f /tmp/gradle-$flavor.properties.bak gradle.properties 2>/dev/null || true
- cp -f /tmp/manager-$flavor.jks.bak /tmp/manager-$flavor.jks 2>/dev/null || true
- if [ -f /tmp/manager-$flavor.jks ] && grep -q KEYSTORE gradle.properties 2>/dev/null; then
- echo "Restored signing after spoof"
- fi
- gradle_build "clean assembleRelease" || true
- fi
- elif [ "$flavor" = "kernelsu" ]; then
- # tiann/KernelSU: single manager, repack embeds ksud+LKM
- gradle_build "clean assembleRelease" || gradle_build "assembleRelease" || true
- 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"
- 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
- gradle_build "clean assembleRelease" || true
- gradle_build "assembleDebug" || true
- if [ -x ./randomizer ]; then
- ./randomizer 2>&1 | tail -20 || true
- gradle_build "clean assembleRelease" || 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"
- count=$(wc -l < /tmp/list.txt 2>/dev/null | tr -d ' ' || echo 0)
- {
- echo "## Managers — Built from Source ($count)"
- echo ""
- if [ "$count" = "0" ]; then
- echo "_No APKs produced._"
- else
- echo "| APK | Size |"
- echo "|---|---|"
- while IFS= read -r f; do
- [ -n "$f" ] || continue
- n="$(basename "$f")"
- sz=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || echo "?")
- hr=$(numfmt --to=iec-i --suffix=B "$sz" 2>/dev/null || echo "${sz} B")
- echo "| \`$n\` | $hr |"
- done < /tmp/list.txt
- fi
- } >> "$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)
- # Skips entirely when apk_list is empty/[] to avoid "Matrix vector item does not contain any values" (Line 818 col 15)
- publish-manager-artifacts:
- needs:
- - list-manager-apks
- - collect-source-managers
- if: ${{ always() && ( (needs.list-manager-apks.outputs.apk_list != '' && needs.list-manager-apks.outputs.apk_list != '[]') || (needs.collect-source-managers.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 != '' && 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
- - name: Download manager cross-blob
- uses: actions/download-artifact@v7
- with:
- name: manager-apks
- path: manager_apks
- merge-multiple: true
- - name: Upload ${{ matrix.item.apk }} as its own artifact
- uses: actions/upload-artifact@v7
- with:
- name: ${{ matrix.item.apk }}
- path: manager_apks/${{ matrix.item.apk }}
- if-no-files-found: error
|