diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index f8c08333e..12eb8cf5f 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -48,7 +48,7 @@ name: Rust release artifacts # against upstream source (present since v0.9.0) # NOT verifiable locally — needs a real tag-push CI run: # * makensis compile of scripts/package-windows.nsi (no makensis on macOS; -# NSIS is preinstalled on windows-latest per the runner-images manifest) +# CI installs NSIS on windows-latest before packaging) # * actual AppImage assembly (appimagetool is a Linux ELF; it also # downloads its type2-runtime from GitHub at pack time — needs network) # * dpkg-deb --build (no dpkg locally; only the layout path was dry-run) @@ -185,6 +185,18 @@ jobs: with: p12-file-base64: ${{ secrets.CSC_LINK }} p12-password: ${{ secrets.CSC_KEY_PASSWORD }} + - name: Resolve macOS signing identity + if: runner.os == 'macOS' && env.HAS_APPLE_CERTIFICATE == 'true' + shell: bash + run: | + set -euo pipefail + identity="$(security find-identity -v -p codesigning | awk -F'"' '/Developer ID Application/ {print $2; exit}')" + if [ -z "$identity" ]; then + security find-identity -v -p codesigning + echo "::error::Developer ID Application identity not found in imported keychain" + exit 1 + fi + echo "MACOS_SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" - name: Package DMG (macos) if: runner.os == 'macOS' shell: bash @@ -199,7 +211,8 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | if [ "$HAS_APPLE_CERTIFICATE" = "true" ]; then - export MACOS_SIGN_IDENTITY="Developer ID Application ($APPLE_TEAM_ID)" + : "${MACOS_SIGN_IDENTITY:?MACOS_SIGN_IDENTITY was not resolved}" + export MACOS_SIGN_IDENTITY fi bash scripts/bundle-macos.sh APP="target/${{ matrix.target }}/release/bundle/osx/OpenPencil.app" @@ -249,22 +262,42 @@ jobs: # The .app inside the DMG was signed by bundle-macos.sh after the # Developer ID cert import above. Sign the DMG container too, then # notarize + staple so Gatekeeper accepts the download. - IDENTITY="Developer ID Application ($APPLE_TEAM_ID)" codesign --force --timestamp --options runtime \ - --sign "$IDENTITY" "$DMG" + --sign "$MACOS_SIGN_IDENTITY" "$DMG" xcrun notarytool submit "$DMG" \ --apple-id "$APPLE_ID" \ --team-id "$APPLE_TEAM_ID" \ --password "$APPLE_APP_SPECIFIC_PASSWORD" \ --wait xcrun stapler staple "$DMG" + - name: Install NSIS (windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + choco install nsis -y --no-progress + $nsisRoots = @( + "${env:ProgramFiles(x86)}\NSIS", + "$env:ProgramFiles\NSIS" + ) + $makensis = $null + foreach ($root in $nsisRoots) { + $candidate = Join-Path $root "makensis.exe" + if (Test-Path $candidate) { + $makensis = $candidate + break + } + } + if (-not $makensis) { + throw "makensis.exe not found after NSIS install" + } + Split-Path $makensis | Out-File -FilePath $env:GITHUB_PATH -Append + & $makensis /VERSION - name: Package NSIS installer (windows) if: runner.os == 'Windows' shell: pwsh run: | - # makensis is preinstalled on windows-latest (runner-images - # manifest). Defines are absolute because NSIS resolves relative - # paths against the .nsi's own directory (scripts/). + # Defines are absolute because NSIS resolves relative paths against + # the .nsi's own directory (scripts/). makensis ` "/DVERSION=$env:OP_VERSION" ` "/DARCH=${{ matrix.arch }}" ` @@ -475,10 +508,16 @@ jobs: publish_pkg() { local name="$1" local tarball="$2" - if npm view "${name}@${version}" version >/dev/null 2>&1; then + local view_output + if view_output="$(npm view "${name}@${version}" version 2>&1)"; then echo "::notice::${name}@${version} is already published; skipping" return fi + if ! grep -Eq 'E404|404 Not Found|is not in this registry' <<<"$view_output"; then + printf '%s\n' "$view_output" + echo "::error::failed to check npm package ${name}@${version}" + exit 1 + fi npm publish "./$tarball" --access public --tag next } publish_pkg "@zseven-w/op-web-sdk" "sdk-packages/zseven-w-op-web-sdk-${version}.tgz" diff --git a/crates/op-host-web/tests/ci_workflow.rs b/crates/op-host-web/tests/ci_workflow.rs index c878853c3..1aa75be5a 100644 --- a/crates/op-host-web/tests/ci_workflow.rs +++ b/crates/op-host-web/tests/ci_workflow.rs @@ -42,6 +42,68 @@ fn release_workflow_documents_current_canvaskit_bundle_path() { ); } +#[test] +fn release_workflow_resolves_macos_signing_identity_from_keychain() { + let workflow = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../.github/workflows/rust-release.yml" + )) + .expect("rust-release workflow is readable"); + + assert!( + workflow.contains("Resolve macOS signing identity"), + "release workflow should resolve the imported Developer ID identity before signing" + ); + assert!( + workflow.contains("security find-identity -v -p codesigning"), + "release workflow should query the imported keychain identity instead of guessing it" + ); + assert!( + !workflow.contains("Developer ID Application ($APPLE_TEAM_ID)"), + "release workflow must not hard-code an incomplete Developer ID identity" + ); +} + +#[test] +fn release_workflow_installs_nsis_on_windows_runner() { + let workflow = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../.github/workflows/rust-release.yml" + )) + .expect("rust-release workflow is readable"); + + assert!( + workflow.contains("Install NSIS (windows)"), + "release workflow should install NSIS before invoking makensis" + ); + assert!( + workflow.contains("choco install nsis"), + "release workflow should not assume makensis is preinstalled on windows-latest" + ); + assert!( + workflow.contains("GITHUB_PATH") && workflow.contains("makensis.exe"), + "release workflow should add makensis.exe to PATH after installing NSIS" + ); +} + +#[test] +fn release_workflow_distinguishes_published_npm_packages_from_check_failures() { + let workflow = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../.github/workflows/rust-release.yml" + )) + .expect("rust-release workflow is readable"); + + assert!( + workflow.contains("already published; skipping"), + "release workflow should skip npm packages that are already published" + ); + assert!( + workflow.contains("E404") && workflow.contains("failed to check npm package"), + "release workflow should publish only on npm 404 and fail on other npm view errors" + ); +} + #[test] fn web_smoke_page_uses_current_canvaskit_bundle_command() { let smoke = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/smoke/step-1b.html")) diff --git a/scripts/package-windows.nsi b/scripts/package-windows.nsi index 119b4a65d..eaaafa4a3 100644 --- a/scripts/package-windows.nsi +++ b/scripts/package-windows.nsi @@ -1,9 +1,9 @@ ; OpenPencil Windows installer (NSIS). ; -; Why NSIS (not Inno Setup): makensis is preinstalled on the GitHub -; windows-latest runner image, and the TS reference pipeline +; Why NSIS (not Inno Setup): the TS reference pipeline ; (apps/desktop/electron-builder.yml) also targets `nsis`, so installer UX -; stays consistent between the Electron and Rust shells. +; stays consistent between the Electron and Rust shells. The release workflow +; installs NSIS explicitly before invoking makensis. ; ; Installs: ; openpencil-desktop.exe editor binary