fix(ci): survive transient GitHub 5xx outages in the release workflow

A GitHub API 503 storm failed the release run four attempts in a row:
the mtg-multi latest-release lookup died on every platform (even s390x,
which never packages the sidecar), and the matrix default fail-fast then
cancelled the six healthy builds alongside the one that hit the outage.

Retry every external fetch with backoff (curl --retry-all-errors, wget
--retry-on-http-error, Invoke-* -MaximumRetryCount), scope the mtg-multi
tag lookup to the platforms that actually package the sidecar, disable
fail-fast on the build matrix, and retry the idempotent dev-latest
publish commands. The workflow file itself now triggers the run's path
filters, so editing it exercises the release build immediately instead
of failing silently at the next code push.
This commit is contained in:
MHSanaei
2026-07-17 01:06:14 +02:00
parent 5e1cb7693b
commit ab1a922806

View File

@@ -16,6 +16,7 @@ on:
- "x-ui.service.debian" - "x-ui.service.debian"
- "x-ui.service.arch" - "x-ui.service.arch"
- "x-ui.service.rhel" - "x-ui.service.rhel"
- ".github/workflows/release.yml"
pull_request: pull_request:
paths: paths:
- "**.go" - "**.go"
@@ -26,12 +27,15 @@ on:
- "x-ui.service.debian" - "x-ui.service.debian"
- "x-ui.service.arch" - "x-ui.service.arch"
- "x-ui.service.rhel" - "x-ui.service.rhel"
- ".github/workflows/release.yml"
jobs: jobs:
build: build:
permissions: permissions:
contents: write contents: write
strategy: strategy:
# One platform hitting a transient outage must not cancel the other six.
fail-fast: false
matrix: matrix:
platform: platform:
- amd64 - amd64
@@ -71,6 +75,8 @@ jobs:
- name: Build 3X-UI - name: Build 3X-UI
run: | 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 CGO_ENABLED=1
export GOOS=linux export GOOS=linux
export GOARCH=${{ matrix.platform }} export GOARCH=${{ matrix.platform }}
@@ -86,11 +92,11 @@ jobs:
esac esac
echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})" 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_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) 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; } [ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
echo "Downloading: $TARBALL_URL" echo "Downloading: $TARBALL_URL"
cd /tmp cd /tmp
curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL" curl -fL -sS $CURL_RETRY -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
tar -xf "$(basename "$TARBALL_URL")" tar -xf "$(basename "$TARBALL_URL")"
TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1) TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH" export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
@@ -120,53 +126,54 @@ jobs:
# Download dependencies # Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.7.11/" Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.7.11/"
if [ "${{ matrix.platform }}" == "amd64" ]; then if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip fetch ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip rm -f Xray-linux-64.zip
elif [ "${{ matrix.platform }}" == "arm64" ]; then elif [ "${{ matrix.platform }}" == "arm64" ]; then
wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip fetch ${Xray_URL}Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip rm -f Xray-linux-arm64-v8a.zip
elif [ "${{ matrix.platform }}" == "armv7" ]; then elif [ "${{ matrix.platform }}" == "armv7" ]; then
wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip fetch ${Xray_URL}Xray-linux-arm32-v7a.zip
unzip Xray-linux-arm32-v7a.zip unzip Xray-linux-arm32-v7a.zip
rm -f Xray-linux-arm32-v7a.zip rm -f Xray-linux-arm32-v7a.zip
elif [ "${{ matrix.platform }}" == "armv6" ]; then elif [ "${{ matrix.platform }}" == "armv6" ]; then
wget -q ${Xray_URL}Xray-linux-arm32-v6.zip fetch ${Xray_URL}Xray-linux-arm32-v6.zip
unzip Xray-linux-arm32-v6.zip unzip Xray-linux-arm32-v6.zip
rm -f Xray-linux-arm32-v6.zip rm -f Xray-linux-arm32-v6.zip
elif [ "${{ matrix.platform }}" == "386" ]; then elif [ "${{ matrix.platform }}" == "386" ]; then
wget -q ${Xray_URL}Xray-linux-32.zip fetch ${Xray_URL}Xray-linux-32.zip
unzip Xray-linux-32.zip unzip Xray-linux-32.zip
rm -f Xray-linux-32.zip rm -f Xray-linux-32.zip
elif [ "${{ matrix.platform }}" == "armv5" ]; then elif [ "${{ matrix.platform }}" == "armv5" ]; then
wget -q ${Xray_URL}Xray-linux-arm32-v5.zip fetch ${Xray_URL}Xray-linux-arm32-v5.zip
unzip Xray-linux-arm32-v5.zip unzip Xray-linux-arm32-v5.zip
rm -f Xray-linux-arm32-v5.zip rm -f Xray-linux-arm32-v5.zip
elif [ "${{ matrix.platform }}" == "s390x" ]; then elif [ "${{ matrix.platform }}" == "s390x" ]; then
wget -q ${Xray_URL}Xray-linux-s390x.zip fetch ${Xray_URL}Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip rm -f Xray-linux-s390x.zip
fi fi
rm -f geoip.dat geosite.dat rm -f geoip.dat geosite.dat
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat fetch 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 fetch 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 fetch -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 fetch -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 fetch -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 fetch -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
mv xray xray-linux-${{ matrix.platform }} mv xray xray-linux-${{ matrix.platform }}
# mtg-multi (MTProto sidecar) ships prebuilt release binaries whose # mtg-multi (MTProto sidecar) ships prebuilt release binaries whose
# platform labels match our matrix, so download and unpack the matching # platform labels match our matrix, so download and unpack the matching
# archive. Only the platforms the fork publishes are packaged. The tag # archive. Only the platforms the fork publishes are packaged — the tag
# is resolved from the fork's latest release so it never needs bumping # lookup lives inside that branch so unpackaged platforms (s390x) never
# here; the token only lifts the API rate limit for a public read. # depend on it. The tag is resolved from the fork's latest release so it
MTG_MULTI_VER=$(curl -sfL -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) # never needs bumping here; the token only lifts the API rate limit.
if [ -z "$MTG_MULTI_VER" ]; then echo "could not resolve the latest mtg-multi release tag"; exit 1; fi
case "${{ matrix.platform }}" in case "${{ matrix.platform }}" in
amd64|arm64|armv7|armv6|386) amd64|arm64|armv7|armv6|386)
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)
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 }}" MTG_PKG="mtg-multi-${MTG_MULTI_VER#v}-linux-${{ matrix.platform }}"
curl -sfLRO "https://github.com/mhsanaei/mtg-multi/releases/download/${MTG_MULTI_VER}/${MTG_PKG}.tar.gz" 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" tar -xzf "${MTG_PKG}.tar.gz"
mv "${MTG_PKG}/mtg-multi" "mtg-linux-${{ matrix.platform }}" mv "${MTG_PKG}/mtg-multi" "mtg-linux-${{ matrix.platform }}"
rm -rf "${MTG_PKG}" "${MTG_PKG}.tar.gz" rm -rf "${MTG_PKG}" "${MTG_PKG}.tar.gz"
@@ -267,6 +274,7 @@ jobs:
- name: Copy and download resources - name: Copy and download resources
shell: pwsh shell: pwsh
run: | run: |
$retry = @{ MaximumRetryCount = 5; RetryIntervalSec = 10 }
mkdir x-ui mkdir x-ui
Copy-Item xui-release.exe x-ui\x-ui.exe Copy-Item xui-release.exe x-ui\x-ui.exe
mkdir x-ui\bin mkdir x-ui\bin
@@ -274,25 +282,25 @@ jobs:
# Download Xray for Windows # Download Xray for Windows
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.7.11/" $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.7.11/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" Invoke-WebRequest @retry -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip" Remove-Item "Xray-windows-64.zip"
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue 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 @retry -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 @retry -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 @retry -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 @retry -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 @retry -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" 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 Rename-Item xray.exe xray-windows-amd64.exe
# mtg-multi (MTProto sidecar) publishes a prebuilt Windows binary, so # mtg-multi (MTProto sidecar) publishes a prebuilt Windows binary, so
# download and unpack it instead of compiling. The tag tracks the # download and unpack it instead of compiling. The tag tracks the
# fork's latest release so it never needs bumping here. # fork's latest release so it never needs bumping here.
$MTG_MULTI_VER = (Invoke-RestMethod -Uri "https://api.github.com/repos/mhsanaei/mtg-multi/releases/latest" -Headers @{ Authorization = "Bearer ${{ secrets.GITHUB_TOKEN }}"; "User-Agent" = "x-ui-release" }).tag_name $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
if (-not $MTG_MULTI_VER) { throw "could not resolve the latest mtg-multi release tag" } if (-not $MTG_MULTI_VER) { throw "could not resolve the latest mtg-multi release tag" }
$MTG_PKG = "mtg-multi-$($MTG_MULTI_VER.TrimStart('v'))-windows-amd64" $MTG_PKG = "mtg-multi-$($MTG_MULTI_VER.TrimStart('v'))-windows-amd64"
curl.exe -sfLRO "https://github.com/mhsanaei/mtg-multi/releases/download/$MTG_MULTI_VER/$MTG_PKG.zip" 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 Expand-Archive -Path "$MTG_PKG.zip" -DestinationPath "mtg-tmp" -Force
Move-Item "mtg-tmp/$MTG_PKG/mtg-multi.exe" "mtg-windows-amd64.exe" Move-Item "mtg-tmp/$MTG_PKG/mtg-multi.exe" "mtg-windows-amd64.exe"
Remove-Item -Recurse -Force "mtg-tmp", "$MTG_PKG.zip" Remove-Item -Recurse -Force "mtg-tmp", "$MTG_PKG.zip"
@@ -359,6 +367,14 @@ jobs:
COMMIT: ${{ github.sha }} COMMIT: ${{ github.sha }}
run: | run: |
set -e set -e
retry() {
for i in 1 2 3 4 5; do
"$@" && return 0
echo "attempt $i failed: $1" >&2
sleep $((i * 5))
done
return 1
}
short="${COMMIT::8}" short="${COMMIT::8}"
notes="Rolling development build — installs via the panel's Dev update channel. notes="Rolling development build — installs via the panel's Dev update channel.
@@ -369,14 +385,14 @@ jobs:
# Force-move the dev-latest tag to this commit so the release tracks it. # Force-move the dev-latest tag to this commit so the release tracks it.
git tag -f dev-latest "${COMMIT}" git tag -f dev-latest "${COMMIT}"
git push -f origin refs/tags/dev-latest retry git push -f origin refs/tags/dev-latest
if gh release view dev-latest >/dev/null 2>&1; then if gh release view dev-latest >/dev/null 2>&1; then
gh release edit dev-latest --prerelease --latest=false \ retry gh release edit dev-latest --prerelease --latest=false \
--title "Dev build ${short}" --notes "${notes}" --title "Dev build ${short}" --notes "${notes}"
else else
gh release create dev-latest --prerelease --latest=false \ retry gh release create dev-latest --prerelease --latest=false \
--target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}" --target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}"
fi fi
gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip --clobber retry gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip --clobber