feat: notary-dry-run.sh for macOS menubar (no submit without NOTARY_SUBMIT=1)
Some checks failed
windows-msi / full-msi-unsigned (push) Has been cancelled

Prints notarytool recipe with secret env names; links CI and backlog.
This commit is contained in:
George Lambert 2026-10-14 01:08:18 -04:00
commit 9f0881ccfc
5 changed files with 295 additions and 2 deletions

View file

@ -17,6 +17,7 @@ Repo now includes **authoring/docs scaffolds** for the two commercial gaps that
| **WiX tray presence MSI skeleton** | [`platform/windows/wix/`](../platform/windows/wix/) — `VeraeTrayShortcuts.wxs`, `VeraeTrayShortcutsFragment.wxs`, [`README.md`](../platform/windows/wix/README.md); driver [`scripts/build-tray-msi.ps1`](../platform/windows/scripts/build-tray-msi.ps1) | Non-elevated **per-user** WiX v3 product + shortcut fragment: stages `verae-tray.ps1`, Start Menu `Programs\Verae\Verae Tray.lnk`, Startup `Verae Tray.lnk`. **ProductVersion** wired from `Get-VeraePackageVersion` → `internal/job.HelperVersion` (or `VERSION=` / `-Version`). `build-tray-msi.ps1` runs **candle/light** when present; otherwise prints WiX install tip (exit 0). | Full elevated commercial MSI (printers, Program Files, Authenticode, NSIS parity, CI-signed MSI in `dist/`). No MSI binary without the WiX toolset on the build host. |
| **Menubar notarize checklist** | [`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../platform/macos/menubar/NOTARIZE-CHECKLIST.md) | Operator step list: unsigned build → codesign hardened runtime → `ditto` zip → `notarytool` (keychain profile **or** API key placeholders) → staple → `spctl`. No secrets. | Automated sign/notarize in `build-menubar-app.sh`, stapled `.app.zip`/DMG release, or live CI keychain import. |
| **Menubar CI notary sketch** | [`platform/macos/menubar/ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example) | GHA/Forgejo **example** workflow: default **unsigned** `build-menubar-app.sh` + artifact; commented notarytool job gated `if: false`; secret **names** only (`APPLE_API_KEY_*`, `APPLE_CODESIGN_IDENTITY`, …). | Not a live workflow under `.github/`; no staple; not Gatekeeper-proven. |
| **Menubar notary dry-run** | [`platform/macos/menubar/notary-dry-run.sh`](../platform/macos/menubar/notary-dry-run.sh) | Local helper: if `notarytool` present print codesign/`ditto`/submit/staple recipe with env **names** only; else Xcode CLT tip (exit 0). Links [`ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example) + this backlog. **Never submits** unless `NOTARY_SUBMIT=1`. | Not live CI; does not codesign by default; not Gatekeeper-proven. |
| **Full elevated MSI (StageFull + light)** | [`Product-Full.wxs`](../platform/windows/wix/Product-Full.wxs) + [`scripts/build-full-msi.ps1`](../platform/windows/scripts/build-full-msi.ps1) (+ outline, [`SILENT_PROPS.md`](../platform/windows/wix/SILENT_PROPS.md), [`CI-SIGN-NOTARY.md`](../platform/windows/wix/CI-SIGN-NOTARY.md), [`DUAL-SHIP.md`](../platform/windows/wix/DUAL-SHIP.md)) | Elevated WiX v3 product: perMachine, Feature/ComponentGroups for **bin** (`verae-print.exe` required), **scripts**, **docs**, Start Menu; silent public props; distinct UpgradeCode. `build-full-msi.ps1` creates `StageFull/{bin,scripts}`, copies or Go-builds EXEs, copies scripts/LICENSE/README, stamps **ProductVersion** from `Get-VeraePackageVersion`, runs candle/light → `VeraePrintStamp-windows-*-<version>.msi`. WiX missing → **exit 1** + clear tip. Version defaults on build-windows / build-pkg / NSIS / build-installer aligned to HelperVersion. Commented deferred CA + `InstallExecuteSequence` stubs for `install-printer.ps1` (no binaries required). Dual-ship runbook documents NSIS vs MSI clean-VM gates + **thin-NSIS exit checklist**. | **Not** Authenticode-signed; printer CAs still **commented** (not scheduled/tested); properties not yet honored by live CAs; not Intune-proven; NSIS still primary full install; CI builds **unsigned** MSI only (sign step not live). |
| **CI unsigned full MSI** | [`.github/workflows/windows-msi.yml`](../.github/workflows/windows-msi.yml) + DUAL-SHIP § CI artifact | `windows-latest`: install WiX v3 → `build-full-msi.ps1` → upload artifact `VeraePrintStamp-windows-msi-unsigned`. Future Authenticode secret **names** only in workflow comments / CI-SIGN-NOTARY. | No live `signtool`; no certs in git; not a production download channel. |
@ -40,6 +41,7 @@ Repo now includes **authoring/docs scaffolds** for the two commercial gaps that
| **Offline dual-ship smoke** [`dual-ship-smoke.ps1`](../platform/windows/scripts/dual-ship-smoke.ps1) (prints A/B/C; **VM C blocked until CAs enabled**; `-VerifyArtifacts` → existence + `fill-manifest-sha256.ps1`) | Does **not** replace clean-VM sign-off; no install/sign/upload |
| **Enable printer CAs** [`enable-printer-cas.ps1`](../platform/windows/scripts/enable-printer-cas.ps1) + wix README § Enable printer CAs | Unwraps `DEFERRED_PRINTER_CAS:OFF` in Product-Full for VM C; does **not** build/test/sign; default remains disabled |
| **Authenticode dry-run** [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1) + [`CI-SIGN-NOTARY.md`](../platform/windows/wix/CI-SIGN-NOTARY.md) | Local verify + print sign cmds only (**no secrets**). Live CI signtool / Trusted Signing still **not** enabled |
| **Notary dry-run** [`notary-dry-run.sh`](../platform/macos/menubar/notary-dry-run.sh) + [`ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example) | Local print submit recipe (env **names**); tip if no notarytool; submit only if `NOTARY_SUBMIT=1`. Live notary CI still **not** enabled |
**Still use for day-to-day presence (no WiX / no Apple certs):**
Windows `scripts/build-tray-startmenu.ps1` + `install-tray-startup.ps1`; macOS `verae-print menubar --install` / unsigned `build-menubar-app.sh`. Full product install remains NSIS (`installer-windows.nsi`) and macOS `pkg/build-pkg.sh`. Optional: `build-tray-msi.ps1` (WiX tip exit 0); `build-full-msi.ps1` (needs WiX; exit 1 if missing).
@ -133,6 +135,7 @@ NSIS product already covers much of the *install experience* (files under Progra
| **Unsigned** menu-bar `.app` builder | [`platform/macos/menubar/build-menubar-app.sh`](../platform/macos/menubar/build-menubar-app.sh) | Dev/local only; copies entitlements but **does not** codesign |
| Notarization **documentation only** (menubar) | [`platform/macos/menubar/README.txt`](../platform/macos/menubar/README.txt) + [PACKAGING.md](PACKAGING.md) § Codesign + notarization | Manual steps; no automated menubar notarize |
| **Notarize checklist** (menubar) | [`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../platform/macos/menubar/NOTARIZE-CHECKLIST.md) | **Scaffold shipped** — notarytool placeholders, staple, `spctl`; no secrets; automation still open |
| **Notary dry-run** (menubar) | [`platform/macos/menubar/notary-dry-run.sh`](../platform/macos/menubar/notary-dry-run.sh) | **Tool** — print submit recipe (env names) or Xcode tip; links `ci-notarize.yml.example` + this backlog; **no submit** unless `NOTARY_SUBMIT=1` |
| Packaging smoke | [`platform/smoke-packaging-metadata.sh`](../platform/smoke-packaging-metadata.sh) | Version + “no elevated MSI” + IPP purity — not Gatekeeper |
### Missing for commercial notarized `.app`
@ -185,6 +188,7 @@ NSIS product already covers much of the *install experience* (files under Progra
| Windows **MSI** full product (files + ARP) | **Yes authoring** (`Product-Full.wxs` + `build-full-msi.ps1` StageFull → light; CA schedule stubs commented) | **No commercial** — unsigned; printer CAs not live; NSIS still primary |
| Dual-ship NSIS↔MSI runbook | **Yes** ([`DUAL-SHIP.md`](../platform/windows/wix/DUAL-SHIP.md)) | **Docs only** — clean-VM validation + thin-NSIS exit checklist still unchecked |
| Menubar CI notary example | **Yes** ([`ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example)) | **Example only** — unsigned default; notary job `if: false` |
| Menubar notary dry-run | **Yes** ([`notary-dry-run.sh`](../platform/macos/menubar/notary-dry-run.sh)) | **Scaffold** — print recipe / tip; submit gated `NOTARY_SUBMIT=1` only |
| Windows Authenticode pipeline | **Draft doc** (`wix/CI-SIGN-NOTARY.md`) + **dry-run** [`sign-dry-run.ps1`](../platform/windows/scripts/sign-dry-run.ps1) | **No** — no live signtool job (**do not claim shipped**) |
| macOS menu bar LaunchAgent | Yes | Yes (dev/ops) |
| macOS unsigned menubar `.app` | Yes | **No** (Gatekeeper) |
@ -209,6 +213,7 @@ NSIS product already covers much of the *install experience* (files under Progra
- [`platform/macos/menubar/`](../platform/macos/menubar/) — menu bar + unsigned `.app` + notarize docs/checklist
- [`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../platform/macos/menubar/NOTARIZE-CHECKLIST.md) — menubar notarytool step list
- [`platform/macos/menubar/ci-notarize.yml.example`](../platform/macos/menubar/ci-notarize.yml.example) — GHA/Forgejo menubar CI sketch (unsigned default; secret names only)
- [`platform/macos/menubar/notary-dry-run.sh`](../platform/macos/menubar/notary-dry-run.sh) — local notarytool dry-run (print recipe; `NOTARY_SUBMIT=1` to submit)
- [`docs/installer-registration-api.md`](installer-registration-api.md) — installer ↔ portal registration API
- [`README.md`](../README.md) — top-level packaging pointers

View file

@ -152,12 +152,17 @@ spctl --assess --type execute -vv "$APP"
- [x] CI workflow **example** (unsigned default; notary secret **names** only, gated off):
[`ci-notarize.yml.example`](ci-notarize.yml.example) — copy into `.github/workflows/` or
`.forgejo/workflows/` when enabling; do not treat the example as a live release job
- [x] Local **dry-run** (print submit recipe / tip; no submit unless gated):
[`notary-dry-run.sh`](notary-dry-run.sh) — if `notarytool` present prints codesign/`ditto`/submit/staple
with env **names** only; else Xcode CLT tip (exit 0). Links `ci-notarize.yml.example` +
[`docs/COMMERCIAL-INSTALLER-BACKLOG.md`](../../../docs/COMMERCIAL-INSTALLER-BACKLOG.md).
**Never submits** unless `NOTARY_SUBMIT=1`.
- [ ] Wire `build-menubar-app.sh` (or shared `sign-and-notarize.sh`) to run the
above when `SIGNING_ENV` / CI secrets exist; keep **unsigned default** for dev
- [ ] Promote example → live CI macOS runner: import Developer ID + notary key → staple → checksums
- [ ] Smoke: `spctl` + `stapler validate` on signed hosts only
Until live CI + script automation land, this checklist is the operator runbook.
Until live CI + automated sign path land, this checklist + `notary-dry-run.sh` are the operator runbook.
---

View file

@ -52,6 +52,9 @@ Codesign + notarization (document only — needs Apple certs)
Operator steps: NOTARIZE-CHECKLIST.md
CI sketch (unsigned default; notary secret names only): ci-notarize.yml.example
Copy into .github/workflows or .forgejo/workflows when enabling; not live.
Local dry-run (print recipe; tip if no notarytool; never submits unless
NOTARY_SUBMIT=1): ./notary-dry-run.sh
Links ci-notarize.yml.example + docs/COMMERCIAL-INSTALLER-BACKLOG.md.
You need an Apple Developer Program membership and a Developer ID
Application certificate in the keychain. This project does not ship
@ -106,4 +109,7 @@ Files
run-menubar.sh compile-or-interpret entry
com.verae.timestamping.menubar.plist LaunchAgent template
build-menubar-app.sh unsigned .app bundle builder
notary-dry-run.sh print notarytool recipe / tip (NOTARY_SUBMIT=1 to submit)
ci-notarize.yml.example GHA/Forgejo sketch (unsigned default)
NOTARIZE-CHECKLIST.md operator codesign + notary + staple steps
README.txt this file (incl. notarization)

View file

@ -0,0 +1,273 @@
#!/usr/bin/env bash
# notary-dry-run.sh — menubar notarytool dry-run (Issue 1 commercial)
#
# Default: locate notarytool, report APP/ZIP presence, print submit/staple
# recipes using env **names** only. Does NOT codesign, submit, or staple.
#
# Live submit only when ALL of:
# NOTARY_SUBMIT=1
# notarytool on PATH
# a zip (or ditto-able .app) exists
# Never prints .p8 contents, passwords, or real secret values.
#
# Related:
# ci-notarize.yml.example
# NOTARIZE-CHECKLIST.md
# platform/windows/wix/CI-SIGN-NOTARY.md
# docs/COMMERCIAL-INSTALLER-BACKLOG.md
#
# Usage:
# ./notary-dry-run.sh
# ./notary-dry-run.sh ~/Applications/Verae\ Menubar.app
# APP=… ZIP=… ./notary-dry-run.sh
# NOTARY_SUBMIT=1 ZIP=/tmp/VeraeMenubar.zip ./notary-dry-run.sh # gated submit
#
# Exit: 0 on dry-run success (including missing notarytool tip).
# 1 on bad args / failed gated submit.
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$DIR/../../.." && pwd)"
APP="${APP:-}"
ZIP="${ZIP:-}"
PROFILE="${NOTARY_KEYCHAIN_PROFILE:-verae-notary}"
# Optional positional: path to .app or .zip
if [[ $# -gt 0 ]]; then
arg="$1"
case "$arg" in
*.zip|*.ZIP)
ZIP="$arg"
;;
*.app|*.APP)
APP="$arg"
;;
*)
if [[ -d "$arg" && "$arg" == *.app ]]; then
APP="$arg"
elif [[ -f "$arg" && "$arg" == *.zip ]]; then
ZIP="$arg"
else
echo "usage: $0 [path/to/Verae Menubar.app|.zip]" >&2
echo " env: APP= ZIP= NOTARY_SUBMIT=1 NOTARY_KEYCHAIN_PROFILE=" >&2
exit 1
fi
;;
esac
fi
# Defaults when unset
if [[ -z "$APP" ]]; then
for cand in \
"$DIR/Verae Menubar.app" \
"$HOME/Applications/Verae Menubar.app" \
"/Applications/Verae Menubar.app"
do
if [[ -d "$cand" ]]; then
APP="$cand"
break
fi
done
fi
if [[ -z "$ZIP" ]]; then
for cand in \
"/tmp/VeraeMenubar.zip" \
"$DIR/VeraeMenubar.zip" \
"${TMPDIR:-/tmp}/VeraeMenubar.zip"
do
if [[ -f "$cand" ]]; then
ZIP="$cand"
break
fi
done
fi
ZIP="${ZIP:-/tmp/VeraeMenubar.zip}"
echo "notary-dry-run — does NOT codesign or staple by default; submit only if NOTARY_SUBMIT=1."
echo "Repo: $ROOT"
echo "Docs: platform/macos/menubar/ci-notarize.yml.example"
echo " platform/macos/menubar/NOTARIZE-CHECKLIST.md"
echo " platform/windows/wix/CI-SIGN-NOTARY.md"
echo " docs/COMMERCIAL-INSTALLER-BACKLOG.md"
echo ""
# --- notarytool presence ---
echo "==== notarytool ===="
NOTARYTOOL=""
if command -v xcrun >/dev/null 2>&1; then
if xcrun --find notarytool >/dev/null 2>&1; then
NOTARYTOOL="$(xcrun --find notarytool 2>/dev/null || true)"
fi
fi
if [[ -z "$NOTARYTOOL" ]] && command -v notarytool >/dev/null 2>&1; then
NOTARYTOOL="$(command -v notarytool)"
fi
if [[ -z "$NOTARYTOOL" ]]; then
echo " notarytool: NOT FOUND"
echo ""
echo " Tip: install Xcode Command Line Tools (or full Xcode), then:"
echo " xcode-select --install"
echo " xcrun --find notarytool"
echo " Live CI notary stays gated off — see ci-notarize.yml.example (if: false)"
echo " and CI-SIGN-NOTARY.md. Exiting 0 (dry-run scaffold; no notarytool required)."
exit 0
fi
echo " notarytool: $NOTARYTOOL"
# --- artifacts ---
echo ""
echo "==== Artifacts ===="
if [[ -n "$APP" && -d "$APP" ]]; then
echo " APP: $APP"
if [[ -f "$APP/Contents/Resources/VERSION" ]]; then
echo " VERSION=$(tr -d '\r\n' < "$APP/Contents/Resources/VERSION")"
fi
else
echo " APP: (none found — build with ./build-menubar-app.sh)"
APP=""
fi
if [[ -f "$ZIP" ]]; then
echo " ZIP: $ZIP ($(wc -c < "$ZIP" | tr -d ' ') bytes)"
else
echo " ZIP: $ZIP (missing — ditto after codesign; see recipe below)"
fi
# --- codesign / staple status (informational only) ---
echo ""
echo "==== Status (informational) ===="
if [[ -n "$APP" ]]; then
if command -v codesign >/dev/null 2>&1; then
if codesign --verify --deep --strict "$APP" >/dev/null 2>&1; then
echo " codesign verify: OK"
else
echo " codesign verify: FAIL / unsigned (expected until Developer ID applied)"
fi
else
echo " codesign: not on PATH"
fi
if command -v xcrun >/dev/null 2>&1 && xcrun --find stapler >/dev/null 2>&1; then
if xcrun stapler validate "$APP" >/dev/null 2>&1; then
echo " stapler validate: OK (ticket present)"
else
echo " stapler validate: no ticket (expected until notary Accepted + staple)"
fi
fi
else
echo " (skip — no APP)"
fi
# --- env secret names + recipe ---
echo ""
echo "==== Submit recipe (env names only — no secrets) ===="
echo " Secret / env names (see CI-SIGN-NOTARY.md + ci-notarize.yml.example):"
echo " APPLE_API_KEY_P8_BASE64 # base64 of AuthKey_*.p8 (CI materialize only)"
echo " APPLE_API_KEY_ID"
echo " APPLE_API_ISSUER_ID"
echo " APPLE_TEAM_ID"
echo " APPLE_CODESIGN_IDENTITY # Developer ID Application: … (TEAMID)"
echo " APPLE_INSTALLER_IDENTITY # optional; product .pkg path"
echo " NOTARY_KEYCHAIN_PROFILE # laptop path (default print: $PROFILE)"
echo " NOTARY_KEY_PATH # path to AuthKey_*.p8 after materialize"
echo " NOTARY_KEY_ID / NOTARY_ISSUER_ID # aliases used in NOTARIZE-CHECKLIST"
echo ""
echo " # 0) Build unsigned (dev default)"
echo " ./build-menubar-app.sh \"\${APP:-\$HOME/Applications/Verae Menubar.app}\""
echo ""
echo " # 1) Codesign hardened runtime (operator machine / gated CI — not this dry-run)"
echo " APP=\"\${APP:-\$HOME/Applications/Verae Menubar.app}\""
echo " ENT=\"\$APP/Contents/Resources/entitlements.plist\""
echo " codesign --force --deep --options runtime --timestamp \\"
echo " --entitlements \"\$ENT\" \\"
echo " --sign \"\$APPLE_CODESIGN_IDENTITY\" \\"
echo " \"\$APP\""
echo " codesign --verify --deep --strict --verbose=2 \"\$APP\""
echo ""
echo " # 2) Zip for notarytool"
echo " ZIP=\"\${ZIP:-/tmp/VeraeMenubar.zip}\""
echo " rm -f \"\$ZIP\""
echo " ditto -c -k --keepParent \"\$APP\" \"\$ZIP\""
echo ""
echo " # 3a) Submit — keychain profile (laptops)"
echo " xcrun notarytool submit \"\$ZIP\" \\"
echo " --keychain-profile \"\${NOTARY_KEYCHAIN_PROFILE:-$PROFILE}\" \\"
echo " --wait"
echo ""
echo " # 3b) Submit — App Store Connect API key (CI-friendly)"
echo " xcrun notarytool submit \"\$ZIP\" \\"
echo " --key \"\$NOTARY_KEY_PATH\" \\"
echo " --key-id \"\${APPLE_API_KEY_ID:-\$NOTARY_KEY_ID}\" \\"
echo " --issuer \"\${APPLE_API_ISSUER_ID:-\$NOTARY_ISSUER_ID}\" \\"
echo " --wait"
echo ""
echo " # 4) Staple + Gatekeeper"
echo " xcrun stapler staple \"\$APP\""
echo " xcrun stapler validate \"\$APP\""
echo " spctl --assess --type execute -vv \"\$APP\""
echo ""
echo " CI sketch (unsigned default; notary job if: false):"
echo " platform/macos/menubar/ci-notarize.yml.example"
echo " Backlog: docs/COMMERCIAL-INSTALLER-BACKLOG.md"
echo " This dry-run does not claim notarization shipped."
# --- gated submit ---
echo ""
echo "==== Gated submit ===="
if [[ "${NOTARY_SUBMIT:-}" != "1" ]]; then
echo " NOTARY_SUBMIT is not 1 — no submit performed (print-only)."
echo " To submit for real: NOTARY_SUBMIT=1 ZIP=… [key env] $0"
exit 0
fi
echo " NOTARY_SUBMIT=1 — attempting notarytool submit --wait"
if [[ ! -f "$ZIP" ]]; then
if [[ -n "$APP" && -d "$APP" ]]; then
echo " ZIP missing; creating via ditto from APP…"
rm -f "$ZIP"
ditto -c -k --keepParent "$APP" "$ZIP"
else
echo " ERROR: NOTARY_SUBMIT=1 but no ZIP and no APP to ditto." >&2
exit 1
fi
fi
# Prefer API key env if NOTARY_KEY_PATH set; else keychain profile
set +e
if [[ -n "${NOTARY_KEY_PATH:-}" || -n "${APPLE_API_KEY_ID:-}" ]]; then
KEY_PATH="${NOTARY_KEY_PATH:-}"
KEY_ID="${APPLE_API_KEY_ID:-${NOTARY_KEY_ID:-}}"
ISSUER="${APPLE_API_ISSUER_ID:-${NOTARY_ISSUER_ID:-}}"
if [[ -z "$KEY_PATH" || -z "$KEY_ID" || -z "$ISSUER" ]]; then
echo " ERROR: API-key submit needs NOTARY_KEY_PATH + APPLE_API_KEY_ID + APPLE_API_ISSUER_ID" >&2
exit 1
fi
echo " mode: API key (key-id=\$APPLE_API_KEY_ID — value not printed)"
xcrun notarytool submit "$ZIP" \
--key "$KEY_PATH" \
--key-id "$KEY_ID" \
--issuer "$ISSUER" \
--wait
rc=$?
else
echo " mode: keychain profile (${NOTARY_KEYCHAIN_PROFILE:-$PROFILE})"
xcrun notarytool submit "$ZIP" \
--keychain-profile "${NOTARY_KEYCHAIN_PROFILE:-$PROFILE}" \
--wait
rc=$?
fi
set -e
if [[ $rc -ne 0 ]]; then
echo " notarytool submit failed (exit $rc)" >&2
exit "$rc"
fi
if [[ -n "$APP" && -d "$APP" ]]; then
echo " stapling $APP"
xcrun stapler staple "$APP"
xcrun stapler validate "$APP" || true
fi
echo " submit path finished."
exit 0

View file

@ -7,6 +7,8 @@
[`.github/workflows/windows-msi.yml`](../../../.github/workflows/windows-msi.yml),
[`docs/COMMERCIAL-INSTALLER-BACKLOG.md`](../../../docs/COMMERCIAL-INSTALLER-BACKLOG.md),
[`platform/windows/scripts/sign-dry-run.ps1`](../scripts/sign-dry-run.ps1) — local `signtool verify` + print real sign commands (**no secrets**; exit 0 if no signtool),
[`platform/macos/menubar/notary-dry-run.sh`](../../macos/menubar/notary-dry-run.sh) — local notarytool recipe / tip (**no secrets**; submit only if `NOTARY_SUBMIT=1`),
[`platform/macos/menubar/ci-notarize.yml.example`](../../macos/menubar/ci-notarize.yml.example),
[`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../../macos/menubar/NOTARIZE-CHECKLIST.md),
[`platform/macos/pkg/build-pkg.sh`](../../macos/pkg/build-pkg.sh).
@ -199,7 +201,9 @@ GHA/Forgejo copy-paste sketch (unsigned default, notary job gated off): [`ci-not
**Live:** [`.github/workflows/windows-msi.yml`](../../../.github/workflows/windows-msi.yml) builds an **unsigned** full MSI when WiX is installed on `windows-latest` and uploads the artifact (see DUAL-SHIP § CI artifact).
**Local dry-run (no CI secrets):** [`../scripts/sign-dry-run.ps1`](../scripts/sign-dry-run.ps1) finds `signtool`, runs `verify /pa` on local MSI/EXE when present, and prints copy-paste `signtool sign` lines using the env **names** above only. Missing signtool → exit 0 + Windows SDK tip. Does **not** sign or import a PFX.
**Local dry-run (no CI secrets):**
- Windows: [`../scripts/sign-dry-run.ps1`](../scripts/sign-dry-run.ps1) finds `signtool`, runs `verify /pa` on local MSI/EXE when present, and prints copy-paste `signtool sign` lines using the env **names** above only. Missing signtool → exit 0 + Windows SDK tip. Does **not** sign or import a PFX.
- macOS menubar: [`../../macos/menubar/notary-dry-run.sh`](../../macos/menubar/notary-dry-run.sh) finds `notarytool`, prints codesign/`ditto`/submit/staple recipe (env **names** only), links [`ci-notarize.yml.example`](../../macos/menubar/ci-notarize.yml.example) + [`COMMERCIAL-INSTALLER-BACKLOG.md`](../../../docs/COMMERCIAL-INSTALLER-BACKLOG.md). Missing notarytool → exit 0 + Xcode CLT tip. **Never submits** unless `NOTARY_SUBMIT=1`.
**Not done:**