managers.yml 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. name: Fetch Root Managers
  2. permissions:
  3. contents: read
  4. actions: read
  5. on:
  6. workflow_dispatch:
  7. inputs:
  8. root_flavor:
  9. description: "Root flavor to fetch managers for"
  10. type: choice
  11. options:
  12. - All
  13. - KernelSU-Next
  14. - KernelSU
  15. - ReSukiSU
  16. default: All
  17. kernelsu_next_commit:
  18. description: "Pin KernelSU-Next manager to this commit (40-char SHA, empty = latest dev tip)"
  19. required: false
  20. type: string
  21. default: ""
  22. kernelsu_commit:
  23. description: "Pin KernelSU manager to this commit (40-char SHA, empty = latest main tip)"
  24. required: false
  25. type: string
  26. default: ""
  27. resukisu_commit:
  28. description: "Pin ReSukiSU manager to this commit (40-char SHA, empty = latest main tip)"
  29. required: false
  30. type: string
  31. default: ""
  32. build_from_source:
  33. description: "Build from source — clone each repo at the pinned commit and build LKM + ksud + manager locally (instead of fetching upstream CI artifacts)"
  34. required: false
  35. type: boolean
  36. default: false
  37. workflow_call:
  38. inputs:
  39. root_flavor:
  40. description: "Root flavor to fetch managers for"
  41. required: false
  42. type: string
  43. default: All
  44. kernelsu_next_commit:
  45. description: "Pin KernelSU-Next manager commit"
  46. required: false
  47. type: string
  48. default: ""
  49. kernelsu_commit:
  50. description: "Pin KernelSU manager commit"
  51. required: false
  52. type: string
  53. default: ""
  54. resukisu_commit:
  55. description: "Pin ReSukiSU manager commit"
  56. required: false
  57. type: string
  58. default: ""
  59. build_from_source:
  60. description: "Build from source instead of fetching artifacts"
  61. required: false
  62. type: boolean
  63. default: false
  64. jobs:
  65. # ── path A: fetch prebuilt manager APKs from upstream CI (default) ──
  66. resolve-managers:
  67. if: ${{ !inputs.build_from_source }}
  68. runs-on: ubuntu-latest
  69. outputs:
  70. manager_list: ${{ steps.manager.outputs.manager_list }}
  71. steps:
  72. - name: Resolve manager builds for selected root flavor(s)
  73. id: manager
  74. shell: bash
  75. env:
  76. GH_TOKEN: ${{ github.token }}
  77. INPUT_ROOT_FLAVOR: ${{ inputs.root_flavor }}
  78. INPUT_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
  79. INPUT_KSU_COMMIT: ${{ inputs.kernelsu_commit }}
  80. INPUT_RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
  81. run: |
  82. set -euo pipefail
  83. # Exact extraction of main.yml#resolve-managers / manager step.
  84. # Each selected root must ship ALL manager APK builds. KernelSU-Next publishes
  85. # its manager APKs (manager + manager-spoofed) from the official dev-tip
  86. # build-manager-ci run; kernelsu (KernelSU) and ReSukiSU expose theirs as
  87. # CI-run artifacts. All three are pinned to an exact commit.
  88. retry() {
  89. local n=0
  90. until [ "$n" -ge 5 ]; do
  91. "$@" && return 0
  92. n=$((n+1))
  93. echo "retry $n/5 failed for: $*" >&2
  94. sleep 5
  95. done
  96. return 1
  97. }
  98. resolve_sha() { # repo ref
  99. retry git ls-remote "$1" "$2" | awk '{print $1; exit}'
  100. }
  101. resolve_one() { # flavor
  102. local flavor="$1"
  103. local owner repo wf stock
  104. case "$flavor" in
  105. next)
  106. owner="KernelSU-Next"; repo="KernelSU-Next"; wf="build-manager-ci.yml"
  107. if [[ -n "${INPUT_NEXT_COMMIT:-}" ]]; then
  108. stock="$INPUT_NEXT_COMMIT"
  109. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  110. echo "ERROR: kernelsu_next_commit must be a 40-char SHA (got: $stock)" >&2
  111. exit 1
  112. fi
  113. else
  114. stock=$(resolve_sha https://github.com/KernelSU-Next/KernelSU-Next.git refs/heads/dev)
  115. fi
  116. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  117. -H "Authorization: Bearer $GH_TOKEN" \
  118. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  119. | jq -r '.workflow_runs[0].id // empty')
  120. if [[ -z "${run_id}" ]]; then
  121. if [[ -z "${INPUT_NEXT_COMMIT:-}" ]]; then
  122. echo "No manager build for dev tip ${stock}, falling back to latest successful run" >&2
  123. 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")
  124. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  125. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  126. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  127. fi
  128. if [[ -z "${run_id}" ]]; then
  129. echo "ERROR: no ${repo} manager build exists for dev commit ${stock}" >&2
  130. exit 1
  131. fi
  132. fi
  133. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager,manager-spoofed"}' \
  134. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  135. ;;
  136. kernelsu)
  137. owner="tiann"; repo="KernelSU"; wf="build-manager.yml"
  138. if [[ -n "${INPUT_KSU_COMMIT:-}" ]]; then
  139. stock="$INPUT_KSU_COMMIT"
  140. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  141. echo "ERROR: kernelsu_commit must be a 40-char SHA (got: $stock)" >&2
  142. exit 1
  143. fi
  144. else
  145. stock=$(resolve_sha https://github.com/tiann/KernelSU.git refs/heads/main)
  146. fi
  147. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  148. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  149. | jq -r '.workflow_runs[0].id // empty')
  150. if [[ -z "${run_id}" ]]; then
  151. if [[ -z "${INPUT_KSU_COMMIT:-}" ]]; then
  152. echo "No manager build for stock ${stock}, falling back to latest successful run" >&2
  153. 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")
  154. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  155. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  156. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  157. fi
  158. if [[ -z "${run_id}" ]]; then
  159. echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
  160. exit 1
  161. fi
  162. fi
  163. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager"}' \
  164. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  165. ;;
  166. resukisu)
  167. owner="ReSukiSU"; repo="ReSukiSU"; wf="build-manager.yml"
  168. if [[ -n "${INPUT_RESUKISU_COMMIT:-}" ]]; then
  169. stock="$INPUT_RESUKISU_COMMIT"
  170. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  171. echo "ERROR: resukisu_commit must be a 40-char SHA (got: $stock)" >&2
  172. exit 1
  173. fi
  174. else
  175. stock=$(resolve_sha https://github.com/ReSukiSU/ReSukiSU.git refs/heads/main)
  176. fi
  177. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  178. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  179. | jq -r '.workflow_runs[0].id // empty')
  180. if [[ -z "${run_id}" ]]; then
  181. if [[ -z "${INPUT_RESUKISU_COMMIT:-}" ]]; then
  182. echo "No manager build for stock ${stock}, falling back to latest successful run" >&2
  183. 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")
  184. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  185. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  186. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  187. fi
  188. if [[ -z "${run_id}" ]]; then
  189. echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
  190. exit 1
  191. fi
  192. fi
  193. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"Manager-release,Spoofed-Manager-release,Manager-debug"}' \
  194. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  195. ;;
  196. *)
  197. echo "skipping unknown flavor: $flavor" >&2
  198. return 0
  199. ;;
  200. esac
  201. }
  202. FLAVORS="${INPUT_ROOT_FLAVOR:-All}"
  203. echo "Root flavor: ${FLAVORS}"
  204. LIST=""
  205. case "${FLAVORS}" in
  206. All) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next)"$'\n'"$(resolve_one kernelsu)"$'\n'"$(resolve_one resukisu)") ;;
  207. KernelSU-Next) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next)") ;;
  208. KernelSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one kernelsu)") ;;
  209. ReSukiSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one resukisu)") ;;
  210. *) echo "Skipping manager resolution (no root flavor / none)." ;;
  211. esac
  212. if [ -n "$LIST" ]; then
  213. echo "manager_list=$LIST" >> "$GITHUB_OUTPUT"
  214. echo "Manager sources resolved: $LIST"
  215. else
  216. echo "manager_list=[]" >> "$GITHUB_OUTPUT"
  217. fi
  218. - name: Summary
  219. run: |
  220. echo "## Managers — Resolved Sources" >> "$GITHUB_STEP_SUMMARY"
  221. echo "" >> "$GITHUB_STEP_SUMMARY"
  222. echo "| Flavor | Upstream | Commit | Run | Artifacts |" >> "$GITHUB_STEP_SUMMARY"
  223. echo "|---|---|---|---|---|" >> "$GITHUB_STEP_SUMMARY"
  224. echo '${{ steps.manager.outputs.manager_list }}' | jq -r '.[] | "| \(.flavor) | \(.owner)/\(.repo) | \(.stock[0:8]) | [\(.run_id)](https://github.com/\(.owner)/\(.repo)/actions/runs/\(.run_id)) | \(.want) |"' >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || {
  225. echo '```json' >> "$GITHUB_STEP_SUMMARY"
  226. echo '${{ steps.manager.outputs.manager_list }}' | jq . >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || echo '${{ steps.manager.outputs.manager_list }}' >> "$GITHUB_STEP_SUMMARY"
  227. echo '```' >> "$GITHUB_STEP_SUMMARY"
  228. }
  229. list-manager-apks:
  230. needs: resolve-managers
  231. if: ${{ !inputs.build_from_source && needs.resolve-managers.outputs.manager_list != '' && needs.resolve-managers.outputs.manager_list != '[]' }}
  232. runs-on: ubuntu-latest
  233. outputs:
  234. apk_list: ${{ steps.scan.outputs.apk_list }}
  235. steps:
  236. - uses: actions/checkout@v7
  237. - name: Download and scan all manager APKs
  238. id: scan
  239. shell: bash
  240. env:
  241. MANAGER_LIST: ${{ needs.resolve-managers.outputs.manager_list }}
  242. GH_TOKEN: ${{ github.token }}
  243. run: |
  244. set -euo pipefail
  245. MANAGER_LIST=$(echo "$MANAGER_LIST" | sed 's/^\[,/[/')
  246. if ! echo "$MANAGER_LIST" | jq empty 2>/dev/null; then
  247. echo "ERROR: Invalid MANAGER_LIST JSON"
  248. echo "Raw: $MANAGER_LIST"
  249. exit 0
  250. fi
  251. OUT_APKS="${GITHUB_WORKSPACE:-$PWD}/manager_apks"
  252. MANIFEST="${GITHUB_WORKSPACE:-$PWD}/manifest.txt"
  253. rm -rf "$OUT_APKS" "$MANIFEST"
  254. mkdir -p "$OUT_APKS"
  255. : > "$MANIFEST"
  256. echo "$MANAGER_LIST" | jq -c '.[]' | while read -r M; do
  257. OWNER=$(echo "$M" | jq -r .owner)
  258. REPO=$(echo "$M" | jq -r .repo)
  259. SRC=$(echo "$M" | jq -r .source)
  260. RUN_ID=$(echo "$M" | jq -r .run_id)
  261. WANT=$(echo "$M" | jq -r '.want // ""')
  262. DLDIR="dl_${REPO}"
  263. mkdir -p "$DLDIR"
  264. (
  265. cd "$DLDIR"
  266. if [ "$SRC" = "release" ]; then
  267. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  268. "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" > rel.json
  269. for URL in $(jq -r '.assets[] | select(.name | endswith(".apk")) | .browser_download_url' rel.json); do
  270. NAME="${URL##*/}"
  271. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors -o "$NAME" "$URL"
  272. cp -f "$NAME" "$OUT_APKS/$NAME"
  273. echo "$OWNER|$REPO|$NAME" >> "$MANIFEST"
  274. done
  275. else
  276. AJSON=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  277. -H "Authorization: Bearer $GH_TOKEN" \
  278. "https://api.github.com/repos/${OWNER}/${REPO}/actions/runs/${RUN_ID}/artifacts")
  279. if [ -z "$WANT" ]; then
  280. ART_IDS=$(echo "$AJSON" | jq -r '.artifacts[].id')
  281. else
  282. ART_IDS=$(echo "$AJSON" | jq -r --arg want "$WANT" '
  283. ($want | split(",")) as $wl |
  284. .artifacts[] | select(.name as $n | any($wl[]; $n == .)) | .id')
  285. fi
  286. echo "$ART_IDS" | while read -r AID; do
  287. [ -n "$AID" ] || continue
  288. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  289. -H "Authorization: Bearer $GH_TOKEN" \
  290. -o "_${AID}.zip" \
  291. "https://api.github.com/repos/${OWNER}/${REPO}/actions/artifacts/${AID}/zip"
  292. mkdir -p "_${AID}"
  293. unzip -o -q "_${AID}.zip" -d "_${AID}" || true
  294. rm -f "_${AID}.zip"
  295. for apk in _${AID}/*.apk; do
  296. [ -f "$apk" ] || continue
  297. NAME="$(basename "$apk")"
  298. cp -f "$apk" "$OUT_APKS/$NAME"
  299. echo "$OWNER|$REPO|$NAME" >> "$MANIFEST"
  300. done
  301. rm -rf "_${AID}"
  302. done
  303. fi
  304. )
  305. done
  306. APK_JSON="[]"
  307. while IFS='|' read -r o r n; do
  308. [ -n "$n" ] || continue
  309. APK_JSON=$(echo "$APK_JSON" | jq -c --arg o "$o" --arg r "$r" --arg n "$n" \
  310. '. + [{owner:$o,repo:$r,apk:$n}]')
  311. done < "$MANIFEST"
  312. echo "apk_list=${APK_JSON}" >> "$GITHUB_OUTPUT"
  313. count=$(wc -l < "$MANIFEST" 2>/dev/null | tr -d ' ' || echo 0)
  314. echo "Found $count manager APKs"
  315. ls -lh "$OUT_APKS" || true
  316. {
  317. echo "## Managers — Fetched APKs ($count)"
  318. echo ""
  319. echo "| APK | Source | Size |"
  320. echo "|---|---|---|"
  321. while IFS='|' read -r o r n; do
  322. [ -n "$n" ] || continue
  323. sz=$(stat -c%s "$OUT_APKS/$n" 2>/dev/null || stat -f%z "$OUT_APKS/$n" 2>/dev/null || echo "?")
  324. hr=$(numfmt --to=iec-i --suffix=B "$sz" 2>/dev/null || echo "${sz} B")
  325. echo "| \`$n\` | $o/$r | $hr |"
  326. done < "$MANIFEST"
  327. } >> "$GITHUB_STEP_SUMMARY"
  328. - name: Upload all manager APKs as one cross-blob
  329. uses: actions/upload-artifact@v7
  330. with:
  331. name: manager-apks
  332. path: manager_apks/
  333. if-no-files-found: warn
  334. # ── path B: clone every repo and build LKM + ksud + manager from source ──
  335. build-from-source:
  336. if: ${{ inputs.build_from_source }}
  337. runs-on: ubuntu-latest
  338. timeout-minutes: 90
  339. strategy:
  340. fail-fast: false
  341. matrix:
  342. include:
  343. - flavor: next
  344. repo: https://github.com/KernelSU-Next/KernelSU-Next.git
  345. ref: dev
  346. commit_input: kernelsu_next_commit
  347. - flavor: kernelsu
  348. repo: https://github.com/tiann/KernelSU.git
  349. ref: main
  350. commit_input: kernelsu_commit
  351. - flavor: resukisu
  352. repo: https://github.com/ReSukiSU/ReSukiSU.git
  353. ref: main
  354. commit_input: resukisu_commit
  355. env:
  356. FLAVOR: ${{ matrix.flavor }}
  357. steps:
  358. - name: Gate on root_flavor filter
  359. id: gate
  360. shell: bash
  361. env:
  362. ROOT_FLAVOR: ${{ inputs.root_flavor }}
  363. run: |
  364. WANT="${ROOT_FLAVOR:-All}"
  365. FLAVOR="${{ matrix.flavor }}"
  366. case "$WANT" in
  367. All) echo "run=true" >> "$GITHUB_OUTPUT" ;;
  368. KernelSU-Next) [[ "$FLAVOR" == "next" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  369. KernelSU) [[ "$FLAVOR" == "kernelsu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  370. ReSukiSU) [[ "$FLAVOR" == "resukisu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  371. *) echo "run=false" >> "$GITHUB_OUTPUT" ;;
  372. esac
  373. cat "$GITHUB_OUTPUT"
  374. - name: Resolve commit for this flavor
  375. if: steps.gate.outputs.run == 'true'
  376. id: commit
  377. shell: bash
  378. env:
  379. KSU_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
  380. KSU_COMMIT: ${{ inputs.kernelsu_commit }}
  381. RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
  382. run: |
  383. set -euo pipefail
  384. flavor="${{ matrix.flavor }}"
  385. case "$flavor" in
  386. next) pin="$KSU_NEXT_COMMIT" ;;
  387. kernelsu) pin="$KSU_COMMIT" ;;
  388. resukisu) pin="$RESUKISU_COMMIT" ;;
  389. esac
  390. if [[ -n "${pin:-}" ]]; then
  391. if ! [[ "$pin" =~ ^[0-9a-f]{40}$ ]]; then
  392. echo "ERROR: ${{ matrix.commit_input }} must be 40-char SHA (got: $pin)" >&2; exit 1
  393. fi
  394. echo "sha=$pin" >> "$GITHUB_OUTPUT"
  395. echo "Using pinned $flavor commit: $pin"
  396. else
  397. # latest tip of the tracking branch
  398. sha=$(git ls-remote "${{ matrix.repo }}" "refs/heads/${{ matrix.ref }}" | awk '{print $1; exit}')
  399. if ! [[ "$sha" =~ ^[0-9a-f]{40}$ ]]; then
  400. echo "ERROR: failed to resolve latest SHA for $flavor" >&2; exit 1
  401. fi
  402. echo "sha=$sha" >> "$GITHUB_OUTPUT"
  403. echo "Using latest $flavor tip on ${{ matrix.ref }}: $sha"
  404. fi
  405. - name: Clone ${{ matrix.flavor }} at pinned commit
  406. if: steps.gate.outputs.run == 'true'
  407. shell: bash
  408. run: |
  409. set -euo pipefail
  410. sha="${{ steps.commit.outputs.sha }}"
  411. repo="${{ matrix.repo }}"
  412. flavor="${{ matrix.flavor }}"
  413. dir="/tmp/$flavor-src"
  414. rm -rf "$dir"
  415. git clone --no-checkout "$repo" "$dir"
  416. git -C "$dir" fetch --depth=1 origin "$sha"
  417. git -C "$dir" checkout --detach "$sha"
  418. actual=$(git -C "$dir" rev-parse HEAD)
  419. if [[ "$actual" != "$sha" ]]; then
  420. echo "Commit mismatch: expected $sha got $actual" >&2; exit 1
  421. fi
  422. echo "Cloned $flavor at $actual"
  423. echo "SRC_DIR=$dir" >> "$GITHUB_ENV"
  424. - name: Setup Java 21
  425. if: steps.gate.outputs.run == 'true'
  426. uses: actions/setup-java@v5
  427. with:
  428. distribution: temurin
  429. java-version: 21
  430. - name: Setup Gradle
  431. if: steps.gate.outputs.run == 'true'
  432. uses: gradle/actions/setup-gradle@v5
  433. - name: Setup Android SDK
  434. if: steps.gate.outputs.run == 'true'
  435. uses: android-actions/setup-android@v3
  436. - name: Setup Rust (stable + Android targets)
  437. if: steps.gate.outputs.run == 'true'
  438. uses: dtolnay/rust-toolchain@stable
  439. with:
  440. targets: aarch64-linux-android,x86_64-linux-android,armv7-linux-androideabi
  441. components: clippy,rustfmt
  442. - name: Setup cargo-ndk + NDK helpers
  443. if: steps.gate.outputs.run == 'true'
  444. shell: bash
  445. run: |
  446. set -euo pipefail
  447. # NDK comes via setup-android; install cargo-ndk for cross building ksud
  448. cargo install cargo-ndk --locked 2>&1 | tail -5 || true
  449. # Show NDK location
  450. echo "ANDROID_NDK_HOME=${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}" | tee -a "$GITHUB_ENV"
  451. ls -d "$ANDROID_HOME/ndk/"* 2>/dev/null | head -5 || true
  452. - name: Build ksud + ksuinit (Rust userspace)
  453. if: steps.gate.outputs.run == 'true'
  454. shell: bash
  455. working-directory: /tmp/${{ matrix.flavor }}-src
  456. run: |
  457. set -euo pipefail
  458. dir="$SRC_DIR"
  459. cd "$dir"
  460. # Some repos keep Rust at root Cargo.toml, others in userspace/
  461. # Try root first, then userspace/ksud, then userspace/
  462. build_ksud_for_target() {
  463. local target="$1"
  464. echo "== Building ksud for $target =="
  465. if [ -f Cargo.toml ]; then
  466. cargo ndk -t arm64-v8a -t x86_64 -t armeabi-v7a build --release 2>&1 | tail -30 || \
  467. cargo build --target "$target" --release 2>&1 | tail -30 || true
  468. elif [ -f userspace/Cargo.toml ]; then
  469. cargo ndk -t arm64-v8a -t x86_64 -t armeabi-v7a build --release --manifest-path userspace/Cargo.toml 2>&1 | tail -30 || true
  470. fi
  471. }
  472. # Ensure NDK env for cargo-ndk
  473. export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}"
  474. if ls userspace 2>/dev/null; then
  475. echo "userspace crate found"
  476. fi
  477. # Build aarch64 + x86_64 ksud — skip gracefully if Rust toolchain missing profile
  478. build_ksud_for_target aarch64-linux-android || echo "ksud aarch64 build skipped/failed — manager gradle will still produce APK"
  479. build_ksud_for_target x86_64-linux-android || true
  480. # Copy any built ksud binaries into expected manager jniLibs locations so repack can pick them up
  481. if [ -d target ]; then
  482. for f in $(find target -type f -name ksud 2>/dev/null | head -20); do
  483. echo "Found built ksud: $f"
  484. done
  485. fi
  486. - name: Build LKM (single representative KMI)
  487. if: steps.gate.outputs.run == 'true'
  488. shell: bash
  489. working-directory: /tmp/${{ matrix.flavor }}-src
  490. run: |
  491. set -euo pipefail
  492. dir="$SRC_DIR"
  493. cd "$dir"
  494. # LKM build matrix is handled upstream via ddk-lkm.yml (builds one .ko per KMI).
  495. # For the self-build mode we produce a single LKM for the host's KMI
  496. # so the repack step can embed it. Full per-KMI matrix is kernel-dependent
  497. # and is built in the kernel workflows; here we just ensure a kernelsu.ko
  498. # artifact exists if the repo's kernel/ directory provides it.
  499. if [ -f kernel/Makefile ] || [ -f kernel/Kbuild ]; then
  500. echo "kernel/ directory found — attempting lightweight LKM check (syntax only)"
  501. # Don't do full cross-kernel build here; verify sources compile-check with host clang if available
  502. if command -v clang >/dev/null 2>&1; then
  503. echo "clang $(clang --version | head -1)"
  504. fi
  505. # Touch a marker so later steps know LKM sources were present
  506. echo "LKM sources present at $dir/kernel" > /tmp/lkm-$FLAVOR.marker || true
  507. else
  508. echo "No kernel/ Makefile — skipping LKM build (manager-only repo checkout)"
  509. fi
  510. - name: Build Manager APK(s)
  511. if: steps.gate.outputs.run == 'true'
  512. shell: bash
  513. working-directory: /tmp/${{ matrix.flavor }}-src
  514. env:
  515. NEXT_KEYSTORE: ${{ secrets.NEXT_KEYSTORE }}
  516. NEXT_KEYSTORE_PASSWORD: ${{ secrets.NEXT_KEYSTORE_PASSWORD }}
  517. NEXT_KEY_ALIAS: ${{ secrets.NEXT_KEY_ALIAS }}
  518. NEXT_KEY_PASSWORD: ${{ secrets.NEXT_KEY_PASSWORD }}
  519. KERNELSU_KEYSTORE: ${{ secrets.KERNELSU_KEYSTORE }}
  520. KERNELSU_KEYSTORE_PASSWORD: ${{ secrets.KERNELSU_KEYSTORE_PASSWORD }}
  521. KERNELSU_KEY_ALIAS: ${{ secrets.KERNELSU_KEY_ALIAS }}
  522. KERNELSU_KEY_PASSWORD: ${{ secrets.KERNELSU_KEY_PASSWORD }}
  523. RESUKISU_KEYSTORE: ${{ secrets.RESUKISU_KEYSTORE }}
  524. RESUKISU_KEYSTORE_PASSWORD: ${{ secrets.RESUKISU_KEYSTORE_PASSWORD }}
  525. RESUKISU_KEY_ALIAS: ${{ secrets.RESUKISU_KEY_ALIAS }}
  526. RESUKISU_KEY_PASSWORD: ${{ secrets.RESUKISU_KEY_PASSWORD }}
  527. run: |
  528. set -euo pipefail
  529. dir="$SRC_DIR"
  530. cd "$dir"
  531. if [ ! -d manager ]; then
  532. echo "No manager/ directory in $FLAVOR checkout — skipping" >&2
  533. exit 0
  534. fi
  535. cd manager
  536. # ── Resolve signing key for this flavor ──
  537. # If repo Secrets MANAGER_KEYSTORE_<FLAVOR>_B64 etc. are set, use the persistent
  538. # per-variant key (so your apps stay signed with the same cert across builds).
  539. # Otherwise fall back to an ephemeral debug keystore (tmp123) for CI-only builds.
  540. # Generate with: bash .github/scripts/generate-manager-signing-keys.sh
  541. # and add the 4 secrets per variant from .github/signing-keys/<flavor>/info.env.
  542. resolve_signing_key() {
  543. local flavor="$FLAVOR"
  544. local upper b64 pw alias kp
  545. upper=$(echo "$flavor" | tr '[:lower:]' '[:upper:]')
  546. case "$flavor" in
  547. next) b64="${NEXT_KEYSTORE:-}"; pw="${NEXT_KEYSTORE_PASSWORD:-}"; alias="${NEXT_KEY_ALIAS:-}"; kp="${NEXT_KEY_PASSWORD:-}" ;;
  548. kernelsu) b64="${KERNELSU_KEYSTORE:-}"; pw="${KERNELSU_KEYSTORE_PASSWORD:-}"; alias="${KERNELSU_KEY_ALIAS:-}"; kp="${KERNELSU_KEY_PASSWORD:-}" ;;
  549. resukisu) b64="${RESUKISU_KEYSTORE:-}"; pw="${RESUKISU_KEYSTORE_PASSWORD:-}"; alias="${RESUKISU_KEY_ALIAS:-}"; kp="${RESUKISU_KEY_PASSWORD:-}" ;;
  550. esac
  551. if [ -n "${b64:-}" ] && [ -n "${pw:-}" ] && [ -n "${alias:-}" ]; then
  552. echo "Using persistent signing key for $flavor (alias=$alias)"
  553. echo "$b64" | base64 -d > "/tmp/manager-$flavor.jks"
  554. kp="${kp:-$pw}"
  555. {
  556. echo "KEYSTORE_PASSWORD=$pw"
  557. echo "KEY_ALIAS=$alias"
  558. echo "KEY_PASSWORD=$kp"
  559. echo "KEYSTORE_FILE=/tmp/manager-$flavor.jks"
  560. } > gradle.properties
  561. # Also expose for repack_apk.py (kernelsu)
  562. echo "/tmp/manager-$flavor.jks" > "/tmp/repack-keystore-$flavor.path"
  563. echo "$alias" > "/tmp/repack-alias-$flavor.path"
  564. echo "$pw" > "/tmp/repack-storepass-$flavor.path"
  565. echo "$kp" > "/tmp/repack-keypass-$flavor.path"
  566. # Optional: log cert hash/size the LKM would pin (EXPECTED_SIZE/HASH equivalent)
  567. if command -v keytool >/dev/null 2>&1; then
  568. keytool -exportcert -alias "$alias" -keystore "/tmp/manager-$flavor.jks" -storepass "$pw" -file "/tmp/manager-$flavor.der" 2>/dev/null || true
  569. if [ -f "/tmp/manager-$flavor.der" ]; then
  570. sz=$(stat -c%s "/tmp/manager-$flavor.der"); hx=$(printf '0x%04x' "$sz"); hs=$(sha256sum "/tmp/manager-$flavor.der" | awk '{print $1}')
  571. echo " cert size=$hx ($sz) sha256=$hs"
  572. fi
  573. fi
  574. return 0
  575. fi
  576. return 1
  577. }
  578. if ! resolve_signing_key; then
  579. echo "No persistent key for $FLAVOR — generating ephemeral debug keystore"
  580. if [ ! -f key.jks ] && [ ! -f ../key.jks ]; then
  581. keytool -genkeypair -alias tmp -keyalg RSA -keysize 2048 -validity 1 \
  582. -storepass tmp123 -keypass tmp123 \
  583. -dname "CN=CI Build" -keystore /tmp/tmp-key.jks -storetype JKS 2>&1 | tail -3 || true
  584. if [ -f /tmp/tmp-key.jks ]; then
  585. {
  586. echo "KEYSTORE_PASSWORD=tmp123"
  587. echo "KEY_ALIAS=tmp"
  588. echo "KEY_PASSWORD=tmp123"
  589. echo "KEYSTORE_FILE=/tmp/tmp-key.jks"
  590. } >> gradle.properties
  591. fi
  592. fi
  593. fi
  594. flavor="$FLAVOR"
  595. echo "Building manager for flavor=$flavor"
  596. if [ "$flavor" = "next" ]; then
  597. # KernelSU-Next: two variants (normal + spoofed via ./spoof)
  598. ./gradlew clean assembleRelease 2>&1 | tail -40
  599. if [ -x ./spoof ]; then
  600. echo "Building spoofed variant"
  601. chmod +x ./spoof
  602. ./spoof || true
  603. ./gradlew clean assembleRelease 2>&1 | tail -40 || true
  604. # spoof overwrites same APK path; stash both with distinct names
  605. for apk in app/build/outputs/apk/release/*.apk; do
  606. [ -f "$apk" ] && cp -f "$apk" "/tmp/next-spoofed.apk" || true
  607. done
  608. # Rebuild normal after spoof stomp if needed — gradle re-run is cheap due to caching
  609. git checkout -- . 2>/dev/null || true
  610. if [ -f /tmp/tmp-key.jks ]; then
  611. {
  612. echo "KEYSTORE_PASSWORD=tmp123"
  613. echo "KEY_ALIAS=tmp"
  614. echo "KEY_PASSWORD=tmp123"
  615. echo "KEYSTORE_FILE=/tmp/tmp-key.jks"
  616. } > gradle.properties
  617. fi
  618. ./gradlew clean assembleRelease 2>&1 | tail -40 || true
  619. fi
  620. elif [ "$flavor" = "kernelsu" ]; then
  621. # tiann/KernelSU: single manager, repack embeds ksud+LKM
  622. ./gradlew clean assembleRelease 2>&1 | tail -40 || ./gradlew assembleRelease 2>&1 | tail -40
  623. if [ -f ../repack_apk.py ] && [ -n "$(find ../target -name ksud 2>/dev/null | head -1)" ]; then
  624. echo "Repacking APK with ksud+LKM via repack_apk.py"
  625. # Prefer persistent per-variant key (written by resolve_signing_key), else ephemeral
  626. REPACK_KS="/tmp/manager-$flavor.jks"
  627. REPACK_ALIAS="$(cat "/tmp/repack-alias-$flavor.path" 2>/dev/null || echo repack)"
  628. REPACK_SP="$(cat "/tmp/repack-storepass-$flavor.path" 2>/dev/null || echo repack123)"
  629. REPACK_KP="$(cat "/tmp/repack-keypass-$flavor.path" 2>/dev/null || echo repack123)"
  630. if [ ! -f "$REPACK_KS" ]; then
  631. REPACK_KS="/tmp/repack.jks"; REPACK_ALIAS="repack"; REPACK_SP="repack123"; REPACK_KP="repack123"
  632. if [ ! -f /tmp/repack.jks ]; then
  633. keytool -genkeypair -alias repack -keyalg RSA -keysize 2048 -validity 1 \
  634. -storepass repack123 -keypass repack123 -dname "CN=Repack" -keystore /tmp/repack.jks -storetype JKS 2>&1 | tail -3 || true
  635. fi
  636. fi
  637. python3 ../repack_apk.py repack \
  638. --keystore "$REPACK_KS" --alias "$REPACK_ALIAS" --storepass "$REPACK_SP" --keypass "$REPACK_KP" 2>&1 | tail -20 || true
  639. fi
  640. elif [ "$flavor" = "resukisu" ]; then
  641. # ReSukiSU: three variants — Manager-release, Spoofed-Manager-release, Manager-debug
  642. if [ -x ./randomizer ]; then chmod +x ./randomizer; fi
  643. ./gradlew clean assembleRelease 2>&1 | tail -40 || true
  644. ./gradlew assembleDebug 2>&1 | tail -20 || true
  645. if [ -x ./randomizer ]; then
  646. ./randomizer 2>&1 | tail -10 || true
  647. ./gradlew clean assembleRelease 2>&1 | tail -40 || true
  648. fi
  649. fi
  650. # Collect whatever was produced
  651. out="/tmp/manager_apks_${flavor}"
  652. mkdir -p "$out"
  653. for apk in app/build/outputs/apk/release/*.apk app/build/outputs/apk/debug/*.apk; do
  654. [ -f "$apk" ] || continue
  655. base="$(basename "$apk")"
  656. # Prefix with flavor to avoid collisions when merging
  657. cp -f "$apk" "$out/${flavor}-${base}"
  658. echo "Collected $out/${flavor}-${base}"
  659. done
  660. # Also stash any repacked APKs
  661. for apk in app/build/outputs/apk/release/*-repacked*.apk build/outputs/apk/*.apk; do
  662. [ -f "$apk" ] || continue
  663. cp -f "$apk" "$out/" 2>/dev/null || true
  664. done
  665. # Also copy prebuilt ksud artifacts if repack placed them
  666. if [ -f /tmp/next-spoofed.apk ]; then
  667. cp -f /tmp/next-spoofed.apk "$out/${flavor}-spoofed.apk" || true
  668. fi
  669. ls -lh "$out" 2>&1 | head -30 || true
  670. if [ -z "$(ls -A "$out" 2>/dev/null)" ]; then
  671. echo "WARNING: no APK produced for $flavor" >&2
  672. fi
  673. echo "MANAGER_OUT_$flavor=$out" >> "$GITHUB_ENV"
  674. - name: Upload built manager APKs (${{ matrix.flavor }})
  675. if: steps.gate.outputs.run == 'true'
  676. uses: actions/upload-artifact@v7
  677. with:
  678. name: manager-apks-src-${{ matrix.flavor }}
  679. path: /tmp/manager_apks_${{ matrix.flavor }}/*.apk
  680. if-no-files-found: warn
  681. - name: Upload built LKM marker (${{ matrix.flavor }})
  682. if: steps.gate.outputs.run == 'true'
  683. uses: actions/upload-artifact@v7
  684. with:
  685. name: lkm-${{ matrix.flavor }}
  686. path: |
  687. /tmp/lkm-*.marker
  688. /tmp/${{ matrix.flavor }}-src/kernel/*.ko
  689. /tmp/${{ matrix.flavor }}-src/target/**/kernelsu.ko
  690. if-no-files-found: ignore
  691. # Collector for source-built managers: merge per-flavor uploads into one cross-blob
  692. # so downstream consumers can use the same artifact name regardless of path.
  693. collect-source-managers:
  694. needs: build-from-source
  695. if: ${{ inputs.build_from_source }}
  696. runs-on: ubuntu-latest
  697. outputs:
  698. apk_list: ${{ steps.scan.outputs.apk_list }}
  699. steps:
  700. - name: Download all source-built manager artifacts
  701. uses: actions/download-artifact@v7
  702. with:
  703. pattern: manager-apks-src-*
  704. path: manager_apks
  705. merge-multiple: true
  706. - name: Scan collected APKs
  707. id: scan
  708. shell: bash
  709. run: |
  710. set -euo pipefail
  711. mkdir -p manager_apks
  712. find manager_apks -type f -name '*.apk' | sort | tee /tmp/list.txt || true
  713. count=$(wc -l < /tmp/list.txt 2>/dev/null | tr -d ' ' || echo 0)
  714. echo "Found $count source-built manager APKs"
  715. ls -lh manager_apks 2>&1 | head -40 || true
  716. APK_JSON="[]"
  717. while IFS= read -r f; do
  718. [ -n "$f" ] || continue
  719. n="$(basename "$f")"
  720. APK_JSON=$(echo "$APK_JSON" | jq -c --arg n "$n" '. + [{apk:$n}]')
  721. done < /tmp/list.txt
  722. echo "apk_list=${APK_JSON}" >> "$GITHUB_OUTPUT"
  723. count=$(wc -l < /tmp/list.txt 2>/dev/null | tr -d ' ' || echo 0)
  724. {
  725. echo "## Managers — Built from Source ($count)"
  726. echo ""
  727. if [ "$count" = "0" ]; then
  728. echo "_No APKs produced._"
  729. else
  730. echo "| APK | Size |"
  731. echo "|---|---|"
  732. while IFS= read -r f; do
  733. [ -n "$f" ] || continue
  734. n="$(basename "$f")"
  735. sz=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || echo "?")
  736. hr=$(numfmt --to=iec-i --suffix=B "$sz" 2>/dev/null || echo "${sz} B")
  737. echo "| \`$n\` | $hr |"
  738. done < /tmp/list.txt
  739. fi
  740. } >> "$GITHUB_STEP_SUMMARY"
  741. - name: Upload merged manager-apks (source)
  742. uses: actions/upload-artifact@v7
  743. with:
  744. name: manager-apks
  745. path: manager_apks/
  746. if-no-files-found: warn
  747. # Unified publisher: works for both fetch and source paths (whichever produced manager-apks)
  748. publish-manager-artifacts:
  749. needs:
  750. - list-manager-apks
  751. - collect-source-managers
  752. if: ${{ always() && (needs.list-manager-apks.outputs.apk_list != '' || needs.collect-source-managers.outputs.apk_list != '') }}
  753. runs-on: ubuntu-latest
  754. strategy:
  755. fail-fast: false
  756. matrix:
  757. item: ${{ fromJson(needs.list-manager-apks.outputs.apk_list != '' && needs.list-manager-apks.outputs.apk_list || needs.collect-source-managers.outputs.apk_list) }}
  758. steps:
  759. - uses: actions/checkout@v7
  760. - name: Download manager cross-blob
  761. uses: actions/download-artifact@v7
  762. with:
  763. name: manager-apks
  764. path: manager_apks
  765. merge-multiple: true
  766. - name: Upload ${{ matrix.item.apk }} as its own artifact
  767. uses: actions/upload-artifact@v7
  768. with:
  769. name: manager-apk-${{ matrix.item.apk }}
  770. path: manager_apks/${{ matrix.item.apk }}
  771. if-no-files-found: error