mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-07 23:33:14 +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 --> <!-- 🚨 Branch Strategy Change (Effective April 3, 2026) 🚨 The `main` branch is now under CODE FREEZE. - main branch: Only accepts critical bug fixes via `hotfix/*` branches. Fix PRs must be minimal in scope and must not include any refactoring code. - v2 branch: All new features, refactoring, and optimizations should be submitted to the `v2` branch. If you are submitting a bug fix to main, please ensure your PR is from a `hotfix/*` branch. --> ### What this PR does Before this PR: GitCode release sync builds signed Windows artifacts and uploads them to GitCode in one self-hosted Windows signing job. If the signing runner has unreliable outbound network connectivity, the GitCode release creation or asset upload can fail after the signed artifacts were already built. The workflow also has no dry-run mode for validating a manual release sync. After this PR: The workflow builds signed Windows artifacts on the Windows signing runner, uploads them as a short-lived GitHub Actions artifact, then performs GitCode release creation and asset upload from `ubuntu-latest`. Manual dispatch supports a `dry_run` mode that previews the release payload and upload file list without creating the GitCode release. Windows code signing also retries timestamping across multiple timestamp servers before failing. <!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: --> Fixes # None ### Why we need it and why it was done in this way The following tradeoffs were made: The release sync now uses an intermediate GitHub Actions artifact to pass signed Windows files from the signing runner to the Ubuntu sync job. This adds one artifact upload/download step, but keeps certificate access constrained to the signing runner while moving GitCode API traffic to a more reliable hosted runner. The following alternatives were considered: Keeping GitCode sync on the signing runner was simpler, but it leaves release sync vulnerable to transient network failures on that runner. Retrying only the GitCode upload would not address timestamp-server flakiness during Windows signing, so this PR also adds timestamp server fallback and retry support in `scripts/win-sign.js`. Links to places where the discussion took place: N/A ### Breaking changes None. ### Special notes for your reviewer Validation performed: - `pnpm format` - `pnpm lint` (passed with one pre-existing unrelated React hook warning in `src/renderer/src/pages/settings/ProviderSettings/ModelList/ManageModelsPopup.tsx`) - `pnpm test` - Parsed `.github/workflows/sync-to-gitcode.yml` with the repository `yaml` package `actionlint` was attempted, but the npm package named `actionlint` does not expose a binary and this environment does not have Go installed to run the upstream Go tool directly. ### 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) - [ ] 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 - [ ] 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 ``` Signed-off-by: zhibisora <73344387+zhibisora@users.noreply.github.com>
500 lines
17 KiB
YAML
500 lines
17 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
|
|
dry_run:
|
|
description: 'Run all steps except GitCode release creation/upload and Feishu notification'
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
build-windows-signed:
|
|
runs-on: [self-hosted, windows-signing]
|
|
outputs:
|
|
tag: ${{ steps.get-tag.outputs.tag }}
|
|
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: Show checked-out revision
|
|
shell: bash
|
|
run: |
|
|
echo "Checkout ref: ${{ steps.get-tag.outputs.tag }}"
|
|
git log -1 --format='%H %s'
|
|
|
|
- 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@v5
|
|
|
|
- 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: Upload signed Windows artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: signed-windows-artifacts
|
|
path: |
|
|
dist/*.exe
|
|
dist/latest.yml
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
sync-to-gitcode:
|
|
runs-on: ubuntu-latest
|
|
needs: build-windows-signed
|
|
steps:
|
|
- name: Check out Git repository
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ needs.build-windows-signed.outputs.tag }}
|
|
|
|
- name: Download GitHub release assets
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG_NAME: ${{ needs.build-windows-signed.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: Download signed Windows artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: signed-windows-artifacts
|
|
path: signed-windows-artifacts
|
|
|
|
- name: Replace Windows files with signed versions
|
|
shell: bash
|
|
run: |
|
|
echo "Replacing Windows files with signed versions..."
|
|
|
|
# Verify signed files exist first
|
|
if ! ls signed-windows-artifacts/*.exe 1>/dev/null 2>&1; then
|
|
echo "ERROR: No signed .exe files found in signed-windows-artifacts/"
|
|
exit 1
|
|
fi
|
|
if [ ! -f signed-windows-artifacts/latest.yml ]; then
|
|
echo "ERROR: No signed latest.yml found in signed-windows-artifacts/"
|
|
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 signed-windows-artifacts/*.exe release-assets/ || { echo "ERROR: Failed to copy .exe files"; exit 1; }
|
|
cp signed-windows-artifacts/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: ${{ needs.build-windows-signed.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: Dry run GitCode release sync
|
|
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' }}
|
|
shell: bash
|
|
env:
|
|
GITCODE_OWNER: ${{ vars.GITCODE_OWNER }}
|
|
GITCODE_REPO: ${{ vars.GITCODE_REPO }}
|
|
GITCODE_API_URL: ${{ vars.GITCODE_API_URL }}
|
|
TAG_NAME: ${{ needs.build-windows-signed.outputs.tag }}
|
|
RELEASE_NAME: ${{ steps.release-info.outputs.name }}
|
|
LANG: C.UTF-8
|
|
LC_ALL: C.UTF-8
|
|
run: |
|
|
API_URL="${GITCODE_API_URL:-https://api.gitcode.com/api/v5}"
|
|
|
|
echo "Dry run enabled. Skipping GitCode release creation and asset upload."
|
|
echo "GitCode API URL: $API_URL"
|
|
echo "Tag: $TAG_NAME"
|
|
echo "Release name: $RELEASE_NAME"
|
|
echo "Repo: ${GITCODE_OWNER:-<unset>}/${GITCODE_REPO:-<unset>}"
|
|
echo
|
|
echo "Release payload preview:"
|
|
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"
|
|
}'
|
|
echo
|
|
echo "Files that would be uploaded:"
|
|
find release-assets -maxdepth 1 -type f -print | sort
|
|
|
|
- name: Create GitCode release and upload files
|
|
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true' }}
|
|
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: ${{ needs.build-windows-signed.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/
|
|
|
|
notify:
|
|
runs-on: ubuntu-latest
|
|
needs: [build-windows-signed, sync-to-gitcode]
|
|
if: >-
|
|
always() &&
|
|
(github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true') &&
|
|
(
|
|
needs.build-windows-signed.result == 'failure' ||
|
|
needs.build-windows-signed.result == 'cancelled' ||
|
|
needs.sync-to-gitcode.result == 'failure' ||
|
|
needs.sync-to-gitcode.result == 'cancelled'
|
|
)
|
|
steps:
|
|
- name: Send failure notification to Feishu
|
|
shell: bash
|
|
env:
|
|
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
|
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
|
|
TAG_NAME: ${{ needs.build-windows-signed.outputs.tag }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
BUILD_RESULT: ${{ needs.build-windows-signed.result }}
|
|
SYNC_RESULT: ${{ needs.sync-to-gitcode.result }}
|
|
run: |
|
|
if [ "$BUILD_RESULT" = "cancelled" ] || [ "$SYNC_RESULT" = "cancelled" ]; then
|
|
STATUS_TEXT="已取消"
|
|
COLOR="orange"
|
|
else
|
|
STATUS_TEXT="失败"
|
|
COLOR="red"
|
|
fi
|
|
|
|
if [ "$BUILD_RESULT" = "failure" ] || [ "$BUILD_RESULT" = "cancelled" ]; then
|
|
STAGE="Windows 签名打包"
|
|
else
|
|
STAGE="同步"
|
|
fi
|
|
|
|
TITLE="GitCode ${STAGE}${STATUS_TEXT}"
|
|
DESCRIPTION=$(printf "**标签:** %s\n\n**阶段:** %s\n\n**状态:** %s\n\n**工作流:** [查看详情](%s)" "${TAG_NAME:-unknown}" "$STAGE" "$STATUS_TEXT" "$RUN_URL")
|
|
export TITLE DESCRIPTION COLOR
|
|
|
|
node <<'NODE'
|
|
const crypto = require('node:crypto')
|
|
const https = require('node:https')
|
|
|
|
const webhookUrl = process.env.FEISHU_WEBHOOK_URL
|
|
const secret = process.env.FEISHU_WEBHOOK_SECRET
|
|
const title = process.env.TITLE
|
|
const description = process.env.DESCRIPTION
|
|
const color = process.env.COLOR
|
|
|
|
if (!webhookUrl) {
|
|
throw new Error('FEISHU_WEBHOOK_URL environment variable is required')
|
|
}
|
|
if (!secret) {
|
|
throw new Error('FEISHU_WEBHOOK_SECRET environment variable is required')
|
|
}
|
|
|
|
const timestamp = Math.floor(Date.now() / 1000)
|
|
const sign = crypto.createHmac('sha256', `${timestamp}\n${secret}`).digest('base64')
|
|
const payload = JSON.stringify({
|
|
timestamp: timestamp.toString(),
|
|
sign,
|
|
msg_type: 'interactive',
|
|
card: {
|
|
elements: [
|
|
{
|
|
tag: 'div',
|
|
text: {
|
|
tag: 'lark_md',
|
|
content: description
|
|
}
|
|
}
|
|
],
|
|
header: {
|
|
template: color,
|
|
title: {
|
|
tag: 'plain_text',
|
|
content: title
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const url = new URL(webhookUrl)
|
|
const req = https.request(
|
|
{
|
|
hostname: url.hostname,
|
|
path: url.pathname + url.search,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(payload)
|
|
}
|
|
},
|
|
(res) => {
|
|
let data = ''
|
|
res.on('data', (chunk) => {
|
|
data += chunk.toString()
|
|
})
|
|
res.on('end', () => {
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
console.log('Notification sent successfully!')
|
|
} else {
|
|
console.error(`Feishu API error: ${res.statusCode} - ${data}`)
|
|
process.exit(1)
|
|
}
|
|
})
|
|
}
|
|
)
|
|
|
|
req.on('error', (error) => {
|
|
console.error(error)
|
|
process.exit(1)
|
|
})
|
|
req.write(payload)
|
|
req.end()
|
|
NODE
|