Skip to content

Upstream watch

Upstream watch #24

name: Upstream watch
# Daily sweep: queries the Go module proxy for the latest tags of the
# three core deps (mihomo, sing-box, xray-core). If any have moved
# beyond what's pinned in go/go.mod, bump them, rebuild the
# xcframework, tag a v<YYYY.MM.DD> release, and publish the zip as a
# GitHub Release asset. Multiple bumps on the same day are batched
# into one release. Same-day re-runs (manual force) append .1, .2…
# to the tag.
on:
schedule:
- cron: '0 8 * * *' # 08:00 UTC daily
workflow_dispatch:
inputs:
force_release:
description: "Cut a release even if no upstream change is detected"
type: boolean
default: false
concurrency:
group: upstream-watch
cancel-in-progress: false
permissions:
contents: write
jobs:
watch-and-release:
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
persist-credentials: true
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go/go.mod
cache: true
cache-dependency-path: go/go.sum
- name: Install gomobile
run: |
set -euo pipefail
go install golang.org/x/mobile/cmd/gomobile@latest
go install golang.org/x/mobile/cmd/gobind@latest
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Detect upstream versions
id: versions
working-directory: go
run: |
set -euo pipefail
# The Go proxy's /@latest endpoint is defined to return the
# newest *stable* tag (no pre-releases) for a module. We
# query it directly rather than `go list -m -versions`, which
# would happily include `v1.14.0-alpha.23` in its output.
# For xray-core this returns the Go-module-compatible
# v1.YYMMDD.0 tag, not the human v26.x.x.
latest() {
curl -sSf "https://proxy.golang.org/$1/@latest" | jq -r '.Version'
}
# Current pin = the version go.mod's require block resolves to.
current() { go list -m -f '{{.Version}}' "$1"; }
mihomo_latest=$(latest github.com/metacubex/mihomo)
singbox_latest=$(latest github.com/sagernet/sing-box)
xray_latest=$(latest github.com/xtls/xray-core)
mihomo_current=$(current github.com/metacubex/mihomo)
singbox_current=$(current github.com/sagernet/sing-box)
xray_current=$(current github.com/xtls/xray-core)
{
echo "### Upstream pin check"
echo
echo "| module | current | latest | bump? |"
echo "|---|---|---|---|"
for trio in \
"mihomo|$mihomo_current|$mihomo_latest" \
"sing-box|$singbox_current|$singbox_latest" \
"xray-core|$xray_current|$xray_latest"; do
IFS='|' read -r name cur lat <<<"$trio"
[[ "$cur" == "$lat" ]] && marker="—" || marker="✓"
echo "| $name | $cur | $lat | $marker |"
done
} >> "$GITHUB_STEP_SUMMARY"
changed=false
[[ "$mihomo_current" != "$mihomo_latest" ]] && changed=true
[[ "$singbox_current" != "$singbox_latest" ]] && changed=true
[[ "$xray_current" != "$xray_latest" ]] && changed=true
{
echo "mihomo_latest=$mihomo_latest"
echo "singbox_latest=$singbox_latest"
echo "xray_latest=$xray_latest"
echo "mihomo_current=$mihomo_current"
echo "singbox_current=$singbox_current"
echo "xray_current=$xray_current"
echo "changed=$changed"
} >> "$GITHUB_OUTPUT"
- name: Decide whether to proceed
id: proceed
run: |
if [[ "${{ steps.versions.outputs.changed }}" == "true" \
|| "${{ inputs.force_release }}" == "true" ]]; then
echo "go=true" >> "$GITHUB_OUTPUT"
else
echo "go=false" >> "$GITHUB_OUTPUT"
echo "::notice::No upstream changes; skipping release."
fi
- name: Bump go.mod
if: steps.proceed.outputs.go == 'true'
working-directory: go
run: |
set -euo pipefail
go get github.com/metacubex/mihomo@${{ steps.versions.outputs.mihomo_latest }}
go get github.com/sagernet/sing-box@${{ steps.versions.outputs.singbox_latest }}
go get github.com/xtls/xray-core@${{ steps.versions.outputs.xray_latest }}
go mod tidy
- name: Build xcframework
if: steps.proceed.outputs.go == 'true'
run: Scripts/build.sh
- name: Compute release tag
if: steps.proceed.outputs.go == 'true'
id: tag
run: |
set -euo pipefail
base="v$(date -u +%Y.%m.%d)"
tag="$base"
n=1
# Already taken? Append .1, .2, … (manual force on the same day).
while git rev-parse --verify --quiet "refs/tags/$tag" >/dev/null; do
n=$((n+1))
tag="$base.$n"
done
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Zip + checksum
if: steps.proceed.outputs.go == 'true'
id: artifact
run: |
set -euo pipefail
zip_name="EverywhereCore-${{ steps.tag.outputs.tag }}.xcframework.zip"
# `ditto -c -k`, not `zip -r`: macOS frameworks rely on a
# symlink farm (`EverywhereCore.framework/EverywhereCore →
# Versions/Current/EverywhereCore`, `Versions/Current → A`,
# etc). Plain `zip` follows those symlinks and stores the
# dereferenced contents, so SwiftPM's extracted artifact
# ends up with duplicated regular files where symlinks
# should be — `codesign` then refuses the embedded
# framework with "code object is not signed at all".
# `ditto -c -k --sequesterRsrc --keepParent` is Apple's
# canonical bundle archiver: it preserves symlinks,
# resource forks, and xattrs, and produces a PKZip stream
# `swift package compute-checksum` reads the same way.
ditto -c -k --sequesterRsrc --keepParent \
EverywhereCore.xcframework "$zip_name"
sha=$(swift package compute-checksum "$zip_name")
echo "zip=$zip_name" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Commit + tag + push
if: steps.proceed.outputs.go == 'true'
run: |
set -euo pipefail
# Stash main's dev Package.swift; restored on top of the
# release commit so main HEAD keeps the local-path form.
dev_package=$(cat Package.swift)
# Release-flavored Package.swift: SwiftPM consumers pin a tag
# and resolve against the GitHub Release asset.
cat > Package.swift <<EOF
// swift-tools-version:5.9
//
// Auto-generated for the ${{ steps.tag.outputs.tag }} release by
// .github/workflows/upstream-watch.yml. The \`main\` branch
// keeps a local \`binaryTarget(path:)\` variant for in-tree
// development; this variant lives only on the tag.
import PackageDescription
let package = Package(
name: "EverywhereCore",
platforms: [
.iOS(.v15),
.macOS(.v13),
],
products: [
.library(name: "EverywhereCore", targets: ["EverywhereCore"]),
],
targets: [
.binaryTarget(
name: "EverywhereCore",
url: "https://github.com/${{ github.repository }}/releases/download/${{ steps.tag.outputs.tag }}/${{ steps.artifact.outputs.zip }}",
checksum: "${{ steps.artifact.outputs.sha }}"
),
]
)
EOF
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add go/go.mod go/go.sum Package.swift
git commit -m "release ${{ steps.tag.outputs.tag }}
mihomo: ${{ steps.versions.outputs.mihomo_current }} → ${{ steps.versions.outputs.mihomo_latest }}
sing-box: ${{ steps.versions.outputs.singbox_current }} → ${{ steps.versions.outputs.singbox_latest }}
xray-core: ${{ steps.versions.outputs.xray_current }} → ${{ steps.versions.outputs.xray_latest }}"
git tag -a "${{ steps.tag.outputs.tag }}" -m "${{ steps.tag.outputs.tag }}"
# Restore the dev Package.swift on top of the release commit so
# main consumers keep resolving the on-disk xcframework.
printf '%s' "$dev_package" > Package.swift
git add Package.swift
git commit -m "post-release: restore dev Package.swift"
# Tag + main in a single push. HEAD is detached at the
# checked-out SHA; HEAD:main pushes it to the main branch.
git push origin "HEAD:${{ github.event.repository.default_branch }}" "${{ steps.tag.outputs.tag }}"
- name: Create GitHub Release
if: steps.proceed.outputs.go == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
notes=$(cat <<EOF
### Upstream bumps
- **mihomo:** \`${{ steps.versions.outputs.mihomo_current }}\` → \`${{ steps.versions.outputs.mihomo_latest }}\`
- **sing-box:** \`${{ steps.versions.outputs.singbox_current }}\` → \`${{ steps.versions.outputs.singbox_latest }}\`
- **xray-core:** \`${{ steps.versions.outputs.xray_current }}\` → \`${{ steps.versions.outputs.xray_latest }}\`
### Consume
\`\`\`swift
.package(url: "https://github.com/${{ github.repository }}", from: "${{ steps.tag.outputs.tag }}")
\`\`\`
EOF
)
gh release create "${{ steps.tag.outputs.tag }}" \
"${{ steps.artifact.outputs.zip }}" \
--title "${{ steps.tag.outputs.tag }}" \
--notes "$notes"