name: Release 3X-UI on: workflow_dispatch: push: tags: - "v*.*" pull_request: paths: - '**.js' - '**.css' - '**.html' - '**.sh' - '**.go' - 'go.mod' - 'go.sum' - 'x-ui.service.debian' - 'x-ui.service.arch' - 'x-ui.service.rhel' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: analyze: name: Analyze Go code permissions: contents: read runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true - name: Check formatting run: | unformatted=$(gofmt -l .) if [ -n "$unformatted" ]; then echo "These files are not gofmt-formatted:" echo "$unformatted" exit 1 fi - name: Run go vet run: go vet ./... - name: Run staticcheck uses: dominikh/staticcheck-action@v1 with: version: "latest" install-go: false - name: Run tests run: go test -race -shuffle=on ./... build: timeout-minutes: 30 permissions: contents: write strategy: matrix: platform: - amd64 # 手动注释,请勿更改 # - arm64 # 手动注释,请勿更改 # - armv7 # 手动注释,请勿更改 # - armv6 # 手动注释,请勿更改 # - 386 # 手动注释,请勿更改 # - armv5 # 手动注释,请勿更改 # - s390x runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true - name: Cache Go build uses: actions/cache@v4 with: path: | ~/.cache/go-build key: gobuild-${{ matrix.platform }}-${{ hashFiles('**/*.go', 'go.mod', 'go.sum') }} restore-keys: | gobuild-${{ matrix.platform }}- - name: Get latest Xray version id: xray_version run: | LATEST=$(curl -fsSL https://api.github.com/repos/XTLS/Xray-core/releases/latest | jq -r '.tag_name') echo "version=$LATEST" >> "$GITHUB_OUTPUT" echo "Latest Xray version: $LATEST" - name: Cache Xray binary id: cache_xray uses: actions/cache@v4 with: path: /tmp/xray key: xray-${{ matrix.platform }}-${{ steps.xray_version.outputs.version }} restore-keys: | xray-${{ matrix.platform }}- - name: Download Xray if: steps.cache_xray.outputs.cache-hit != 'true' run: | Xray_URL="https://github.com/XTLS/Xray-core/releases/download/${{ steps.xray_version.outputs.version }}/" declare -A XRAY_MAP=( [amd64]="Xray-linux-64.zip" [arm64]="Xray-linux-arm64-v8a.zip" [armv7]="Xray-linux-arm32-v7a.zip" [armv6]="Xray-linux-arm32-v6.zip" [386]="Xray-linux-32.zip" [armv5]="Xray-linux-arm32-v5.zip" [s390x]="Xray-linux-s390x.zip" ) XRAY_FILE="${XRAY_MAP[${{ matrix.platform }}]}" [ -z "$XRAY_FILE" ] && { echo "Unknown platform: ${{ matrix.platform }}" >&2; exit 1; } mkdir -p /tmp/xray curl -fL -sS --retry 3 --retry-delay 5 -o "/tmp/xray/$XRAY_FILE" "${Xray_URL}${XRAY_FILE}" || { echo "Failed to download Xray" >&2; exit 1; } cd /tmp/xray unzip -q "$XRAY_FILE" || { echo "Failed to unzip Xray" >&2; exit 1; } rm -f "$XRAY_FILE" [ -f xray ] || { echo "xray binary not found after extraction" >&2; exit 1; } - name: Get cache date id: cache_date run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" - name: Cache geo data id: cache_geo uses: actions/cache@v4 with: path: /tmp/geo key: geo-${{ steps.cache_date.outputs.date }} - name: Download geo data if: steps.cache_geo.outputs.cache-hit != 'true' run: | mkdir -p /tmp/geo cd /tmp/geo curl -fL -sS --retry 3 -o geoip.dat \ https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat & curl -fL -sS --retry 3 -o geosite.dat \ https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat & curl -fL -sS --retry 3 -o geoip_IR.dat \ https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat & curl -fL -sS --retry 3 -o geosite_IR.dat \ https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat & curl -fL -sS --retry 3 -o geoip_RU.dat \ https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat & curl -fL -sS --retry 3 -o geosite_RU.dat \ https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat & FAILED=0 for job in $(jobs -p); do wait "$job" || FAILED=1; done if [ "$FAILED" -eq 1 ]; then echo "One or more geo data downloads failed" >&2 exit 1 fi for f in *.dat; do if [ ! -s "$f" ]; then echo "Geo file $f is empty" >&2; exit 1 fi done - name: Cache Bootlin toolchain uses: actions/cache@v4 with: path: /tmp/toolchain key: bootlin-${{ matrix.platform }}-${{ hashFiles('.github/workflows/release.yml') }} restore-keys: | bootlin-${{ matrix.platform }}- - 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 TOOLCHAIN_DIR=$(find /tmp/toolchain -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" 2>/dev/null | head -n1) if [ -z "$TOOLCHAIN_DIR" ]; then echo "Cache miss, downloading toolchain..." mkdir -p /tmp/toolchain 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/toolchain curl -fL -sS --retry 3 --retry-delay 5 -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL" || { echo "Failed to download toolchain" >&2; exit 1; } tar -xf "$(basename "$TARBALL_URL")" || { echo "Failed to extract toolchain tarball" >&2; exit 1; } TOOLCHAIN_DIR=$(find /tmp/toolchain -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1) cd - else echo "Using cached toolchain: $TOOLCHAIN_DIR" fi 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; } go build -ldflags "-w -s -linkmode external -extldflags '-static'" -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 # Copy Xray from cache cp /tmp/xray/xray . # Copy geo data from cache cp /tmp/geo/* . mv xray xray-linux-${{ matrix.platform }} 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: Create release and upload assets if: startsWith(github.ref, 'refs/tags/') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${{ github.ref_name }}" if ! gh release view "$TAG" > /dev/null 2>&1; then gh release create "$TAG" --prerelease --title "$TAG" --generate-notes \ --notes "**Xray-core:** ${{ steps.xray_version.outputs.version }}" fi gh release upload "$TAG" x-ui-linux-${{ matrix.platform }}.tar.gz --clobber # ================================= # Windows Build (disabled) # ================================= # build-windows: # name: Build for Windows # permissions: # contents: write # strategy: # matrix: # platform: # - amd64 # runs-on: windows-latest # steps: # - name: Checkout repository # uses: actions/checkout@v6 # # - name: Setup Go # uses: actions/setup-go@v6 # with: # go-version-file: go.mod # check-latest: true # # - 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 # # go build -ldflags "-w -s" -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.2.6/" # 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 # 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 == 'release' # 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