release.yml 17 KB

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