778 lines
30 KiB
YAML
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
|