The base-version step (c4fd73cd) adds an `upstream` remote (SagerNet/sing-box)
so git-describe can see the v1.14.0-alpha.* tags. But `gh release create` without
--repo resolves the target repo from the remotes and picked `upstream` →
HTTP 403 "Resource not accessible by integration" against
api.github.com/repos/SagerNet/sing-box/releases (the token has no rights there).
This is why rc.19's builds all succeeded but publish failed, while rc.18 (before
the upstream remote existed) published fine.
Pin --repo "${{ github.repository }}" so publish always targets this fork
regardless of what remotes the earlier steps added.
407 lines
18 KiB
YAML
407 lines
18 KiB
YAML
name: lx-release
|
||
|
||
# Cross-builds the drop-in `sing-box` binary for all platforms and publishes a
|
||
# GitHub Release with archives + checksums. Triggered by pushing a tag like
|
||
# v1.13.13-lx.1, or manually via workflow_dispatch. See SPECS/004.
|
||
|
||
on:
|
||
push:
|
||
tags: ['v*-lx.*']
|
||
workflow_dispatch:
|
||
inputs:
|
||
tag:
|
||
description: 'Release tag, e.g. v1.13.13-lx.1 (created at the current ref if missing)'
|
||
required: true
|
||
|
||
permissions:
|
||
contents: write
|
||
|
||
# Build tags are owned by Makefile.lx (single source of truth). The desktop/CLI build
|
||
# uses its LX_TAGS default (which KEEPS with_clash_api — CLI binaries are driven by
|
||
# external dashboards over the Clash REST API); the Android AAR uses build_libbox's own
|
||
# tag set (which DROPS with_clash_api — LxBox uses the native CommandClient). The two
|
||
# sets diverge by design. `make -f Makefile.lx -s lx-print-tags` prints the desktop set
|
||
# for the release notes so nothing is duplicated here.
|
||
|
||
jobs:
|
||
build:
|
||
name: build ${{ matrix.goos }}/${{ matrix.goarch }}
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
# Linux lives in the build_linux_musl job (static musl + naive, for
|
||
# routers). See SPECS/006. This job covers the purego/native targets
|
||
# where libdl is a non-issue.
|
||
- { goos: darwin, goarch: amd64 }
|
||
- { goos: darwin, goarch: arm64 }
|
||
- { goos: windows, goarch: amd64, ext: .exe }
|
||
- { goos: windows, goarch: arm64, ext: .exe }
|
||
# Windows 7 (32-bit): built with a Win7-patched Go, and without
|
||
# with_naive_outbound (cronet-go has no windows/386 build). See SPECS/004.
|
||
- { goos: windows, goarch: "386", ext: .exe, legacy_win7: true, legacy_name: windows-7 }
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
submodules: recursive
|
||
fetch-depth: 0
|
||
|
||
- uses: actions/setup-go@v5
|
||
if: ${{ ! matrix.legacy_win7 }}
|
||
with:
|
||
go-version-file: go.mod
|
||
check-latest: true
|
||
|
||
# Win7 needs a Go toolchain that still targets Windows 7: setup_go_for_windows7.sh
|
||
# fetches stock Go and applies MetaCubeX/go patches reverting the Win7 removals.
|
||
- name: Cache Win7 Go toolchain
|
||
if: matrix.legacy_win7
|
||
id: cache-go-win7
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: ~/go/go_win7
|
||
key: go_win7_${{ hashFiles('.github/setup_go_for_windows7.sh') }}
|
||
- name: Build Win7 Go toolchain
|
||
if: matrix.legacy_win7 && steps.cache-go-win7.outputs.cache-hit != 'true'
|
||
env:
|
||
GITHUB_TOKEN: ${{ github.token }}
|
||
run: bash .github/setup_go_for_windows7.sh
|
||
- name: Use Win7 Go toolchain
|
||
if: matrix.legacy_win7
|
||
run: |
|
||
echo "PATH=$HOME/go/go_win7/bin:$PATH" >> "$GITHUB_ENV"
|
||
echo "GOROOT=$HOME/go/go_win7" >> "$GITHUB_ENV"
|
||
|
||
- name: Resolve version
|
||
id: ver
|
||
run: |
|
||
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Build
|
||
env:
|
||
GOOS: ${{ matrix.goos }}
|
||
GOARCH: ${{ matrix.goarch }}
|
||
run: |
|
||
TAGS="$(make -f Makefile.lx -s lx-print-tags)"
|
||
if [ "${{ matrix.legacy_win7 }}" = "true" ]; then
|
||
# cronet-go (with_naive_outbound) has no windows/386 build — drop it.
|
||
TAGS="${TAGS/with_naive_outbound,/}"
|
||
fi
|
||
make -f Makefile.lx lx-build \
|
||
LX_TAGS="$TAGS" \
|
||
LX_VERSION="${{ steps.ver.outputs.version }}" \
|
||
LX_OUTPUT="sing-box${{ matrix.ext }}"
|
||
|
||
- name: Package
|
||
run: |
|
||
NAME="sing-box-${{ steps.ver.outputs.version }}-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||
if [ -n "${{ matrix.legacy_name }}" ]; then
|
||
NAME="${NAME}-legacy-${{ matrix.legacy_name }}"
|
||
fi
|
||
mkdir -p "stage/$NAME" dist
|
||
cp "sing-box${{ matrix.ext }}" "stage/$NAME/"
|
||
cp LICENSE LICENSING.md README.md "stage/$NAME/" 2>/dev/null || true
|
||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||
(cd stage && zip -qr "../dist/$NAME.zip" "$NAME")
|
||
else
|
||
tar -C stage -czf "dist/$NAME.tar.gz" "$NAME"
|
||
fi
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: dist-${{ matrix.goos }}-${{ matrix.goarch }}
|
||
path: dist/*
|
||
if-no-files-found: error
|
||
|
||
# Static musl Linux builds for routers (AsusWRT Merlin, OpenWrt, Keenetic).
|
||
# The desktop `build` job ships Linux via with_purego, which pulls a dynamic
|
||
# libdl.so.2 dependency (purego's //go:cgo_import_dynamic) and won't load on
|
||
# musl. Here we mirror upstream build.yml: clone cronet-go, fetch the Chromium
|
||
# musl toolchain via its cmd/build-naive, and build CGO_ENABLED=1 with
|
||
# `with_musl` — libcronet.a is linked statically and naive is preserved.
|
||
# See SPECS/006.
|
||
build_linux_musl:
|
||
name: build linux-musl/${{ matrix.asset }}
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- { arch: amd64, asset: linux-amd64 }
|
||
- { arch: arm64, asset: linux-arm64 }
|
||
- { arch: arm, goarm: "7", asset: linux-armv7 }
|
||
- { arch: mipsle, gomips: softfloat, asset: linux-mipsle-softfloat }
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
submodules: recursive
|
||
fetch-depth: 0
|
||
|
||
- uses: actions/setup-go@v5
|
||
with:
|
||
go-version-file: go.mod
|
||
check-latest: true
|
||
|
||
- name: Resolve version
|
||
id: ver
|
||
run: |
|
||
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
||
|
||
# cronet-go carries the build-naive tool + the naiveproxy/src Chromium
|
||
# toolchain sources. Pin to the same commit go.mod depends on.
|
||
- name: Clone cronet-go
|
||
run: |
|
||
set -xeuo pipefail
|
||
CRONET_GO_VERSION="$(cat .github/CRONET_GO_VERSION)"
|
||
git init ~/cronet-go
|
||
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
|
||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||
git -C ~/cronet-go checkout FETCH_HEAD
|
||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||
|
||
- name: Regenerate Debian keyring
|
||
run: |
|
||
set -xeuo pipefail
|
||
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
||
cd ~/cronet-go
|
||
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
||
|
||
- name: Cache Chromium toolchain
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: |
|
||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
||
~/cronet-go/naiveproxy/src/gn/out/
|
||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
||
key: chromium-toolchain-musl-${{ matrix.arch }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||
|
||
- name: Download Chromium musl toolchain
|
||
run: |
|
||
set -xeuo pipefail
|
||
cd ~/cronet-go
|
||
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
|
||
|
||
- name: Set Chromium toolchain environment
|
||
run: |
|
||
set -xeuo pipefail
|
||
cd ~/cronet-go
|
||
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> "$GITHUB_ENV"
|
||
|
||
- name: Build (musl + naive, static)
|
||
env:
|
||
CGO_ENABLED: "1"
|
||
GOOS: linux
|
||
GOARCH: ${{ matrix.arch }}
|
||
GOARM: ${{ matrix.goarm }}
|
||
GOMIPS: ${{ matrix.gomips }}
|
||
run: |
|
||
set -xeuo pipefail
|
||
# LX_TAGS is the single source of truth (Makefile.lx). Swap the purego
|
||
# cronet loader for the static musl one; with_naive_outbound stays.
|
||
TAGS="$(make -f Makefile.lx -s lx-print-tags)"
|
||
TAGS="${TAGS/with_purego/with_musl}"
|
||
mkdir -p dist
|
||
go build -v -trimpath -tags "$TAGS" \
|
||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ steps.ver.outputs.version }}' -checklinkname=0 -s -w -buildid=" \
|
||
-o dist/sing-box ./cmd/sing-box
|
||
|
||
- name: Verify static (no libdl)
|
||
run: |
|
||
set -xeuo pipefail
|
||
file dist/sing-box
|
||
file dist/sing-box | grep -q "statically linked"
|
||
if strings -a dist/sing-box | grep -q "libdl.so.2"; then
|
||
echo "FAIL: libdl.so.2 reference present — not a static musl build"; exit 1
|
||
fi
|
||
echo "OK: statically linked, no libdl.so.2"
|
||
|
||
- name: Package
|
||
run: |
|
||
NAME="sing-box-${{ steps.ver.outputs.version }}-${{ matrix.asset }}"
|
||
mkdir -p "stage/$NAME"
|
||
cp dist/sing-box "stage/$NAME/"
|
||
cp LICENSE LICENSING.md README.md "stage/$NAME/" 2>/dev/null || true
|
||
tar -C stage -czf "dist/$NAME.tar.gz" "$NAME"
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: dist-${{ matrix.asset }}
|
||
path: dist/*.tar.gz
|
||
if-no-files-found: error
|
||
|
||
build_android:
|
||
name: build android (libbox.aar)
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
submodules: recursive
|
||
fetch-depth: 0
|
||
|
||
- uses: actions/setup-go@v5
|
||
with:
|
||
go-version-file: go.mod
|
||
check-latest: true
|
||
|
||
- name: Setup Android NDK
|
||
id: setup-ndk
|
||
uses: nttld/setup-ndk@v1
|
||
with:
|
||
ndk-version: r28
|
||
|
||
- name: Setup OpenJDK 17
|
||
run: sudo apt-get update && sudo apt-get install -y openjdk-17-jdk-headless
|
||
|
||
- name: Resolve version
|
||
id: ver
|
||
run: |
|
||
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
||
# build_libbox stamps Libbox.version() from `git describe` — ensure the tag exists.
|
||
git tag "$TAG" -f
|
||
|
||
- name: Build libbox.aar (with_xhttp + with_awg baked in by build_libbox)
|
||
run: |
|
||
make lib_install
|
||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||
make lib_android
|
||
env:
|
||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||
|
||
- name: Package AARs
|
||
run: |
|
||
mkdir -p dist
|
||
V="${{ steps.ver.outputs.version }}"
|
||
cp libbox.aar "dist/libbox-$V.aar"
|
||
cp libbox-legacy.aar "dist/libbox-legacy-$V.aar"
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: dist-android
|
||
path: dist/*
|
||
if-no-files-found: error
|
||
|
||
release:
|
||
name: publish release
|
||
needs: [build, build_linux_musl, build_android]
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- uses: actions/download-artifact@v4
|
||
with:
|
||
path: dist
|
||
merge-multiple: true
|
||
|
||
- name: Resolve tag
|
||
id: ver
|
||
run: |
|
||
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
||
# Derive the upstream base version for the release notes instead of hardcoding it
|
||
# (it drifted to alpha.35 across rc.14/15/16 and had to be hand-fixed each time).
|
||
# Primary source: the nearest reachable upstream alpha tag in HEAD's ancestry
|
||
# (`git describe`). This reads the commit GRAPH, not commit-message text, so it's
|
||
# correct even when a merge subject omits the alpha number — e.g. alpha.37 was
|
||
# merged in a commit titled "Merge upstream/testing (bump version, fix linux ping)"
|
||
# with no "alpha.37" in it, which the subject-grep fallback misses (shipped rc.17/18
|
||
# notes as alpha.36). Fallback: scan "Merge upstream" subjects for the highest
|
||
# alpha.NN. Last resort: a generic label so notes never carry a stale hardcoded version.
|
||
#
|
||
# NOTE: actions/checkout only fetches THIS repo's tags, not upstream's. The
|
||
# v1.14.0-alpha.* tags are SagerNet/sing-box tags, so without this fetch `git
|
||
# describe` finds nothing in CI and silently drops to the weaker subject-grep (which
|
||
# is why CI kept resolving alpha.36 while a local clone with upstream tags gets 37).
|
||
# Fetch the upstream alpha tags first so the primary `git describe` path works.
|
||
git remote add upstream https://github.com/SagerNet/sing-box.git 2>/dev/null || true
|
||
git fetch --no-tags upstream 'refs/tags/v1.14.0-alpha.*:refs/tags/v1.14.0-alpha.*' 2>/dev/null || true
|
||
BASE="$(git describe --tags --match 'v1.14.0-alpha.*' --abbrev=0 HEAD 2>/dev/null || true)"
|
||
if [ -z "$BASE" ]; then
|
||
BASE_N="$(git log --grep='Merge upstream' --pretty=%s | grep -oE 'alpha\.[0-9]+' | sed 's/alpha\.//' | sort -n | tail -1)"
|
||
[ -n "$BASE_N" ] && BASE="v1.14.0-alpha.${BASE_N}"
|
||
fi
|
||
[ -z "$BASE" ] && BASE="v1.14.x"
|
||
echo "base=$BASE" >> "$GITHUB_OUTPUT"
|
||
echo "Resolved upstream base: $BASE"
|
||
|
||
- name: Checksums
|
||
run: (cd dist && sha256sum * > SHA256SUMS && cat SHA256SUMS)
|
||
|
||
- name: Release notes
|
||
run: |
|
||
# What's-new for THIS tag comes from docs-lx/lx-changelog.md (single source of
|
||
# truth, kept per release) — extract the section for this version, between its
|
||
# "#### vX" header and the next "#### ", into a SEPARATE file. It must NOT be
|
||
# interpolated through the heredoc below: changelog prose contains backticks and
|
||
# $(...) that the shell would execute (command injection from doc text). We splice
|
||
# the file in with sed after the heredoc instead.
|
||
VERSION="${{ steps.ver.outputs.version }}"
|
||
awk -v v="#### v${VERSION}" '
|
||
$0==v {f=1; next} /^#### / {f=0} f' docs-lx/lx-changelog.md > changes.md
|
||
if [ -z "$(tr -d '[:space:]' < changes.md)" ]; then
|
||
echo "See [docs-lx/lx-changelog.md](https://github.com/Leadaxe/sing-box-lx/blob/lx-1.14/docs-lx/lx-changelog.md)." > changes.md
|
||
fi
|
||
cat > notes.md <<EOF
|
||
**sing-box-lx ${{ steps.ver.outputs.version }}** — a thin downstream of [sing-box](https://github.com/SagerNet/sing-box) (base **${{ steps.ver.outputs.base }}**, branch \`lx-1.14\`).
|
||
|
||
### What's new in this release
|
||
__CHANGES__
|
||
|
||
### Standing features
|
||
- **AmneziaWG 2.0** (\`with_awg\`) — \`wireguard\` endpoint with \`jc/jmin/jmax\`, \`s1\`–\`s4\`, \`h1\`–\`h4\`, \`i1\`–\`i5\`.
|
||
- **XHTTP** transport (\`with_xhttp\`) — Xray-compatible "splithttp", composes with Reality (use \`auto\`; \`stream-one\` has a known framing bug).
|
||
- **CommandClient extensions** (\`with_lx_command\`) — native libbox gRPC parity for the Clash API dropped from the **Android AAR**: URLTestOutbound, GetRules, GetGroups/GetOutbounds, Connection.Detour, SubscribeDNSQueries. (Desktop/CLI binaries keep \`with_clash_api\` for external dashboards.)
|
||
|
||
### Binaries
|
||
Drop-in \`sing-box\` for **darwin / windows** × {amd64, arm64}, plus a **Windows 7 (32-bit)** legacy build (\`sing-box-${{ steps.ver.outputs.version }}-windows-386-legacy-windows-7.zip\` — built with a Win7-patched Go; without naive/cronet, which has no windows/386 target).
|
||
|
||
**Linux — static musl builds for routers** (AsusWRT Merlin, OpenWrt, Keenetic): \`linux-amd64\`, \`linux-arm64\`, \`linux-armv7\`, \`linux-mipsle-softfloat\`. These are statically linked (no \`libdl.so.2\`/glibc dependency) and **keep NaïveProxy** — they run on musl routers where the previous dynamic builds failed with \`libdl.so.2: cannot open shared object file\`. See SPECS/006.
|
||
|
||
Each archive contains the \`sing-box\` binary (\`sing-box version\` reports \`${{ steps.ver.outputs.version }}\`). Verify downloads against \`SHA256SUMS\`.
|
||
|
||
### Android
|
||
\`libbox-${{ steps.ver.outputs.version }}.aar\` (+ \`libbox-legacy-…\` for SDK 21) — gomobile build of \`experimental/libbox\` with \`with_xhttp\`+\`with_awg\` enabled, for embedding in an Android app. \`Libbox.version()\` reports the lx version.
|
||
|
||
### Build tags
|
||
Desktop binary: \`$(make -f Makefile.lx -s lx-print-tags)\`
|
||
|
||
Config reference: [docs-lx/lx-config.md](https://github.com/Leadaxe/sing-box-lx/blob/lx/docs-lx/lx-config.md).
|
||
EOF
|
||
|
||
# Splice the changelog section in place of the placeholder. `sed r` inserts the
|
||
# file verbatim (no shell/sed interpretation of its contents), so backticks and
|
||
# $(...) in the prose are inert. Then drop the placeholder line itself. Written
|
||
# via a temp file (not sed -i, whose flag syntax differs BSD vs GNU).
|
||
sed -e '/__CHANGES__/r changes.md' -e '/__CHANGES__/d' notes.md > notes.final.md
|
||
mv notes.final.md notes.md
|
||
|
||
- name: Publish
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
run: |
|
||
# Pre-release tags (-rc.N / -alpha.N / -beta.N) publish as GitHub
|
||
# pre-releases so an unverified build never becomes "Latest". A plain
|
||
# vX.Y.Z-lx.N tag (no such suffix) publishes as a normal release.
|
||
EXTRA=""
|
||
case "${{ steps.ver.outputs.tag }}" in
|
||
*-rc.*|*-alpha.*|*-beta.*) EXTRA="--prerelease" ;;
|
||
esac
|
||
# --repo is REQUIRED: the base-version step adds an `upstream` remote
|
||
# (SagerNet/sing-box) so git-describe can see the alpha tags, and without
|
||
# --repo `gh` resolves the target repo from the remotes and picks upstream
|
||
# → HTTP 403 (the token has no rights there). Pin it to this repo.
|
||
gh release create "${{ steps.ver.outputs.tag }}" dist/* \
|
||
--repo "${{ github.repository }}" \
|
||
--title "sing-box-lx ${{ steps.ver.outputs.version }}" \
|
||
--notes-file notes.md \
|
||
--target "$GITHUB_SHA" \
|
||
$EXTRA
|