mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix(release): prepare unsigned macOS v0.4.0 install path
Bump the Electron desktop package version to 0.4.0, publish a macOS unsigned install helper, and let the release workflow continue when Developer ID signing is not configured. Tested: bash -n desktop/scripts/install-macos-unsigned.sh Tested: bun test scripts/pr/release-workflow.test.ts Tested: bun run scripts/release.ts 0.4.0 --dry Tested: git diff --check Tested: bun run check:docs Tested: bun run check:policy Confidence: high Scope-risk: moderate
This commit is contained in:
parent
5cf4480d22
commit
9e4893ac36
3
.github/workflows/build-desktop-dev.yml
vendored
3
.github/workflows/build-desktop-dev.yml
vendored
@ -126,6 +126,9 @@ jobs:
|
||||
| while read -r artifact; do
|
||||
cp -f "$artifact" "$STAGING/"
|
||||
done
|
||||
if [ "${{ matrix.smoke_platform }}" = "macos" ]; then
|
||||
cp -f desktop/scripts/install-macos-unsigned.sh "$STAGING/"
|
||||
fi
|
||||
echo "staging_dir=$STAGING" >> "$GITHUB_OUTPUT"
|
||||
ls -lh "$STAGING/" 2>/dev/null || true
|
||||
|
||||
|
||||
22
.github/workflows/release-desktop.yml
vendored
22
.github/workflows/release-desktop.yml
vendored
@ -51,8 +51,11 @@ jobs:
|
||||
|
||||
signing-preflight:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
macos_signed: ${{ steps.validate.outputs.macos_signed }}
|
||||
steps:
|
||||
- name: Validate release signing and notarization secrets
|
||||
id: validate
|
||||
shell: bash
|
||||
env:
|
||||
CSC_LINK: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
@ -63,8 +66,9 @@ jobs:
|
||||
WIN_CSC_LINK: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
# macOS signing + notarization is mandatory: Squirrel.Mac auto-update and
|
||||
# first-launch Gatekeeper approval both require a notarized Developer ID build.
|
||||
# macOS signing + notarization is preferred: Squirrel.Mac auto-update and
|
||||
# first-launch Gatekeeper approval work best with a notarized Developer ID build.
|
||||
# A migration release may still ship unsigned while Apple Developer ID setup is pending.
|
||||
missing=()
|
||||
[ -n "$CSC_LINK" ] || missing+=("MACOS_CERTIFICATE")
|
||||
[ -n "$CSC_KEY_PASSWORD" ] || missing+=("MACOS_CERTIFICATE_PASSWORD")
|
||||
@ -72,8 +76,10 @@ jobs:
|
||||
[ -n "$APPLE_APP_SPECIFIC_PASSWORD" ] || missing+=("APPLE_APP_SPECIFIC_PASSWORD")
|
||||
[ -n "$APPLE_TEAM_ID" ] || missing+=("APPLE_TEAM_ID")
|
||||
if [ "${#missing[@]}" -gt 0 ]; then
|
||||
printf '::error::Missing required macOS signing/notarization secrets: %s\n' "${missing[*]}"
|
||||
exit 1
|
||||
printf '::warning::Missing macOS signing/notarization secrets (%s): macOS artifacts will be unsigned and users must use install-macos-unsigned.sh.\n' "${missing[*]}"
|
||||
echo "macos_signed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "macos_signed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
# Windows signing is optional: an unsigned NSIS installer still auto-updates
|
||||
# (electron-updater), it only triggers SmartScreen warnings. Warn, do not block,
|
||||
@ -187,6 +193,7 @@ jobs:
|
||||
working-directory: desktop
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
|
||||
CSC_LINK: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
@ -200,9 +207,13 @@ jobs:
|
||||
run: bun run test:package-smoke --platform ${{ matrix.smoke_platform }} --package-kind release --artifacts-dir desktop/build-artifacts/electron
|
||||
|
||||
- name: Verify macOS launch policy
|
||||
if: matrix.smoke_platform == 'macos'
|
||||
if: matrix.smoke_platform == 'macos' && needs.signing-preflight.outputs.macos_signed == 'true'
|
||||
run: bun run test:package-smoke --platform macos --package-kind release --artifacts-dir desktop/build-artifacts/electron --require-macos-gatekeeper
|
||||
|
||||
- name: Warn unsigned macOS launch policy skipped
|
||||
if: matrix.smoke_platform == 'macos' && needs.signing-preflight.outputs.macos_signed != 'true'
|
||||
run: echo "::warning::Skipping macOS Gatekeeper package-smoke because this release is unsigned. Ship install-macos-unsigned.sh with the DMG."
|
||||
|
||||
- name: Validate matrix release asset set
|
||||
shell: bash
|
||||
working-directory: desktop/build-artifacts/electron
|
||||
@ -426,3 +437,4 @@ jobs:
|
||||
artifacts/release-assets/**/*.deb
|
||||
artifacts/release-assets/**/*.blockmap
|
||||
artifacts/update-metadata-standard/*.yml
|
||||
desktop/scripts/install-macos-unsigned.sh
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "claude-code-desktop",
|
||||
"private": true,
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"description": "Desktop coding agent workbench for Claude Code Haha.",
|
||||
"homepage": "https://github.com/NanmiCoder/cc-haha",
|
||||
"author": {
|
||||
|
||||
103
desktop/scripts/install-macos-unsigned.sh
Executable file
103
desktop/scripts/install-macos-unsigned.sh
Executable file
@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_NAME="Claude Code Haha.app"
|
||||
APP_PATH="/Applications/${APP_NAME}"
|
||||
|
||||
script_dir() {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1
|
||||
pwd
|
||||
}
|
||||
|
||||
find_dmg() {
|
||||
local dir="$1"
|
||||
local arch
|
||||
arch="$(uname -m)"
|
||||
|
||||
shopt -s nullglob
|
||||
local matches=("${dir}"/Claude-Code-Haha-*-mac-*.dmg)
|
||||
shopt -u nullglob
|
||||
|
||||
if [ "${#matches[@]}" -eq 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "${#matches[@]}" -eq 1 ]; then
|
||||
printf '%s\n' "${matches[0]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local wanted="mac-x64"
|
||||
if [ "$arch" = "arm64" ]; then
|
||||
wanted="mac-arm64"
|
||||
fi
|
||||
|
||||
local dmg
|
||||
for dmg in "${matches[@]}"; do
|
||||
if [[ "$(basename "$dmg")" == *"${wanted}.dmg" ]]; then
|
||||
printf '%s\n' "$dmg"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s\n' "${matches[0]}"
|
||||
}
|
||||
|
||||
main() {
|
||||
local base_dir
|
||||
base_dir="$(script_dir)"
|
||||
|
||||
local dmg="${1:-}"
|
||||
if [ -z "$dmg" ]; then
|
||||
if ! dmg="$(find_dmg "$base_dir")"; then
|
||||
echo "No Claude Code Haha macOS DMG found next to this script."
|
||||
echo "Usage: bash install-macos-unsigned.sh /path/to/Claude-Code-Haha-0.4.0-mac-arm64.dmg"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "$dmg" ]; then
|
||||
echo "DMG not found: $dmg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using DMG: $dmg"
|
||||
xattr -dr com.apple.quarantine "$dmg" 2>/dev/null || true
|
||||
|
||||
local mount_output volume app_in_volume
|
||||
mount_output="$(hdiutil attach "$dmg" -nobrowse -readonly)"
|
||||
volume="$(printf '%s\n' "$mount_output" | sed -n 's#^.*\(/Volumes/.*\)$#\1#p' | head -n 1)"
|
||||
if [ -z "$volume" ] || [ ! -d "$volume" ]; then
|
||||
echo "Could not find mounted DMG volume."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
hdiutil detach "$volume" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
app_in_volume="${volume}/${APP_NAME}"
|
||||
if [ ! -d "$app_in_volume" ]; then
|
||||
echo "Could not find ${APP_NAME} in mounted DMG: $volume"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
osascript -e 'quit app "Claude Code Haha"' >/dev/null 2>&1 || true
|
||||
|
||||
if [ -d "$APP_PATH" ]; then
|
||||
local backup
|
||||
backup="${HOME}/.Trash/Claude Code Haha.$(date +%Y%m%d%H%M%S).app"
|
||||
echo "Moving existing app to: $backup"
|
||||
mv "$APP_PATH" "$backup"
|
||||
fi
|
||||
|
||||
echo "Installing to: $APP_PATH"
|
||||
ditto "$app_in_volume" "$APP_PATH"
|
||||
xattr -dr com.apple.quarantine "$APP_PATH" 2>/dev/null || true
|
||||
|
||||
echo "Opening Claude Code Haha..."
|
||||
open "$APP_PATH"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -40,55 +40,6 @@
|
||||
| Linux x64 | `Claude-Code-Haha-0.4.0-linux-x86_64.AppImage` 或 `Claude-Code-Haha-0.4.0-linux-amd64.deb` |
|
||||
| Linux ARM64 | `Claude-Code-Haha-0.4.0-linux-arm64.AppImage` 或 `Claude-Code-Haha-0.4.0-linux-arm64.deb` |
|
||||
|
||||
### macOS
|
||||
|
||||
正式签名并公证的 DMG:
|
||||
|
||||
1. 下载与你 Mac 架构匹配的 DMG。
|
||||
2. 打开 DMG,把 `Claude Code Haha.app` 拖到 `Applications`。
|
||||
3. 从 `Applications` 打开应用。
|
||||
|
||||
如果本次 0.4.0 因 Apple Developer ID 还未准备好,只能先发布未签名构建,macOS 可能提示 DMG "已损坏"或"无法验证开发者"。这时不要直接双击反复尝试,按下面命令安装。
|
||||
|
||||
Apple Silicon 示例:
|
||||
|
||||
```bash
|
||||
ARTIFACT_DIR="$HOME/Downloads/Claude-Code-Haha-v0.4.0-macos-arm64"
|
||||
DMG="$ARTIFACT_DIR/Claude-Code-Haha-0.4.0-mac-arm64.dmg"
|
||||
APP="/Applications/Claude Code Haha.app"
|
||||
|
||||
xattr -dr com.apple.quarantine "$ARTIFACT_DIR" 2>/dev/null || true
|
||||
xattr -dr com.apple.quarantine "$DMG" 2>/dev/null || true
|
||||
|
||||
MOUNT_OUTPUT="$(hdiutil attach "$DMG" -nobrowse -readonly)"
|
||||
VOLUME="$(printf '%s\n' "$MOUNT_OUTPUT" | sed -n 's#^.*\(/Volumes/.*\)$#\1#p' | head -n 1)"
|
||||
if [ -z "$VOLUME" ]; then
|
||||
echo "未找到 DMG 挂载目录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
osascript -e 'quit app "Claude Code Haha"' >/dev/null 2>&1 || true
|
||||
|
||||
if [ -d "$APP" ]; then
|
||||
BACKUP="$HOME/.Trash/Claude Code Haha.$(date +%Y%m%d%H%M%S).app"
|
||||
mv "$APP" "$BACKUP"
|
||||
fi
|
||||
|
||||
ditto "$VOLUME/Claude Code Haha.app" "$APP"
|
||||
hdiutil detach "$VOLUME"
|
||||
xattr -dr com.apple.quarantine "$APP" 2>/dev/null || true
|
||||
open "$APP"
|
||||
```
|
||||
|
||||
如果你是直接从 GitHub Release 下载 DMG,而不是从 GitHub Actions 的 artifact 外层 ZIP 解压出来,把前两行改成:
|
||||
|
||||
```bash
|
||||
ARTIFACT_DIR="$HOME/Downloads"
|
||||
DMG="$ARTIFACT_DIR/Claude-Code-Haha-0.4.0-mac-arm64.dmg"
|
||||
```
|
||||
|
||||
Intel Mac 把 `macos-arm64` / `mac-arm64` 替换为 `macos-x64` / `mac-x64`。
|
||||
|
||||
### Windows
|
||||
|
||||
下载 `Claude-Code-Haha-0.4.0-win-x64.exe` 后双击安装。如果 Windows SmartScreen 提示未知发布者,选择"更多信息"再选择"仍要运行"。
|
||||
@ -110,20 +61,21 @@ sudo apt install ./Claude-Code-Haha-0.4.0-linux-amd64.deb
|
||||
|
||||
ARM64 Linux 对应把文件名替换为 `Claude-Code-Haha-0.4.0-linux-arm64.AppImage` 或 `Claude-Code-Haha-0.4.0-linux-arm64.deb`。
|
||||
|
||||
### macOS
|
||||
|
||||
正式签名并公证的 DMG:下载与你 Mac 架构匹配的 DMG,打开后把 `Claude Code Haha.app` 拖到 `Applications`,再从 `Applications` 打开。
|
||||
|
||||
如果本次 0.4.0 因 Apple Developer ID 还未准备好,只能先发布未签名构建,macOS 可能提示 DMG "已损坏"或"无法验证开发者"。这种情况下请下载同一个 Release 里的 `install-macos-unsigned.sh`,和 DMG 放在同一目录后运行文末命令。M 系列和 Intel 都用同一个脚本;脚本会自动选择同目录里匹配当前机器架构的 DMG,也可以手动传入 DMG 路径。
|
||||
|
||||
## 发布者备注
|
||||
|
||||
- GitHub Release 正文继续以 `release-notes/v0.4.0.md` 作为来源。
|
||||
- 发布前请确认 `desktop/package.json` 版本、tag `v0.4.0` 和本文件名完全一致。
|
||||
- 正式 release workflow 目前要求 macOS Developer ID 签名和 notarization secrets;如果缺少 Apple ID / team / app-specific password,正式 signed release 会被 preflight 阻止。
|
||||
- 如果需要在签名就绪前先给用户一个临时迁移包,请在 GitHub Release 文案中明确这是 unsigned build,并保留上面的 macOS 命令安装步骤。
|
||||
- 如果 macOS Developer ID / notarization secrets 已配置,release workflow 会额外跑 Gatekeeper 验证;如果暂时没有 Apple ID,本版本会以 unsigned macOS 构建发布,并依赖 `install-macos-unsigned.sh` 完成首次安装。
|
||||
- 不要只上传 DMG 后删除 ZIP、blockmap 或 `latest*.yml`。DMG 是手动安装入口,ZIP 和 metadata 是 Electron auto-update 链路需要的资产。
|
||||
|
||||
发布前建议先运行:
|
||||
macOS 未签名构建安装命令:
|
||||
|
||||
```bash
|
||||
bun run scripts/release.ts 0.4.0 --dry
|
||||
bun run verify
|
||||
bun run quality:gate --mode release --allow-live --provider-model <provider:model[:label]>
|
||||
bash ~/Downloads/install-macos-unsigned.sh
|
||||
```
|
||||
|
||||
如果 live provider、Apple Developer ID 或平台实机不可用,请在 release 记录里把 blocker 写清楚。
|
||||
|
||||
@ -57,6 +57,8 @@ describe('release desktop workflow', () => {
|
||||
expect(collectStep).toContain('*.zip')
|
||||
expect(collectStep).toContain('*.blockmap')
|
||||
expect(collectStep).toContain('*.yml')
|
||||
expect(collectStep).toContain('install-macos-unsigned.sh')
|
||||
expect(collectStep).toContain('[ "${{ matrix.smoke_platform }}" = "macos" ]')
|
||||
expect(collectStep).not.toContain('-type d -name "*.app"')
|
||||
})
|
||||
|
||||
@ -82,18 +84,23 @@ describe('release desktop workflow', () => {
|
||||
expect(desktopPackage.build?.linux?.maintainer).toBe('NanmiCoder <relakkes@gmail.com>')
|
||||
})
|
||||
|
||||
test('release workflow requires macOS Gatekeeper launch approval before upload', () => {
|
||||
test('release workflow requires macOS Gatekeeper launch approval for signed builds', () => {
|
||||
const workflow = readReleaseWorkflow()
|
||||
const gatekeeperStep = workflow.match(
|
||||
/- name: Verify macOS launch policy[\s\S]*?(?:\n\s{6}- name:|$)/,
|
||||
)?.[0]
|
||||
const unsignedWarningStep = workflow.match(
|
||||
/- name: Warn unsigned macOS launch policy skipped[\s\S]*?(?:\n\s{6}- name:|$)/,
|
||||
)?.[0]
|
||||
|
||||
expect(gatekeeperStep).toContain("if: matrix.smoke_platform == 'macos'")
|
||||
expect(gatekeeperStep).toContain("if: matrix.smoke_platform == 'macos' && needs.signing-preflight.outputs.macos_signed == 'true'")
|
||||
expect(gatekeeperStep).toContain('bun run test:package-smoke --platform macos --package-kind release --artifacts-dir desktop/build-artifacts/electron --require-macos-gatekeeper')
|
||||
expect(unsignedWarningStep).toContain("if: matrix.smoke_platform == 'macos' && needs.signing-preflight.outputs.macos_signed != 'true'")
|
||||
expect(unsignedWarningStep).toContain('install-macos-unsigned.sh')
|
||||
expect(workflow.indexOf('Verify macOS launch policy')).toBeLessThan(workflow.indexOf('Upload release artifacts for final publish'))
|
||||
})
|
||||
|
||||
test('release workflow blocks on macOS signing/notarization secrets and warns for Windows signing', () => {
|
||||
test('release workflow records macOS signing state and warns for unsigned builds', () => {
|
||||
const workflow = readReleaseWorkflow()
|
||||
const signingJob = workflow.match(
|
||||
/signing-preflight:[\s\S]*?(?:\n {2}[a-zA-Z0-9_-]+:|$)/,
|
||||
@ -101,6 +108,8 @@ describe('release desktop workflow', () => {
|
||||
const buildJob = extractJob(workflow, 'build')
|
||||
|
||||
expect(signingJob).toContain('Validate release signing and notarization secrets')
|
||||
expect(signingJob).toContain('outputs:')
|
||||
expect(signingJob).toContain('macos_signed: ${{ steps.validate.outputs.macos_signed }}')
|
||||
for (const secret of [
|
||||
'MACOS_CERTIFICATE',
|
||||
'MACOS_CERTIFICATE_PASSWORD',
|
||||
@ -116,7 +125,11 @@ describe('release desktop workflow', () => {
|
||||
]) {
|
||||
expect(signingJob).toContain(secret)
|
||||
}
|
||||
expect(signingJob).toContain('Missing required macOS signing/notarization secrets')
|
||||
expect(signingJob).toContain('Missing macOS signing/notarization secrets')
|
||||
expect(signingJob).toContain('macOS artifacts will be unsigned')
|
||||
expect(signingJob).toContain('install-macos-unsigned.sh')
|
||||
expect(signingJob).toContain('macos_signed=false')
|
||||
expect(signingJob).toContain('macos_signed=true')
|
||||
expect(signingJob).toContain('Windows signing secrets missing')
|
||||
expect(signingJob).toContain('::warning::Windows signing secrets missing')
|
||||
|
||||
@ -126,7 +139,7 @@ describe('release desktop workflow', () => {
|
||||
const windowsOptionalBlock = signingJob?.match(
|
||||
/win_missing=\(\)[\s\S]*?fi\n/,
|
||||
)?.[0]
|
||||
expect(macRequiredBlock).toContain('exit 1')
|
||||
expect(macRequiredBlock).not.toContain('exit 1')
|
||||
expect(windowsOptionalBlock).toContain('::warning::')
|
||||
expect(windowsOptionalBlock).not.toContain('exit 1')
|
||||
expect(buildJob).toContain('- quality-preflight')
|
||||
@ -179,6 +192,7 @@ describe('release desktop workflow', () => {
|
||||
expect(publishJob).toContain('artifacts/release-assets/**/*.exe')
|
||||
expect(publishJob).toContain('artifacts/release-assets/**/*.AppImage')
|
||||
expect(publishJob).toContain('artifacts/update-metadata-standard/*.yml')
|
||||
expect(publishJob).toContain('desktop/scripts/install-macos-unsigned.sh')
|
||||
expect(publishJob).toContain('fail_on_unmatched_files: true')
|
||||
expect(publishJob).toContain('Load release notes')
|
||||
expect(workflow.indexOf('publish-release:')).toBeGreaterThan(workflow.indexOf('build:'))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user