Files
CherryHQ-cherry-studio/scripts/win-sign.js
zhibisora b0c77f6415 fix(ci): improve GitCode sync reliability (#15063)
<!-- 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>
2026-05-14 10:06:56 +08:00

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
}
}
}
}