managers.yml 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  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. - SukiSU-Ultra
  17. - KowSU
  18. - No Root
  19. - KernelSU-Next-SUSFS
  20. default: All
  21. kernelsu_next_commit:
  22. description: "Pin KernelSU-Next manager to this commit (40-char SHA, empty = latest dev tip)"
  23. required: false
  24. type: string
  25. default: ""
  26. kernelsu_commit:
  27. description: "Pin KernelSU manager to this commit (40-char SHA, empty = latest main tip)"
  28. required: false
  29. type: string
  30. default: ""
  31. resukisu_commit:
  32. description: "Pin ReSukiSU manager to this commit (40-char SHA, empty = latest main tip)"
  33. required: false
  34. type: string
  35. default: ""
  36. sukisu_ultra_commit:
  37. description: "Pin SukiSU-Ultra manager to this commit (40-char SHA, empty = latest main tip)"
  38. required: false
  39. type: string
  40. default: ""
  41. kernelsu_next_susfs_commit:
  42. description: "Pin KernelSU-Next SUSFS (pershoot dev-susfs) manager to this commit (40-char SHA, empty = latest dev-susfs tip)"
  43. required: false
  44. type: string
  45. default: ""
  46. build_from_source:
  47. description: "Build from source — clone each repo at the pinned commit and build LKM + ksud + manager locally (instead of fetching upstream CI artifacts)"
  48. required: false
  49. type: boolean
  50. default: false
  51. workflow_call:
  52. inputs:
  53. root_flavor:
  54. description: "Root flavor to fetch managers for"
  55. required: false
  56. type: string
  57. default: All
  58. kernelsu_next_commit:
  59. description: "Pin KernelSU-Next manager commit"
  60. required: false
  61. type: string
  62. default: ""
  63. kernelsu_commit:
  64. description: "Pin KernelSU manager commit"
  65. required: false
  66. type: string
  67. default: ""
  68. resukisu_commit:
  69. description: "Pin ReSukiSU manager commit"
  70. required: false
  71. type: string
  72. default: ""
  73. sukisu_ultra_commit:
  74. description: "Pin SukiSU-Ultra manager commit"
  75. required: false
  76. type: string
  77. default: ""
  78. kernelsu_next_susfs_commit:
  79. description: "Pin KernelSU-Next SUSFS manager commit"
  80. required: false
  81. type: string
  82. default: ""
  83. build_from_source:
  84. description: "Build from source instead of fetching artifacts"
  85. required: false
  86. type: boolean
  87. default: false
  88. jobs:
  89. # ── path A: fetch prebuilt manager APKs from upstream CI (default) ──
  90. resolve-managers:
  91. if: ${{ !inputs.build_from_source }}
  92. runs-on: ubuntu-latest
  93. outputs:
  94. manager_list: ${{ steps.manager.outputs.manager_list }}
  95. steps:
  96. - name: Resolve manager builds for selected root flavor(s)
  97. id: manager
  98. shell: bash
  99. env:
  100. GH_TOKEN: ${{ github.token }}
  101. INPUT_ROOT_FLAVOR: ${{ inputs.root_flavor }}
  102. INPUT_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
  103. INPUT_KSU_COMMIT: ${{ inputs.kernelsu_commit }}
  104. INPUT_RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
  105. INPUT_SUKISU_ULTRA_COMMIT: ${{ inputs.sukisu_ultra_commit }}
  106. INPUT_NEXT_SUSFS_COMMIT: ${{ inputs.kernelsu_next_susfs_commit }}
  107. run: |
  108. set -euo pipefail
  109. # Exact extraction of main.yml#resolve-managers / manager step.
  110. # Each selected root must ship ALL manager APK builds. KernelSU-Next publishes
  111. # its manager APKs (manager + manager-spoofed) from the official dev-tip
  112. # build-manager-ci run; kernelsu (KernelSU), ReSukiSU, SukiSU-Ultra,
  113. # and KowSU expose theirs as CI-run artifacts. All are pinned to an exact commit.
  114. # No Root ships no manager.
  115. retry() {
  116. local n=0
  117. until [ "$n" -ge 5 ]; do
  118. "$@" && return 0
  119. n=$((n+1))
  120. echo "retry $n/5 failed for: $*" >&2
  121. sleep 5
  122. done
  123. return 1
  124. }
  125. resolve_sha() { # repo ref
  126. retry git ls-remote "$1" "$2" | awk '{print $1; exit}'
  127. }
  128. resolve_one() { # flavor
  129. local flavor="$1"
  130. local owner repo wf stock
  131. case "$flavor" in
  132. next)
  133. owner="KernelSU-Next"; repo="KernelSU-Next"; wf="build-manager-ci.yml"
  134. if [[ -n "${INPUT_NEXT_COMMIT:-}" ]]; then
  135. stock="$INPUT_NEXT_COMMIT"
  136. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  137. echo "ERROR: kernelsu_next_commit must be a 40-char SHA (got: $stock)" >&2
  138. exit 1
  139. fi
  140. else
  141. stock=$(resolve_sha https://github.com/KernelSU-Next/KernelSU-Next.git refs/heads/dev)
  142. fi
  143. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  144. -H "Authorization: Bearer $GH_TOKEN" \
  145. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  146. | jq -r '.workflow_runs[0].id // empty')
  147. if [[ -z "${run_id}" ]]; then
  148. if [[ -z "${INPUT_NEXT_COMMIT:-}" ]]; then
  149. echo "No manager build for dev tip ${stock}, falling back to latest successful run" >&2
  150. 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")
  151. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  152. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  153. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  154. fi
  155. if [[ -z "${run_id}" ]]; then
  156. echo "ERROR: no ${repo} manager build exists for dev commit ${stock}" >&2
  157. exit 1
  158. fi
  159. fi
  160. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager,manager-spoofed"}' \
  161. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  162. ;;
  163. kernelsu)
  164. owner="tiann"; repo="KernelSU"; wf="build-manager.yml"
  165. if [[ -n "${INPUT_KSU_COMMIT:-}" ]]; then
  166. stock="$INPUT_KSU_COMMIT"
  167. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  168. echo "ERROR: kernelsu_commit must be a 40-char SHA (got: $stock)" >&2
  169. exit 1
  170. fi
  171. else
  172. stock=$(resolve_sha https://github.com/tiann/KernelSU.git refs/heads/main)
  173. fi
  174. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  175. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  176. | jq -r '.workflow_runs[0].id // empty')
  177. if [[ -z "${run_id}" ]]; then
  178. if [[ -z "${INPUT_KSU_COMMIT:-}" ]]; then
  179. echo "No manager build for stock ${stock}, falling back to latest successful run" >&2
  180. 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")
  181. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  182. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  183. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  184. fi
  185. if [[ -z "${run_id}" ]]; then
  186. echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
  187. exit 1
  188. fi
  189. fi
  190. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager"}' \
  191. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  192. ;;
  193. next-susfs)
  194. owner="pershoot"; repo="KernelSU-Next"; wf="build-manager-ci.yml"
  195. if [[ -n "${INPUT_NEXT_SUSFS_COMMIT:-}" ]]; then
  196. stock="$INPUT_NEXT_SUSFS_COMMIT"
  197. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  198. echo "ERROR: kernelsu_next_susfs_commit must be a 40-char SHA (got: $stock)" >&2
  199. exit 1
  200. fi
  201. else
  202. stock=$(resolve_sha https://github.com/pershoot/KernelSU-Next.git refs/heads/dev-susfs)
  203. # fallback to upstream KernelSU-Next dev-susfs if pershoot fork tip not found
  204. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  205. stock=$(resolve_sha https://github.com/KernelSU-Next/KernelSU-Next.git refs/heads/dev-susfs)
  206. fi
  207. fi
  208. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  209. -H "Authorization: Bearer ***" \
  210. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  211. | jq -r '.workflow_runs[0].id // empty')
  212. if [[ -z "${run_id}" ]]; then
  213. if [[ -z "${INPUT_NEXT_SUSFS_COMMIT:-}" ]]; then
  214. echo "No manager build for dev-susfs tip ${stock}, falling back to latest successful run" >&2
  215. 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")
  216. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  217. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  218. if [[ -z "$run_id" ]]; then
  219. # dev-susfs has no CI on this fork — try dev fallback
  220. echo "No dev-susfs CI run, trying dev branch fallback" >&2
  221. 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")
  222. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  223. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  224. fi
  225. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  226. fi
  227. if [[ -z "${run_id}" ]]; then
  228. echo "WARNING: no ${owner}/${repo} manager build for dev-susfs commit ${stock} — will need build_from_source" >&2
  229. # emit empty want so fetch path skips gracefully; source build will handle it
  230. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":null,"stock":"%s","want":"manager,manager-spoofed","note":"no-ci-run"}' \
  231. "$flavor" "$owner" "$repo" "$stock"
  232. return 0
  233. fi
  234. fi
  235. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager,manager-spoofed"}' \
  236. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  237. ;;
  238. resukisu)
  239. owner="ReSukiSU"; repo="ReSukiSU"; wf="build-manager.yml"
  240. if [[ -n "${INPUT_RESUKISU_COMMIT:-}" ]]; then
  241. stock="$INPUT_RESUKISU_COMMIT"
  242. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  243. echo "ERROR: resukisu_commit must be a 40-char SHA (got: $stock)" >&2
  244. exit 1
  245. fi
  246. else
  247. stock=$(resolve_sha https://github.com/ReSukiSU/ReSukiSU.git refs/heads/main)
  248. fi
  249. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  250. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  251. | jq -r '.workflow_runs[0].id // empty')
  252. if [[ -z "${run_id}" ]]; then
  253. if [[ -z "${INPUT_RESUKISU_COMMIT:-}" ]]; then
  254. echo "No manager build for stock ${stock}, falling back to latest successful run" >&2
  255. 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")
  256. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  257. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  258. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  259. fi
  260. if [[ -z "${run_id}" ]]; then
  261. echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
  262. exit 1
  263. fi
  264. fi
  265. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"Manager-release,Spoofed-Manager-release"}' \
  266. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  267. ;;
  268. sukisu-ultra)
  269. owner="SukiSU-Ultra"; repo="SukiSU-Ultra"; wf="build-manager.yml"
  270. if [[ -n "${INPUT_SUKISU_ULTRA_COMMIT:-}" ]]; then
  271. stock="$INPUT_SUKISU_ULTRA_COMMIT"
  272. if ! [[ "$stock" =~ ^[0-9a-f]{40}$ ]]; then
  273. echo "ERROR: sukisu_ultra_commit must be a 40-char SHA (got: $stock)" >&2
  274. exit 1
  275. fi
  276. else
  277. stock=$(resolve_sha https://github.com/SukiSU-Ultra/SukiSU-Ultra.git refs/heads/main)
  278. fi
  279. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  280. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  281. | jq -r '.workflow_runs[0].id // empty')
  282. if [[ -z "${run_id}" ]]; then
  283. if [[ -z "${INPUT_SUKISU_ULTRA_COMMIT:-}" ]]; then
  284. echo "No manager build for stock ${stock}, falling back to latest successful run" >&2
  285. 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")
  286. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  287. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  288. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  289. fi
  290. if [[ -z "${run_id}" ]]; then
  291. echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
  292. exit 1
  293. fi
  294. fi
  295. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"Manager,Spoofed-Manager"}' \
  296. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  297. ;;
  298. kowsu)
  299. # KowSU has no SUSFS support and always tracks latest master.
  300. owner="KOWX712"; repo="KernelSU"; wf="build-manager.yml"
  301. stock=$(resolve_sha https://github.com/KOWX712/KernelSU.git refs/heads/master)
  302. run_id=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  303. "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?head_sha=${stock}" \
  304. | jq -r '.workflow_runs[0].id // empty')
  305. if [[ -z "${run_id}" ]]; then
  306. echo "No manager build for stock ${stock}, falling back to latest successful run" >&2
  307. fb=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://api.github.com/repos/${owner}/${repo}/actions/workflows/${wf}/runs?branch=master&status=success&per_page=1")
  308. run_id=$(echo "$fb" | jq -r '.workflow_runs[0].id // empty')
  309. stock=$(echo "$fb" | jq -r '.workflow_runs[0].head_sha // empty')
  310. if [[ -n "$run_id" ]]; then echo "Fallback to ${stock} run ${run_id}" >&2; fi
  311. if [[ -z "${run_id}" ]]; then
  312. echo "ERROR: no ${repo} manager build exists for stock commit ${stock}" >&2
  313. exit 1
  314. fi
  315. fi
  316. printf '{"flavor":"%s","owner":"%s","repo":"%s","source":"run","run_id":%s,"stock":"%s","want":"manager"}' \
  317. "$flavor" "$owner" "$repo" "$run_id" "$stock"
  318. ;;
  319. *)
  320. echo "skipping unknown flavor: $flavor" >&2
  321. return 0
  322. ;;
  323. esac
  324. }
  325. FLAVORS="${INPUT_ROOT_FLAVOR:-All}"
  326. echo "Root flavor: ${FLAVORS}"
  327. LIST=""
  328. case "${FLAVORS}" in
  329. All) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next)"$'\n'"$(resolve_one next-susfs)"$'\n'"$(resolve_one kernelsu)"$'\n'"$(resolve_one resukisu)") ;;
  330. KernelSU-Next) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next)") ;;
  331. KernelSU-Next-SUSFS) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one next-susfs)") ;;
  332. KernelSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one kernelsu)") ;;
  333. ReSukiSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one resukisu)") ;;
  334. SukiSU-Ultra) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one sukisu-ultra)") ;;
  335. KowSU) LIST=$(jq -c -n '[inputs]' <<< "$(resolve_one kowsu)") ;;
  336. *) echo "Skipping manager resolution (no root flavor / none)." ;;
  337. esac
  338. if [ -n "$LIST" ]; then
  339. echo "manager_list=$LIST" >> "$GITHUB_OUTPUT"
  340. echo "Manager sources resolved: $LIST"
  341. else
  342. echo "manager_list=[]" >> "$GITHUB_OUTPUT"
  343. fi
  344. list-manager-apks:
  345. needs: resolve-managers
  346. if: ${{ !inputs.build_from_source && needs.resolve-managers.outputs.manager_list != '' && needs.resolve-managers.outputs.manager_list != '[]' }}
  347. runs-on: ubuntu-latest
  348. outputs:
  349. apk_list: ${{ steps.scan.outputs.apk_list }}
  350. steps:
  351. - uses: actions/checkout@v7
  352. - name: Download and scan all manager APKs
  353. id: scan
  354. shell: bash
  355. env:
  356. MANAGER_LIST: ${{ needs.resolve-managers.outputs.manager_list }}
  357. GH_TOKEN: ${{ github.token }}
  358. run: |
  359. set -euo pipefail
  360. MANAGER_LIST=$(echo "$MANAGER_LIST" | sed 's/^\[,/[/')
  361. if ! echo "$MANAGER_LIST" | jq empty 2>/dev/null; then
  362. echo "ERROR: Invalid MANAGER_LIST JSON"
  363. echo "Raw: $MANAGER_LIST"
  364. exit 0
  365. fi
  366. OUT_APKS="${GITHUB_WORKSPACE:-$PWD}/manager_apks"
  367. MANIFEST="${GITHUB_WORKSPACE:-$PWD}/manifest.txt"
  368. rm -rf "$OUT_APKS" "$MANIFEST"
  369. mkdir -p "$OUT_APKS"
  370. : > "$MANIFEST"
  371. echo "$MANAGER_LIST" | jq -c '.[] | select(.run_id != null and .run_id != "null")' | while read -r M; do
  372. OWNER=$(echo "$M" | jq -r .owner)
  373. REPO=$(echo "$M" | jq -r .repo)
  374. SRC=$(echo "$M" | jq -r .source)
  375. RUN_ID=$(echo "$M" | jq -r .run_id)
  376. WANT=$(echo "$M" | jq -r '.want // ""')
  377. DLDIR="dl_${REPO}"
  378. mkdir -p "$DLDIR"
  379. (
  380. cd "$DLDIR"
  381. if [ "$SRC" = "release" ]; then
  382. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  383. "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" > rel.json
  384. for URL in $(jq -r '.assets[] | select(.name | endswith(".apk")) | .browser_download_url' rel.json); do
  385. NAME="${URL##*/}"
  386. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors -o "$NAME" "$URL"
  387. cp -f "$NAME" "$OUT_APKS/$NAME"
  388. echo "$OWNER|$REPO|$NAME" >> "$MANIFEST"
  389. done
  390. else
  391. AJSON=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  392. -H "Authorization: Bearer $GH_TOKEN" \
  393. "https://api.github.com/repos/${OWNER}/${REPO}/actions/runs/${RUN_ID}/artifacts")
  394. if [ -z "$WANT" ]; then
  395. ART_IDS=$(echo "$AJSON" | jq -r '.artifacts[].id')
  396. else
  397. ART_IDS=$(echo "$AJSON" | jq -r --arg want "$WANT" '
  398. ($want | split(",")) as $wl |
  399. .artifacts[] | select(.name as $n | any($wl[]; $n == .)) | .id')
  400. fi
  401. echo "$ART_IDS" | while read -r AID; do
  402. [ -n "$AID" ] || continue
  403. curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors \
  404. -H "Authorization: Bearer $GH_TOKEN" \
  405. -o "_${AID}.zip" \
  406. "https://api.github.com/repos/${OWNER}/${REPO}/actions/artifacts/${AID}/zip"
  407. mkdir -p "_${AID}"
  408. unzip -o -q "_${AID}.zip" -d "_${AID}" || true
  409. rm -f "_${AID}.zip"
  410. for apk in _${AID}/*.apk; do
  411. [ -f "$apk" ] || continue
  412. NAME="$(basename "$apk")"
  413. cp -f "$apk" "$OUT_APKS/$NAME"
  414. echo "$OWNER|$REPO|$NAME" >> "$MANIFEST"
  415. done
  416. rm -rf "_${AID}"
  417. done
  418. fi
  419. )
  420. done
  421. # Filter ReSukiSU to only release universal and universal spoofed
  422. if grep -q "ReSukiSU|ReSukiSU|" "$MANIFEST" 2>/dev/null; then
  423. tmp_manifest="${MANIFEST}.filtered"
  424. : > "$tmp_manifest"
  425. while IFS='|' read -r o r n; do
  426. [ -n "$n" ] || continue
  427. if [ "$o" = "ReSukiSU" ] && [ "$r" = "ReSukiSU" ]; then
  428. ln=$(echo "$n" | tr '[:upper:]' '[:lower:]')
  429. if [[ "$ln" != *universal* ]]; then echo "Dropping ReSukiSU non-universal $n" >&2; rm -f "$OUT_APKS/$n"; continue; fi
  430. if [[ "$ln" == *debug* ]]; then echo "Dropping ReSukiSU debug $n" >&2; rm -f "$OUT_APKS/$n"; continue; fi
  431. fi
  432. echo "$o|$r|$n" >> "$tmp_manifest"
  433. done < "$MANIFEST"
  434. mv "$tmp_manifest" "$MANIFEST"
  435. fi
  436. APK_JSON="[]"
  437. while IFS='|' read -r o r n; do
  438. [ -n "$n" ] || continue
  439. APK_JSON=$(echo "$APK_JSON" | jq -c --arg o "$o" --arg r "$r" --arg n "$n" \
  440. '. + [{owner:$o,repo:$r,apk:$n}]')
  441. done < "$MANIFEST"
  442. echo "apk_list=${APK_JSON}" >> "$GITHUB_OUTPUT"
  443. echo "Found $(wc -l < "$MANIFEST" 2>/dev/null | tr -d ' ' || echo 0) manager APKs"
  444. ls -lh "$OUT_APKS" || true
  445. - name: Upload all manager APKs as one cross-blob
  446. uses: actions/upload-artifact@v7
  447. with:
  448. name: manager-apks
  449. path: manager_apks/
  450. if-no-files-found: ignore
  451. # ── path B: build from source — mirror upstream workflow order ──
  452. # Upstream: build-lkm -> build-ksuinit -> build-ksud (matrix) -> build-manager (needs ksud, copies to jniLibs)
  453. # We mirror that order so manager embeds ksud correctly. No extra gradle.properties edits beside your key.
  454. build-ksud-from-source:
  455. if: ${{ inputs.build_from_source }}
  456. runs-on: ubuntu-latest
  457. timeout-minutes: 40
  458. strategy:
  459. fail-fast: false
  460. matrix:
  461. include:
  462. - flavor: next
  463. repo: https://github.com/KernelSU-Next/KernelSU-Next.git
  464. ref: dev
  465. - flavor: next-susfs
  466. repo: https://github.com/pershoot/KernelSU-Next.git
  467. ref: dev-susfs
  468. - flavor: kernelsu
  469. repo: https://github.com/tiann/KernelSU.git
  470. ref: main
  471. - flavor: resukisu
  472. repo: https://github.com/ReSukiSU/ReSukiSU.git
  473. ref: main
  474. - flavor: sukisu-ultra
  475. repo: https://github.com/SukiSU-Ultra/SukiSU-Ultra.git
  476. ref: main
  477. - flavor: kowsu
  478. repo: https://github.com/KOWX712/KernelSU.git
  479. ref: master
  480. steps:
  481. - name: Gate on root_flavor filter
  482. id: gate
  483. shell: bash
  484. env:
  485. ROOT_FLAVOR: ${{ inputs.root_flavor }}
  486. run: |
  487. WANT="${{ inputs.root_flavor }}"
  488. WANT="${WANT:-All}"
  489. FLAVOR="${{ matrix.flavor }}"
  490. case "$WANT" in
  491. All) echo "run=true" >> "$GITHUB_OUTPUT" ;;
  492. KernelSU-Next) [[ "$FLAVOR" == "next" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  493. KernelSU-Next-SUSFS) [[ "$FLAVOR" == "next-susfs" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  494. KernelSU) [[ "$FLAVOR" == "kernelsu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  495. ReSukiSU) [[ "$FLAVOR" == "resukisu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  496. SukiSU-Ultra) [[ "$FLAVOR" == "sukisu-ultra" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  497. KowSU) [[ "$FLAVOR" == "kowsu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  498. *) echo "run=false" >> "$GITHUB_OUTPUT" ;;
  499. esac
  500. - name: Resolve commit for this flavor
  501. if: steps.gate.outputs.run == 'true'
  502. id: commit
  503. shell: bash
  504. env:
  505. KSU_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
  506. KSU_NEXT_SUSFS_COMMIT: ${{ inputs.kernelsu_next_susfs_commit }}
  507. KSU_COMMIT: ${{ inputs.kernelsu_commit }}
  508. RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
  509. SUKISU_ULTRA_COMMIT: ${{ inputs.sukisu_ultra_commit }}
  510. run: |
  511. set -euo pipefail
  512. flavor="${{ matrix.flavor }}"
  513. case "$flavor" in
  514. next) pin="$KSU_NEXT_COMMIT" ;;
  515. next-susfs) pin="$KSU_NEXT_SUSFS_COMMIT" ;;
  516. kernelsu) pin="$KSU_COMMIT" ;;
  517. resukisu) pin="$RESUKISU_COMMIT" ;;
  518. sukisu-ultra) pin="$SUKISU_ULTRA_COMMIT" ;;
  519. kowsu) pin="" ;;
  520. esac
  521. if [[ -n "${pin:-}" ]]; then
  522. echo "sha=$pin" >> "$GITHUB_OUTPUT"
  523. else
  524. sha=$(git ls-remote "${{ matrix.repo }}" "refs/heads/${{ matrix.ref }}" | awk '{print $1; exit}')
  525. echo "sha=$sha" >> "$GITHUB_OUTPUT"
  526. fi
  527. - name: Clone ${{ matrix.flavor }} at pinned commit
  528. if: steps.gate.outputs.run == 'true'
  529. shell: bash
  530. run: |
  531. set -euo pipefail
  532. sha="${{ steps.commit.outputs.sha }}"
  533. repo="${{ matrix.repo }}"
  534. flavor="${{ matrix.flavor }}"
  535. dir="/tmp/$flavor-src"
  536. rm -rf "$dir"
  537. git clone --no-checkout "$repo" "$dir"
  538. git -C "$dir" fetch --prune --tags --force origin "+refs/heads/*:refs/remotes/origin/*" "+refs/tags/*:refs/tags/*" 2>&1 | tail -5 || true
  539. git -C "$dir" checkout --detach "$sha"
  540. actual=$(git -C "$dir" rev-parse HEAD)
  541. if [[ "$actual" != "$sha" ]]; then echo "mismatch $sha vs $actual" >&2; exit 1; fi
  542. echo "Cloned $flavor at $actual"
  543. echo "SRC_DIR=$dir" >> "$GITHUB_ENV"
  544. - name: Setup Rust
  545. if: steps.gate.outputs.run == 'true'
  546. uses: dtolnay/rust-toolchain@stable
  547. with:
  548. targets: aarch64-linux-android,x86_64-linux-android
  549. - name: Setup Android NDK
  550. if: steps.gate.outputs.run == 'true'
  551. uses: android-actions/setup-android@v4
  552. - name: Setup rustup (like upstream ksud.yml)
  553. if: steps.gate.outputs.run == 'true'
  554. shell: bash
  555. run: |
  556. rustup update stable
  557. rustup target add aarch64-linux-android x86_64-linux-android aarch64-apple-darwin x86_64-apple-darwin || true
  558. rustup target add aarch64-linux-android || true
  559. rustup show
  560. - name: Rust cache
  561. if: steps.gate.outputs.run == 'true'
  562. uses: Swatinem/rust-cache@v2
  563. with:
  564. workspaces: userspace/ksud
  565. cache-targets: false
  566. cache-bin: false
  567. - name: Install cross (like upstream ksud.yml)
  568. if: steps.gate.outputs.run == 'true'
  569. shell: bash
  570. run: |
  571. RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 2>&1 | tail -20 || RUSTFLAGS="" cargo install cross --locked 2>&1 | tail -20 || true
  572. cross --version || true
  573. - name: Assemble LKM bootstrap (like upstream ksud.yml)
  574. if: steps.gate.outputs.run == 'true'
  575. shell: bash
  576. run: |
  577. set -euo pipefail
  578. dir="${{ env.SRC_DIR }}"
  579. cd "$dir"
  580. if [ -f userspace/ksud/src/lkm_image_bootstrap.S ]; then
  581. clang --target=aarch64-linux-gnu -c -nostdlib -o userspace/ksud/.lkm_image_bootstrap.o userspace/ksud/src/lkm_image_bootstrap.S 2>&1 | tail -20 || echo "lkm bootstrap optional"
  582. ls -lh userspace/ksud/.lkm_image_bootstrap.o 2>&1 | head -5 || true
  583. else
  584. echo "no lkm_image_bootstrap.S, skip"
  585. fi
  586. - name: Build ksud (aarch64 + x86_64 via cross)
  587. if: steps.gate.outputs.run == 'true'
  588. shell: bash
  589. env:
  590. BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android: --sysroot=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/sysroot -I$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/aarch64-linux-android -I$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include
  591. BINDGEN_EXTRA_CLANG_ARGS_x86_64_linux_android: --sysroot=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/sysroot -I$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/x86_64-linux-android -I$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include
  592. run: |
  593. set -euo pipefail
  594. dir="$SRC_DIR"
  595. cd "$dir"
  596. export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}"
  597. export ANDROID_NDK_ROOT="${ANDROID_NDK_ROOT:-$ANDROID_NDK_HOME}"
  598. echo "NDK: $ANDROID_NDK_HOME"
  599. # upstream ksud.yml uses cross, not cargo-ndk
  600. for target in aarch64-linux-android x86_64-linux-android; do
  601. echo "== cross build $target =="
  602. CROSS_NO_WARNINGS=0 cross build --target "$target" --release --manifest-path ./userspace/ksud/Cargo.toml 2>&1 | tail -100 || echo "$target cross build failed — will still try manager without it"
  603. done
  604. mkdir -p "/tmp/ksud-out-${{ matrix.flavor }}/aarch64-linux-android/release" "/tmp/ksud-out-${{ matrix.flavor }}/x86_64-linux-android/release"
  605. for p in "userspace/ksud/target/aarch64-linux-android/release/ksud" "target/aarch64-linux-android/release/ksud"; do [ -f "$p" ] && cp -fv "$p" "/tmp/ksud-out-${{ matrix.flavor }}/aarch64-linux-android/release/ksud" && break || true; done
  606. for p in "userspace/ksud/target/x86_64-linux-android/release/ksud" "target/x86_64-linux-android/release/ksud"; do [ -f "$p" ] && cp -fv "$p" "/tmp/ksud-out-${{ matrix.flavor }}/x86_64-linux-android/release/ksud" && break || true; done
  607. ls -lhR /tmp/ksud-out-${{ matrix.flavor }} 2>&1 | head -50 || true
  608. echo "ksud build summary: aarch64=$(ls -l /tmp/ksud-out-${{ matrix.flavor }}/aarch64-linux-android/release/ksud 2>&1 | head -1); x86_64=$(ls -l /tmp/ksud-out-${{ matrix.flavor }}/x86_64-linux-android/release/ksud 2>&1 | head -1)"
  609. - name: Upload ksud (${{ matrix.flavor }} aarch64)
  610. if: steps.gate.outputs.run == 'true'
  611. uses: actions/upload-artifact@v7
  612. with:
  613. name: ksud-${{ matrix.flavor }}-aarch64-linux-android
  614. path: /tmp/ksud-out-${{ matrix.flavor }}/aarch64-linux-android/release/ksud
  615. if-no-files-found: ignore
  616. - name: Upload ksud (${{ matrix.flavor }} x86_64)
  617. if: steps.gate.outputs.run == 'true'
  618. uses: actions/upload-artifact@v7
  619. with:
  620. name: ksud-${{ matrix.flavor }}-x86_64-linux-android
  621. path: /tmp/ksud-out-${{ matrix.flavor }}/x86_64-linux-android/release/ksud
  622. if-no-files-found: ignore
  623. # ── path B: build from source — mirror upstream exactly (clone + stock build reqs + key only) ──
  624. # Each flavor mirrors its upstream build-manager workflow 1:1. No extra gradle.properties edits,
  625. # no cargo LKM hacks — just checkout, write key (yours), setup Java/Gradle/SDK, spoof/randomizer if upstream does it, then ./gradlew.
  626. build-from-source:
  627. needs: build-ksud-from-source
  628. if: ${{ always() && inputs.build_from_source }}
  629. runs-on: ubuntu-latest
  630. timeout-minutes: 90
  631. strategy:
  632. fail-fast: false
  633. matrix:
  634. include:
  635. - flavor: next
  636. repo: https://github.com/KernelSU-Next/KernelSU-Next.git
  637. ref: dev
  638. - flavor: kernelsu
  639. repo: https://github.com/tiann/KernelSU.git
  640. ref: main
  641. - flavor: resukisu
  642. repo: https://github.com/ReSukiSU/ReSukiSU.git
  643. ref: main
  644. - flavor: sukisu-ultra
  645. repo: https://github.com/SukiSU-Ultra/SukiSU-Ultra.git
  646. ref: main
  647. - flavor: kowsu
  648. repo: https://github.com/KOWX712/KernelSU.git
  649. ref: master
  650. - flavor: next-susfs
  651. repo: https://github.com/pershoot/KernelSU-Next.git
  652. ref: dev-susfs
  653. env:
  654. FLAVOR: ${{ matrix.flavor }}
  655. steps:
  656. - name: Gate on root_flavor filter
  657. id: gate
  658. shell: bash
  659. env:
  660. ROOT_FLAVOR: ${{ inputs.root_flavor }}
  661. run: |
  662. WANT="${ROOT_FLAVOR:-All}"
  663. FLAVOR="${{ matrix.flavor }}"
  664. case "$WANT" in
  665. All) echo "run=true" >> "$GITHUB_OUTPUT" ;;
  666. KernelSU-Next) [[ "$FLAVOR" == "next" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  667. KernelSU-Next-SUSFS) [[ "$FLAVOR" == "next-susfs" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  668. KernelSU) [[ "$FLAVOR" == "kernelsu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  669. ReSukiSU) [[ "$FLAVOR" == "resukisu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  670. SukiSU-Ultra) [[ "$FLAVOR" == "sukisu-ultra" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  671. KowSU) [[ "$FLAVOR" == "kowsu" ]] && echo "run=true" >> "$GITHUB_OUTPUT" || echo "run=false" >> "$GITHUB_OUTPUT" ;;
  672. *) echo "run=false" >> "$GITHUB_OUTPUT" ;;
  673. esac
  674. cat "$GITHUB_OUTPUT"
  675. - name: Resolve commit for this flavor
  676. if: steps.gate.outputs.run == 'true'
  677. id: commit
  678. shell: bash
  679. env:
  680. KSU_NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
  681. KSU_NEXT_SUSFS_COMMIT: ${{ inputs.kernelsu_next_susfs_commit }}
  682. KSU_COMMIT: ${{ inputs.kernelsu_commit }}
  683. RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
  684. SUKISU_ULTRA_COMMIT: ${{ inputs.sukisu_ultra_commit }}
  685. run: |
  686. set -euo pipefail
  687. flavor="${{ matrix.flavor }}"
  688. case "$flavor" in
  689. next) pin="$KSU_NEXT_COMMIT" ;;
  690. next-susfs) pin="$KSU_NEXT_SUSFS_COMMIT" ;;
  691. kernelsu) pin="$KSU_COMMIT" ;;
  692. resukisu) pin="$RESUKISU_COMMIT" ;;
  693. sukisu-ultra) pin="$SUKISU_ULTRA_COMMIT" ;;
  694. kowsu) pin="" ;;
  695. esac
  696. if [[ -n "${pin:-}" ]]; then
  697. if ! [[ "$pin" =~ ^[0-9a-f]{40}$ ]]; then
  698. echo "ERROR: pin must be 40-char SHA (got: $pin)" >&2; exit 1
  699. fi
  700. echo "sha=$pin" >> "$GITHUB_OUTPUT"
  701. echo "Using pinned $flavor commit: $pin"
  702. else
  703. sha=$(git ls-remote "${{ matrix.repo }}" "refs/heads/${{ matrix.ref }}" | awk '{print $1; exit}')
  704. if ! [[ "$sha" =~ ^[0-9a-f]{40}$ ]]; then
  705. echo "ERROR: failed to resolve latest SHA for $flavor" >&2; exit 1
  706. fi
  707. echo "sha=$sha" >> "$GITHUB_OUTPUT"
  708. echo "Using latest $flavor tip on ${{ matrix.ref }}: $sha"
  709. fi
  710. - name: Clone ${{ matrix.flavor }} at pinned commit
  711. if: steps.gate.outputs.run == 'true'
  712. shell: bash
  713. run: |
  714. set -euo pipefail
  715. sha="${{ steps.commit.outputs.sha }}"
  716. repo="${{ matrix.repo }}"
  717. flavor="${{ matrix.flavor }}"
  718. dir="/tmp/$flavor-src"
  719. rm -rf "$dir"
  720. # Mirror upstream checkout@v7 fetch-depth: 0 with all tags
  721. # All 4 managers use git describe --tags --long / rev-list --count
  722. git clone --no-checkout "$repo" "$dir"
  723. # fetch all branches + all tags, full history (faithful to upstream)
  724. git -C "$dir" fetch --prune --tags --force origin "+refs/heads/*:refs/remotes/origin/*" "+refs/tags/*:refs/tags/*" 2>&1 | tail -5 || true
  725. git -C "$dir" checkout --detach "$sha"
  726. actual=$(git -C "$dir" rev-parse HEAD)
  727. if [[ "$actual" != "$sha" ]]; then
  728. echo "Commit mismatch: expected $sha got $actual" >&2; exit 1
  729. fi
  730. echo "Cloned $flavor at $actual (describe: $(git -C "$dir" describe --tags --long HEAD 2>&1 | head -1) count: $(git -C "$dir" rev-list --count HEAD 2>&1 | head -1) tags: $(git -C "$dir" tag | wc -l))"
  731. echo "SRC_DIR=$dir" >> "$GITHUB_ENV"
  732. # ── Stock build requirements per upstream — no edits except key ──
  733. - name: Setup Java 21
  734. if: steps.gate.outputs.run == 'true'
  735. uses: actions/setup-java@v5
  736. with:
  737. distribution: temurin
  738. java-version: 21
  739. - name: Setup Gradle
  740. if: steps.gate.outputs.run == 'true'
  741. uses: gradle/actions/setup-gradle@v5
  742. # tiann/ReSukiSU use v6, Next uses v5 — v5 works for all, keep stock default
  743. - name: Setup Android SDK
  744. if: steps.gate.outputs.run == 'true'
  745. uses: android-actions/setup-android@v4
  746. # ReSukiSU/Next use v3/v4 — v3 is the common baseline
  747. - name: Download ksud aarch64 (Next/pershoot, like upstream)
  748. if: steps.gate.outputs.run == 'true' && (matrix.flavor == 'next' || matrix.flavor == 'next-susfs')
  749. continue-on-error: true
  750. uses: actions/download-artifact@v7
  751. with:
  752. name: ksud-${{ matrix.flavor }}-aarch64-linux-android
  753. path: /tmp/ksud-dl-aarch64
  754. - name: Download ksud x86_64 (Next/pershoot, like upstream)
  755. if: steps.gate.outputs.run == 'true' && (matrix.flavor == 'next' || matrix.flavor == 'next-susfs')
  756. continue-on-error: true
  757. uses: actions/download-artifact@v7
  758. with:
  759. name: ksud-${{ matrix.flavor }}-x86_64-linux-android
  760. path: /tmp/ksud-dl-x86_64
  761. - name: Copy ksud to jniLibs (like upstream build-manager)
  762. if: steps.gate.outputs.run == 'true' && (matrix.flavor == 'next' || matrix.flavor == 'next-susfs')
  763. shell: bash
  764. run: |
  765. set -euo pipefail
  766. dir="/tmp/${{ matrix.flavor }}-src/manager"
  767. mkdir -p "$dir/app/src/main/jniLibs/arm64-v8a" "$dir/app/src/main/jniLibs/x86_64"
  768. for p in "/tmp/ksud-dl-aarch64/ksud" "/tmp/ksud-dl-aarch64/aarch64-linux-android/release/ksud"; do [ -f "$p" ] && cp -fv "$p" "$dir/app/src/main/jniLibs/arm64-v8a/libksud.so" && echo "copied aarch64 ksud" && break || true; done
  769. # fallback search
  770. if [ ! -f "$dir/app/src/main/jniLibs/arm64-v8a/libksud.so" ]; then f=$(find /tmp/ksud-dl-aarch64 -type f -name ksud 2>/dev/null | head -1 || true); [ -n "${f:-}" ] && cp -fv "$f" "$dir/app/src/main/jniLibs/arm64-v8a/libksud.so" || echo "no aarch64 ksud — manager will build without it"; fi
  771. for p in "/tmp/ksud-dl-x86_64/ksud" "/tmp/ksud-dl-x86_64/x86_64-linux-android/release/ksud"; do [ -f "$p" ] && cp -fv "$p" "$dir/app/src/main/jniLibs/x86_64/libksud.so" && echo "copied x86_64 ksud" && break || true; done
  772. if [ ! -f "$dir/app/src/main/jniLibs/x86_64/libksud.so" ]; then f=$(find /tmp/ksud-dl-x86_64 -type f -name ksud 2>/dev/null | head -1 || true); [ -n "${f:-}" ] && cp -fv "$f" "$dir/app/src/main/jniLibs/x86_64/libksud.so" || echo "no x86_64 ksud — manager will build without it"; fi
  773. ls -lh "$dir/app/src/main/jniLibs"/*/* 2>&1 | head -20 || echo "no jniLibs"
  774. - name: Spoof Package ID (Next only, like upstream build-manager-ci)
  775. if: steps.gate.outputs.run == 'true' && (matrix.flavor == 'next' || matrix.flavor == 'next-susfs')
  776. shell: bash
  777. working-directory: /tmp/${{ matrix.flavor }}-src/manager
  778. run: |
  779. # Upstream Next builds two variants; for build-from-source we build one normal + one spoofed sequentially
  780. # This step just prepares — actual spoof run happens in Build step
  781. if [ -x ./spoof ]; then echo "spoof present"; else echo "no spoof script"; fi
  782. - name: Randomizer check (ReSukiSU only, like upstream)
  783. if: steps.gate.outputs.run == 'true' && matrix.flavor == 'resukisu'
  784. shell: bash
  785. working-directory: /tmp/${{ matrix.flavor }}-src/manager
  786. run: |
  787. if [ -x ./randomizer ]; then chmod +x ./randomizer; ls -l ./randomizer; else echo "no randomizer"; fi
  788. - name: Build Manager APK(s) — stock gradle, key only
  789. if: steps.gate.outputs.run == 'true'
  790. shell: bash
  791. working-directory: /tmp/${{ matrix.flavor }}-src
  792. env:
  793. NEXT_KEYSTORE: ${{ secrets.NEXT_KEYSTORE }}
  794. NEXT_KEYSTORE_PASSWORD: ${{ secrets.NEXT_KEYSTORE_PASSWORD }}
  795. NEXT_KEY_ALIAS: ${{ secrets.NEXT_KEY_ALIAS }}
  796. NEXT_KEY_PASSWORD: ${{ secrets.NEXT_KEY_PASSWORD }}
  797. KERNELSU_KEYSTORE: ${{ secrets.KERNELSU_KEYSTORE }}
  798. KERNELSU_KEYSTORE_PASSWORD: ${{ secrets.KERNELSU_KEYSTORE_PASSWORD }}
  799. KERNELSU_KEY_ALIAS: ${{ secrets.KERNELSU_KEY_ALIAS }}
  800. KERNELSU_KEY_PASSWORD: ${{ secrets.KERNELSU_KEY_PASSWORD }}
  801. RESUKISU_KEYSTORE: ${{ secrets.RESUKISU_KEYSTORE }}
  802. RESUKISU_KEYSTORE_PASSWORD: ${{ secrets.RESUKISU_KEYSTORE_PASSWORD }}
  803. RESUKISU_KEY_ALIAS: ${{ secrets.RESUKISU_KEY_ALIAS }}
  804. RESUKISU_KEY_PASSWORD: ${{ secrets.RESUKISU_KEY_PASSWORD }}
  805. SUKISU_ULTRA_KEYSTORE: ${{ secrets.SUKISU_ULTRA_KEYSTORE }}
  806. SUKISU_ULTRA_KEYSTORE_PASSWORD: ${{ secrets.SUKISU_ULTRA_KEYSTORE_PASSWORD }}
  807. SUKISU_ULTRA_KEY_ALIAS: ${{ secrets.SUKISU_ULTRA_KEY_ALIAS }}
  808. SUKISU_ULTRA_KEY_PASSWORD: ${{ secrets.SUKISU_ULTRA_KEY_PASSWORD }}
  809. run: |
  810. set -euo pipefail
  811. dir="$SRC_DIR"
  812. cd "$dir"
  813. flavor="$FLAVOR"
  814. echo "== Stock build for $flavor at $dir =="
  815. if [ ! -d manager ]; then
  816. echo "No manager/ directory — skipping" >&2; exit 0
  817. fi
  818. cd manager
  819. # ── Key only (minimal edit) ──
  820. resolve_key() {
  821. local b64 pw alias kp
  822. case "$flavor" in
  823. next|next-susfs) b64="${NEXT_KEYSTORE:-}"; pw="${NEXT_KEYSTORE_PASSWORD:-}"; alias="${NEXT_KEY_ALIAS:-}"; kp="${NEXT_KEY_PASSWORD:-}" ;;
  824. kernelsu) b64="${KERNELSU_KEYSTORE:-}"; pw="${KERNELSU_KEYSTORE_PASSWORD:-}"; alias="${KERNELSU_KEY_ALIAS:-}"; kp="${KERNELSU_KEY_PASSWORD:-}" ;;
  825. resukisu) b64="${RESUKISU_KEYSTORE:-}"; pw="${RESUKISU_KEYSTORE_PASSWORD:-}"; alias="${RESUKISU_KEY_ALIAS:-}"; kp="${RESUKISU_KEY_PASSWORD:-}" ;;
  826. sukisu-ultra) b64="${SUKISU_ULTRA_KEYSTORE:-}"; pw="${SUKISU_ULTRA_KEYSTORE_PASSWORD:-}"; alias="${SUKISU_ULTRA_KEY_ALIAS:-}"; kp="${SUKISU_ULTRA_KEY_PASSWORD:-}" ;;
  827. esac
  828. if [ -n "${b64:-}" ] && [ -n "${pw:-}" ] && [ -n "${alias:-}" ]; then
  829. echo "Using your persistent key for $flavor (alias=$alias)"
  830. echo "$b64" | base64 -d > "/tmp/manager-$flavor.jks"
  831. kp="${kp:-$pw}"
  832. {
  833. echo "KEYSTORE_PASSWORD=$pw"
  834. echo "KEY_ALIAS=$alias"
  835. echo "KEY_PASSWORD=$kp"
  836. echo "KEYSTORE_FILE=/tmp/manager-$flavor.jks"
  837. } >> gradle.properties
  838. return 0
  839. fi
  840. return 1
  841. }
  842. if ! resolve_key; then
  843. echo "No persistent key for $flavor — using ephemeral debug keystore (tmp123) like upstream PR builds"
  844. if [ ! -f key.jks ] && [ ! -f ../key.jks ]; then
  845. keytool -genkeypair -alias tmp -keyalg RSA -keysize 2048 -validity 1 \
  846. -storepass tmp123 -keypass tmp123 -dname "CN=CI Build" -keystore /tmp/tmp-key.jks -storetype JKS 2>&1 | tail -3 || true
  847. if [ -f /tmp/tmp-key.jks ]; then
  848. {
  849. echo "KEYSTORE_PASSWORD=tmp123"
  850. echo "KEY_ALIAS=tmp"
  851. echo "KEY_PASSWORD=tmp123"
  852. echo "KEYSTORE_FILE=/tmp/tmp-key.jks"
  853. } >> gradle.properties
  854. fi
  855. fi
  856. fi
  857. echo "--- manager/gradle.properties (stock + key only) ---"
  858. cat gradle.properties || true
  859. # ── Stock gradle invocation per upstream ──
  860. gradle_build() {
  861. local task="$1"
  862. echo ">> ./gradlew $task"
  863. set -o pipefail
  864. if ! ./gradlew $task 2>&1 | tee /tmp/gradle-$flavor.log; then
  865. echo "--- gradle $task FAILED for $flavor ---" >&2
  866. tail -n 200 /tmp/gradle-$flavor.log 2>/dev/null || true
  867. return 1
  868. fi
  869. }
  870. # Flavor-specific stock commands (mirror upstream files verbatim)
  871. if [ "$flavor" = "next" ] || [ "$flavor" = "next-susfs" ]; then
  872. # Upstream Next: ./gradlew clean assembleRelease (+ spoof variant)
  873. # Build normal
  874. ok=false
  875. if gradle_build "clean assembleRelease"; then ok=true; fi
  876. # Spoof variant if present (upstream does matrix spoofed=true/false)
  877. if [ -x ./spoof ]; then
  878. echo "Building spoofed variant (upstream match)"
  879. chmod +x ./spoof; ./spoof || true
  880. if gradle_build "clean assembleRelease"; then
  881. for apk in app/build/outputs/apk/release/*.apk; do [ -f "$apk" ] && cp -f "$apk" "/tmp/next-spoofed.apk" || true; done
  882. ok=true
  883. fi
  884. git checkout -- app/src/main/java 2>/dev/null || git checkout -- . 2>/dev/null || true
  885. fi
  886. if ! $ok; then echo "ERROR: Next gradle failed" >&2; tail -n 300 /tmp/gradle-$flavor.log; exit 1; fi
  887. elif [ "$flavor" = "kernelsu" ]; then
  888. # Upstream tiann: ./gradlew clean assembleRelease (repack is separate job — we just build manager here)
  889. gradle_build "clean assembleRelease" || gradle_build "assembleRelease" || true
  890. elif [ "$flavor" = "kowsu" ]; then
  891. # Upstream KowSU is tiann-style: clean assembleRelease
  892. gradle_build "clean assembleRelease" || gradle_build "assembleRelease" || true
  893. elif [ "$flavor" = "resukisu" ]; then
  894. # Upstream ReSukiSU: assembleRelease + randomizer (only universal variants)
  895. gradle_build "clean assembleRelease" || true
  896. # skip assembleDebug — only release universal and universal spoofed
  897. if [ -x ./randomizer ]; then
  898. ./randomizer 2>&1 | tail -20 || true
  899. gradle_build "clean assembleRelease" || true
  900. fi
  901. elif [ "$flavor" = "sukisu-ultra" ]; then
  902. # Upstream SukiSU-Ultra: clean assembleRelease, spoofed via randomizer
  903. gradle_build "clean assembleRelease" || true
  904. if [ -x ./randomizer ]; then
  905. ./randomizer 2>&1 | tail -20 || true
  906. gradle_build "clean assembleRelease" || true
  907. fi
  908. fi
  909. # Collect (only release variants for resukisu)
  910. out="/tmp/manager_apks_${flavor}"
  911. mkdir -p "$out"
  912. if [ "$flavor" = "resukisu" ]; then
  913. for apk in app/build/outputs/apk/release/*.apk; do
  914. [ -f "$apk" ] || continue
  915. cp -f "$apk" "$out/${flavor}-$(basename "$apk")"
  916. echo "Collected $out/${flavor}-$(basename "$apk")"
  917. done
  918. # Filter resukisu to only universal + not debug (release universal and universal spoofed)
  919. for f in "$out"/*.apk; do
  920. [ -f "$f" ] || continue
  921. ln=$(basename "$f" | tr '[:upper:]' '[:lower:]')
  922. if [[ "$ln" != *universal* ]]; then echo "Dropping resukisu non-universal $(basename "$f")" >&2; rm -f "$f"; continue; fi
  923. if [[ "$ln" == *debug* ]]; then echo "Dropping resukisu debug $(basename "$f")" >&2; rm -f "$f"; continue; fi
  924. done
  925. else
  926. for apk in app/build/outputs/apk/release/*.apk app/build/outputs/apk/debug/*.apk; do
  927. [ -f "$apk" ] || continue
  928. cp -f "$apk" "$out/${flavor}-$(basename "$apk")"
  929. echo "Collected $out/${flavor}-$(basename "$apk")"
  930. done
  931. fi
  932. if [ -f /tmp/next-spoofed.apk ]; then cp -f /tmp/next-spoofed.apk "$out/${flavor}-spoofed.apk" || true; fi
  933. ls -lh "$out" 2>&1 | head -30 || true
  934. if [ -z "$(ls -A "$out" 2>/dev/null)" ]; then echo "WARNING: no APK produced for $flavor" >&2; fi
  935. echo "MANAGER_OUT_$flavor=$out" >> "$GITHUB_ENV"
  936. - name: Upload gradle logs (${{ matrix.flavor }})
  937. if: always() && steps.gate.outputs.run == 'true'
  938. uses: actions/upload-artifact@v7
  939. with:
  940. name: gradle-log-${{ matrix.flavor }}
  941. path: /tmp/gradle-*.log
  942. if-no-files-found: ignore
  943. - name: Upload built manager APKs (${{ matrix.flavor }})
  944. if: steps.gate.outputs.run == 'true'
  945. uses: actions/upload-artifact@v7
  946. with:
  947. name: manager-apks-src-${{ matrix.flavor }}
  948. path: /tmp/manager_apks_${{ matrix.flavor }}/*.apk
  949. if-no-files-found: ignore
  950. # Collector for source-built managers: merge per-flavor uploads into one cross-blob
  951. # so downstream consumers can use the same artifact name regardless of path.
  952. collect-source-managers:
  953. needs: build-from-source
  954. if: ${{ inputs.build_from_source }}
  955. runs-on: ubuntu-latest
  956. outputs:
  957. apk_list: ${{ steps.scan.outputs.apk_list }}
  958. steps:
  959. - name: Download all source-built manager artifacts
  960. uses: actions/download-artifact@v7
  961. with:
  962. pattern: manager-apks-src-*
  963. path: manager_apks
  964. merge-multiple: true
  965. - name: Scan collected APKs
  966. id: scan
  967. shell: bash
  968. run: |
  969. set -euo pipefail
  970. mkdir -p manager_apks
  971. find manager_apks -type f -name '*.apk' | sort | tee /tmp/list.txt || true
  972. # Filter ReSukiSU to only universal release and universal spoofed
  973. if grep -qi "resukisu" /tmp/list.txt 2>/dev/null; then
  974. tmp_list="/tmp/list.filtered"
  975. : > "$tmp_list"
  976. while IFS= read -r f; do
  977. [ -n "$f" ] || continue
  978. ln=$(basename "$f" | tr '[:upper:]' '[:lower:]')
  979. if [[ "$ln" == *resukisu* ]]; then
  980. if [[ "$ln" != *universal* ]]; then echo "Dropping resukisu non-universal $f" >&2; rm -f "$f"; continue; fi
  981. if [[ "$ln" == *debug* ]]; then echo "Dropping resukisu debug $f" >&2; rm -f "$f"; continue; fi
  982. fi
  983. echo "$f" >> "$tmp_list"
  984. done < /tmp/list.txt
  985. mv "$tmp_list" /tmp/list.txt
  986. fi
  987. count=$(wc -l < /tmp/list.txt 2>/dev/null | tr -d ' ' || echo 0)
  988. echo "Found $count source-built manager APKs"
  989. ls -lh manager_apks 2>&1 | head -40 || true
  990. APK_JSON="[]"
  991. while IFS= read -r f; do
  992. [ -n "$f" ] || continue
  993. n="$(basename "$f")"
  994. APK_JSON=$(echo "$APK_JSON" | jq -c --arg n "$n" '. + [{apk:$n}]')
  995. done < /tmp/list.txt
  996. echo "apk_list=${APK_JSON}" >> "$GITHUB_OUTPUT"
  997. echo "Found $(wc -l < /tmp/list.txt 2>/dev/null | tr -d ' ' || echo 0) source-built manager APKs"
  998. - name: Upload merged manager-apks (source)
  999. uses: actions/upload-artifact@v7
  1000. with:
  1001. name: manager-apks
  1002. path: manager_apks/
  1003. if-no-files-found: ignore
  1004. # Unified publisher: works for both fetch and source paths (whichever produced manager-apks)
  1005. # Skips entirely when apk_list is empty/[] to avoid "Matrix vector item does not contain any values" (Line 818 col 15)
  1006. publish-manager-artifacts:
  1007. needs:
  1008. - list-manager-apks
  1009. - collect-source-managers
  1010. 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 != '[]') ) }}
  1011. runs-on: ubuntu-latest
  1012. strategy:
  1013. fail-fast: false
  1014. matrix:
  1015. 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) }}
  1016. steps:
  1017. - uses: actions/checkout@v7
  1018. - name: Download manager cross-blob
  1019. uses: actions/download-artifact@v7
  1020. with:
  1021. name: manager-apks
  1022. path: manager_apks
  1023. merge-multiple: true
  1024. - name: Upload ${{ matrix.item.apk }} as its own artifact
  1025. uses: actions/upload-artifact@v7
  1026. with:
  1027. name: ${{ matrix.item.apk }}
  1028. path: manager_apks/${{ matrix.item.apk }}
  1029. if-no-files-found: error
  1030. notes-preview:
  1031. if: always()
  1032. needs: [resolve-managers, build-ksud-from-source, build-from-source, collect-source-managers, list-manager-apks, publish-manager-artifacts]
  1033. runs-on: ubuntu-latest
  1034. steps:
  1035. - uses: actions/checkout@v7
  1036. - name: Render release notes preview
  1037. env:
  1038. ROOT_FLAVOR: ${{ inputs.root_flavor }}
  1039. BUILD_FROM_SOURCE: ${{ inputs.build_from_source }}
  1040. NEXT_COMMIT: ${{ inputs.kernelsu_next_commit }}
  1041. KSU_COMMIT: ${{ inputs.kernelsu_commit }}
  1042. RESUKISU_COMMIT: ${{ inputs.resukisu_commit }}
  1043. SUKISU_ULTRA_COMMIT: ${{ inputs.sukisu_ultra_commit }}
  1044. NEXT_SUSFS_COMMIT: ${{ inputs.kernelsu_next_susfs_commit }}
  1045. MANAGER_LIST_FETCH: ${{ needs.resolve-managers.outputs.manager_list }}
  1046. run: |
  1047. set -e
  1048. python3 - <<'PYEOF' > release_body.md
  1049. import json, os
  1050. def env(k, d=""):
  1051. return os.environ.get(k, d).strip()
  1052. root = env("ROOT_FLAVOR", "All")
  1053. src = env("BUILD_FROM_SOURCE", "false") == "true"
  1054. commits = {
  1055. "KernelSU-Next": env("NEXT_COMMIT") or "latest dev tip",
  1056. "KernelSU": env("KSU_COMMIT") or "latest main tip",
  1057. "ReSukiSU": env("RESUKISU_COMMIT") or "latest tip",
  1058. "SukiSU-Ultra": env("SUKISU_ULTRA_COMMIT") or "latest tip",
  1059. "KowSU": "latest tip",
  1060. "KernelSU-Next-SUSFS": env("NEXT_SUSFS_COMMIT") or "latest dev-susfs tip",
  1061. }
  1062. # enrich from fetch path manager_list (pure commit info, same style as prebuilt)
  1063. raw = env("MANAGER_LIST_FETCH")
  1064. if raw:
  1065. try:
  1066. for m in json.loads(raw):
  1067. label = {"next":"KernelSU-Next","kernelsu":"KernelSU","resukisu":"ReSukiSU","sukisu-ultra":"SukiSU-Ultra","kowsu":"KowSU","next-susfs":"KernelSU-Next-SUSFS"}.get(m.get("flavor",""), "")
  1068. if label and m.get("stock"):
  1069. commits[label] = m["stock"]
  1070. except: pass
  1071. lines = []
  1072. lines.append("# Managers — Release Notes Preview")
  1073. lines.append("")
  1074. lines.append("## Features")
  1075. lines.append("")
  1076. if root == "All":
  1077. lines.append("- All manager flavors (KernelSU-Next, KernelSU, ReSukiSU, SukiSU-Ultra, KernelSU-Next-SUSFS)")
  1078. else:
  1079. lines.append(f"- {root}")
  1080. lines.append(f"- {'Built from source' if src else 'Fetched prebuilt (upstream CI)'}")
  1081. lines.append("")
  1082. lines.append("## Commits")
  1083. lines.append("")
  1084. for k, v in commits.items():
  1085. if len(v) == 40:
  1086. lines.append(f"- **{k}:** `{v[:12]}` `{v}`")
  1087. else:
  1088. lines.append(f"- **{k}:** `{v}`")
  1089. output = "\n".join(lines) + "\n"
  1090. print(output, end="")
  1091. s = os.environ.get("GITHUB_STEP_SUMMARY")
  1092. if s:
  1093. open(s, "a").write(output)
  1094. PYEOF
  1095. cat release_body.md
  1096. - name: Upload release notes preview artifact
  1097. uses: actions/upload-artifact@v7
  1098. with:
  1099. name: release-notes-preview
  1100. path: release_body.md
  1101. retention-days: 7
  1102. if-no-files-found: ignore