Compare commits

..
9 Commits
Author SHA1 Message Date
naeeo d78c071818 构建状态增加平台终态只读核对,修复回调丢失后任务长期卡在构建中
工作流在脚本收尾前失败(拉取/编译/runner 异常)或 GENURL 不可达时,
结束回调会永久丢失,而等待页 30 秒轮询与列表页此前只读本地库,
任务只能等 6 小时本地超时,平台侧已失败也无法及时反映。

- GitHub/Gitea 后端新增只读 get_run(),仅在平台报告终态时返回结论,
  在途、非 200、网络异常、坏 JSON 一律返回 None;不取消、不写平台
- 等待页 30 秒轮询同步核对一次真实状态:20 秒节流、6 秒超时、异常静默
- 我的构建/后台仪表盘改为后台守护线程核对,页面渲染零等待
- 仅做非终态到终态的条件更新,回调抢先到达时不会被覆盖
- 停止/删除纯本地语义、6 小时本地超时兜底等现有逻辑保持不变
2026-09-30 12:46:05 +08:00
naeeo b0219603be 构建列表操作按钮修正:成功任务不再显示重试,统一操作列排版
- 成功状态的构建记录不再显示「重试」按钮,仅保留下载产物/日志/删除;
  失败、取消、超时仍保留一键重试(线上核对确认仅新快照成功任务误显示)
- retry_build 后端增加 success 状态拦截,防止绕过页面直接 POST
- 操作列改为统一 flex 容器(flex-wrap + gap),按钮数量随状态变化时
  间距一致、左对齐、窄屏自动换行;后台任务管理页同款统一
2026-09-30 12:03:56 +08:00
naeeo 73ca96fa67 完善 Gitea 工作流并修复移动端导航与列表按钮图标
前端:
- 「我的构建」列表「查看详情/查看进度」按钮补充与其他按钮同风格的眼睛小图标
- 顶部导航增加 768px 以下移动端适配: 顶栏纵向排列, 导航单行横向滑动, 修复手机端错位

Gitea Actions 工作流 (仅 .gitea, 不动 .github):
- 5 个生成器工作流全部补充 /updategh 终态状态回调 (成功/取消/失败均回调),
  修复无回调导致构建 6 小时后被误判为超时的 P0 缺陷
- Windows 两个工作流 job 级默认 shell 设为 pwsh, 兼容 act 在 Windows 默认 bash
- 修复 VCPKG_BINARY_SOURCES 中 \v 的 YAML 非法转义 (双引号改单引号)
- Flutter 补丁路径改为 .rdgen-src/.github/patches/, run-on-arch 容器内用 /workspace 前缀
- run-on-arch githubToken 置空, 避免 Gitea token 拉 ghcr.io 失败
- runs-on 标签对齐现场 runner: ubuntu-22.04 改为 ubuntu-24.04
- run-vcpkg/setup-ndk 钉到 node20 运行时 SHA, 规避 node24 action 不兼容
- decrypt-secrets 的 pip 安装增加三级回退; cleanzip 请求增加超时重试
- 所有产物上传 curl 增加 --fail/超时/重试参数
- macOS 动态定位 *.app 不再硬编码 RustDesk.app, DMG 路径改用 GITHUB_WORKSPACE
- 修复签名条件判断、PowerShell 变量空格等小问题
- setup.md 更新 runner 标签表及 Windows/macOS host 准备要求
2026-09-30 11:40:33 +08:00
naeeo 6e5754fb59 停止/删除改为纯本地操作,页面不再查询 Gitea
停止只在本平台标记已取消、删除只删本地记录,均不再调用 GitHub/Gitea 的查询或取消接口,操作秒响应;构建状态完全以工作流结束回调为准,未收到回调的在途任务由纯本地 6 小时超时兜底。

移除上一版的 Gitea 网页登录取消、网页账密配置项、runs 批量状态查询及两后端 get_run/cancel_run 接口,消除页面加载时的远程等待。
2026-09-30 09:20:28 +08:00
naeeo c8f41a34c0 修复列表页加载慢并支持 Gitea 1.27 一键停止
1. 状态同步加 20 秒节流、超时缩短到 8 秒;Gitea 改用 runs 列表接口一次批量取回全部任务状态,在途任务并发补查作业,消除逐任务串行 API 请求导致的后台/我的构建/详情页慢。

2. 停止时先区分 404:运行已删除则本地标记取消;Gitea 1.27 无取消 API 时,若后台配置了 Gitea 网页账号密码则模拟网页登录取消,否则回退手动链接提示。

3. 后台 Gitea 连接设置新增网页登录账号/密码两个可选项。
2026-09-30 01:04:16 +08:00
naeeo 5ff6bcfe51 docs: 补充 act_runner 注册地址必须为作业容器可达地址的说明
使用 compose 内部服务名(如 http://gitea:3000)注册会导致 host 网络作业容器无法解析,artifact 上传报 ENOTFOUND。
2026-09-30 00:20:30 +08:00
naeeo 4fd930aa0b feat: 同步构建真实状态并支持在列表停止在途任务
- Gitea 运行未结束时探测作业级结论,修复前置作业失败后可复用工作流父作业/等待标签作业长期挂起导致状态永远构建中

- 打开我的构建/管理仪表盘时主动向构建平台同步在途任务状态;创建超 6 小时仍在途的任务判超时

- 构建中任务列表增加停止按钮(Gitea 需 1.28+ 取消 API,旧版本提示去运行页手动 Cancel),终态任务保留删除

- GitHub/Gitea 后端统一 cancel_run 接口
2026-09-29 23:45:25 +08:00
naeeo 7168c71d34 docs: add full Chinese feature overview to README 2026-09-29 23:08:14 +08:00
naeeo 32918755f3 Fix secrets download on self-hosted runners: use urllib stdlib instead of requests 2026-09-29 23:08:13 +08:00
22 changed files with 700 additions and 125 deletions
+6 -1
View File
@@ -15,7 +15,12 @@ runs:
- name: install python deps
shell: bash
run: |
pip install pyzipper
# Ubuntu 24.04 enforces PEP 668 (externally-managed-environment) and
# minimal macOS runners ship no pip by default: try the system installer
# first, then a --user install, finally bootstrap pip via ensurepip.
python3 -m pip install --break-system-packages -q pyzipper 2>/dev/null \
|| python3 -m pip install -q --user pyzipper 2>/dev/null \
|| { python3 -m ensurepip --user && python3 -m pip install -q --user pyzipper; }
- name: Decrypt and Mask
shell: python
env:
+6 -5
View File
@@ -20,20 +20,21 @@ jobs:
retry_wait_seconds: 15
shell: python
command: |
import requests
import json
import time
import urllib.request
input_data = json.loads('${{ inputs.zip_url_json }}')
url = f"{input_data['url']}/get_zip?filename={input_data['file']}"
for attempt in range(5):
try:
print(f"Downloading (Attempt {attempt + 1})...")
r = requests.get(url, timeout=20)
r.raise_for_status()
# 仅使用标准库(urllib),兼容未预装 requests 的自建 runner 容器
with urllib.request.urlopen(url, timeout=20) as r:
content = r.read()
with open('secrets.zip', 'wb') as f:
f.write(r.content)
f.write(content)
break
except Exception as e:
if attempt < 4:
+36 -7
View File
@@ -105,6 +105,8 @@ jobs:
method: 'POST'
customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
@@ -195,17 +197,17 @@ jobs:
continue-on-error: true
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.rdgen-src/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- uses: nttld/setup-ndk@v1
- uses: nttld/setup-ndk@afb4c9964b521afb97c864b7d40b11e6911bd410 # v1 pinned to node20 runtime
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -599,7 +601,7 @@ jobs:
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
continue-on-error: true
if: env.ANDROID_SIGNING_KEY != null
if: env.ANDROID_SIGNING_KEY != ''
id: sign-rustdesk
with:
releaseDirectory: ./signed-apk
@@ -639,7 +641,7 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
- name: send file to api server
if: ${{ env.rdgen == 'false' }}
@@ -650,10 +652,11 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" ${{ env.apiServer }}/api/save_custom_client
deploy:
needs: [build-rustdesk-android]
if: always()
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
@@ -679,6 +682,8 @@ jobs:
method: 'POST'
customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Set rdgen value
if: ${{ env.rdgen == 'true' }}
@@ -690,6 +695,30 @@ jobs:
run: |
echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV
- name: Report build status
if: always()
shell: bash
env:
BUILD_RESULT: ${{ needs.build-rustdesk-android.result }}
run: |
# Single aggregated callback for the whole matrix: per-leg callbacks
# could flip a failed run back to success when legs finish out of order.
case "$BUILD_RESULT" in
success) STATUS=success ;;
cancelled) STATUS=cancelled ;;
*) STATUS=failure ;;
esac
if [ -z "${{ env.uuid }}" ] || [ -z "$STATUS_URL" ]; then
echo "::warning::uuid or STATUS_URL missing, skip status callback"
exit 0
fi
echo "Reporting status=$STATUS for uuid=${{ env.uuid }}"
curl --fail --silent --show-error \
--connect-timeout 10 --max-time 30 --retry 3 --retry-delay 5 \
-X POST -H "Content-Type: application/json" \
-d "{\"uuid\":\"${{ env.uuid }}\",\"status\":\"$STATUS\"}" \
"$STATUS_URL" || echo "::warning::status callback failed"
- uses: geekyeggo/delete-artifact@v4
continue-on-error: true
with:
+66 -27
View File
@@ -65,7 +65,7 @@ jobs:
arch: x86_64,
target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04,
on: ubuntu-22.04,
on: ubuntu-24.04,
deb_arch: amd64,
vcpkg-triplet: x64-linux,
}
@@ -73,7 +73,7 @@ jobs:
arch: aarch64,
target: aarch64-unknown-linux-gnu,
distro: ubuntu18.04,
on: ubuntu-22.04-arm,
on: ubuntu-24.04-arm,
deb_arch: arm64,
vcpkg-triplet: arm64-linux,
}
@@ -172,7 +172,7 @@ jobs:
- name: Setup vcpkg with Github Actions binary cache
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: lukka/run-vcpkg@v11
uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -402,7 +402,9 @@ jobs:
with:
arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }}
githubToken: ${{ github.token }}
# Gitea Actions tokens cannot authenticate against ghcr.io; leave it
# empty so run-on-arch pulls the image anonymously.
githubToken: ''
setup: |
ls -l "${PWD}"
ls -l /opt/artifacts/vcpkg/installed
@@ -533,7 +535,9 @@ jobs:
pushd /opt/flutter
;;
esac
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
# Inside the run-on-arch container the host workspace is mounted
# at /workspace, and rdgen itself lives in .rdgen-src.
git apply /workspace/.rdgen-src/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
popd
fi
@@ -686,10 +690,10 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.deb" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.rpm" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.deb" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.rpm" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true
- name: send file to api server
if: ${{ env.rdgen == 'false' }}
@@ -700,10 +704,10 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.deb" ${{ env.apiServer }}/api/save_custom_client
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.rpm" ${{ env.apiServer }}/api/save_custom_client
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" ${{ env.apiServer }}/api/save_custom_client
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst" ${{ env.apiServer }}/api/save_custom_client || true
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.deb" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.rpm" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst" ${{ env.apiServer }}/api/save_custom_client || true
- name: Upload deb
uses: actions/upload-artifact@v4
@@ -715,7 +719,7 @@ jobs:
build-rustdesk-linux-drm:
needs: [generate-bridge-linux, setup]
name: build rustdesk linux drm x86_64
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
# Only rustdesk's master branch carries the drm/libdrmtap support this job
# drives: build.py at tag 1.4.9 has neither build_libdrmtap_so nor
# assert_staged_binary_is_drm, so a versioned build dies at
@@ -824,7 +828,7 @@ jobs:
path: ./
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -1072,7 +1076,9 @@ jobs:
with:
arch: x86_64
distro: ubuntu18.04
githubToken: ${{ github.token }}
# Gitea Actions tokens cannot authenticate against ghcr.io; leave it
# empty so run-on-arch pulls the image anonymously.
githubToken: ''
setup: |
ls -l "${PWD}"
ls -l /opt/artifacts/vcpkg/installed
@@ -1165,7 +1171,9 @@ jobs:
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
pushd /opt/flutter
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
# Inside the run-on-arch container the host workspace is mounted
# at /workspace, and rdgen itself lives in .rdgen-src.
git apply /workspace/.rdgen-src/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
popd
fi
@@ -1237,7 +1245,7 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-unattended-wayland-x86_64.deb" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-unattended-wayland-x86_64.deb" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
- name: send file to api server
if: ${{ env.rdgen == 'false' && env.DRM_SUPPORTED == 'true' }}
@@ -1248,7 +1256,7 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-unattended-wayland-x86_64.deb" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-unattended-wayland-x86_64.deb" ${{ env.apiServer }}/api/save_custom_client
- name: Upload deb
uses: actions/upload-artifact@v4
@@ -1260,7 +1268,7 @@ jobs:
build-appimage:
name: Build appimage ${{ matrix.job.target }}
needs: [build-rustdesk-linux]
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@@ -1388,7 +1396,7 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./appimage/${{ env.filename }}-${{ matrix.job.arch }}.AppImage" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./appimage/${{ env.filename }}-${{ matrix.job.arch }}.AppImage" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
- name: send file to api server
if: ${{ env.rdgen == 'false' }}
@@ -1399,7 +1407,7 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./appimage/${{ env.filename }}-${{ matrix.job.arch }}.AppImage" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./appimage/${{ env.filename }}-${{ matrix.job.arch }}.AppImage" ${{ env.apiServer }}/api/save_custom_client
build-flatpak:
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
@@ -1413,7 +1421,7 @@ jobs:
- {
target: x86_64-unknown-linux-gnu,
distro: ubuntu22.04,
on: ubuntu-22.04,
on: ubuntu-24.04,
arch: x86_64,
suffix: "",
}
@@ -1421,7 +1429,7 @@ jobs:
target: aarch64-unknown-linux-gnu,
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
distro: ubuntu22.04,
on: ubuntu-22.04-arm,
on: ubuntu-24.04-arm,
arch: aarch64,
suffix: "",
}
@@ -1473,7 +1481,9 @@ jobs:
with:
arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }}
githubToken: ${{ github.token }}
# Gitea Actions tokens cannot authenticate against ghcr.io; leave it
# empty so run-on-arch pulls the image anonymously.
githubToken: ''
setup: |
ls -l "${PWD}"
dockerRunArgs: |
@@ -1529,7 +1539,7 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./flatpak/${{ env.filename }}-${{ matrix.job.arch }}.flatpak" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./flatpak/${{ env.filename }}-${{ matrix.job.arch }}.flatpak" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
- name: send file to api server
if: ${{ env.rdgen == 'false' }}
@@ -1540,7 +1550,7 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./flatpak/${{ env.filename }}-${{ matrix.job.arch }}.flatpak" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./flatpak/${{ env.filename }}-${{ matrix.job.arch }}.flatpak" ${{ env.apiServer }}/api/save_custom_client
deploy:
needs: [build-rustdesk-linux,build-flatpak,build-appimage]
@@ -1570,6 +1580,8 @@ jobs:
method: 'POST'
customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Set rdgen value
if: ${{ env.rdgen == 'true' }}
@@ -1581,6 +1593,33 @@ jobs:
run: |
echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV
- name: Report build status
if: always()
shell: bash
env:
BUILD_RESULTS: "${{ needs.build-rustdesk-linux.result }}/${{ needs.build-flatpak.result }}/${{ needs.build-appimage.result }}"
run: |
# Aggregate the deb/rpm, flatpak and appimage jobs. The optional drm
# job is not a gating artifact and is intentionally excluded.
if [ "$BUILD_RESULTS" = "success/success/success" ]; then
STATUS=success
elif echo "$BUILD_RESULTS" | grep -q "cancelled"; then
STATUS=cancelled
else
# failure / skipped (e.g. setup failed and all builds were skipped)
STATUS=failure
fi
if [ -z "${{ env.uuid }}" ] || [ -z "$STATUS_URL" ]; then
echo "::warning::uuid or STATUS_URL missing, skip status callback"
exit 0
fi
echo "Reporting status=$STATUS ($BUILD_RESULTS) for uuid=${{ env.uuid }}"
curl --fail --silent --show-error \
--connect-timeout 10 --max-time 30 --retry 3 --retry-delay 5 \
-X POST -H "Content-Type: application/json" \
-d "{\"uuid\":\"${{ env.uuid }}\",\"status\":\"$STATUS\"}" \
"$STATUS_URL" || echo "::warning::status callback failed"
- uses: geekyeggo/delete-artifact@v4
continue-on-error: true
with:
+60 -8
View File
@@ -101,6 +101,8 @@ jobs:
method: 'POST'
customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Checkout source code
@@ -294,7 +296,7 @@ jobs:
continue-on-error: true
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.rdgen-src/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Workaround for flutter issue
shell: bash
@@ -500,7 +502,7 @@ jobs:
path: ./
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
@@ -550,7 +552,7 @@ jobs:
ls -l "$APP/Contents/MacOS/custom_.txt"
- name: Install rcodesign tool
if: env.MACOS_P12_BASE64 != null
if: env.MACOS_P12_BASE64 != ''
shell: bash
run: |
pushd /tmp
@@ -584,7 +586,11 @@ jobs:
continue-on-error: false
shell: bash
run: |
ASSETS_DIR="build/macos/Build/Products/Release/RustDesk.app/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/assets"
# build.py renames the bundle to the custom app name: locate *.app
# dynamically instead of assuming RustDesk.app still exists.
APP=$(find ./flutter/build/macos/Build/Products/Release -maxdepth 1 -name "*.app" | head -n 1)
echo "App bundle: $APP"
ASSETS_DIR="$APP/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/assets"
mkdir -p "$ASSETS_DIR"
if [ -f "$ASSETS_DIR/icon.svg" ]; then
mv "$ASSETS_DIR/icon.svg" "$ASSETS_DIR/icon.svg.bak"
@@ -600,7 +606,11 @@ jobs:
continue-on-error: false
shell: bash
run: |
ASSETS_DIR="build/macos/Build/Products/Release/RustDesk.app/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/assets"
# build.py renames the bundle to the custom app name: locate *.app
# dynamically instead of assuming RustDesk.app still exists.
APP=$(find ./flutter/build/macos/Build/Products/Release -maxdepth 1 -name "*.app" | head -n 1)
echo "App bundle: $APP"
ASSETS_DIR="$APP/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/assets"
mkdir -p "$ASSETS_DIR"
curl -k -L --tlsv1.2 --proto =https --ssl-reqd \
-H "User-Agent: Mozilla/5.0" \
@@ -696,7 +706,7 @@ jobs:
- name: Create DMG
run: |
cd /Users/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/flutter/build/macos/Build/Products/Release
cd $GITHUB_WORKSPACE/flutter/build/macos/Build/Products/Release
# Print directory contents for debugging
echo "Directory contents:"
ls -la
@@ -745,7 +755,7 @@ jobs:
if: ${{ env.rdgen == 'true' }}
shell: bash
run: |
curl -i -X POST \
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 \
-H "Content-Type: multipart/form-data" \
-H "Authorization: Bearer ${{ env.token }}" \
-F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \
@@ -757,7 +767,7 @@ jobs:
if: ${{ env.rdgen == 'false' }}
shell: bash
run: |
curl -i -X POST \
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 \
-H "Content-Type: multipart/form-data" \
-H "Authorization: Bearer ${{ env.token }}" \
-F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \
@@ -769,6 +779,48 @@ jobs:
continue-on-error: true
if: always()
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
path: .rdgen-src
- uses: actions/download-artifact@v4
with:
name: encrypted-secrets-zip
- name: Load Secrets
uses: ./.gitea/actions/decrypt-secrets
with:
zip_password: ${{ secrets.ZIP_PASSWORD }}
- name: Report build status
if: always()
shell: bash
env:
BUILD_RESULT: ${{ needs.build-for-macos.result }}
run: |
# Single aggregated callback for the x86_64/aarch64 matrix.
case "$BUILD_RESULT" in
success) STATUS=success ;;
cancelled) STATUS=cancelled ;;
*) STATUS=failure ;;
esac
if [ "${{ env.rdgen }}" = "false" ]; then
STATUS_URL="${{ env.apiServer }}/api/updategh"
else
STATUS_URL="${{ secrets.GENURL }}/updategh"
fi
if [ -z "${{ env.uuid }}" ]; then
echo "::warning::uuid missing, skip status callback"
exit 0
fi
echo "Reporting status=$STATUS for uuid=${{ env.uuid }}"
curl --fail --silent --show-error \
--connect-timeout 10 --max-time 30 --retry 3 --retry-delay 5 \
-X POST -H "Content-Type: application/json" \
-d "{\"uuid\":\"${{ env.uuid }}\",\"status\":\"$STATUS\"}" \
"$STATUS_URL" || echo "::warning::status callback failed"
- name: Delete secrets artifact
uses: geekyeggo/delete-artifact@v4
with:
+55 -6
View File
@@ -28,7 +28,7 @@ env:
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;files,D:\vcpkg-cache,readwrite"
VCPKG_BINARY_SOURCES: 'clear;files,D:\vcpkg-cache,readwrite'
# vcpkg version: 2025.08.27
# If we change the `VCPKG COMMIT_ID`, please remember:
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
@@ -56,6 +56,11 @@ jobs:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
needs: setup
runs-on: ${{ matrix.job.os }}
# Gitea act defaults to bash on Windows hosts; most unmarked steps here are
# PowerShell. Requires PowerShell 7 (pwsh) installed on the self-hosted runner.
defaults:
run:
shell: pwsh
# Temporarily disable this action due to additional test is needed.
# if: false
strategy:
@@ -95,6 +100,8 @@ jobs:
method: 'POST'
customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Set rdgen value
@@ -277,7 +284,7 @@ jobs:
prefix-key: ${{ matrix.job.os }}-sciter
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -422,9 +429,9 @@ jobs:
cargo build --target ${{ matrix.job.target }} --features inline,vram,hwcodec --release --bins
mkdir -p ./Release
mv ./target/${{ matrix.job.target }}/release/rustdesk.exe ./Release/rustdesk.exe
curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
curl --fail -LJ --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 5 -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
echo "output_folder=./Release" >> $GITHUB_OUTPUT
curl -LJ -o ./usbmmidd_v2.zip https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip
curl --fail -LJ --connect-timeout 30 --max-time 300 --retry 3 --retry-delay 5 -o ./usbmmidd_v2.zip https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip
unzip usbmmidd_v2.zip
# Do not remove x64 files, because the user may run the 32bit version on a 64bit system.
# Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64,
@@ -534,13 +541,13 @@ jobs:
if: ${{ env.rdgen == 'true' }}
shell: bash
run: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
- name: send file to api server
if: ${{ env.rdgen == 'false' }}
shell: bash
run: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client
cleanup:
needs: [build-for-windows-sciter]
@@ -548,6 +555,48 @@ jobs:
continue-on-error: true
if: always()
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
path: .rdgen-src
- uses: actions/download-artifact@v4
with:
name: encrypted-secrets-zip
- name: Load Secrets
uses: ./.gitea/actions/decrypt-secrets
with:
zip_password: ${{ secrets.ZIP_PASSWORD }}
- name: Report build status
if: always()
shell: bash
env:
BUILD_RESULT: ${{ needs.build-for-windows-sciter.result }}
run: |
# Single aggregated callback (the build job may be skipped/cancelled).
case "$BUILD_RESULT" in
success) STATUS=success ;;
cancelled) STATUS=cancelled ;;
*) STATUS=failure ;;
esac
if [ "${{ env.rdgen }}" = "false" ]; then
STATUS_URL="${{ env.apiServer }}/api/updategh"
else
STATUS_URL="${{ secrets.GENURL }}/updategh"
fi
if [ -z "${{ env.uuid }}" ]; then
echo "::warning::uuid missing, skip status callback"
exit 0
fi
echo "Reporting status=$STATUS for uuid=${{ env.uuid }}"
curl --fail --silent --show-error \
--connect-timeout 10 --max-time 30 --retry 3 --retry-delay 5 \
-X POST -H "Content-Type: application/json" \
-d "{\"uuid\":\"${{ env.uuid }}\",\"status\":\"$STATUS\"}" \
"$STATUS_URL" || echo "::warning::status callback failed"
- name: Delete secrets artifact
uses: geekyeggo/delete-artifact@v4
with:
+57 -7
View File
@@ -28,7 +28,7 @@ env:
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;files,D:\vcpkg-cache,readwrite"
VCPKG_BINARY_SOURCES: 'clear;files,D:\vcpkg-cache,readwrite'
# vcpkg version: 2024.07.12
VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
@@ -70,6 +70,11 @@ jobs:
name: Build Windows
needs: [build-RustDeskTempTopMostWindow, generate-bridge, setup]
runs-on: ${{ matrix.job.os }}
# Gitea act defaults to bash on Windows hosts; most unmarked steps here are
# PowerShell. Requires PowerShell 7 (pwsh) installed on the self-hosted runner.
defaults:
run:
shell: pwsh
strategy:
fail-fast: false
matrix:
@@ -107,6 +112,8 @@ jobs:
method: 'POST'
customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Set rdgen value
@@ -385,7 +392,7 @@ jobs:
- name: Patch flutter
shell: bash
run: |
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
cp .rdgen-src/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
@@ -401,7 +408,7 @@ jobs:
prefix-key: ${{ matrix.job.os }}
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11
uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -723,8 +730,8 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true
- name: send file to api server
if: ${{ env.rdgen == 'false' }}
@@ -735,8 +742,8 @@ jobs:
retry_wait_seconds: 15
shell: bash
command: |
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client
curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" ${{ env.apiServer }}/api/save_custom_client || true
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client
curl --fail -i -X POST --connect-timeout 10 --max-time 300 --retry 3 --retry-delay 5 -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" ${{ env.apiServer }}/api/save_custom_client || true
cleanup:
needs: [build-for-windows-flutter]
@@ -744,6 +751,49 @@ jobs:
continue-on-error: true
if: always()
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
path: .rdgen-src
- uses: actions/download-artifact@v4
with:
name: encrypted-secrets-zip
- name: Load Secrets
uses: ./.gitea/actions/decrypt-secrets
with:
zip_password: ${{ secrets.ZIP_PASSWORD }}
- name: Report build status
if: always()
shell: bash
env:
BUILD_RESULT: ${{ needs.build-for-windows-flutter.result }}
run: |
# Single aggregated callback. needs.result also covers the upstream
# reusable jobs: if third-party/bridge/setup fail the build is "skipped".
case "$BUILD_RESULT" in
success) STATUS=success ;;
cancelled) STATUS=cancelled ;;
*) STATUS=failure ;;
esac
if [ "${{ env.rdgen }}" = "false" ]; then
STATUS_URL="${{ env.apiServer }}/api/updategh"
else
STATUS_URL="${{ secrets.GENURL }}/updategh"
fi
if [ -z "${{ env.uuid }}" ]; then
echo "::warning::uuid missing, skip status callback"
exit 0
fi
echo "Reporting status=$STATUS for uuid=${{ env.uuid }}"
curl --fail --silent --show-error \
--connect-timeout 10 --max-time 30 --retry 3 --retry-delay 5 \
-X POST -H "Content-Type: application/json" \
-d "{\"uuid\":\"${{ env.uuid }}\",\"status\":\"$STATUS\"}" \
"$STATUS_URL" || echo "::warning::status callback failed"
- name: Delete secrets artifact
uses: geekyeggo/delete-artifact@v4
with:
+1 -1
View File
@@ -28,7 +28,7 @@ env:
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;files,D:\vcpkg-cache,readwrite"
VCPKG_BINARY_SOURCES: 'clear;files,D:\vcpkg-cache,readwrite'
# vcpkg version: 2024.07.12
VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
@@ -27,6 +27,13 @@ on:
required: true
type: string
default: 'Windows10'
secrets:
ZIP_PASSWORD:
description: 'Password for the encrypted secrets zip'
required: true
GENURL:
description: 'rdgen platform base URL'
required: true
env:
project_path: WindowInjection/WindowInjection.vcxproj
@@ -63,6 +70,8 @@ jobs:
method: 'POST'
customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Download the source code
run: |
@@ -78,7 +87,7 @@ jobs:
shell: pwsh
command: |
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796
if ($env:privacylink_url-ne "false") {
if ($env:privacylink_url -ne "false") {
Invoke-WebRequest -Uri ${{ env.privacylink_url }}/get_png?filename=${{ env.privacylink_file }}"&"uuid=${{ env.privacylink_uuid }} -OutFile privacy.png
Copy-Item ../.github/patches/privacyScreen.py privacyScreen.py
python privacyScreen.py
@@ -19,20 +19,21 @@ jobs:
retry_wait_seconds: 15
shell: python
command: |
import requests
import json
import time
import urllib.request
input_data = json.loads('${{ inputs.zip_url_json }}')
url = f"{input_data['url']}/get_zip?filename={input_data['file']}"
for attempt in range(5):
try:
print(f"Downloading (Attempt {attempt + 1})...")
r = requests.get(url, timeout=20)
r.raise_for_status()
# 仅使用标准库(urllib),无需预装 requests,兼容各类 runner 环境
with urllib.request.urlopen(url, timeout=20) as r:
content = r.read()
with open('secrets.zip', 'wb') as f:
f.write(r.content)
f.write(content)
break
except Exception as e:
if attempt < 4:
+73
View File
@@ -29,3 +29,76 @@ Gitea 1.27+ and your own registered `act_runner` machines. Full setup: [setup.md
- Avoid special characters or non-English characters in app name and file name
- Build time is currently 30 - 45 minutes
---
# 功能说明(中文)
RDGen 是一个基于 **Django + Docker** 的 RustDesk 客户端在线定制平台:用户在网页上填表选择版本、服务器、图标和各类高级选项,平台自动触发 CI(GitHub Actions 或自建 Gitea Actions)编译出专属客户端,并在构建完成后回收安装包供下载。支持多用户、任务管理、配置方案复用和开放 JSON API。
## 一、客户端定制功能
- **内置服务器信息**:预置 ID/中继服务器地址、端口、公钥(Key)、API 服务器地址,客户端开箱即连自建服务器
- **品牌定制**:自定义应用名称、可执行文件名、应用图标、界面 Logo、隐私声明页图片(建议 256×256 方形图标)
- **下载/文档链接**:可配置官网链接、下载链接、关于页链接,以及安卓包名(Android applicationId)
- **默认行为**:设置默认连接方向(接收/发起/双向)、是否显示安装程序、是否开放设置入口、浅色/深色/跟随系统主题
- **安全策略**:预设永久访问密码、密码审批方式(仅密码/仅点击/两者)、是否禁止局域网发现、是否允许 IP 直连、无操作自动断开
- **权限矩阵**:可逐项开关被控端的键鼠控制、剪贴板、文件传输、音频、TCP 隧道、远程重启、会话录制、屏蔽输入、远程改配置、打印机、摄像头、终端、移除壁纸等
- **高级设置**:修复第三方 API 延迟、离线设备标记、隐藏连接窗口、移除新版本通知、自定义默认/覆盖手册文案等,覆盖 RustDesk 官方 advanced settings 全部常用项
- **版本选择**:版本下拉自动跟随 `rustdesk/rustdesk` 官方 tags 更新(缓存 6 小时),网络不可用时回退内置版本列表,也支持手动指定 master/main
## 二、构建产物(目标平台)
一次构建可产出对应平台的多份安装包,构建耗时约 30~45 分钟:
- **Windows 64 位**:`.exe` 便携版 + `.msi` 安装版
- **Windows 32 位**:`.exe`
- **Linux**:deb / rpm / suse-rpm / pkg.tar.zst / AppImage / flatpak,含 x86_64 与 aarch64
- **Android**:aarch64(主流)、x86_64、armv7 三份 `.apk`
- **macOS**:Intel(x86_64)与 Apple 芯片(aarch64)两份 `.dmg`
## 三、双构建平台可切换
- 支持 **GitHub Actions**(云托管 Windows/macOS/Linux 构建机)与 **Gitea Actions**(自建、可全内网运行)两种后端
- 后台「构建平台配置」页一键切换(或环境变量 `BUILD_PLATFORM=github|gitea`),切换后新任务走新平台,**历史任务仍按原平台查询状态和跳转日志**
- Gitea 模式可独立配置服务器地址、属主、仓库、分支、令牌、代理,需 Gitea ≥ 1.23(建议 1.27+)并自行注册 act_runner(Linux/Windows/macOS 标签机)
- 后台提供「测试连接」按钮,即时验证令牌、仓库、Actions 权限和版本兼容性;配置缺失时顶部显示中文告警
- 触发参数以 AES 加密 ZIP 包传递给工作流(`ZIP_PASSWORD`),回调支持 `X-Webhook-Secret` 请求头签名校验(留空则兼容旧工作流)
## 四、构建任务管理
- 提交后进入构建进度页,页面每 30 秒自动轮询,状态含排队中/构建中/成功/失败/取消/超时等,中文状态徽章实时展示
- 「我的构建」列表:查看本人全部任务、目标平台、**构建平台徽章(GitHub 深色 / Gitea 绿色)**、状态、编号
- 任务详情(构建中/失败/成功三页)均标明构建平台,并提供对应平台的一键日志跳转链接
- 失败/终态任务可用原始配置快照**一键重试**,也可删除任务(同时清理服务器上的安装包产物)
- 构建产物经 CI 加密回传,页面按平台列出所有可下载文件,下载链接带 UUID 鉴权
## 五、用户、权限与配置方案
- 账号登录制,普通用户只能看到和操作自己的任务;管理员(is_staff)可访问后台、查看全部任务
- 管理员后台可新建/编辑用户:启停账号、授予/撤销管理员、重置密码
- **配置方案**:常用定制参数可保存为方案(与账号绑定),定制页一键载入,支持导出 JSON、查看方案数据和删除
- **API 令牌**:用户可创建命名个人令牌(记录最后使用时间、可停用/删除),用于命令行或第三方系统调用
## 六、开放 JSON API
- `POST /api/generate`:以 JSON 提交与网页表单完全等价的定制参数,通过 `Authorization: Token <key>` 认证,自动触发构建
- `GET /api/status`:按 UUID 查询构建状态与日志链接
- 兼容第三方自建 RustDesk API 服务的 `/startgh`、`/save_custom_client`、`/updategh` 等无会话回调端点(可选 Webhook 密钥校验)
- 可配合 [rdgen-cli](https://github.com/AlekseyLapunov/rdgen-cli) 在 Windows/Linux/macOS 命令行直接发起构建
## 七、后台运维能力
- **管理仪表盘**:任务总数、进行中、成功、失败、用户数、有效令牌数等统计,最近 100 条任务总览,可直接删除任务
- **站点配置存储**:后台配置 → 环境变量 → 默认值三级优先级,所有密钥类字段密文存储、可单独清除,无需改环境变量即可在线调整
- **定时维护**:容器内置维护守护进程(每小时巡检),按保留天数自动清理过期任务及产物、清理临时加密包;可在后台「维护设置」调整或「立即执行」
- **数据库备份**:SQLite 在线备份,默认每日一份、保留 7 份(可配 1~100),后台可下载备份文件
- **健康检查**:`/healthz/` 供容器编排探活
- **网络适配**:GitHub/Gitea 后端可分别配置 HTTP/SOCKS5 代理,适配内网通过代理出网或 Gitea 内网直连的混合环境
## 八、部署方式
- 一条 `docker compose up -d` 完成部署:Gunicorn 托管 Django,自动执行数据库迁移与静态文件收集,并启动维护守护进程
- 数据库(账号、任务记录、配置方案、站点设置)保存在独立命名卷 `rdgen-data`,重建镜像不丢数据;安装包/图片/临时文件走宿主机绑定挂载
- 首次启动按环境变量自动创建初始管理员;详细部署、Fork 仓库、Secrets 配置、Gitea act_runner 注册见 [setup.md](setup.md)
+1
View File
@@ -20,6 +20,7 @@ urlpatterns = [
path('', views.generator_view, name='generator'),
path('my-builds/', web_views.my_builds, name='my_builds'),
path('my-builds/<int:run_id>/retry/', web_views.retry_build, name='retry_build'),
path('my-builds/<int:run_id>/stop/', web_views.stop_build, name='stop_build'),
path('my-builds/<int:run_id>/delete/', web_views.delete_my_build, name='delete_my_build'),
path('configs/', web_views.saved_configs, name='saved_configs'),
path('configs/save/', web_views.save_config, name='save_config'),
+9 -7
View File
@@ -20,9 +20,10 @@ class DispatchResult:
@dataclass
class RunInfo:
finished: bool # 是否已进入终态
status: str = '' # 终态结论:success/failure/cancelled/skipped;未结束为进行中状态
html_url: str = ''
"""平台单次运行的状态快照。"""
finished: bool = False # 是否已结束(拿到终态结论)
status: str = '' # finished=True 时为 rdgen 内部终态码
html_url: str = '' # 平台上的运行详情页地址
# rdgen 目标平台 -> 工作流文件名(两平台同名)
@@ -57,13 +58,14 @@ class BuildBackend:
"""触发工作流。inputs 为完整输入字典(GitHub/Gitea 均为 {ref, inputs})。"""
raise NotImplementedError
def get_run(self, run_id, *, timeout=12):
"""查询单次运行;网络异常或非 200 返回 None(调用方按「仍在进行」处理)。"""
raise NotImplementedError
def log_url(self, run_id) -> str:
raise NotImplementedError
def get_run(self, run_id, *, timeout=8):
"""只读查询单次运行:已结束返回 RunInfo(finished=True, status=内部终态码);
仍在途、查询失败或不支持时返回 None(调用方按「仍在进行」处理)。"""
return None
# ---- 后台连接测试 ----
def test_connection(self) -> dict:
"""返回 {'ok': bool, 'message': 中文说明},不抛异常。"""
+26 -12
View File
@@ -4,22 +4,26 @@ API 与 GitHub Actions 几乎同形(已对照 Gitea 官方 Swagger 核实)
- 触发:POST /api/v1/repos/{owner}/{repo}/actions/workflows/{file}/dispatches
?return_run_details=true,body {ref, inputs},200 返回
{workflow_run_id, html_url, run_url},不传该参数为 204;
- 状态:GET /api/v1/repos/{owner}/{repo}/actions/runs/{id},
status/conclusion 字段仿 GitHub;
- 鉴权:Authorization: token <PAT>。
注意:本平台不主动取消 Gitea 上的运行,停止/删除只作用于本平台本地记录。
构建状态以工作流结束时的回调(update_github_run)为第一来源;get_run 仅做
只读终态核对,兜底回调丢失(脚本未跑到收尾或 GENURL 不可达)的情况。
"""
from .. import app_settings, ghnet
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
# Gitea 未结束状态(blocked=等待审批,waiting=等待 runner)
_GITEA_ACTIVE = {'queued', 'in_progress', 'waiting', 'blocked', 'requested', 'pending'}
# Gitea 结论 -> rdgen 内部状态
_CONCLUSION_MAP = {
# 部分 Gitea 版本 run.status 直接返回终态(无 completed+conclusion 包裹)
_GITEA_TERMINAL = {'success', 'failure', 'cancelled', 'skipped', 'timed_out', 'stopped', 'dead'}
# Gitea 结论 -> rdgen 内部终态
_GITEA_CONCLUSION_MAP = {
'success': 'success',
'failure': 'failure',
'cancelled': 'cancelled',
'skipped': 'skipped',
'timed_out': 'timed_out',
'stopped': 'cancelled',
'dead': 'failure',
}
@@ -140,8 +144,8 @@ class GiteaBackend(BuildBackend):
print(f'查询 Gitea 最新运行失败:{e}')
return None
# ---- 状态查询 ----
def get_run(self, run_id, *, timeout=12):
# ---- 只读状态查询(回调丢失时的终态兜底) ----
def get_run(self, run_id, *, timeout=8):
url = self._api(f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}')
try:
resp = ghnet.get(
@@ -153,18 +157,28 @@ class GiteaBackend(BuildBackend):
return None
if resp.status_code != 200:
return None
data = resp.json()
try:
data = resp.json()
except ValueError:
return None
html_url = data.get('html_url') or self.log_url(run_id)
status = (data.get('status') or '').lower()
conclusion = (data.get('conclusion') or '').lower()
if status == 'completed':
return RunInfo(
finished=True,
status=_CONCLUSION_MAP.get(conclusion, 'failure'),
status=_GITEA_CONCLUSION_MAP.get(conclusion, 'failure'),
html_url=html_url,
)
# blocked(等待审批)等也算进行中
return RunInfo(finished=False, status=status or 'in_progress', html_url=html_url)
# 部分 Gitea 版本直接在 status 返回终态(无 completed 包裹)
if status in _GITEA_TERMINAL:
return RunInfo(
finished=True,
status=_GITEA_CONCLUSION_MAP.get(status, 'failure'),
html_url=html_url,
)
# queued/in_progress/waiting/blocked 等在途状态返回 None,交给回调与下次核对
return None
# ---- 日志链接 ----
def log_url(self, run_id):
+30 -11
View File
@@ -1,11 +1,23 @@
"""GitHub Actions 构建后端(迁自 views.py / web_views.py 的原有逻辑,行为保持一致)。"""
"""GitHub Actions 构建后端(迁自 views.py / web_views.py 的原有逻辑,行为保持一致)。
注意:本平台不主动取消 GitHub 上的运行,停止/删除只作用于本平台本地记录。
构建状态以工作流结束时的回调(update_github_run)为第一来源;get_run 仅做
只读终态核对,兜底回调丢失(脚本未跑到收尾或 GENURL 不可达)的情况。
"""
from .. import app_settings, ghnet
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
API = 'https://api.github.com'
# 未进入 completed 的状态一律视为进行中
_GITHUB_ACTIVE = {'queued', 'in_progress', 'waiting', 'requested', 'pending'}
# GitHub conclusion -> rdgen 内部终态;无人值守构建不会有人处理审批,
# action_required/neutral/stale/startup_failure 等异常结论一律按失败呈现
_GH_CONCLUSION_MAP = {
'success': 'success',
'failure': 'failure',
'cancelled': 'cancelled',
'skipped': 'skipped',
'timed_out': 'timed_out',
}
class GitHubBackend(BuildBackend):
@@ -56,8 +68,8 @@ class GitHubBackend(BuildBackend):
pass
return DispatchResult(run_id=data.get('workflow_run_id'), html_url=data.get('html_url') or '')
# ---- 状态查询 ----
def get_run(self, run_id, *, timeout=12):
# ---- 只读状态查询(回调丢失时的终态兜底) ----
def get_run(self, run_id, *, timeout=8):
user = app_settings.get_value('GHUSER')
repo = app_settings.get_value('REPONAME')
url = f'{API}/repos/{user}/{repo}/actions/runs/{run_id}'
@@ -68,12 +80,19 @@ class GitHubBackend(BuildBackend):
return None
if resp.status_code != 200:
return None
data = resp.json()
if data.get('status') == 'completed':
return RunInfo(finished=True, status=data.get('conclusion') or 'failure',
html_url=data.get('html_url') or self.log_url(run_id))
return RunInfo(finished=False, status=data.get('status') or 'in_progress',
html_url=data.get('html_url') or self.log_url(run_id))
try:
data = resp.json()
except ValueError:
return None
# 只关心终态:在途(queued/in_progress/waiting)返回 None,交给回调与下次核对
if data.get('status') != 'completed':
return None
conclusion = (data.get('conclusion') or 'failure').lower()
return RunInfo(
finished=True,
status=_GH_CONCLUSION_MAP.get(conclusion, 'failure'),
html_url=data.get('html_url') or self.log_url(run_id),
)
# ---- 日志链接 ----
def log_url(self, run_id):
@@ -134,6 +134,26 @@ a:hover { text-decoration: underline; }
}
.btn-logout:hover { border-color: var(--danger); color: var(--danger); }
/* ===== 顶部导航移动端适配:窄屏上下两行,导航单行横向滑动不折行 ===== */
@media (max-width: 768px) {
.topbar-inner {
height: auto;
flex-direction: column;
align-items: stretch;
gap: 8px;
padding: 10px 16px;
}
.topbar nav {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: 2px;
}
.topbar nav::-webkit-scrollbar { display: none; }
.nav-link, .nav-user, .btn-logout { flex: none; }
}
/* ===== 页面容器与标题 ===== */
.page {
flex: 1;
@@ -367,6 +387,8 @@ input[type="file"].form-control::-webkit-file-upload-button {
.btn-secondary:hover { border-color: var(--primary-border); color: var(--primary); }
.btn-danger { background: #fff; color: var(--danger); border-color: #f3c2c2; }
.btn-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
.btn-warning { background: #fff; color: var(--warning); border-color: #f3d9a6; }
.btn-warning:hover { background: var(--warning); color: #fff; border-color: var(--warning); }
.btn-sm { min-height: 30px; padding: 0 12px; font-size: 12.5px; border-radius: 6px; }
.btn-sm .icon { font-size: 13px; }
.btn-block { width: 100%; }
@@ -374,6 +396,14 @@ input[type="file"].form-control::-webkit-file-upload-button {
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; }
.inline-form { display: inline-flex; vertical-align: middle; }
.table .btn + .btn, .table .btn + .inline-form { margin-left: 8px; }
/* 表格操作列:按钮数量随任务状态变化,用单一 flex 容器统一间距与换行,
避免各行 2~4 个按钮时排列参差 */
.col-actions { min-width: 168px; }
.table .row-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.table .row-actions > .btn,
.table .row-actions > .inline-form { margin-left: 0; }
.table .row-actions .inline-form { margin: 0; }
.table .row-actions .btn { white-space: nowrap; }
/* ===== 平台选择卡 ===== */
.platform-grid {
+6
View File
@@ -76,6 +76,9 @@
<symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>
</symbol>
<symbol id="i-stop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="6" y="6" width="12" height="12" rx="2"/>
</symbol>
<symbol id="i-check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/><path d="m8.5 12 2.5 2.5 5-5"/>
</symbol>
@@ -108,6 +111,9 @@
<circle cx="12" cy="12" r="9"/>
<path d="M12 7v5l3 2"/>
</symbol>
<symbol id="i-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/>
</symbol>
<symbol id="i-database" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="12" cy="5" rx="8" ry="3"/>
<path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5"/>
+3 -1
View File
@@ -49,7 +49,8 @@
<td>{{ run.platform_label }}</td>
<td><span class="badge {{ run.status_badge }} {% if not run.is_finished %}badge-dot{% endif %}">{{ run.status_label }}</span></td>
<td class="mono text-muted">{{ run.short_uuid }}</td>
<td style="white-space:nowrap">
<td class="col-actions">
<div class="row-actions">
<a class="btn btn-secondary btn-sm"
href="/check_for_file?filename={{ run.filename|urlencode }}&uuid={{ run.uuid|urlencode }}&platform={{ run.platform|urlencode }}">查看</a>
<form method="post" class="inline-form" action="{% url 'delete_build' run.id %}"
@@ -59,6 +60,7 @@
<svg class="icon"><use href="#i-trash"/></svg> 删除
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
+14 -3
View File
@@ -32,17 +32,18 @@
<td><span class="badge {{ run.backend_badge_class }}">{{ run.backend_label }}</span></td>
<td><span class="badge {{ run.status_badge }} {% if not run.is_finished %}badge-dot{% endif %}">{{ run.status_label }}</span></td>
<td class="mono text-muted">{{ run.short_uuid }}</td>
<td style="white-space:nowrap">
<td class="col-actions">
<div class="row-actions">
<a class="btn btn-secondary btn-sm"
href="/check_for_file?filename={{ run.filename|urlencode }}&uuid={{ run.uuid|urlencode }}&platform={{ run.platform|urlencode }}">
{% if run.status == 'success' %}<svg class="icon"><use href="#i-download"/></svg> 下载产物{% elif run.is_finished %}查看详情{% else %}查看进度{% endif %}
{% if run.status == 'success' %}<svg class="icon"><use href="#i-download"/></svg> 下载产物{% elif run.is_finished %}<svg class="icon"><use href="#i-eye"/></svg> 查看详情{% else %}<svg class="icon"><use href="#i-eye"/></svg> 查看进度{% endif %}
</a>
{% if run.github_log_url %}
<a class="btn btn-secondary btn-sm" href="{{ run.github_log_url }}" target="_blank" rel="noopener">
<svg class="icon"><use href="#i-external"/></svg> 日志
</a>
{% endif %}
{% if run.is_finished and run.config_data %}
{% if run.is_finished and run.status != 'success' and run.config_data %}
<form method="post" class="inline-form" action="{% url 'retry_build' run.id %}"
onsubmit="return confirm('将使用该任务的原始配置重新提交一次构建(原任务记录保留),是否继续?');">
{% csrf_token %}
@@ -51,6 +52,15 @@
</button>
</form>
{% endif %}
{% if not run.is_finished %}
<form method="post" class="inline-form" action="{% url 'stop_build' run.id %}"
onsubmit="return confirm('确定停止构建任务「{{ run.filename|default:run.short_uuid|escapejs }}」吗?停止后可在列表中使用「重试」重新发起。');">
{% csrf_token %}
<button type="submit" class="btn btn-warning btn-sm">
<svg class="icon"><use href="#i-stop"/></svg> 停止
</button>
</form>
{% endif %}
{% if run.is_finished %}
<form method="post" class="inline-form" action="{% url 'delete_my_build' run.id %}"
onsubmit="return confirm('确定删除任务「{{ run.filename|default:run.short_uuid|escapejs }}」吗?将同时删除服务器上的安装包文件,且不可恢复。');">
@@ -60,6 +70,7 @@
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
+123 -8
View File
@@ -2,7 +2,10 @@ import io
import json
import os
import re
import threading
import time
import uuid
from datetime import timedelta
from pathlib import Path
from urllib.parse import quote
@@ -22,12 +25,128 @@ from django.http import (
Http404,
)
from django.shortcuts import render
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from . import app_settings, build_backends, versions as rdeskVersions
from .forms import GenerateForm
from .models import GithubRun, STATUS_LABELS, STATUS_BADGE
# 终态状态集合
TERMINAL_STATUSES = ('success', 'failure', 'cancelled', 'timed_out', 'skipped')
# 正常构建约 30~45 分钟;超过该时长仍在进行中的,视为 runner 离线/作业僵死。
# 状态以构建脚本结束回调为第一来源;回调丢失时由 get_run 只读核对平台终态兜底,
# 超时判断只做本地兜底。
STALE_BUILD_HOURS = 6
# 主动核对节流:同一在途任务两次平台查询的最小间隔,避免轮询/刷页面打满 API
REFRESH_THROTTLE_SECONDS = 20
# 平台查询短超时,平台抖动时等待页轮询最多多等这几秒,不拖垮页面
REFRESH_TIMEOUT = 6
_refresh_stamps = {}
_refresh_lock = threading.Lock()
def _claim_refresh_slot(run_pk):
"""节流:到间隔才占位成功;先占位再请求,避免并发页面/线程重复打平台。"""
now = time.monotonic()
with _refresh_lock:
if now - _refresh_stamps.get(run_pk, 0) >= REFRESH_THROTTLE_SECONDS:
_refresh_stamps[run_pk] = now
return True
return False
def _apply_remote_terminal(gh_run, info):
"""平台明确终态且本地仍在途时落库(条件更新,避免覆盖刚到达的回调)。返回是否变化。"""
if info is None or not info.finished:
return False
new_status = info.status
if new_status not in TERMINAL_STATUSES or new_status == gh_run.status:
return False
updated = GithubRun.objects.filter(pk=gh_run.pk) \
.exclude(status__in=TERMINAL_STATUSES) \
.update(status=new_status, updated_at=timezone.now())
if updated:
gh_run.status = new_status
return bool(updated)
def refresh_active_run(gh_run):
"""同步向构建平台核对单个在途任务(等待页 30 秒轮询 / 状态接口用)。
20 秒节流、6 秒超时;任何网络异常静默。仅当平台报告终态时落库,
工作流结束回调仍是第一状态来源,本核对只兜底回调丢失。
"""
if gh_run.is_finished() or not gh_run.github_run_id:
return False
if not _claim_refresh_slot(gh_run.pk):
return False
try:
backend = build_backends.get_backend(gh_run.backend or 'github')
info = backend.get_run(gh_run.github_run_id, timeout=REFRESH_TIMEOUT)
except Exception as e:
print(f"核对构建状态出错:{e}")
return False
return _apply_remote_terminal(gh_run, info)
def _refresh_runs_in_background(run_pks):
"""后台线程体:逐个核对终态;与请求/渲染完全解耦,异常不影响任何页面。"""
try:
for pk in run_pks:
gh_run = GithubRun.objects.filter(pk=pk).first()
if gh_run is None or gh_run.is_finished() or not gh_run.github_run_id:
continue
try:
backend = build_backends.get_backend(gh_run.backend or 'github')
info = backend.get_run(gh_run.github_run_id, timeout=REFRESH_TIMEOUT)
_apply_remote_terminal(gh_run, info)
except Exception as e:
print(f"后台核对构建状态出错:{e}")
finally:
# 子线程使用独立数据库连接,结束时必须关闭
from django.db import connections
connections.close_all()
def schedule_refresh_for_runs(gh_runs):
"""列表/仪表盘用:为在途任务安排后台核对,页面渲染零等待。
与等待页共用 20 秒节流;无在途任务或刚核对过时不创建线程。
"""
due_pks = [
r.pk for r in gh_runs
if not r.is_finished() and r.github_run_id and _claim_refresh_slot(r.pk)
]
if not due_pks:
return
threading.Thread(
target=_refresh_runs_in_background, args=(due_pks,), daemon=True).start()
def mark_stale_builds(gh_runs=None):
"""纯本地兜底:在途任务超过 STALE_BUILD_HOURS 未收到结束回调的,标记为超时。
不产生任何网络请求。传入任务列表时只处理这些任务;不传则扫描全表。
"""
cutoff = timezone.now() - timedelta(hours=STALE_BUILD_HOURS)
qs = GithubRun.objects.exclude(status__in=TERMINAL_STATUSES)
if gh_runs is not None:
ids = [r.pk for r in gh_runs if not r.is_finished()]
qs = qs.filter(pk__in=ids)
qs.filter(created_at__lt=cutoff).update(
status='timed_out', updated_at=timezone.now())
def mark_run_stale_if_needed(gh_run):
"""单个任务的纯本地超时兜底(详情/状态接口用)。"""
if not gh_run.is_finished() and gh_run.created_at < \
timezone.now() - timedelta(hours=STALE_BUILD_HOURS):
gh_run.status = 'timed_out'
gh_run.save(update_fields=['status', 'updated_at'])
return True
return False
def _webhook_denied(request):
"""配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。"""
@@ -390,14 +509,10 @@ def _get_run_status(uuid_val):
backend = build_backends.get_backend(gh_run.backend or 'github')
github_log_url = backend.log_url(gh_run.github_run_id) if gh_run.github_run_id else ''
if gh_run.status not in ['success', 'failure', 'cancelled', 'timed_out', 'skipped'] and gh_run.github_run_id:
try:
info = backend.get_run(gh_run.github_run_id)
if info is not None and info.finished:
gh_run.status = info.status
gh_run.save()
except Exception as e:
print(f"查询构建状态出错:{e}")
# 状态以构建脚本结束回调为准;此处先做平台终态只读核对(20 秒节流、6 秒超时,
# 回调丢失时能及时发现平台侧已失败/取消),再做纯本地的 6 小时超时兜底
refresh_active_run(gh_run)
mark_run_stale_if_needed(gh_run)
return {
"found": True,
+38 -3
View File
@@ -31,7 +31,11 @@ def staff_required(view_func):
@login_required
def my_builds(request):
builds = GithubRun.objects.filter(created_by=request.user)
builds = list(GithubRun.objects.filter(created_by=request.user))
# 回调为第一状态来源;本地 6 小时超时兜底 + 后台线程只读核对平台终态(不阻塞页面)
from .views import mark_stale_builds, schedule_refresh_for_runs
mark_stale_builds(builds)
schedule_refresh_for_runs(builds)
return render(request, 'my_builds.html', {'builds': builds})
@@ -48,7 +52,7 @@ def delete_my_build(request, run_id):
"""用户删除自己的已结束任务;进行中的任务不允许删除(避免与 GitHub 回调竞态)。"""
run = get_object_or_404(GithubRun, pk=run_id, created_by=request.user)
if not run.is_finished():
messages.error(request, "该任务仍在进行中,请等待结束(或先在 GitHub Actions 中取消)后再删除。")
messages.error(request, "该任务仍在进行中,请先点「停止」取消构建(或等待结束)后再删除。")
else:
_remove_run_artifacts(run)
run.delete()
@@ -56,6 +60,30 @@ def delete_my_build(request, run_id):
return redirect('my_builds')
@login_required
@require_POST
def stop_build(request, run_id):
"""停止构建:仅在本平台把任务标记为已取消,不调用 GitHub/Gitea 的远程取消接口。
构建平台上的流水线可能仍在继续运行,需要真正停止时请到对应平台的运行页手动 Cancel。
"""
run = get_object_or_404(GithubRun, pk=run_id, created_by=request.user)
if run.is_finished():
messages.info(request, "该任务已经结束,无需停止。")
else:
run.status = 'cancelled'
run.save(update_fields=['status', 'updated_at'])
tip = "已在本平台停止构建任务"
if run.github_run_id:
from . import build_backends
backend = build_backends.get_backend(run.backend or 'github')
tip += f"(仅本地标记;{run.backend_label}上的流水线若仍在运行,请前往运行页手动 Cancel:{backend.log_url(run.github_run_id)})"
else:
tip += "(仅本地标记)"
messages.success(request, tip)
return redirect('my_builds')
@login_required
@require_POST
def retry_build(request, run_id):
@@ -66,6 +94,9 @@ def retry_build(request, run_id):
if not run.is_finished():
messages.error(request, "该任务尚未结束,无需重试。")
return redirect('my_builds')
if run.status == 'success':
messages.info(request, "该任务已构建成功,请直接下载产物;如需重新生成请重新提交配置。")
return redirect('my_builds')
if not run.config_data:
messages.error(request, "该任务提交时未保存配置快照(较早的历史任务),无法一键重试,请重新填写配置后提交。")
return redirect('my_builds')
@@ -118,7 +149,11 @@ def delete_token(request, key):
@staff_required
def dashboard(request):
builds = GithubRun.objects.select_related('created_by').all()[:100]
builds = list(GithubRun.objects.select_related('created_by').all()[:100])
# 回调为第一状态来源;本地 6 小时超时兜底 + 后台线程只读核对平台终态(不阻塞页面)
from .views import mark_stale_builds, schedule_refresh_for_runs
mark_stale_builds(builds)
schedule_refresh_for_runs(builds)
stats = {
'total': GithubRun.objects.count(),
'in_progress': GithubRun.objects.exclude(
+44 -12
View File
@@ -104,7 +104,9 @@ action)。
### 1. Gitea 服务器与仓库
1. 部署 Gitea **1.27 或更高版本**(最低 1.23;1.27 随附 act_runner 2.0,对
artifact/cache 与第三方 action 的兼容性最好)。
artifact/cache 与第三方 action 的兼容性最好)。注意:列表页的一键「停止」依赖
Gitea 1.28+ 的取消运行 API;1.27 及更早版本点击停止会给出运行页链接,需到
Gitea 界面手动 Cancel。
2. 把本仓库整体推送到 Gitea(保留 `.gitea/` 目录与默认分支名,后台填写的
GITEA_BRANCH 必须与实际分支一致)。
3. 进入仓库 **Settings**,勾选 **Enable Repository Actions**。
@@ -123,11 +125,18 @@ Gitea 不提供云构建机,每个需要构建的平台都要自行注册 runn
| 标签 | 用途 | 运行方式 |
| --- | --- | --- |
| `ubuntu-22.04` | Linux x86_64、Android 构建 | Linux 主机 Docker 模式 |
| `ubuntu-22.04-arm` | Linux arm64、Android arm 构建 | arm64 Linux 主机 Docker 模式 |
| `windows-2022` | Windows x64/x86 构建 | Windows 主机 host 模式(真机/虚拟机) |
| `macos-14` | macOS arm64 构建 | Apple Silicon Mac host 模式 |
| `macos-15-intel` | macOS x64 构建 | Intel Mac host 模式 |
| `ubuntu-24.04` | Linux x86_64、Android(armv7/aarch64 走 NDK 交叉编译)、drm/appimage/flatpak x86_64 | Linux x86_64 主机 Docker 模式 |
| `ubuntu-24.04-arm` | Linux arm64 的 deb/flatpak 原生构建 | arm64 Linux 主机 Docker 模式(需另注册) |
| `windows-2022` | Windows x64/x86 构建 | Windows 主机 host 模式(真机/虚拟机,需另注册) |
| `macos-14` | macOS arm64 构建 | Apple Silicon Mac host 模式(需另注册) |
| `macos-15-intel` | macOS x64 构建 | Intel Mac host 模式(需另注册) |
另外大量轻量步骤(拉取加密配置、收尾回调、artifact 清理)运行在
`ubuntu-latest` 标签上——act_runner 默认自带的 `ubuntu-latest`/`docker`/`amd64`
标签即可承接,但必须确保有 runner 显式提供 `ubuntu-24.04` 标签,否则主构建会
一直处于 waiting。只注册一台 x86_64 Linux runner 时 Android 与 Linux x86_64
构建即可工作;arm64 Linux、Windows、macOS 必须各自再注册对应机器,对应任务在
机器就绪前会排队等待。
**Linux(Docker 模式,推荐)**:
@@ -141,13 +150,29 @@ docker run -d --name gitea-runner --restart always \
```
首次启动生成 `/data/config.yaml` 后,按需把 `labels` 改为上面的标签,例如
`- "ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04"`,再重启容器。
`- "ubuntu-24.04:docker://ghcr.io/catthehacker/ubuntu:act-24.04"`(act 官方
镜像还有 `ubuntu:act-latest` 可挂到 `ubuntu-latest` 标签),再重启容器。
工作流把 vcpkg 二进制缓存放在容器内 `/opt/vcpkg-cache`,如需跨任务复用,可在
job 容器配置中把该路径挂为持久卷。
**Windows(host 模式)**:在准备好构建环境的 Windows 机器上
(Git、PowerShell、Visual Studio 2022、vcpkg 位于 `C:\vcpkg`,并预创建
`D:\vcpkg-cache`;ImageMagick 等由工作流自动安装):
> **坑:注册地址必须是作业容器也能访问的地址。** 若 Gitea 与 act_runner 用
> docker-compose 部署,千万不要用 `http://gitea:3000` 这类 compose 内部服务名
> 注册(act_runner 容器自身可达,但 act_runner v3+ 的作业容器默认 host 网络,
> 解析不了该名)。否则上传/下载 artifact 时报
> `Failed to CreateArtifact: ... ENOTFOUND`。`GITEA_INSTANCE_URL` 用公网地址
> (如 `https://gitea.example.com`)或宿主机映射地址(如 `http://10.0.0.10:39630`),
> 改完编辑 `/data/config.yaml` 的 `runner.address` 重启即可,无需重新注册。
**Windows(host 模式)**:在准备好构建环境的 Windows 机器上构建。Gitea act
在 Windows 上默认 shell 是 bash,而本仓库工作流已显式把默认 shell 设为
**PowerShell 7(pwsh)**,因此机器上必须安装:
* **PowerShell 7+**(`pwsh` 必须在 PATH 中;仅有 Windows 自带的 5.1 不够)
* **Git for Windows**(提供 Git Bash,部分步骤显式使用 `shell: bash`)
* **Python 3**(decrypt-secrets composite action 需要,且能 `python -m pip`)
* **Visual Studio 2022**(含 C++ 桌面开发、MSVC、Windows SDK)
* **vcpkg** 固定位于 `C:\vcpkg`,并**预创建 `D:\vcpkg-cache`** 二进制缓存目录
* **Chocolatey / NuGet**(工作流用其安装 ImageMagick、WiX 等构建依赖)
```powershell
# 下载 https://gitea.com/gitea/act_runner/releases 的 act_runner.exe
@@ -158,8 +183,11 @@ job 容器配置中把该路径挂为持久卷。
确认稳定后再用任务计划程序/NSSM 注册为开机服务。
**macOS(host 模式)**:使用 Apple 硬件(arm64 对应 `macos-14`,Intel 对应
`macos-15-intel`,安装 Xcode 命令行工具),同样下载 act_runner 二进制后以
`macos-14:host` 标签注册并 launchd 守护。
`macos-15-intel`)。仅安装 Xcode 命令行工具不够,必须安装**完整版 Xcode**
(Flutter macOS 构建与代码签名需要),并具备 Homebrew、CocoaPods
(`sudo gem install cocoapods` 或 brew 版)、运行 runner 的用户可**免密
sudo**。同样下载 act_runner 二进制后以 `macos-14:host` 标签注册并 launchd
守护。
### 3. 内网网络说明
@@ -178,6 +206,10 @@ job 容器配置中把该路径挂为持久卷。
(`.rdgen-src/.github/patches/`),不依赖 raw.githubusercontent.com。
* rdgen 后台的 `GITEA_PROXY` 仅用于「服务端 → Gitea API」的调用;Gitea 通常
与 rdgen 在同一内网,留空直连即可。
* **runner 必须能反向访问 rdgen(`GENURL`)**:每个工作流结束时(无论成功、
失败还是取消)都会向 `${GENURL}/updategh` 回调最终状态。收不到回调的构建
会在 6 小时后被服务端误判为超时,因此 `GENURL` 要填 runner 实际可达的地址,
不能只填浏览器端能访问的地址。
### 4. 在 rdgen 侧启用