managers.yml 56 KB

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