chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:22 +08:00
commit 4c395b06e4
1964 changed files with 333726 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
# GPT Workflows
本项目使用 GPT 驱动的 GitHub Actions 工作流辅助 PR 审查。
## 架构概览
```
.github/
├── actions/ # 公共 Composite Actions
│ ├── gather-pr-diff/action.yml # 收集 PR diff 和变更文件列表
│ ├── read-file-contents/action.yml # 按优先级读取变更文件内容
│ └── call-openai/action.yml # 调用 OpenAI API(含重试逻辑)
└── workflows/
├── gpt-review.yml # 代码质量审查
├── gpt-pr-assessment.yml # PR 价值评估
└── pr-checks.yml # PR 检查入口(触发 gpt-review + gpt-pr-assessment
```
## 两个 GPT 工作流对比
| | GPT Review | GPT PR Assessment |
| ------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| **目的** | 代码质量审查(bug、安全、性能) | 维护者价值评估(合并优先级、风险) |
| **角色** | 代码审查专家 | 项目维护者 / 技术负责人 |
| **触发** | PR 创建时自动触发(via pr-checks.yml+ 手动触发(workflow_dispatch | 外部贡献者 PR 自动触发(via pr-checks.yml+ 手动触发(workflow_dispatch |
| **额外数据** | 无 | 关联 Issue 内容 |
| **输出方式** | PR ReviewcreateReview | Issue Comment(可更新,不重复创建) |
| **输出模板** | 按严重性分级的问题列表 | 7 维度结构化评估报告 |
## 公共 Actions
三个 Composite Action 封装了两个工作流的公共逻辑,避免重复代码:
### `gather-pr-diff`
收集 PR 的 diff 和变更文件列表。
- **输入**: `pr_number`(手动触发时需要)
- **输出**: `skip`, `pr_number`, `additions`, `deletions`, `total_lines`, `file_count`, `diff_truncated`
- **临时文件**: `pr_diff.txt`, `file_list.json`(写入 `RUNNER_TEMP`
### `read-file-contents`
按优先级顺序读取变更文件的完整内容,供 GPT 做跨文件分析。
- **输出**: `contents_truncated`
- **临时文件**: `file_contents.txt`(写入 `RUNNER_TEMP`
- **前置条件**: 需要先运行 `checkout``gather-pr-diff`
- **限制**: 跳过锁文件/二进制文件,总内容上限 80K 字符
文件读取优先级(由高到低):
1. `packages/desktop/src/process/`, `packages/desktop/src/process/agent/`, `packages/desktop/src/process/webserver/` — 核心后端
2. `packages/desktop/src/process/channels/` — Agent 通信
3. `packages/desktop/src/common/` — 公共模块
4. `packages/desktop/src/process/worker/` — Worker 进程
5. `packages/desktop/src/renderer/` — 前端 UI
6. 其他 `.ts/.tsx/.js/.jsx` 文件
7. 其余文件
### `call-openai`
调用 OpenAI Chat Completions API,包含自动重试、截断提示和长度限制处理。
- **输入**: `openai_api_key`, `output_file`, `diff_truncated`, `contents_truncated`
- **临时文件读取**: `system_prompt.txt`, `user_prompt.txt`(从 `RUNNER_TEMP` 读取)
- **临时文件写入**: `{output_file}`(写入 `RUNNER_TEMP`
- **模型**: `gpt-5.2`
- **重试策略**: 最多 2 次重试,指数退避(429/5xx 状态码和网络错误)
## 数据流
```
┌─────────────────────┐
│ gather-pr-diff │
│ (GitHub API) │
└────────┬────────────┘
pr_diff.txt, file_list.json
┌──────────────┼──────────────┐
│ │ │
▼ │ ▼
┌────────────────┐ │ ┌───────────────────┐
│ read-file- │ │ │ fetch PR metadata │
│ contents │ │ │ + linked issues │
│ (本地文件系统) │ │ │ (assessment only) │
└───────┬────────┘ │ └────────┬──────────┘
│ │ │
file_contents.txt │ pr_meta.json,
│ │ linked_issues.json
└──────┬───────┘ │
│ │
▼ │
┌──────────────────┐ │
│ Construct GPT │◄───────────┘
│ Prompts │
│ (各工作流自定义) │
└────────┬─────────┘
system_prompt.txt, user_prompt.txt
┌──────────────────┐
│ call-openai │
│ (OpenAI API) │
└────────┬─────────┘
{output_file}
┌──────────────────┐
│ Post Comment │
│ (各工作流自定义) │
└──────────────────┘
```
## 语言检测
两个工作流都会自动检测 PR 标题和描述的语言,并使用相同语言输出回复:
- PR 内容为中文 → 中文评论
- PR 内容为英文 → 英文评论
- 混合或无法判断 → 默认英文
## 使用方式
### GPT Review(自动 + 手动触发)
**自动触发**:通过 `pr-checks.yml` 在 PR 首次提交时自动触发,无需手动操作。
**手动触发**
1. 进入 GitHub 仓库 → Actions 页面
2. 左侧选择 **GPT Review**
3. 点击 **Run workflow**
4. 输入 PR number
5. 等待执行完成,审查结果将以 PR Review 形式出现
### GPT PR Assessment(自动 + 手动触发)
**自动触发**:当非项目成员(即 `author_association` 既不是 `OWNER` 也不是 `MEMBER`)首次提交 PR 时,`pr-checks.yml` 会在代码质量检查通过后自动触发评估。
**手动触发**
1. 进入 GitHub 仓库 → Actions 页面
2. 左侧选择 **GPT PR Assessment**
3. 点击 **Run workflow**
4. 输入 PR number
5. 等待执行完成,评估报告将作为评论出现在 PR 中
重复对同一 PR 触发时,评论会被**更新**而非重复创建。
## Secrets 配置
| Secret | 用途 |
| ---------------- | ----------------------------------- |
| `OPENAI_API_KEY` | OpenAI API 访问密钥,两个工作流共用 |
## 修改指南
- **更换模型**: 修改 `.github/actions/call-openai/action.yml` 中的 `model` 字段
- **调整重试逻辑**: 修改 `.github/actions/call-openai/action.yml` 中的 `callOpenAI` 函数
- **修改文件优先级**: 修改 `.github/actions/read-file-contents/action.yml` 中的 `filePriority` 函数
- **修改 Review prompt**: 修改 `gpt-review.yml` 中的 "Construct GPT prompts" step
- **修改 Assessment prompt**: 修改 `gpt-pr-assessment.yml` 中的 "Construct GPT prompts" step
+687
View File
@@ -0,0 +1,687 @@
name: Build Pipeline (Reusable)
on:
workflow_call:
inputs:
matrix:
description: 'Build matrix as JSON string (passed via fromJSON in caller)'
type: string
required: true
ref:
description: 'Git ref to checkout (leave empty to use the triggering commit)'
type: string
default: ''
skip_code_quality:
description: 'Skip code quality checks (faster builds for debugging)'
type: boolean
default: false
append_commit_hash:
description: 'Append short commit hash to artifact names'
type: boolean
default: false
upload_installers_only:
description: 'Only upload primary installers (exe/msi/dmg/deb), skip zip/yml/blockmap'
type: boolean
default: false
aioncore_run_id:
description: 'Optional AionCore Manual Build workflow run id to bundle instead of the pinned release'
type: string
default: ''
env:
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
AIONUI_HUB_TAG: 'dist-ebc6a2b'
jobs:
code-quality:
name: Code Quality
runs-on: ubuntu-latest
if: ${{ !inputs.skip_code_quality }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: bun run postinstall || true
- name: Run ESLint
run: bun run lint
- name: Check Prettier formatting
run: bun run format:check
- name: TypeScript type check
run: bunx tsc --noEmit
- name: Run unit tests
run: bunx vitest run
build:
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
needs: code-quality
if: ${{ always() && (inputs.skip_code_quality || needs.code-quality.result == 'success') }}
outputs:
commit-short: ${{ steps.commit.outputs.short }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(inputs.matrix) }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Get commit hash
id: commit
shell: bash
run: |
SHORT=$(git rev-parse --short HEAD)
echo "short=$SHORT" >> $GITHUB_OUTPUT
echo "Commit: $SHORT"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
# Restore Electron/Electron-Builder caches before any build-related install
- name: Cache Electron artifacts
id: electron-cache
uses: actions/cache@v4
with:
path: |
${{ runner.temp }}/.cache/electron
${{ runner.temp }}/.cache/electron-builder
~/.cache/electron
~/.cache/electron-builder
${{ env.LOCALAPPDATA }}\electron-builder\Cache
key: electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('package.json', 'bun.lock') }}
restore-keys: |
electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-
electron-cache-${{ matrix.platform }}-
- name: Cache status
run: echo "electron-cache-hit=${{ steps.electron-cache.outputs.cache-hit }}"
- name: Install dependencies
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: bun install --frozen-lockfile
- name: Run postinstall
run: bun run postinstall || true
- name: Setup Windows native dependencies (for Windows only)
if: startsWith(matrix.platform, 'windows')
run: |
bun install --global node-gyp
- name: Install MSVC ARM64 toolchain (Windows ARM64 only)
if: matrix.platform == 'windows-arm64'
continue-on-error: true
uses: nick-fields/retry@v3
with:
timeout_minutes: 20
max_attempts: 3
retry_wait_seconds: 60
shell: pwsh
command: |
choco install visualstudio2022buildtools --yes --params "'--add Microsoft.VisualStudio.Component.VC.Tools.ARM64 --add Microsoft.VisualStudio.Component.Windows11SDK.22000 --includeRecommended --includeOptional'"
- name: Setup Windows build environment (for Windows only)
if: startsWith(matrix.platform, 'windows')
run: |
echo "Setting Windows SDK version for ConPty support"
echo "WindowsTargetPlatformVersion=10.0.19041.0" >> $env:GITHUB_ENV
- name: Setup MSBuild (for Windows only)
if: startsWith(matrix.platform, 'windows')
uses: microsoft/setup-msbuild@v2
with:
vs-version: '17.0'
- name: Rebuild native modules for Electron
if: "!startsWith(matrix.platform, 'windows')"
run: bunx electron-builder install-app-deps
env:
npm_config_runtime: electron
npm_config_disturl: https://electronjs.org/headers
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
- name: Print build configuration
shell: bash
run: |
echo "=========================================="
echo "BUILD CONFIGURATION"
echo "=========================================="
echo "Platform: ${{ matrix.platform }}"
echo "Target Architecture: ${{ matrix.arch }}"
echo "Build Command: ${{ matrix.command }}"
echo "Runner OS: ${{ runner.os }}"
echo "Runner Arch: ${{ runner.arch }}"
echo "Ref: ${{ inputs.ref || '(default)' }}"
echo "Commit: ${{ steps.commit.outputs.short }}"
echo "=========================================="
- name: Resolve Sentry release name
shell: bash
run: |
set -euo pipefail
VERSION=$(node -p "require('./package.json').version")
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
RELEASE_NAME="${GITHUB_REF_NAME}"
elif [[ "$GITHUB_REF_NAME" == "dev" ]]; then
RELEASE_NAME="v${VERSION}-dev-${{ steps.commit.outputs.short }}"
else
RELEASE_NAME="v${VERSION}-${{ steps.commit.outputs.short }}"
fi
echo "SENTRY_RELEASE=$RELEASE_NAME" >> "$GITHUB_ENV"
echo "Sentry release: $RELEASE_NAME"
- name: Configure Sentry source map upload owner
shell: bash
run: |
set -euo pipefail
if [[ "${{ matrix.platform }}" == "linux-x64" ]]; then
echo "SENTRY_UPLOAD_SOURCE_MAPS=true" >> "$GITHUB_ENV"
echo "Sentry source maps will be uploaded by linux-x64"
else
echo "SENTRY_UPLOAD_SOURCE_MAPS=false" >> "$GITHUB_ENV"
echo "Sentry source map upload skipped for ${{ matrix.platform }}"
fi
- name: Validate Sentry source map upload configuration
if: matrix.platform == 'linux-x64'
shell: bash
run: |
set -euo pipefail
missing=0
for name in SENTRY_AUTH_TOKEN SENTRY_ORG SENTRY_PROJECT SENTRY_RELEASE; do
if [ -z "${!name:-}" ]; then
echo "::error title=Sentry configuration missing::$name is required for desktop source map uploads"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
exit 1
fi
echo "Sentry source map upload configured for $SENTRY_ORG/$SENTRY_PROJECT ($SENTRY_RELEASE)"
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
# macOS code signing certificate installation
- name: Setup macOS code signing (macOS only)
if: startsWith(matrix.platform, 'macos')
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD || 'temp-keychain-password' }}
run: |
if [ -n "$BUILD_CERTIFICATE_BASE64" ]; then
echo "Installing code signing certificate..."
# Create certificate file
echo $BUILD_CERTIFICATE_BASE64 | base64 --decode > certificate.p12
# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
# Import certificate
security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
# Clean up certificate file
rm certificate.p12
echo "Certificate imported successfully"
else
echo "No certificate provided - building unsigned application"
echo "To enable code signing, add BUILD_CERTIFICATE_BASE64 and P12_PASSWORD secrets"
fi
- name: Install Linux dependencies
if: startsWith(matrix.platform, 'linux')
run: |
sudo apt-get update
sudo apt-get install -y build-essential python3 python3-pip pkg-config libsqlite3-dev fakeroot dpkg-dev rpm libnss3-dev libatk-bridge2.0-dev libdrm2 libxkbcommon-dev libxss1 libatspi2.0-dev libgtk-3-dev libxrandr2 libasound2-dev
- name: Rebuild native modules for Electron (Windows)
if: startsWith(matrix.platform, 'windows')
shell: pwsh
continue-on-error: false
run: |
$arch = "${{ matrix.arch }}"
$electronVer = (node -p "require('./package.json').devDependencies.electron.replace(/[\^~]/g, '')").Trim()
Write-Host "=========================================="
Write-Host "Rebuilding native modules for Electron $electronVer ($arch)"
Write-Host "=========================================="
# Strategy: Try prebuild-install first (faster, more reliable)
# Fallback to electron-rebuild only if prebuild-install fails
$success = $false
# Step 1: Try prebuild-install (downloads prebuilt binaries)
Write-Host ""
Write-Host "[Step 1] Trying prebuild-install..."
$prebuildResult = $null
try {
$prebuildResult = bun x --yes prebuild-install --runtime=electron --target=$electronVer --platform=win32 --arch=$arch --force 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "✅ prebuild-install succeeded"
$success = $true
} else {
Write-Host "⚠️ prebuild-install exited with code $LASTEXITCODE"
}
} catch {
Write-Host "⚠️ prebuild-install failed: $_"
}
# Step 2: Verify or fallback to electron-rebuild
$sqliteNode = "node_modules/better-sqlite3/build/Release/better_sqlite3.node"
if (-not (Test-Path $sqliteNode)) {
Write-Host ""
Write-Host "[Step 2] prebuild-install didn't produce binary, trying electron-rebuild..."
try {
bunx electron-rebuild -f -w better-sqlite3
if ($LASTEXITCODE -eq 0) {
Write-Host "✅ electron-rebuild succeeded"
$success = $true
}
} catch {
Write-Host "❌ electron-rebuild failed: $_"
}
}
# Final verification
Write-Host ""
Write-Host "Verifying native modules..."
if (Test-Path $sqliteNode) {
$size = [math]::Round((Get-Item $sqliteNode).Length / 1MB, 1)
Write-Host "✅ better-sqlite3 found: $sqliteNode ($size MB)"
} else {
Write-Error "❌ better-sqlite3 native module not found at: $sqliteNode"
exit 1
}
Write-Host ""
Write-Host "=========================================="
Write-Host "✅ All critical native modules verified"
Write-Host "=========================================="
exit 0
env:
npm_config_runtime: electron
npm_config_arch: ${{ matrix.arch }}
npm_config_target_arch: ${{ matrix.arch }}
npm_config_disturl: https://electronjs.org/headers
# Don't force build_from_source - let prebuild-install try first
MSVS_VERSION: 2022
GYP_MSVS_VERSION: 2022
- name: Prepare aioncore binary
shell: bash
run: node scripts/prepareAioncore.js
env:
# Version is pinned in repo-root package.json (aioncoreVersion).
# Set AIONUI_BACKEND_VERSION here only to override for ad-hoc builds.
AIONUI_BACKEND_RUN_ID: ${{ inputs.aioncore_run_id }}
AIONUI_BACKEND_ARCH: ${{ matrix.arch }}
GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build with electron-builder (Windows)
if: startsWith(matrix.platform, 'windows')
id: windows-build
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$Command = $env:WINDOWS_BUILD_COMMAND
$BuildFailed = $false
try {
Write-Host "Running Windows build command: $Command"
iex $Command
Write-Host "Windows build completed successfully"
} catch {
$BuildFailed = $true
Write-Warning "${{ matrix.platform }} build failed but will not block the workflow"
Write-Host "::notice title=Windows build skipped::${{ matrix.platform }} build failed (see logs above) but workflow is allowed to continue."
}
if ($BuildFailed) {
"result=failure" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
} else {
"result=success" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
}
env:
WINDOWS_BUILD_COMMAND: ${{ matrix.command }}
AIONUI_BACKEND_RUN_ID: ${{ inputs.aioncore_run_id }}
# Node.js memory limit for vite bundling (prevents OOM during build)
# Node.js 内存限制,防止 vite 打包时内存不足
NODE_OPTIONS: '--max-old-space-size=8192'
# Target arch / 目标架构
npm_config_arch: ${{ matrix.arch }}
npm_config_target_arch: ${{ matrix.arch }}
# Native module compilation / 原生模块编译
npm_config_runtime: electron
npm_config_disturl: https://electronjs.org/headers
npm_config_build_from_source: true
npm_config_cache: ${{ runner.temp }}/.npm
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
# Signing & IDs / 签名与应用 ID 配置
APP_ID: ${{ secrets.APP_ID }}
appleId: ${{ secrets.APPLE_ID }}
appleIdPassword: ${{ secrets.APPLE_ID_PASSWORD }}
teamId: ${{ secrets.TEAM_ID }}
identity: ${{ secrets.IDENTITY }}
CSC_NAME: ${{ secrets.IDENTITY }}
CSC_IDENTITY_AUTO_DISCOVERY: false
# Windows build flags / Windows 构建参数
MSVS_VERSION: 2022
GYP_MSVS_VERSION: 2022
WindowsTargetPlatformVersion: 10.0.19041.0
_WIN32_WINNT: 0x0A00
# Sentry DSN (injected at build time via vite define)
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
# CI flag & GitHub token / CI 标识与 GitHub Token
CI: true
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Clean up any stale disk images before DMG creation (macOS only)
# 清理可能残留的磁盘镜像,避免 hdiutil "Device not configured" 错误
- name: Clean up stale disk images (macOS only)
if: startsWith(matrix.platform, 'macos')
run: |
echo "Cleaning up any stale disk images..."
# Detach any mounted disk images
hdiutil info 2>/dev/null | grep '/dev/disk' | awk '{print $1}' | while read disk; do
echo "Detaching $disk..."
hdiutil detach "$disk" -force 2>/dev/null || true
done
# Clean up any temporary DMG files
rm -f /tmp/*.dmg 2>/dev/null || true
echo "Cleanup complete"
# macOS: Build with notarization - DMG failure = CI failure, notarization failure = warning only
# macOS: 构建并公证 - DMG 失败 = CI 失败,公证失败 = 仅警告
- name: Build with electron-builder (macOS)
if: startsWith(matrix.platform, 'macos')
id: macos-build
shell: bash
run: |
set +e # Handle errors manually
# Run build command
${{ matrix.command }} 2>&1 | tee "${RUNNER_TEMP}/build.log"
BUILD_EXIT_CODE=${PIPESTATUS[0]}
# Check if DMG was created (most important artifact)
DMG_EXISTS=false
if ls out/*.dmg 1>/dev/null 2>&1; then
DMG_EXISTS=true
echo "✅ DMG created successfully"
fi
# If build succeeded completely
if [ $BUILD_EXIT_CODE -eq 0 ]; then
echo "✅ Build completed successfully"
echo "notarization_status=success" >> $GITHUB_OUTPUT
exit 0
fi
# Build failed - check if it's just notarization failure
if [ "$DMG_EXISTS" = true ]; then
# DMG exists but build failed - likely notarization issue
if grep -qiE "notariz|staple" "${RUNNER_TEMP}/build.log"; then
echo "⚠️ DMG created but notarization failed"
echo "notarization_status=failed" >> $GITHUB_OUTPUT
echo "::warning title=Notarization Failed::DMG was built and signed successfully, but **notarization failed**."
echo "" >> $GITHUB_STEP_SUMMARY
echo "## ⚠️ Notarization Warning" >> $GITHUB_STEP_SUMMARY
echo "DMG was built and signed successfully, but **notarization failed**." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Users may see Gatekeeper warnings when opening the app for the first time." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "To fix: Right-click the app → Open → Open" >> $GITHUB_STEP_SUMMARY
exit 0 # Allow CI to continue
fi
fi
# DMG doesn't exist or other fatal error - fail CI
echo "❌ Build failed - DMG was not created"
echo "notarization_status=build_failed" >> $GITHUB_OUTPUT
exit 1
env:
NODE_OPTIONS: '--max-old-space-size=8192'
AIONUI_BACKEND_RUN_ID: ${{ inputs.aioncore_run_id }}
npm_config_arch: ${{ matrix.arch }}
APP_ID: ${{ secrets.APP_ID }}
appleId: ${{ secrets.APPLE_ID }}
appleIdPassword: ${{ secrets.APPLE_ID_PASSWORD }}
teamId: ${{ secrets.TEAM_ID }}
identity: ${{ secrets.IDENTITY }}
CSC_NAME: ${{ secrets.IDENTITY }}
CSC_IDENTITY_AUTO_DISCOVERY: false
npm_config_cache: ${{ runner.temp }}/.npm
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
ELECTRON_MIRROR: ''
npm_config_disturl: https://electronjs.org/headers
npm_config_runtime: electron
npm_config_target_arch: ${{ matrix.arch }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
CI: true
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Linux: Standard build without special error handling
# Linux: 标准构建,无特殊错误处理
- name: Build with electron-builder (Linux)
if: startsWith(matrix.platform, 'linux')
uses: nick-fields/retry@v3
with:
timeout_minutes: 60
max_attempts: 2
retry_wait_seconds: 60
command: ${{ matrix.command }}
env:
NODE_OPTIONS: '--max-old-space-size=8192'
AIONUI_BACKEND_RUN_ID: ${{ inputs.aioncore_run_id }}
npm_config_arch: ${{ matrix.arch }}
npm_config_cache: ${{ runner.temp }}/.npm
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/
npm_config_electron_builder_binaries_mirror: https://github.com/electron-userland/electron-builder-binaries/releases/download/
ELECTRON_MIRROR: ''
npm_config_disturl: https://electronjs.org/headers
npm_config_runtime: electron
npm_config_target_arch: ${{ matrix.arch }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
CI: true
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS 钥匙串清理
- name: Clean up keychain (macOS only)
if: startsWith(matrix.platform, 'macos') && always()
run: |
if security list-keychains | grep -q build.keychain; then
security delete-keychain build.keychain
echo "Temporary keychain deleted"
fi
# macOS 构建失败通知
# macOS build failure notification
- name: Notify on macOS build failure
if: startsWith(matrix.platform, 'macos') && failure()
run: |
echo "::error title=macOS Build Failed::Build failed. Check the logs for details."
echo ""
echo "❌ macOS Build Failed"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Common causes:"
echo " • hdiutil/DMG creation failure (Device not configured)"
echo " • Code signing issues"
echo " • Notarization timeout"
echo " • Out of memory (OOM)"
echo ""
echo "Check the build logs above for the specific error."
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- name: List build artifacts
if: success() || failure()
shell: bash
run: |
echo "=========================================="
echo "BUILD ARTIFACTS"
echo "=========================================="
if [ -d "out" ]; then
ls -lh out/ || true
echo "=========================================="
echo "Total artifacts:"
find out/ -type f \( -name "*.exe" -o -name "*.msi" -o -name "*.dmg" -o -name "*.deb" -o -name "*.zip" -o -name "*.yml" \) -exec basename {} \; || true
else
echo "No out directory found!"
fi
# Verify desktop asar does not contain packages/web-cli/ contamination
# 验证桌面 asar 没有被 web-cli 代码污染
- name: Verify desktop asar isolation
if: >-
success() &&
(!startsWith(matrix.platform, 'windows') || steps.windows-build.outputs.result == 'success')
shell: bash
run: |
set -euo pipefail
ASAR=$(find out -type f -name 'app.asar' -not -path '*node_modules*' | head -n 1 || true)
if [ -z "$ASAR" ]; then
echo "::notice::No app.asar found for ${{ matrix.platform }}, skipping isolation check"
exit 0
fi
echo "Inspecting $ASAR for web-cli leakage..."
LEAK_COUNT=$(bunx @electron/asar list "$ASAR" 2>/dev/null | grep -cE '^/packages/web-cli(/|$)' || true)
if [ "$LEAK_COUNT" != "0" ]; then
echo "::error::Desktop asar contains $LEAK_COUNT web-cli entries:"
bunx @electron/asar list "$ASAR" | grep -E '^/packages/web-cli(/|$)' | head -20
exit 1
fi
echo "✅ asar isolation verified: no packages/web-cli entries"
# Upload artifacts when:
# - Build succeeded (all platforms)
# - macOS: notarization timeout but artifacts exist (handled in build step)
# - Windows: only if windows-build step succeeded
# 上传产物条件:
# - 构建成功(所有平台)
# - macOS: 公证超时但有产物(已在构建步骤中处理)
# - Windows: 仅当 windows-build 步骤成功
- name: Clean up non-installer artifacts
if: >-
inputs.upload_installers_only && success() &&
(!startsWith(matrix.platform, 'windows') || steps.windows-build.outputs.result == 'success')
shell: bash
run: |
echo "Removing non-installer files from out/ ..."
rm -f out/*.blockmap out/*.zip out/*.yml out/builder-debug.yml 2>/dev/null || true
echo "Remaining artifacts:"
ls -lh out/ || true
- name: Upload build artifacts
uses: actions/upload-artifact@v6
if: >-
success() &&
(!startsWith(matrix.platform, 'windows') || steps.windows-build.outputs.result == 'success')
with:
name: ${{ inputs.append_commit_hash && format('{0}-{1}', matrix.artifact-name, steps.commit.outputs.short) || matrix.artifact-name }}
path: |
out/*.exe
out/*.msi
out/*.dmg
out/*.deb
out/AionUi-*-mac-*.zip
out/*.yml
if-no-files-found: warn
retention-days: 7
build-summary:
name: Build Summary
runs-on: ubuntu-latest
needs: [code-quality, build]
if: always()
steps:
- name: Write build summary
env:
MATRIX: ${{ inputs.matrix }}
run: |
PLATFORMS=$(echo "$MATRIX" | jq -r '[.include[].platform] | join(", ")')
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Key | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-----|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Ref | \`${{ inputs.ref || '(default)' }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Commit | \`${{ needs.build.outputs.commit-short }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Platforms | $PLATFORMS |" >> $GITHUB_STEP_SUMMARY
echo "| Code Quality | ${{ needs.code-quality.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY
+314
View File
@@ -0,0 +1,314 @@
name: Build and Release
permissions:
contents: write
issues: read
pull-requests: read
actions: write
on:
push:
branches: [dev]
tags:
- '*'
env:
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
jobs:
# 独立 code-quality job,供桌面构建和 web-cli 打包共用,避免重复跑
code-quality:
name: Code Quality
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-dev-'))
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: bun run postinstall || true
- name: Run ESLint
run: bun run lint
- name: Check Prettier formatting
run: bun run format:check
- name: TypeScript type check
run: bunx tsc --noEmit
- name: Run unit tests
run: bunx vitest run
build-pipeline:
name: Build Pipeline
uses: ./.github/workflows/_build-reusable.yml
needs: code-quality
# dev 分支或正式 tag 触发构建(排除 -dev- tag,避免重复构建)
if: needs.code-quality.result == 'success' && (github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-dev-')))
with:
skip_code_quality: true
matrix: >-
{"include":[
{"platform":"macos-arm64","os":"macos-14","command":"node scripts/build-with-builder.js arm64 --mac --arm64","artifact-name":"macos-build-arm64","arch":"arm64"},
{"platform":"macos-x64","os":"macos-14","command":"node scripts/build-with-builder.js x64 --mac --x64","artifact-name":"macos-build-x64","arch":"x64"},
{"platform":"windows-x64","os":"windows-2022","command":"node scripts/build-with-builder.js x64 --win --x64","artifact-name":"windows-build-x64","arch":"x64"},
{"platform":"windows-arm64","os":"windows-11-arm","command":"node scripts/build-with-builder.js arm64 --win --arm64","artifact-name":"windows-build-arm64","arch":"arm64"},
{"platform":"linux-x64","os":"ubuntu-latest","command":"node scripts/build-with-builder.js x64 --linux --x64","artifact-name":"linux-build-x64","arch":"x64"},
{"platform":"linux-arm64","os":"ubuntu-24.04-arm","command":"node scripts/build-with-builder.js arm64 --linux --arm64","artifact-name":"linux-build-arm64","arch":"arm64"}
]}
secrets: inherit
pack-web-cli:
name: Pack Web CLI
uses: ./.github/workflows/pack-web-cli.yml
needs: code-quality
# 与桌面构建同频触发:dev 分支或正式 tag(排除 -dev- tag
if: needs.code-quality.result == 'success' && (github.ref == 'refs/heads/dev' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-dev-')))
with:
ref: ''
append_commit_hash: false
skip_code_quality: true
secrets: inherit
# 自动重试 workflow(当构建失败时)
auto-retry-workflow:
name: Auto Retry on Build Failure
runs-on: ubuntu-latest
needs: build-pipeline
# 关键:只在首次失败时触发,避免无限循环
if: |
failure() &&
github.run_attempt == 1 &&
(github.event_name == 'push' || github.event_name == 'schedule')
steps:
- name: Log retry information
run: |
echo "=========================================="
echo "🔄 Auto retry triggered (first failure)"
echo "=========================================="
echo "Build failed on first attempt, preparing auto retry..."
echo "Current attempt: ${{ github.run_attempt }}"
echo "Wait strategy: 5 minutes cooldown before retry"
echo "=========================================="
- name: Wait before retry (5 min cooldown)
run: |
echo "⏳ Waiting 5 minutes before retry..."
echo "Start: $(date)"
sleep 300
echo "End: $(date)"
echo "Triggering retry..."
- name: Trigger workflow rerun
run: |
echo "🔄 Triggering full workflow rerun (attempt 2)..."
# Use re-run API (not rerun-failed-jobs, to avoid loops)
response=$(curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-w "\n%{http_code}" \
https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/rerun)
http_code=$(echo "$response" | tail -n1)
if [ "$http_code" = "201" ]; then
echo ""
echo "✅ Retry triggered successfully"
echo "This will be attempt 2"
echo "Details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
else
echo ""
echo "❌ Retry trigger failed, HTTP status: $http_code"
echo "Response:"
echo "$response" | head -n-1
exit 1
fi
# 自动创建tag(仅 dev 分支推送时)
create-tag:
name: Create Tag from Branch
runs-on: ubuntu-latest
needs: [build-pipeline]
if: success() && github.ref == 'refs/heads/dev'
outputs:
tag_name: ${{ steps.create_tag.outputs.tag_name }}
is_dev: ${{ steps.create_tag.outputs.is_dev }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
# GH_TOKEN is a PAT with `repo` + `workflow` scopes — required because
# tag pushes whose reachable history contains .github/workflows/
# changes are rejected for the built-in GITHUB_TOKEN (lacks the
# `workflow` scope, which GITHUB_TOKEN can never grant).
token: ${{ secrets.GH_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Create and push tag
id: create_tag
run: |
VERSION=$(node -p "require('./package.json').version")
COMMIT_SHORT=$(git rev-parse --short HEAD)
BRANCH_NAME=${GITHUB_REF#refs/heads/}
# 根据分支确定 tag 格式
if [ "$BRANCH_NAME" = "dev" ]; then
TAG_NAME="v${VERSION}-dev-${COMMIT_SHORT}"
IS_DEV="true"
echo "🔧 Development release: $TAG_NAME"
else
TAG_NAME="v$VERSION"
IS_DEV="false"
echo "🚀 Production release: $TAG_NAME"
fi
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "is_dev=$IS_DEV" >> $GITHUB_OUTPUT
# 检查远程tag是否已存在
if git ls-remote --tags origin | grep -q "refs/tags/$TAG_NAME$"; then
if [ "$IS_DEV" = "false" ]; then
# Main 分支:需要递增版本并修改 package.json
echo "⚠️ Tag $TAG_NAME already exists, auto-incrementing version..."
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
echo "📝 Updating package.json version to $NEW_VERSION"
# 更新 package.json 版本号
bun pm version $NEW_VERSION
# 配置git用户信息
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# 提交版本更新
git add package.json bun.lock
git commit -m "chore: bump version to $NEW_VERSION"
git remote set-url origin https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git
git push origin $BRANCH_NAME
# 获取新的 commit ID
COMMIT_SHORT=$(git rev-parse --short HEAD)
TAG_NAME="v${NEW_VERSION}-${COMMIT_SHORT}"
echo "🚀 New tag with updated version: $TAG_NAME"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
else
TAG_NAME="v${VERSION}-${COMMIT_SHORT}"
echo "⚠️ Fallback: creating tag with commit ID: $TAG_NAME"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
fi
else
# Dev 分支 tag 已存在则失败(不应该发生)
echo "⚠️ Dev tag $TAG_NAME already exists, this shouldn't happen"
exit 1
fi
fi
# 配置git用户信息(如果前面没配置)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# 创建并推送tag
echo "Creating tag: $TAG_NAME"
git tag $TAG_NAME
git remote set-url origin https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git
git push origin $TAG_NAME
echo "✅ Successfully created and pushed tag: $TAG_NAME"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
# 发布到 GitHub Releases
release:
name: Create Release
runs-on: ubuntu-latest
needs: [build-pipeline, create-tag, pack-web-cli]
# Tag 推送时 create-tag 会被跳过,所以需要检查两种情况(排除 -dev- tag 避免重复)
if: always() && needs.build-pipeline.result == 'success' && needs.pack-web-cli.result == 'success' && (needs.create-tag.result == 'success' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-dev-')))
# dev 分支使用 dev-release 环境,正式 tag 使用 release 环境
environment: ${{ needs.create-tag.outputs.is_dev == 'true' && 'dev-release' || 'release' }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Get version for release
id: version
run: |
# 判断触发来源:正式 tag 推送 or dev 分支
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
# 正式 Tag 推送触发(-dev- tag 已被排除,不会到达这里)
TAG_NAME="${GITHUB_REF#refs/tags/}"
IS_DEV="false"
echo "🚀 Creating stable release from tag: $TAG_NAME"
else
# dev 分支触发,使用 create-tag job 的输出
TAG_NAME="${{ needs.create-tag.outputs.tag_name }}"
IS_DEV="${{ needs.create-tag.outputs.is_dev }}"
echo "🔧 Creating development release: $TAG_NAME"
fi
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "is_dev=$IS_DEV" >> $GITHUB_OUTPUT
- name: Download all build artifacts
uses: actions/download-artifact@v7
with:
path: build-artifacts
- name: Prepare release assets (normalize updater metadata)
shell: bash
run: bash scripts/prepare-release-assets.sh build-artifacts release-assets
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag_name }}
name: ${{ steps.version.outputs.is_dev == 'true' && format('Development Build {0}', steps.version.outputs.tag_name) || steps.version.outputs.tag_name }}
files: |
release-assets/**/*.exe
release-assets/**/*.msi
release-assets/**/*.dmg
release-assets/**/*.deb
release-assets/**/*.zip
release-assets/**/*.yml
release-assets/**/*.tar.gz
release-assets/**/*.sha256
release-assets/install-web.sh
generate_release_notes: true
draft: true
prerelease: ${{ steps.version.outputs.is_dev == 'true' || contains(steps.version.outputs.tag_name, 'beta') || contains(steps.version.outputs.tag_name, 'alpha') || contains(steps.version.outputs.tag_name, 'rc') }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
+93
View File
@@ -0,0 +1,93 @@
name: '🔨 Manual Build'
permissions:
contents: write
actions: write
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to build from'
required: true
default: 'main'
type: string
platform:
description: 'Platform to build'
required: true
type: choice
options:
- macos-arm64
- macos-x64
- windows-x64
- windows-arm64
- linux-x64
- linux-arm64
- all
default: 'macos-arm64'
skip_code_quality:
description: 'Skip code quality checks (faster builds for debugging)'
required: false
type: boolean
default: false
aioncore_run_id:
description: 'Optional AionCore Manual Build workflow run id to bundle instead of the pinned release'
required: false
type: string
default: ''
env:
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
jobs:
prepare-matrix:
name: Prepare Build Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Generate build matrix
id: set-matrix
shell: bash
run: |
PLATFORM="${{ inputs.platform }}"
MACOS_ARM64='{"platform":"macos-arm64","os":"macos-14","command":"node scripts/build-with-builder.js arm64 --mac --arm64","artifact-name":"macos-build-arm64","arch":"arm64"}'
MACOS_X64='{"platform":"macos-x64","os":"macos-14","command":"node scripts/build-with-builder.js x64 --mac --x64","artifact-name":"macos-build-x64","arch":"x64"}'
WINDOWS_X64='{"platform":"windows-x64","os":"windows-2022","command":"node scripts/build-with-builder.js x64 --win --x64","artifact-name":"windows-build-x64","arch":"x64"}'
WINDOWS_ARM64='{"platform":"windows-arm64","os":"windows-11-arm","command":"node scripts/build-with-builder.js arm64 --win --arm64","artifact-name":"windows-build-arm64","arch":"arm64"}'
LINUX_X64='{"platform":"linux-x64","os":"ubuntu-latest","command":"node scripts/build-with-builder.js x64 --linux --x64","artifact-name":"linux-build-x64","arch":"x64"}'
LINUX_ARM64='{"platform":"linux-arm64","os":"ubuntu-24.04-arm","command":"node scripts/build-with-builder.js arm64 --linux --arm64","artifact-name":"linux-build-arm64","arch":"arm64"}'
if [ "$PLATFORM" = "all" ]; then
MATRIX="{\"include\":[$MACOS_ARM64,$MACOS_X64,$WINDOWS_X64,$WINDOWS_ARM64,$LINUX_X64,$LINUX_ARM64]}"
elif [ "$PLATFORM" = "macos-arm64" ]; then
MATRIX="{\"include\":[$MACOS_ARM64]}"
elif [ "$PLATFORM" = "macos-x64" ]; then
MATRIX="{\"include\":[$MACOS_X64]}"
elif [ "$PLATFORM" = "windows-x64" ]; then
MATRIX="{\"include\":[$WINDOWS_X64]}"
elif [ "$PLATFORM" = "windows-arm64" ]; then
MATRIX="{\"include\":[$WINDOWS_ARM64]}"
elif [ "$PLATFORM" = "linux-x64" ]; then
MATRIX="{\"include\":[$LINUX_X64]}"
elif [ "$PLATFORM" = "linux-arm64" ]; then
MATRIX="{\"include\":[$LINUX_ARM64]}"
fi
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
echo "Generated matrix for platform '$PLATFORM':"
echo "$MATRIX" | python3 -m json.tool
build-pipeline:
needs: prepare-matrix
uses: ./.github/workflows/_build-reusable.yml
with:
matrix: ${{ needs.prepare-matrix.outputs.matrix }}
ref: ${{ inputs.branch }}
skip_code_quality: ${{ inputs.skip_code_quality }}
aioncore_run_id: ${{ inputs.aioncore_run_id }}
append_commit_hash: true
upload_installers_only: true
secrets: inherit
+132
View File
@@ -0,0 +1,132 @@
name: Verify Homebrew Cask
# The aionui cask is on Homebrew's autobump list — BrewTestBot automatically
# creates version-bump PRs every ~3 hours. This workflow only *verifies* that
# the cask has been updated, it no longer submits bump PRs itself.
on:
# Daily check at 08:00 UTC
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
inputs:
version:
description: 'Version to verify (e.g., 1.8.20). Leave empty to use latest release.'
required: false
type: string
permissions:
contents: read
jobs:
verify-cask:
name: Verify Homebrew Cask
runs-on: ubuntu-latest
steps:
- name: Get expected version
id: release
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
VERSION="${VERSION#v}"
else
# Fetch latest non-prerelease release tag
TAG=$(gh release view --repo "${{ github.repository }}" --json tagName --jq '.tagName' 2>/dev/null || echo "")
if [ -z "$TAG" ]; then
echo "::error::Could not determine latest release tag"
exit 1
fi
VERSION="${TAG#v}"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Expected Homebrew cask version: $VERSION"
- name: Verify cask version
id: verify
run: |
EXPECTED="${{ steps.release.outputs.version }}"
# Fetch the cask file directly from the Homebrew repo
CASK_CONTENT=$(curl -fsSL "https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/a/aionui.rb" 2>/dev/null || echo "")
if [ -z "$CASK_CONTENT" ]; then
echo "::error::Failed to fetch cask file from Homebrew repo"
echo "status=fetch_failed" >> $GITHUB_OUTPUT
exit 1
fi
# Extract version from cask file
CASK_VERSION=$(echo "$CASK_CONTENT" | grep -oP 'version\s+"\K[^"]+' | head -1)
echo " Expected: $EXPECTED"
echo " Homebrew: $CASK_VERSION"
echo "cask_version=$CASK_VERSION" >> $GITHUB_OUTPUT
if [ "$CASK_VERSION" = "$EXPECTED" ]; then
echo "status=match" >> $GITHUB_OUTPUT
else
echo "status=mismatch" >> $GITHUB_OUTPUT
fi
- name: Check for pending PR
if: steps.verify.outputs.status == 'mismatch'
id: check_pr
env:
GH_TOKEN: ${{ github.token }}
run: |
EXPECTED="${{ steps.release.outputs.version }}"
# Search for open PRs updating aionui in homebrew-cask
PR_LIST=$(gh pr list --repo Homebrew/homebrew-cask \
--search "aionui $EXPECTED in:title" --state open \
--json number,title,url --limit 5 2>/dev/null || echo "[]")
PR_COUNT=$(echo "$PR_LIST" | jq length)
if [ "$PR_COUNT" -gt 0 ]; then
echo "Found pending PR(s):"
echo "$PR_LIST" | jq -r '.[] | " #\(.number) \(.title) — \(.url)"'
echo "status=pr_pending" >> $GITHUB_OUTPUT
else
echo "status=no_pr" >> $GITHUB_OUTPUT
fi
- name: Report result
if: always() && steps.release.outputs.version != ''
run: |
EXPECTED="${{ steps.release.outputs.version }}"
STATUS="${{ steps.verify.outputs.status }}"
CASK_VERSION="${{ steps.verify.outputs.cask_version }}"
PR_STATUS="${{ steps.check_pr.outputs.status }}"
echo ""
echo "======================================="
echo " Homebrew Cask Verification Report"
echo "======================================="
echo ""
if [ "$STATUS" = "match" ]; then
echo "Homebrew cask is up to date!"
echo " Version: $CASK_VERSION"
echo " Install: brew install --cask aionui"
elif [ "$STATUS" = "mismatch" ] && [ "$PR_STATUS" = "pr_pending" ]; then
echo "Homebrew bump PR is pending review."
echo " Current cask: $CASK_VERSION"
echo " Expected: $EXPECTED"
echo " BrewTestBot has created a PR, awaiting Homebrew maintainer merge."
elif [ "$STATUS" = "mismatch" ]; then
echo "::warning::Homebrew cask has NOT been updated to $EXPECTED (current: $CASK_VERSION)"
echo " Current cask: $CASK_VERSION"
echo " Expected: $EXPECTED"
echo ""
echo " If this persists, manually create a PR:"
echo " 1. Fork https://github.com/Homebrew/homebrew-cask"
echo " 2. Update Casks/a/aionui.rb with new version and SHA256"
echo " 3. Submit PR to Homebrew/homebrew-cask"
exit 1
else
echo "::error::Failed to verify — could not fetch cask file from Homebrew repo"
exit 1
fi
+389
View File
@@ -0,0 +1,389 @@
name: '📋 GPT PR Assessment'
on:
workflow_call:
inputs:
pr_number:
description: 'PR number to assess'
required: false
type: string
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to assess'
required: true
type: string
concurrency:
group: 'gpt-pr-assessment-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
assess:
runs-on: 'ubuntu-latest'
timeout-minutes: 10
permissions:
contents: 'read'
pull-requests: 'write'
issues: 'read'
steps:
- name: 'Checkout repository'
uses: 'actions/checkout@v4'
- name: 'Gather PR diff and changed files'
id: 'gather'
uses: './.github/actions/gather-pr-diff'
with:
pr_number: '${{ inputs.pr_number }}'
- name: 'Fetch PR metadata and linked issues'
id: 'metadata'
if: steps.gather.outputs.skip != 'true'
uses: 'actions/github-script@v7'
env:
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
with:
script: |
const fs = require('fs');
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
const prNumber = context.payload.pull_request?.number
|| Number(process.env.INPUT_PR_NUMBER);
// Fetch full PR data via API (needed for manual triggers too)
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const prMeta = {
title: pr.title || '',
body: pr.body || '',
author: pr.user.login || '',
labels: (pr.labels || []).map(l => l.name),
head_branch: pr.head.ref || '',
base_branch: pr.base.ref || '',
state: pr.state,
created_at: pr.created_at,
updated_at: pr.updated_at,
};
fs.writeFileSync(`${tmpDir}/pr_meta.json`, JSON.stringify(prMeta));
// Parse linked issues from PR body
const prBody = prMeta.body;
const linkedIssueNumbers = new Set();
const referencedIssueNumbers = new Set();
// Explicit linking keywords: Fixes #N, Closes #N, Resolves #N
const explicitPatterns = [
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi,
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/gi,
];
for (const pattern of explicitPatterns) {
let match;
while ((match = pattern.exec(prBody)) !== null) {
linkedIssueNumbers.add(Number(match[1]));
}
}
// General #N references (not already captured)
const refPattern = /#(\d+)/g;
let refMatch;
while ((refMatch = refPattern.exec(prBody)) !== null) {
const num = Number(refMatch[1]);
if (!linkedIssueNumbers.has(num)) {
referencedIssueNumbers.add(num);
}
}
// Fetch issue details, filtering out PRs
const EXPLICIT_BODY_LIMIT = 5000;
const REFERENCE_BODY_LIMIT = 3000;
async function fetchIssue(issueNum, bodyLimit) {
try {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNum,
});
// Skip if this is actually a PR (issues API returns PRs too)
if (issue.pull_request) {
return null;
}
const body = (issue.body || '').substring(0, bodyLimit);
const truncated = (issue.body || '').length > bodyLimit;
return {
number: issue.number,
title: issue.title,
state: issue.state,
labels: (issue.labels || []).map(l => typeof l === 'string' ? l : l.name),
body: truncated ? body + '\n... [truncated]' : body,
};
} catch (err) {
core.warning(`Failed to fetch issue #${issueNum}: ${err.message}`);
return null;
}
}
const linkedIssues = [];
for (const num of linkedIssueNumbers) {
const issue = await fetchIssue(num, EXPLICIT_BODY_LIMIT);
if (issue) linkedIssues.push({ ...issue, relation: 'explicit' });
}
for (const num of referencedIssueNumbers) {
const issue = await fetchIssue(num, REFERENCE_BODY_LIMIT);
if (issue) linkedIssues.push({ ...issue, relation: 'referenced' });
}
fs.writeFileSync(`${tmpDir}/linked_issues.json`, JSON.stringify(linkedIssues));
core.info(`Found ${linkedIssues.length} linked issue(s)`);
- name: 'Read changed file contents'
id: 'read_contents'
if: steps.gather.outputs.skip != 'true'
uses: './.github/actions/read-file-contents'
- name: 'Construct GPT prompts'
if: steps.gather.outputs.skip != 'true'
uses: 'actions/github-script@v7'
env:
FILE_COUNT: '${{ steps.gather.outputs.file_count }}'
ADDITIONS: '${{ steps.gather.outputs.additions }}'
DELETIONS: '${{ steps.gather.outputs.deletions }}'
TOTAL_LINES: '${{ steps.gather.outputs.total_lines }}'
with:
script: |
const fs = require('fs');
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
const prMeta = JSON.parse(fs.readFileSync(`${tmpDir}/pr_meta.json`, 'utf-8'));
const linkedIssues = JSON.parse(fs.readFileSync(`${tmpDir}/linked_issues.json`, 'utf-8'));
const diff = fs.readFileSync(`${tmpDir}/pr_diff.txt`, 'utf-8');
const fileListRaw = fs.readFileSync(`${tmpDir}/file_list.json`, 'utf-8');
let fileContents = '';
try {
fileContents = fs.readFileSync(`${tmpDir}/file_contents.txt`, 'utf-8');
} catch {
core.warning('Could not read file contents, proceeding with diff only');
}
const fileCount = process.env.FILE_COUNT || '0';
const additions = process.env.ADDITIONS || '0';
const deletions = process.env.DELETIONS || '0';
const totalLines = process.env.TOTAL_LINES || '0';
// Build linked issues section for user prompt
let issuesSection = '';
if (linkedIssues.length > 0) {
const issueTexts = linkedIssues.map(issue => {
const relation = issue.relation === 'explicit' ? 'Explicitly linked' : 'Referenced';
return [
`### Issue #${issue.number}: ${issue.title}`,
`- **Relation**: ${relation}`,
`- **State**: ${issue.state}`,
`- **Labels**: ${issue.labels.length > 0 ? issue.labels.join(', ') : 'None'}`,
'',
'**Body**:',
issue.body || '(No body)',
].join('\n');
});
issuesSection = [
'## Linked Issues',
'',
...issueTexts,
].join('\n');
} else {
issuesSection = [
'## Linked Issues',
'',
'No linked issues found. The PR does not reference any issues via "Fixes #N", "Closes #N", "Resolves #N", or "#N" patterns.',
'Please assess the PR based on its title, description, and code changes.',
].join('\n');
}
const systemPrompt = [
'You are a senior project maintainer and technical lead for the AionUi project — a cross-platform Electron desktop application that provides a unified AI agent graphical interface.',
'',
'Your role is to provide a structured PR assessment to help maintainers quickly evaluate the value, correctness, and merge priority of pull requests.',
'',
'## Tech Stack',
'- Electron 37.x + React 19.x + TypeScript 5.8.x (strict mode)',
'- Express 5.x (WebUI server), Better SQLite3 (local DB)',
'- Arco Design 2.x (UI), UnoCSS 66.x (atomic CSS), Monaco Editor 4.x',
'- Anthropic SDK, Google GenAI, OpenAI SDK, MCP SDK',
'- Webpack 6.x, Electron Forge 7.8.x',
'',
'## Architecture',
'- Multi-process: Main (Electron + DB + IPC), Renderer (React UI), Worker (background AI tasks)',
'- IPC via secure contextBridge isolation',
'- WebUI: Express + WebSocket + JWT auth',
'- Agent system: channels/, agent/ directories',
'',
'## Priority Definitions',
'',
'| Priority | Meaning | Examples |',
'|----------|---------|----------|',
'| P0 Critical | Must merge immediately | Crash fix, data loss, security vulnerability |',
'| P1 High | Should merge within days | Core bug fix, urgently needed feature |',
'| P2 Medium | Merge in regular cycle | Enhancement, minor bug, UX improvement |',
'| P3 Low | Merge when convenient | Cosmetic, docs, minor refactor |',
'',
'## Output Format',
'',
'**CRITICAL: Detect the language of the PR title and body. Write your ENTIRE assessment in that same language.** If the PR is in Chinese, write in Chinese. If in English, write in English. If mixed or unclear, default to English.',
'',
'Use this EXACT structure:',
'',
'# PR Assessment',
'',
'## Change Overview',
'[2-4 sentences summarizing what this PR does and why]',
'',
'## Linked Issue Analysis',
'[If linked issues exist]:',
'| Issue | Status | Addressed? |',
'|-------|--------|------------|',
'| #N: [title] | Open/Closed | Yes/Partially/No |',
'',
'[For each issue, analyze whether the PR actually solves the problem described]',
'',
'[If no linked issues]:',
'State that no issues are linked, then infer the PR\'s purpose and motivation from its title, description, and code changes.',
'',
'## Change Type',
'- **Type**: Bug Fix / New Feature / Enhancement / Refactor / Docs / Build / Test / ...',
'- **Scope**: Core / UI / Backend / Build / Config / ...',
'- **Breaking Changes**: Yes/No [explain if yes]',
'',
'## Merge Recommendation',
'One of: ✅ Recommend Merge / ⚠️ Conditional Merge / ❌ Do Not Merge',
'',
'**Rationale**: [Specific reasons for the recommendation]',
'',
'## Merge Priority',
'**Priority**: P0-P3 — Critical/High/Medium/Low',
'',
'**Rationale**: [Why this priority level]',
'',
'## Risk Assessment',
'| Risk | Level | Details |',
'|------|-------|---------|',
'| Regression risk | Low/Medium/High | ... |',
'| Security impact | None/Low/Medium/High | ... |',
'| Performance impact | None/Low/Medium/High | ... |',
'| Compatibility | None/Low/Medium/High | ... |',
'',
'## Reviewer Tips',
'- [Advice for human reviewers]',
'- [Key files or logic to focus on]',
'',
'## Rules',
'- Be objective and evidence-based. Base your assessment on actual code changes and issue content.',
'- Do NOT inflate risk or severity. If the PR is straightforward and safe, say so.',
'- Do NOT fabricate concerns. It is fine to have a short, positive assessment for clean PRs.',
'- For Linked Issue Analysis, carefully compare the issue description with the actual code changes to determine if the problem is truly addressed.',
'- Be specific in Reviewer Tips — point to exact files, functions, or patterns that need attention.',
'',
'## Footer',
'After the Reviewer Tips section, ALWAYS append this exact footer:',
'',
'---',
'',
'*🤖 This assessment was generated by AI and serves as a reference for maintainers. Please use your own judgment for final merge decisions.*',
].join('\n');
const userPrompt = [
'## Pull Request',
'',
`**Title**: ${prMeta.title}`,
`**Author**: ${prMeta.author}`,
`**Labels**: ${prMeta.labels.length > 0 ? prMeta.labels.join(', ') : 'None'}`,
`**Branch**: ${prMeta.head_branch} → ${prMeta.base_branch}`,
`**Stats**: ${fileCount} files changed, +${additions} -${deletions} (${totalLines} total lines)`,
'',
'**Description**:',
prMeta.body || '(No description provided)',
'',
issuesSection,
'',
'**Changed Files**:',
fileListRaw,
'',
'## Diff',
'',
diff,
'',
'## Full File Contents (for cross-file analysis)',
'',
fileContents || '(No file contents available)',
].join('\n');
fs.writeFileSync(`${tmpDir}/system_prompt.txt`, systemPrompt);
fs.writeFileSync(`${tmpDir}/user_prompt.txt`, userPrompt);
- name: 'Call GPT for PR assessment'
if: steps.gather.outputs.skip != 'true'
uses: './.github/actions/call-openai'
with:
openai_api_key: '${{ secrets.OPENAI_API_KEY }}'
output_file: 'assessment_body.txt'
diff_truncated: '${{ steps.gather.outputs.diff_truncated }}'
contents_truncated: '${{ steps.read_contents.outputs.contents_truncated }}'
- name: 'Post or update assessment comment'
if: steps.gather.outputs.skip != 'true' && success()
uses: 'actions/github-script@v7'
env:
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
with:
script: |
const fs = require('fs');
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
const assessmentBody = fs.readFileSync(`${tmpDir}/assessment_body.txt`, 'utf-8');
const prNumber = context.payload.pull_request?.number
|| Number(process.env.INPUT_PR_NUMBER);
const MARKER = '<!-- gpt-pr-assessment-bot -->';
const fullBody = `${MARKER}\n\n${assessmentBody}`;
// Check for existing assessment comment to update
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existingComment = comments.data.find(comment =>
comment.body.includes(MARKER)
);
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: fullBody,
});
core.info(`Updated existing assessment comment (id: ${existingComment.id}) on PR #${prNumber}`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: fullBody,
});
core.info(`Created new assessment comment on PR #${prNumber}`);
}
+294
View File
@@ -0,0 +1,294 @@
name: '🤖 GPT Review'
on:
workflow_call:
inputs:
pr_number:
description: 'PR number to review'
required: false
type: string
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: string
concurrency:
group: 'gpt-review-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
review:
runs-on: 'ubuntu-latest'
timeout-minutes: 10
permissions:
contents: 'read'
pull-requests: 'write'
steps:
- name: 'Checkout repository'
uses: 'actions/checkout@v4'
- name: 'Gather PR diff and changed files'
id: 'gather'
uses: './.github/actions/gather-pr-diff'
with:
pr_number: '${{ inputs.pr_number }}'
- name: 'Read changed file contents'
id: 'read_contents'
if: steps.gather.outputs.skip != 'true'
uses: './.github/actions/read-file-contents'
- name: 'Construct GPT prompts'
id: 'prompts'
if: steps.gather.outputs.skip != 'true'
uses: 'actions/github-script@v7'
env:
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
FILE_COUNT: '${{ steps.gather.outputs.file_count }}'
ADDITIONS: '${{ steps.gather.outputs.additions }}'
DELETIONS: '${{ steps.gather.outputs.deletions }}'
TOTAL_LINES: '${{ steps.gather.outputs.total_lines }}'
with:
script: |
const fs = require('fs');
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
// Read PR metadata: from event context or fetch via API for manual trigger
let prTitle, prBody, prAuthor;
if (context.payload.pull_request) {
prTitle = context.payload.pull_request.title || '';
prBody = context.payload.pull_request.body || '';
prAuthor = context.payload.pull_request.user.login || '';
} else {
const prNum = Number(process.env.INPUT_PR_NUMBER);
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNum,
});
prTitle = pr.title || '';
prBody = pr.body || '';
prAuthor = pr.user.login || '';
}
const fileCount = process.env.FILE_COUNT || '0';
const additions = process.env.ADDITIONS || '0';
const deletions = process.env.DELETIONS || '0';
const totalLines = process.env.TOTAL_LINES || '0';
// Read large data from temp files
const diff = fs.readFileSync(`${tmpDir}/pr_diff.txt`, 'utf-8');
const fileListRaw = fs.readFileSync(`${tmpDir}/file_list.json`, 'utf-8');
let fileContents = '';
try {
fileContents = fs.readFileSync(`${tmpDir}/file_contents.txt`, 'utf-8');
} catch {
core.warning('Could not read file contents, proceeding with diff only');
}
const systemPrompt = [
'You are a world-class code review expert for the AionUi project — a cross-platform Electron desktop application that provides a unified AI agent graphical interface.',
'',
'## Tech Stack',
'- Electron 37.x + React 19.x + TypeScript 5.8.x (strict mode)',
'- Express 5.x (WebUI server), Better SQLite3 (local DB)',
'- Arco Design 2.x (UI), UnoCSS 66.x (atomic CSS), Monaco Editor 4.x',
'- Anthropic SDK, Google GenAI, OpenAI SDK, MCP SDK',
'- Webpack 6.x, Electron Forge 7.8.x',
'',
'## Architecture',
'- Multi-process: Main (Electron + DB + IPC), Renderer (React UI), Worker (background AI tasks)',
'- IPC via secure contextBridge isolation',
'- WebUI: Express + WebSocket + JWT auth',
'- Agent system: channels/, agent/ directories',
'- Security-sensitive paths: packages/desktop/src/process/, packages/desktop/src/process/agent/, packages/desktop/src/process/webserver/auth/',
'',
'## Code Conventions',
'- TypeScript strict mode, prefer `type` over `interface`',
'- Functional React components only, hooks with `use*` prefix',
'- IMMUTABILITY: always create new objects, never mutate',
'- Path aliases: @/*, @process/*, @renderer/*, @worker/*',
'- UnoCSS atomic classes + CSS modules',
'- English code comments, conventional commits',
'',
'## ESLint Key Rules',
'- @typescript-eslint/consistent-type-definitions: prefer type',
'- @typescript-eslint/no-explicit-any: warn',
'- no-console: warn (no console.log in production code — flag any NEW console.log added by the PR)',
'- react-hooks/rules-of-hooks: error',
'- react-hooks/exhaustive-deps: warn',
'',
'## Review Dimensions (Priority Order)',
'1. **Correctness** — Logic errors, unhandled edge cases, race conditions, incorrect API usage',
'2. **Security** — Injection, insecure storage, access control, secrets exposure, OWASP Top 10',
'3. **Performance** — Bottlenecks, memory leaks, unnecessary computation, inefficient data structures',
'4. **Maintainability** — Readability, modularity, naming, adherence to project conventions',
'5. **Immutability** — Object mutations, array mutations, state mutations (CRITICAL for this project)',
'6. **Error Handling** — Missing try/catch, swallowed errors, unhelpful error messages',
'7. **Type Safety** — any usage, missing types, incorrect type assertions, unsafe casts',
'8. **Debug Hygiene** — New console.log/console.debug statements left in production code (flag as HIGH if clearly debug-only, MEDIUM if arguable)',
'',
'## Cross-File Analysis',
'You are provided with both the diff AND the full content of changed files. Use the full file content to:',
'- Trace function call chains across files',
'- Verify exported/imported symbols exist and are correct',
'- Check that cache/state management is consistent',
'- Identify dead code introduced by the changes',
'- Verify error handling propagation across module boundaries',
'',
'## Output Format',
'',
'**CRITICAL: Detect the language of the PR title and body. Write your ENTIRE review in that same language.** If the PR is in Chinese, write in Chinese. If in English, write in English. If mixed or unclear, default to English.',
'',
'Structure your review as a single comprehensive comment using this EXACT format:',
'',
'# Code Review',
'',
'## CRITICAL Issues',
'',
'### 1. [Issue Title]',
'',
'**File**: `path/to/file.ts:LINE-LINE`',
'',
'```typescript',
'// problematic code snippet',
'```',
'',
'**Problem**: [Detailed explanation of why this is critical]',
'',
'**Fix**: [Concrete fix suggestion with code if applicable]',
'',
'---',
'',
'## HIGH Issues',
'',
'### N. [Issue Title]',
'',
'**File**: `path/to/file.ts:LINE-LINE`',
'',
'**Problem**: [Explanation]',
'',
'**Fix**: [Suggestion]',
'',
'---',
'',
'## MEDIUM Issues',
'',
'### N. [Issue Title]',
'',
'**File**: `path/to/file.ts`',
'',
'[Description of the issue and recommendation]',
'',
'---',
'',
'## Summary',
'',
'| Level | Count |',
'|-------|-------|',
'| CRITICAL | N |',
'| HIGH | N |',
'| MEDIUM | N |',
'',
'## Rules',
'- ONLY report issues you are **highly confident** about. If you are unsure whether something is a real problem, DO NOT report it.',
'- **CRITICAL** means the code WILL break, crash, or cause data loss/security breach in production. Theoretical edge cases or defensive programming suggestions are NOT critical.',
'- **HIGH** means the code has a clear bug or significant problem that will likely manifest in normal usage. Stylistic preferences or "could be improved" suggestions are NOT high.',
'- **MEDIUM** is for genuine improvements, not hypothetical concerns.',
'- Do NOT inflate severity. If you cannot point to a specific, realistic scenario where the issue causes a failure, lower the severity or omit it.',
'- Do NOT fabricate issues to fill sections. It is perfectly fine — and preferred — to report fewer issues or none at all.',
'- If a section has no issues, OMIT that section entirely (do not write "None found").',
'- Include code snippets from the diff to pinpoint exact locations.',
'- Each issue must have a concrete, actionable fix suggestion.',
'- Number issues sequentially across all sections (1, 2, 3...).',
'- For CRITICAL and HIGH issues, always include the specific file path and line numbers.',
'- Do NOT comment on: lock files, auto-generated files, license headers, formatting-only changes, or well-known API parameters used correctly.',
'- If the PR has NO issues at any level, output exactly: "# Code Review\\n\\nNo issues found. The changes are clean and well-implemented."',
'',
'## Footer',
'After the Summary section, ALWAYS append this exact footer:',
'',
'---',
'',
'*🤖 This review was generated by AI and may contain inaccuracies. Please focus on issues you agree with and feel free to disregard any that seem incorrect. Thank you for your contribution!*',
].join('\n');
const userPrompt = [
'## Pull Request',
'',
`**Title**: ${prTitle}`,
`**Author**: ${prAuthor}`,
`**Stats**: ${fileCount} files changed, +${additions} -${deletions} (${totalLines} total lines)`,
'',
'**Description**:',
prBody || '(No description provided)',
'',
'**Changed Files**:',
fileListRaw,
'',
'## Diff',
'',
diff,
'',
'## Full File Contents (for cross-file analysis)',
'',
fileContents || '(No file contents available)',
].join('\n');
fs.writeFileSync(`${tmpDir}/system_prompt.txt`, systemPrompt);
fs.writeFileSync(`${tmpDir}/user_prompt.txt`, userPrompt);
- name: 'Call OpenAI GPT for code review'
if: steps.gather.outputs.skip != 'true'
uses: './.github/actions/call-openai'
with:
openai_api_key: '${{ secrets.OPENAI_API_KEY }}'
output_file: 'review_body.txt'
diff_truncated: '${{ steps.gather.outputs.diff_truncated }}'
contents_truncated: '${{ steps.read_contents.outputs.contents_truncated }}'
- name: 'Submit review to PR'
if: steps.gather.outputs.skip != 'true' && success()
uses: 'actions/github-script@v7'
env:
INPUT_PR_NUMBER: '${{ inputs.pr_number }}'
with:
script: |
const fs = require('fs');
const tmpDir = process.env.RUNNER_TEMP || '/tmp';
const reviewBody = fs.readFileSync(`${tmpDir}/review_body.txt`, 'utf-8');
const prNumber = context.payload.pull_request?.number
|| Number(process.env.INPUT_PR_NUMBER);
core.info(`Submitting COMMENT review to PR #${prNumber}`);
try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
event: 'COMMENT',
body: reviewBody,
});
core.info('Review submitted successfully via createReview');
} catch (reviewError) {
core.warning(`createReview failed: ${reviewError.message}, falling back to comment`);
try {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `<!-- gpt-review-bot -->\n\n${reviewBody}`,
});
core.info('Review posted as comment (fallback)');
} catch (commentError) {
core.setFailed(`Failed to post review: ${commentError.message}`);
}
}
+249
View File
@@ -0,0 +1,249 @@
name: Pack Web CLI
on:
workflow_call:
inputs:
ref:
description: 'Git ref to checkout (leave empty to use the triggering commit)'
type: string
default: ''
append_commit_hash:
description: 'Append short commit hash to artifact names'
type: boolean
default: false
skip_code_quality:
description: 'Skip code quality checks (caller already ran them)'
type: boolean
default: false
workflow_dispatch:
inputs:
ref:
description: 'Git ref to checkout (leave empty to use the triggering commit)'
type: string
default: ''
skip_code_quality:
description: 'Skip code quality checks'
type: boolean
default: false
env:
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
jobs:
code-quality:
name: Code Quality
runs-on: ubuntu-latest
if: ${{ !inputs.skip_code_quality }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: bun run postinstall || true
- name: Run ESLint
run: bun run lint
- name: Check Prettier formatting
run: bun run format:check
- name: TypeScript type check
run: bunx tsc --noEmit
- name: Run unit tests
run: bunx vitest run
pack-web-cli:
name: Pack web-cli ${{ matrix.platform }}-${{ matrix.arch }}
runs-on: ${{ matrix.os }}
needs: code-quality
if: ${{ always() && (inputs.skip_code_quality || needs.code-quality.result == 'success') }}
strategy:
fail-fast: false
matrix:
include:
- { platform: darwin, arch: arm64, os: macos-14 }
- { platform: darwin, arch: x64, os: macos-14 }
- { platform: linux, arch: x64, os: ubuntu-latest }
- { platform: linux, arch: arm64, os: ubuntu-24.04-arm }
- { platform: win32, arch: x64, os: windows-2022 }
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Get commit hash
id: commit
shell: bash
run: |
SHORT=$(git rev-parse --short HEAD)
echo "short=$SHORT" >> $GITHUB_OUTPUT
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build desktop renderer (for static files)
run: bunx electron-vite build --config packages/desktop/electron.vite.config.ts
env:
NODE_OPTIONS: '--max-old-space-size=8192'
- name: Pack web-cli tarball
shell: bash
run: node scripts/pack-web-cli.js
env:
PACK_PLATFORM: ${{ matrix.platform }}
PACK_ARCH: ${{ matrix.arch }}
# Version is pinned in repo-root package.json (aioncoreVersion).
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload tarball artifact
uses: actions/upload-artifact@v6
with:
name: ${{ inputs.append_commit_hash && format('web-cli-{0}-{1}-{2}', matrix.platform, matrix.arch, steps.commit.outputs.short) || format('web-cli-{0}-{1}', matrix.platform, matrix.arch) }}
path: |
dist-web-cli/*.tar.gz
dist-web-cli/*.sha256
retention-days: 7
prepare-install-script:
name: Prepare install-web.sh for release
runs-on: ubuntu-latest
needs: pack-web-cli
if: ${{ always() && needs.pack-web-cli.result == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Get commit hash
id: commit
shell: bash
run: |
SHORT=$(git rev-parse --short HEAD)
echo "short=$SHORT" >> $GITHUB_OUTPUT
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Get version from package.json
id: version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Replace __VERSION__ placeholder in install-web.sh
run: |
mkdir -p dist-scripts
sed "s/__VERSION__/${{ steps.version.outputs.version }}/g" scripts/install-web.sh > dist-scripts/install-web.sh
chmod +x dist-scripts/install-web.sh
- name: Upload install-web.sh artifact
uses: actions/upload-artifact@v6
with:
name: ${{ inputs.append_commit_hash && format('install-web-script-{0}', steps.commit.outputs.short) || 'install-web-script' }}
path: dist-scripts/install-web.sh
retention-days: 7
smoke-test:
name: Smoke test web-cli tarball (Linux x86_64)
runs-on: ubuntu-latest
needs: pack-web-cli
if: ${{ always() && needs.pack-web-cli.result == 'success' }}
container:
image: debian:bookworm-slim
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install dependencies
run: |
apt-get update
apt-get install -y curl tar gzip nodejs
- name: Download linux-x86_64 tarball
uses: actions/download-artifact@v7
with:
name: web-cli-linux-x64
path: dist-web-cli
- name: Run smoke test
shell: bash
run: |
chmod +x scripts/smoke-test-web-cli.sh
TARBALL=$(ls dist-web-cli/*.tar.gz | head -1)
bash scripts/smoke-test-web-cli.sh "$TARBALL"
smoke-test-install:
name: Smoke test install-web.sh (Linux x86_64)
runs-on: ubuntu-latest
needs: [pack-web-cli, prepare-install-script]
if: ${{ always() && needs.pack-web-cli.result == 'success' && needs.prepare-install-script.result == 'success' }}
container:
image: debian:bookworm-slim
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install dependencies
run: |
apt-get update
apt-get install -y curl tar gzip nodejs coreutils
- name: Resolve version
id: version
shell: bash
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Resolved version: $VERSION"
- name: Download linux-x86_64 tarball
uses: actions/download-artifact@v7
with:
name: web-cli-linux-x64
path: /tmp/releases/v${{ steps.version.outputs.version }}
- name: Download install-web.sh
uses: actions/download-artifact@v7
with:
name: install-web-script
path: /tmp/releases
- name: Run smoke test
shell: bash
run: |
chmod +x scripts/smoke-test-install-web.sh
bash scripts/smoke-test-install-web.sh file:///tmp/releases ${{ steps.version.outputs.version }}
+66
View File
@@ -0,0 +1,66 @@
name: 'PR Checks (docs only)'
on:
pull_request:
types: [opened, synchronize, edited, ready_for_review]
branches: [main, dev]
paths:
- '**/*.md'
- 'docs/**'
- '.vscode/**'
- '.github/ISSUE_TEMPLATE/**'
permissions:
contents: read
jobs:
code-quality:
name: Code Quality
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: npm run postinstall || true
- name: Install prek
run: npm install -g @j178/prek
- name: Run prek checks
run: prek run --from-ref origin/${{ github.base_ref }} --to-ref HEAD
unit-tests-ubuntu:
name: Unit Tests (ubuntu-latest)
runs-on: ubuntu-latest
steps:
- run: echo "Docs-only PR, skipping unit tests."
unit-tests-macos:
name: Unit Tests (macos-14)
runs-on: ubuntu-latest
steps:
- run: echo "Docs-only PR, skipping unit tests."
unit-tests-windows:
name: Unit Tests (windows-2022)
runs-on: ubuntu-latest
steps:
- run: echo "Docs-only PR, skipping unit tests."
+535
View File
@@ -0,0 +1,535 @@
name: 'PR Checks'
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to run checks on'
required: true
type: string
skip_build_test:
description: 'Skip the build test job (saves ~45 min)'
type: boolean
required: false
default: false
pull_request:
types: [opened, synchronize, edited, closed, ready_for_review]
branches: [main, dev]
paths-ignore:
- '**/*.md'
- 'docs/**'
- '.vscode/**'
- '.github/ISSUE_TEMPLATE/**'
concurrency:
group: pr-checks-${{ github.event.pull_request.number || inputs.pr_number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
issues: read
checks: write
id-token: write
env:
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
jobs:
# Cancel in-progress runs when PR is closed (merged or manually closed)
cancel-if-closed:
name: Cancel if PR closed
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- run: echo "PR closed, cancelling in-progress runs via concurrency."
# Job 1: Code quality checks (TypeScript, Oxlint, Oxfmt)
code-quality:
name: Code Quality
if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve PR context
uses: ./.github/actions/checkout-pr
with:
pr_number: ${{ inputs.pr_number }}
github_token: ${{ github.token }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: npm run postinstall || true
- name: Install prek
run: npm install -g @j178/prek
- name: Run prek checks
run: prek run --from-ref origin/${{ env.PR_BASE_REF }} --to-ref HEAD
# Job 2: Unit tests across all platforms
unit-tests:
name: Unit Tests (${{ matrix.os }})
if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-14, windows-2022]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Resolve PR context
uses: ./.github/actions/checkout-pr
with:
pr_number: ${{ inputs.pr_number }}
github_token: ${{ github.token }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true
- name: Disable Windows Defender (Windows only)
if: matrix.os == 'windows-2022'
shell: pwsh
continue-on-error: true
run: |
try { Set-MpPreference -DisableRealtimeMonitoring $true } catch { Write-Host "::warning::Failed to disable real-time monitoring: $_" }
try { Add-MpPreference -ExclusionPath "${{ github.workspace }}" } catch { Write-Host "::warning::Failed to add exclusion path: $_" }
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: npm run postinstall || true
- name: Run extension system tests
run: bunx vitest run
# Job 3: Coverage test (Linux only)
coverage-tests:
name: Coverage Test
if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Resolve PR context
uses: ./.github/actions/checkout-pr
with:
pr_number: ${{ inputs.pr_number }}
github_token: ${{ github.token }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: npm run postinstall || true
- name: Run coverage tests
id: coverage
continue-on-error: true
run: bun run test:coverage
- name: Upload coverage to Codecov (Linux coverage only)
if: always() && hashFiles('coverage/lcov.info') != '' && github.repository == 'iOfficeAI/AionUi'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
use_oidc: ${{ secrets.CODECOV_TOKEN == '' }}
files: coverage/lcov.info
fail_ci_if_error: false
verbose: true
- name: Skip Codecov upload when preconditions are not met
if: always() && hashFiles('coverage/lcov.info') != '' && github.repository != 'iOfficeAI/AionUi'
shell: bash
run: |
echo 'Skipping Codecov upload due to unmet preconditions.'
echo "Repository: ${{ github.repository }}"
- name: Coverage result summary
if: always()
shell: bash
run: |
if [ "${{ steps.coverage.outcome }}" = "failure" ]; then
echo "::warning::Coverage tests failed (non-blocking). Check logs for failed test details."
echo "## Coverage check (non-blocking warning)" >> $GITHUB_STEP_SUMMARY
echo "Coverage command failed in this run. Please review test failures in logs." >> $GITHUB_STEP_SUMMARY
if [ -f coverage/lcov.info ]; then
echo "lcov.info exists, Codecov upload can still run." >> $GITHUB_STEP_SUMMARY
else
echo "lcov.info not found, Codecov upload was skipped." >> $GITHUB_STEP_SUMMARY
fi
else
echo "## Coverage check" >> $GITHUB_STEP_SUMMARY
echo "Coverage command passed." >> $GITHUB_STEP_SUMMARY
if [ -f coverage/lcov.info ]; then
echo "Uploaded Linux (ubuntu-latest) coverage to Codecov." >> $GITHUB_STEP_SUMMARY
else
echo "Coverage passed but lcov.info is missing; Codecov upload was skipped." >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Upload coverage artifacts
if: always() && hashFiles('coverage/lcov.info') != ''
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
if-no-files-found: warn
i18n-check:
name: I18n Check
if: github.event_name == 'workflow_dispatch' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: npm run postinstall || true
- name: Run i18n validation (warning-only)
id: i18n
shell: bash
run: |
set -o pipefail
node scripts/check-i18n.js 2>&1 | tee i18n-check.log
- name: Publish i18n summary
if: always()
shell: bash
run: |
echo "## i18n validation" >> $GITHUB_STEP_SUMMARY
if grep -q "⚠️" i18n-check.log; then
echo "Missing/incomplete translations found. Please review warnings below." >> $GITHUB_STEP_SUMMARY
else
echo "No i18n warnings detected." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details><summary>i18n log</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```text' >> $GITHUB_STEP_SUMMARY
cat i18n-check.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
# Job 4: Build test across representative PR platforms (parallel with code-quality and unit-tests)
build-test:
name: Build Test (${{ matrix.platform }})
if: github.event.action != 'closed' && github.event.pull_request.draft == false && inputs.skip_build_test != true
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- platform: 'macos-arm64'
os: 'macos-14'
arch: 'arm64'
target: '--mac'
build_args: '--mac --arm64'
unpacked_dir: 'mac-arm64'
- platform: 'windows-x64'
os: 'windows-2022'
arch: 'x64'
target: '--win'
build_args: '--win --x64'
unpacked_dir: 'win-unpacked'
- platform: 'linux-x64'
os: 'ubuntu-latest'
arch: 'x64'
target: '--linux'
build_args: '--linux --x64'
unpacked_dir: 'linux-unpacked'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
cache: true
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: npm run postinstall || true
- name: Install Linux dependencies
if: matrix.platform == 'linux-x64'
run: |
sudo apt-get update
sudo apt-get install -y build-essential python3 python3-pip pkg-config libsqlite3-dev fakeroot dpkg-dev rpm libnss3-dev libatk-bridge2.0-dev libdrm2 libxkbcommon-dev libxss1 libatspi2.0-dev libgtk-3-dev libxrandr2 libasound2-dev
- name: Get Electron version
id: electron-version
shell: bash
run: |
ELECTRON_VERSION=$(node -p "require('./package.json').devDependencies.electron.replace(/[\^~]/g, '')")
echo "version=$ELECTRON_VERSION" >> $GITHUB_OUTPUT
echo "Electron version: $ELECTRON_VERSION"
# Restore Electron/Electron-Builder caches before install-app-deps
- name: Cache Electron artifacts
id: electron-cache
uses: actions/cache@v4
with:
path: |
${{ runner.temp }}/.cache/electron
${{ runner.temp }}/.cache/electron-builder
~/.cache/electron
~/.cache/electron-builder
${{ env.LOCALAPPDATA }}\electron-builder\Cache
key: electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('package.json', 'bun.lock') }}
restore-keys: |
electron-cache-${{ matrix.platform }}-${{ matrix.arch }}-
electron-cache-${{ matrix.platform }}-
- name: Cache status
run: echo "electron-cache-hit=${{ steps.electron-cache.outputs.cache-hit }}"
- name: Rebuild native modules for Electron (non-Windows)
if: "!startsWith(matrix.platform, 'windows')"
run: bunx electron-builder install-app-deps
env:
npm_config_runtime: electron
npm_config_disturl: https://electronjs.org/headers
ELECTRON_CACHE: ${{ runner.temp }}/.cache/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/.cache/electron-builder
- name: Setup MSBuild (Windows only)
if: startsWith(matrix.platform, 'windows')
uses: microsoft/setup-msbuild@v2
with:
vs-version: '17.0'
- name: Build test (Windows x64)
if: matrix.platform == 'windows-x64'
shell: pwsh
run: |
Write-Host "=========================================="
Write-Host "BUILD TEST: ${{ matrix.platform }}"
Write-Host "=========================================="
node scripts/build-with-builder.js x64 --win --x64
Write-Host "✓Build test passed for ${{ matrix.platform }}"
env:
NODE_OPTIONS: '--max-old-space-size=8192'
npm_config_runtime: electron
npm_config_arch: x64
npm_config_target_arch: x64
npm_config_disturl: https://electronjs.org/headers
npm_config_build_from_source: true
MSVS_VERSION: 2022
GYP_MSVS_VERSION: 2022
WindowsTargetPlatformVersion: 10.0.19041.0
CI: true
GH_TOKEN: ${{ github.token }}
- name: Build test (non-Windows)
if: "!startsWith(matrix.platform, 'windows')"
shell: bash
run: |
echo "=========================================="
echo "BUILD TEST: ${{ matrix.platform }}"
echo "=========================================="
node scripts/build-with-builder.js auto ${{ matrix.build_args }}
echo "✓Build test passed for ${{ matrix.platform }}"
env:
NODE_OPTIONS: '--max-old-space-size=8192'
npm_config_arch: ${{ matrix.arch }}
npm_config_target_arch: ${{ matrix.arch }}
npm_config_runtime: electron
npm_config_target: ${{ steps.electron-version.outputs.version }}
npm_config_disturl: https://electronjs.org/headers
CI: true
GH_TOKEN: ${{ github.token }}
- name: Verify build artifacts exist
shell: bash
run: |
echo "=========================================="
echo "VERIFY ARTIFACTS: ${{ matrix.platform }}"
echo "=========================================="
ls -lah out || true
case "${{ matrix.platform }}" in
windows-*)
ls out/*.exe out/*latest*.yml >/dev/null
;;
macos-*)
ls out/*.dmg out/*.zip out/*latest*.yml >/dev/null
;;
linux-x64)
ls out/*.deb out/*latest*.yml >/dev/null
;;
esac
- name: Silent install smoke test (Windows x64)
if: matrix.platform == 'windows-x64'
shell: pwsh
run: |
Write-Host "=========================================="
Write-Host "SMOKE INSTALL: windows-x64"
Write-Host "=========================================="
$installer = Get-ChildItem -Path out -Filter "AionUi-*-win-*.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $installer) {
throw "No Windows installer found in out/"
}
Write-Host "Using installer: $($installer.FullName)"
Start-Process -FilePath $installer.FullName -ArgumentList '/S' -Wait -NoNewWindow
$candidates = @(
"$env:LOCALAPPDATA\\Programs\\AionUi\\AionUi.exe",
"$env:ProgramFiles\\AionUi\\AionUi.exe",
"$env:ProgramFiles(x86)\\AionUi\\AionUi.exe"
)
$installedExe = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $installedExe) {
throw "Silent install finished but app executable not found in expected locations"
}
Write-Host "Installed executable: $installedExe"
- name: Install smoke test (macOS arm64)
if: matrix.platform == 'macos-arm64'
shell: bash
run: |
set -euo pipefail
echo "=========================================="
echo "SMOKE INSTALL: macos-arm64"
echo "=========================================="
DMG_FILE=$(ls out/*.dmg | head -n 1)
MOUNT_POINT="/tmp/aionui-smoke-mount"
APP_DIR="/tmp/aionui-smoke-app"
rm -rf "$MOUNT_POINT" "$APP_DIR"
mkdir -p "$MOUNT_POINT" "$APP_DIR"
hdiutil attach "$DMG_FILE" -nobrowse -mountpoint "$MOUNT_POINT"
cp -R "$MOUNT_POINT"/*.app "$APP_DIR"/
hdiutil detach "$MOUNT_POINT"
APP_PATH=$(ls -d "$APP_DIR"/*.app | head -n 1)
APP_BIN="$APP_PATH/Contents/MacOS/AionUi"
test -x "$APP_BIN"
"$APP_BIN" --version || true
- name: Install smoke test (Linux)
if: matrix.platform == 'linux-x64'
shell: bash
run: |
set -euo pipefail
echo "=========================================="
echo "SMOKE INSTALL: linux-x64"
echo "=========================================="
DEB_FILE=$(ls out/*.deb | head -n 1)
PKG_NAME=$(dpkg-deb -f "$DEB_FILE" Package)
sudo dpkg -i "$DEB_FILE" || sudo apt-get install -f -y
INSTALLED_BIN=$(dpkg -L "$PKG_NAME" | grep -Ei '/(bin|opt)/.*(aionui)$' | head -n 1 || true)
if [ -z "$INSTALLED_BIN" ]; then
echo "Package files:"
dpkg -L "$PKG_NAME" | head -n 50
echo "No installed executable path matched expected pattern"
exit 1
fi
test -x "$INSTALLED_BIN"
# Job 5: Test release scripts (fast, no build required)
release-script-test:
name: Release Script Test
if: github.event.action != 'closed' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create mock build artifacts
shell: bash
run: bash scripts/create-mock-release-artifacts.sh build-artifacts
- name: Run prepare-release-assets script
shell: bash
env:
MOCK_VERSION: '1.0.0'
run: bash scripts/prepare-release-assets.sh build-artifacts release-assets
- name: Validate script output
shell: bash
run: bash scripts/verify-release-assets.sh release-assets
+68
View File
@@ -0,0 +1,68 @@
name: '✓PR E2E Artifacts'
on:
workflow_dispatch:
concurrency:
group: pr-e2e-artifacts-${{ github.run_id }}
cancel-in-progress: true
permissions:
contents: read
env:
BUN_INSTALL_REGISTRY: 'https://registry.npmjs.org/'
jobs:
e2e-artifacts:
name: E2E Artifacts (Linux)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup just
uses: extractions/setup-just@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run postinstall
run: npm run postinstall || true
- name: Install Linux display dependencies (E2E)
run: |
sudo apt-get update
sudo apt-get install -y libgbm-dev xvfb
- name: Run E2E tests (with screenshots)
run: xvfb-run --auto-servernum just e2e-test
env:
CI: true
E2E_DEV: '1'
E2E_SCREENSHOTS: '1'
AIONUI_EXTENSIONS_PATH: ${{ github.workspace }}/examples
- name: Upload E2E report (self-contained with screenshots & traces)
if: always()
uses: actions/upload-artifact@v6
with:
name: e2e-report-linux-pr
path: |
tests/e2e/report/
tests/e2e/results/
tests/e2e/screenshots/
retention-days: 14
if-no-files-found: ignore
+355
View File
@@ -0,0 +1,355 @@
name: '📋 Project Automation'
on:
workflow_dispatch: # disabled: requires GitHub App credentials (APP_ID + APP_PRIVATE_KEY)
# Minimal permissions at workflow level, jobs use PROJECT_TOKEN secret
permissions:
contents: 'read'
env:
PROJECT_NUMBER: 5
ORG_NAME: iOfficeAI
PROJECT_ID: PVT_kwDOCKhK-M4BN1Ah
STATUS_FIELD_ID: PVTSSF_lADOCKhK-M4BN1Ahzg8tcak
# Status option IDs
STATUS_TO_TRIAGE: 8e8f2d59
STATUS_BLOCKED: cd8368e3
STATUS_READY_FOR_WORK: 864da50e
STATUS_IN_PROGRESS: a5c1c67b
STATUS_CLOSED: 91437030
STATUS_DONE: 1aa2425f
jobs:
# ============================================
# Issue Events: Add to Project & manage status
# ============================================
add-issue-to-project:
name: Add Issue to Project
runs-on: ubuntu-latest
if: github.event_name == 'issues' && github.event.action == 'opened'
permissions:
contents: 'read'
outputs:
item_id: ${{ steps.add.outputs.itemId }}
steps:
- name: Mint identity token
id: mint_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Add issue to project
id: add
uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/iOfficeAI/projects/5
github-token: ${{ steps.mint_token.outputs.token }}
set-issue-initial-status:
name: Set Issue Initial Status
runs-on: ubuntu-latest
needs: add-issue-to-project
if: github.event_name == 'issues' && github.event.action == 'opened' && needs.add-issue-to-project.outputs.item_id
permissions:
contents: 'read'
steps:
- name: Mint identity token
id: mint_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Set status to To Triage
uses: actions/github-script@v7
with:
github-token: ${{ steps.mint_token.outputs.token }}
script: |
const mutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
`;
await github.graphql(mutation, {
projectId: process.env.PROJECT_ID,
itemId: '${{ needs.add-issue-to-project.outputs.item_id }}',
fieldId: process.env.STATUS_FIELD_ID,
optionId: process.env.STATUS_TO_TRIAGE
});
core.info('Issue added to project with status: To Triage');
update-issue-on-close:
name: Update Issue on Close
runs-on: ubuntu-latest
if: github.event_name == 'issues' && github.event.action == 'closed'
permissions:
contents: 'read'
steps:
- name: Mint identity token
id: mint_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Update issue status to Done
uses: actions/github-script@v7
with:
github-token: ${{ steps.mint_token.outputs.token }}
script: |
const contentId = context.payload.issue.node_id;
// Find the item in project
const query = `
query($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
items(first: 100) {
nodes {
id
content { ... on Issue { id } }
}
}
}
}
}
`;
const result = await github.graphql(query, { org: 'iOfficeAI', number: 5 });
const item = result.organization.projectV2.items.nodes.find(i => i.content?.id === contentId);
if (!item) {
core.info('Issue not found in project, skipping');
return;
}
const mutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
`;
await github.graphql(mutation, {
projectId: process.env.PROJECT_ID,
itemId: item.id,
fieldId: process.env.STATUS_FIELD_ID,
optionId: process.env.STATUS_DONE
});
core.info('Issue closed, status updated to Done');
update-issue-on-reopen:
name: Update Issue on Reopen
runs-on: ubuntu-latest
if: github.event_name == 'issues' && github.event.action == 'reopened'
permissions:
contents: 'read'
steps:
- name: Mint identity token
id: mint_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Update issue status to To Triage
uses: actions/github-script@v7
with:
github-token: ${{ steps.mint_token.outputs.token }}
script: |
const contentId = context.payload.issue.node_id;
const query = `
query($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
items(first: 100) {
nodes {
id
content { ... on Issue { id } }
}
}
}
}
}
`;
const result = await github.graphql(query, { org: 'iOfficeAI', number: 5 });
const item = result.organization.projectV2.items.nodes.find(i => i.content?.id === contentId);
if (!item) {
core.info('Issue not found in project, skipping');
return;
}
const mutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
`;
await github.graphql(mutation, {
projectId: process.env.PROJECT_ID,
itemId: item.id,
fieldId: process.env.STATUS_FIELD_ID,
optionId: process.env.STATUS_TO_TRIAGE
});
core.info('Issue reopened, status updated to To Triage');
# ============================================
# PR Events: Update linked Issue status
# ============================================
pr-update-linked-issue:
name: Update Linked Issue Status
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: 'read'
pull-requests: 'read'
steps:
- name: Mint identity token
id: mint_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Update linked issue status based on PR event
uses: actions/github-script@v7
with:
github-token: ${{ steps.mint_token.outputs.token }}
script: |
const prBody = context.payload.pull_request.body || '';
const prNumber = context.payload.pull_request.number;
const action = context.payload.action;
const isMerged = context.payload.pull_request.merged === true;
// Extract linked issue numbers from PR body
const issuePatterns = [
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi,
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/gi
];
const linkedIssues = new Set();
for (const pattern of issuePatterns) {
let match;
while ((match = pattern.exec(prBody)) !== null) {
linkedIssues.add(parseInt(match[1], 10));
}
}
if (linkedIssues.size === 0) {
core.info('No linked issues found in PR body, skipping');
return;
}
core.info(`Found linked issues: ${[...linkedIssues].join(', ')}`);
// Determine target status based on PR action
let targetStatusId;
let statusName;
if (action === 'opened' || action === 'reopened') {
// PR opened/reopened -> Issue is In Progress
targetStatusId = process.env.STATUS_IN_PROGRESS;
statusName = 'In Progress';
} else if (action === 'closed') {
if (isMerged) {
// PR merged -> Issue is Done
targetStatusId = process.env.STATUS_DONE;
statusName = 'Done';
} else {
// PR closed without merge -> Issue back to Ready for Work
targetStatusId = process.env.STATUS_READY_FOR_WORK;
statusName = 'Ready for Work';
}
}
if (!targetStatusId) {
core.info(`No status change needed for action: ${action}`);
return;
}
// Query to find issues in project
const query = `
query($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
items(first: 100) {
nodes {
id
content {
... on Issue {
id
number
}
}
}
}
}
}
}
`;
const result = await github.graphql(query, { org: 'iOfficeAI', number: 5 });
const projectItems = result.organization.projectV2.items.nodes;
const mutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
`;
// Update each linked issue
for (const issueNumber of linkedIssues) {
const item = projectItems.find(i => i.content?.number === issueNumber);
if (!item) {
core.info(`Issue #${issueNumber} not found in project, skipping`);
continue;
}
await github.graphql(mutation, {
projectId: process.env.PROJECT_ID,
itemId: item.id,
fieldId: process.env.STATUS_FIELD_ID,
optionId: targetStatusId
});
core.info(`Issue #${issueNumber} status updated to ${statusName} (PR #${prNumber} ${action}${isMerged ? ' merged' : ''})`);
}
+188
View File
@@ -0,0 +1,188 @@
name: Distribute Release Assets
# Triggered after a GitHub Release is officially published.
# Mirrors installer assets to the configured distribution endpoint for external download.
#
# Why `published` (not `released`)?
# - `released` is supposed to fire when a non-prerelease release is published,
# but GitHub sometimes only emits `published` (not `released`) when a pre-existing
# draft is publicly released, causing the workflow to be silently skipped.
# - `published` reliably fires on every draft → public transition, including
# prereleases. The `if` guard on the job filters out prereleases explicitly.
#
# All credentials / identifiers are read from repository secrets:
# - AWS_REGION
# - AWS_ROLE_ARN
# - AWS_S3_BUCKET
# Do NOT hardcode these values in this file.
on:
release:
types: [published]
# Manual trigger for smoke testing or for retrying a missed release:
# pick any existing release tag to re-run the distribution against.
workflow_dispatch:
inputs:
tag:
description: 'Existing release tag to redistribute (e.g. v1.9.21)'
required: true
type: string
permissions:
id-token: write # required for OIDC token
contents: read
env:
S3_RELEASE_PREFIX: releases
jobs:
distribute:
name: Distribute release assets
runs-on: ubuntu-latest
# Skip prereleases: manual dispatch always runs, automatic event runs only when not a prerelease.
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
steps:
- name: Extract version from tag
id: version
env:
EVENT_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ github.event.inputs.tag }}
run: |
TAG="${EVENT_TAG:-$INPUT_TAG}"
if [ -z "$TAG" ]; then
echo "::error::No tag provided (event did not supply one)."
exit 1
fi
VERSION="${TAG#v}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Tag: $TAG / Version: $VERSION"
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Guard against same-version overwrite
env:
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
PREFIX="${S3_RELEASE_PREFIX}/${VERSION}/"
DEST="s3://${S3_BUCKET}/${PREFIX}"
COUNT=$(aws s3api list-objects-v2 \
--bucket "$S3_BUCKET" \
--prefix "$PREFIX" \
--no-paginate \
--query 'KeyCount' \
--output text)
if [ "$COUNT" -gt 0 ]; then
echo "::error::Version directory already contains $COUNT file(s). Refusing to overwrite."
echo "::error::Same-version re-publish is not allowed (downstream caches would serve stale files). Release a new version instead."
aws s3 ls "$DEST"
exit 1
fi
- name: Download release assets from GitHub
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
mkdir -p dist
gh release download "${{ steps.version.outputs.tag }}" \
--repo "${{ github.repository }}" \
--dir dist \
--pattern "*.dmg" \
--pattern "*.exe" \
--pattern "*.msi" \
--pattern "*.deb" \
--pattern "*.zip" \
--pattern "latest*.yml"
echo "Downloaded files:"
ls -la dist
- name: Validate updater metadata
id: metadata
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
metadata_files=(
dist/latest.yml
dist/latest-win-arm64.yml
dist/latest-mac.yml
dist/latest-arm64-mac.yml
dist/latest-linux.yml
dist/latest-linux-arm64.yml
)
for file in "${metadata_files[@]}"; do
if [ ! -f "$file" ]; then
echo "::error::Missing updater metadata file: $file."
exit 1
fi
metadata_version=$(grep -E "^version:" "$file" | head -n 1 | sed -E "s/^version:[[:space:]]*['\"]?([^'\"]+).*/\1/")
if [ "$metadata_version" != "$VERSION" ]; then
echo "::error::$file version is $metadata_version, expected $VERSION."
exit 1
fi
done
{
echo "files<<EOF"
for file in "${metadata_files[@]}"; do
basename "$file"
done
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Upload assets
env:
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
aws s3 cp dist/ "s3://${S3_BUCKET}/${S3_RELEASE_PREFIX}/${VERSION}/" \
--recursive
echo "Uploaded release ${VERSION}"
- name: Upload updater metadata
env:
S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
run: |
set -euo pipefail
aws s3 cp dist/ "s3://${S3_BUCKET}/${S3_RELEASE_PREFIX}/" \
--recursive \
--exclude "*" \
--include "latest*.yml" \
--content-type "text/yaml" \
--cache-control "public, max-age=60"
echo "Uploaded updater metadata"
- name: Summary
env:
VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
FILES: ${{ steps.metadata.outputs.files }}
run: |
{
echo "### Release assets distributed"
echo ""
echo "- Tag: \`${TAG}\`"
echo "- Version: \`${VERSION}\`"
echo "- Asset prefix: \`${S3_RELEASE_PREFIX}/${VERSION}/\`"
echo "- Metadata prefix: \`${S3_RELEASE_PREFIX}/\`"
echo ""
echo "Uploaded metadata:"
while IFS= read -r file; do
[ -n "$file" ] && echo "- \`${S3_RELEASE_PREFIX}/$file\`"
done <<< "$FILES"
} >> $GITHUB_STEP_SUMMARY