mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-10 12:17:14 +00:00
d0edbcec81
Bump xtls/xray-core to 52a412d9e2f5 (v26.9.9) and the three binary pins in DockerInit.sh and release.yml in lockstep. Upstream moved UDP port hopping out of finalmask.quicParams.udpHop and into a standalone "udphop" UDP mask with a different shape (mode / interval / remotePorts / remoteIPs). The old key is gone from QuicParams, and since the config loader ignores unknown fields it is now silently dropped rather than rejected — port hopping just stops. The panel adapts where that key was live: - Both link importers rebuilt quicParams.udpHop from the standard mport param, so an imported hysteria2 link produced an outbound that no longer hops. They now emit a udphop mask in intervalremote mode, which is what the old key did. The mode is required: UDPHop.Build() rejects an empty or unknown one. - validFinalMaskUDPTypes and UdpMaskTypeSchema learn "udphop", otherwise the Go link generator strips the mask from every link and sub, and Zod strips it on the next form round trip. - mport generation (Go and frontend) reads the mask first and keeps reading the legacy key, so inbounds stored before the upgrade still advertise their range. On an inbound the old key was always inert — only hysteria's dialer consumed it — so nothing regresses server-side and no migration is needed. udphop stays out of the mask dropdown on purpose: it is client-only in core, which refuses to wrap a server socket, and that form is shared with the inbound editor.
410 lines
17 KiB
YAML
410 lines
17 KiB
YAML
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"
|
|
- ".github/workflows/release.yml"
|
|
pull_request:
|
|
paths:
|
|
- "**.go"
|
|
- "go.mod"
|
|
- "go.sum"
|
|
- "**.sh"
|
|
- "frontend/**"
|
|
- "x-ui.service.debian"
|
|
- "x-ui.service.arch"
|
|
- "x-ui.service.rhel"
|
|
- ".github/workflows/release.yml"
|
|
|
|
jobs:
|
|
build:
|
|
permissions:
|
|
contents: write
|
|
strategy:
|
|
# One platform hitting a transient outage must not cancel the other six.
|
|
fail-fast: false
|
|
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@v7
|
|
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@v7
|
|
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: |
|
|
CURL_RETRY="--retry 5 --retry-all-errors --retry-delay 3"
|
|
fetch() { wget -q --tries=5 --waitretry=10 --retry-on-http-error=429,500,502,503 "$@"; }
|
|
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 $CURL_RETRY "$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 $CURL_RETRY -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 -buildvcs=true -ldflags "$LDFLAGS" -o xui-release -v .
|
|
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.9.9/"
|
|
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
|
fetch ${Xray_URL}Xray-linux-64.zip
|
|
unzip Xray-linux-64.zip
|
|
rm -f Xray-linux-64.zip
|
|
elif [ "${{ matrix.platform }}" == "arm64" ]; then
|
|
fetch ${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
|
|
fetch ${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
|
|
fetch ${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
|
|
fetch ${Xray_URL}Xray-linux-32.zip
|
|
unzip Xray-linux-32.zip
|
|
rm -f Xray-linux-32.zip
|
|
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
|
fetch ${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
|
|
fetch ${Xray_URL}Xray-linux-s390x.zip
|
|
unzip Xray-linux-s390x.zip
|
|
rm -f Xray-linux-s390x.zip
|
|
fi
|
|
rm -f geoip.dat geosite.dat
|
|
fetch https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
|
fetch https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
|
fetch -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
|
fetch -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
|
fetch -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
|
fetch -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
|
mv xray xray-linux-${{ matrix.platform }}
|
|
# mtg-multi (MTProto sidecar) ships prebuilt release binaries whose
|
|
# platform labels match our matrix, so download and unpack the matching
|
|
# archive. Only the platforms the fork publishes are packaged — the tag
|
|
# lookup lives inside that branch so unpackaged platforms (s390x) never
|
|
# depend on it. The tag comes from the release-page redirect on
|
|
# github.com — the host the downloads need anyway — because api.github.com
|
|
# has 503'd whole release runs while asset downloads kept working.
|
|
case "${{ matrix.platform }}" in
|
|
amd64|arm64|armv7|armv6|386)
|
|
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')
|
|
if [ -z "$MTG_MULTI_VER" ]; then echo "could not resolve the latest mtg-multi release tag"; exit 1; fi
|
|
MTG_PKG="mtg-multi-${MTG_MULTI_VER#v}-linux-${{ matrix.platform }}"
|
|
curl -sfLRO $CURL_RETRY "https://github.com/mhsanaei/mtg-multi/releases/download/${MTG_MULTI_VER}/${MTG_PKG}.tar.gz"
|
|
tar -xzf "${MTG_PKG}.tar.gz"
|
|
mv "${MTG_PKG}/mtg-multi" "mtg-linux-${{ matrix.platform }}"
|
|
rm -rf "${MTG_PKG}" "${MTG_PKG}.tar.gz"
|
|
;;
|
|
esac
|
|
cd ../..
|
|
|
|
- name: Package
|
|
run: |
|
|
tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
|
sha256sum x-ui-linux-${{ matrix.platform }}.tar.gz > x-ui-linux-${{ matrix.platform }}.tar.gz.sha256
|
|
|
|
- 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
|
|
./x-ui-linux-${{ matrix.platform }}.tar.gz.sha256
|
|
|
|
- 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*
|
|
file_glob: true
|
|
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@v7
|
|
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@v7
|
|
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: >-
|
|
git
|
|
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 -buildvcs=true -ldflags "$LDFLAGS" -o xui-release.exe -v .
|
|
|
|
- name: Copy and download resources
|
|
shell: pwsh
|
|
run: |
|
|
$retry = @{ MaximumRetryCount = 5; RetryIntervalSec = 10 }
|
|
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.9.9/"
|
|
Invoke-WebRequest @retry -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 @retry -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
|
|
Invoke-WebRequest @retry -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
|
|
Invoke-WebRequest @retry -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
|
|
Invoke-WebRequest @retry -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
|
|
Invoke-WebRequest @retry -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
|
|
Invoke-WebRequest @retry -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
|
|
|
|
# mtg-multi (MTProto sidecar) publishes a prebuilt Windows binary, so
|
|
# download and unpack it instead of compiling. The tag comes from the
|
|
# release-page redirect on github.com — not api.github.com, whose
|
|
# outages have failed release runs while asset downloads kept working.
|
|
$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/', ''
|
|
if (-not $MTG_MULTI_VER -or $MTG_MULTI_VER -notmatch '^v[\d.]+$') { throw "could not resolve the latest mtg-multi release tag" }
|
|
$MTG_PKG = "mtg-multi-$($MTG_MULTI_VER.TrimStart('v'))-windows-amd64"
|
|
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"
|
|
Expand-Archive -Path "$MTG_PKG.zip" -DestinationPath "mtg-tmp" -Force
|
|
Move-Item "mtg-tmp/$MTG_PKG/mtg-multi.exe" "mtg-windows-amd64.exe"
|
|
Remove-Item -Recurse -Force "mtg-tmp", "$MTG_PKG.zip"
|
|
|
|
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"
|
|
$hash = (Get-FileHash x-ui-windows-amd64.zip -Algorithm SHA256).Hash.ToLower()
|
|
[IO.File]::WriteAllText("$PWD\x-ui-windows-amd64.zip.sha256", "$hash x-ui-windows-amd64.zip`n")
|
|
|
|
- name: Upload files to Artifacts
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: x-ui-windows-amd64
|
|
path: |
|
|
./x-ui-windows-amd64.zip
|
|
./x-ui-windows-amd64.zip.sha256
|
|
|
|
- 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*
|
|
file_glob: true
|
|
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@v8
|
|
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
|
|
retry() {
|
|
for i in 1 2 3 4 5; do
|
|
"$@" && return 0
|
|
echo "attempt $i failed: ${*:1:3}" >&2
|
|
sleep $((i * 5))
|
|
done
|
|
return 1
|
|
}
|
|
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}"
|
|
retry git push -f origin refs/tags/dev-latest
|
|
|
|
# The release exists on every run but the first; edit-first avoids an
|
|
# existence probe that can 503 and mis-route into create (422).
|
|
if ! retry gh release edit dev-latest --prerelease --latest=false \
|
|
--title "Dev build ${short}" --notes "${notes}"; then
|
|
retry gh release create dev-latest --prerelease --latest=false \
|
|
--target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}"
|
|
fi
|
|
|
|
retry gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip dev-artifacts/*.sha256 --clobber
|