Files
dependabot[bot] 9f6cee7a5e build(deps): bump Nuitka/Nuitka-Action from 1.4 to 1.5 (#757)
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>
2026-10-20 14:13:43 +08:00

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