| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- name: Release 3X-UI
- on:
- workflow_dispatch:
- push:
- branches:
- - "**"
- tags:
- - "v*.*.*"
- paths:
- - "**.go"
- - "go.mod"
- - "go.sum"
- - "**.sh"
- - "frontend/**"
- - "x-ui.service.debian"
- - "x-ui.service.arch"
- - "x-ui.service.rhel"
- pull_request:
- paths:
- - "**.go"
- - "go.mod"
- - "go.sum"
- - "**.sh"
- - "frontend/**"
- - "x-ui.service.debian"
- - "x-ui.service.arch"
- - "x-ui.service.rhel"
- jobs:
- build:
- permissions:
- contents: write
- strategy:
- matrix:
- platform:
- - amd64
- - arm64
- - armv7
- - armv6
- - 386
- - armv5
- - s390x
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v7
- - name: Setup Go
- uses: actions/setup-go@v6
- with:
- go-version-file: go.mod
- check-latest: true
- # Frontend dist must be built BEFORE go build — Go's //go:embed
- # all:dist directive in internal/web/web.go requires internal/web/dist/ to exist
- # at compile time. internal/web/dist/ is .gitignored, so on a fresh CI
- # checkout it doesn't exist until vite emits it.
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: .nvmrc
- cache: 'npm'
- cache-dependency-path: frontend/package-lock.json
- - name: Build frontend bundle
- run: |
- npm ci
- npm run build
- working-directory: frontend
- - name: Build 3X-UI
- run: |
- export CGO_ENABLED=1
- export GOOS=linux
- export GOARCH=${{ matrix.platform }}
- # Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series)
- case "${{ matrix.platform }}" in
- amd64) BOOTLIN_ARCH="x86-64" ;;
- arm64) BOOTLIN_ARCH="aarch64" ;;
- armv7) BOOTLIN_ARCH="armv7-eabihf"; export GOARCH=arm GOARM=7 ;;
- armv6) BOOTLIN_ARCH="armv6-eabihf"; export GOARCH=arm GOARM=6 ;;
- armv5) BOOTLIN_ARCH="armv5-eabi"; export GOARCH=arm GOARM=5 ;;
- 386) BOOTLIN_ARCH="x86-i686" ;;
- s390x) BOOTLIN_ARCH="s390x-z13" ;;
- esac
- echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
- TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
- TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
- [ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
- echo "Downloading: $TARBALL_URL"
- cd /tmp
- curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
- tar -xf "$(basename "$TARBALL_URL")"
- TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
- export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
- export CC=$(realpath "$(find "$TOOLCHAIN_DIR/bin" -name '*-gcc.br_real' -type f -executable | head -n1)")
- [ -z "$CC" ] && { echo "No gcc.br_real found in $TOOLCHAIN_DIR/bin" >&2; exit 1; }
- cd -
- # Stamp the commit into per-commit (dev channel) builds only; tagged
- # stable releases stay unstamped so config.IsDevBuild() returns false.
- LDFLAGS="-w -s -linkmode external -extldflags '-static'"
- if [[ "$GITHUB_REF" != refs/tags/* ]]; then
- 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)"
- fi
- go build -ldflags "$LDFLAGS" -o xui-release -v main.go
- file xui-release
- ldd xui-release || echo "Static binary confirmed"
- mkdir x-ui
- cp xui-release x-ui/
- cp x-ui.service.debian x-ui/
- cp x-ui.service.arch x-ui/
- cp x-ui.service.rhel x-ui/
- cp x-ui.sh x-ui/
- mv x-ui/xui-release x-ui/x-ui
- mkdir x-ui/bin
- cd x-ui/bin
- # Download dependencies
- Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.6.22/"
- if [ "${{ matrix.platform }}" == "amd64" ]; then
- wget -q ${Xray_URL}Xray-linux-64.zip
- unzip Xray-linux-64.zip
- rm -f Xray-linux-64.zip
- elif [ "${{ matrix.platform }}" == "arm64" ]; then
- wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip
- unzip Xray-linux-arm64-v8a.zip
- rm -f Xray-linux-arm64-v8a.zip
- elif [ "${{ matrix.platform }}" == "armv7" ]; then
- wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip
- unzip Xray-linux-arm32-v7a.zip
- rm -f Xray-linux-arm32-v7a.zip
- elif [ "${{ matrix.platform }}" == "armv6" ]; then
- wget -q ${Xray_URL}Xray-linux-arm32-v6.zip
- unzip Xray-linux-arm32-v6.zip
- rm -f Xray-linux-arm32-v6.zip
- elif [ "${{ matrix.platform }}" == "386" ]; then
- wget -q ${Xray_URL}Xray-linux-32.zip
- unzip Xray-linux-32.zip
- rm -f Xray-linux-32.zip
- elif [ "${{ matrix.platform }}" == "armv5" ]; then
- wget -q ${Xray_URL}Xray-linux-arm32-v5.zip
- unzip Xray-linux-arm32-v5.zip
- rm -f Xray-linux-arm32-v5.zip
- elif [ "${{ matrix.platform }}" == "s390x" ]; then
- wget -q ${Xray_URL}Xray-linux-s390x.zip
- unzip Xray-linux-s390x.zip
- rm -f Xray-linux-s390x.zip
- fi
- rm -f geoip.dat geosite.dat
- wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
- wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
- wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
- wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
- wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
- wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
- mv xray xray-linux-${{ matrix.platform }}
- # mtg (MTProto sidecar) - only for arches mtg publishes
- MTG_VER="2.2.8"
- case "${{ matrix.platform }}" in
- amd64|arm64|armv7|armv6|386)
- wget -q "https://github.com/9seconds/mtg/releases/download/v${MTG_VER}/mtg-${MTG_VER}-linux-${{ matrix.platform }}.tar.gz"
- tar -xzf "mtg-${MTG_VER}-linux-${{ matrix.platform }}.tar.gz"
- mv "mtg-${MTG_VER}-linux-${{ matrix.platform }}/mtg" "mtg-linux-${{ matrix.platform }}" 2>/dev/null || mv mtg "mtg-linux-${{ matrix.platform }}"
- rm -rf "mtg-${MTG_VER}-linux-${{ matrix.platform }}" "mtg-${MTG_VER}-linux-${{ matrix.platform }}.tar.gz"
- ;;
- esac
- cd ../..
- - name: Package
- run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
- - name: Upload files to Artifacts
- uses: actions/upload-artifact@v7
- with:
- name: x-ui-linux-${{ matrix.platform }}
- path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
- - name: Upload files to GH release
- uses: svenstaro/upload-release-action@v2
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
- with:
- repo_token: ${{ secrets.GITHUB_TOKEN }}
- tag: ${{ github.ref_name }}
- file: x-ui-linux-${{ matrix.platform }}.tar.gz
- asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
- overwrite: true
- prerelease: true
- # =================================
- # Windows Build
- # =================================
- build-windows:
- name: Build for Windows
- permissions:
- contents: write
- strategy:
- matrix:
- platform:
- - amd64
- runs-on: windows-latest
- steps:
- - name: Checkout repository
- uses: actions/checkout@v7
- - name: Setup Go
- uses: actions/setup-go@v6
- with:
- go-version-file: go.mod
- check-latest: true
- # Frontend dist must be built BEFORE go build — see comment on the
- # Linux job above. This step is identical except npm runs on the
- # Windows runner here.
- - name: Setup Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: .nvmrc
- cache: 'npm'
- cache-dependency-path: frontend/package-lock.json
- - name: Build frontend bundle
- shell: pwsh
- run: |
- npm ci
- npm run build
- working-directory: frontend
- - name: Install MSYS2
- uses: msys2/setup-msys2@v2
- with:
- msystem: MINGW64
- update: true
- install: >-
- mingw-w64-x86_64-gcc
- mingw-w64-x86_64-sqlite3
- mingw-w64-x86_64-pkg-config
- - name: Build 3X-UI for Windows (CGO)
- shell: msys2 {0}
- run: |
- export PATH="/c/hostedtoolcache/windows/go/$(ls /c/hostedtoolcache/windows/go | sort -V | tail -n1)/x64/bin:$PATH"
- export CGO_ENABLED=1
- export GOOS=windows
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
- which go
- go version
- gcc --version
- # Stamp the commit into per-commit (dev channel) builds only.
- LDFLAGS="-w -s"
- if [[ "$GITHUB_REF" != refs/tags/* ]]; then
- 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)"
- fi
- go build -ldflags "$LDFLAGS" -o xui-release.exe -v main.go
- - name: Copy and download resources
- shell: pwsh
- run: |
- mkdir x-ui
- Copy-Item xui-release.exe x-ui\x-ui.exe
- mkdir x-ui\bin
- cd x-ui\bin
- # Download Xray for Windows
- $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.6.22/"
- Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
- Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
- Remove-Item "Xray-windows-64.zip"
- Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
- Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
- Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
- Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
- Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
- Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
- Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
- Rename-Item xray.exe xray-windows-amd64.exe
- # Download mtg (MTProto sidecar) for Windows
- $MTG_VER = "2.2.8"
- Invoke-WebRequest -Uri "https://github.com/9seconds/mtg/releases/download/v$MTG_VER/mtg-$MTG_VER-windows-amd64.zip" -OutFile "mtg-windows-amd64.zip"
- Expand-Archive -Path "mtg-windows-amd64.zip" -DestinationPath "mtg-tmp"
- $mtgExe = Get-ChildItem -Path "mtg-tmp" -Recurse -Filter "mtg.exe" | Select-Object -First 1
- Move-Item $mtgExe.FullName "mtg-windows-amd64.exe"
- Remove-Item "mtg-windows-amd64.zip", "mtg-tmp" -Recurse -Force
- cd ..
- Copy-Item -Path ..\windows_files\* -Destination . -Recurse
- cd ..
- - name: Package to Zip
- shell: pwsh
- run: |
- Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
- - name: Upload files to Artifacts
- uses: actions/upload-artifact@v7
- with:
- name: x-ui-windows-amd64
- path: ./x-ui-windows-amd64.zip
- - name: Upload files to GH release
- uses: svenstaro/upload-release-action@v2
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
- with:
- repo_token: ${{ secrets.GITHUB_TOKEN }}
- tag: ${{ github.ref_name }}
- file: x-ui-windows-amd64.zip
- asset_name: x-ui-windows-amd64.zip
- overwrite: true
- prerelease: true
- # =================================
- # Rolling dev channel (per-commit)
- # =================================
- # Publishes/overwrites the build artifacts to a single fixed-tag pre-release
- # `dev-latest`, force-moved to the new commit on every push to main. The panel's
- # "Dev" update channel installs from this tag. `--latest=false` is load-bearing:
- # it keeps releases/latest pointing at the real stable tag, so the stable
- # channel is unaffected.
- publish-dev:
- name: Publish rolling dev release
- needs: [build, build-windows]
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- runs-on: ubuntu-latest
- permissions:
- contents: write
- # Serialize racing pushes; never cancel an in-flight upload, or the dev
- # release could be left with a partial asset set.
- concurrency:
- group: dev-release
- cancel-in-progress: false
- steps:
- - name: Checkout repository
- uses: actions/checkout@v7
- - name: Download all build artifacts
- uses: actions/download-artifact@v7
- with:
- path: dev-artifacts
- merge-multiple: true
- - name: Publish dev-latest pre-release
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- COMMIT: ${{ github.sha }}
- run: |
- set -e
- short="${COMMIT::8}"
- notes="Rolling development build — installs via the panel's Dev update channel.
- commit=${COMMIT}
- built=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- Automated per-commit build from main. Not a stable release."
- # Force-move the dev-latest tag to this commit so the release tracks it.
- git tag -f dev-latest "${COMMIT}"
- git push -f origin refs/tags/dev-latest
- if gh release view dev-latest >/dev/null 2>&1; then
- gh release edit dev-latest --prerelease --latest=false \
- --title "Dev build ${short}" --notes "${notes}"
- else
- gh release create dev-latest --prerelease --latest=false \
- --target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}"
- fi
- gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip --clobber
|