Files
shater/.github/workflows/lx-release.yml
T
Leadaxe cb6f7f44fe fix(lx-release): pin gh release create to this repo with --repo
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.
2026-07-01 12:55:27 +03:00

407 lines
18 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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