| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- # This workflow will install Python dependencies, run tests and lint with a single version of Python
- # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
- name: Build
- on:
- push:
- branches: ["master", "main"]
- pull_request:
- branches: ["master", "main", "abc"]
- permissions:
- contents: read
- pull-requests: read
- # This allows a subsequently queued workflow run to interrupt previous runs
- concurrency:
- group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
- cancel-in-progress: true
- env:
- DOCKER_IMG: "ghcr.io/newfuture/ddns"
- jobs:
- lint:
- runs-on: ubuntu-latest
- timeout-minutes: 3
- steps:
- - uses: actions/checkout@v4
- - uses: astral-sh/ruff-action@v3
- with:
- src: "."
- args: "check --output-format=github"
- python:
- strategy:
- fail-fast: false
- matrix:
- version: [ "2.7", "3", "3.8", "3.10", "3.12", "3.13", "3.14-dev"]
- env:
- PY: python${{ matrix.version == '3.14-dev' && '3.14' || matrix.version }}
- runs-on: ubuntu-22.04
- timeout-minutes: 5
- steps:
- - uses: actions/checkout@v4
- - run: sudo apt-get update && sudo apt-get install -y python${{ matrix.version }}
- if: matrix.version == '2.7' || matrix.version == '3'
- - uses: actions/setup-python@v5
- if: matrix.version != '2.7' && matrix.version != '3'
- with:
- python-version: ${{ matrix.version }}
- - name: test help command
- run: ${{env.PY}} run.py -h
- - name: test config generation
- run: ${{env.PY}} run.py || test -f config.json
- - name: test version
- run: ${{env.PY}} run.py --version
- - name: test run module
- run: ${{env.PY}} -m "ddns" -h
-
- - name: install mock for Python 2.7
- if: ${{ matrix.version == '2.7' }}
- run: |
- curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py
- sudo ${{env.PY}} get-pip.py
- ${{env.PY}} -m pip install mock==3.0.5
- working-directory: /tmp
- - run: rm config.json -f
- - name: run unit tests
- run: ${{env.PY}} -m unittest discover tests -v
- env:
- PYTHONIOENCODING: utf-8
- - run: ${{env.PY}} run.py -c tests/config/callback.json
- - run: ${{env.PY}} -m ddns -c tests/config/multi-provider.json
- - run: ${{env.PY}} -m ddns -c tests/config/debug.json -c tests/config/noip.json
- - run: ${{env.PY}} -m ddns -c https://ddns.newfuture.cc/tests/config/debug.json
- - run: ${{env.PY}} -m ddns -c tests/config/he-proxies.json --debug
- - name: Test task management functionality
- run: tests/scripts/test-task-systemd.sh "$(which ${{env.PY}}) -m ddns"
- - name: test patch
- if: ${{ matrix.version != '2.7' }}
- run: python3 .github/patch.py
- - name: test help
- if: ${{ matrix.version != '2.7' }}
- run: python3 run.py -h
- - name: test run
- if: ${{ matrix.version != '2.7' }}
- run: python3 run.py || test -f config.json
- - name: test version
- run: ${{env.PY}} run.py --version
- - name: test run module
- if: ${{ matrix.version != '2.7' }}
- run: ${{env.PY}} -m "ddns" -h
- pypi:
- runs-on: ubuntu-latest
- timeout-minutes: 5
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
- with:
- python-version: "3.x"
- - name: Install dependencies
- run: pip install build
- - run: python3 .github/patch.py version
- - name: Replace url in Readme
- run: sed -i'' -E 's#([("'\''`])(/doc/[^)"'\''`]+)\.md([)"'\''`])#\1https://ddns.newfuture.cc\2.html\3#g; s#([("'\''`])/doc/#\1https://ddns.newfuture.cc/doc/#g' README.md
- - 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
-
- - 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 tarball file found"; exit 1; }
- python3 -m pip install --force-reinstall "$tarball_file"
- ddns --version
- python3 -m ddns --help
-
- - name: run unit tests
- run: python3 -m unittest -v
- - name: Test task management functionality
- run: tests/scripts/test-task-systemd.sh "python3 -m ddns"
- - run: python3 -m ddns -c tests/config/callback.json
- - run: python3 -m ddns -c tests/config/multi-provider.json
- - run: python3 -m ddns -c tests/config/debug.json -c tests/config/noip.json
- - run: python3 -m ddns -c https://ddns.newfuture.cc/tests/config/debug.json
- - run: python3 -m ddns -c tests/config/he-proxies.json --debug
- - uses: actions/upload-artifact@v4
- with:
- name: pypi
- path: dist/
- retention-days: ${{ github.event_name == 'push' && 14 || 3 }}
- nuitka:
- needs: [ python, lint ]
- strategy:
- 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-13
- arch: x64
- - os: macos-latest
- arch: arm64
- runs-on: ${{ matrix.os }}
- env:
- OS_NAME: ${{ contains(matrix.os,'ubuntu') && 'ubuntu' || contains(matrix.os, 'mac') && 'mac' || 'windows' }}
- timeout-minutes: ${{ matrix.arch == 'x86' && 20 || contains(matrix.os, 'windows') && 12 || 8 }}
- steps:
- - uses: actions/checkout@v4
- - name: Set up Python 3.12
- uses: actions/setup-python@v5
- with:
- python-version: 3.12
- architecture: ${{ matrix.arch }}
- - run: python3 .github/patch.py
- - name: Set up on Linux
- if: runner.os == 'Linux'
- run: sudo apt-get install -y --no-install-recommends patchelf
- - name: Set up on macOS
- if: runner.os == 'macOS'
- run: python3 -m pip install imageio
-
- - run: python3 ./run.py -h
- - name: run unit tests
- run: python3 -m unittest -v
- - name: test callback config
- run: python3 -m ddns -c tests/config/callback.json
- - name: Build Executable
- uses: Nuitka/[email protected]
- with:
- nuitka-version: main
- script-name: run.py
- mode: onefile
- output-dir: dist
- lto: yes
- file-description: "DDNS客户端[测试版 Alpha]"
- windows-console-mode: ${{ runner.os == 'Windows' && 'attach' || '' }}
- windows-icon-from-ico: ${{ runner.os == 'Windows' && 'favicon.ico' || '' }}
- linux-icon: ${{ runner.os == 'Linux' && 'doc/img/ddns.svg' || '' }}
- static-libpython: ${{ runner.os == 'Linux' && 'yes' || 'auto' }}
- macos-app-name: ${{ runner.os == 'macOS' && 'DDNS' || '' }}
- macos-app-icon: ${{ runner.os == 'macOS' && 'doc/img/ddns.png' || '' }}
- - run: ./dist/ddns -v
- - run: ./dist/ddns -h
- - run: ./dist/ddns || test -f config.json
- - 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"
- - run: ./dist/ddns -c tests/config/multi-provider.json
- - run: ./dist/ddns -c tests/config/debug.json -c tests/config/noip.json
- - run: ./dist/ddns -c tests/config/he-proxies.json --debug
- - run: ./dist/ddns -c https://ddns.newfuture.cc/tests/config/debug.json
- - name: Build Windows App (standalone)
- if: runner.os == 'Windows'
- uses: Nuitka/[email protected]
- with:
- nuitka-version: main
- script-name: run.py
- mode: standalone
- output-dir: dist-app
- lto: yes
- file-description: "DDNS客户端[测试版 Alpha]"
- windows-console-mode: attach
- windows-icon-from-ico: favicon.ico
- - name: Package Windows app as ddns.zip
- 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' }
- if (Test-Path 'dist/ddns.zip') { Remove-Item 'dist/ddns.zip' -Force }
- # Package the contents of the .dist folder directly, not the folder itself
- Compress-Archive -Path "$($appDir.FullName)\*" -DestinationPath 'dist/ddns.zip'
- # Clean up temporary output directory
- Remove-Item 'dist-app' -Recurse -Force
- # Upload build result
- - name: Upload Artifacts
- uses: actions/upload-artifact@v4
- with:
- name: ddns-${{ env.OS_NAME }}-${{ matrix.arch }}
- if-no-files-found: error
- path: |
- dist/*.exe
- dist/*.bin
- dist/*.app
- dist/ddns
- dist/*.zip
- retention-days: ${{ github.event_name == 'push' && 30 || 3 }}
- linux-binary:
- needs: [ python ]
- strategy:
- matrix:
- host: [ amd, arm ]
- libc: [ musl, glibc ]
- runs-on: ubuntu-${{ matrix.host == 'arm' && '24.04-arm' || 'latest' }}
- env:
- # glibc: 基于 debian linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8
- # musl: 基于 alpine linux/386,linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
- 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' }}
- timeout-minutes: 8
- steps:
- - uses: actions/checkout@v4
- - uses: docker/setup-buildx-action@v3
- - uses: docker/build-push-action@v6
- 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=${{ github.ref_name }}
- # 测试构建的二进制文件
- - 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
- - name: Upload build result
- uses: actions/upload-artifact@v4
- with:
- name: ddns-${{ matrix.libc }}-${{ matrix.host}}
- path: dist/*
- if-no-files-found: error
- retention-days: ${{ github.event_name == 'push' && 14 || 3 }}
-
- docker:
- needs: [ python ]
- timeout-minutes: ${{ matrix.host == 'qemu' && 60 || 30 }}
- strategy:
- matrix:
- host: ['amd','arm','qemu']
- event:
- - ${{github.event_name}}
- exclude:
- # PR 时不构建 QEMU 镜像
- - host: qemu
- event: pull_request
- runs-on: ubuntu-${{ matrix.host == 'arm' && '24.04-arm' || 'latest' }}
- env:
- 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'
- }}
- steps:
- - uses: actions/checkout@v4
- - run: python3 .github/patch.py docker
- env:
- GITHUB_REF_NAME: ${{ github.ref_name }}
- - uses: docker/setup-qemu-action@v3 # 仅仅在需要时启用 QEMU 支持
- if: matrix.host == 'qemu'
- with:
- platforms: ${{ env.platforms }}
- - uses: docker/setup-buildx-action@v3
- - uses: docker/build-push-action@v6
- with:
- context: .
- file: docker/Dockerfile
- platforms: ${{ env.platforms }}
- push: false
- tags: ddns:test
- outputs: type=oci,dest=./multi-platform-image.tar
- build-args: |
- BUILDER=ghcr.io/newfuture/nuitka-builder:master
- GITHUB_REF_NAME=${{ github.ref_name }}
- # 准备测试环境
- - name: Prepare test environment
- run: mkdir -p oci-image && tar -vxf multi-platform-image.tar -C oci-image
- # 使用 skopeo 批量提取所有平台镜像,正确处理变体
- - 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 }}
- run: |
- IFS="," read -ra PLATFORMS <<< "$PLATFORMS"
- for platform in "${PLATFORMS[@]}"; do
- echo "=== Extracting image for: $platform ==="
- tag=$(echo "${{ github.ref_name || 'test' }}-$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:${{ env.DOCKER_IMG }}:${tag}
- done
- # 测试各个平台的镜像
- - name: Test platform images
- run: |
- set -e
- # 解析平台列表
- IFS=',' read -ra PLATFORMS <<< "${{ env.platforms }}"
-
- # 测试每个平台的镜像
- for platform in "${PLATFORMS[@]}"; do
- echo "=== Testing platform: $platform ==="
- # 将平台标识符转换为有效的文件名
- tag=$(echo "${{ github.ref_name || 'test' }}-$platform" | tr '/' '_')
- echo "Loading image for $platform..."
- docker load < ddns-oci-${tag}.tar
- echo "Running test..."
- docker run --platform $platform --rm ${{ env.DOCKER_IMG }}:$tag -v
- docker run --platform $platform --rm ${{ env.DOCKER_IMG }}:$tag -h
- docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag || test -e "config.json"
- sudo rm -f config.json
- echo "Testing with config files..."
- docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/callback.json
- docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/multi-provider.json
- docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/debug.json -c /ddns/tests/config/noip.json
- docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c https://ddns.newfuture.cc/tests/config/debug.json
- docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/he-proxies.json
- done
-
- # 上传测试结果和镜像
- - name: Upload images
- uses: actions/upload-artifact@v4
- with:
- name: docker-${{ matrix.host }}
- path: ddns-oci-*.tar
- if-no-files-found: error
- retention-days: ${{ github.event_name == 'push' && 7 || 3 }}
- preview-pypi:
- runs-on: ubuntu-latest
- if: github.event_name == 'push'
- needs: [lint, pypi, python]
- timeout-minutes: 3
- environment:
- name: preview
- url: https://test.pypi.org/project/ddns/
- permissions:
- id-token: write
- steps:
- - uses: actions/download-artifact@v4
- with:
- name: pypi
- path: dist
- - uses: pypa/gh-action-pypi-publish@release/v1
- with:
- repository-url: https://test.pypi.org/legacy/
- print-hash: true
- verbose: true
- preview-docker:
- if: github.event_name == 'push'
- needs: [lint, docker, python]
- runs-on: ubuntu-latest
- timeout-minutes: 5
- environment:
- name: preview
- url: https://github.com/NewFuture/DDNS/pkgs/container/ddns/?tag=master
- permissions:
- packages: write
- steps:
- - uses: actions/download-artifact@v4
- with:
- name: docker-amd
- - uses: actions/download-artifact@v4
- with:
- name: docker-arm
- - uses: actions/download-artifact@v4
- with:
- name: docker-qemu
- - uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- - uses: docker/setup-buildx-action@v3
- - name: docker load and push
- run: |
- set -ex
- for f in $(ls ddns-oci-*.tar); do
- docker load -i "$f"
- docker push ${{ env.DOCKER_IMG }}:$(basename "$f" | sed 's/ddns-oci-//;s/.tar//')
- done
- - name: merge and push images
- run: |
- set -ex
- docker buildx imagetools create \
- -t ${{ env.DOCKER_IMG }}:${{ github.ref_name }} \
- -t newfuture/ddns:${{ github.ref_name }} \
- ${{ github.ref_name=='master' && '-t $DOCKER_IMG:edge' }} \
- ${{ github.ref_name=='master' && '-t newfuture/ddns:edge' }} \
- $(docker images --format "{{.Repository}}:{{.Tag}}" ${{ env.DOCKER_IMG }}:*) \
- --annotation "index,manifest:org.opencontainers.image.url=https://ddns.newfuture.cc" \
- --annotation "index,manifest:org.opencontainers.image.description=DDNS docker ${{ github.ref_name }} CI build (unstable version),集成测试(非稳定版)" \
- --annotation "index,manifest:org.opencontainers.image.authors=NewFuture,NN708"
|