fix(release,hy2): sane auto release notes; Loon-friendly HY2 share links
Release workflow: previous stable tag only, merge-base range, first-parent log, noise filter, and per-section caps so notes stay readable. Hysteria2 share/URI: resolve SNI from access profile/public host, emit allowInsecure alongside insecure, encode userinfo via url.User so Loon and similar clients can complete TLS.
This commit is contained in:
1 parent
81e465cc43
commit
bbe10da416
3 files changed
+112
-27
No files matched your search
@@ -159,32 +159,57 @@ jobs:
|
||||
SHA: ${{ needs.metadata.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Previous formal SemVer tag (ignore pre / the tag we are publishing).
|
||||
PREV="$(git tag -l 'v*' --sort=-version:refname | grep -E '^v[0-9]' | grep -vxF "$TAG" | head -n1 || true)"
|
||||
# Previous *stable* SemVer only (vMAJOR.MINOR.PATCH). Ignore RC tags and
|
||||
# the tag we are publishing. version:refname can otherwise pair a new
|
||||
# release with a divergent or pre-release tip and dump the whole history.
|
||||
PREV="$(git tag -l 'v*' --sort=-version:refname \
|
||||
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
| grep -vxF "$TAG" \
|
||||
| head -n1 || true)"
|
||||
{
|
||||
if [ -n "${PREV}" ]; then
|
||||
if [ -n "${PREV}" ] && git rev-parse "${PREV}^{commit}" >/dev/null 2>&1; then
|
||||
echo "## Changes since ${PREV}"
|
||||
echo
|
||||
RANGE="${PREV}..${SHA}"
|
||||
# Use merge-base so rewritten / non-linear history does not list
|
||||
# every commit reachable from SHA but not from the tag tip alone.
|
||||
MB="$(git merge-base "${PREV}^{commit}" "$SHA" 2>/dev/null || true)"
|
||||
if [ -n "$MB" ]; then
|
||||
RANGE="${MB}..${SHA}"
|
||||
else
|
||||
RANGE="${PREV}..${SHA}"
|
||||
fi
|
||||
else
|
||||
echo "## Changes"
|
||||
echo
|
||||
RANGE="${SHA}"
|
||||
fi
|
||||
# Categorize conventional commits (same shape as v1.3.7 notes).
|
||||
feats=$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges | grep -E '^- feat' || true)
|
||||
fixes=$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges | grep -E '^- fix' || true)
|
||||
perfs=$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges | grep -E '^- perf' || true)
|
||||
docs=$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges | grep -E '^- docs' || true)
|
||||
chores=$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges | grep -E '^- (chore|ci|build|refactor|style|test)' || true)
|
||||
other=$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges | grep -Ev '^- (feat|fix|perf|docs|chore|ci|build|refactor|style|test)' || true)
|
||||
if [ -n "$feats" ]; then echo "### Features"; echo "$feats"; echo; fi
|
||||
if [ -n "$fixes" ]; then echo "### Fixes"; echo "$fixes"; echo; fi
|
||||
if [ -n "$perfs" ]; then echo "### Performance"; echo "$perfs"; echo; fi
|
||||
if [ -n "$docs" ]; then echo "### Docs"; echo "$docs"; echo; fi
|
||||
if [ -n "$chores" ]; then echo "### Chore / CI"; echo "$chores"; echo; fi
|
||||
# Conventional commits on the first-parent path only; drop pure
|
||||
# formatting / CI noise that bloats notes (as seen on v1.3.9).
|
||||
log_lines() {
|
||||
git log "$RANGE" --first-parent --pretty=format:'- %s (%h)' --no-merges \
|
||||
| grep -Ev '^- (style:|style\(|chore: gofmt|ci: |test: format|test: provide|test: cover|test: preserve)' \
|
||||
|| true
|
||||
}
|
||||
ALL="$(log_lines)"
|
||||
# Cap each section so a large range stays readable in the GitHub UI.
|
||||
section() {
|
||||
local title="$1" pattern="$2"
|
||||
local body
|
||||
body="$(printf '%s\n' "$ALL" | grep -E "$pattern" | head -n 40 || true)"
|
||||
if [ -n "$body" ]; then
|
||||
echo "### ${title}"
|
||||
echo "$body"
|
||||
echo
|
||||
fi
|
||||
}
|
||||
section "Features" '^- feat'
|
||||
section "Fixes" '^- fix'
|
||||
section "Performance" '^- perf'
|
||||
section "Docs" '^- docs'
|
||||
section "Chore / CI" '^- (chore|ci|build|refactor|test)'
|
||||
other="$(printf '%s\n' "$ALL" | grep -Ev '^- (feat|fix|perf|docs|chore|ci|build|refactor|style|test)' | head -n 20 || true)"
|
||||
if [ -n "$other" ]; then echo "### Other"; echo "$other"; echo; fi
|
||||
if [ -z "$feats$fixes$perfs$docs$chores$other" ]; then
|
||||
if [ -z "$(printf '%s' "$ALL" | tr -d '[:space:]')" ]; then
|
||||
echo "_No conventional commits in range._"
|
||||
echo
|
||||
fi
|
||||
|
||||
@@ -297,6 +297,11 @@ func hysteria2URIs(name, host, port string, cfg map[string]interface{}) ([]strin
|
||||
if v, ok := cfg["sni"].(string); ok && v != "" {
|
||||
params["sni"] = v
|
||||
}
|
||||
if params["sni"] == "" {
|
||||
if s := uriTLSServerName(host); s != "" {
|
||||
params["sni"] = s
|
||||
}
|
||||
}
|
||||
// Matches the config-embedded users branch below: same listener must
|
||||
// produce the same link whichever way its users are stored.
|
||||
if clientSkipCert(cfg, host) {
|
||||
@@ -318,7 +323,8 @@ func hysteria2URIs(name, host, port string, cfg map[string]interface{}) ([]strin
|
||||
if v, ok := cfg["down"].(string); ok && v != "" {
|
||||
params["down"] = v
|
||||
}
|
||||
result = append(result, addName(query("hysteria2://"+url.PathEscape(password)+"@"+netutil.JoinHostPort(host, port), params), name))
|
||||
userinfo := url.User(password).String()
|
||||
result = append(result, addName(query("hysteria2://"+userinfo+"@"+netutil.JoinHostPort(host, port), params), name))
|
||||
}
|
||||
if len(result) > 0 {
|
||||
return result, nil
|
||||
@@ -338,6 +344,11 @@ func hysteria2URIs(name, host, port string, cfg map[string]interface{}) ([]strin
|
||||
if v, ok := cfg["sni"].(string); ok && v != "" {
|
||||
params["sni"] = v
|
||||
}
|
||||
if params["sni"] == "" {
|
||||
if s := uriTLSServerName(host); s != "" {
|
||||
params["sni"] = s
|
||||
}
|
||||
}
|
||||
if clientSkipCert(cfg, host) {
|
||||
params["insecure"] = "1"
|
||||
params["allowInsecure"] = "1"
|
||||
@@ -355,7 +366,8 @@ func hysteria2URIs(name, host, port string, cfg map[string]interface{}) ([]strin
|
||||
params["down"] = v
|
||||
}
|
||||
_ = username
|
||||
result = append(result, addName(query("hysteria2://"+url.PathEscape(password)+"@"+netutil.JoinHostPort(host, port), params), name))
|
||||
userinfo := url.User(password).String()
|
||||
result = append(result, addName(query("hysteria2://"+userinfo+"@"+netutil.JoinHostPort(host, port), params), name))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -594,3 +606,16 @@ func stringValue(v interface{}, fallback string) string {
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// uriTLSServerName returns the first non-IP hostname among candidates (for SNI).
|
||||
func uriTLSServerName(candidates ...string) string {
|
||||
for _, c := range candidates {
|
||||
c = strings.TrimSpace(c)
|
||||
c = strings.Trim(c, "[]")
|
||||
if c == "" || net.ParseIP(c) != nil {
|
||||
continue
|
||||
}
|
||||
return c
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -299,12 +299,24 @@ func (Hysteria2Compiler) BuildShare(in ShareInput) (Share, error) {
|
||||
if pass == "" {
|
||||
return Share{}, fmt.Errorf("hysteria2 share requires password")
|
||||
}
|
||||
params := map[string]string{}
|
||||
if spec.SNI != "" {
|
||||
params["sni"] = spec.SNI
|
||||
// SNI: listener field → access profile → public host / dial host when not an IP.
|
||||
// Loon and similar clients fail TLS handshake on bare-IP HY2 links without sni.
|
||||
sni := strings.TrimSpace(spec.SNI)
|
||||
if sni == "" {
|
||||
sni = strings.TrimSpace(in.Node.AccessSNI)
|
||||
}
|
||||
if spec.SkipCert {
|
||||
if sni == "" {
|
||||
sni = shareTLSServerName(host, in.Node.PublicHost)
|
||||
}
|
||||
skipCert := spec.SkipCert || shareHostLooksLikeIP(host)
|
||||
params := map[string]string{}
|
||||
if sni != "" {
|
||||
params["sni"] = sni
|
||||
}
|
||||
if skipCert {
|
||||
// insecure = Meta/Clash; allowInsecure = Loon / some mobile parsers
|
||||
params["insecure"] = "1"
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
if spec.Obfs != "" {
|
||||
params["obfs"] = spec.Obfs
|
||||
@@ -319,15 +331,17 @@ func (Hysteria2Compiler) BuildShare(in ShareInput) (Share, error) {
|
||||
params["down"] = spec.Down
|
||||
}
|
||||
applyALPNParams(params, spec.ALPN)
|
||||
// url.User encodes userinfo correctly (PathEscape alone breaks special passwords).
|
||||
userinfo := url.User(pass).String()
|
||||
uri := shareName(
|
||||
shareQuery("hysteria2://"+url.PathEscape(pass)+"@"+netutil.JoinHostPort(host, port), params),
|
||||
shareQuery("hysteria2://"+userinfo+"@"+netutil.JoinHostPort(host, port), params),
|
||||
in.Node.Name,
|
||||
)
|
||||
extra := map[string]interface{}{"password": pass}
|
||||
if spec.SNI != "" {
|
||||
extra["sni"] = spec.SNI
|
||||
if sni != "" {
|
||||
extra["sni"] = sni
|
||||
}
|
||||
if spec.SkipCert {
|
||||
if skipCert {
|
||||
extra["skip-cert-verify"] = true
|
||||
}
|
||||
if spec.Obfs != "" {
|
||||
@@ -880,6 +894,27 @@ func applyTransportParams(params map[string]string, t TransportSpec) {
|
||||
}
|
||||
}
|
||||
|
||||
// shareTLSServerName picks a hostname suitable for TLS SNI on share links.
|
||||
// Prefers non-IP candidates so clients (Loon, etc.) can complete the handshake
|
||||
// when the published endpoint is a raw address.
|
||||
func shareTLSServerName(candidates ...string) string {
|
||||
for _, c := range candidates {
|
||||
c = strings.TrimSpace(c)
|
||||
c = strings.Trim(c, "[]")
|
||||
if c == "" || net.ParseIP(c) != nil {
|
||||
continue
|
||||
}
|
||||
return c
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func shareHostLooksLikeIP(host string) bool {
|
||||
h := strings.TrimSpace(host)
|
||||
h = strings.Trim(h, "[]")
|
||||
return h != "" && net.ParseIP(h) != nil
|
||||
}
|
||||
|
||||
func applyALPNParams(params map[string]string, alpn []string) {
|
||||
clean := make([]string, 0, len(alpn))
|
||||
for _, value := range alpn {
|
||||
|
||||
Reference in new issue
Block a user