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 - name: install python deps
shell: bash shell: bash
run: | 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 - name: Decrypt and Mask
shell: python shell: python
env: env:
+5 -4
View File
@@ -20,9 +20,9 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: python shell: python
command: | command: |
import requests
import json import json
import time import time
import urllib.request
input_data = json.loads('${{ inputs.zip_url_json }}') input_data = json.loads('${{ inputs.zip_url_json }}')
url = f"{input_data['url']}/get_zip?filename={input_data['file']}" url = f"{input_data['url']}/get_zip?filename={input_data['file']}"
@@ -30,10 +30,11 @@ jobs:
for attempt in range(5): for attempt in range(5):
try: try:
print(f"Downloading (Attempt {attempt + 1})...") print(f"Downloading (Attempt {attempt + 1})...")
r = requests.get(url, timeout=20) # 仅使用标准库(urllib),兼容未预装 requests 的自建 runner 容器
r.raise_for_status() with urllib.request.urlopen(url, timeout=20) as r:
content = r.read()
with open('secrets.zip', 'wb') as f: with open('secrets.zip', 'wb') as f:
f.write(r.content) f.write(content)
break break
except Exception as e: except Exception as e:
if attempt < 4: if attempt < 4:
+35 -6
View File
@@ -105,6 +105,8 @@ jobs:
method: 'POST' method: 'POST'
customHeaders: '{"Content-Type": "application/json"}' customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}' data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main uses: jlumbroso/free-disk-space@main
@@ -195,17 +197,17 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
cd $(dirname $(dirname $(which flutter))) 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 id: setup-ndk
with: with:
ndk-version: ${{ env.NDK_VERSION }} ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true add-to-path: true
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with: with:
vcpkgDirectory: /opt/artifacts/vcpkg vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -599,7 +601,7 @@ jobs:
- uses: r0adkll/sign-android-release@v1 - uses: r0adkll/sign-android-release@v1
name: Sign app APK name: Sign app APK
continue-on-error: true continue-on-error: true
if: env.ANDROID_SIGNING_KEY != null if: env.ANDROID_SIGNING_KEY != ''
id: sign-rustdesk id: sign-rustdesk
with: with:
releaseDirectory: ./signed-apk releaseDirectory: ./signed-apk
@@ -639,7 +641,7 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 - name: send file to api server
if: ${{ env.rdgen == 'false' }} if: ${{ env.rdgen == 'false' }}
@@ -650,10 +652,11 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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: deploy:
needs: [build-rustdesk-android] needs: [build-rustdesk-android]
if: always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@@ -679,6 +682,8 @@ jobs:
method: 'POST' method: 'POST'
customHeaders: '{"Content-Type": "application/json"}' customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}' data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Set rdgen value - name: Set rdgen value
if: ${{ env.rdgen == 'true' }} if: ${{ env.rdgen == 'true' }}
@@ -690,6 +695,30 @@ jobs:
run: | run: |
echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV 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 - uses: geekyeggo/delete-artifact@v4
continue-on-error: true continue-on-error: true
with: with:
+66 -27
View File
@@ -65,7 +65,7 @@ jobs:
arch: x86_64, arch: x86_64,
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04, distro: ubuntu18.04,
on: ubuntu-22.04, on: ubuntu-24.04,
deb_arch: amd64, deb_arch: amd64,
vcpkg-triplet: x64-linux, vcpkg-triplet: x64-linux,
} }
@@ -73,7 +73,7 @@ jobs:
arch: aarch64, arch: aarch64,
target: aarch64-unknown-linux-gnu, target: aarch64-unknown-linux-gnu,
distro: ubuntu18.04, distro: ubuntu18.04,
on: ubuntu-22.04-arm, on: ubuntu-24.04-arm,
deb_arch: arm64, deb_arch: arm64,
vcpkg-triplet: arm64-linux, vcpkg-triplet: arm64-linux,
} }
@@ -172,7 +172,7 @@ jobs:
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' 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: with:
vcpkgDirectory: /opt/artifacts/vcpkg vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -402,7 +402,9 @@ jobs:
with: with:
arch: ${{ matrix.job.arch }} arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }} 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: | setup: |
ls -l "${PWD}" ls -l "${PWD}"
ls -l /opt/artifacts/vcpkg/installed ls -l /opt/artifacts/vcpkg/installed
@@ -533,7 +535,9 @@ jobs:
pushd /opt/flutter pushd /opt/flutter
;; ;;
esac 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 popd
fi fi
@@ -686,10 +690,10 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 --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 -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 --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 -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 --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 -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 }}.pkg.tar.zst" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true
- name: send file to api server - name: send file to api server
if: ${{ env.rdgen == 'false' }} if: ${{ env.rdgen == 'false' }}
@@ -700,10 +704,10 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 --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 -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 --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 -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 --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 -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 }}.pkg.tar.zst" ${{ env.apiServer }}/api/save_custom_client || true
- name: Upload deb - name: Upload deb
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -715,7 +719,7 @@ jobs:
build-rustdesk-linux-drm: build-rustdesk-linux-drm:
needs: [generate-bridge-linux, setup] needs: [generate-bridge-linux, setup]
name: build rustdesk linux drm x86_64 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 # 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 # 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 # assert_staged_binary_is_drm, so a versioned build dies at
@@ -824,7 +828,7 @@ jobs:
path: ./ path: ./
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with: with:
vcpkgDirectory: /opt/artifacts/vcpkg vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -1072,7 +1076,9 @@ jobs:
with: with:
arch: x86_64 arch: x86_64
distro: ubuntu18.04 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: | setup: |
ls -l "${PWD}" ls -l "${PWD}"
ls -l /opt/artifacts/vcpkg/installed ls -l /opt/artifacts/vcpkg/installed
@@ -1165,7 +1171,9 @@ jobs:
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
pushd /opt/flutter 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 popd
fi fi
@@ -1237,7 +1245,7 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 - name: send file to api server
if: ${{ env.rdgen == 'false' && env.DRM_SUPPORTED == 'true' }} if: ${{ env.rdgen == 'false' && env.DRM_SUPPORTED == 'true' }}
@@ -1248,7 +1256,7 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 - name: Upload deb
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -1260,7 +1268,7 @@ jobs:
build-appimage: build-appimage:
name: Build appimage ${{ matrix.job.target }} name: Build appimage ${{ matrix.job.target }}
needs: [build-rustdesk-linux] needs: [build-rustdesk-linux]
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -1388,7 +1396,7 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 - name: send file to api server
if: ${{ env.rdgen == 'false' }} if: ${{ env.rdgen == 'false' }}
@@ -1399,7 +1407,7 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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: build-flatpak:
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }} name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
@@ -1413,7 +1421,7 @@ jobs:
- { - {
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
distro: ubuntu22.04, distro: ubuntu22.04,
on: ubuntu-22.04, on: ubuntu-24.04,
arch: x86_64, arch: x86_64,
suffix: "", suffix: "",
} }
@@ -1421,7 +1429,7 @@ jobs:
target: aarch64-unknown-linux-gnu, target: aarch64-unknown-linux-gnu,
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub" # try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
distro: ubuntu22.04, distro: ubuntu22.04,
on: ubuntu-22.04-arm, on: ubuntu-24.04-arm,
arch: aarch64, arch: aarch64,
suffix: "", suffix: "",
} }
@@ -1473,7 +1481,9 @@ jobs:
with: with:
arch: ${{ matrix.job.arch }} arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }} 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: | setup: |
ls -l "${PWD}" ls -l "${PWD}"
dockerRunArgs: | dockerRunArgs: |
@@ -1529,7 +1539,7 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 - name: send file to api server
if: ${{ env.rdgen == 'false' }} if: ${{ env.rdgen == 'false' }}
@@ -1540,7 +1550,7 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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: deploy:
needs: [build-rustdesk-linux,build-flatpak,build-appimage] needs: [build-rustdesk-linux,build-flatpak,build-appimage]
@@ -1570,6 +1580,8 @@ jobs:
method: 'POST' method: 'POST'
customHeaders: '{"Content-Type": "application/json"}' customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}' data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Set rdgen value - name: Set rdgen value
if: ${{ env.rdgen == 'true' }} if: ${{ env.rdgen == 'true' }}
@@ -1581,6 +1593,33 @@ jobs:
run: | run: |
echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV 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 - uses: geekyeggo/delete-artifact@v4
continue-on-error: true continue-on-error: true
with: with:
+60 -8
View File
@@ -101,6 +101,8 @@ jobs:
method: 'POST' method: 'POST'
customHeaders: '{"Content-Type": "application/json"}' customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}' data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Checkout source code - name: Checkout source code
@@ -294,7 +296,7 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
cd $(dirname $(dirname $(which flutter))) 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 - name: Workaround for flutter issue
shell: bash shell: bash
@@ -500,7 +502,7 @@ jobs:
path: ./ path: ./
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with: with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false doNotCache: false
@@ -550,7 +552,7 @@ jobs:
ls -l "$APP/Contents/MacOS/custom_.txt" ls -l "$APP/Contents/MacOS/custom_.txt"
- name: Install rcodesign tool - name: Install rcodesign tool
if: env.MACOS_P12_BASE64 != null if: env.MACOS_P12_BASE64 != ''
shell: bash shell: bash
run: | run: |
pushd /tmp pushd /tmp
@@ -584,7 +586,11 @@ jobs:
continue-on-error: false continue-on-error: false
shell: bash shell: bash
run: | 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" mkdir -p "$ASSETS_DIR"
if [ -f "$ASSETS_DIR/icon.svg" ]; then if [ -f "$ASSETS_DIR/icon.svg" ]; then
mv "$ASSETS_DIR/icon.svg" "$ASSETS_DIR/icon.svg.bak" mv "$ASSETS_DIR/icon.svg" "$ASSETS_DIR/icon.svg.bak"
@@ -600,7 +606,11 @@ jobs:
continue-on-error: false continue-on-error: false
shell: bash shell: bash
run: | 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" mkdir -p "$ASSETS_DIR"
curl -k -L --tlsv1.2 --proto =https --ssl-reqd \ curl -k -L --tlsv1.2 --proto =https --ssl-reqd \
-H "User-Agent: Mozilla/5.0" \ -H "User-Agent: Mozilla/5.0" \
@@ -696,7 +706,7 @@ jobs:
- name: Create DMG - name: Create DMG
run: | 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 # Print directory contents for debugging
echo "Directory contents:" echo "Directory contents:"
ls -la ls -la
@@ -745,7 +755,7 @@ jobs:
if: ${{ env.rdgen == 'true' }} if: ${{ env.rdgen == 'true' }}
shell: bash shell: bash
run: | 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 "Content-Type: multipart/form-data" \
-H "Authorization: Bearer ${{ env.token }}" \ -H "Authorization: Bearer ${{ env.token }}" \
-F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \ -F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \
@@ -757,7 +767,7 @@ jobs:
if: ${{ env.rdgen == 'false' }} if: ${{ env.rdgen == 'false' }}
shell: bash shell: bash
run: | 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 "Content-Type: multipart/form-data" \
-H "Authorization: Bearer ${{ env.token }}" \ -H "Authorization: Bearer ${{ env.token }}" \
-F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \ -F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \
@@ -769,6 +779,48 @@ jobs:
continue-on-error: true continue-on-error: true
if: always() if: always()
steps: 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 - name: Delete secrets artifact
uses: geekyeggo/delete-artifact@v4 uses: geekyeggo/delete-artifact@v4
with: with:
+55 -6
View File
@@ -28,7 +28,7 @@ env:
# for arm64 linux because official Dart SDK does not work # for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}" 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 # vcpkg version: 2025.08.27
# If we change the `VCPKG COMMIT_ID`, please remember: # If we change the `VCPKG COMMIT_ID`, please remember:
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`. # 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 }}) name: ${{ matrix.job.target }} (${{ matrix.job.os }})
needs: setup needs: setup
runs-on: ${{ matrix.job.os }} 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. # Temporarily disable this action due to additional test is needed.
# if: false # if: false
strategy: strategy:
@@ -95,6 +100,8 @@ jobs:
method: 'POST' method: 'POST'
customHeaders: '{"Content-Type": "application/json"}' customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}' data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Set rdgen value - name: Set rdgen value
@@ -277,7 +284,7 @@ jobs:
prefix-key: ${{ matrix.job.os }}-sciter prefix-key: ${{ matrix.job.os }}-sciter
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with: with:
vcpkgDirectory: C:\vcpkg vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -422,9 +429,9 @@ jobs:
cargo build --target ${{ matrix.job.target }} --features inline,vram,hwcodec --release --bins cargo build --target ${{ matrix.job.target }} --features inline,vram,hwcodec --release --bins
mkdir -p ./Release mkdir -p ./Release
mv ./target/${{ matrix.job.target }}/release/rustdesk.exe ./Release/rustdesk.exe 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 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 unzip usbmmidd_v2.zip
# Do not remove x64 files, because the user may run the 32bit version on a 64bit system. # 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, # 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' }} if: ${{ env.rdgen == 'true' }}
shell: bash shell: bash
run: | 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 - name: send file to api server
if: ${{ env.rdgen == 'false' }} if: ${{ env.rdgen == 'false' }}
shell: bash shell: bash
run: | 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: cleanup:
needs: [build-for-windows-sciter] needs: [build-for-windows-sciter]
@@ -548,6 +555,48 @@ jobs:
continue-on-error: true continue-on-error: true
if: always() if: always()
steps: 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 - name: Delete secrets artifact
uses: geekyeggo/delete-artifact@v4 uses: geekyeggo/delete-artifact@v4
with: with:
+57 -7
View File
@@ -28,7 +28,7 @@ env:
# for arm64 linux because official Dart SDK does not work # for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}" 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 version: 2024.07.12
VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
@@ -70,6 +70,11 @@ jobs:
name: Build Windows name: Build Windows
needs: [build-RustDeskTempTopMostWindow, generate-bridge, setup] needs: [build-RustDeskTempTopMostWindow, generate-bridge, setup]
runs-on: ${{ matrix.job.os }} 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: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -107,6 +112,8 @@ jobs:
method: 'POST' method: 'POST'
customHeaders: '{"Content-Type": "application/json"}' customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}' data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Set rdgen value - name: Set rdgen value
@@ -385,7 +392,7 @@ jobs:
- name: Patch flutter - name: Patch flutter
shell: bash shell: bash
run: | 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))) cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff [[ "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 }} prefix-key: ${{ matrix.job.os }}
- name: Setup vcpkg with Github Actions binary cache - name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@v11 uses: lukka/run-vcpkg@1737e223b9c3f4fb07a7cf1cfa123cc243fcc0dd # v11 pinned to node20 runtime
with: with:
vcpkgDirectory: C:\vcpkg vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -723,8 +730,8 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 --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 -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 }}.msi" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true
- name: send file to api server - name: send file to api server
if: ${{ env.rdgen == 'false' }} if: ${{ env.rdgen == 'false' }}
@@ -735,8 +742,8 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: bash shell: bash
command: | 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 --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 -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 }}.msi" ${{ env.apiServer }}/api/save_custom_client || true
cleanup: cleanup:
needs: [build-for-windows-flutter] needs: [build-for-windows-flutter]
@@ -744,6 +751,49 @@ jobs:
continue-on-error: true continue-on-error: true
if: always() if: always()
steps: 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 - name: Delete secrets artifact
uses: geekyeggo/delete-artifact@v4 uses: geekyeggo/delete-artifact@v4
with: with:
+1 -1
View File
@@ -28,7 +28,7 @@ env:
# for arm64 linux because official Dart SDK does not work # for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9" FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}" 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 version: 2024.07.12
VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
@@ -27,6 +27,13 @@ on:
required: true required: true
type: string type: string
default: 'Windows10' default: 'Windows10'
secrets:
ZIP_PASSWORD:
description: 'Password for the encrypted secrets zip'
required: true
GENURL:
description: 'rdgen platform base URL'
required: true
env: env:
project_path: WindowInjection/WindowInjection.vcxproj project_path: WindowInjection/WindowInjection.vcxproj
@@ -63,6 +70,8 @@ jobs:
method: 'POST' method: 'POST'
customHeaders: '{"Content-Type": "application/json"}' customHeaders: '{"Content-Type": "application/json"}'
data: '{"uuid": "${{ env.uuid }}"}' data: '{"uuid": "${{ env.uuid }}"}'
timeout: '15000'
retry: '3'
- name: Download the source code - name: Download the source code
run: | run: |
@@ -78,7 +87,7 @@ jobs:
shell: pwsh shell: pwsh
command: | command: |
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796 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 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 Copy-Item ../.github/patches/privacyScreen.py privacyScreen.py
python privacyScreen.py python privacyScreen.py
@@ -19,9 +19,9 @@ jobs:
retry_wait_seconds: 15 retry_wait_seconds: 15
shell: python shell: python
command: | command: |
import requests
import json import json
import time import time
import urllib.request
input_data = json.loads('${{ inputs.zip_url_json }}') input_data = json.loads('${{ inputs.zip_url_json }}')
url = f"{input_data['url']}/get_zip?filename={input_data['file']}" url = f"{input_data['url']}/get_zip?filename={input_data['file']}"
@@ -29,10 +29,11 @@ jobs:
for attempt in range(5): for attempt in range(5):
try: try:
print(f"Downloading (Attempt {attempt + 1})...") print(f"Downloading (Attempt {attempt + 1})...")
r = requests.get(url, timeout=20) # 仅使用标准库(urllib),无需预装 requests,兼容各类 runner 环境
r.raise_for_status() with urllib.request.urlopen(url, timeout=20) as r:
content = r.read()
with open('secrets.zip', 'wb') as f: with open('secrets.zip', 'wb') as f:
f.write(r.content) f.write(content)
break break
except Exception as e: except Exception as e:
if attempt < 4: 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 - Avoid special characters or non-English characters in app name and file name
- Build time is currently 30 - 45 minutes - 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('', views.generator_view, name='generator'),
path('my-builds/', web_views.my_builds, name='my_builds'), 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>/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('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/', web_views.saved_configs, name='saved_configs'),
path('configs/save/', web_views.save_config, name='save_config'), path('configs/save/', web_views.save_config, name='save_config'),
+9 -7
View File
@@ -20,9 +20,10 @@ class DispatchResult:
@dataclass @dataclass
class RunInfo: class RunInfo:
finished: bool # 是否已进入终态 """平台单次运行的状态快照。"""
status: str = '' # 终态结论:success/failure/cancelled/skipped;未结束为进行中状态 finished: bool = False # 是否已结束(拿到终态结论)
html_url: str = '' status: str = '' # finished=True 时为 rdgen 内部终态码
html_url: str = '' # 平台上的运行详情页地址
# rdgen 目标平台 -> 工作流文件名(两平台同名) # rdgen 目标平台 -> 工作流文件名(两平台同名)
@@ -57,13 +58,14 @@ class BuildBackend:
"""触发工作流。inputs 为完整输入字典(GitHub/Gitea 均为 {ref, inputs})。""" """触发工作流。inputs 为完整输入字典(GitHub/Gitea 均为 {ref, inputs})。"""
raise NotImplementedError raise NotImplementedError
def get_run(self, run_id, *, timeout=12):
"""查询单次运行;网络异常或非 200 返回 None(调用方按「仍在进行」处理)。"""
raise NotImplementedError
def log_url(self, run_id) -> str: def log_url(self, run_id) -> str:
raise NotImplementedError raise NotImplementedError
def get_run(self, run_id, *, timeout=8):
"""只读查询单次运行:已结束返回 RunInfo(finished=True, status=内部终态码);
仍在途、查询失败或不支持时返回 None(调用方按「仍在进行」处理)。"""
return None
# ---- 后台连接测试 ---- # ---- 后台连接测试 ----
def test_connection(self) -> dict: def test_connection(self) -> dict:
"""返回 {'ok': bool, 'message': 中文说明},不抛异常。""" """返回 {'ok': bool, 'message': 中文说明},不抛异常。"""
+25 -11
View File
@@ -4,22 +4,26 @@ API 与 GitHub Actions 几乎同形(已对照 Gitea 官方 Swagger 核实)
- 触发:POST /api/v1/repos/{owner}/{repo}/actions/workflows/{file}/dispatches - 触发:POST /api/v1/repos/{owner}/{repo}/actions/workflows/{file}/dispatches
?return_run_details=true,body {ref, inputs},200 返回 ?return_run_details=true,body {ref, inputs},200 返回
{workflow_run_id, html_url, run_url},不传该参数为 204; {workflow_run_id, html_url, run_url},不传该参数为 204;
- 状态:GET /api/v1/repos/{owner}/{repo}/actions/runs/{id},
status/conclusion 字段仿 GitHub;
- 鉴权:Authorization: token <PAT>。 - 鉴权:Authorization: token <PAT>。
注意:本平台不主动取消 Gitea 上的运行,停止/删除只作用于本平台本地记录。
构建状态以工作流结束时的回调(update_github_run)为第一来源;get_run 仅做
只读终态核对,兜底回调丢失(脚本未跑到收尾或 GENURL 不可达)的情况。
""" """
from .. import app_settings, ghnet from .. import app_settings, ghnet
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
# Gitea 未结束状态(blocked=等待审批,waiting=等待 runner) # 部分 Gitea 版本 run.status 直接返回终态(无 completed+conclusion 包裹)
_GITEA_ACTIVE = {'queued', 'in_progress', 'waiting', 'blocked', 'requested', 'pending'} _GITEA_TERMINAL = {'success', 'failure', 'cancelled', 'skipped', 'timed_out', 'stopped', 'dead'}
# Gitea 结论 -> rdgen 内部状态 # Gitea 结论 -> rdgen 内部终态
_CONCLUSION_MAP = { _GITEA_CONCLUSION_MAP = {
'success': 'success', 'success': 'success',
'failure': 'failure', 'failure': 'failure',
'cancelled': 'cancelled', 'cancelled': 'cancelled',
'skipped': 'skipped', 'skipped': 'skipped',
'timed_out': 'timed_out', 'timed_out': 'timed_out',
'stopped': 'cancelled',
'dead': 'failure',
} }
@@ -140,8 +144,8 @@ class GiteaBackend(BuildBackend):
print(f'查询 Gitea 最新运行失败:{e}') print(f'查询 Gitea 最新运行失败:{e}')
return None 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}') url = self._api(f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}')
try: try:
resp = ghnet.get( resp = ghnet.get(
@@ -153,18 +157,28 @@ class GiteaBackend(BuildBackend):
return None return None
if resp.status_code != 200: if resp.status_code != 200:
return None return None
try:
data = resp.json() data = resp.json()
except ValueError:
return None
html_url = data.get('html_url') or self.log_url(run_id) html_url = data.get('html_url') or self.log_url(run_id)
status = (data.get('status') or '').lower() status = (data.get('status') or '').lower()
conclusion = (data.get('conclusion') or '').lower() conclusion = (data.get('conclusion') or '').lower()
if status == 'completed': if status == 'completed':
return RunInfo( return RunInfo(
finished=True, finished=True,
status=_CONCLUSION_MAP.get(conclusion, 'failure'), status=_GITEA_CONCLUSION_MAP.get(conclusion, 'failure'),
html_url=html_url, html_url=html_url,
) )
# blocked(等待审批)等也算进行中 # 部分 Gitea 版本直接在 status 返回终态(无 completed 包裹)
return RunInfo(finished=False, status=status or 'in_progress', html_url=html_url) 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): def log_url(self, run_id):
+29 -10
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 .. import app_settings, ghnet
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
API = 'https://api.github.com' API = 'https://api.github.com'
# 未进入 completed 的状态一律视为进行中 # GitHub conclusion -> rdgen 内部终态;无人值守构建不会有人处理审批,
_GITHUB_ACTIVE = {'queued', 'in_progress', 'waiting', 'requested', 'pending'} # action_required/neutral/stale/startup_failure 等异常结论一律按失败呈现
_GH_CONCLUSION_MAP = {
'success': 'success',
'failure': 'failure',
'cancelled': 'cancelled',
'skipped': 'skipped',
'timed_out': 'timed_out',
}
class GitHubBackend(BuildBackend): class GitHubBackend(BuildBackend):
@@ -56,8 +68,8 @@ class GitHubBackend(BuildBackend):
pass pass
return DispatchResult(run_id=data.get('workflow_run_id'), html_url=data.get('html_url') or '') 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') user = app_settings.get_value('GHUSER')
repo = app_settings.get_value('REPONAME') repo = app_settings.get_value('REPONAME')
url = f'{API}/repos/{user}/{repo}/actions/runs/{run_id}' url = f'{API}/repos/{user}/{repo}/actions/runs/{run_id}'
@@ -68,12 +80,19 @@ class GitHubBackend(BuildBackend):
return None return None
if resp.status_code != 200: if resp.status_code != 200:
return None return None
try:
data = resp.json() data = resp.json()
if data.get('status') == 'completed': except ValueError:
return RunInfo(finished=True, status=data.get('conclusion') or 'failure', return None
html_url=data.get('html_url') or self.log_url(run_id)) # 只关心终态:在途(queued/in_progress/waiting)返回 None,交给回调与下次核对
return RunInfo(finished=False, status=data.get('status') or 'in_progress', if data.get('status') != 'completed':
html_url=data.get('html_url') or self.log_url(run_id)) 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): 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); } .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 { .page {
flex: 1; 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-secondary:hover { border-color: var(--primary-border); color: var(--primary); }
.btn-danger { background: #fff; color: var(--danger); border-color: #f3c2c2; } .btn-danger { background: #fff; color: var(--danger); border-color: #f3c2c2; }
.btn-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); } .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 { min-height: 30px; padding: 0 12px; font-size: 12.5px; border-radius: 6px; }
.btn-sm .icon { font-size: 13px; } .btn-sm .icon { font-size: 13px; }
.btn-block { width: 100%; } .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; } .btn-row { display: flex; gap: 10px; flex-wrap: wrap; }
.inline-form { display: inline-flex; vertical-align: middle; } .inline-form { display: inline-flex; vertical-align: middle; }
.table .btn + .btn, .table .btn + .inline-form { margin-left: 8px; } .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 { .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"> <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"/> <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>
<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"> <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"/> <circle cx="12" cy="12" r="10"/><path d="m8.5 12 2.5 2.5 5-5"/>
</symbol> </symbol>
@@ -108,6 +111,9 @@
<circle cx="12" cy="12" r="9"/> <circle cx="12" cy="12" r="9"/>
<path d="M12 7v5l3 2"/> <path d="M12 7v5l3 2"/>
</symbol> </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"> <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"/> <ellipse cx="12" cy="5" rx="8" ry="3"/>
<path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5"/> <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>{{ 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><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 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" <a class="btn btn-secondary btn-sm"
href="/check_for_file?filename={{ run.filename|urlencode }}&uuid={{ run.uuid|urlencode }}&platform={{ run.platform|urlencode }}">查看</a> 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 %}" <form method="post" class="inline-form" action="{% url 'delete_build' run.id %}"
@@ -59,6 +60,7 @@
<svg class="icon"><use href="#i-trash"/></svg> 删除 <svg class="icon"><use href="#i-trash"/></svg> 删除
</button> </button>
</form> </form>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% 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.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><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 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" <a class="btn btn-secondary btn-sm"
href="/check_for_file?filename={{ run.filename|urlencode }}&uuid={{ run.uuid|urlencode }}&platform={{ run.platform|urlencode }}"> 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> </a>
{% if run.github_log_url %} {% if run.github_log_url %}
<a class="btn btn-secondary btn-sm" href="{{ run.github_log_url }}" target="_blank" rel="noopener"> <a class="btn btn-secondary btn-sm" href="{{ run.github_log_url }}" target="_blank" rel="noopener">
<svg class="icon"><use href="#i-external"/></svg> 日志 <svg class="icon"><use href="#i-external"/></svg> 日志
</a> </a>
{% endif %} {% 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 %}" <form method="post" class="inline-form" action="{% url 'retry_build' run.id %}"
onsubmit="return confirm('将使用该任务的原始配置重新提交一次构建(原任务记录保留),是否继续?');"> onsubmit="return confirm('将使用该任务的原始配置重新提交一次构建(原任务记录保留),是否继续?');">
{% csrf_token %} {% csrf_token %}
@@ -51,6 +52,15 @@
</button> </button>
</form> </form>
{% endif %} {% 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 %} {% if run.is_finished %}
<form method="post" class="inline-form" action="{% url 'delete_my_build' run.id %}" <form method="post" class="inline-form" action="{% url 'delete_my_build' run.id %}"
onsubmit="return confirm('确定删除任务「{{ run.filename|default:run.short_uuid|escapejs }}」吗?将同时删除服务器上的安装包文件,且不可恢复。');"> onsubmit="return confirm('确定删除任务「{{ run.filename|default:run.short_uuid|escapejs }}」吗?将同时删除服务器上的安装包文件,且不可恢复。');">
@@ -60,6 +70,7 @@
</button> </button>
</form> </form>
{% endif %} {% endif %}
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
+123 -8
View File
@@ -2,7 +2,10 @@ import io
import json import json
import os import os
import re import re
import threading
import time
import uuid import uuid
from datetime import timedelta
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
@@ -22,12 +25,128 @@ from django.http import (
Http404, Http404,
) )
from django.shortcuts import render from django.shortcuts import render
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from . import app_settings, build_backends, versions as rdeskVersions from . import app_settings, build_backends, versions as rdeskVersions
from .forms import GenerateForm from .forms import GenerateForm
from .models import GithubRun, STATUS_LABELS, STATUS_BADGE 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): def _webhook_denied(request):
"""配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。""" """配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。"""
@@ -390,14 +509,10 @@ def _get_run_status(uuid_val):
backend = build_backends.get_backend(gh_run.backend or 'github') 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 '' 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: # 状态以构建脚本结束回调为准;此处先做平台终态只读核对(20 秒节流、6 秒超时,
try: # 回调丢失时能及时发现平台侧已失败/取消),再做纯本地的 6 小时超时兜底
info = backend.get_run(gh_run.github_run_id) refresh_active_run(gh_run)
if info is not None and info.finished: mark_run_stale_if_needed(gh_run)
gh_run.status = info.status
gh_run.save()
except Exception as e:
print(f"查询构建状态出错:{e}")
return { return {
"found": True, "found": True,
+38 -3
View File
@@ -31,7 +31,11 @@ def staff_required(view_func):
@login_required @login_required
def my_builds(request): 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}) return render(request, 'my_builds.html', {'builds': builds})
@@ -48,7 +52,7 @@ def delete_my_build(request, run_id):
"""用户删除自己的已结束任务;进行中的任务不允许删除(避免与 GitHub 回调竞态)。""" """用户删除自己的已结束任务;进行中的任务不允许删除(避免与 GitHub 回调竞态)。"""
run = get_object_or_404(GithubRun, pk=run_id, created_by=request.user) run = get_object_or_404(GithubRun, pk=run_id, created_by=request.user)
if not run.is_finished(): if not run.is_finished():
messages.error(request, "该任务仍在进行中,请等待结束(或先在 GitHub Actions 中取消)后再删除。") messages.error(request, "该任务仍在进行中,请先点「停止」取消构建(或等待结束)后再删除。")
else: else:
_remove_run_artifacts(run) _remove_run_artifacts(run)
run.delete() run.delete()
@@ -56,6 +60,30 @@ def delete_my_build(request, run_id):
return redirect('my_builds') 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 @login_required
@require_POST @require_POST
def retry_build(request, run_id): def retry_build(request, run_id):
@@ -66,6 +94,9 @@ def retry_build(request, run_id):
if not run.is_finished(): if not run.is_finished():
messages.error(request, "该任务尚未结束,无需重试。") messages.error(request, "该任务尚未结束,无需重试。")
return redirect('my_builds') return redirect('my_builds')
if run.status == 'success':
messages.info(request, "该任务已构建成功,请直接下载产物;如需重新生成请重新提交配置。")
return redirect('my_builds')
if not run.config_data: if not run.config_data:
messages.error(request, "该任务提交时未保存配置快照(较早的历史任务),无法一键重试,请重新填写配置后提交。") messages.error(request, "该任务提交时未保存配置快照(较早的历史任务),无法一键重试,请重新填写配置后提交。")
return redirect('my_builds') return redirect('my_builds')
@@ -118,7 +149,11 @@ def delete_token(request, key):
@staff_required @staff_required
def dashboard(request): 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 = { stats = {
'total': GithubRun.objects.count(), 'total': GithubRun.objects.count(),
'in_progress': GithubRun.objects.exclude( 'in_progress': GithubRun.objects.exclude(
+44 -12
View File
@@ -104,7 +104,9 @@ action)。
### 1. Gitea 服务器与仓库 ### 1. Gitea 服务器与仓库
1. 部署 Gitea **1.27 或更高版本**(最低 1.23;1.27 随附 act_runner 2.0,对 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/` 目录与默认分支名,后台填写的 2. 把本仓库整体推送到 Gitea(保留 `.gitea/` 目录与默认分支名,后台填写的
GITEA_BRANCH 必须与实际分支一致)。 GITEA_BRANCH 必须与实际分支一致)。
3. 进入仓库 **Settings**,勾选 **Enable Repository Actions**。 3. 进入仓库 **Settings**,勾选 **Enable Repository Actions**。
@@ -123,11 +125,18 @@ Gitea 不提供云构建机,每个需要构建的平台都要自行注册 runn
| 标签 | 用途 | 运行方式 | | 标签 | 用途 | 运行方式 |
| --- | --- | --- | | --- | --- | --- |
| `ubuntu-22.04` | Linux x86_64、Android 构建 | Linux 主机 Docker 模式 | | `ubuntu-24.04` | Linux x86_64、Android(armv7/aarch64 走 NDK 交叉编译)、drm/appimage/flatpak x86_64 | Linux x86_64 主机 Docker 模式 |
| `ubuntu-22.04-arm` | Linux arm64、Android arm 构建 | arm64 Linux 主机 Docker 模式 | | `ubuntu-24.04-arm` | Linux arm64 的 deb/flatpak 原生构建 | arm64 Linux 主机 Docker 模式(需另注册) |
| `windows-2022` | Windows x64/x86 构建 | Windows 主机 host 模式(真机/虚拟机) | | `windows-2022` | Windows x64/x86 构建 | Windows 主机 host 模式(真机/虚拟机,需另注册) |
| `macos-14` | macOS arm64 构建 | Apple Silicon Mac host 模式 | | `macos-14` | macOS arm64 构建 | Apple Silicon Mac host 模式(需另注册) |
| `macos-15-intel` | macOS x64 构建 | Intel 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 模式,推荐)**: **Linux(Docker 模式,推荐)**:
@@ -141,13 +150,29 @@ docker run -d --name gitea-runner --restart always \
``` ```
首次启动生成 `/data/config.yaml` 后,按需把 `labels` 改为上面的标签,例如 首次启动生成 `/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`,如需跨任务复用,可在 工作流把 vcpkg 二进制缓存放在容器内 `/opt/vcpkg-cache`,如需跨任务复用,可在
job 容器配置中把该路径挂为持久卷。 job 容器配置中把该路径挂为持久卷。
**Windows(host 模式)**:在准备好构建环境的 Windows 机器上 > **坑:注册地址必须是作业容器也能访问的地址。** 若 Gitea 与 act_runner 用
(Git、PowerShell、Visual Studio 2022、vcpkg 位于 `C:\vcpkg`,并预创建 > docker-compose 部署,千万不要用 `http://gitea:3000` 这类 compose 内部服务名
`D:\vcpkg-cache`;ImageMagick 等由工作流自动安装): > 注册(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 ```powershell
# 下载 https://gitea.com/gitea/act_runner/releases 的 act_runner.exe # 下载 https://gitea.com/gitea/act_runner/releases 的 act_runner.exe
@@ -158,8 +183,11 @@ job 容器配置中把该路径挂为持久卷。
确认稳定后再用任务计划程序/NSSM 注册为开机服务。 确认稳定后再用任务计划程序/NSSM 注册为开机服务。
**macOS(host 模式)**:使用 Apple 硬件(arm64 对应 `macos-14`,Intel 对应 **macOS(host 模式)**:使用 Apple 硬件(arm64 对应 `macos-14`,Intel 对应
`macos-15-intel`,安装 Xcode 命令行工具),同样下载 act_runner 二进制后以 `macos-15-intel`)。仅安装 Xcode 命令行工具不够,必须安装**完整版 Xcode**
`macos-14:host` 标签注册并 launchd 守护。 (Flutter macOS 构建与代码签名需要),并具备 Homebrew、CocoaPods
(`sudo gem install cocoapods` 或 brew 版)、运行 runner 的用户可**免密
sudo**。同样下载 act_runner 二进制后以 `macos-14:host` 标签注册并 launchd
守护。
### 3. 内网网络说明 ### 3. 内网网络说明
@@ -178,6 +206,10 @@ job 容器配置中把该路径挂为持久卷。
(`.rdgen-src/.github/patches/`),不依赖 raw.githubusercontent.com。 (`.rdgen-src/.github/patches/`),不依赖 raw.githubusercontent.com。
* rdgen 后台的 `GITEA_PROXY` 仅用于「服务端 → Gitea API」的调用;Gitea 通常 * rdgen 后台的 `GITEA_PROXY` 仅用于「服务端 → Gitea API」的调用;Gitea 通常
与 rdgen 在同一内网,留空直连即可。 与 rdgen 在同一内网,留空直连即可。
* **runner 必须能反向访问 rdgen(`GENURL`)**:每个工作流结束时(无论成功、
失败还是取消)都会向 `${GENURL}/updategh` 回调最终状态。收不到回调的构建
会在 6 小时后被服务端误判为超时,因此 `GENURL` 要填 runner 实际可达的地址,
不能只填浏览器端能访问的地址。
### 4. 在 rdgen 侧启用 ### 4. 在 rdgen 侧启用