Conversation
* feat(ffmpeg): add China mirror support for FFmpeg downloads - Add IP-based region detection using ipinfo.io API - Support China mainland, Hong Kong, Macau, Taiwan regions - Add dedicated China mirror URLs from gitcode.com - Implement automatic fallback from China to global mirrors - Add comprehensive test coverage for new functionality - Default to China mirror on detection failure for better UX Breaking change: Service now defaults to China mirror for better performance in Chinese regions * fix(test): remove unused parameter in FFmpegDownloadService test - Fix TypeScript error TS6133 for unused 'url' parameter - Replace unused 'url' with underscore in mock implementation
WalkthroughAdds a GitHub Actions workflow to sync releases to GitCode, introduces a Node.js asset uploader for GitCode, and updates FFmpegDownloadService to support region-based (China/global) mirrors with detection, manual override, and fallback. Includes new tests covering IP-based region detection, mirror APIs, URL selection, and fallback behavior. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Dev as Developer
participant GH as GitHub Actions
participant GHAPI as GitHub API
participant GC as GitCode Git
participant GCAPI as GitCode API
participant Upl as upload-assets.js
Dev->>GH: Release published/edited or workflow_dispatch
GH->>GH: Determine release metadata (tag, name, body, flags)
alt Not test_mode
GH->>GC: git push --force branches (main/dev/alpha/beta) if present
GH->>GC: git push --force --tags
else test_mode
GH->>GH: Log intended git operations
end
alt Manual trigger
GH->>GHAPI: Fetch release by tag
GHAPI-->>GH: Release + assets (if any)
else Release event
GH-->>GH: Use event payload assets
end
GH->>GCAPI: Check tag and release existence
alt Release not exists
GH->>GCAPI: POST create release
else Release exists
GH->>GCAPI: PATCH update release
end
alt Assets present and not test_mode
GH->>Upl: Run uploader with token/owner/repo/tag/files
Upl->>GCAPI: Get release + upload URLs
Upl->>GCAPI: Upload assets (concurrent, retries)
else No assets or test_mode
GH->>GH: Log asset handling
end
GH-->>Dev: Step Summary (actions, results)
sequenceDiagram
autonumber
participant App as App
participant FF as FFmpegDownloadService
participant IP as ipinfo.io
participant CN as China Mirror
participant GL as Global Mirror
App->>FF: downloadFFmpeg()
par Region detection (with timeout)
FF->>IP: fetch country (<=5s)
alt Success (CN/HK/MO/TW)
FF-->>FF: useChinaMirror = true
else Failure/timeout
FF-->>FF: useChinaMirror = true (default)
end
and Await (<=10s)
FF-->>FF: wait for detection or timeout
end
alt useChinaMirror
FF->>CN: Attempt download
alt Success
FF-->>App: Download complete
else Failure
FF->>GL: Fallback download
GL-->>FF: Result
FF-->>App: Completion or error
end
else global mirror
FF->>GL: Download
FF-->>App: Completion or error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/main/services/FFmpegDownloadService.ts (3)
411-511: Always clean tempDir on failure to avoid disk bloatcleanupTempDir is only called on success. Ensure it also runs on errors/cancel.
- private async performDownload( + private async performDownload( platform: Platform, arch: Arch, version: FFmpegVersion, onProgress?: (progress: DownloadProgress) => void ): Promise<boolean> { - const key = `${platform}-${arch}` + const key = `${platform}-${arch}` const controller = new AbortController() this.downloadController.set(key, controller) @@ - try { - // 创建目标目录 - const platformDir = `${version.version}-${platform}-${arch}` - const targetDir = path.join(this.binariesDir, platformDir) - const tempDir = path.join(this.binariesDir, '.temp', key) + // 提前声明以便 finally 中清理 + const platformDir = `${version.version}-${platform}-${arch}` + const targetDir = path.join(this.binariesDir, platformDir) + const tempDir = path.join(this.binariesDir, '.temp', key) + try { this.ensureDir(targetDir) this.ensureDir(tempDir) @@ - return false - } finally { + return false + } finally { this.downloadProgress.delete(key) this.downloadController.delete(key) + // 成功或失败均清理临时目录 + this.cleanupTempDir(tempDir) } }
589-692: Handle more redirect codes and relative Location headersSupport 303/307/308 and resolve relative redirects.
- if (response.statusCode === 301 || response.statusCode === 302) { - const redirectUrl = response.headers.location + if ([301, 302, 303, 307, 308].includes(response.statusCode || 0)) { + const location = response.headers.location - if (redirectUrl) { - download(redirectUrl, redirectCount + 1) + if (location) { + const next = new URL(location, currentUrl).toString() + download(next, redirectCount + 1) return } }
704-718: Extraction dependency: unzip may be missing on Linuxunzip isn’t guaranteed in minimal distros. Consider falling back to 7z or using a Node zip lib.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
.github/workflows/sync-release-to-gitcode.yml(1 hunks)scripts/upload-assets.js(1 hunks)src/main/services/FFmpegDownloadService.ts(4 hunks)src/main/services/__tests__/FFmpegDownloadService.test.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: 统一使用 loggerService 记录日志,禁止使用 console
logger 使用示例:logger.error('Error in :', { error }); 第二个参数必须是对象字面量 {}
Files:
scripts/upload-assets.jssrc/main/services/FFmpegDownloadService.tssrc/main/services/__tests__/FFmpegDownloadService.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 在组件/Hook 中避免硬编码尺寸和时长,优先使用 useTheme() 的 token(如 motionDurationMid、borderRadiusSM/MD 等)或集中定义的样式变量
定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装),避免通过全局 SCSS 与全局 className 覆盖
项目使用 Zustand 结合 Immer 中间件与自定义中间件栈(持久化、DevTools、订阅选择器)进行状态管理
Files:
src/main/services/FFmpegDownloadService.tssrc/main/services/__tests__/FFmpegDownloadService.test.ts
**/*.{tsx,ts}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{tsx,ts}: Zustand 必须在组件/Hook 顶层使用 selector(useStore(selector));禁止在 useMemo/useEffect 内部调用 store Hook
避免使用返回对象的 useStore 选择器(如 useStore(s => ({ a: s.a, b: s.b }))),应使用单字段选择器或配合 shallow 比较器
Files:
src/main/services/FFmpegDownloadService.tssrc/main/services/__tests__/FFmpegDownloadService.test.ts
**/*.{test,spec}.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
测试使用 vitest 作为测试框架
Files:
src/main/services/__tests__/FFmpegDownloadService.test.ts
🧬 Code graph analysis (2)
src/main/services/FFmpegDownloadService.ts (1)
src/renderer/src/services/Logger.ts (1)
error(422-424)
src/main/services/__tests__/FFmpegDownloadService.test.ts (1)
scripts/upload-assets.js (1)
options(360-367)
🪛 YAMLlint (1.37.1)
.github/workflows/sync-release-to-gitcode.yml
[warning] 3-3: truthy value should be one of [false, true]
(truthy)
[warning] 49-49: too few spaces before comment: expected 2
(comments)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (ubuntu-latest, 20)
- GitHub Check: test (windows-latest, 20)
- GitHub Check: test (macos-latest, 20)
🔇 Additional comments (3)
src/main/services/__tests__/FFmpegDownloadService.test.ts (2)
105-109: Good: explicit mirror selection for deterministic testsExplicitly forcing the global mirror removes flakiness from region defaults.
271-338: AbortSignal type in expectations may fail under TS config without DOM libsExpect.any(AbortSignal) depends on lib.dom or Node 18+ types. If CI lacks DOM, use globalThis.AbortSignal or loosen to expect.anything().
- signal: expect.any(AbortSignal), + signal: expect.any((globalThis as any).AbortSignal ?? Object),If needed, ensure tsconfig includes appropriate lib for fetch/AbortController.
.github/workflows/sync-release-to-gitcode.yml (1)
36-40: Align API base with uploader scriptYou export GITCODE_API_BASE=https://gitcode.com/api/v5; ensure the uploader reads this (after script change) so both paths hit the same host.
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 # Fetch all history for all branches and tags |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
YAML lint: add two spaces before comment
Minor formatting nit only.
- fetch-depth: 0 # Fetch all history for all branches and tags
+ fetch-depth: 0 # Fetch all history for all branches and tags📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fetch-depth: 0 # Fetch all history for all branches and tags | |
| fetch-depth: 0 # Fetch all history for all branches and tags |
🧰 Tools
🪛 YAMLlint (1.37.1)
[warning] 49-49: too few spaces before comment: expected 2
(comments)
🤖 Prompt for AI Agents
.github/workflows/sync-release-to-gitcode.yml around line 49: the inline comment
after the fetch-depth key uses only one space before the '#' which fails the
project's YAML lint rule; update the line so there are two spaces before the
comment (i.e., ensure one space after the value and an additional leading space
before '#') to satisfy linting.
| - name: Sync repository to GitCode | ||
| if: steps.release.outputs.test_mode != 'true' | ||
| run: | | ||
| echo "🔄 Syncing repository to GitCode using HTTPS..." | ||
|
|
||
| # Configure git with token authentication | ||
| git config user.name "GitHub Actions" | ||
| git config user.email "actions@github.com" | ||
|
|
||
| # Construct GitCode repository URL with token authentication | ||
| GITCODE_REPO_URL="https://oauth2:${{ secrets.GITCODE_ACCESS_TOKEN }}@gitcode.com/$GITCODE_OWNER/$GITCODE_REPO.git" | ||
|
|
||
| echo "Repository: $GITCODE_OWNER/$GITCODE_REPO" | ||
|
|
||
| # Add GitCode remote (remove if exists) | ||
| if git remote | grep -q "gitcode"; then | ||
| echo "Removing existing gitcode remote" | ||
| git remote remove gitcode | ||
| fi | ||
|
|
||
| echo "Adding GitCode remote with HTTPS authentication" | ||
| git remote add gitcode "$GITCODE_REPO_URL" | ||
|
|
||
| echo "📤 Force pushing branches to GitCode..." | ||
|
|
||
| # Show available branches | ||
| echo "Available branches:" | ||
| git branch -a | grep -E "(main|dev|alpha|beta)" || echo "Target branches not found" | ||
|
|
||
| # Force push main branches to GitCode | ||
| for branch in main dev alpha beta; do | ||
| if git show-ref --verify --quiet refs/heads/$branch || git show-ref --verify --quiet refs/remotes/origin/$branch; then | ||
| echo "Pushing branch: $branch" | ||
| if git show-ref --verify --quiet refs/heads/$branch; then | ||
| git push --force gitcode $branch:$branch || { | ||
| echo "❌ Failed to push local branch $branch" | ||
| exit 1 | ||
| } | ||
| else | ||
| git push --force gitcode origin/$branch:$branch || { | ||
| echo "❌ Failed to push remote branch $branch" | ||
| exit 1 | ||
| } | ||
| fi | ||
| echo "✅ Successfully pushed branch: $branch" | ||
| else | ||
| echo "⚠️ Branch $branch not found, skipping" | ||
| fi | ||
| done | ||
|
|
||
| echo "🏷️ Pushing all tags to GitCode..." | ||
| echo "Available tags (last 10):" | ||
| git tag | tail -10 || echo "No tags found" | ||
|
|
||
| git push --force gitcode --tags || { | ||
| echo "❌ Failed to push tags" | ||
| exit 1 | ||
| } | ||
|
|
||
| echo "✅ Repository sync completed successfully" | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Force-pushing branches and tags is risky
--force on branches and tags can rewrite history on GitCode. Prefer --force-with-lease for branches and push tags without force unless necessary.
- git push --force gitcode $branch:$branch
+ git push --force-with-lease gitcode $branch:$branch
@@
- git push --force gitcode --tags
+ git push gitcode --tagsCommittable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
.github/workflows/sync-release-to-gitcode.yml around lines 83 to 143: the
workflow currently uses --force for branch and tag pushes which can rewrite
remote history; change branch pushes to use --force-with-lease instead of
--force (both when pushing local branch and when pushing origin/branch) and
remove the --force flag when pushing tags (use a normal git push --tags) so tags
are not forcibly rewritten unless absolutely required.
| - name: Check if release exists on GitCode | ||
| id: check-release | ||
| run: | | ||
| if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then | ||
| echo "🧪 Test mode enabled - skipping GitCode API check" | ||
| echo "exists=false" >> $GITHUB_OUTPUT | ||
| echo "Test mode: Simulating release does not exist on GitCode" | ||
| else | ||
| # First check if tag exists using GitCode tags API | ||
| echo "Checking if tag exists..." | ||
| tags_response=$(curl -s -w "%{http_code}" \ | ||
| -H "Accept: application/json" \ | ||
| "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/tags?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") | ||
|
|
||
| tags_http_code="${tags_response: -3}" | ||
| tags_response_body="${tags_response%???}" | ||
|
|
||
| echo "Tags API HTTP Code: $tags_http_code" | ||
|
|
||
| tag_exists=false | ||
| if [ "$tags_http_code" = "200" ] || [ "$tags_http_code" = "201" ]; then | ||
| echo "Available tags (first 20):" | ||
| echo "$tags_response_body" | jq -r '.[] | .name' 2>/dev/null | head -20 || echo "Failed to parse tags" | ||
|
|
||
| # Check if our target tag exists | ||
| if echo "$tags_response_body" | jq -e --arg tag "${{ steps.release.outputs.tag_name }}" '.[] | select(.name == $tag)' > /dev/null 2>&1; then | ||
| tag_exists=true | ||
| echo "✅ Tag ${{ steps.release.outputs.tag_name }} exists on GitCode" | ||
| else | ||
| echo "❌ Tag ${{ steps.release.outputs.tag_name }} does not exist on GitCode" | ||
| fi | ||
| else | ||
| echo "❌ Failed to fetch tags from GitCode (HTTP $tags_http_code): $tags_response_body" | ||
| fi | ||
|
|
||
| # Then check if release exists (only if tag exists) | ||
| if [ "$tag_exists" = "true" ]; then | ||
| echo "Checking if release exists..." | ||
| response=$(curl -s -w "%{http_code}" \ | ||
| -H "Accept: application/json" \ | ||
| "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases/tags/${{ steps.release.outputs.tag_name }}?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") | ||
| else | ||
| echo "⚠️ Skipping release check since tag does not exist" | ||
| response="404Not Found" | ||
| fi | ||
|
|
||
| http_code="${response: -3}" | ||
| response_body="${response%???}" | ||
|
|
||
| echo "HTTP Code: $http_code" | ||
| echo "Response: $response_body" | ||
|
|
||
| if [ "$http_code" = "200" ]; then | ||
| echo "exists=true" >> $GITHUB_OUTPUT | ||
| echo "Release already exists on GitCode" | ||
| else | ||
| echo "exists=false" >> $GITHUB_OUTPUT | ||
| echo "Release does not exist on GitCode" | ||
| fi | ||
| fi | ||
|
|
There was a problem hiding this comment.
Token in query string in GitCode API calls
curl requests pass access_token via query. Prefer Authorization header to avoid leakage.
- "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/tags?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}")
+ -H "Authorization: token ${{ secrets.GITCODE_ACCESS_TOKEN }}" \
+ "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/tags")Apply similarly to subsequent releases API calls (create/update/check).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Check if release exists on GitCode | |
| id: check-release | |
| run: | | |
| if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then | |
| echo "🧪 Test mode enabled - skipping GitCode API check" | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "Test mode: Simulating release does not exist on GitCode" | |
| else | |
| # First check if tag exists using GitCode tags API | |
| echo "Checking if tag exists..." | |
| tags_response=$(curl -s -w "%{http_code}" \ | |
| -H "Accept: application/json" \ | |
| "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/tags?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") | |
| tags_http_code="${tags_response: -3}" | |
| tags_response_body="${tags_response%???}" | |
| echo "Tags API HTTP Code: $tags_http_code" | |
| tag_exists=false | |
| if [ "$tags_http_code" = "200" ] || [ "$tags_http_code" = "201" ]; then | |
| echo "Available tags (first 20):" | |
| echo "$tags_response_body" | jq -r '.[] | .name' 2>/dev/null | head -20 || echo "Failed to parse tags" | |
| # Check if our target tag exists | |
| if echo "$tags_response_body" | jq -e --arg tag "${{ steps.release.outputs.tag_name }}" '.[] | select(.name == $tag)' > /dev/null 2>&1; then | |
| tag_exists=true | |
| echo "✅ Tag ${{ steps.release.outputs.tag_name }} exists on GitCode" | |
| else | |
| echo "❌ Tag ${{ steps.release.outputs.tag_name }} does not exist on GitCode" | |
| fi | |
| else | |
| echo "❌ Failed to fetch tags from GitCode (HTTP $tags_http_code): $tags_response_body" | |
| fi | |
| # Then check if release exists (only if tag exists) | |
| if [ "$tag_exists" = "true" ]; then | |
| echo "Checking if release exists..." | |
| response=$(curl -s -w "%{http_code}" \ | |
| -H "Accept: application/json" \ | |
| "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases/tags/${{ steps.release.outputs.tag_name }}?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") | |
| else | |
| echo "⚠️ Skipping release check since tag does not exist" | |
| response="404Not Found" | |
| fi | |
| http_code="${response: -3}" | |
| response_body="${response%???}" | |
| echo "HTTP Code: $http_code" | |
| echo "Response: $response_body" | |
| if [ "$http_code" = "200" ]; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "Release already exists on GitCode" | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "Release does not exist on GitCode" | |
| fi | |
| fi | |
| - name: Check if release exists on GitCode | |
| id: check-release | |
| run: | | |
| if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then | |
| echo "🧪 Test mode enabled - skipping GitCode API check" | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "Test mode: Simulating release does not exist on GitCode" | |
| else | |
| # First check if tag exists using GitCode tags API | |
| echo "Checking if tag exists..." | |
| tags_response=$(curl -s -w "%{http_code}" \ | |
| -H "Accept: application/json" \ | |
| -H "Authorization: token ${{ secrets.GITCODE_ACCESS_TOKEN }}" \ | |
| "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/tags") | |
| tags_http_code="${tags_response: -3}" | |
| tags_response_body="${tags_response%???}" | |
| echo "Tags API HTTP Code: $tags_http_code" | |
| tag_exists=false | |
| if [ "$tags_http_code" = "200" ] || [ "$tags_http_code" = "201" ]; then | |
| echo "Available tags (first 20):" | |
| echo "$tags_response_body" | jq -r '.[] | .name' 2>/dev/null | head -20 || echo "Failed to parse tags" | |
| # Check if our target tag exists | |
| if echo "$tags_response_body" | jq -e --arg tag "${{ steps.release.outputs.tag_name }}" '.[] | select(.name == $tag)' > /dev/null 2>&1; then | |
| tag_exists=true | |
| echo "✅ Tag ${{ steps.release.outputs.tag_name }} exists on GitCode" | |
| else | |
| echo "❌ Tag ${{ steps.release.outputs.tag_name }} does not exist on GitCode" | |
| fi | |
| else | |
| echo "❌ Failed to fetch tags from GitCode (HTTP $tags_http_code): $tags_response_body" | |
| fi | |
| # Then check if release exists (only if tag exists) | |
| if [ "$tag_exists" = "true" ]; then | |
| echo "Checking if release exists..." | |
| response=$(curl -s -w "%{http_code}" \ | |
| -H "Accept: application/json" \ | |
| "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases/tags/${{ steps.release.outputs.tag_name }}?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") | |
| else | |
| echo "⚠️ Skipping release check since tag does not exist" | |
| response="404Not Found" | |
| fi | |
| http_code="${response: -3}" | |
| response_body="${response%???}" | |
| echo "HTTP Code: $http_code" | |
| echo "Response: $response_body" | |
| if [ "$http_code" = "200" ]; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "Release already exists on GitCode" | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "Release does not exist on GitCode" | |
| fi | |
| fi |
🤖 Prompt for AI Agents
.github/workflows/sync-release-to-gitcode.yml around lines 198-258: the curl
calls include the access token in the query string which can leak secrets;
change the requests to send the token in an Authorization header (e.g.,
Authorization: token ${{ secrets.GITCODE_ACCESS_TOKEN }}) instead of appending
?access_token=... to the URL, update both the tags API and releases/tags
requests (and any subsequent create/update calls) to remove the access_token
query param, add the header to each curl -H call, and verify the URLs and
response parsing remain unchanged.
| - name: Create release on GitCode | ||
| if: steps.check-release.outputs.exists == 'false' | ||
| id: create-release | ||
| run: | | ||
| payload=$(jq -n \ | ||
| --arg tag_name "${{ steps.release.outputs.tag_name }}" \ | ||
| --arg name "${{ steps.release.outputs.release_name }}" \ | ||
| --arg body "${{ steps.release.outputs.release_body }}" \ | ||
| --argjson prerelease "${{ steps.release.outputs.prerelease }}" \ | ||
| --argjson draft "${{ steps.release.outputs.draft }}" \ | ||
| '{ | ||
| tag_name: $tag_name, | ||
| name: $name, | ||
| body: $body, | ||
| prerelease: $prerelease, | ||
| draft: $draft | ||
| }') | ||
|
|
||
| echo "Creating release with payload:" | ||
| echo "$payload" | jq . | ||
|
|
||
| if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then | ||
| echo "🧪 Test mode enabled - skipping release creation on GitCode" | ||
| echo "✅ Test mode: Would create release successfully on GitCode" | ||
| echo "created=true" >> $GITHUB_OUTPUT | ||
| else | ||
| response=$(curl -s -w "%{http_code}" \ | ||
| -X POST \ | ||
| -H "Content-Type: application/json" \ | ||
| -H "Accept: application/json" \ | ||
| -d "$payload" \ | ||
| "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") | ||
|
|
||
| http_code="${response: -3}" | ||
| response_body="${response%???}" | ||
|
|
||
| echo "Create Release Response Code: $http_code" | ||
| echo "Create Release Response: $response_body" | ||
|
|
||
| if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then | ||
| echo "✅ Release created successfully on GitCode (HTTP $http_code)" | ||
| echo "created=true" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "❌ Failed to create release on GitCode (HTTP $http_code)" | ||
| echo "Response: $response_body" | ||
| echo "created=false" >> $GITHUB_OUTPUT | ||
| exit 1 | ||
| fi | ||
| fi |
There was a problem hiding this comment.
Use Authorization header for release create/update too
Keep auth out of URL for all POST/PATCH calls.
- "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}")
+ -H "Authorization: token ${{ secrets.GITCODE_ACCESS_TOKEN }}" \
+ "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases")- "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases/${{ steps.release.outputs.tag_name }}?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}")
+ -H "Authorization: token ${{ secrets.GITCODE_ACCESS_TOKEN }}" \
+ "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases/${{ steps.release.outputs.tag_name }}")Also applies to: 309-355
🤖 Prompt for AI Agents
.github/workflows/sync-release-to-gitcode.yml lines 259-307 (and similarly
309-355): the release create/update curl calls currently include the access
token in the request URL; move the token to an Authorization header instead to
avoid leaking credentials in URLs. Modify the curl commands to remove the
?access_token=... query parameter and add a header -H "Authorization: Bearer
${GITCODE_ACCESS_TOKEN}" (or use the secrets variable) while keeping
Content-Type/Accept headers and the POST/PATCH method and body handling the
same; ensure the workflow uses the GitHub secret (secrets.GITCODE_ACCESS_TOKEN)
for the header and that response handling (http_code/response_body) remains
unchanged.
| this.baseUrl = 'https://api.gitcode.com/api/v5' | ||
| } |
There was a problem hiding this comment.
API base mismatch with workflow env; allow override via env/option
Workflow uses GITCODE_API_BASE=https://gitcode.com/api/v5, but script hardcodes https://api.gitcode.com/api/v5. This will break in one of the paths. Read from options/env and default to the workflow value.
- this.baseUrl = 'https://api.gitcode.com/api/v5'
+ this.baseUrl = options.baseUrl || process.env.GITCODE_API_BASE || 'https://gitcode.com/api/v5'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| this.baseUrl = 'https://api.gitcode.com/api/v5' | |
| } | |
| this.baseUrl = options.baseUrl || process.env.GITCODE_API_BASE || 'https://gitcode.com/api/v5' | |
| } |
🤖 Prompt for AI Agents
In scripts/upload-assets.js around lines 24 to 25, the API base URL is hardcoded
to https://api.gitcode.com/api/v5 which conflicts with the workflow's
GITCODE_API_BASE (https://gitcode.com/api/v5); change this to allow overriding
by reading an option or environment variable and falling back to the current
hardcoded value — e.g. check options.apiBase then process.env.GITCODE_API_BASE
and assign the first defined value to this.baseUrl, defaulting to the existing
literal if neither is set.
| // 解析命令行参数 | ||
| const options = { | ||
| accessToken: process.env.GITCODE_ACCESS_TOKEN, | ||
| owner: process.env.GITCODE_OWNER, | ||
| repo: process.env.GITCODE_REPO, | ||
| tag: process.env.GITCODE_TAG, | ||
| concurrency: 3, | ||
| retryAttempts: 3 | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
CLI should accept/propagate API base to match workflow env
Allow --api-base or read env already set by workflow to avoid mismatch.
const options = {
accessToken: process.env.GITCODE_ACCESS_TOKEN,
owner: process.env.GITCODE_OWNER,
repo: process.env.GITCODE_REPO,
tag: process.env.GITCODE_TAG,
+ baseUrl: process.env.GITCODE_API_BASE,
concurrency: 3,
retryAttempts: 3
}- } else if (!arg.startsWith('--')) {
+ } else if (arg === '--api-base' && i + 1 < args.length) {
+ options.baseUrl = args[++i]
+ } else if (!arg.startsWith('--')) {
filePaths.push(arg)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 解析命令行参数 | |
| const options = { | |
| accessToken: process.env.GITCODE_ACCESS_TOKEN, | |
| owner: process.env.GITCODE_OWNER, | |
| repo: process.env.GITCODE_REPO, | |
| tag: process.env.GITCODE_TAG, | |
| concurrency: 3, | |
| retryAttempts: 3 | |
| } | |
| // 解析命令行参数 | |
| const options = { | |
| accessToken: process.env.GITCODE_ACCESS_TOKEN, | |
| owner: process.env.GITCODE_OWNER, | |
| repo: process.env.GITCODE_REPO, | |
| tag: process.env.GITCODE_TAG, | |
| baseUrl: process.env.GITCODE_API_BASE, | |
| concurrency: 3, | |
| retryAttempts: 3 | |
| } |
🤖 Prompt for AI Agents
In scripts/upload-assets.js around lines 359 to 367, the CLI/options block
doesn't accept or propagate the API base URL used by the workflow; add support
for an --api-base CLI flag and/or read a GITCODE_API_BASE (or similar)
environment variable and include it in the options object (e.g., apiBase:
process.env.GITCODE_API_BASE || parsedFlag). Then pass this options.apiBase
through to the code that constructs the GitCode API client so the script uses
the same API base as the workflow.
| // Mock global fetch for IP detection tests | ||
| global.fetch = vi.fn() | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Use vi.stubGlobal for fetch and restore after tests
Directly assigning global.fetch can leak across suites. Prefer vi.stubGlobal('fetch', fn) and restore in afterAll.
-// Mock global fetch for IP detection tests
-global.fetch = vi.fn()
+// Mock global fetch for IP detection tests
+vi.stubGlobal('fetch', vi.fn())
+afterAll(() => {
+ // @ts-ignore
+ global.fetch = undefined
+})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Mock global fetch for IP detection tests | |
| global.fetch = vi.fn() | |
| // Mock global fetch for IP detection tests | |
| vi.stubGlobal('fetch', vi.fn()) | |
| afterAll(() => { | |
| // @ts-ignore | |
| global.fetch = undefined | |
| }) |
🤖 Prompt for AI Agents
In src/main/services/__tests__/FFmpegDownloadService.test.ts around lines 28 to
30, the test directly assigns global.fetch = vi.fn(), which can leak the mock
across test suites; replace that assignment with vi.stubGlobal('fetch', vi.fn())
and add an afterAll hook that calls vi.unstubAllGlobals() to restore the global
fetch after the tests complete.
| it('should fallback to global mirror when China mirror not supported', () => { | ||
| service.setMirrorSource(true) | ||
|
|
||
| // 假设有一个平台在中国镜像源中不支持(这里只是测试逻辑) | ||
| // 实际上所有平台都支持,所以这个测试更多是为了测试回退逻辑的代码结构 | ||
| const version = service.getFFmpegVersion('darwin', 'arm64') | ||
| expect(version).toBeDefined() | ||
| expect(version?.platform).toBe('darwin') | ||
| expect(version?.arch).toBe('arm64') | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add a real fallback test covering China→global retry path
Currently this test doesn’t simulate a China-mirror failure. Spy performDownload to fail once then succeed on fallback via global version.
it('should fallback to global mirror when China mirror not supported', async () => {
service.setMirrorSource(true)
- const version = service.getFFmpegVersion('darwin', 'arm64')
- expect(version).toBeDefined()
- expect(version?.platform).toBe('darwin')
- expect(version?.arch).toBe('arm64')
+ const spy = vi.spyOn(service as any, 'performDownload')
+ .mockResolvedValueOnce(false) // China mirror fails
+ .mockResolvedValueOnce(true) // Global mirror succeeds
+ const ok = await (service as any).downloadFFmpegWithFallback('darwin','arm64')
+ expect(ok).toBe(true)
+ expect(spy).toHaveBeenCalledTimes(2)
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should fallback to global mirror when China mirror not supported', () => { | |
| service.setMirrorSource(true) | |
| // 假设有一个平台在中国镜像源中不支持(这里只是测试逻辑) | |
| // 实际上所有平台都支持,所以这个测试更多是为了测试回退逻辑的代码结构 | |
| const version = service.getFFmpegVersion('darwin', 'arm64') | |
| expect(version).toBeDefined() | |
| expect(version?.platform).toBe('darwin') | |
| expect(version?.arch).toBe('arm64') | |
| }) | |
| it('should fallback to global mirror when China mirror not supported', async () => { | |
| service.setMirrorSource(true) | |
| // 假设有一个平台在中国镜像源中不支持(这里只是测试逻辑) | |
| // 实际上所有平台都支持,所以这个测试更多是为了测试回退逻辑的代码结构 | |
| const spy = vi.spyOn(service as any, 'performDownload') | |
| .mockResolvedValueOnce(false) // China mirror fails | |
| .mockResolvedValueOnce(true) // Global mirror succeeds | |
| const ok = await (service as any).downloadFFmpegWithFallback('darwin','arm64') | |
| expect(ok).toBe(true) | |
| expect(spy).toHaveBeenCalledTimes(2) | |
| }) |
| logger.warn('无法检测用户地区,使用默认镜像源', { error }) | ||
| this.useChinaMirror = true // 检测失败时默认使用中国镜像源 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
Defaulting to China mirror on detection failure: add opt-out
Hard-defaulting to CN impacts non-CN users offline. Provide an env/config override (e.g., ECHOPLAYER_FFMPEG_DEFAULT_MIRROR=global).
I can add a small config check in constructor if desired.
🤖 Prompt for AI Agents
In src/main/services/FFmpegDownloadService.ts around lines 195-198, the code
currently hard-defaults to the China mirror on region-detection failure; add an
environment/config override so callers can opt out. Before setting
this.useChinaMirror = true in the catch path, read a config/env var (e.g.
process.env.ECHOPLAYER_FFMPEG_DEFAULT_MIRROR) in the constructor and use it to
decide the default: accept values like "global" or "china" (or "auto" to
preserve current behavior), validate the value and then, on detection failure,
set this.useChinaMirror based on that config instead of unconditionally true;
keep the existing logger call but include which default was applied. Ensure
invalid values fall back to the previous behavior (or a safe default) and
document the new env var in code comments.
| private async getIpCountry(): Promise<string> { | ||
| try { | ||
| // 使用 AbortController 设置 5 秒超时 | ||
| const controller = new AbortController() | ||
| const timeoutId = setTimeout(() => controller.abort(), 5000) | ||
|
|
||
| const response = await fetch('https://ipinfo.io/json', { | ||
| signal: controller.signal, | ||
| headers: { | ||
| 'User-Agent': 'EchoPlayer-FFmpeg-Downloader/2.0', | ||
| 'Accept-Language': 'en-US,en;q=0.9' | ||
| } | ||
| }) | ||
|
|
||
| clearTimeout(timeoutId) | ||
| const data = await response.json() | ||
| return data.country || 'CN' // 默认返回 CN,这样中国用户即使检测失败也能使用中国镜像源 | ||
| } catch (error) { | ||
| logger.warn('获取IP地理位置失败,默认使用中国镜像源', { error }) | ||
| return 'CN' // 默认返回 CN | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick (assertive)
IP geo lookup privacy note; allow disabling
Calling ipinfo.io leaks user IP to a third party. Provide a toggle to skip detection (env or setting) and default to global if disabled.
🤖 Prompt for AI Agents
In src/main/services/FFmpegDownloadService.ts around lines 203 to 224, the IP
geolocation lookup calls ipinfo.io which leaks user IP to a third party; add a
configurable opt-out so fetching is skipped when disabled (prefer an environment
variable like FFMEPG_GEO_LOOKUP_ENABLED or a passed-in service setting) and
default to "GLOBAL" when disabled. Modify getIpCountry to first check the
config/ENV: if lookup is disabled, immediately return 'GLOBAL' (and log a
debug/info line noting lookup is skipped); otherwise proceed with the existing
abort-controller fetch logic. Ensure the new config is type-safe, documented
where the service is instantiated, and that unit tests or callers are updated to
set the env/setting in test scenarios.
🚀 Dev2Alpha: GitCode 发布同步自动化
将 dev 分支的最新功能合并到 alpha 分支,主要包含 GitCode 发布同步的完整自动化解决方案。
✨ 新增功能
🤖 GitCode 发布同步工作流
.github/workflows/sync-release-to-gitcode.yml📤 GitCode 资产上传脚本
scripts/upload-assets.js🔧 技术实现
📋 测试说明
可以通过以下方式测试此功能:
手动触发测试:
实际 Release 同步: 直接发布 GitHub release,工作流将自动执行
🎯 使用场景
🔒 安全考虑
测试状态: ✅ 已通过手动测试和 dry-run 验证
兼容性: ✅ 兼容现有的 release 流程,无破坏性变更
文档: ✅ 包含完整的内联文档和使用说明