mirror of
https://github.com/NewFuture/DDNS.git
synced 2026-10-20 08:17:43 +00:00
Bumps [Nuitka/Nuitka-Action](https://github.com/nuitka/nuitka-action) from 1.4 to 1.5. - [Release notes](https://github.com/nuitka/nuitka-action/releases) - [Commits](https://github.com/nuitka/nuitka-action/compare/v1.4...v1.5) --- updated-dependencies: - dependency-name: Nuitka/Nuitka-Action dependency-version: '1.5' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
674 lines
27 KiB
YAML
674 lines
27 KiB
YAML
name: Publish
|
|
run-name: Release ${{ inputs.release_tag }} from ${{ github.ref_name }}
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
release_tag:
|
|
description: "Release tag to create (for example v4.2.0 or v4.2.0-beta1)"
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: release-${{ inputs.release_tag }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
preflight:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 3
|
|
outputs:
|
|
release_tag: ${{ steps.validate.outputs.release_tag }}
|
|
steps:
|
|
- name: Validate selected branch, tag, and immutable source
|
|
id: validate
|
|
shell: bash
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.release_tag }}
|
|
SELECTED_REF_NAME: ${{ github.ref_name }}
|
|
SELECTED_REF_TYPE: ${{ github.ref_type }}
|
|
SELECTED_SHA: ${{ github.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
test "$SELECTED_REF_TYPE" = "branch"
|
|
case "$SELECTED_REF_NAME" in
|
|
master|v4) ;;
|
|
*) echo "Release dispatches must run from master or v4, not $SELECTED_REF_NAME" >&2; exit 1 ;;
|
|
esac
|
|
if ! [[ "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(alpha|beta|rc)([1-9][0-9]*))?$ ]]; then
|
|
echo "release_tag must use canonical form such as v4.2.0 or v4.2.0-beta1" >&2
|
|
exit 1
|
|
fi
|
|
if ! [[ "$SELECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then
|
|
echo "GitHub did not provide a full immutable commit SHA" >&2
|
|
exit 1
|
|
fi
|
|
printf 'release_tag=%s\n' "$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
|
|
build-pypi:
|
|
needs: preflight
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: actions/setup-python@v7
|
|
with:
|
|
python-version: "3.x"
|
|
- name: Install build dependency
|
|
run: pip install build
|
|
- name: Patch the selected release version
|
|
run: python3 .github/patch.py version
|
|
env:
|
|
DDNS_RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
- name: Replace relative README documentation URLs
|
|
run: sed -i'' -E 's#([("'"'"'`])(/docs/[^)"'"'"'`]+)\.md([)"'"'"'`])#\1https://ddns.newfuture.cc\2.html\3#g; s#([("'"'"'`])/docs/#\1https://ddns.newfuture.cc/docs/#g' README.md
|
|
- name: Run unit tests
|
|
run: python -m unittest discover tests -v
|
|
- name: Test package configuration fixtures
|
|
run: |
|
|
python3 -m ddns -c tests/config/callback.json
|
|
python3 -m ddns -c tests/config/multi-provider.json
|
|
python3 -m ddns -c tests/config/debug.json -c tests/config/noip.json
|
|
python3 -m ddns -c tests/config/he-proxies.json --debug
|
|
- name: Verify deployed debug configuration
|
|
run: python3 -m ddns -c https://ddns.newfuture.cc/tests/config/debug.json
|
|
- name: Build package
|
|
run: python -m build --sdist --wheel --outdir dist/
|
|
- name: Test pip install from wheel
|
|
run: |
|
|
wheel_file=$(find dist -name "*.whl" -type f)
|
|
test -n "$wheel_file" || { echo "No wheel file found"; exit 1; }
|
|
python3 -m pip install --force-reinstall "$wheel_file"
|
|
ddns --version
|
|
ddns --help
|
|
ddns --new-config /tmp/test-pip-config.json
|
|
test -f /tmp/test-pip-config.json
|
|
python3 -m ddns --version
|
|
(cd /tmp && python3 -c 'from ddns.web.server import _resource_bytes; assert b"<html" in _resource_bytes("index.html")')
|
|
- name: Test pip install from source distribution
|
|
run: |
|
|
python3 -m pip uninstall -y ddns
|
|
tarball_file=$(find dist -name "*.tar.gz" -type f)
|
|
test -n "$tarball_file" || { echo "No source distribution found"; exit 1; }
|
|
python3 -m pip install --force-reinstall "$tarball_file"
|
|
ddns --version
|
|
python3 -m ddns --help
|
|
(cd /tmp && python3 -c 'from ddns.web.server import _resource_bytes; assert b"<html" in _resource_bytes("index.html")')
|
|
- name: Test task management from the source distribution
|
|
working-directory: /tmp
|
|
run: |
|
|
python_path="$(command -v python3)"
|
|
"$GITHUB_WORKSPACE/tests/scripts/test-task-systemd.sh" "$python_path -m ddns"
|
|
- name: Prepare versioned release document
|
|
run: python3 .github/patch.py release
|
|
env:
|
|
DDNS_RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-pypi
|
|
path: dist/
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-document
|
|
path: docs/release.md
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
build-native-binaries:
|
|
needs: preflight
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: windows-latest
|
|
arch: x64
|
|
- os: windows-latest
|
|
arch: x86
|
|
- os: windows-11-arm
|
|
arch: arm64
|
|
- os: ubuntu-latest
|
|
arch: x64
|
|
- os: ubuntu-24.04-arm
|
|
arch: arm64
|
|
- os: macos-15-intel
|
|
arch: x64
|
|
- os: macos-latest
|
|
arch: arm64
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: ${{ matrix.arch == 'x86' && 25 || contains(matrix.os, 'windows') && 16 || 12 }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: actions/setup-python@v7
|
|
with:
|
|
python-version: "3.12"
|
|
architecture: ${{ matrix.arch }}
|
|
- name: Patch the selected release version
|
|
run: python3 .github/patch.py
|
|
env:
|
|
DDNS_RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
- name: Set up Linux
|
|
if: runner.os == 'Linux'
|
|
run: sudo apt-get install -y --no-install-recommends patchelf
|
|
- name: Set up macOS
|
|
if: runner.os == 'macOS'
|
|
run: python3 -m pip install imageio
|
|
- name: Run unit tests
|
|
run: python -m unittest discover tests -v
|
|
- name: Test run.py
|
|
run: python3 ./run.py -h
|
|
- name: Build onefile executable
|
|
uses: Nuitka/Nuitka-Action@v1.5
|
|
with:
|
|
nuitka-version: 4.2
|
|
script-name: run.py
|
|
mode: onefile
|
|
output-dir: dist
|
|
lto: yes
|
|
windows-icon-from-ico: ${{ runner.os == 'Windows' && 'docs/public/favicon.ico' || '' }}
|
|
linux-icon: ${{ runner.os == 'Linux' && 'docs/public/img/ddns.svg' || '' }}
|
|
static-libpython: ${{ runner.os == 'Linux' && 'yes' || 'auto' }}
|
|
macos-app-name: ${{ runner.os == 'macOS' && 'DDNS' || '' }}
|
|
macos-app-icon: ${{ runner.os == 'macOS' && 'docs/public/img/ddns.png' || '' }}
|
|
- run: ./dist/ddns || test -e config.json
|
|
- run: ./dist/ddns -v
|
|
- run: ./dist/ddns -h
|
|
- name: Run onefile binary E2E tests
|
|
run: python -m unittest tests.e2e -v
|
|
env:
|
|
DDNS_E2E_EXECUTABLE: ./dist/ddns${{ runner.os == 'Windows' && '.exe' || '' }}
|
|
PYTHONIOENCODING: utf-8
|
|
- run: ./dist/ddns task --status
|
|
- name: Test Windows schtasks task
|
|
if: runner.os == 'Windows'
|
|
run: tests/scripts/test-task-windows.bat .\\dist\\ddns.exe
|
|
- name: Test Linux systemd service
|
|
if: runner.os == 'Linux'
|
|
run: tests/scripts/test-task-systemd.sh "./dist/ddns"
|
|
- name: Test macOS launchd service
|
|
if: runner.os == 'macOS'
|
|
run: tests/scripts/test-task-macos.sh "./dist/ddns"
|
|
- name: Build Windows standalone application
|
|
if: runner.os == 'Windows'
|
|
uses: Nuitka/Nuitka-Action@v1.5
|
|
with:
|
|
nuitka-version: 4.2
|
|
script-name: run.py
|
|
mode: standalone
|
|
output-dir: dist-app
|
|
lto: yes
|
|
file-description: "DDNS客户端 ${{ needs.preflight.outputs.release_tag }}"
|
|
windows-console-mode: attach
|
|
windows-icon-from-ico: docs/public/favicon.ico
|
|
- name: Run Windows standalone E2E tests
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
$appDir = Get-ChildItem -Path 'dist-app' -Directory -Filter '*.dist' | Select-Object -First 1
|
|
if (-not $appDir) { throw 'Standalone app folder (*.dist) not found' }
|
|
$executable = Join-Path $appDir.FullName 'ddns.exe'
|
|
if (-not (Test-Path $executable)) { throw 'Standalone ddns.exe not found' }
|
|
$env:DDNS_E2E_EXECUTABLE = $executable
|
|
$env:PYTHONIOENCODING = 'utf-8'
|
|
python -m unittest tests.e2e -v
|
|
- name: Package Windows standalone application
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$appDir = Get-ChildItem -Path 'dist-app' -Directory -Filter '*.dist' | Select-Object -First 1
|
|
if (-not $appDir) { throw 'Standalone app folder (*.dist) not found in dist' }
|
|
$zipName = "ddns-windows-${{ matrix.arch }}.zip"
|
|
if (Test-Path "dist/$zipName") { Remove-Item "dist/$zipName" -Force }
|
|
Compress-Archive -Path "$($appDir.FullName)\*" -DestinationPath "dist/$zipName"
|
|
Remove-Item 'dist-app' -Recurse -Force
|
|
- run: mv ./dist/ddns ./dist/ddns-ubuntu24.04-${{ matrix.arch }}
|
|
if: runner.os == 'Linux'
|
|
- run: mv ./dist/ddns ./dist/ddns-mac-${{ matrix.arch }}
|
|
if: runner.os == 'macOS'
|
|
- run: mv ./dist/ddns.exe ./dist/ddns-windows-${{ matrix.arch }}.exe
|
|
if: runner.os == 'Windows'
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-native-${{ runner.os }}-${{ matrix.arch }}
|
|
path: dist/ddns*
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
build-linux-binaries:
|
|
needs: preflight
|
|
strategy:
|
|
matrix:
|
|
host: [amd, arm]
|
|
libc: [musl, glibc]
|
|
runs-on: ubuntu-${{ matrix.host == 'arm' && '24.04-arm' || 'latest' }}
|
|
timeout-minutes: 10
|
|
env:
|
|
platforms: ${{ matrix.host == 'amd' && 'linux/386,linux/amd64' || matrix.libc == 'glibc' && 'linux/arm/v7,linux/arm64/v8' || 'linux/arm/v6,linux/arm/v7,linux/arm64/v8' }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: docker/setup-buildx-action@v4
|
|
- uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
file: docker/${{ matrix.libc }}.Dockerfile
|
|
platforms: ${{ env.platforms }}
|
|
push: false
|
|
tags: ddnsbin
|
|
target: export
|
|
outputs: type=local,dest=./output
|
|
build-args: |
|
|
BUILDER=ghcr.io/newfuture/nuitka-builder:${{ matrix.libc }}-master
|
|
GITHUB_REF_NAME=${{ needs.preflight.outputs.release_tag }}
|
|
- name: Test built binaries
|
|
run: |
|
|
set -ex
|
|
for f in output/*/ddns; do
|
|
platform=$(basename "$(dirname "$f")" | tr '_' '/')
|
|
tests/scripts/test-in-docker.sh "$platform" "${{ matrix.libc }}" "$f"
|
|
done
|
|
- name: Flatten output directory structure
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
for f in output/*/ddns; do
|
|
name=$(basename "$(dirname "$f")")
|
|
mv "$f" "dist/ddns-${{ matrix.libc }}-$name"
|
|
done
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-linux-${{ matrix.libc }}-${{ matrix.host }}
|
|
path: dist/*
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
build-docker:
|
|
needs: preflight
|
|
timeout-minutes: ${{ matrix.host == 'qemu' && 90 || 30 }}
|
|
strategy:
|
|
matrix:
|
|
host: [amd, arm, qemu]
|
|
runs-on: ubuntu-${{ matrix.host == 'arm' && '24.04-arm' || 'latest' }}
|
|
env:
|
|
DOCKER_IMG: ghcr.io/newfuture/ddns
|
|
platforms: ${{ matrix.host == 'amd' && 'linux/386,linux/amd64' || matrix.host == 'arm' && 'linux/arm/v6,linux/arm/v7,linux/arm64/v8' || 'linux/ppc64le,linux/riscv64,linux/s390x' }}
|
|
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- name: Patch the selected release version for Docker
|
|
run: python3 .github/patch.py docker
|
|
env:
|
|
DDNS_RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
- uses: docker/setup-qemu-action@v4
|
|
if: matrix.host == 'qemu'
|
|
with:
|
|
platforms: ${{ env.platforms }}
|
|
- uses: docker/setup-buildx-action@v4
|
|
id: buildx
|
|
- uses: docker/metadata-action@v6
|
|
id: meta
|
|
with:
|
|
images: ghcr.io/newfuture/ddns
|
|
tags: |
|
|
type=raw,value=${{ needs.preflight.outputs.release_tag }}
|
|
env:
|
|
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
|
- uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
file: docker/Dockerfile
|
|
platforms: ${{ env.platforms }}
|
|
push: false
|
|
tags: ddns:test
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
annotations: ${{ steps.meta.outputs.annotations }}
|
|
outputs: type=oci,dest=./multi-platform-image.tar
|
|
build-args: |
|
|
BUILDER=ghcr.io/newfuture/nuitka-builder:master
|
|
- name: Prepare test environment
|
|
run: mkdir -p oci-image && tar -vxf multi-platform-image.tar -C oci-image
|
|
- name: Extract platform images with skopeo
|
|
uses: addnab/docker-run-action@v3
|
|
with:
|
|
image: quay.io/skopeo/stable:latest
|
|
options: |
|
|
-v ${{ github.workspace }}/:/oci
|
|
-e PLATFORMS=${{ env.platforms }}
|
|
-e RELEASE_TAG=${{ env.RELEASE_TAG }}
|
|
-e DOCKER_IMG=${{ env.DOCKER_IMG }}
|
|
run: |
|
|
IFS="," read -ra PLATFORMS <<< "$PLATFORMS"
|
|
for platform in "${PLATFORMS[@]}"; do
|
|
tag=$(echo "$RELEASE_TAG-$platform" | tr '/' '_')
|
|
arch=$(echo "$platform" | cut -d'/' -f2)
|
|
variant=$(echo "$platform" | cut -d'/' -f3)
|
|
variantFlag=""
|
|
if [ -n "$variant" ]; then variantFlag="--override-variant $variant"; fi
|
|
skopeo copy --override-os linux --override-arch "$arch" $variantFlag \
|
|
oci:/oci/oci-image:test "docker-archive:/oci/ddns-oci-${tag}.tar:${DOCKER_IMG}:${tag}"
|
|
done
|
|
- name: Test platform images
|
|
run: |
|
|
set -e
|
|
IFS=',' read -ra PLATFORMS <<< "$platforms"
|
|
for platform in "${PLATFORMS[@]}"; do
|
|
tag=$(echo "$RELEASE_TAG-$platform" | tr '/' '_')
|
|
docker load < "ddns-oci-${tag}.tar"
|
|
docker run --platform "$platform" --rm "$DOCKER_IMG:$tag" -v
|
|
docker run --platform "$platform" --rm "$DOCKER_IMG:$tag" -h
|
|
docker run --platform "$platform" --rm -v "$(pwd):/ddns/" "$DOCKER_IMG:$tag" || test -e config.json
|
|
sudo rm -f config.json
|
|
docker run --platform "$platform" --rm -v "$(pwd):/ddns/" "$DOCKER_IMG:$tag" -c /ddns/tests/config/callback.json
|
|
docker run --platform "$platform" --rm -v "$(pwd):/ddns/" "$DOCKER_IMG:$tag" -c /ddns/tests/config/multi-provider.json
|
|
docker run --platform "$platform" --rm -v "$(pwd):/ddns/" "$DOCKER_IMG:$tag" -c /ddns/tests/config/debug.json -c /ddns/tests/config/noip.json
|
|
docker run --platform "$platform" --rm -v "$(pwd):/ddns/" "$DOCKER_IMG:$tag" -c https://ddns.newfuture.cc/tests/config/debug.json
|
|
docker run --platform "$platform" --rm -v "$(pwd):/ddns/" "$DOCKER_IMG:$tag" -c /ddns/tests/config/he-proxies.json
|
|
if [ "$platform" = "linux/amd64" ]; then
|
|
sh tests/scripts/test-docker-zombies.sh "$DOCKER_IMG:$tag" "$platform"
|
|
fi
|
|
done
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-docker-${{ matrix.host }}
|
|
path: ddns-oci-*.tar
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
compression-level: 0
|
|
|
|
validate-release:
|
|
needs: [preflight, build-pypi, build-docker, build-native-binaries, build-linux-binaries]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 3
|
|
permissions:
|
|
# Draft releases are visible only to tokens with push access.
|
|
contents: write
|
|
env:
|
|
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
TARGET_SHA: ${{ github.sha }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
steps:
|
|
- name: Reject conflicting release state
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
refs_json="$(gh api "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$RELEASE_TAG")"
|
|
ref_json="$(jq -c --arg ref "refs/tags/$RELEASE_TAG" '.[] | select(.ref == $ref)' <<< "$refs_json")"
|
|
tag_sha=""
|
|
if [ -n "$ref_json" ]; then
|
|
object_type="$(jq -r '.object.type' <<< "$ref_json")"
|
|
tag_sha="$(jq -r '.object.sha' <<< "$ref_json")"
|
|
if [ "$object_type" = "tag" ]; then
|
|
tag_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_sha" --jq '.object.sha')"
|
|
fi
|
|
test "$tag_sha" = "$TARGET_SHA" || {
|
|
echo "Tag $RELEASE_TAG already targets $tag_sha, not $TARGET_SHA" >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
release_matches="$(
|
|
gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/releases?per_page=100" |
|
|
jq -c --arg tag "$RELEASE_TAG" '[.[][] | select(.tag_name == $tag)]'
|
|
)"
|
|
release_count="$(jq 'length' <<< "$release_matches")"
|
|
test "$release_count" -le 1 || {
|
|
echo "Multiple releases unexpectedly use tag $RELEASE_TAG." >&2
|
|
exit 1
|
|
}
|
|
if [ "$release_count" -eq 1 ]; then
|
|
release_json="$(jq -c '.[0]' <<< "$release_matches")"
|
|
test "$(jq -r '.draft' <<< "$release_json")" = "true" || {
|
|
echo "Release $RELEASE_TAG is already published." >&2
|
|
exit 1
|
|
}
|
|
test "$(jq -r '.tag_name' <<< "$release_json")" = "$RELEASE_TAG" || {
|
|
echo "Draft release tag does not match $RELEASE_TAG." >&2
|
|
exit 1
|
|
}
|
|
test -n "$tag_sha" || {
|
|
echo "Draft release $RELEASE_TAG has no corresponding tag." >&2
|
|
exit 1
|
|
}
|
|
expected_prerelease=false
|
|
case "$RELEASE_TAG" in
|
|
*-alpha*|*-beta*|*-rc*) expected_prerelease=true ;;
|
|
esac
|
|
test "$(jq -r '.prerelease' <<< "$release_json")" = "$expected_prerelease" || {
|
|
echo "Draft prerelease state does not match $RELEASE_TAG." >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
publish-pypi:
|
|
needs: [preflight, build-pypi, build-docker, build-native-binaries, build-linux-binaries, validate-release]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
environment:
|
|
name: publish
|
|
url: https://pypi.org/project/ddns/
|
|
permissions:
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
name: release-pypi
|
|
path: dist
|
|
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
with:
|
|
packages-dir: dist
|
|
print-hash: true
|
|
|
|
publish-docker:
|
|
needs: [preflight, build-pypi, build-docker, build-native-binaries, build-linux-binaries, validate-release]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 150
|
|
environment:
|
|
name: publish
|
|
url: https://hub.docker.com/r/newfuture/ddns
|
|
permissions:
|
|
packages: write
|
|
env:
|
|
DOCKER_IMG: ghcr.io/newfuture/ddns
|
|
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x
|
|
steps:
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: release-docker-*
|
|
path: promotion
|
|
merge-multiple: true
|
|
- uses: docker/login-action@v4
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
- uses: docker/login-action@v4
|
|
with:
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
- uses: docker/setup-buildx-action@v4
|
|
- uses: docker/metadata-action@v6
|
|
id: meta
|
|
with:
|
|
images: |
|
|
ghcr.io/newfuture/ddns
|
|
newfuture/ddns
|
|
flavor: |
|
|
latest=${{ !(contains(needs.preflight.outputs.release_tag, 'a') || contains(needs.preflight.outputs.release_tag, 'b') || contains(needs.preflight.outputs.release_tag, 'c')) }}
|
|
tags: |
|
|
type=raw,value=${{ needs.preflight.outputs.release_tag }}
|
|
type=raw,value=next,enable=${{ !contains(needs.preflight.outputs.release_tag, 'alpha') }}
|
|
env:
|
|
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
|
- name: Push tested platform images and registry-local indexes
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
archives=(promotion/ddns-oci-*.tar)
|
|
test "${#archives[@]}" -eq 8
|
|
for archive in "${archives[@]}"; do docker load -i "$archive"; done
|
|
|
|
IFS=',' read -ra PLATFORMS <<< "$platforms"
|
|
ghcr_sources=()
|
|
dockerhub_sources=()
|
|
for platform in "${PLATFORMS[@]}"; do
|
|
platform_tag=$(printf '%s-%s' "$RELEASE_TAG" "$platform" | tr '/' '_')
|
|
ghcr_source="$DOCKER_IMG:$platform_tag"
|
|
dockerhub_source="newfuture/ddns:$platform_tag"
|
|
docker image inspect "$ghcr_source" >/dev/null
|
|
docker tag "$ghcr_source" "$dockerhub_source"
|
|
docker push "$ghcr_source"
|
|
docker push "$dockerhub_source"
|
|
ghcr_sources+=("$ghcr_source")
|
|
dockerhub_sources+=("$dockerhub_source")
|
|
done
|
|
|
|
ghcr_tags=()
|
|
dockerhub_tags=()
|
|
while IFS= read -r tag; do
|
|
case "$tag" in
|
|
ghcr.io/newfuture/ddns:*) ghcr_tags+=(--tag "$tag") ;;
|
|
newfuture/ddns:*) dockerhub_tags+=(--tag "$tag") ;;
|
|
*) echo "Unexpected metadata tag: $tag" >&2; exit 1 ;;
|
|
esac
|
|
done <<< "${{ steps.meta.outputs.tags }}"
|
|
annotations=()
|
|
while IFS= read -r annotation; do
|
|
test -n "$annotation" && annotations+=(--annotation "$annotation")
|
|
done <<< "${{ steps.meta.outputs.annotations }}"
|
|
docker buildx imagetools create "${ghcr_tags[@]}" "${annotations[@]}" "${ghcr_sources[@]}"
|
|
docker buildx imagetools create "${dockerhub_tags[@]}" "${annotations[@]}" "${dockerhub_sources[@]}"
|
|
|
|
publish-github:
|
|
needs: [preflight, build-pypi, build-docker, build-native-binaries, build-linux-binaries, validate-release]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
environment:
|
|
name: publish
|
|
url: https://github.com/NewFuture/DDNS/releases/tag/${{ needs.preflight.outputs.release_tag }}
|
|
permissions:
|
|
contents: write
|
|
env:
|
|
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
|
|
TARGET_SHA: ${{ github.sha }}
|
|
steps:
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: release-native-*
|
|
path: assets
|
|
merge-multiple: true
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
name: release-document
|
|
path: release-document
|
|
- uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: release-linux-*
|
|
path: assets
|
|
merge-multiple: true
|
|
- name: Create or safely resume the immutable release draft
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GH_REPO: ${{ github.repository }}
|
|
run: |
|
|
set -euo pipefail
|
|
test -n "$(find assets -maxdepth 1 -type f -print -quit)"
|
|
release_document="$(find release-document -type f -name release.md -print -quit)"
|
|
test -n "$release_document"
|
|
|
|
resolve_tag_sha() {
|
|
local ref object_type object_sha
|
|
ref="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" 2>/dev/null)" || return 1
|
|
object_type="$(jq -r '.object.type' <<< "$ref")"
|
|
object_sha="$(jq -r '.object.sha' <<< "$ref")"
|
|
if [ "$object_type" = "tag" ]; then
|
|
object_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" --jq '.object.sha')"
|
|
fi
|
|
printf '%s\n' "$object_sha"
|
|
}
|
|
if tag_sha="$(resolve_tag_sha)"; then
|
|
test "$tag_sha" = "$TARGET_SHA" || {
|
|
echo "Tag $RELEASE_TAG already targets $tag_sha, not $TARGET_SHA" >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
is_prerelease=false
|
|
case "$RELEASE_TAG" in
|
|
*-alpha*|*-beta*|*-rc*) is_prerelease=true ;;
|
|
esac
|
|
|
|
release_json=""
|
|
if release_json="$(gh release view "$RELEASE_TAG" --json isDraft,isPrerelease,assets 2>/dev/null)"; then
|
|
if [ "$(jq -r '.isDraft' <<< "$release_json")" != "true" ]; then
|
|
echo "Release $RELEASE_TAG is already published; immutable releases are not modified by this workflow." >&2
|
|
exit 1
|
|
fi
|
|
if [ "$(jq -r '.isPrerelease' <<< "$release_json")" != "$is_prerelease" ]; then
|
|
echo "Draft $RELEASE_TAG has an unexpected prerelease setting." >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
if ! resolve_tag_sha >/dev/null; then
|
|
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
|
|
-f ref="refs/tags/$RELEASE_TAG" \
|
|
-f sha="$TARGET_SHA" >/dev/null
|
|
fi
|
|
prerelease_args=()
|
|
if [ "$is_prerelease" = "true" ]; then
|
|
prerelease_args=(--prerelease)
|
|
fi
|
|
gh release create "$RELEASE_TAG" --draft --verify-tag --generate-notes --title "$RELEASE_TAG" "${prerelease_args[@]}"
|
|
fi
|
|
|
|
require_draft() {
|
|
release_state="$(gh release view "$RELEASE_TAG" --json isDraft,isPrerelease)"
|
|
if [ "$(jq -r '.isDraft' <<< "$release_state")" != "true" ] ||
|
|
[ "$(jq -r '.isPrerelease' <<< "$release_state")" != "$is_prerelease" ]; then
|
|
echo "Release $RELEASE_TAG no longer has the expected draft state; refusing to modify it." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
tag_sha="$(resolve_tag_sha)"
|
|
test "$tag_sha" = "$TARGET_SHA" || {
|
|
echo "Release tag $RELEASE_TAG does not resolve to $TARGET_SHA" >&2
|
|
exit 1
|
|
}
|
|
|
|
release_json="$(gh release view "$RELEASE_TAG" --json isDraft,isPrerelease,assets)"
|
|
gh release view "$RELEASE_TAG" --json body -q '.body' > changelog.md
|
|
expected_assets="$(
|
|
find assets -maxdepth 1 -type f -printf '%f\n'
|
|
printf '%s\n' changelog.md
|
|
)"
|
|
while IFS= read -r existing_asset; do
|
|
if ! grep -Fxq -- "$existing_asset" <<< "$expected_assets"; then
|
|
echo "Draft $RELEASE_TAG contains unexpected asset $existing_asset." >&2
|
|
exit 1
|
|
fi
|
|
done < <(jq -r '.assets[].name' <<< "$release_json")
|
|
|
|
if ! grep -Fq "## 各版本一览表" changelog.md; then
|
|
cat "$release_document" >> changelog.md
|
|
require_draft
|
|
gh release edit "$RELEASE_TAG" --notes-file changelog.md
|
|
fi
|
|
require_draft
|
|
gh release upload "$RELEASE_TAG" assets/* changelog.md --clobber
|
|
require_draft
|
|
tag_sha="$(resolve_tag_sha)"
|
|
test "$tag_sha" = "$TARGET_SHA" || {
|
|
echo "Release tag $RELEASE_TAG changed before publication" >&2
|
|
exit 1
|
|
}
|
|
# This is intentionally the final release-mutating command.
|
|
gh release edit "$RELEASE_TAG" --draft=false
|