mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-31 04:12:13 +03:00
Bump xtls/xray-core to 5ca6f4b7d4dc (v26.7.28) and move the three binary pins (DockerInit.sh, the Linux and Windows URLs in release.yml) in lockstep so the in-process conf.Build() validation and the child binary agree. XMC finalmask (#6487) is the breaking change. The mask's `usernames` string list is gone, replaced by a required `profiles` array whose entries each need a 3-16 character [A-Za-z0-9_] username, a parseable UUID and both Mojang texture fields; the "default to Dream when empty" fallback was removed, so an xmc mask saved by an older panel now fails to build and takes the whole config down with it rather than degrading one inbound. The textures are a signed blob only Mojang's session server can issue, so a legacy username cannot be upgraded automatically. The panel now: - rejects an incomplete xmc mask at save time (AddInbound/UpdateInbound), pointing at the specific field that is missing; - drops only the offending mask when generating the core config, for rows that never went through the form (upgrade, node sync, restored backup, direct DB edit), warning which inbound lost its obfuscation instead of leaving every inbound offline; - carries legacy usernames into profile stubs in the finalmask form so the operator keeps their player names and sees exactly what still needs filling in, and edits profiles through a list editor. No destructive DB migration: unlike the removed shadowsocks ciphers there is no valid replacement to rewrite to, and dropping the mask from stored rows would discard the operator's hostname and password for config they can still repair. The generation-time strip already prevents the startup failure. Also track the core's xmux maxConnections fallback, lowered from 6 to 3 for anti-TSPU, in the fresh-XMUX seed so a new panel config matches what the core would pick on its own. TUN gained a `desc` key and random utunN naming, but the Go validator no longer accepts TUN inbounds and the panel only renders legacy saved rows, so nothing there needs adapting. The remaining commits are REALITY log-warning wording, gRPC/XHTTP localAddr accuracy and a routing tweak, none of which change the JSON config surface. Tests cross-check the panel's profile predicate against conf.XMCProfile.Build() so a future core release that tightens or relaxes the rules fails loudly rather than silently emitting configs the core refuses to start on.
401 lines
17 KiB
YAML
401 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 -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.7.28/"
|
|
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
|
|
|
|
- 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@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: >-
|
|
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: |
|
|
$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.7.28/"
|
|
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"
|
|
|
|
- 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@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 --clobber
|