mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 12:27:41 +08:00
<!-- Template from https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?--> <!-- Thanks for sending a pull request! Here are some tips for you: 1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md --> <!-- ⚠️ Important: Redux/IndexedDB Data-Changing Feature PRs Temporarily On Hold ⚠️ Please note: For our current development cycle, we are not accepting feature Pull Requests that introduce changes to Redux data models or IndexedDB schemas. While we value your contributions, PRs of this nature will be blocked without merge. We welcome all other contributions (bug fixes, perf enhancements, docs, etc.). Thank you! Once version 2.0.0 is released, we will resume reviewing feature PRs. --> ### What this PR does Before this PR: - CI workflows hardcoded `node-version: 22` - `.node-version` and `.nvmrc` files were at version 22 After this PR: - All CI workflows read Node version from `.node-version` file using `node-version-file: '.node-version'` - `.node-version` and `.nvmrc` updated to `24.11.1` to match Electron runtime version <!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: --> Fixes # ### Why we need it and why it was done in this way The project recently updated Node.js requirement to 24.11.1 in package.json, but the CI workflows were still hardcoded to use Node 22, causing warning. This change makes the CI workflows automatically use the Node version defined in `.node-version` file, ensuring consistency between local development and CI environments. The following tradeoffs were made: - Using `.node-version` instead of reading from `package.json` directly - simpler and more compatible with GitHub Actions The following alternatives were considered: Links to places where the discussion took place: <!-- optional: slack, other GH issue, mailinglist, ... --> ### Breaking changes <!-- optional --> If this PR introduces breaking changes, please describe the changes and the impact on users. ### Special notes for your reviewer <!-- optional --> ### Checklist This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR. Approvers are expected to review this list. - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle) - [x] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [x] Self-review: I have reviewed my own code (e.g., via [`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`, or GitHub UI) before requesting review from others ### Release note <!-- Write your release note: 1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required". 2. If no release note is required, just write "NONE". 3. Only include user-facing changes (new features, bug fixes visible to users, UI changes, behavior changes). For CI, maintenance, internal refactoring, build tooling, or other non-user-facing work, write "NONE". --> ```release-note NONE ``` Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
331 lines
12 KiB
YAML
331 lines
12 KiB
YAML
name: Sync Release to GitCode
|
|
|
|
on:
|
|
release:
|
|
types: [published]
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Release tag (e.g. v1.0.0)'
|
|
required: true
|
|
clean:
|
|
description: 'Clean node_modules before build'
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
build-and-sync-to-gitcode:
|
|
runs-on: [self-hosted, windows-signing]
|
|
steps:
|
|
- name: Get tag name
|
|
id: get-tag
|
|
shell: bash
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Check out Git repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ steps.get-tag.outputs.tag }}
|
|
|
|
- name: Set package.json version
|
|
shell: bash
|
|
run: |
|
|
TAG="${{ steps.get-tag.outputs.tag }}"
|
|
VERSION="${TAG#v}"
|
|
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
|
|
|
- name: Install Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: '.node-version'
|
|
|
|
- name: Install pnpm
|
|
uses: pnpm/action-setup@v4
|
|
|
|
- name: Clean node_modules
|
|
if: ${{ github.event.inputs.clean == 'true' }}
|
|
shell: bash
|
|
run: rm -rf node_modules
|
|
|
|
- name: Install Dependencies
|
|
shell: bash
|
|
run: pnpm install
|
|
|
|
- name: Build Windows with code signing
|
|
shell: bash
|
|
run: pnpm build:win
|
|
env:
|
|
WIN_SIGN: true
|
|
CHERRY_CERT_PATH: ${{ secrets.CHERRY_CERT_PATH }}
|
|
CHERRY_CERT_KEY: ${{ secrets.CHERRY_CERT_KEY }}
|
|
CHERRY_CERT_CSP: ${{ secrets.CHERRY_CERT_CSP }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
NODE_OPTIONS: --max-old-space-size=8192
|
|
MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }}
|
|
MAIN_VITE_MINERU_API_KEY: ${{ secrets.MAIN_VITE_MINERU_API_KEY }}
|
|
RENDERER_VITE_AIHUBMIX_SECRET: ${{ secrets.RENDERER_VITE_AIHUBMIX_SECRET }}
|
|
RENDERER_VITE_PPIO_APP_SECRET: ${{ secrets.RENDERER_VITE_PPIO_APP_SECRET }}
|
|
|
|
- name: List built Windows artifacts
|
|
shell: bash
|
|
run: |
|
|
echo "Built Windows artifacts:"
|
|
ls -la dist/*.exe dist/latest*.yml
|
|
|
|
- name: Download GitHub release assets
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG_NAME: ${{ steps.get-tag.outputs.tag }}
|
|
run: |
|
|
echo "Downloading release assets for $TAG_NAME..."
|
|
mkdir -p release-assets
|
|
cd release-assets
|
|
|
|
# Download all assets from the release
|
|
gh release download "$TAG_NAME" \
|
|
--repo "${{ github.repository }}" \
|
|
--pattern "*" \
|
|
--skip-existing
|
|
|
|
echo "Downloaded GitHub release assets:"
|
|
ls -la
|
|
|
|
- name: Replace Windows files with signed versions
|
|
shell: bash
|
|
run: |
|
|
echo "Replacing Windows files with signed versions..."
|
|
|
|
# Verify signed files exist first
|
|
if ! ls dist/*.exe 1>/dev/null 2>&1; then
|
|
echo "ERROR: No signed .exe files found in dist/"
|
|
exit 1
|
|
fi
|
|
|
|
# Remove unsigned Windows files from downloaded assets
|
|
rm -f release-assets/*.exe release-assets/latest.yml 2>/dev/null || true
|
|
|
|
# Copy signed Windows files with error checking
|
|
cp dist/*.exe release-assets/ || { echo "ERROR: Failed to copy .exe files"; exit 1; }
|
|
cp dist/latest.yml release-assets/ || { echo "ERROR: Failed to copy latest.yml"; exit 1; }
|
|
|
|
echo "Final release assets:"
|
|
ls -la release-assets/
|
|
|
|
- name: Get release info
|
|
id: release-info
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG_NAME: ${{ steps.get-tag.outputs.tag }}
|
|
LANG: C.UTF-8
|
|
LC_ALL: C.UTF-8
|
|
run: |
|
|
# Always use gh cli to avoid special character issues
|
|
RELEASE_NAME=$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json name -q '.name')
|
|
# Use delimiter to safely handle special characters in release name
|
|
{
|
|
echo 'name<<EOF'
|
|
echo "$RELEASE_NAME"
|
|
echo 'EOF'
|
|
} >> $GITHUB_OUTPUT
|
|
# Extract releaseNotes from electron-builder.yml (from releaseNotes: | to end of file, remove 4-space indent)
|
|
sed -n '/releaseNotes: |/,$ { /releaseNotes: |/d; s/^ //; p }' electron-builder.yml > release_body.txt
|
|
|
|
- name: Create GitCode release and upload files
|
|
shell: bash
|
|
env:
|
|
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
|
|
GITCODE_OWNER: ${{ vars.GITCODE_OWNER }}
|
|
GITCODE_REPO: ${{ vars.GITCODE_REPO }}
|
|
GITCODE_API_URL: ${{ vars.GITCODE_API_URL }}
|
|
TAG_NAME: ${{ steps.get-tag.outputs.tag }}
|
|
RELEASE_NAME: ${{ steps.release-info.outputs.name }}
|
|
LANG: C.UTF-8
|
|
LC_ALL: C.UTF-8
|
|
run: |
|
|
# Validate required environment variables
|
|
if [ -z "$GITCODE_TOKEN" ]; then
|
|
echo "ERROR: GITCODE_TOKEN is not set"
|
|
exit 1
|
|
fi
|
|
if [ -z "$GITCODE_OWNER" ]; then
|
|
echo "ERROR: GITCODE_OWNER is not set"
|
|
exit 1
|
|
fi
|
|
if [ -z "$GITCODE_REPO" ]; then
|
|
echo "ERROR: GITCODE_REPO is not set"
|
|
exit 1
|
|
fi
|
|
|
|
API_URL="${GITCODE_API_URL:-https://api.gitcode.com/api/v5}"
|
|
|
|
echo "Creating GitCode release..."
|
|
echo "Tag: $TAG_NAME"
|
|
echo "Repo: $GITCODE_OWNER/$GITCODE_REPO"
|
|
|
|
# Step 1: Create release
|
|
# Use --rawfile to read body directly from file, avoiding shell variable encoding issues
|
|
jq -n \
|
|
--arg tag "$TAG_NAME" \
|
|
--arg name "$RELEASE_NAME" \
|
|
--rawfile body release_body.txt \
|
|
'{
|
|
tag_name: $tag,
|
|
name: $name,
|
|
body: $body,
|
|
target_commitish: "main"
|
|
}' > /tmp/release_payload.json
|
|
|
|
RELEASE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
|
--connect-timeout 30 --max-time 60 \
|
|
"${API_URL}/repos/${GITCODE_OWNER}/${GITCODE_REPO}/releases" \
|
|
-H "Content-Type: application/json; charset=utf-8" \
|
|
-H "Authorization: Bearer ${GITCODE_TOKEN}" \
|
|
--data-binary "@/tmp/release_payload.json")
|
|
|
|
HTTP_CODE=$(echo "$RELEASE_RESPONSE" | tail -n1)
|
|
RESPONSE_BODY=$(echo "$RELEASE_RESPONSE" | sed '$d')
|
|
|
|
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
echo "Release created successfully"
|
|
else
|
|
echo "Warning: Release creation returned HTTP $HTTP_CODE"
|
|
echo "$RESPONSE_BODY"
|
|
exit 1
|
|
fi
|
|
|
|
# Step 2: Upload files to release
|
|
echo "Uploading files to GitCode release..."
|
|
|
|
# Function to upload a single file with retry
|
|
upload_file() {
|
|
local file="$1"
|
|
local filename=$(basename "$file")
|
|
local max_retries=3
|
|
local retry=0
|
|
local curl_status=0
|
|
|
|
echo "Uploading: $filename"
|
|
|
|
# URL encode the filename
|
|
encoded_filename=$(printf '%s' "$filename" | jq -sRr @uri)
|
|
|
|
while [ $retry -lt $max_retries ]; do
|
|
# Get upload URL
|
|
curl_status=0
|
|
UPLOAD_INFO=$(curl -s --connect-timeout 30 --max-time 60 \
|
|
-H "Authorization: Bearer ${GITCODE_TOKEN}" \
|
|
"${API_URL}/repos/${GITCODE_OWNER}/${GITCODE_REPO}/releases/${TAG_NAME}/upload_url?file_name=${encoded_filename}") || curl_status=$?
|
|
|
|
if [ $curl_status -eq 0 ]; then
|
|
UPLOAD_URL=$(echo "$UPLOAD_INFO" | jq -r '.url // empty')
|
|
|
|
if [ -n "$UPLOAD_URL" ]; then
|
|
# Write headers to temp file to avoid shell escaping issues
|
|
echo "$UPLOAD_INFO" | jq -r '.headers | to_entries[] | "header = \"" + .key + ": " + .value + "\""' > /tmp/upload_headers.txt
|
|
|
|
# Upload file using PUT with headers from file
|
|
curl_status=0
|
|
UPLOAD_RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
|
|
-K /tmp/upload_headers.txt \
|
|
--data-binary "@${file}" \
|
|
"$UPLOAD_URL") || curl_status=$?
|
|
|
|
if [ $curl_status -eq 0 ]; then
|
|
HTTP_CODE=$(echo "$UPLOAD_RESPONSE" | tail -n1)
|
|
RESPONSE_BODY=$(echo "$UPLOAD_RESPONSE" | sed '$d')
|
|
|
|
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
echo " Uploaded: $filename"
|
|
return 0
|
|
else
|
|
echo " Failed (HTTP $HTTP_CODE), retry $((retry + 1))/$max_retries"
|
|
echo " Response: $RESPONSE_BODY"
|
|
fi
|
|
else
|
|
echo " Upload request failed (curl exit $curl_status), retry $((retry + 1))/$max_retries"
|
|
fi
|
|
else
|
|
echo " Failed to get upload URL, retry $((retry + 1))/$max_retries"
|
|
echo " Response: $UPLOAD_INFO"
|
|
fi
|
|
else
|
|
echo " Failed to get upload URL (curl exit $curl_status), retry $((retry + 1))/$max_retries"
|
|
echo " Response: $UPLOAD_INFO"
|
|
fi
|
|
|
|
retry=$((retry + 1))
|
|
[ $retry -lt $max_retries ] && sleep 3
|
|
done
|
|
|
|
echo " Failed: $filename after $max_retries retries"
|
|
exit 1
|
|
}
|
|
|
|
# Upload non-yml/json files first
|
|
for file in release-assets/*; do
|
|
if [ -f "$file" ]; then
|
|
filename=$(basename "$file")
|
|
if [[ ! "$filename" =~ \.(yml|yaml|json)$ ]]; then
|
|
upload_file "$file"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Upload yml/json files last
|
|
for file in release-assets/*; do
|
|
if [ -f "$file" ]; then
|
|
filename=$(basename "$file")
|
|
if [[ "$filename" =~ \.(yml|yaml|json)$ ]]; then
|
|
upload_file "$file"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo "GitCode release sync completed!"
|
|
|
|
- name: Cleanup temp files
|
|
if: always()
|
|
shell: bash
|
|
run: |
|
|
rm -f /tmp/release_payload.json /tmp/upload_headers.txt release_body.txt
|
|
rm -rf release-assets/
|
|
|
|
- name: Send failure notification to Feishu
|
|
if: always() && (failure() || cancelled())
|
|
shell: bash
|
|
env:
|
|
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
|
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
|
|
TAG_NAME: ${{ steps.get-tag.outputs.tag }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
JOB_STATUS: ${{ job.status }}
|
|
run: |
|
|
# Determine status and color
|
|
if [ "$JOB_STATUS" = "cancelled" ]; then
|
|
STATUS_TEXT="已取消"
|
|
COLOR="orange"
|
|
else
|
|
STATUS_TEXT="失败"
|
|
COLOR="red"
|
|
fi
|
|
|
|
# Build description using printf
|
|
DESCRIPTION=$(printf "**标签:** %s\n\n**状态:** %s\n\n**工作流:** [查看详情](%s)" "$TAG_NAME" "$STATUS_TEXT" "$RUN_URL")
|
|
|
|
# Send notification
|
|
pnpm tsx scripts/feishu-notify.ts send \
|
|
-t "GitCode 同步${STATUS_TEXT}" \
|
|
-d "$DESCRIPTION" \
|
|
-c "${COLOR}"
|