Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ try {
}
```

For Windows installer shortcut verification, record the minimum matrix:

- English Windows fresh install, `Just me`, desktop shortcut checked: current user desktop shortcut exists and launches PawWork
- English Windows fresh install, `All users`, desktop shortcut checked: public desktop shortcut exists and launches PawWork
- Chinese Windows fresh install, `Just me`, desktop shortcut checked: current user desktop shortcut is `爪印.lnk` and launches PawWork
- Chinese Windows fresh install, `All users`, desktop shortcut checked: public desktop shortcut is `爪印.lnk` and launches PawWork
- unchecked install: no desktop shortcut is created and the Start Menu entry still launches PawWork
- reinstall with desktop shortcut checked: missing standard desktop shortcut is repaired
- reinstall with desktop shortcut unchecked: existing desktop shortcut state is left unchanged
- scope switch between `Just me` and `All users`: standard desktop shortcut only exists in the selected install scope
- Chinese reinstall over an older standard `PawWork.lnk`: standard desktop shortcut migrates to `爪印.lnk`
- app language change after install: desktop shortcut name is not changed
- auto-update from the previous affected version: existing desktop shortcut state is left unchanged, including the no-desktop-shortcut state

Do not close the Windows desktop shortcut issue until this real Windows installer evidence is recorded.

Keep `.zip`, `.blockmap`, and `latest*.yml` assets unless updater requirements are proven safe without them.

If verification fails, check the reported missing or malformed asset first, rerun only the affected build phase, and publish the release only after the verification helper passes.
Expand Down
13 changes: 13 additions & 0 deletions packages/desktop-electron/electron-builder-app-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ describe("electron builder app-update config", () => {
})
})

test("windows nsis installer uses PawWork shortcut customizations", () => {
const config = createConfig("prod")

expect(config.nsis).toMatchObject({
oneClick: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: false,
createStartMenuShortcut: true,
include: "resources/installer.nsh",
installerLanguages: ["en_US", "zh_CN"],
})
})

test("all channels share the versioned artifact name", () => {
expect(createConfig("dev").artifactName).toBe("pawwork-${os}-${arch}-${version}.${ext}")
expect(createConfig("beta").artifactName).toBe("pawwork-${os}-${arch}-${version}.${ext}")
Expand Down
89 changes: 89 additions & 0 deletions packages/desktop-electron/electron-builder-nsis-shortcut.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, test } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"

const script = readFileSync(join(import.meta.dir, "resources", "installer.nsh"), "utf8")

describe("windows nsis desktop shortcut customization", () => {
test("adds an assisted installer checkbox with English and Chinese labels", () => {
expect(script).toContain("AddDesktopShortcut")
expect(script).toContain("添加桌面快捷方式")
expect(script).toContain("Add desktop shortcut")
expect(script).toContain("Shortcut Options")
expect(script).toContain("快捷方式选项")
expect(script).toContain('LangString PawWorkAddDesktopShortcut 1033 "Add desktop shortcut"')
expect(script).toContain('LangString PawWorkAddDesktopShortcut 2052 "添加桌面快捷方式"')
expect(script).toContain('LangString PawWorkShortcutOptions 1033 "Shortcut Options"')
expect(script).toContain('LangString PawWorkShortcutOptions 2052 "快捷方式选项"')
expect(script).not.toContain("LANG_ENGLISH")
expect(script).not.toContain("LANG_SIMPCHINESE")
expect(script).toContain("BST_CHECKED")
})

test("uses language-aware standard shortcut names", () => {
expect(script).toContain("PawWork")
expect(script).toContain("爪印")
expect(script).toContain("爪印 Beta")
expect(script).toContain("爪印 Dev")
expect(script).toContain("$LANGUAGE")
})

test("does not mutate desktop shortcuts during auto-update", () => {
expect(script).toContain("!include FileFunc.nsh")
expect(script).toContain('"--updated"')
expect(script).not.toContain("${isUpdated}")
expect(script).not.toContain("!insertmacro skipPageIfUpdated")
expect(script).toContain("PAWWORK_SKIP_DESKTOP_SHORTCUT")
})

test("keeps custom renamed shortcuts out of scope", () => {
expect(script).toContain("PAWWORK_STANDARD_SHORTCUT")
expect(script).not.toContain("我的 AI 工具")
})

test("declares a real custom page instead of running page commands inline", () => {
expect(script).toContain("!ifndef BUILD_UNINSTALLER")
expect(script).toContain("!ifndef BUILD_UNINSTALLER\n Var AddDesktopShortcutCheckbox")
expect(script).toContain("PageEx custom")
expect(script).toContain('Caption "$(PawWorkShortcutOptions)"')
expect(script).toContain("PageCallbacks PawWorkDesktopShortcutPageCreate PawWorkDesktopShortcutPageLeave")
expect(script).toContain('Function "PawWorkDesktopShortcutPageCreate"')
expect(script).toContain('Function "PawWorkDesktopShortcutPageLeave"')
})

test("uses channel-specific shortcut names instead of hard-coded prod names", () => {
expect(script).toContain("${SHORTCUT_NAME}")
expect(script).toContain('${AndIf} "${SHORTCUT_NAME}" == "PawWork"')
expect(script).toContain('${AndIf} "${SHORTCUT_NAME}" == "PawWork Beta"')
expect(script).toContain('${AndIf} "${SHORTCUT_NAME}" == "PawWork Dev"')
expect(script).toContain('Delete "$DESKTOP\\${SHORTCUT_NAME}.lnk"')
expect(script).toContain('Delete "$DESKTOP\\爪印.lnk"')
expect(script).toContain('Delete "$DESKTOP\\爪印 Beta.lnk"')
expect(script).toContain('Delete "$DESKTOP\\爪印 Dev.lnk"')
})

test("cleans standard shortcuts across scopes only for checked installs", () => {
expect(script).toContain("PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_ALL_INSTALL_SCOPES")
expect(script).toMatch(
/\$AddDesktopShortcut == \$\{BST_CHECKED\}[\s\S]*PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_ALL_INSTALL_SCOPES[\s\S]*PAWWORK_RESTORE_INSTALL_SCOPE[\s\S]*CreateShortCut/,
)
})

test("uses an elevated public desktop cleanup when switching from all-users to just-me", () => {
expect(script).toContain("PAWWORK_REMOVE_PUBLIC_STANDARD_SHORTCUTS_ELEVATED")
expect(script).toContain('${IfNot} ${UAC_IsAdmin}')
expect(script).toContain('$installMode != "all"')
expect(script).toContain('$hasPerMachineInstallation == "1"')
expect(script).toContain('${StdUtils.ExecShellWaitEx} $0 $1 "$SYSDIR\\cmd.exe" "runas"')
})

test("owns uninstall cleanup for standard shortcuts in the selected install scope", () => {
expect(script).toContain("customUnInstall")
expect(script).toContain("PAWWORK_REMOVE_STANDARD_SHORTCUTS")
expect(script).not.toContain("PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_BOTH_SCOPES")
expect(script).toContain("SetShellVarContext current")
expect(script).toContain("SetShellVarContext all")
expect(script).toContain("PAWWORK_RESTORE_INSTALL_SCOPE")
expect(script).toMatch(/PAWWORK_RESTORE_INSTALL_SCOPE\s+!insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS/)
})
})
4 changes: 4 additions & 0 deletions packages/desktop-electron/electron-builder.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ const getBase = (): Configuration => ({
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: false,
createStartMenuShortcut: true,
include: "resources/installer.nsh",
installerLanguages: ["en_US", "zh_CN"],
installerIcon: `resources/icons/icon.ico`,
installerHeaderIcon: `resources/icons/icon.ico`,
},
Expand Down
141 changes: 141 additions & 0 deletions packages/desktop-electron/resources/installer.nsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
!include nsDialogs.nsh
!include LogicLib.nsh
!include FileFunc.nsh
!include StdUtils.nsh
!include UAC.nsh

Var PawWorkStandardShortcutName

LangString PawWorkAddDesktopShortcut 1033 "Add desktop shortcut"
LangString PawWorkAddDesktopShortcut 2052 "添加桌面快捷方式"
LangString PawWorkShortcutOptions 1033 "Shortcut Options"
LangString PawWorkShortcutOptions 2052 "快捷方式选项"

!macro PAWWORK_STANDARD_SHORTCUT
StrCpy $PawWorkStandardShortcutName "${SHORTCUT_NAME}"
${If} $LANGUAGE == 2052
${AndIf} "${SHORTCUT_NAME}" == "PawWork"
StrCpy $PawWorkStandardShortcutName "爪印"
${ElseIf} $LANGUAGE == 2052
${AndIf} "${SHORTCUT_NAME}" == "PawWork Beta"
StrCpy $PawWorkStandardShortcutName "爪印 Beta"
${ElseIf} $LANGUAGE == 2052
${AndIf} "${SHORTCUT_NAME}" == "PawWork Dev"
StrCpy $PawWorkStandardShortcutName "爪印 Dev"
${EndIf}
Comment thread
Astro-Han marked this conversation as resolved.
!macroend

!macro PAWWORK_REMOVE_STANDARD_SHORTCUTS
!insertmacro PAWWORK_STANDARD_SHORTCUT
Delete "$DESKTOP\$PawWorkStandardShortcutName.lnk"
Delete "$DESKTOP\${SHORTCUT_NAME}.lnk"
${If} "${SHORTCUT_NAME}" == "PawWork"
Delete "$DESKTOP\PawWork.lnk"
Delete "$DESKTOP\爪印.lnk"
${ElseIf} "${SHORTCUT_NAME}" == "PawWork Beta"
Delete "$DESKTOP\PawWork Beta.lnk"
Delete "$DESKTOP\爪印 Beta.lnk"
${ElseIf} "${SHORTCUT_NAME}" == "PawWork Dev"
Delete "$DESKTOP\PawWork Dev.lnk"
Delete "$DESKTOP\爪印 Dev.lnk"
${EndIf}
Comment thread
Astro-Han marked this conversation as resolved.
!macroend

!macro PAWWORK_REMOVE_PUBLIC_STANDARD_SHORTCUTS_ELEVATED
# A per-user reinstall cannot remove Public Desktop shortcuts without elevation.
${IfNot} ${UAC_IsAdmin}
${AndIf} $installMode != "all"
${AndIf} $hasPerMachineInstallation == "1"
SetShellVarContext all
${If} "${SHORTCUT_NAME}" == "PawWork"
${StdUtils.ExecShellWaitEx} $0 $1 "$SYSDIR\cmd.exe" "runas" '/C del /F /Q "$DESKTOP\PawWork.lnk" "$DESKTOP\爪印.lnk"'
${ElseIf} "${SHORTCUT_NAME}" == "PawWork Beta"
${StdUtils.ExecShellWaitEx} $0 $1 "$SYSDIR\cmd.exe" "runas" '/C del /F /Q "$DESKTOP\PawWork Beta.lnk" "$DESKTOP\爪印 Beta.lnk"'
${ElseIf} "${SHORTCUT_NAME}" == "PawWork Dev"
${StdUtils.ExecShellWaitEx} $0 $1 "$SYSDIR\cmd.exe" "runas" '/C del /F /Q "$DESKTOP\PawWork Dev.lnk" "$DESKTOP\爪印 Dev.lnk"'
${EndIf}
${EndIf}
!macroend

!macro PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_ALL_INSTALL_SCOPES
SetShellVarContext current
!insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS
SetShellVarContext all
!insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS
!insertmacro PAWWORK_REMOVE_PUBLIC_STANDARD_SHORTCUTS_ELEVATED
!macroend

!macro PAWWORK_RESTORE_INSTALL_SCOPE
${If} $installMode == "all"
SetShellVarContext all
${Else}
SetShellVarContext current
${EndIf}
!macroend

!ifndef BUILD_UNINSTALLER
Var AddDesktopShortcutCheckbox
Var AddDesktopShortcut

!macro customPageAfterChangeDir
PageEx custom
PageCallbacks PawWorkDesktopShortcutPageCreate PawWorkDesktopShortcutPageLeave
Caption "$(PawWorkShortcutOptions)"
PageExEnd
!macroend

Function "PawWorkDesktopShortcutPageCreate"
ClearErrors
${GetParameters} $0
${GetOptions} $0 "--updated" $1
${IfNot} ${Errors}
Abort
${EndIf}

nsDialogs::Create 1018
Pop $0
${If} $0 == error
Abort
${EndIf}

${NSD_CreateCheckbox} 0 0 100% 12u "$(PawWorkAddDesktopShortcut)"
Pop $AddDesktopShortcutCheckbox
${NSD_Check} $AddDesktopShortcutCheckbox
nsDialogs::Show
FunctionEnd

Function "PawWorkDesktopShortcutPageLeave"
StrCpy $AddDesktopShortcut "0"
${NSD_GetState} $AddDesktopShortcutCheckbox $AddDesktopShortcut
FunctionEnd

!macro customInstall
ClearErrors
${GetParameters} $0
${GetOptions} $0 "--updated" $1
${IfNot} ${Errors}
StrCpy $AddDesktopShortcut "PAWWORK_SKIP_DESKTOP_SHORTCUT"
${EndIf}

${If} $AddDesktopShortcut == ${BST_CHECKED}
!insertmacro PAWWORK_STANDARD_SHORTCUT
!insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS_IN_ALL_INSTALL_SCOPES
!insertmacro PAWWORK_RESTORE_INSTALL_SCOPE
CreateShortCut "$DESKTOP\$PawWorkStandardShortcutName.lnk" "$appExe" "" "$appExe" 0 "" "" "${APP_DESCRIPTION}"
ClearErrors
WinShell::SetLnkAUMI "$DESKTOP\$PawWorkStandardShortcutName.lnk" "${APP_ID}"
${EndIf}
!macroend
!endif

!ifdef BUILD_UNINSTALLER
!macro customUnInstall
ClearErrors
${GetParameters} $0
${GetOptions} $0 "--updated" $1
${If} ${Errors}
!insertmacro PAWWORK_RESTORE_INSTALL_SCOPE
!insertmacro PAWWORK_REMOVE_STANDARD_SHORTCUTS
${EndIf}
!macroend
!endif
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,21 @@ describe("release workflow app-update verification", () => {
expect(workflow).toContain('officecli_platform="win32"')
})
})

const checklist = readFileSync(join(import.meta.dir, "..", "..", "..", ".github", "RELEASE_CHECKLIST.md"), "utf8")

describe("release checklist Windows installer verification", () => {
test("records the Windows desktop shortcut verification matrix", () => {
expect(checklist).toContain("English Windows fresh install")
expect(checklist).toContain("Chinese Windows fresh install")
expect(checklist).toContain("unchecked install")
expect(checklist).toContain("reinstall with desktop shortcut checked")
expect(checklist).toContain("reinstall with desktop shortcut unchecked")
expect(checklist).toContain("scope switch between `Just me` and `All users`")
expect(checklist).toContain("older standard `PawWork.lnk`")
expect(checklist).toContain("app language change after install")
expect(checklist).toContain("previous affected version")
expect(checklist).toContain("no-desktop-shortcut state")
expect(checklist).toContain("Do not close the Windows desktop shortcut issue")
})
})
Loading