release.yml 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. name: Release 3X-UI
  2. on:
  3. workflow_dispatch:
  4. # Only main (dev channel) and version tags ship binaries; build any other
  5. # branch on demand via workflow_dispatch.
  6. push:
  7. branches:
  8. - main
  9. tags:
  10. - "v*.*.*"
  11. paths:
  12. - "**.go"
  13. - "go.mod"
  14. - "go.sum"
  15. - "**.sh"
  16. - "frontend/**"
  17. - "x-ui.service.debian"
  18. - "x-ui.service.arch"
  19. - "x-ui.service.rhel"
  20. - ".github/workflows/release.yml"
  21. jobs:
  22. build:
  23. permissions:
  24. contents: write
  25. strategy:
  26. # One platform hitting a transient outage must not cancel the other six.
  27. fail-fast: false
  28. matrix:
  29. platform:
  30. - amd64
  31. - arm64
  32. - armv7
  33. - armv6
  34. - 386
  35. - armv5
  36. - s390x
  37. runs-on: ubuntu-latest
  38. steps:
  39. - name: Checkout repository
  40. uses: actions/checkout@v7
  41. - name: Setup Go
  42. uses: actions/setup-go@v7
  43. with:
  44. go-version-file: go.mod
  45. check-latest: true
  46. # Frontend dist must be built BEFORE go build — Go's //go:embed
  47. # all:dist directive in internal/web/web.go requires internal/web/dist/ to exist
  48. # at compile time. internal/web/dist/ is .gitignored, so on a fresh CI
  49. # checkout it doesn't exist until vite emits it.
  50. - name: Setup Node.js
  51. uses: actions/setup-node@v7
  52. with:
  53. node-version-file: .nvmrc
  54. cache: 'npm'
  55. cache-dependency-path: frontend/package-lock.json
  56. - name: Build frontend bundle
  57. run: |
  58. npm ci
  59. npm run build
  60. working-directory: frontend
  61. - name: Build 3X-UI
  62. run: |
  63. CURL_RETRY="--retry 5 --retry-all-errors --retry-delay 3"
  64. fetch() { wget -q --tries=5 --waitretry=10 --retry-on-http-error=429,500,502,503 "$@"; }
  65. export CGO_ENABLED=1
  66. export GOOS=linux
  67. export GOARCH=${{ matrix.platform }}
  68. # Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series)
  69. case "${{ matrix.platform }}" in
  70. amd64) BOOTLIN_ARCH="x86-64" ;;
  71. arm64) BOOTLIN_ARCH="aarch64" ;;
  72. armv7) BOOTLIN_ARCH="armv7-eabihf"; export GOARCH=arm GOARM=7 ;;
  73. armv6) BOOTLIN_ARCH="armv6-eabihf"; export GOARCH=arm GOARM=6 ;;
  74. armv5) BOOTLIN_ARCH="armv5-eabi"; export GOARCH=arm GOARM=5 ;;
  75. 386) BOOTLIN_ARCH="x86-i686" ;;
  76. s390x) BOOTLIN_ARCH="s390x-z13" ;;
  77. esac
  78. echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
  79. TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
  80. TARBALL_URL=$(curl -fsSL $CURL_RETRY "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
  81. [ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
  82. echo "Downloading: $TARBALL_URL"
  83. cd /tmp
  84. curl -fL -sS $CURL_RETRY -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
  85. tar -xf "$(basename "$TARBALL_URL")"
  86. TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
  87. export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
  88. export CC=$(realpath "$(find "$TOOLCHAIN_DIR/bin" -name '*-gcc.br_real' -type f -executable | head -n1)")
  89. [ -z "$CC" ] && { echo "No gcc.br_real found in $TOOLCHAIN_DIR/bin" >&2; exit 1; }
  90. cd -
  91. # Stamp the commit into per-commit (dev channel) builds only; tagged
  92. # stable releases stay unstamped so config.IsDevBuild() returns false.
  93. LDFLAGS="-w -s -linkmode external -extldflags '-static'"
  94. if [[ "$GITHUB_REF" != refs/tags/* ]]; then
  95. LDFLAGS="$LDFLAGS -X github.com/mhsanaei/3x-ui/v3/internal/config.buildCommit=${GITHUB_SHA::8} -X github.com/mhsanaei/3x-ui/v3/internal/config.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  96. fi
  97. go build -buildvcs=true -ldflags "$LDFLAGS" -o xui-release -v .
  98. file xui-release
  99. ldd xui-release || echo "Static binary confirmed"
  100. mkdir x-ui
  101. cp xui-release x-ui/
  102. cp x-ui.service.debian x-ui/
  103. cp x-ui.service.arch x-ui/
  104. cp x-ui.service.rhel x-ui/
  105. cp x-ui.sh x-ui/
  106. mv x-ui/xui-release x-ui/x-ui
  107. mkdir x-ui/bin
  108. cd x-ui/bin
  109. # Download dependencies
  110. Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.9.9/"
  111. if [ "${{ matrix.platform }}" == "amd64" ]; then
  112. fetch ${Xray_URL}Xray-linux-64.zip
  113. unzip Xray-linux-64.zip
  114. rm -f Xray-linux-64.zip
  115. elif [ "${{ matrix.platform }}" == "arm64" ]; then
  116. fetch ${Xray_URL}Xray-linux-arm64-v8a.zip
  117. unzip Xray-linux-arm64-v8a.zip
  118. rm -f Xray-linux-arm64-v8a.zip
  119. elif [ "${{ matrix.platform }}" == "armv7" ]; then
  120. fetch ${Xray_URL}Xray-linux-arm32-v7a.zip
  121. unzip Xray-linux-arm32-v7a.zip
  122. rm -f Xray-linux-arm32-v7a.zip
  123. elif [ "${{ matrix.platform }}" == "armv6" ]; then
  124. fetch ${Xray_URL}Xray-linux-arm32-v6.zip
  125. unzip Xray-linux-arm32-v6.zip
  126. rm -f Xray-linux-arm32-v6.zip
  127. elif [ "${{ matrix.platform }}" == "386" ]; then
  128. fetch ${Xray_URL}Xray-linux-32.zip
  129. unzip Xray-linux-32.zip
  130. rm -f Xray-linux-32.zip
  131. elif [ "${{ matrix.platform }}" == "armv5" ]; then
  132. fetch ${Xray_URL}Xray-linux-arm32-v5.zip
  133. unzip Xray-linux-arm32-v5.zip
  134. rm -f Xray-linux-arm32-v5.zip
  135. elif [ "${{ matrix.platform }}" == "s390x" ]; then
  136. fetch ${Xray_URL}Xray-linux-s390x.zip
  137. unzip Xray-linux-s390x.zip
  138. rm -f Xray-linux-s390x.zip
  139. fi
  140. rm -f geoip.dat geosite.dat
  141. fetch https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
  142. fetch https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
  143. fetch -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
  144. fetch -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
  145. fetch -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
  146. fetch -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
  147. mv xray xray-linux-${{ matrix.platform }}
  148. # mtg-multi (MTProto sidecar) ships prebuilt release binaries whose
  149. # platform labels match our matrix, so download and unpack the matching
  150. # archive. Only the platforms the fork publishes are packaged — the tag
  151. # lookup lives inside that branch so unpackaged platforms (s390x) never
  152. # depend on it. The tag comes from the release-page redirect on
  153. # github.com — the host the downloads need anyway — because api.github.com
  154. # has 503'd whole release runs while asset downloads kept working.
  155. case "${{ matrix.platform }}" in
  156. amd64|arm64|armv7|armv6|386)
  157. MTG_MULTI_VER=$(curl -sf $CURL_RETRY -o /dev/null -w '%{redirect_url}' "https://github.com/mhsanaei/mtg-multi/releases/latest" | sed -n 's#.*/releases/tag/##p')
  158. if [ -z "$MTG_MULTI_VER" ]; then echo "could not resolve the latest mtg-multi release tag"; exit 1; fi
  159. MTG_PKG="mtg-multi-${MTG_MULTI_VER#v}-linux-${{ matrix.platform }}"
  160. curl -sfLRO $CURL_RETRY "https://github.com/mhsanaei/mtg-multi/releases/download/${MTG_MULTI_VER}/${MTG_PKG}.tar.gz"
  161. tar -xzf "${MTG_PKG}.tar.gz"
  162. mv "${MTG_PKG}/mtg-multi" "mtg-linux-${{ matrix.platform }}"
  163. rm -rf "${MTG_PKG}" "${MTG_PKG}.tar.gz"
  164. ;;
  165. esac
  166. case "${{ matrix.platform }}" in
  167. amd64)
  168. curl -sfLRO $CURL_RETRY "https://github.com/EAimTY/tuic/releases/download/tuic-server-1.0.0/tuic-server-1.0.0-x86_64-unknown-linux-musl"
  169. mv "tuic-server-1.0.0-x86_64-unknown-linux-musl" "tuic-server"
  170. chmod +x "tuic-server"
  171. ;;
  172. arm64)
  173. curl -sfLRO $CURL_RETRY "https://github.com/EAimTY/tuic/releases/download/tuic-server-1.0.0/tuic-server-1.0.0-aarch64-unknown-linux-musl"
  174. mv "tuic-server-1.0.0-aarch64-unknown-linux-musl" "tuic-server"
  175. chmod +x "tuic-server"
  176. ;;
  177. armv7)
  178. curl -sfLRO $CURL_RETRY "https://github.com/EAimTY/tuic/releases/download/tuic-server-1.0.0/tuic-server-1.0.0-armv7-unknown-linux-musleabihf"
  179. mv "tuic-server-1.0.0-armv7-unknown-linux-musleabihf" "tuic-server"
  180. chmod +x "tuic-server"
  181. ;;
  182. 386)
  183. curl -sfLRO $CURL_RETRY "https://github.com/EAimTY/tuic/releases/download/tuic-server-1.0.0/tuic-server-1.0.0-i686-unknown-linux-musl"
  184. mv "tuic-server-1.0.0-i686-unknown-linux-musl" "tuic-server"
  185. chmod +x "tuic-server"
  186. ;;
  187. esac
  188. cd ../..
  189. - name: Package
  190. run: |
  191. tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
  192. sha256sum x-ui-linux-${{ matrix.platform }}.tar.gz > x-ui-linux-${{ matrix.platform }}.tar.gz.sha256
  193. - name: Upload files to Artifacts
  194. uses: actions/upload-artifact@v7
  195. with:
  196. name: x-ui-linux-${{ matrix.platform }}
  197. path: |
  198. ./x-ui-linux-${{ matrix.platform }}.tar.gz
  199. ./x-ui-linux-${{ matrix.platform }}.tar.gz.sha256
  200. - name: Upload files to GH release
  201. uses: svenstaro/upload-release-action@v2
  202. if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
  203. with:
  204. repo_token: ${{ secrets.GITHUB_TOKEN }}
  205. tag: ${{ github.ref_name }}
  206. file: x-ui-linux-${{ matrix.platform }}.tar.gz*
  207. file_glob: true
  208. overwrite: true
  209. prerelease: true
  210. # =================================
  211. # Windows Build
  212. # =================================
  213. build-windows:
  214. name: Build for Windows
  215. permissions:
  216. contents: write
  217. strategy:
  218. matrix:
  219. platform:
  220. - amd64
  221. runs-on: windows-latest
  222. steps:
  223. - name: Checkout repository
  224. uses: actions/checkout@v7
  225. - name: Setup Go
  226. uses: actions/setup-go@v7
  227. with:
  228. go-version-file: go.mod
  229. check-latest: true
  230. # Frontend dist must be built BEFORE go build — see comment on the
  231. # Linux job above. This step is identical except npm runs on the
  232. # Windows runner here.
  233. - name: Setup Node.js
  234. uses: actions/setup-node@v7
  235. with:
  236. node-version-file: .nvmrc
  237. cache: 'npm'
  238. cache-dependency-path: frontend/package-lock.json
  239. - name: Build frontend bundle
  240. shell: pwsh
  241. run: |
  242. npm ci
  243. npm run build
  244. working-directory: frontend
  245. - name: Install MSYS2
  246. uses: msys2/setup-msys2@v2
  247. with:
  248. msystem: MINGW64
  249. update: true
  250. install: >-
  251. git
  252. mingw-w64-x86_64-gcc
  253. mingw-w64-x86_64-sqlite3
  254. mingw-w64-x86_64-pkg-config
  255. - name: Build 3X-UI for Windows (CGO)
  256. shell: msys2 {0}
  257. run: |
  258. export PATH="/c/hostedtoolcache/windows/go/$(ls /c/hostedtoolcache/windows/go | sort -V | tail -n1)/x64/bin:$PATH"
  259. export CGO_ENABLED=1
  260. export GOOS=windows
  261. export GOARCH=amd64
  262. export CC=x86_64-w64-mingw32-gcc
  263. which go
  264. go version
  265. gcc --version
  266. # Stamp the commit into per-commit (dev channel) builds only.
  267. LDFLAGS="-w -s"
  268. if [[ "$GITHUB_REF" != refs/tags/* ]]; then
  269. LDFLAGS="$LDFLAGS -X github.com/mhsanaei/3x-ui/v3/internal/config.buildCommit=${GITHUB_SHA:0:8} -X github.com/mhsanaei/3x-ui/v3/internal/config.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  270. fi
  271. go build -buildvcs=true -ldflags "$LDFLAGS" -o xui-release.exe -v .
  272. - name: Copy and download resources
  273. shell: pwsh
  274. run: |
  275. $retry = @{ MaximumRetryCount = 5; RetryIntervalSec = 10 }
  276. mkdir x-ui
  277. Copy-Item xui-release.exe x-ui\x-ui.exe
  278. mkdir x-ui\bin
  279. cd x-ui\bin
  280. # Download Xray for Windows
  281. $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.9.9/"
  282. Invoke-WebRequest @retry -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
  283. Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
  284. Remove-Item "Xray-windows-64.zip"
  285. Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
  286. Invoke-WebRequest @retry -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
  287. Invoke-WebRequest @retry -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
  288. Invoke-WebRequest @retry -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
  289. Invoke-WebRequest @retry -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
  290. Invoke-WebRequest @retry -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
  291. Invoke-WebRequest @retry -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
  292. Rename-Item xray.exe xray-windows-amd64.exe
  293. # mtg-multi (MTProto sidecar) publishes a prebuilt Windows binary, so
  294. # download and unpack it instead of compiling. The tag comes from the
  295. # release-page redirect on github.com — not api.github.com, whose
  296. # outages have failed release runs while asset downloads kept working.
  297. $MTG_MULTI_VER = (curl.exe -sf --retry 5 --retry-all-errors --retry-delay 3 -o NUL -w '%{redirect_url}' "https://github.com/mhsanaei/mtg-multi/releases/latest") -replace '^.*/releases/tag/', ''
  298. if (-not $MTG_MULTI_VER -or $MTG_MULTI_VER -notmatch '^v[\d.]+$') { throw "could not resolve the latest mtg-multi release tag" }
  299. $MTG_PKG = "mtg-multi-$($MTG_MULTI_VER.TrimStart('v'))-windows-amd64"
  300. curl.exe -sfLRO --retry 5 --retry-all-errors --retry-delay 3 "https://github.com/mhsanaei/mtg-multi/releases/download/$MTG_MULTI_VER/$MTG_PKG.zip"
  301. Expand-Archive -Path "$MTG_PKG.zip" -DestinationPath "mtg-tmp" -Force
  302. Move-Item "mtg-tmp/$MTG_PKG/mtg-multi.exe" "mtg-windows-amd64.exe"
  303. Remove-Item -Recurse -Force "mtg-tmp", "$MTG_PKG.zip"
  304. # TUIC sidecar for Windows
  305. curl.exe -sfLRo "tuic-server-windows-amd64.exe" --retry 5 --retry-all-errors --retry-delay 3 "https://github.com/EAimTY/tuic/releases/download/tuic-server-1.0.0/tuic-server-1.0.0-x86_64-pc-windows-msvc.exe"
  306. cd ..
  307. Copy-Item -Path ..\windows_files\* -Destination . -Recurse
  308. cd ..
  309. - name: Package to Zip
  310. shell: pwsh
  311. run: |
  312. Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
  313. $hash = (Get-FileHash x-ui-windows-amd64.zip -Algorithm SHA256).Hash.ToLower()
  314. [IO.File]::WriteAllText("$PWD\x-ui-windows-amd64.zip.sha256", "$hash x-ui-windows-amd64.zip`n")
  315. - name: Upload files to Artifacts
  316. uses: actions/upload-artifact@v7
  317. with:
  318. name: x-ui-windows-amd64
  319. path: |
  320. ./x-ui-windows-amd64.zip
  321. ./x-ui-windows-amd64.zip.sha256
  322. - name: Upload files to GH release
  323. uses: svenstaro/upload-release-action@v2
  324. if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
  325. with:
  326. repo_token: ${{ secrets.GITHUB_TOKEN }}
  327. tag: ${{ github.ref_name }}
  328. file: x-ui-windows-amd64.zip*
  329. file_glob: true
  330. overwrite: true
  331. prerelease: true
  332. # =================================
  333. # Rolling dev channel (per-commit)
  334. # =================================
  335. # Publishes/overwrites the build artifacts to a single fixed-tag pre-release
  336. # `dev-latest`, force-moved to the new commit on every push to main. The panel's
  337. # "Dev" update channel installs from this tag. `--latest=false` is load-bearing:
  338. # it keeps releases/latest pointing at the real stable tag, so the stable
  339. # channel is unaffected.
  340. publish-dev:
  341. name: Publish rolling dev release
  342. needs: [build, build-windows]
  343. if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  344. runs-on: ubuntu-latest
  345. permissions:
  346. contents: write
  347. # Serialize racing pushes; never cancel an in-flight upload, or the dev
  348. # release could be left with a partial asset set.
  349. concurrency:
  350. group: dev-release
  351. cancel-in-progress: false
  352. steps:
  353. - name: Checkout repository
  354. uses: actions/checkout@v7
  355. - name: Download all build artifacts
  356. uses: actions/download-artifact@v8
  357. with:
  358. path: dev-artifacts
  359. merge-multiple: true
  360. - name: Publish dev-latest pre-release
  361. env:
  362. GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  363. COMMIT: ${{ github.sha }}
  364. run: |
  365. set -e
  366. retry() {
  367. for i in 1 2 3 4 5; do
  368. "$@" && return 0
  369. echo "attempt $i failed: ${*:1:3}" >&2
  370. sleep $((i * 5))
  371. done
  372. return 1
  373. }
  374. short="${COMMIT::8}"
  375. notes="Rolling development build — installs via the panel's Dev update channel.
  376. commit=${COMMIT}
  377. built=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  378. Automated per-commit build from main. Not a stable release."
  379. # Force-move the dev-latest tag to this commit so the release tracks it.
  380. git tag -f dev-latest "${COMMIT}"
  381. retry git push -f origin refs/tags/dev-latest
  382. # The release exists on every run but the first; edit-first avoids an
  383. # existence probe that can 503 and mis-route into create (422).
  384. if ! retry gh release edit dev-latest --prerelease --latest=false \
  385. --title "Dev build ${short}" --notes "${notes}"; then
  386. retry gh release create dev-latest --prerelease --latest=false \
  387. --target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}"
  388. fi
  389. retry gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip dev-artifacts/*.sha256 --clobber