diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 225638def259..6e0f1da40cdf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,18 @@ name: Build # 可复用的单平台构建工作流,由 release.yml 按平台矩阵调用。 # 每个平台的所有安装包在此完成并上传为 release-- 工件。 +# +# 签名:私钥放在秘密变量 SIGNATURE_KEYS。macOS 的证书来源按优先级: +# 1) secrets.CRT 或 vars.CRT —— 完整证书链(叶子 + 逐级签发 CA,最后是自签名根) +# 2) vars.SIGNATURE_CRT —— 旧的单张证书变量 +# Windows 仍使用 vars.SIGNATURE_CRT(只需叶子证书)。两者均为 PEM。构建时会临时合成 +# PFX(Windows)和 P12(macOS),对 exe / msi / msix 以及 app / dmg 做本地签名; +# 未设置时跳过所有签名步骤。 +# macOS 的证书建议用完整链:codesign 需要自己构建到自签名根的链,只给 CA 签发的叶子 +# 证书会因缺链而报 errSecInternalComponent。若只给了叶子证书,macOS 步骤会用同一把 +# 私钥临时生成一张自签名证书完成签名(每次构建都不同)。 +# macOS 会按公钥自动识别叶子证书并重排证书顺序,叶子证书在前/在后都能用。 +# 可选 SIGNATURE_KEYS_PASSWORD:私钥若带口令,用它解密。 on: workflow_call: inputs: @@ -36,6 +48,12 @@ jobs: VSCODE_ARCH: ${{ inputs.arch }} VSCODE_QUALITY: stable GITHUB_TOKEN: ${{ github.token }} + HAS_SIGNING_KEYS: ${{ secrets.SIGNATURE_KEYS != '' }} + # GitHub Releases 偶发 504,Electron 二进制统一走 npmmirror 镜像。 + # 内容完整性不受影响:@electron/get 从镜像取 SHASUMS256.txt, + # gulp-electron 还会再对照仓库内 build/checksums/electron.txt 校验。 + # 若某次升级 Electron 时镜像尚未同步,临时删除此变量即可回退到 GitHub。 + ELECTRON_MIRROR: https://npmmirror.com/mirrors/electron/ steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -100,6 +118,85 @@ jobs: tar -czf snap-src.tar.gz -C "$(dirname "$SNAP_DIR")" "$(basename "$SNAP_DIR")" # -------------------------------------------------------------- Windows + # 先产出客户端,签名后被打进 exe / zip / msi / msix,保证安装后主程序也是已签名的 + - name: Build Windows client + if: inputs.platform == 'win32' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + npm run gulp vscode-win32-x64-min + npm run gulp vscode-win32-x64-inno-updater + + # 私钥来自 secrets.SIGNATURE_KEYS,证书来自 vars.SIGNATURE_CRT,分别落盘后再合成 PFX; + # 证书 Subject 同时用作 MSIX 的 Publisher,否则 MSIX 签名会失败 + - name: Prepare signing certificate (Windows) + if: inputs.platform == 'win32' && env.HAS_SIGNING_KEYS == 'true' + shell: pwsh + env: + SIGNING_KEY: ${{ secrets.SIGNATURE_KEYS }} + SIGNING_CRT: ${{ vars.SIGNATURE_CRT }} + SIGNATURE_KEYS_PASSWORD: ${{ secrets.SIGNATURE_KEYS_PASSWORD }} + run: | + $ErrorActionPreference = 'Stop' + + if (-not $env:SIGNING_CRT) { throw 'SIGNATURE_CRT variable is not set; it must contain the PEM certificate' } + + $keyPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.key' + $certPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.crt' + [System.IO.File]::WriteAllText($keyPath, $env:SIGNING_KEY) + [System.IO.File]::WriteAllText($certPath, $env:SIGNING_CRT) + if (-not (Select-String -Path $certPath -Pattern 'BEGIN CERTIFICATE' -Quiet)) { + throw 'SIGNATURE_CRT does not contain a BEGIN CERTIFICATE block' + } + + $openssl = (Get-Command openssl.exe -ErrorAction SilentlyContinue).Source + if (-not $openssl) { + $gitOpenSsl = Join-Path $env:ProgramFiles 'Git\usr\bin\openssl.exe' + if (Test-Path $gitOpenSsl) { $openssl = $gitOpenSsl } + } + if (-not $openssl) { throw 'openssl.exe not found, cannot build the PFX from SIGNATURE_KEYS/SIGNATURE_CRT' } + + $password = [System.Guid]::NewGuid().ToString('N') + $pfxPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.pfx' + $keyArgs = @() + if ($env:SIGNATURE_KEYS_PASSWORD) { $keyArgs = @('-passin', "pass:$env:SIGNATURE_KEYS_PASSWORD") } + + & $openssl pkcs12 -export -inkey $keyPath -in $certPath -out $pfxPath -passout "pass:$password" -name BlockIDE @keyArgs + if ($LASTEXITCODE -ne 0) { throw "openssl pkcs12 exited with code $LASTEXITCODE" } + + $subject = ((& $openssl x509 -in $certPath -noout -subject -nameopt RFC2253) -join '') -replace '^subject\s*=\s*', '' + if (-not $subject) { throw 'Failed to read certificate subject from SIGNATURE_CRT' } + + "SIGNING_PFX_PATH=$pfxPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "SIGNING_PFX_PASSWORD=$password" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "BLOCKIDE_MSIX_PUBLISHER=$subject" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "Signing certificate subject: $subject" + + - name: Sign Windows client + if: inputs.platform == 'win32' && env.HAS_SIGNING_KEYS == 'true' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $sdkRoot = 'C:\Program Files (x86)\Windows Kits\10\bin' + # bin 下同时存在 x86/x64 等目录,只按版本号目录排序,避免选中 x86 拼出错路径 + $signtool = Get-ChildItem $sdkRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^\d+(\.\d+)+$' } | + Sort-Object { [version]$_.Name } -Descending | + ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } | + Where-Object { Test-Path $_ } | + Select-Object -First 1 + if (-not $signtool) { throw "signtool.exe not found under $sdkRoot" } + + $clientDir = Join-Path (Split-Path $env:GITHUB_WORKSPACE) 'VSCode-win32-x64' + $targets = Get-ChildItem $clientDir -Filter *.exe -File + if (-not $targets) { throw "No executable found in $clientDir" } + + foreach ($file in $targets) { + & $signtool sign /fd sha256 /f $env:SIGNING_PFX_PATH /p $env:SIGNING_PFX_PASSWORD $file.FullName + if ($LASTEXITCODE -ne 0) { throw "signtool failed for $($file.Name)" } + } + - name: Package Windows (exe / zip / msi / msix) if: inputs.platform == 'win32' shell: pwsh @@ -120,13 +217,12 @@ jobs: $env:PATH = "$env:WIX_BIN;$env:PATH" $sdkDirectory = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Directory -ErrorAction SilentlyContinue | - Sort-Object Name -Descending | Select-Object -First 1 + Where-Object { $_.Name -match '^\d+(\.\d+)+$' } | + Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1 if ($sdkDirectory) { $env:PATH = "$(Join-Path $sdkDirectory.FullName 'x64');$env:PATH" } - npm run gulp vscode-win32-x64-min - npm run gulp vscode-win32-x64-inno-updater npm run gulp vscode-win32-x64-system-setup npm run gulp vscode-win32-x64-msi npm run gulp vscode-win32-x64-msix @@ -142,13 +238,276 @@ jobs: Pop-Location } + - name: Sign Windows installers + if: inputs.platform == 'win32' && env.HAS_SIGNING_KEYS == 'true' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $sdkRoot = 'C:\Program Files (x86)\Windows Kits\10\bin' + # bin 下同时存在 x86/x64 等目录,只按版本号目录排序,避免选中 x86 拼出错路径 + $signtool = Get-ChildItem $sdkRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^\d+(\.\d+)+$' } | + Sort-Object { [version]$_.Name } -Descending | + ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } | + Where-Object { Test-Path $_ } | + Select-Object -First 1 + if (-not $signtool) { throw "signtool.exe not found under $sdkRoot" } + + $targets = Get-ChildItem artifacts -File | Where-Object { $_.Extension -in '.exe', '.msi', '.msix' } + if (-not $targets) { throw 'No installer artifacts found to sign' } + + foreach ($file in $targets) { + & $signtool sign /fd sha256 /f $env:SIGNING_PFX_PATH /p $env:SIGNING_PFX_PASSWORD $file.FullName + if ($LASTEXITCODE -ne 0) { throw "signtool failed for $($file.Name)" } + } + # ----------------------------------------------------------------- macOS + - name: Build macOS client + if: inputs.platform == 'darwin' + run: | + set -euo pipefail + npm run gulp vscode-darwin-arm64-min + + # 临时钥匙串路径必须是 $AGENT_TEMPDIRECTORY/buildagent.keychain,build/darwin/sign.ts 依赖它 + - name: Prepare signing certificate (macOS) + if: inputs.platform == 'darwin' && env.HAS_SIGNING_KEYS == 'true' + env: + SIGNING_KEY: ${{ secrets.SIGNATURE_KEYS }} + # 优先用 CRT(完整证书链,secret 或 variable 均可),没设置时回退到旧的单证书变量 + SIGNING_CRT: ${{ secrets.CRT || vars.CRT || vars.SIGNATURE_CRT }} + SIGNATURE_KEYS_PASSWORD: ${{ secrets.SIGNATURE_KEYS_PASSWORD }} + run: | + set -euo pipefail + + if [ -z "${SIGNING_CRT:-}" ]; then + echo 'No signing certificate found: set secrets.CRT (or vars.CRT / vars.SIGNATURE_CRT) to the PEM chain.' + exit 1 + fi + + KEY="$RUNNER_TEMP/blockide-signing.key" + CERT="$RUNNER_TEMP/blockide-signing.crt" + P12="$RUNNER_TEMP/blockide-signing.p12" + KEYCHAIN="$RUNNER_TEMP/buildagent.keychain" + # 统一去掉 CRLF,避免从 Windows 粘贴进来的换行影响 PEM 解析 + printf '%s\n' "$SIGNING_KEY" | tr -d '\r' > "$KEY" + printf '%s\n' "$SIGNING_CRT" | tr -d '\r' > "$CERT" + + # 按证书块拆分 PEM:即使 secret 里混入了私钥,也只会取证书部分 + CERT_DIR="$RUNNER_TEMP/blockide-certs" + rm -rf "$CERT_DIR" + mkdir -p "$CERT_DIR" + awk -v dir="$CERT_DIR" ' + /-----BEGIN CERTIFICATE-----/ { n++; file = dir "/cert-" n ".pem"; capture = 1 } + capture { print >> file } + /-----END CERTIFICATE-----/ { capture = 0 } + ' "$CERT" + CERT_COUNT="$(ls -1 "$CERT_DIR" | wc -l | tr -d ' ')" + if [ "$CERT_COUNT" -lt 1 ]; then + echo '::error::The signing certificate contains no BEGIN CERTIFICATE block.' + exit 1 + fi + + # 用公钥匹配找出真正与 SIGNATURE_KEYS 配对的叶子证书,并把链首调整为它, + # 这样证书在 CRT 里的先后顺序(叶子在前 / CA 在前)都不影响结果。 + if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then + KEY_PUB="$(/usr/bin/openssl pkey -in "$KEY" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -pubout)" + else + KEY_PUB="$(/usr/bin/openssl pkey -in "$KEY" -pubout)" + fi + LEAF="" + for cert in "$CERT_DIR"/cert-*.pem; do + if [ "$(/usr/bin/openssl x509 -in "$cert" -noout -pubkey)" = "$KEY_PUB" ]; then + LEAF="$cert" + break + fi + done + if [ -z "$LEAF" ]; then + echo '::error::None of the certificates matches SIGNATURE_KEYS (public key mismatch).' + exit 1 + fi + if [ "$LEAF" != "$CERT_DIR/cert-1.pem" ]; then + echo '::notice::Leaf certificate was not first in the PEM; reordering the chain leaf-first.' + CHAIN_DIR="$RUNNER_TEMP/blockide-chain" + rm -rf "$CHAIN_DIR" + mkdir -p "$CHAIN_DIR" + cp "$LEAF" "$CHAIN_DIR/cert-1.pem" + index=1 + for cert in "$CERT_DIR"/cert-*.pem; do + if [ "$cert" != "$LEAF" ]; then + index=$((index + 1)) + cp "$cert" "$CHAIN_DIR/cert-$index.pem" + fi + done + CERT_DIR="$CHAIN_DIR" + LEAF="$CERT_DIR/cert-1.pem" + fi + ROOT="$CERT_DIR/cert-$CERT_COUNT.pem" + + echo "Signing PEM contains $CERT_COUNT certificate(s); leaf certificate:" + /usr/bin/openssl x509 -in "$LEAF" -noout -subject -issuer -dates + # 过期证书无论怎么设置信任都无法用于签名,提前给出明确错误 + if ! /usr/bin/openssl x509 -in "$LEAF" -noout -checkend 0 >/dev/null 2>&1; then + echo '::error::The signing certificate has expired; codesign cannot use it.' + exit 1 + fi + + # 校验链完整性:相邻证书的 issuer/subject 必须对上,最后一张必须自签名 + chain_complete=1 + index=1 + while [ "$index" -lt "$CERT_COUNT" ]; do + child_issuer="$(/usr/bin/openssl x509 -in "$CERT_DIR/cert-$index.pem" -noout -issuer -nameopt RFC2253 | sed 's/^issuer=//')" + parent_subject="$(/usr/bin/openssl x509 -in "$CERT_DIR/cert-$((index + 1)).pem" -noout -subject -nameopt RFC2253 | sed 's/^subject=//')" + if [ "$child_issuer" != "$parent_subject" ]; then + chain_complete=0 + break + fi + index=$((index + 1)) + done + root_issuer="$(/usr/bin/openssl x509 -in "$ROOT" -noout -issuer -nameopt RFC2253 | sed 's/^issuer=//')" + root_subject="$(/usr/bin/openssl x509 -in "$ROOT" -noout -subject -nameopt RFC2253 | sed 's/^subject=//')" + if [ "$root_issuer" != "$root_subject" ]; then + chain_complete=0 + fi + + if [ "$chain_complete" -eq 1 ]; then + SIGN_CERT="$LEAF" + TRUST_CERT="$ROOT" + echo "Using the provided certificate chain (root: $ROOT)" + else + # 证书是 CA 签发的叶子证书,但缺签发它的 CA,codesign 无法 + # 构建到自签名根的链。这里用同一把私钥就地补一张自签名证书, + # 保留原证书的 subject(CN/O/OU),让 CI 无需人工改变量也能签名。 + # 代价:每次构建生成的证书不同(序列号/有效期不同)。如果想固定证书, + # 把签发 CA 的证书补进 secrets.CRT 即可,工作流会自动改回用你的链。 + echo "::warning::The signing certificate does not end in a self-signed root (last issuer: $root_issuer)." + echo '::warning::Generating a self-signed certificate from SIGNATURE_KEYS for this build.' + echo '::warning::Add the issuing CA certificate to secrets.CRT to sign with your own stable certificate chain.' + SIGN_CERT="$RUNNER_TEMP/blockide-signing-selfsigned.crt" + TRUST_CERT="$SIGN_CERT" + EXT_FILE="$RUNNER_TEMP/blockide-signing-selfsigned.ext" + CSR_FILE="$RUNNER_TEMP/blockide-signing-selfsigned.csr" + printf '%s\n' \ + '[codesign]' \ + 'basicConstraints=critical,CA:TRUE' \ + 'keyUsage=critical,digitalSignature,keyCertSign' \ + 'extendedKeyUsage=critical,codeSigning' > "$EXT_FILE" + if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then + /usr/bin/openssl x509 -x509toreq -in "$LEAF" -signkey "$KEY" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -out "$CSR_FILE" + /usr/bin/openssl x509 -req -in "$CSR_FILE" -signkey "$KEY" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -sha256 -days 7300 -extfile "$EXT_FILE" -extensions codesign -out "$SIGN_CERT" + else + /usr/bin/openssl x509 -x509toreq -in "$LEAF" -signkey "$KEY" -out "$CSR_FILE" + /usr/bin/openssl x509 -req -in "$CSR_FILE" -signkey "$KEY" -sha256 -days 7300 -extfile "$EXT_FILE" -extensions codesign -out "$SIGN_CERT" + fi + echo 'Generated self-signed signing certificate:' + /usr/bin/openssl x509 -in "$SIGN_CERT" -noout -subject -issuer -dates + fi + + # Apple 的时间戳服务不保证支持非 Apple 签发的身份(codesign/osx-sign + # 文档均注明),私有证书的时间戳也没有实际意义:直接关闭,避免签名阶段 + # 因时间戳服务拒绝而失败。只有证书链以 Apple 根证书结尾时才启用。 + if [ "$chain_complete" -eq 1 ] && printf '%s' "$root_subject" | grep -qi 'O=Apple Inc\.'; then + echo 'Using the Apple timestamp service (chain is anchored at an Apple root)' + else + echo 'Disabling codesign timestamping (non-Apple signing identity)' + echo 'CODESIGN_TIMESTAMP=none' >> "$GITHUB_ENV" + fi + + KEYCHAIN_PASSWORD="$(openssl rand -hex 24)" + P12_PASSWORD="$(openssl rand -hex 24)" + + # 用系统自带的 LibreSSL 导出,默认算法较旧,Security.framework 才能导入。 + # 这里不拼空数组参数,macOS 默认的 bash 3.2 在 set -u 下展开空数组会报错。 + # 只导出叶子证书;证书链中的其他证书稍后单独导入临时钥匙串。 + if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then + /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$SIGN_CERT" -out "$P12" \ + -passout "pass:$P12_PASSWORD" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -name BlockIDE + else + /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$SIGN_CERT" -out "$P12" \ + -passout "pass:$P12_PASSWORD" -name BlockIDE + fi + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" + security import "$P12" -k "$KEYCHAIN" -P "$P12_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" + security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') + + # 中间/根 CA 必须进钥匙串,codesign 才能把完整的链嵌进签名。 + # 自动生成自签名证书时没有额外证书需要导入。 + if [ "$SIGN_CERT" = "$LEAF" ]; then + for extra in "$CERT_DIR"/cert-*.pem; do + if [ "$extra" != "$LEAF" ]; then + security import "$extra" -k "$KEYCHAIN" + fi + done + fi + + # 只把链上的自签名根(或自动生成的自签名证书)注册为信任锚, + # 叶子证书保持“使用系统默认”。千万不要把叶子证书设成 trustAsRoot: + # 那会让链在叶子处截断,codesign 报 + # "unable to build chain to self-signed root" + errSecInternalComponent。 + echo "Registering chain root as trustRoot for the codeSign policy: $TRUST_CERT" + sudo security add-trusted-cert -d -r trustRoot -p codeSign -k /Library/Keychains/System.keychain "$TRUST_CERT" + + # 用证书的 SHA-1 指纹而不是展示名作为身份:find-identity 的展示名可能带 + # "(CSSMERR_TP_NOT_TRUSTED)" 之类的诊断后缀,按引号截取会把后缀一起写进 + # CODESIGN_IDENTITY,codesign 于是报 "no identity found"。 + identity="$(security find-identity -v -p codesigning "$KEYCHAIN" | grep -oE '[0-9A-Fa-f]{40}' | head -1 || true)" + if [ -z "$identity" ]; then + # 身份尚未被判定为 valid 时 -v 可能什么都不列,退回完整身份列表 + identity="$(security find-identity -p codesigning "$KEYCHAIN" | grep -oE '[0-9A-Fa-f]{40}' | head -1 || true)" + fi + if [ -z "$identity" ]; then + security find-identity -p codesigning "$KEYCHAIN" || true + echo 'No code signing identity found in the temporary keychain' + exit 1 + fi + + valid_line="$(security find-identity -v -p codesigning "$KEYCHAIN" | grep "$identity" || true)" + if [ -z "$valid_line" ] || printf '%s\n' "$valid_line" | grep -q 'CSSMERR'; then + security find-identity -p codesigning "$KEYCHAIN" || true + security verify-cert -c "$SIGN_CERT" -p codeSign || true + echo "::error::Signing identity $identity is still not trusted after registering $TRUST_CERT as the trust root." + exit 1 + fi + echo "Signing identity: $identity" + + # 用主可执行文件的副本做一次真实签名预检:证书链不完整或钥匙串访问 + # 有问题时 codesign 会返回 errSecInternalComponent,这里提前失败, + # 不必等到几分钟后签整个 app 才报错。 + PREFLIGHT="$RUNNER_TEMP/blockide-signing-preflight" + PRODUCT_NAME="$(node -p "require('./product.json').nameLong")" + APP_BINARY="$(dirname "$GITHUB_WORKSPACE")/VSCode-darwin-${VSCODE_ARCH}/${PRODUCT_NAME}.app/Contents/MacOS/${PRODUCT_NAME}" + if [ -f "$APP_BINARY" ]; then + cp -p "$APP_BINARY" "$PREFLIGHT" + else + printf 'preflight\n' > "$PREFLIGHT" + fi + if ! codesign --force --sign "$identity" --keychain "$KEYCHAIN" --timestamp=none "$PREFLIGHT"; then + echo '::error::codesign preflight failed; the signing identity or its certificate chain is unusable.' + exit 1 + fi + rm -f "$PREFLIGHT" + + { + echo "CODESIGN_IDENTITY=$identity" + echo "SIGNING_KEYCHAIN=$KEYCHAIN" + echo "AGENT_TEMPDIRECTORY=$RUNNER_TEMP" + } >> "$GITHUB_ENV" + + - name: Sign macOS app + if: inputs.platform == 'darwin' && env.HAS_SIGNING_KEYS == 'true' + run: | + set -euo pipefail + node build/darwin/sign.ts "$(dirname "$GITHUB_WORKSPACE")" + - name: Package macOS (zip) if: inputs.platform == 'darwin' run: | set -euo pipefail mkdir -p artifacts - npm run gulp vscode-darwin-arm64-min (cd "$(dirname "$GITHUB_WORKSPACE")" \ && zip -Xry "$GITHUB_WORKSPACE/artifacts/BlockIDE-darwin-arm64-${VERSION}.zip" VSCode-darwin-arm64) @@ -160,6 +519,13 @@ jobs: node build/darwin/create-dmg.ts "$(dirname "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE/artifacts" mv "artifacts/VSCode-darwin-arm64.dmg" "artifacts/BlockIDE-darwin-arm64-${VERSION}.dmg" + - name: Sign macOS dmg + if: inputs.platform == 'darwin' && env.HAS_SIGNING_KEYS == 'true' + run: | + set -euo pipefail + dmg="artifacts/BlockIDE-darwin-arm64-${VERSION}.dmg" + codesign --force --timestamp=none --sign "$CODESIGN_IDENTITY" --keychain "$SIGNING_KEYCHAIN" "$dmg" + - name: Upload artifacts uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93d43a196884..37c0723a33e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,10 +79,12 @@ jobs: # 三平台各自独立调用同一个可复用工作流,互不阻塞。 # 拆成三个 job(而非一个 matrix)是为了让 snap 只等待 Linux,而不是等整组矩阵完成。 + # secrets: inherit 让签名密钥 SIGNATURE_KEYS 透传给可复用工作流。 build-linux: name: Build linux-x64 needs: meta uses: ./.github/workflows/build.yml + secrets: inherit with: os: ubuntu-24.04 platform: linux @@ -93,6 +95,7 @@ jobs: name: Build win32-x64 needs: meta uses: ./.github/workflows/build.yml + secrets: inherit with: os: windows-2025 platform: win32 @@ -103,6 +106,7 @@ jobs: name: Build darwin-arm64 needs: meta uses: ./.github/workflows/build.yml + secrets: inherit with: os: macos-26 platform: darwin diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 4552dc313d9c..01c2cd4f456b 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -79,10 +79,23 @@ export async function retryOnTransientSignError(fn: () => Promise, maxAtte } } +/** + * `security find-identity` appends trust-evaluation diagnostics such as + * `(CSSMERR_TP_NOT_TRUSTED)` to an identity's display name. If such a name is + * passed to `codesign --sign`, codesign looks for the whole string and fails + * with "no identity found", so strip the suffix before signing. + */ +export function normalizeSigningIdentity(identity: string | undefined): string | undefined { + return identity?.replace(/\s*\(CSSMERR_[A-Z0-9_]+\)\s*$/, ''); +} + async function main(buildDir?: string): Promise { const tempDir = process.env['AGENT_TEMPDIRECTORY']; const arch = process.env['VSCODE_ARCH']; - const identity = process.env['CODESIGN_IDENTITY']; + const identity = normalizeSigningIdentity(process.env['CODESIGN_IDENTITY']); + // Apple 的时间戳服务不保证支持非 Apple 签发的身份,使用自签名/私有 CA 证书时 + // 工作流会传 `none` 显式关闭时间戳;未设置时沿用 osx-sign 的默认行为。 + const timestamp = process.env['CODESIGN_TIMESTAMP']; if (!buildDir) { throw new Error('$AGENT_BUILDDIRECTORY not set'); @@ -106,12 +119,17 @@ async function main(buildDir?: string): Promise { optionsForFile: (filePath) => ({ entitlements: getEntitlementsForFile(filePath), hardenedRuntime: true, + timestamp, }), preAutoEntitlements: false, preEmbedProvisioningProfile: false, keychain: getKeychainPath(tempDir), version: getElectronVersion(), identity, + // 工作流的“Prepare signing certificate”步骤已经校验过证书链并确认身份可用, + // 且 CODESIGN_IDENTITY 传的是证书 SHA-1 指纹。这里跳过 osx-sign 自己的 + // find-identity 名称匹配,避免展示名与指纹的格式差异再次导致身份匹配失败。 + identityValidation: false, }; // Only overwrite plist entries for x64 and arm64 builds, diff --git a/build/lib/download.ts b/build/lib/download.ts index 48352e7cd7bf..b108ef956682 100644 --- a/build/lib/download.ts +++ b/build/lib/download.ts @@ -46,7 +46,8 @@ export async function download(url: string, options: IDownloadOptions = {}): Pro lastError = error instanceof Error ? error : new Error(String(error)); if (attempt < attempts) { options.onRetry?.(lastError); - await new Promise(resolve => setTimeout(resolve, retryDelay)); + // 指数退避:1x, 2x, 4x ... 避免连续打同一个源 + await new Promise(resolve => setTimeout(resolve, retryDelay * 2 ** (attempt - 1))); } } } diff --git a/build/lib/test/darwinSign.test.ts b/build/lib/test/darwinSign.test.ts index 732df5ac1fc5..cc45e1bce53b 100644 --- a/build/lib/test/darwinSign.test.ts +++ b/build/lib/test/darwinSign.test.ts @@ -5,7 +5,7 @@ import assert from 'node:assert/strict'; import { suite, test } from 'node:test'; -import { retryOnTransientSignError } from '../../darwin/sign.ts'; +import { normalizeSigningIdentity, retryOnTransientSignError } from '../../darwin/sign.ts'; function codesignError(stderr: string): Error { return new Error(`Command failed: codesign --sign ABC --force --timestamp --options runtime locale.pak\nlocale.pak: ${stderr}`); @@ -47,3 +47,27 @@ suite('darwin sign', () => { assert.deepStrictEqual(await sign([timestampError, timestampError, timestampError], 3), { attempts: 3, error: 'locale.pak: A timestamp was expected but was not found.' }); }); }); + +suite('normalize signing identity', () => { + test('strips trust diagnostics appended by find-identity', () => { + assert.strictEqual( + normalizeSigningIdentity('ZIT Studio BlockIDE Code Signing (CSSMERR_TP_NOT_TRUSTED)'), + 'ZIT Studio BlockIDE Code Signing'); + assert.strictEqual( + normalizeSigningIdentity('ZIT Studio BlockIDE Code Signing (CSSMERR_TP_CERT_EXPIRED)'), + 'ZIT Studio BlockIDE Code Signing'); + }); + + test('keeps names and hashes that have no diagnostics', () => { + assert.strictEqual( + normalizeSigningIdentity('ZIT Studio BlockIDE Code Signing'), + 'ZIT Studio BlockIDE Code Signing'); + assert.strictEqual( + normalizeSigningIdentity('C7D5DE396508C18020A30E6C4E41764B97EA0490'), + 'C7D5DE396508C18020A30E6C4E41764B97EA0490'); + }); + + test('preserves undefined', () => { + assert.strictEqual(normalizeSigningIdentity(undefined), undefined); + }); +}); diff --git a/build/npm/electronTypes.ts b/build/npm/electronTypes.ts index 8ab9b13359ae..fdee71590ec3 100644 --- a/build/npm/electronTypes.ts +++ b/build/npm/electronTypes.ts @@ -27,8 +27,35 @@ export async function ensureElectronTypes(): Promise { } const { electronVersion } = getElectronVersion(); - const url = `https://github.com/electron/electron/releases/download/v${electronVersion}/electron.d.ts`; - const contents = await download(url, { checksumSha256: expectedChecksum }); + + // GitHub Releases 偶发 504,依次尝试多个镜像;所有镜像都会校验 SHA-256, + // 与 build/checksums/electron.txt 比对,保证内容一致。 + const urls = [ + `https://github.com/electron/electron/releases/download/v${electronVersion}/electron.d.ts`, + `https://registry.npmmirror.com/-/binary/electron/v${electronVersion}/electron.d.ts`, + `https://npmmirror.com/mirrors/electron/v${electronVersion}/electron.d.ts` + ]; + + let contents: Uint8Array | undefined; + let lastError: Error | undefined; + for (const url of urls) { + try { + contents = await download(url, { + checksumSha256: expectedChecksum, + attempts: 3, + retryDelay: 2000, + timeout: 60_000, + onRetry: error => console.warn(`Retrying ${url}: ${error.message}`) + }); + break; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + console.warn(`Mirror failed, trying next: ${url}`); + } + } + if (!contents) { + throw lastError ?? new Error('Failed to download electron.d.ts from all mirrors'); + } fs.mkdirSync(path.dirname(electronTypesPath), { recursive: true }); fs.writeFileSync(electronTypesPath, contents); diff --git a/package.json b/package.json index fdb1e83bc0fc..92a5bb31ac2b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test-node": "mocha test/unit/node/index.js --delay --ui=tdd --exit", "test-extension": "vscode-test", "test-build-scripts": "cd build && npm run test", - "test-agent-host-e2e": "node test/integration/agentHost/runner.ts", + "test-agent-host-e2e": "node te st/integration/agentHost/runner.ts", "markdown-editor-package-json-check": "npm --prefix extensions/markdown-language-features run check-markdown-editor-package-json", "test-agent-host-e2e-coverage": "node test/integration/agentHost/coverage.ts", "check-cyclic-dependencies": "node build/lib/checkCyclicDependencies.ts out",