@@ -9,11 +9,25 @@ set -euo pipefail
99#
1010# Usage: ./scripts/sign-and-deploy.sh all <version>
1111# Example: ./scripts/sign-and-deploy.sh all 0.1.0
12+ #
13+ # INVARIANT: Downloaded artifacts in $DOWNLOAD_DIR are NEVER modified.
14+ # All signing/patching operates on copies in $SIGN_DIR.
15+ # This allows re-running any signing step without re-downloading.
1216# =============================================================================
1317
1418SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1519REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
1620WORK_DIR="$REPO_ROOT/release-signed"
21+ DOWNLOAD_DIR="$WORK_DIR/downloads"
22+ SIGN_DIR="$WORK_DIR/work"
23+
24+ # Known artifact names (must match release.yml matrix artifact-name values)
25+ ARTIFACT_NAMES=(
26+ standalone-mac-aarch64
27+ standalone-win-x64
28+ standalone-linux-x64
29+ vscode-extension
30+ )
1731
1832# =============================================================================
1933# Configuration
@@ -91,16 +105,23 @@ check_command() {
91105 command -v "$1" &>/dev/null || error "Required command not found: $1. Install with: $2"
92106}
93107
94- artifacts_cached() {
95- local version="$1"
96- [[ -f "$WORK_DIR/.version" ]] && [[ "$(cat "$WORK_DIR/.version")" == "$version" ]]
108+ # Returns 0 if a specific artifact has already been downloaded
109+ artifact_downloaded() {
110+ local name="$1"
111+ [[ -f "$DOWNLOAD_DIR/.downloaded-$name" ]]
112+ }
113+
114+ # Returns 0 if ALL known artifacts have been downloaded
115+ all_artifacts_downloaded() {
116+ for name in "${ARTIFACT_NAMES[@]}"; do
117+ artifact_downloaded "$name" || return 1
118+ done
119+ return 0
97120}
98121
99122check_git_clean() {
100123 log "Checking git status..."
101124
102- rm -rf "$WORK_DIR"
103-
104125 if ! git -C "$REPO_ROOT" diff --quiet || ! git -C "$REPO_ROOT" diff --cached --quiet; then
105126 error "Local changes detected. Commit or stash changes before deploying."
106127 fi
@@ -125,8 +146,22 @@ check_git_clean() {
125146 log "Git status clean."
126147}
127148
149+ # Copies downloaded artifacts to $SIGN_DIR for mutation.
150+ # Call this before any signing step to get a fresh working copy.
151+ prepare_sign_dir() {
152+ log "Preparing working copies from downloaded artifacts..."
153+ rm -rf "$SIGN_DIR"
154+ mkdir -p "$SIGN_DIR"
155+ # Copy only the artifact directories (not marker files)
156+ for name in "${ARTIFACT_NAMES[@]}"; do
157+ if [[ -d "$DOWNLOAD_DIR/$name" ]]; then
158+ cp -R "$DOWNLOAD_DIR/$name" "$SIGN_DIR/$name"
159+ fi
160+ done
161+ }
162+
128163find_nsis_script() {
129- find "$WORK_DIR /standalone-win-x64" \
164+ find "$SIGN_DIR /standalone-win-x64" \
130165 -name "installer.nsi" \
131166 -print \
132167 | head -1
@@ -148,12 +183,12 @@ rebuild_windows_installer() {
148183 # The .nsi contains ~60 absolute Windows paths from the CI runner.
149184 # Replace them all with local artifact paths using the helper script.
150185 local artifact_dir
151- artifact_dir="$(cd "$WORK_DIR /standalone-win-x64" && pwd)"
186+ artifact_dir="$(cd "$SIGN_DIR /standalone-win-x64" && pwd)"
152187 perl "$SCRIPT_DIR/patch-nsis-paths.pl" "$script_path" "$artifact_dir"
153188
154189 # Patch ADDITIONALPLUGINSPATH separately — it is outside the checkout tree.
155190 local plugin_dir
156- plugin_dir=$(find "$WORK_DIR /standalone-win-x64" -name "nsis_tauri_utils.dll" -exec dirname {} \; | head -1)
191+ plugin_dir=$(find "$SIGN_DIR /standalone-win-x64" -name "nsis_tauri_utils.dll" -exec dirname {} \; | head -1)
157192 if [[ -n "$plugin_dir" ]]; then
158193 local abs_plugin_dir
159194 abs_plugin_dir="$(cd "$plugin_dir" && pwd)"
@@ -204,15 +239,47 @@ find_release_run_id() {
204239}
205240
206241# =============================================================================
207- # Download CI Artifacts
242+ # Download CI Artifacts (per-artifact caching)
208243# =============================================================================
209244
245+ # Downloads artifacts individually, skipping any already cached.
246+ # Artifacts are stored in $DOWNLOAD_DIR and NEVER modified after download.
247+ download_artifacts_from_run() {
248+ local run_id="$1"
249+
250+ mkdir -p "$DOWNLOAD_DIR"
251+
252+ for name in "${ARTIFACT_NAMES[@]}"; do
253+ if artifact_downloaded "$name"; then
254+ log " $name: already downloaded, skipping"
255+ continue
256+ fi
257+
258+ log " $name: downloading..."
259+ if gh run download "$run_id" \
260+ --repo "$GITHUB_REPO" \
261+ --name "$name" \
262+ --dir "$DOWNLOAD_DIR"; then
263+ touch "$DOWNLOAD_DIR/.downloaded-$name"
264+ log " $name: done"
265+ else
266+ warn " $name: download failed (will retry on next run)"
267+ fi
268+ done
269+
270+ if all_artifacts_downloaded; then
271+ log "All artifacts downloaded to $DOWNLOAD_DIR"
272+ else
273+ error "Some artifacts failed to download. Re-run to retry."
274+ fi
275+ }
276+
210277download_artifacts() {
211278 local version="$1"
212279 local tag="v$version"
213280
214- if artifacts_cached "$version" ; then
215- log "Artifacts already downloaded for $version , skipping download "
281+ if all_artifacts_downloaded ; then
282+ log "All artifacts already downloaded, skipping"
216283 return
217284 fi
218285
@@ -246,26 +313,16 @@ download_artifacts() {
246313 || error "Workflow failed. Check: https://github.com/$GITHUB_REPO/actions/runs/$run_id"
247314
248315 log "Workflow completed successfully!"
249-
250- rm -rf "$WORK_DIR"
251- mkdir -p "$WORK_DIR"
252-
253316 log "Downloading artifacts..."
254- gh run download "$run_id" \
255- --repo "$GITHUB_REPO" \
256- --dir "$WORK_DIR"
257-
258- echo "$version" > "$WORK_DIR/.version"
259- log "Artifacts downloaded to $WORK_DIR"
260- ls -la "$WORK_DIR"
317+ download_artifacts_from_run "$run_id"
261318}
262319
263320resume_download() {
264321 local version="$1"
265322 local tag="v$version"
266323
267- if artifacts_cached "$version" ; then
268- log "Artifacts already downloaded for $version , skipping download "
324+ if all_artifacts_downloaded ; then
325+ log "All artifacts already downloaded, skipping"
269326 return
270327 fi
271328
@@ -288,18 +345,8 @@ resume_download() {
288345 fi
289346
290347 log "Found completed workflow run: $run_id"
291-
292- rm -rf "$WORK_DIR"
293- mkdir -p "$WORK_DIR"
294-
295348 log "Downloading artifacts..."
296- gh run download "$run_id" \
297- --repo "$GITHUB_REPO" \
298- --dir "$WORK_DIR"
299-
300- echo "$version" > "$WORK_DIR/.version"
301- log "Artifacts downloaded to $WORK_DIR"
302- ls -la "$WORK_DIR"
349+ download_artifacts_from_run "$run_id"
303350}
304351
305352# =============================================================================
@@ -346,7 +393,7 @@ sign_macos() {
346393 log "Starting macOS code signing..."
347394
348395 local app
349- app=$(find "$WORK_DIR /standalone-mac-aarch64" -name "*.app" -type d | head -1)
396+ app=$(find "$SIGN_DIR /standalone-mac-aarch64" -name "*.app" -type d | head -1)
350397
351398 [[ -n "$app" ]] && sign_macos_app "$app" "aarch64"
352399
@@ -363,7 +410,7 @@ notarize_macos_app() {
363410
364411 log "Notarizing macOS app ($arch_label)..."
365412
366- local zip_path="$WORK_DIR /notarize-${arch_label}.zip"
413+ local zip_path="$SIGN_DIR /notarize-${arch_label}.zip"
367414
368415 ditto -c -k --keepParent "$app_path" "$zip_path"
369416
@@ -390,7 +437,7 @@ notarize_macos() {
390437 prompt_secret APPLE_SIGN_PASS "Enter Apple ID password (or app-specific password)"
391438
392439 local app
393- app=$(find "$WORK_DIR /standalone-mac-aarch64" -name "*.app" -type d | head -1)
440+ app=$(find "$SIGN_DIR /standalone-mac-aarch64" -name "*.app" -type d | head -1)
394441
395442 [[ -n "$app" ]] && notarize_macos_app "$app" "aarch64"
396443
@@ -401,10 +448,10 @@ notarize_macos() {
401448
402449 log "Creating $FNAME_MAC_DMG..."
403450 hdiutil create -volname "MouseTerm" -srcfolder "$app" \
404- -ov -format UDZO "$WORK_DIR /$FNAME_MAC_DMG"
451+ -ov -format UDZO "$SIGN_DIR /$FNAME_MAC_DMG"
405452
406453 log "Creating $FNAME_MAC_UPDATE..."
407- tar -czf "$WORK_DIR /$FNAME_MAC_UPDATE" -C "$(dirname "$app")" "$app_name"
454+ tar -czf "$SIGN_DIR /$FNAME_MAC_UPDATE" -C "$(dirname "$app")" "$app_name"
408455 fi
409456
410457 log "All macOS notarization and packaging complete"
@@ -422,7 +469,7 @@ sign_windows() {
422469
423470 # Find the inner exe
424471 local exe_path
425- exe_path=$(find "$WORK_DIR /standalone-win-x64" \( -name "MouseTerm.exe" -o -name "mouseterm.exe" \) -not -name "*setup*" -not -name "*install*" | head -1)
472+ exe_path=$(find "$SIGN_DIR /standalone-win-x64" \( -name "MouseTerm.exe" -o -name "mouseterm.exe" \) -not -name "*setup*" -not -name "*install*" | head -1)
426473 [[ -n "$exe_path" ]] || error "Windows executable not found"
427474
428475 log "Signing inner executable: $exe_path"
@@ -436,7 +483,7 @@ sign_windows() {
436483
437484 # Find the NSIS installer
438485 local installer_path
439- installer_path=$(find "$WORK_DIR /standalone-win-x64" -name "*setup*.exe" -o -name "*install*.exe" | head -1)
486+ installer_path=$(find "$SIGN_DIR /standalone-win-x64" -name "*setup*.exe" -o -name "*install*.exe" | head -1)
440487
441488 if [[ -n "$installer_path" ]]; then
442489 rebuild_windows_installer "$exe_path" "$installer_path"
@@ -450,7 +497,7 @@ sign_windows() {
450497 "$installer_path"
451498
452499 # Copy with stable filename
453- cp "$installer_path" "$WORK_DIR /$FNAME_WIN_EXE"
500+ cp "$installer_path" "$SIGN_DIR /$FNAME_WIN_EXE"
454501 fi
455502
456503 log "Windows signing complete"
@@ -472,18 +519,18 @@ sign_updates() {
472519
473520 # Collect and rename update bundles with stable filenames
474521 # macOS .tar.gz (already created by notarize step)
475- [[ -f "$WORK_DIR /$FNAME_MAC_UPDATE" ]] && cp "$WORK_DIR /$FNAME_MAC_UPDATE" "$release_dir/"
476- [[ -f "$WORK_DIR /$FNAME_MAC_DMG" ]] && cp "$WORK_DIR /$FNAME_MAC_DMG" "$release_dir/"
522+ [[ -f "$SIGN_DIR /$FNAME_MAC_UPDATE" ]] && cp "$SIGN_DIR /$FNAME_MAC_UPDATE" "$release_dir/"
523+ [[ -f "$SIGN_DIR /$FNAME_MAC_DMG" ]] && cp "$SIGN_DIR /$FNAME_MAC_DMG" "$release_dir/"
477524
478525 # Windows NSIS zip — rebuild with signed exe so Tauri auto-update gets the signed binary
479526 local win_nsis
480- win_nsis=$(find "$WORK_DIR /standalone-win-x64" -name "*.nsis.zip" | head -1)
527+ win_nsis=$(find "$SIGN_DIR /standalone-win-x64" -name "*.nsis.zip" | head -1)
481528 if [[ -n "$win_nsis" ]]; then
482529 local signed_exe
483- signed_exe=$(find "$WORK_DIR /standalone-win-x64" -name "MouseTerm.exe" -not -name "*setup*" -not -name "*install*" | head -1)
530+ signed_exe=$(find "$SIGN_DIR /standalone-win-x64" -name "MouseTerm.exe" -not -name "*setup*" -not -name "*install*" | head -1)
484531 if [[ -n "$signed_exe" ]]; then
485532 log "Rebuilding NSIS zip with signed executable..."
486- local nsis_tmp="$WORK_DIR /nsis-repack"
533+ local nsis_tmp="$SIGN_DIR /nsis-repack"
487534 mkdir -p "$nsis_tmp"
488535 unzip -o "$win_nsis" -d "$nsis_tmp"
489536 # Replace the unsigned exe inside the extracted zip with the signed one
@@ -504,19 +551,19 @@ sign_updates() {
504551 fi
505552
506553 # Windows installer
507- [[ -f "$WORK_DIR /$FNAME_WIN_EXE" ]] && cp "$WORK_DIR /$FNAME_WIN_EXE" "$release_dir/"
554+ [[ -f "$SIGN_DIR /$FNAME_WIN_EXE" ]] && cp "$SIGN_DIR /$FNAME_WIN_EXE" "$release_dir/"
508555
509556 # Linux AppImage
510557 local linux_appimage
511- linux_appimage=$(find "$WORK_DIR /standalone-linux-x64" -name "*.AppImage" -not -name "*.tar.gz" | head -1)
558+ linux_appimage=$(find "$SIGN_DIR /standalone-linux-x64" -name "*.AppImage" -not -name "*.tar.gz" | head -1)
512559 [[ -n "$linux_appimage" ]] && cp "$linux_appimage" "$release_dir/$FNAME_LINUX_APPIMAGE"
513560
514561 local linux_update
515- linux_update=$(find "$WORK_DIR /standalone-linux-x64" -name "*.AppImage.tar.gz" | head -1)
562+ linux_update=$(find "$SIGN_DIR /standalone-linux-x64" -name "*.AppImage.tar.gz" | head -1)
516563 [[ -n "$linux_update" ]] && cp "$linux_update" "$release_dir/$FNAME_LINUX_UPDATE"
517564
518565 local linux_deb
519- linux_deb=$(find "$WORK_DIR /standalone-linux-x64" -name "*.deb" | head -1)
566+ linux_deb=$(find "$SIGN_DIR /standalone-linux-x64" -name "*.deb" | head -1)
520567 [[ -n "$linux_deb" ]] && cp "$linux_deb" "$release_dir/$FNAME_LINUX_DEB"
521568
522569 # Generate .sig files for update bundles using Tauri CLI
@@ -678,6 +725,7 @@ main() {
678725
679726 check_git_clean
680727 download_artifacts "$version"
728+ prepare_sign_dir
681729 sign_macos
682730 notarize_macos
683731 sign_windows
@@ -689,24 +737,29 @@ main() {
689737 [[ -z "$version" ]] && error "Usage: $(basename "$0") resume <version>"
690738
691739 resume_download "$version"
740+ prepare_sign_dir
692741 sign_macos
693742 notarize_macos
694743 sign_windows
695744 sign_updates "$version"
696745 create_release "$version"
697746 ;;
698747 sign-mac)
748+ prepare_sign_dir
699749 sign_macos
700750 ;;
701751 notarize)
752+ prepare_sign_dir
702753 notarize_macos
703754 ;;
704755 sign-win)
756+ prepare_sign_dir
705757 sign_windows
706758 ;;
707759 sign-updates)
708760 local version="${2:-}"
709761 [[ -z "$version" ]] && error "Usage: $(basename "$0") sign-updates <version>"
762+ prepare_sign_dir
710763 sign_updates "$version"
711764 ;;
712765 release)
0 commit comments