build.yml 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. # This workflow will install Python dependencies, run tests and lint with a single version of Python
  2. # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
  3. name: Build
  4. on:
  5. push:
  6. branches: ["master", "main"]
  7. pull_request:
  8. branches: ["master", "main", "abc"]
  9. permissions:
  10. contents: read
  11. pull-requests: read
  12. # This allows a subsequently queued workflow run to interrupt previous runs
  13. concurrency:
  14. group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
  15. cancel-in-progress: true
  16. env:
  17. DOCKER_IMG: "ghcr.io/newfuture/ddns"
  18. jobs:
  19. lint:
  20. runs-on: ubuntu-latest
  21. timeout-minutes: 3
  22. steps:
  23. - uses: actions/checkout@v4
  24. - uses: astral-sh/ruff-action@v3
  25. with:
  26. src: "."
  27. args: "check --output-format=github"
  28. python:
  29. strategy:
  30. fail-fast: false
  31. matrix:
  32. version: [ "2.7", "3", "3.8", "3.10", "3.12", "3.13", "3.14-dev"]
  33. env:
  34. PY: python${{ matrix.version == '3.14-dev' && '3.14' || matrix.version }}
  35. runs-on: ubuntu-22.04
  36. timeout-minutes: 5
  37. steps:
  38. - uses: actions/checkout@v4
  39. - run: sudo apt-get update && sudo apt-get install -y python${{ matrix.version }}
  40. if: matrix.version == '2.7' || matrix.version == '3'
  41. - uses: actions/setup-python@v5
  42. if: matrix.version != '2.7' && matrix.version != '3'
  43. with:
  44. python-version: ${{ matrix.version }}
  45. - name: test help command
  46. run: ${{env.PY}} run.py -h
  47. - name: test config generation
  48. run: ${{env.PY}} run.py || test -f config.json
  49. - name: test version
  50. run: ${{env.PY}} run.py --version
  51. - name: test run module
  52. run: ${{env.PY}} -m "ddns" -h
  53. - name: install mock for Python 2.7
  54. if: ${{ matrix.version == '2.7' }}
  55. run: |
  56. curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py
  57. sudo ${{env.PY}} get-pip.py
  58. ${{env.PY}} -m pip install mock==3.0.5
  59. working-directory: /tmp
  60. - run: rm config.json -f
  61. - name: run unit tests
  62. run: ${{env.PY}} -m unittest discover tests -v
  63. env:
  64. PYTHONIOENCODING: utf-8
  65. - run: ${{env.PY}} run.py -c tests/config/callback.json
  66. - run: ${{env.PY}} -m ddns -c tests/config/multi-provider.json
  67. - run: ${{env.PY}} -m ddns -c tests/config/debug.json -c tests/config/noip.json
  68. - run: ${{env.PY}} -m ddns -c https://ddns.newfuture.cc/tests/config/debug.json
  69. - run: ${{env.PY}} -m ddns -c tests/config/he-proxies.json --debug
  70. - name: Test task management functionality
  71. run: tests/scripts/test-task-systemd.sh "$(which ${{env.PY}}) -m ddns"
  72. - name: test patch
  73. if: ${{ matrix.version != '2.7' }}
  74. run: python3 .github/patch.py
  75. - name: test help
  76. if: ${{ matrix.version != '2.7' }}
  77. run: python3 run.py -h
  78. - name: test run
  79. if: ${{ matrix.version != '2.7' }}
  80. run: python3 run.py || test -f config.json
  81. - name: test version
  82. run: ${{env.PY}} run.py --version
  83. - name: test run module
  84. if: ${{ matrix.version != '2.7' }}
  85. run: ${{env.PY}} -m "ddns" -h
  86. pypi:
  87. runs-on: ubuntu-latest
  88. timeout-minutes: 5
  89. steps:
  90. - uses: actions/checkout@v4
  91. - uses: actions/setup-python@v5
  92. with:
  93. python-version: "3.x"
  94. - name: Install dependencies
  95. run: pip install build
  96. - run: python3 .github/patch.py version
  97. - name: Replace url in Readme
  98. 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
  99. - name: Build package
  100. run: python -m build --sdist --wheel --outdir dist/
  101. - name: Test pip install from wheel
  102. run: |
  103. wheel_file=$(find dist -name "*.whl" -type f)
  104. test -n "$wheel_file" || { echo "No wheel file found"; exit 1; }
  105. python3 -m pip install --force-reinstall "$wheel_file"
  106. ddns --version
  107. ddns --help
  108. ddns --new-config /tmp/test-pip-config.json
  109. test -f /tmp/test-pip-config.json
  110. python3 -m ddns --version
  111. - name: Test pip install from source distribution
  112. run: |
  113. python3 -m pip uninstall -y ddns
  114. tarball_file=$(find dist -name "*.tar.gz" -type f)
  115. test -n "$tarball_file" || { echo "No tarball file found"; exit 1; }
  116. python3 -m pip install --force-reinstall "$tarball_file"
  117. ddns --version
  118. python3 -m ddns --help
  119. - name: run unit tests
  120. run: python3 -m unittest -v
  121. - name: Test task management functionality
  122. run: tests/scripts/test-task-systemd.sh "python3 -m ddns"
  123. - run: python3 -m ddns -c tests/config/callback.json
  124. - run: python3 -m ddns -c tests/config/multi-provider.json
  125. - run: python3 -m ddns -c tests/config/debug.json -c tests/config/noip.json
  126. - run: python3 -m ddns -c https://ddns.newfuture.cc/tests/config/debug.json
  127. - run: python3 -m ddns -c tests/config/he-proxies.json --debug
  128. - uses: actions/upload-artifact@v4
  129. with:
  130. name: pypi
  131. path: dist/
  132. retention-days: ${{ github.event_name == 'push' && 14 || 3 }}
  133. nuitka:
  134. needs: [ python, lint ]
  135. strategy:
  136. matrix:
  137. include:
  138. - os: windows-latest
  139. arch: x64
  140. - os: windows-latest
  141. arch: x86
  142. - os: windows-11-arm
  143. arch: arm64
  144. - os: ubuntu-latest
  145. arch: x64
  146. - os: ubuntu-24.04-arm
  147. arch: arm64
  148. - os: macos-13
  149. arch: x64
  150. - os: macos-latest
  151. arch: arm64
  152. runs-on: ${{ matrix.os }}
  153. env:
  154. OS_NAME: ${{ contains(matrix.os,'ubuntu') && 'ubuntu' || contains(matrix.os, 'mac') && 'mac' || 'windows' }}
  155. timeout-minutes: ${{ matrix.arch == 'x86' && 20 || contains(matrix.os, 'windows') && 12 || 8 }}
  156. steps:
  157. - uses: actions/checkout@v4
  158. - name: Set up Python 3.12
  159. uses: actions/setup-python@v5
  160. with:
  161. python-version: 3.12
  162. architecture: ${{ matrix.arch }}
  163. - run: python3 .github/patch.py
  164. - name: Set up on Linux
  165. if: runner.os == 'Linux'
  166. run: sudo apt-get install -y --no-install-recommends patchelf
  167. - name: Set up on macOS
  168. if: runner.os == 'macOS'
  169. run: python3 -m pip install imageio
  170. - run: python3 ./run.py -h
  171. - name: run unit tests
  172. run: python3 -m unittest -v
  173. - name: test callback config
  174. run: python3 -m ddns -c tests/config/callback.json
  175. - name: Build Executable
  176. uses: Nuitka/[email protected]
  177. with:
  178. nuitka-version: main
  179. script-name: run.py
  180. mode: onefile
  181. output-dir: dist
  182. lto: yes
  183. file-description: "DDNS客户端[测试版 Alpha]"
  184. windows-console-mode: ${{ runner.os == 'Windows' && 'attach' || '' }}
  185. windows-icon-from-ico: ${{ runner.os == 'Windows' && 'favicon.ico' || '' }}
  186. linux-icon: ${{ runner.os == 'Linux' && 'doc/img/ddns.svg' || '' }}
  187. static-libpython: ${{ runner.os == 'Linux' && 'yes' || 'auto' }}
  188. macos-app-name: ${{ runner.os == 'macOS' && 'DDNS' || '' }}
  189. macos-app-icon: ${{ runner.os == 'macOS' && 'doc/img/ddns.png' || '' }}
  190. - run: ./dist/ddns -v
  191. - run: ./dist/ddns -h
  192. - run: ./dist/ddns || test -f config.json
  193. - run: ./dist/ddns task --status
  194. - name: Test Windows schtasks task
  195. if: runner.os == 'Windows'
  196. run: tests/scripts/test-task-windows.bat .\\dist\\ddns.exe
  197. - name: Test Linux systemd service
  198. if: runner.os == 'Linux'
  199. run: tests/scripts/test-task-systemd.sh "./dist/ddns"
  200. - name: Test macOS launchd service
  201. if: runner.os == 'macOS'
  202. run: tests/scripts/test-task-macos.sh "./dist/ddns"
  203. - run: ./dist/ddns -c tests/config/multi-provider.json
  204. - run: ./dist/ddns -c tests/config/debug.json -c tests/config/noip.json
  205. - run: ./dist/ddns -c tests/config/he-proxies.json --debug
  206. - run: ./dist/ddns -c https://ddns.newfuture.cc/tests/config/debug.json
  207. - name: Build Windows App (standalone)
  208. if: runner.os == 'Windows'
  209. uses: Nuitka/[email protected]
  210. with:
  211. nuitka-version: main
  212. script-name: run.py
  213. mode: standalone
  214. output-dir: dist-app
  215. lto: yes
  216. file-description: "DDNS客户端[测试版 Alpha]"
  217. windows-console-mode: attach
  218. windows-icon-from-ico: favicon.ico
  219. - name: Package Windows app as ddns.zip
  220. if: runner.os == 'Windows'
  221. shell: pwsh
  222. run: |
  223. $ErrorActionPreference = 'Stop'
  224. $appDir = Get-ChildItem -Path 'dist-app' -Directory -Filter '*.dist' | Select-Object -First 1
  225. if (-not $appDir) { throw 'Standalone app folder (*.dist) not found in dist' }
  226. if (Test-Path 'dist/ddns.zip') { Remove-Item 'dist/ddns.zip' -Force }
  227. # Package the contents of the .dist folder directly, not the folder itself
  228. Compress-Archive -Path "$($appDir.FullName)\*" -DestinationPath 'dist/ddns.zip'
  229. # Clean up temporary output directory
  230. Remove-Item 'dist-app' -Recurse -Force
  231. # Upload build result
  232. - name: Upload Artifacts
  233. uses: actions/upload-artifact@v4
  234. with:
  235. name: ddns-${{ env.OS_NAME }}-${{ matrix.arch }}
  236. if-no-files-found: error
  237. path: |
  238. dist/*.exe
  239. dist/*.bin
  240. dist/*.app
  241. dist/ddns
  242. dist/*.zip
  243. retention-days: ${{ github.event_name == 'push' && 30 || 3 }}
  244. linux-binary:
  245. needs: [ python ]
  246. strategy:
  247. matrix:
  248. host: [ amd, arm ]
  249. libc: [ musl, glibc ]
  250. runs-on: ubuntu-${{ matrix.host == 'arm' && '24.04-arm' || 'latest' }}
  251. env:
  252. # glibc: 基于 debian linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8
  253. # musl: 基于 alpine linux/386,linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
  254. 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' }}
  255. timeout-minutes: 8
  256. steps:
  257. - uses: actions/checkout@v4
  258. - uses: docker/setup-buildx-action@v3
  259. - uses: docker/build-push-action@v6
  260. with:
  261. context: .
  262. file: docker/${{ matrix.libc }}.Dockerfile
  263. platforms: ${{ env.platforms }}
  264. push: false
  265. tags: ddnsbin
  266. target: export
  267. outputs: type=local,dest=./output
  268. build-args: |
  269. BUILDER=ghcr.io/newfuture/nuitka-builder:${{matrix.libc}}-master
  270. GITHUB_REF_NAME=${{ github.ref_name }}
  271. # 测试构建的二进制文件
  272. - name: Test built binaries
  273. run: |
  274. set -ex
  275. for f in output/*/ddns; do
  276. platform=$(basename $(dirname "$f") | tr '_' '/')
  277. tests/scripts/test-in-docker.sh $platform ${{matrix.libc}} "$f"
  278. done
  279. # 输出目录结构扁平化
  280. - name: Flatten output directory structure
  281. run: |
  282. set -e
  283. mkdir -p dist
  284. for f in output/*/ddns; do
  285. name=$(basename "$(dirname "$f")")
  286. mv "$f" "dist/ddns-${{ matrix.libc }}-$name"
  287. done
  288. - name: Upload build result
  289. uses: actions/upload-artifact@v4
  290. with:
  291. name: ddns-${{ matrix.libc }}-${{ matrix.host}}
  292. path: dist/*
  293. if-no-files-found: error
  294. retention-days: ${{ github.event_name == 'push' && 14 || 3 }}
  295. docker:
  296. needs: [ python ]
  297. timeout-minutes: ${{ matrix.host == 'qemu' && 60 || 30 }}
  298. strategy:
  299. matrix:
  300. host: ['amd','arm','qemu']
  301. event:
  302. - ${{github.event_name}}
  303. exclude:
  304. # PR 时不构建 QEMU 镜像
  305. - host: qemu
  306. event: pull_request
  307. runs-on: ubuntu-${{ matrix.host == 'arm' && '24.04-arm' || 'latest' }}
  308. env:
  309. platforms: ${{
  310. matrix.host == 'amd' && 'linux/386,linux/amd64' ||
  311. matrix.host == 'arm' && 'linux/arm/v6,linux/arm/v7,linux/arm64/v8'||
  312. 'linux/ppc64le,linux/riscv64,linux/s390x'
  313. }}
  314. steps:
  315. - uses: actions/checkout@v4
  316. - run: python3 .github/patch.py docker
  317. env:
  318. GITHUB_REF_NAME: ${{ github.ref_name }}
  319. - uses: docker/setup-qemu-action@v3 # 仅仅在需要时启用 QEMU 支持
  320. if: matrix.host == 'qemu'
  321. with:
  322. platforms: ${{ env.platforms }}
  323. - uses: docker/setup-buildx-action@v3
  324. - uses: docker/build-push-action@v6
  325. with:
  326. context: .
  327. file: docker/Dockerfile
  328. platforms: ${{ env.platforms }}
  329. push: false
  330. tags: ddns:test
  331. outputs: type=oci,dest=./multi-platform-image.tar
  332. build-args: |
  333. BUILDER=ghcr.io/newfuture/nuitka-builder:master
  334. GITHUB_REF_NAME=${{ github.ref_name }}
  335. # 准备测试环境
  336. - name: Prepare test environment
  337. run: mkdir -p oci-image && tar -vxf multi-platform-image.tar -C oci-image
  338. # 使用 skopeo 批量提取所有平台镜像,正确处理变体
  339. - name: Extract platform images with skopeo
  340. uses: addnab/docker-run-action@v3
  341. with:
  342. image: quay.io/skopeo/stable:latest
  343. options: |
  344. -v ${{ github.workspace }}/:/oci
  345. -e PLATFORMS=${{ env.platforms }}
  346. run: |
  347. IFS="," read -ra PLATFORMS <<< "$PLATFORMS"
  348. for platform in "${PLATFORMS[@]}"; do
  349. echo "=== Extracting image for: $platform ==="
  350. tag=$(echo "${{ github.ref_name || 'test' }}-$platform" | tr '/' '_')
  351. arch=$(echo $platform | cut -d'/' -f2)
  352. variant=$(echo $platform | cut -d'/' -f3)
  353. variantFlag=""
  354. if [ -n "$variant" ]; then
  355. variantFlag="--override-variant $variant"
  356. fi
  357. skopeo copy --override-os linux --override-arch $arch $variantFlag \
  358. oci:/oci/oci-image:test docker-archive:/oci/ddns-oci-${tag}.tar:${{ env.DOCKER_IMG }}:${tag}
  359. done
  360. # 测试各个平台的镜像
  361. - name: Test platform images
  362. run: |
  363. set -e
  364. # 解析平台列表
  365. IFS=',' read -ra PLATFORMS <<< "${{ env.platforms }}"
  366. # 测试每个平台的镜像
  367. for platform in "${PLATFORMS[@]}"; do
  368. echo "=== Testing platform: $platform ==="
  369. # 将平台标识符转换为有效的文件名
  370. tag=$(echo "${{ github.ref_name || 'test' }}-$platform" | tr '/' '_')
  371. echo "Loading image for $platform..."
  372. docker load < ddns-oci-${tag}.tar
  373. echo "Running test..."
  374. docker run --platform $platform --rm ${{ env.DOCKER_IMG }}:$tag -v
  375. docker run --platform $platform --rm ${{ env.DOCKER_IMG }}:$tag -h
  376. docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag || test -e "config.json"
  377. sudo rm -f config.json
  378. echo "Testing with config files..."
  379. docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/callback.json
  380. docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/multi-provider.json
  381. docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/debug.json -c /ddns/tests/config/noip.json
  382. docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c https://ddns.newfuture.cc/tests/config/debug.json
  383. docker run --platform $platform --rm -v "$(pwd):/ddns/" ${{ env.DOCKER_IMG }}:$tag -c /ddns/tests/config/he-proxies.json
  384. done
  385. # 上传测试结果和镜像
  386. - name: Upload images
  387. uses: actions/upload-artifact@v4
  388. with:
  389. name: docker-${{ matrix.host }}
  390. path: ddns-oci-*.tar
  391. if-no-files-found: error
  392. retention-days: ${{ github.event_name == 'push' && 7 || 3 }}
  393. preview-pypi:
  394. runs-on: ubuntu-latest
  395. if: github.event_name == 'push'
  396. needs: [lint, pypi, python]
  397. timeout-minutes: 3
  398. environment:
  399. name: preview
  400. url: https://test.pypi.org/project/ddns/
  401. permissions:
  402. id-token: write
  403. steps:
  404. - uses: actions/download-artifact@v4
  405. with:
  406. name: pypi
  407. path: dist
  408. - uses: pypa/gh-action-pypi-publish@release/v1
  409. with:
  410. repository-url: https://test.pypi.org/legacy/
  411. print-hash: true
  412. verbose: true
  413. preview-docker:
  414. if: github.event_name == 'push'
  415. needs: [lint, docker, python]
  416. runs-on: ubuntu-latest
  417. timeout-minutes: 5
  418. environment:
  419. name: preview
  420. url: https://github.com/NewFuture/DDNS/pkgs/container/ddns/?tag=master
  421. permissions:
  422. packages: write
  423. steps:
  424. - uses: actions/download-artifact@v4
  425. with:
  426. name: docker-amd
  427. - uses: actions/download-artifact@v4
  428. with:
  429. name: docker-arm
  430. - uses: actions/download-artifact@v4
  431. with:
  432. name: docker-qemu
  433. - uses: docker/login-action@v3
  434. with:
  435. registry: ghcr.io
  436. username: ${{ github.actor }}
  437. password: ${{ secrets.GITHUB_TOKEN }}
  438. - uses: docker/login-action@v3
  439. with:
  440. username: ${{ secrets.DOCKERHUB_USERNAME }}
  441. password: ${{ secrets.DOCKERHUB_TOKEN }}
  442. - uses: docker/setup-buildx-action@v3
  443. - name: docker load and push
  444. run: |
  445. set -ex
  446. for f in $(ls ddns-oci-*.tar); do
  447. docker load -i "$f"
  448. docker push ${{ env.DOCKER_IMG }}:$(basename "$f" | sed 's/ddns-oci-//;s/.tar//')
  449. done
  450. - name: merge and push images
  451. run: |
  452. set -ex
  453. docker buildx imagetools create \
  454. -t ${{ env.DOCKER_IMG }}:${{ github.ref_name }} \
  455. -t newfuture/ddns:${{ github.ref_name }} \
  456. ${{ github.ref_name=='master' && '-t $DOCKER_IMG:edge' }} \
  457. ${{ github.ref_name=='master' && '-t newfuture/ddns:edge' }} \
  458. $(docker images --format "{{.Repository}}:{{.Tag}}" ${{ env.DOCKER_IMG }}:*) \
  459. --annotation "index,manifest:org.opencontainers.image.url=https://ddns.newfuture.cc" \
  460. --annotation "index,manifest:org.opencontainers.image.description=DDNS docker ${{ github.ref_name }} CI build (unstable version),集成测试(非稳定版)" \
  461. --annotation "index,manifest:org.opencontainers.image.authors=NewFuture,NN708"