diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50bf8707..bc9aa416 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -382,6 +382,48 @@ jobs: path: bundles/* if-no-files-found: error + windows-installer: + needs: [build, resolve] + if: ${{ !cancelled() && needs.build.result == 'success' }} + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.resolve.outputs.source_ref }} + - uses: actions/download-artifact@v4 + with: + path: artifacts + pattern: 'codewhale*-windows-x64.exe' + - name: Install NSIS + shell: pwsh + run: choco install nsis -y --no-progress + - name: Build NSIS installer + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $version = "${{ needs.resolve.outputs.tag }}".TrimStart("v") + Copy-Item "artifacts\codewhale-windows-x64.exe\codewhale-windows-x64.exe" "scripts\installer\codewhale.exe" + Copy-Item "artifacts\codewhale-tui-windows-x64.exe\codewhale-tui-windows-x64.exe" "scripts\installer\codewhale-tui.exe" + $makensis = "${env:ProgramFiles(x86)}\NSIS\makensis.exe" + if (!(Test-Path $makensis)) { + $makensis = "${env:ProgramFiles}\NSIS\makensis.exe" + } + if (!(Test-Path $makensis)) { + throw "makensis.exe not found after NSIS install" + } + Push-Location scripts\installer + & $makensis "/DVERSION=$version" "codewhale.nsi" + Pop-Location + if (!(Test-Path "scripts\installer\CodeWhaleSetup.exe")) { + throw "CodeWhaleSetup.exe was not produced" + } + - name: Upload installer artifact + uses: actions/upload-artifact@v4 + with: + name: CodeWhaleSetup.exe + path: scripts/installer/CodeWhaleSetup.exe + if-no-files-found: error + docker: needs: [build, resolve] if: ${{ !cancelled() && needs.build.result == 'success' }} @@ -451,8 +493,8 @@ jobs: cache-to: type=gha,mode=max release: - needs: [build, bundle, docker, resolve] - if: ${{ !cancelled() && needs.build.result == 'success' && needs.bundle.result == 'success' && needs.docker.result == 'success' }} + needs: [build, bundle, windows-installer, docker, resolve] + if: ${{ !cancelled() && needs.build.result == 'success' && needs.bundle.result == 'success' && needs.windows-installer.result == 'success' && needs.docker.result == 'success' }} runs-on: ubuntu-latest permissions: contents: write @@ -552,6 +594,7 @@ jobs: | Linux RISC-V | `codewhale-linux-riscv64.tar.gz` | `install.sh` | | macOS x64 | `codewhale-macos-x64.tar.gz` | `install.sh` | | macOS ARM | `codewhale-macos-arm64.tar.gz` | `install.sh` | + | Windows x64 (installer) | `CodeWhaleSetup.exe` | NSIS setup | | Windows x64 | `codewhale-windows-x64.zip` | `install.bat` | | Windows x64 (portable) | `codewhale-windows-x64-portable.zip` | — | @@ -563,11 +606,12 @@ jobs: ``` **Windows:** + - For the installer path, run `CodeWhaleSetup.exe`; it installs both binaries under `%LOCALAPPDATA%\Programs\CodeWhale\bin` and adds that directory to the current-user PATH. - Extract `codewhale-windows-x64.zip` - Run `install.bat` (copies to `%USERPROFILE%\bin`) - Add `%USERPROFILE%\bin` to your PATH - The **portable** Windows archive skips the install script — extract and run from any directory. + The **portable** Windows archive skips the install script — extract and run from any directory. The NSIS installer is currently unsigned and may trigger Windows SmartScreen until a signing certificate is wired into the release pipeline. Individual binaries are also attached below for scripting and the npm wrapper. Legacy `deepseek-*` and `deepseek-tui-*` assets are compatibility-only deprecation shims for v0.8.x so that existing `deepseek update` invocations on v0.8.40 keep working; they forward to the canonical binaries. The legacy npm package `deepseek-tui` is deprecated and is not republished. diff --git a/docs/CLASSROOM_INSTALL.md b/docs/CLASSROOM_INSTALL.md index 07a7d14e..319dd99b 100644 --- a/docs/CLASSROOM_INSTALL.md +++ b/docs/CLASSROOM_INSTALL.md @@ -15,8 +15,9 @@ machines running Windows. | 1 | Confirm Windows version: `winver` → 10 build 17763+ or 11 | ☐ | | 2 | Ensure the user account is a **standard user** (not a local admin). The installer does not require elevation. | ☐ | | 3 | Verify outbound HTTPS (port 443) is open to `api.openai.com` (or whichever LLM provider the course uses). | ☐ | -| 4 | Obtain the installer: download `CodeWhaleSetup.exe` from the [latest release](https://github.com/Hmbown/CodeWhale/releases/latest) or from your department mirror. | ☐ | -| 5 | (Optional) Verify SHA-256 hash matches the published manifest. | ☐ | +| 4 | Obtain the installer: download `CodeWhaleSetup.exe` from a v0.8.50+ [release](https://github.com/Hmbown/CodeWhale/releases/latest) or from your department mirror. | ☐ | +| 5 | Verify SHA-256 hash against `codewhale-artifacts-sha256.txt` before deploying. | ☐ | +| 6 | Note that the public installer is currently unsigned and may trigger Windows SmartScreen unless your organization signs it before deployment. | ☐ | --- @@ -25,7 +26,7 @@ machines running Windows. ### Option A — Silent install (recommended for imaging / SCCM / Intune) ```powershell -# Run as admin or via deployment tool +# Run as the target user or via a per-user deployment tool CodeWhaleSetup.exe /S ``` @@ -52,13 +53,15 @@ New-Item -ItemType Directory -Force -Path $binDir # 2. Download binaries (adjust URL to your mirror or release tag) $tag = (Invoke-RestMethod -Uri "https://api.github.com/repos/Hmbown/CodeWhale/releases/latest").tag_name -Invoke-WebRequest -Uri "https://github.com/Hmbown/CodeWhale/releases/download/$tag/codewhale-x64.exe" -OutFile "$binDir\codewhale.exe" -Invoke-WebRequest -Uri "https://github.com/Hmbown/CodeWhale/releases/download/$tag/codewhale-tui-x64.exe" -OutFile "$binDir\codewhale-tui.exe" +Invoke-WebRequest -Uri "https://github.com/Hmbown/CodeWhale/releases/download/$tag/codewhale-windows-x64.exe" -OutFile "$binDir\codewhale.exe" +Invoke-WebRequest -Uri "https://github.com/Hmbown/CodeWhale/releases/download/$tag/codewhale-tui-windows-x64.exe" -OutFile "$binDir\codewhale-tui.exe" # 3. Add to user PATH (persistent) $currentPath = [Environment]::GetEnvironmentVariable("Path", "User") -if ($currentPath -notlike "*$binDir*") { - [Environment]::SetEnvironmentVariable("Path", "$currentPath;$binDir", "User") +$pathParts = @($currentPath -split ";" | Where-Object { $_ }) +if ($pathParts -notcontains $binDir) { + $newPath = (@($pathParts) + $binDir) -join ";" + [Environment]::SetEnvironmentVariable("Path", $newPath, "User") } # 4. Refresh current session PATH @@ -79,6 +82,19 @@ Run these on **each machine** (or spot-check a sample): If `codewhale` is not found, the user may need to open a **new** terminal window for PATH changes to take effect. +## Lab validation checklist + +Run this once on a clean lab machine, and again on a machine that already has a +previous CodeWhale install: + +| # | Scenario | Expected result | Done? | +|---|----------|-----------------|-------| +| 1 | Install with no existing CodeWhale PATH entry | Adds exactly `%LOCALAPPDATA%\Programs\CodeWhale\bin` | ☐ | +| 2 | Install twice | PATH is not duplicated | ☐ | +| 3 | Install with a neighboring PATH entry such as `C:\Tools\CodeWhale\bin-extra` | Neighboring entry is preserved | ☐ | +| 4 | Upgrade by installing a newer `CodeWhaleSetup.exe` over an older one | Apps & Features version and both `--version` outputs match the new build | ☐ | +| 5 | Silent uninstall with `Uninstall.exe /S` | Files, uninstall registry entry, and only the exact installer PATH entry are removed | ☐ | + --- ## API key provisioning @@ -129,7 +145,7 @@ Remove-Item -Recurse -Force (Split-Path $binDir) # Remove from PATH $currentPath = [Environment]::GetEnvironmentVariable("Path", "User") -$newPath = ($currentPath -split ";" | Where-Object { $_ -ne $binDir }) -join ";" +$newPath = ($currentPath -split ";" | Where-Object { $_ -and ($_ -ne $binDir) }) -join ";" [Environment]::SetEnvironmentVariable("Path", $newPath, "User") ``` @@ -175,4 +191,4 @@ If building a golden image (WIM/FFU): --- -*Last updated: 2026-05-25* +*Last updated: 2026-06-02* diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 30ab955f..6afab754 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -301,8 +301,9 @@ when you need the newest version immediately. ### Windows NSIS Installer -A standalone NSIS-based installer is available for Windows users who prefer a -traditional double-click setup (no npm, no Scoop, no Cargo required). +A standalone NSIS-based installer is available starting with v0.8.50 for +Windows users who prefer a traditional double-click setup (no npm, no Scoop, no +Cargo required). **Download** `CodeWhaleSetup.exe` from the [Releases page](https://github.com/Hmbown/CodeWhale/releases/latest). @@ -320,12 +321,21 @@ traditional double-click setup (no npm, no Scoop, no Cargo required). CodeWhaleSetup.exe /S ``` +The installer is per-user and does not request elevation. Run silent installs in +the target user's context, or use a deployment tool that can run the installer +for each user profile that needs CodeWhale. + +The release-built installer is currently unsigned and may trigger Windows +SmartScreen. Verify the SHA-256 checksum from `codewhale-artifacts-sha256.txt` +before deploying, and sign the installer in your internal deployment pipeline if +your environment requires signed application packages. + **Build the installer yourself** (requires [NSIS](https://nsis.sourceforge.io)): ```powershell cd scripts\installer # Place codewhale.exe and codewhale-tui.exe here, then: -makensis /DVERSION=0.9.0 codewhale.nsi +makensis /DVERSION= codewhale.nsi ``` **Manual fallback** — if the installer is blocked by group policy, see the diff --git a/scripts/installer/codewhale.nsi b/scripts/installer/codewhale.nsi index 29e94f09..47cd025e 100644 --- a/scripts/installer/codewhale.nsi +++ b/scripts/installer/codewhale.nsi @@ -11,11 +11,8 @@ ; codewhale.exe ; codewhale-tui.exe ; 2. Build: -; makensis codewhale.nsi +; makensis /DVERSION=1.2.3 codewhale.nsi ; 3. Output: CodeWhaleSetup.exe (in current directory) -; -; You can override version at build time: -; makensis /DVERSION=1.2.3 codewhale.nsi ;-------------------------------- ; Includes @@ -83,11 +80,12 @@ Section "Install" SecInstall WriteUninstaller "$INSTDIR\Uninstall.exe" ; Add to current-user PATH - ; Read existing PATH, append if not already present + ; Read existing PATH, append only when the exact entry is absent. ReadRegStr $0 HKCU "Environment" "Path" - ${StrStr} $1 $0 "$INSTDIR\bin" + StrCpy $2 ";$0;" + StrCpy $3 ";$INSTDIR\bin;" + ${StrStr} $1 $2 $3 StrCmp $1 "" 0 path_already_set - ; Not found — append StrCmp $0 "" empty_path WriteRegExpandStr HKCU "Environment" "Path" "$0;$INSTDIR\bin" Goto path_done @@ -128,9 +126,10 @@ Section "Uninstall" ; Remove from current-user PATH ReadRegStr $0 HKCU "Environment" "Path" - ${un.StrStr} $1 $0 "$INSTDIR\bin" + StrCpy $2 ";$0;" + StrCpy $3 ";$INSTDIR\bin;" + ${UnStrStr} $1 $2 $3 StrCmp $1 "" path_clean_done - ; Remove the entry Push "$0" Push "$INSTDIR\bin" Call un.RemoveFromPath @@ -145,7 +144,7 @@ Section "Uninstall" SectionEnd ;-------------------------------- -; Helper: Remove a directory from PATH (uninstaller version) +; Helper: Remove exact directory entries from PATH (uninstaller version) ; Input: PATH string (on stack), directory to remove (on stack) ; Output: cleaned PATH (on stack) ;-------------------------------- @@ -153,48 +152,80 @@ Function un.RemoveFromPath Exch $R0 ; directory to remove Exch Exch $R1 ; original PATH - Push $R2 ; prefix - Push $R3 ; suffix + Push $R2 ; padded path + Push $R3 ; padded needle Push $R4 ; match result + Push $R5 ; prefix + Push $R6 ; suffix + Push $R7 ; offset/length - ${un.StrStr} $R4 $R1 $R0 - StrCmp $R4 "" done + loop: + StrCmp $R1 "" done + StrCpy $R2 ";$R1;" + StrCpy $R3 ";$R0;" + ${UnStrStr} $R4 $R2 $R3 + StrCmp $R4 "" done - ; Calculate prefix - StrLen $R2 $R1 - StrLen $R3 $R4 - IntOp $R3 $R2 - $R3 ; Match offset - StrCpy $R2 $R1 $R3 ; Prefix string + ; Prefix before the exact `;dir;` match in the padded PATH. + StrLen $R5 $R2 + StrLen $R6 $R4 + IntOp $R6 $R5 - $R6 + StrCpy $R5 $R2 $R6 - ; Calculate suffix - StrLen $R4 $R0 - IntOp $R4 $R3 + $R4 ; Suffix offset = Match offset + Dir length - StrCpy $R3 $R1 "" $R4 ; Suffix string + ; Suffix after the exact `;dir;` match in the padded PATH. + StrLen $R7 $R3 + IntOp $R7 $R6 + $R7 + StrCpy $R6 $R2 "" $R7 - ; Clean up semicolons - StrCpy $R4 $R3 1 - StrCmp $R4 ";" 0 +2 - StrCpy $R3 $R3 "" 1 ; Strip leading semicolon from suffix + Push $R5 + Call un.TrimPathEdgeSemicolons + Pop $R5 + Push $R6 + Call un.TrimPathEdgeSemicolons + Pop $R6 - StrLen $R4 $R2 - IntOp $R4 $R4 - 1 - StrCpy $R0 $R2 1 $R4 - StrCmp $R0 ";" 0 +2 - StrCpy $R2 $R2 $R4 ; Strip trailing semicolon from prefix - - ; Concatenate - StrCmp $R2 "" 0 +3 - StrCpy $R1 $R3 - Goto done - StrCmp $R3 "" 0 +3 - StrCpy $R1 $R2 - Goto done - StrCpy $R1 "$R2;$R3" + StrCmp $R5 "" 0 +3 + StrCpy $R1 $R6 + Goto loop + StrCmp $R6 "" 0 +3 + StrCpy $R1 $R5 + Goto loop + StrCpy $R1 "$R5;$R6" + Goto loop done: + Pop $R7 + Pop $R6 + Pop $R5 Pop $R4 Pop $R3 Pop $R2 Pop $R0 Exch $R1 FunctionEnd + +Function un.TrimPathEdgeSemicolons + Exch $R9 + Push $R8 + + trim_leading: + StrCpy $R8 $R9 1 + StrCmp $R8 ";" 0 trim_trailing + StrCpy $R9 $R9 "" 1 + Goto trim_leading + + trim_trailing: + StrLen $R8 $R9 + IntCmp $R8 0 trim_done + IntOp $R8 $R8 - 1 + StrCpy $R8 $R9 1 $R8 + StrCmp $R8 ";" 0 trim_done + StrLen $R8 $R9 + IntOp $R8 $R8 - 1 + StrCpy $R9 $R9 $R8 + Goto trim_trailing + + trim_done: + Pop $R8 + Exch $R9 +FunctionEnd