Files
SubConverter-Extended/.github/workflows/build-dockerhub.yml
2026-05-27 21:24:10 +08:00

778 lines
30 KiB
YAML

name: Build and Push Docker image
on:
push:
branches:
- dev
tags:
- 'v*.*.*'
paths-ignore:
- 'README.md'
- 'README-*.md'
- 'docker-compose.yml'
- '.github/workflows/**'
- '!.github/workflows/build-dockerhub.yml'
pull_request:
branches:
- master
- dev
workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to rebuild, e.g. v1.2.3"
required: false
type: string
overwrite_existing_release:
description: "Overwrite assets/body of an existing GitHub Release"
required: false
default: false
type: boolean
schedule:
- cron: '0 19 * * 0'
permissions:
contents: write
packages: write
concurrency:
group: dockerhub-${{ github.ref_name }}
cancel-in-progress: true
jobs:
prepare:
name: "🔧 Prepare Metadata"
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') && (github.event_name != 'schedule' || github.ref == 'refs/heads/master')
outputs:
sha_short: ${{ steps.vars.outputs.sha_short }}
version: ${{ steps.version.outputs.version }}
is_release: ${{ steps.version.outputs.is_release }}
tags: ${{ steps.meta.outputs.tags }}
build_date: ${{ steps.build_date.outputs.build_date }}
linux_matrix: ${{ steps.linux_matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
token: ${{ secrets.PAT_TOKEN || github.token }}
fetch-depth: 0
- name: Get short SHA
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Determine version
id: version
env:
DISPATCH_RELEASE_TAG: ${{ inputs.release_tag }}
OVERWRITE_EXISTING_RELEASE: ${{ inputs.overwrite_existing_release }}
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
echo "Detected version from tag: $VERSION"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "$OVERWRITE_EXISTING_RELEASE" == "true" ]]; then
git fetch --tags --force origin
VERSION="${DISPATCH_RELEASE_TAG:-}"
VERSION="${VERSION#"${VERSION%%[![:space:]]*}"}"
VERSION="${VERSION%"${VERSION##*[![:space:]]}"}"
if [[ "$VERSION" != v* ]]; then
VERSION="v$VERSION"
fi
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid release tag '$VERSION'. Use vX.Y.Z, for example v1.2.3."
exit 1
fi
if ! git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "::error::Tag '$VERSION' does not exist on origin."
exit 1
fi
TAG_COMMIT="$(git rev-list -n 1 "$VERSION")"
MASTER_COMMIT="$(git rev-parse HEAD)"
echo "Overwrite release tag: $VERSION"
echo "Tag commit: $TAG_COMMIT"
echo "Master commit used for rebuilt assets: $MASTER_COMMIT"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == refs/heads/master ]] && ([[ "${{ github.event_name }}" == "workflow_dispatch" ]] || [[ "${{ github.event_name }}" == "schedule" ]]); then
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LATEST_TAG" ]; then
VERSION="$LATEST_TAG"
echo "Detected latest tag: $VERSION"
else
VERSION="master-$(git rev-parse --short HEAD)"
echo "No tags found, using commit hash: $VERSION"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
echo "Master branch build (manual/schedule): $VERSION"
else
echo "version=dev" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
echo "Using dev version"
fi
- name: Set build date
id: build_date
run: echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
- name: Build Linux matrix
id: linux_matrix
shell: bash
run: |
if [ "${{ steps.version.outputs.is_release }}" = "true" ]; then
{
echo 'matrix<<JSON'
cat <<'JSON'
{"include":[{"arch":"amd64","runner":"ubuntu-latest","dockerfile":"./Dockerfile","builder_platform":"linux/amd64","image_platform":"linux/amd64","builder_tag":"subconverter-temp:amd64-builder","threads":"16","cache_scope":"subconverter-alpine","extract_mode":"shared","extract_generated":"true","openwrt_arches":"x86_64","qemu_platforms":""},{"arch":"arm64","runner":"ubuntu-24.04-arm","dockerfile":"./Dockerfile","builder_platform":"linux/arm64","image_platform":"linux/arm64","builder_tag":"subconverter-temp:arm64-builder","threads":"4","cache_scope":"subconverter-alpine-arm64","extract_mode":"shared","extract_generated":"false","openwrt_arches":"aarch64_generic,aarch64_cortex-a53,aarch64_cortex-a72","qemu_platforms":""},{"arch":"armv7","runner":"ubuntu-latest","dockerfile":"./docker/Dockerfile.armv7-cross","builder_platform":"linux/amd64","image_platform":"linux/arm/v7","builder_tag":"subconverter-temp:armv7-builder","threads":"2","cache_scope":"subconverter-armv7-cross","extract_mode":"root","extract_generated":"false","openwrt_arches":"arm_cortex-a5_vfpv4,arm_cortex-a7,arm_cortex-a7_vfpv4,arm_cortex-a7_neon-vfpv4,arm_cortex-a8_vfpv3,arm_cortex-a9,arm_cortex-a9_neon,arm_cortex-a9_vfpv3-d16,arm_cortex-a15_neon-vfpv4","qemu_platforms":"arm"}]}
JSON
echo 'JSON'
} >> "$GITHUB_OUTPUT"
else
{
echo 'matrix<<JSON'
cat <<'JSON'
{"include":[{"arch":"amd64","runner":"ubuntu-latest","dockerfile":"./Dockerfile","builder_platform":"linux/amd64","image_platform":"linux/amd64","builder_tag":"subconverter-temp:amd64-builder","threads":"16","cache_scope":"subconverter-alpine","extract_mode":"shared","extract_generated":"true","openwrt_arches":"x86_64","qemu_platforms":""}]}
JSON
echo 'JSON'
} >> "$GITHUB_OUTPUT"
fi
- name: Docker meta
id: meta
shell: bash
env:
VERSION: ${{ steps.version.outputs.version }}
IS_RELEASE: ${{ steps.version.outputs.is_release }}
run: |
{
echo 'tags<<EOF'
if [ "$IS_RELEASE" = "true" ]; then
printf '%s\n' \
"aethersailor/subconverter-extended:latest" \
"aethersailor/subconverter-extended:${VERSION}" \
"ghcr.io/aethersailor/subconverter-extended:latest" \
"ghcr.io/aethersailor/subconverter-extended:${VERSION}"
fi
if [ "${GITHUB_REF}" = "refs/heads/dev" ]; then
printf '%s\n' \
"aethersailor/subconverter-extended:dev" \
"ghcr.io/aethersailor/subconverter-extended:dev"
fi
echo 'EOF'
} >> "$GITHUB_OUTPUT"
build-linux:
name: "🐳 Docker Build (${{ matrix.arch }})"
runs-on: ${{ matrix.runner }}
needs: prepare
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.prepare.outputs.linux_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
token: ${{ github.event_name == 'pull_request' && github.token || secrets.PAT_TOKEN }}
fetch-depth: 0
- name: Set up Docker Buildx
shell: bash
run: |
docker buildx create --name sce-builder --driver docker-container --use
docker buildx inspect --bootstrap
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
shell: bash
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: echo "$DOCKERHUB_TOKEN" | docker login --username "$DOCKERHUB_USERNAME" --password-stdin
- name: Log in to GHCR
if: github.event_name != 'pull_request'
shell: bash
env:
GHCR_USERNAME: ${{ github.actor }}
GHCR_TOKEN: ${{ github.token }}
run: echo "$GHCR_TOKEN" | docker login ghcr.io --username "$GHCR_USERNAME" --password-stdin
- name: Prepare Docker build args
id: docker_args
shell: bash
env:
THREADS: ${{ matrix.threads }}
SHA: ${{ needs.prepare.outputs.sha_short }}
VERSION: ${{ needs.prepare.outputs.version }}
BUILD_DATE: ${{ needs.prepare.outputs.build_date }}
MIHOMO_REF: Meta
MIHOMO_CACHE_BUST: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' && matrix.extract_generated == 'true' && github.run_id || 'stable' }}
SOURCE_DEPS_CACHE_BUST: ${{ github.event_name != 'pull_request' && github.run_id || 'stable' }}
REFRESH_GO_DEPS: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' && matrix.extract_generated == 'true' }}
REFRESH_HEADERS: ${{ github.event_name == 'push' && github.ref == 'refs/heads/dev' && matrix.extract_generated == 'true' }}
run: |
{
echo 'args<<EOF'
bash scripts/ci/docker-build-args.sh
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Build to builder stage
shell: bash
env:
BUILD_ARGS: ${{ steps.docker_args.outputs.args }}
run: |
set -euo pipefail
args=()
while IFS= read -r arg; do
[ -n "$arg" ] && args+=(--build-arg "$arg")
done <<< "$BUILD_ARGS"
docker buildx build \
--file "${{ matrix.dockerfile }}" \
--load \
--tag "${{ matrix.builder_tag }}" \
--target builder \
--platform "${{ matrix.builder_platform }}" \
"${args[@]}" \
.
- name: Extract builder output
shell: bash
env:
EXTRACT_GENERATED: ${{ matrix.extract_generated }}
run: bash scripts/ci/extract-builder-output.sh "${{ matrix.builder_tag }}" "${{ matrix.extract_mode }}"
- name: Package Release Artifacts (Linux ${{ matrix.arch }})
if: needs.prepare.outputs.is_release == 'true'
shell: bash
run: bash scripts/ci/build-linux-release.sh "${{ needs.prepare.outputs.version }}" "${{ matrix.arch }}" "${{ matrix.openwrt_arches }}"
- name: Set up QEMU for smoke test
if: needs.prepare.outputs.is_release == 'true' && matrix.qemu_platforms != ''
shell: bash
run: docker run --privileged --rm tonistiigi/binfmt --install "${{ matrix.qemu_platforms }}"
- name: Smoke test artifact (Linux ${{ matrix.arch }})
if: needs.prepare.outputs.is_release == 'true'
timeout-minutes: 3
uses: ./.github/actions/smoke-linux-artifact
with:
artifact: SubConverter-Extended-${{ needs.prepare.outputs.version }}-linux-${{ matrix.arch }}.tar.gz
- name: Smoke test APKs (OpenWrt ${{ matrix.arch }})
if: needs.prepare.outputs.is_release == 'true'
timeout-minutes: 3
uses: ./.github/actions/smoke-openwrt-apk
with:
artifacts: SubConverter-Extended-${{ needs.prepare.outputs.version }}-openwrt-*.apk
- name: Upload Artifact (Linux ${{ matrix.arch }})
if: needs.prepare.outputs.is_release == 'true'
uses: actions/upload-artifact@v7
with:
name: linux-${{ matrix.arch }}
path: SubConverter-Extended-*.tar.gz
- name: Upload Artifact (OpenWrt ${{ matrix.arch }})
if: needs.prepare.outputs.is_release == 'true'
uses: actions/upload-artifact@v7
with:
name: openwrt-${{ matrix.arch }}
path: SubConverter-Extended-*-openwrt-*.apk
- name: Clean release packaging files
if: needs.prepare.outputs.is_release == 'true'
shell: bash
run: rm -rf SubConverter-Extended SubConverter-Extended-*.tar.gz SubConverter-Extended-*.apk subconverter runtime-libs runtime-root libmihomo.so build/openwrt-apk
- name: Check for file changes
id: check_changes
if: matrix.extract_generated == 'true'
shell: bash
run: |
if [ -n "$(git status --porcelain -- bridge/go.mod bridge/go.sum bridge/libmihomo.h src/parser/mihomo_schemes.h src/parser/param_compat.h include/httplib.h include/nlohmann/json.hpp include/inja.hpp include/jpcre2.hpp include/quickjspp.hpp include/libcron include/date include/toml.hpp include/toml11)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Commit and push generated files
if: steps.check_changes.outputs.changed == 'true' && github.event_name != 'pull_request' && github.ref == 'refs/heads/dev'
shell: bash
run: |
BRANCH_NAME="dev"
echo "Dev branch build, pushing generated files to: $BRANCH_NAME"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add bridge/go.mod bridge/go.sum bridge/libmihomo.h src/parser/mihomo_schemes.h src/parser/param_compat.h include/httplib.h include/nlohmann/json.hpp include/inja.hpp include/jpcre2.hpp include/quickjspp.hpp include/libcron include/date include/toml.hpp include/toml11
git commit -m "chore: update auto-generated files and header libraries from build [skip ci]"
git fetch --no-tags origin "refs/heads/$BRANCH_NAME:refs/remotes/origin/$BRANCH_NAME"
git rebase "origin/$BRANCH_NAME"
git push origin "HEAD:refs/heads/$BRANCH_NAME"
- name: Report generated file changes outside dev
if: steps.check_changes.outputs.changed == 'true' && github.event_name != 'pull_request' && github.ref != 'refs/heads/dev'
shell: bash
run: |
echo "::warning::Generated file changes were detected on ${GITHUB_REF}, but source updates are committed only to dev."
- name: Build and push final image
id: build_image
shell: bash
env:
BUILD_ARGS: ${{ steps.docker_args.outputs.args }}
IMAGE_TAGS: ${{ needs.prepare.outputs.tags }}
run: |
set -euo pipefail
args=()
while IFS= read -r arg; do
[ -n "$arg" ] && args+=(--build-arg "$arg")
done <<< "$BUILD_ARGS"
tags=()
while IFS= read -r tag; do
[ -n "$tag" ] && tags+=(--tag "$tag")
done <<< "$IMAGE_TAGS"
if [ "${#tags[@]}" -eq 0 ]; then
tags+=(--tag "subconverter-extended:${{ matrix.arch }}-ci")
fi
output=(--load)
if [ "${{ github.event_name }}" != "pull_request" ]; then
output=(--push)
fi
metadata_file="build-metadata-${{ matrix.arch }}.json"
docker buildx build \
--file "${{ matrix.dockerfile }}" \
--platform "${{ matrix.image_platform }}" \
"${output[@]}" \
"${tags[@]}" \
"${args[@]}" \
--metadata-file "$metadata_file" \
.
digest="$(python3 - "$metadata_file" <<'PY'
import json
import sys
with open(sys.argv[1], "r", encoding="utf-8") as handle:
data = json.load(handle)
print(data.get("containerimage.digest", ""))
PY
)"
if [ -n "$digest" ]; then
echo "digest=$digest" >> "$GITHUB_OUTPUT"
fi
- name: Save image digest
if: needs.prepare.outputs.is_release == 'true' && github.event_name != 'pull_request'
shell: bash
run: |
mkdir -p docker-digests
printf '%s\n' "${{ steps.build_image.outputs.digest }}" > "docker-digests/${{ matrix.arch }}.txt"
- name: Upload image digest
if: needs.prepare.outputs.is_release == 'true' && github.event_name != 'pull_request'
uses: actions/upload-artifact@v7
with:
name: docker-digest-${{ matrix.arch }}
path: docker-digests/${{ matrix.arch }}.txt
- name: Smoke test image (${{ matrix.arch }})
if: github.event_name != 'pull_request' && needs.prepare.outputs.is_release == 'true'
timeout-minutes: 3
uses: ./.github/actions/smoke-docker-image
with:
image: ghcr.io/aethersailor/subconverter-extended@${{ steps.build_image.outputs.digest }}
platform: ${{ matrix.image_platform }}
build-windows-amd64:
name: "🪟 Windows Build (amd64)"
runs-on: windows-latest
needs: prepare
if: needs.prepare.outputs.is_release == 'true'
steps:
- name: Checkout
uses: actions/checkout@v6
with:
token: ${{ github.event_name == 'pull_request' && github.token || secrets.PAT_TOKEN }}
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: stable
cache-dependency-path: bridge/go.sum
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
path-type: inherit
update: true
install: >-
git
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja
mingw-w64-ucrt-x86_64-pkgconf
mingw-w64-ucrt-x86_64-curl
mingw-w64-ucrt-x86_64-yaml-cpp
mingw-w64-ucrt-x86_64-pcre2
mingw-w64-ucrt-x86_64-rapidjson
- name: Build Windows amd64
shell: msys2 {0}
run: |
THREADS=4 bash scripts/build-windows-amd64.sh \
"${{ needs.prepare.outputs.version }}" \
"${{ needs.prepare.outputs.sha_short }}" \
"${{ needs.prepare.outputs.build_date }}"
- name: Package Release Artifact (Windows amd64)
shell: pwsh
run: ./scripts/package-windows-portable.ps1 -Version "${{ needs.prepare.outputs.version }}"
- name: Smoke test artifact (Windows amd64)
timeout-minutes: 3
uses: ./.github/actions/smoke-windows-artifact
with:
artifact: SubConverter-Extended-${{ needs.prepare.outputs.version }}-windows-amd64.zip
- name: Upload Artifact (Windows amd64)
uses: actions/upload-artifact@v7
with:
name: windows-amd64
path: SubConverter-Extended-*.zip
merge-manifest:
name: "🔗 Docker Manifest"
runs-on: ubuntu-latest
needs: [prepare, build-linux]
if: needs.prepare.outputs.is_release == 'true' && github.event_name != 'pull_request'
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download image digests
uses: actions/download-artifact@v8
with:
path: digests
pattern: docker-digest-*
merge-multiple: true
- name: Create and push manifest list
shell: bash
run: |
set -euo pipefail
for arch in amd64 arm64 armv7; do
test -s "digests/${arch}.txt"
done
AMD64_DIGEST="$(cat digests/amd64.txt)"
ARM64_DIGEST="$(cat digests/arm64.txt)"
ARMV7_DIGEST="$(cat digests/armv7.txt)"
TAGS="${{ needs.prepare.outputs.tags }}"
IMAGE_NAMES=(
"aethersailor/subconverter-extended"
"ghcr.io/aethersailor/subconverter-extended"
)
for IMAGE_NAME in "${IMAGE_NAMES[@]}"; do
for TAG in $TAGS; do
if [[ "$TAG" == "$IMAGE_NAME:"* ]]; then
docker buildx imagetools create \
-t "$TAG" \
"${IMAGE_NAME}@${AMD64_DIGEST}" \
"${IMAGE_NAME}@${ARM64_DIGEST}" \
"${IMAGE_NAME}@${ARMV7_DIGEST}"
fi
done
done
- name: Verify manifest platforms
shell: bash
run: |
set -euo pipefail
TAGS="${{ needs.prepare.outputs.tags }}"
for TAG in $TAGS; do
if [[ "$TAG" != ghcr.io/* ]]; then
echo "Skipping Docker Hub platform inspection for $TAG to avoid pull rate limits."
echo "The manifest push step above already covered Docker Hub publication."
continue
fi
echo "Inspecting $TAG"
for attempt in $(seq 1 6); do
if docker buildx imagetools inspect "$TAG" > manifest.txt; then
break
fi
if [ "$attempt" -eq 6 ]; then
echo "Failed to inspect $TAG"
exit 1
fi
sleep 5
done
cat manifest.txt
for PLATFORM in "linux/amd64" "linux/arm64" "linux/arm/v7"; do
if ! grep -Eq "Platform:[[:space:]]+${PLATFORM}$" manifest.txt; then
echo "Missing platform ${PLATFORM} in ${TAG}"
exit 1
fi
done
done
create-release:
name: "🚀 Release Artifacts"
runs-on: ubuntu-latest
needs: [prepare, build-linux, build-windows-amd64, merge-manifest]
if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.overwrite_existing_release == true)
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Prepare Release Notes Context
env:
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
OVERWRITE_EXISTING_RELEASE: ${{ inputs.overwrite_existing_release }}
run: |
set -euo pipefail
git fetch --tags --force origin
CURRENT_TAG="$RELEASE_VERSION"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ] && [ "${OVERWRITE_EXISTING_RELEASE}" = "true" ]; then
CURRENT_COMMIT="$(git rev-parse HEAD)"
TAG_COMMIT="$(git rev-list -n 1 "$CURRENT_TAG")"
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "${TAG_COMMIT}^" 2>/dev/null || true)"
else
CURRENT_COMMIT="$(git rev-list -n 1 "$CURRENT_TAG")"
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "${CURRENT_COMMIT}^" 2>/dev/null || true)"
fi
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
if [ -n "$PREVIOUS_TAG" ]; then
RANGE_LABEL="$PREVIOUS_TAG..$CURRENT_COMMIT"
git log --pretty=format:'- %h %s' "$PREVIOUS_TAG..$CURRENT_COMMIT" > commits.md
git diff --name-only "$PREVIOUS_TAG" "$CURRENT_COMMIT" > changed-files.md
git diff --stat "$PREVIOUS_TAG" "$CURRENT_COMMIT" > diffstat.md
else
RANGE_LABEL="$CURRENT_COMMIT"
git log --pretty=format:'- %h %s' "$CURRENT_COMMIT" > commits.md
git diff --name-only "$EMPTY_TREE" "$CURRENT_COMMIT^{tree}" > changed-files.md
git diff --stat "$EMPTY_TREE" "$CURRENT_COMMIT^{tree}" > diffstat.md
fi
{
echo "# Release Context"
echo
echo "Current tag: $CURRENT_TAG"
echo "Current commit: $CURRENT_COMMIT"
echo "Previous tag: ${PREVIOUS_TAG:-none}"
echo "Range: $RANGE_LABEL"
echo
echo "## Commits"
cat commits.md
echo
echo
echo "## Changed files"
sed 's/^/- /' changed-files.md
echo
echo
echo "## Diff stat"
cat diffstat.md
} > release-context.md
{
echo "## English"
echo
echo "### Changes"
if [ -s commits.md ]; then
cat commits.md
else
echo "- No commit summary was available."
fi
echo
echo "## 中文"
echo
echo "### 变更"
echo "本次发布包含以下提交:"
if [ -s commits.md ]; then
cat commits.md
else
echo "- 未获取到提交摘要。"
fi
} > release-notes.fallback.md
- name: Check Copilot Token
id: copilot_token
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
run: |
if [ -n "${COPILOT_GITHUB_TOKEN:-}" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Node.js
if: steps.copilot_token.outputs.available == 'true'
uses: actions/setup-node@v6
- name: Generate Bilingual Release Notes with Copilot
if: steps.copilot_token.outputs.available == 'true'
continue-on-error: true
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
run: |
set -euo pipefail
npm install -g @github/copilot
PROMPT="$(cat <<'EOF'
You are writing release notes for SubConverter-Extended.
Use only the release context provided below. Do not invent features, fixes, platforms, or compatibility claims.
Summarize user-visible changes first. Mention maintenance or dependency changes only when relevant.
Ignore auto-generated files unless the context shows they affect users.
Return only Markdown. Do not wrap the answer in a code fence.
Do not include a downloads section; it will be appended by the workflow.
Required format:
## English
### Highlights
- ...
### Changes
- ...
## 中文
### 亮点
- ...
### 变更
- ...
Omit empty sections. Keep each language concise and accurate.
EOF
)"
PROMPT="$PROMPT
$(cat release-context.md)"
copilot -p "$PROMPT" --no-ask-user > release-notes.ai.md
grep -q '[[:alnum:]]' release-notes.ai.md
- name: Assemble Release Notes
run: |
set -euo pipefail
if [ -s release-notes.ai.md ]; then
cp release-notes.ai.md release-notes.md
echo "Using Copilot-generated release notes."
else
cp release-notes.fallback.md release-notes.md
echo "Using fallback release notes."
fi
cat >> release-notes.md <<'EOF'
## Docker Images
Docker images are published to:
- `aethersailor/subconverter-extended`
- `ghcr.io/aethersailor/subconverter-extended`
Supported Docker platforms:
- `linux/amd64`
- `linux/arm64`
- `linux/arm/v7`
## Downloads
### Linux (portable, glibc runtime bundled)
- **amd64**: `SubConverter-Extended-${{ needs.prepare.outputs.version }}-linux-amd64.tar.gz`
- **arm64**: `SubConverter-Extended-${{ needs.prepare.outputs.version }}-linux-arm64.tar.gz`
- **armv7**: `SubConverter-Extended-${{ needs.prepare.outputs.version }}-linux-armv7.tar.gz`
### Windows (portable)
- **amd64**: `SubConverter-Extended-${{ needs.prepare.outputs.version }}-windows-amd64.zip`
### OpenWrt 25.12+ APK (unsigned)
Install with `apk add --allow-untrusted ./<package>.apk`. Choose the APK whose OpenWrt architecture suffix matches `apk print-arch`.
User configuration is created at `/etc/subconverter/pref.toml` on first start and is not overwritten by upgrades.
- **x86_64**: `SubConverter-Extended-${{ needs.prepare.outputs.version }}-openwrt-x86_64.apk`
- **aarch64**: `SubConverter-Extended-${{ needs.prepare.outputs.version }}-openwrt-aarch64_generic.apk`, `...-aarch64_cortex-a53.apk`, `...-aarch64_cortex-a72.apk`
- **armv7**: `SubConverter-Extended-${{ needs.prepare.outputs.version }}-openwrt-arm_cortex-*.apk`
### Checksums
- `SHA256SUMS`
EOF
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.overwrite_existing_release }}" = "true" ]; then
{
echo
echo "<!-- rebuilt-from: $(git rev-parse HEAD) -->"
echo "<!-- rebuilt-at: $(date -u +%Y-%m-%dT%H:%M:%SZ) -->"
} >> release-notes.md
fi
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
pattern: '*'
merge-multiple: false
- name: Generate checksums
run: |
set -euo pipefail
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.apk' \) -print | sort | while read -r file; do
sha256sum "$file" | sed 's# artifacts/[^/]*/# #'
done > SHA256SUMS
cat SHA256SUMS
- name: Create Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with:
tag_name: ${{ needs.prepare.outputs.version }}
files: |
artifacts/linux-*/*.tar.gz
artifacts/openwrt-*/*.apk
artifacts/windows-*/*.zip
SHA256SUMS
body_path: release-notes.md
overwrite_files: true