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.
247 lines
9.7 KiB
YAML
247 lines
9.7 KiB
YAML
name: Release stable or release candidate
|
|
|
|
# Ways to release:
|
|
# 1) Push a SemVer tag: git tag v1.3.5 && git push origin v1.3.5
|
|
# 2) GitHub web UI → Actions → "Release stable or release candidate" → Run workflow
|
|
# Version: 1.3.5 or v1.3.5 (required)
|
|
# Ref: branch or SHA to tag (default main)
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: "Version to release (1.3.5 or v1.3.5; RC e.g. 1.4.0-rc.1)"
|
|
required: true
|
|
type: string
|
|
ref:
|
|
description: "Git ref to tag (branch name or commit SHA). Default: main"
|
|
required: false
|
|
type: string
|
|
default: main
|
|
permissions:
|
|
contents: read
|
|
concurrency:
|
|
group: release-formal-${{ inputs.version || github.ref_name }}
|
|
cancel-in-progress: false
|
|
jobs:
|
|
metadata:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
tag: ${{ steps.meta.outputs.tag }}
|
|
sha: ${{ steps.meta.outputs.sha }}
|
|
prerelease: ${{ steps.meta.outputs.prerelease }}
|
|
latest: ${{ steps.meta.outputs.latest }}
|
|
image_tag: ${{ steps.meta.outputs.image_tag }}
|
|
steps:
|
|
- name: Normalize version to SemVer tag
|
|
id: tagname
|
|
env:
|
|
RAW: ${{ inputs.version || github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
RAW="${RAW#refs/tags/}"
|
|
case "$RAW" in
|
|
v*) TAG="$RAW" ;;
|
|
*) TAG="v$RAW" ;;
|
|
esac
|
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
echo "Normalized release tag: $TAG"
|
|
|
|
- name: Checkout source (web UI uses ref; tag push uses the tag)
|
|
if: github.event_name == 'workflow_dispatch'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ inputs.ref || 'main' }}
|
|
fetch-depth: 0
|
|
|
|
- name: Checkout tag (tag-push path)
|
|
if: github.event_name == 'push'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ steps.tagname.outputs.tag }}
|
|
fetch-depth: 0
|
|
|
|
- name: Create annotated tag when releasing from the web UI
|
|
if: github.event_name == 'workflow_dispatch'
|
|
env:
|
|
TAG: ${{ steps.tagname.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
SHA="$(git rev-parse HEAD)"
|
|
echo "Source ref resolves to $SHA"
|
|
if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
|
|
# Peel annotated tags to the underlying commit.
|
|
EXISTING="$(git rev-parse "refs/tags/$TAG^{commit}" 2>/dev/null || git rev-parse "refs/tags/$TAG")"
|
|
if [ "$EXISTING" != "$SHA" ]; then
|
|
echo "Tag $TAG already points at $EXISTING, not $SHA. Refusing to move an immutable release tag." >&2
|
|
exit 1
|
|
fi
|
|
echo "Tag $TAG already exists on $SHA"
|
|
else
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git tag -a "$TAG" -m "Release $TAG" "$SHA"
|
|
git push origin "refs/tags/$TAG"
|
|
echo "Created and pushed tag $TAG → $SHA"
|
|
fi
|
|
|
|
- name: Resolve exact tag and channel
|
|
id: meta
|
|
env:
|
|
TAG: ${{ steps.tagname.outputs.tag }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
python3 scripts/release-metadata.py "$TAG"
|
|
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
if gh release view "$TAG" >/dev/null 2>release-error; then
|
|
echo "Release $TAG already exists. Published versions cannot be replaced." >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -qiE 'release not found|HTTP 404' release-error; then
|
|
cat release-error >&2
|
|
exit 1
|
|
fi
|
|
|
|
build:
|
|
needs: metadata
|
|
uses: ./.github/workflows/release-build.yml
|
|
with:
|
|
source_sha: ${{ needs.metadata.outputs.sha }}
|
|
version: ${{ needs.metadata.outputs.tag }}
|
|
|
|
release:
|
|
needs: [metadata, build]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.metadata.outputs.sha }}
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
path: release-files
|
|
merge-multiple: true
|
|
- name: Collect scripts and checksum every public asset
|
|
env:
|
|
VERSION: ${{ needs.metadata.outputs.tag }}
|
|
REPOSITORY: ${{ github.repository }}
|
|
run: |
|
|
cp scripts/{install.sh,update.sh,uninstall.sh,3m-ui.sh,3m-ui} release-files/
|
|
cp docker-compose*.yml release-files/
|
|
cp distribution/mihomo.env release-files/
|
|
sed -i "s#ghcr.io/kazeyukiro/3m-ui#ghcr.io/${REPOSITORY,,}#g" release-files/docker-compose*.yml
|
|
sed -i "s#kazeyukiro/3m-ui#${REPOSITORY}#g" release-files/install.sh
|
|
printf '%s\n' "$VERSION" > release-files/VERSION
|
|
printf '%s\n' "$REPOSITORY" > release-files/REPOSITORY
|
|
cd release-files
|
|
sha256sum * > SHA256SUMS
|
|
- uses: sigstore/cosign-installer@v3
|
|
- name: Sign checksums using this workflow identity
|
|
working-directory: release-files
|
|
run: |
|
|
cosign sign-blob --yes --output-certificate SHA256SUMS.pem \
|
|
--output-signature SHA256SUMS.sig SHA256SUMS
|
|
- name: Publish once without overwriting assets
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ needs.metadata.outputs.tag }}
|
|
PRERELEASE: ${{ needs.metadata.outputs.prerelease }}
|
|
LATEST: ${{ needs.metadata.outputs.latest }}
|
|
SHA: ${{ needs.metadata.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
# 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}" ] && git rev-parse "${PREV}^{commit}" >/dev/null 2>&1; then
|
|
echo "## Changes since ${PREV}"
|
|
echo
|
|
# 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
|
|
# 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 "$(printf '%s' "$ALL" | tr -d '[:space:]')" ]; then
|
|
echo "_No conventional commits in range._"
|
|
echo
|
|
fi
|
|
echo "---"
|
|
echo
|
|
echo "Complete Linux installation bundles are provided for amd64 and arm64."
|
|
echo "Each bundle includes the panel, its pinned Mihomo core and lifecycle scripts."
|
|
echo "Other architectures are standalone panel builds for advanced installations."
|
|
echo "Verify downloads against SHA256SUMS. SHA256SUMS.pem and SHA256SUMS.sig"
|
|
echo "provide Sigstore keyless verification for this release workflow."
|
|
echo "Docker images are published to ghcr.io/<owner>/<repository>:<tag>."
|
|
echo "The latest image and default installer follow stable releases only."
|
|
} > release-notes.md
|
|
cat release-notes.md
|
|
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
echo "Release $TAG already exists; refusing to overwrite."
|
|
exit 1
|
|
fi
|
|
gh release create "$TAG" --verify-tag --draft --title "$TAG" \
|
|
--notes-file release-notes.md
|
|
gh release upload "$TAG" release-files/*
|
|
gh release edit "$TAG" --draft=false --prerelease="$PRERELEASE" --latest="$LATEST"
|
|
|
|
image:
|
|
needs: [metadata, release]
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
uses: ./.github/workflows/release-image.yml
|
|
with:
|
|
source_sha: ${{ needs.metadata.outputs.sha }}
|
|
version: ${{ needs.metadata.outputs.tag }}
|
|
image_tag: ${{ needs.metadata.outputs.image_tag }}
|
|
latest: ${{ needs.metadata.outputs.latest == 'true' }}
|