mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 20:59:22 +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>
83 lines
2.6 KiB
JavaScript
83 lines
2.6 KiB
JavaScript
const { execSync } = require('child_process')
|
|
|
|
const DEFAULT_TIMESTAMP_URLS = [
|
|
'http://timestamp.digicert.com',
|
|
'http://timestamp.sectigo.com',
|
|
'http://timestamp.globalsign.com/tsa/r6advanced1',
|
|
'http://timestamp.globalsign.com/tsa/r45standard'
|
|
]
|
|
|
|
const MAX_SIGN_ATTEMPTS_PER_TIMESTAMP = 3
|
|
const SIGN_RETRY_DELAYS_MS = [5000, 10000]
|
|
|
|
function getTimestampUrls() {
|
|
const configuredUrls = process.env.WIN_SIGN_TIMESTAMP_URLS?.split(',')
|
|
.map((url) => url.trim())
|
|
.filter(Boolean)
|
|
|
|
return configuredUrls?.length ? configuredUrls : DEFAULT_TIMESTAMP_URLS
|
|
}
|
|
|
|
function sleep(ms) {
|
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
}
|
|
|
|
function signFile({ certPath, csp, keyContainer, path, timestampUrl }) {
|
|
const signCommand = `signtool sign /tr "${timestampUrl}" /td sha256 /fd sha256 /v /f "${certPath}" /csp "${csp}" /k "${keyContainer}" "${path}"`
|
|
execSync(signCommand, { stdio: 'inherit' })
|
|
}
|
|
|
|
function signFileWithRetry(options) {
|
|
const timestampUrls = getTimestampUrls()
|
|
let lastError
|
|
|
|
for (const timestampUrl of timestampUrls) {
|
|
for (let attempt = 1; attempt <= MAX_SIGN_ATTEMPTS_PER_TIMESTAMP; attempt++) {
|
|
try {
|
|
console.log(
|
|
`Signing attempt ${attempt}/${MAX_SIGN_ATTEMPTS_PER_TIMESTAMP} with timestamp server: ${timestampUrl}`
|
|
)
|
|
signFile({ ...options, timestampUrl })
|
|
return
|
|
} catch (error) {
|
|
lastError = error
|
|
|
|
if (attempt < MAX_SIGN_ATTEMPTS_PER_TIMESTAMP) {
|
|
const delayMs = SIGN_RETRY_DELAYS_MS[attempt - 1] ?? SIGN_RETRY_DELAYS_MS[SIGN_RETRY_DELAYS_MS.length - 1]
|
|
console.warn(`Code signing attempt failed. Retrying in ${delayMs / 1000}s...`)
|
|
sleep(delayMs)
|
|
} else {
|
|
console.warn(`Timestamp server failed after ${MAX_SIGN_ATTEMPTS_PER_TIMESTAMP} attempts: ${timestampUrl}`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError
|
|
}
|
|
|
|
exports.default = async function (configuration) {
|
|
if (process.env.WIN_SIGN) {
|
|
const { path } = configuration
|
|
if (configuration.path) {
|
|
try {
|
|
const certPath = process.env.CHERRY_CERT_PATH
|
|
const keyContainer = process.env.CHERRY_CERT_KEY
|
|
const csp = process.env.CHERRY_CERT_CSP
|
|
|
|
if (!certPath || !keyContainer || !csp) {
|
|
throw new Error('CHERRY_CERT_PATH, CHERRY_CERT_KEY or CHERRY_CERT_CSP is not set')
|
|
}
|
|
|
|
console.log('Start code signing...')
|
|
console.log('Signing file:', path)
|
|
signFileWithRetry({ certPath, csp, keyContainer, path })
|
|
console.log('Code signing completed')
|
|
} catch (error) {
|
|
console.error('Code signing failed:', error)
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
}
|