@@ -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