release: publish-update.sh — resolve a real signing identity, notarize, record it in the appcast
SIGN_IDENTITY now comes from REDLINE_SIGN_IDENTITY, else the first "Developer ID Application" identity in the keychain (REDLINE_KEYCHAIN adds --keychain everywhere it's searched/used), else the existing Apple Development identity with a loud WARNING that Gatekeeper will block first install elsewhere. build-app.sh still has to sign first with its own hardcoded Apple Development identity (that pair is what keeps the Screen Recording grant alive) — this script now re-signs the resulting bundle with the resolved identity, --options runtime --timestamp, before zipping. Notarization is optional: set REDLINE_NOTARY_PROFILE (a notarytool keychain profile) or all three of REDLINE_NOTARY_KEY_ID/_ISSUER/_KEY_PATH, and after the zip is built the script submits it, waits, and on Accepted staples Redline.app, rebuilds the zip and DMG from the stapled app (a new build_dmg() that ditto-copies whatever is already at .build/Redline.app rather than re-invoking make-dmg.sh, which would rebuild from source and strip both the resolved signature and the staple), staples the DMG, and requires `spctl -a -vv -t exec` to say "accepted" or the script aborts. Absent notary config: prints NOT NOTARIZED and continues exactly as before. appcast.json gains "notarized" and "teamIdentifier" (from codesign -dv); confirmed UpdateChecker's Appcast Decodable already ignores unknown JSON keys (verified with a standalone decode), so no app-side change was needed for old appcasts to keep working. Ends with a summary block: identity used, notarized yes/no, spctl verdict, team, sha256. --test dry-run behaviour (upload to .../test/, skip the git commit) is unchanged. Known gap, out of scope here: build-app.sh's own pre-sign step still hard- requires its hardcoded Apple Development identity in the keychain even when a Developer ID identity is what will actually ship — untouched per the task boundary (this file only). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+204
-29
@@ -13,7 +13,10 @@ PLISTBUDDY="/usr/libexec/PlistBuddy"
|
|||||||
REMOTE_HOST="mmd01"
|
REMOTE_HOST="mmd01"
|
||||||
REMOTE_BASE="/opt/mmd-installer-content/cowork/redline"
|
REMOTE_BASE="/opt/mmd-installer-content/cowork/redline"
|
||||||
PUBLIC_BASE="https://get.baobab-ts.com/cowork/redline"
|
PUBLIC_BASE="https://get.baobab-ts.com/cowork/redline"
|
||||||
SIGN_IDENTITY="Apple Development: ben@flow-master.ai (QH2H9G2LK5)"
|
BUNDLE_ID="ai.flowmaster.shotdeck"
|
||||||
|
# Used both as the fallback signing identity and as what build-app.sh itself
|
||||||
|
# still hardcodes for its own (pre-final) signing pass.
|
||||||
|
FALLBACK_SIGN_IDENTITY="Apple Development: ben@flow-master.ai (QH2H9G2LK5)"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: $0 <version> [\"notes\"]" >&2
|
echo "Usage: $0 <version> [\"notes\"]" >&2
|
||||||
@@ -72,6 +75,7 @@ else
|
|||||||
PUBLIC_DIR="${PUBLIC_BASE}"
|
PUBLIC_DIR="${PUBLIC_BASE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
APP_BUNDLE="${ROOT}/.build/Redline.app"
|
||||||
ZIP_NAME="Redline-${VERSION}.zip"
|
ZIP_NAME="Redline-${VERSION}.zip"
|
||||||
DMG_NAME="Redline-${VERSION}.dmg"
|
DMG_NAME="Redline-${VERSION}.dmg"
|
||||||
ZIP_PATH="${ROOT}/.build/${ZIP_NAME}"
|
ZIP_PATH="${ROOT}/.build/${ZIP_NAME}"
|
||||||
@@ -139,6 +143,38 @@ else
|
|||||||
echo "==> --test: skipping git commit of version bump"
|
echo "==> --test: skipping git commit of version bump"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Resolve the signing identity for the shipped artifacts -----------------
|
||||||
|
# REDLINE_KEYCHAIN (optional): a specific keychain to search/sign against,
|
||||||
|
# for hosts where the Developer ID identity does not live in the login
|
||||||
|
# keychain that codesign searches by default.
|
||||||
|
FIND_IDENTITY_ARGS=(-v -p codesigning)
|
||||||
|
CODESIGN_KEYCHAIN_ARGS=()
|
||||||
|
if [[ -n "${REDLINE_KEYCHAIN:-}" ]]; then
|
||||||
|
FIND_IDENTITY_ARGS+=("${REDLINE_KEYCHAIN}")
|
||||||
|
CODESIGN_KEYCHAIN_ARGS=(--keychain "${REDLINE_KEYCHAIN}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${REDLINE_SIGN_IDENTITY:-}" ]]; then
|
||||||
|
SIGN_IDENTITY="${REDLINE_SIGN_IDENTITY}"
|
||||||
|
echo "==> Signing identity: ${SIGN_IDENTITY} (REDLINE_SIGN_IDENTITY)"
|
||||||
|
else
|
||||||
|
DEVELOPER_ID_LINE="$(security find-identity "${FIND_IDENTITY_ARGS[@]}" 2>/dev/null \
|
||||||
|
| grep -o '"Developer ID Application:[^"]*"' | head -n1 || true)"
|
||||||
|
DEVELOPER_ID="${DEVELOPER_ID_LINE//\"/}"
|
||||||
|
if [[ -n "${DEVELOPER_ID}" ]]; then
|
||||||
|
SIGN_IDENTITY="${DEVELOPER_ID}"
|
||||||
|
echo "==> Signing identity: ${SIGN_IDENTITY} (auto-detected Developer ID Application)"
|
||||||
|
else
|
||||||
|
SIGN_IDENTITY="${FALLBACK_SIGN_IDENTITY}"
|
||||||
|
echo
|
||||||
|
echo "************************************************************************"
|
||||||
|
echo "WARNING: signing with Apple Development identity — not Developer ID;"
|
||||||
|
echo "Gatekeeper will block first install on other Macs."
|
||||||
|
echo "************************************************************************"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Restricted HOMEs (agent sandboxes) hide the login keychain from codesign.
|
# Restricted HOMEs (agent sandboxes) hide the login keychain from codesign.
|
||||||
# Re-run signed steps with the account's real home when the identity is missing.
|
# Re-run signed steps with the account's real home when the identity is missing.
|
||||||
signing_home() {
|
signing_home() {
|
||||||
@@ -167,31 +203,85 @@ run_signed() {
|
|||||||
echo "==> Building signed Redline.app"
|
echo "==> Building signed Redline.app"
|
||||||
run_signed ./scripts/build-app.sh
|
run_signed ./scripts/build-app.sh
|
||||||
|
|
||||||
if [[ ! -d "${ROOT}/.build/Redline.app" ]]; then
|
if [[ ! -d "${APP_BUNDLE}" ]]; then
|
||||||
echo "Signed app missing at ${ROOT}/.build/Redline.app" >&2
|
echo "Signed app missing at ${APP_BUNDLE}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# build-app.sh always signs with its own hardcoded Apple Development identity
|
||||||
|
# first (it has to — that identifier+identity pair is what keeps the Screen
|
||||||
|
# Recording grant alive). Re-sign here with the identity actually resolved
|
||||||
|
# above, which is what ships. A no-op when the two happen to be the same.
|
||||||
|
echo "==> Signing ${APP_BUNDLE} with resolved identity"
|
||||||
|
run_signed codesign --force --options runtime --timestamp \
|
||||||
|
"${CODESIGN_KEYCHAIN_ARGS[@]}" \
|
||||||
|
--sign "${SIGN_IDENTITY}" \
|
||||||
|
--identifier "${BUNDLE_ID}" \
|
||||||
|
"${APP_BUNDLE}"
|
||||||
|
|
||||||
|
build_zip() {
|
||||||
|
mkdir -p "${ROOT}/.build"
|
||||||
|
(
|
||||||
|
cd "${ROOT}/.build"
|
||||||
|
rm -f "${ZIP_NAME}"
|
||||||
|
ditto -c -k --keepParent Redline.app "${ZIP_NAME}"
|
||||||
|
)
|
||||||
|
if [[ ! -s "${ZIP_PATH}" ]]; then
|
||||||
|
echo "Zip was not created at ${ZIP_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rebuilds the manual-installer DMG from whatever is currently at
|
||||||
|
# ${APP_BUNDLE} — never re-invokes build-app.sh, so a prior custom signature
|
||||||
|
# or notarization staple on ${APP_BUNDLE} survives into the DMG untouched.
|
||||||
|
build_dmg() {
|
||||||
|
local staging="${ROOT}/.build/dmg-staging"
|
||||||
|
local mount_point="${ROOT}/.build/dmg-mnt"
|
||||||
|
|
||||||
|
rm -rf "${staging}"
|
||||||
|
mkdir -p "${staging}"
|
||||||
|
ditto "${APP_BUNDLE}" "${staging}/Redline.app"
|
||||||
|
ln -s /Applications "${staging}/Applications"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${DMG_PATH}")"
|
||||||
|
rm -f "${DMG_PATH}"
|
||||||
|
hdiutil create -volname "Redline" -srcfolder "${staging}" -ov -format UDZO "${DMG_PATH}"
|
||||||
|
|
||||||
|
if [[ -d "${mount_point}" ]] && /sbin/mount | grep -F -q "${mount_point}"; then
|
||||||
|
hdiutil detach "${mount_point}" || hdiutil detach "${mount_point}" -force
|
||||||
|
fi
|
||||||
|
rm -rf "${mount_point}"
|
||||||
|
mkdir -p "${mount_point}"
|
||||||
|
|
||||||
|
hdiutil attach "${DMG_PATH}" -nobrowse -readonly -mountpoint "${mount_point}"
|
||||||
|
|
||||||
|
local ok=1
|
||||||
|
if [[ ! -d "${mount_point}/Redline.app" ]]; then
|
||||||
|
echo "Verification failed: Redline.app missing from mounted DMG" >&2
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
if [[ "${ok}" -eq 1 && "$(readlink "${mount_point}/Applications" 2>/dev/null || true)" != "/Applications" ]]; then
|
||||||
|
echo "Verification failed: Applications does not point at /Applications" >&2
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
if [[ "${ok}" -eq 1 ]] && ! codesign --verify --deep --verbose=2 "${mount_point}/Redline.app"; then
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
hdiutil detach "${mount_point}" || hdiutil detach "${mount_point}" -force || true
|
||||||
|
|
||||||
|
if [[ "${ok}" -ne 1 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -s "${DMG_PATH}" ]]; then
|
||||||
|
echo "DMG was not created at ${DMG_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
echo "==> Zipping Redline.app -> ${ZIP_PATH}"
|
echo "==> Zipping Redline.app -> ${ZIP_PATH}"
|
||||||
mkdir -p "${ROOT}/.build"
|
build_zip
|
||||||
(
|
|
||||||
cd "${ROOT}/.build"
|
|
||||||
rm -f "${ZIP_NAME}"
|
|
||||||
ditto -c -k --keepParent Redline.app "${ZIP_NAME}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ ! -s "${ZIP_PATH}" ]]; then
|
|
||||||
echo "Zip was not created at ${ZIP_PATH}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> Building manual installer DMG"
|
|
||||||
run_signed ./scripts/make-dmg.sh
|
|
||||||
|
|
||||||
if [[ ! -s "${DMG_PATH}" ]]; then
|
|
||||||
echo "DMG was not created at ${DMG_PATH}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SHA256="$(shasum -a 256 "${ZIP_PATH}" | awk '{print $1}')"
|
SHA256="$(shasum -a 256 "${ZIP_PATH}" | awk '{print $1}')"
|
||||||
ZIP_BYTES="$(stat -f%z "${ZIP_PATH}")"
|
ZIP_BYTES="$(stat -f%z "${ZIP_PATH}")"
|
||||||
@@ -200,18 +290,95 @@ PUBDATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|||||||
echo "==> Zip SHA256: ${SHA256}"
|
echo "==> Zip SHA256: ${SHA256}"
|
||||||
echo " Zip bytes: ${ZIP_BYTES}"
|
echo " Zip bytes: ${ZIP_BYTES}"
|
||||||
|
|
||||||
|
# --- Notarization (optional) -------------------------------------------------
|
||||||
|
# Either REDLINE_NOTARY_PROFILE (a `notarytool store-credentials` keychain
|
||||||
|
# profile) or all three of REDLINE_NOTARY_KEY_ID / REDLINE_NOTARY_ISSUER /
|
||||||
|
# REDLINE_NOTARY_KEY_PATH (App Store Connect API key). Absent both: skip.
|
||||||
|
NOTARIZED=0
|
||||||
|
NOTARY_CONFIGURED=0
|
||||||
|
if [[ -n "${REDLINE_NOTARY_PROFILE:-}" ]]; then
|
||||||
|
NOTARY_CONFIGURED=1
|
||||||
|
elif [[ -n "${REDLINE_NOTARY_KEY_ID:-}" && -n "${REDLINE_NOTARY_ISSUER:-}" && -n "${REDLINE_NOTARY_KEY_PATH:-}" ]]; then
|
||||||
|
NOTARY_CONFIGURED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${NOTARY_CONFIGURED}" -eq 1 ]]; then
|
||||||
|
echo "==> Submitting ${ZIP_PATH} to notarytool"
|
||||||
|
NOTARY_ARGS=(xcrun notarytool submit "${ZIP_PATH}" --wait --timeout 30m)
|
||||||
|
if [[ -n "${REDLINE_NOTARY_PROFILE:-}" ]]; then
|
||||||
|
NOTARY_ARGS+=(--keychain-profile "${REDLINE_NOTARY_PROFILE}")
|
||||||
|
else
|
||||||
|
NOTARY_ARGS+=(
|
||||||
|
--key "${REDLINE_NOTARY_KEY_PATH}"
|
||||||
|
--key-id "${REDLINE_NOTARY_KEY_ID}"
|
||||||
|
--issuer "${REDLINE_NOTARY_ISSUER}"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +e
|
||||||
|
NOTARY_OUTPUT="$("${NOTARY_ARGS[@]}" 2>&1)"
|
||||||
|
NOTARY_STATUS=$?
|
||||||
|
set -e
|
||||||
|
echo "${NOTARY_OUTPUT}"
|
||||||
|
if [[ "${NOTARY_STATUS}" -ne 0 ]]; then
|
||||||
|
echo "notarytool submit failed (exit ${NOTARY_STATUS})." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -qi 'status: *Accepted' <<<"${NOTARY_OUTPUT}"; then
|
||||||
|
echo "notarytool did not report Accepted." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NOTARIZED=1
|
||||||
|
|
||||||
|
echo "==> Stapling ${APP_BUNDLE}"
|
||||||
|
xcrun stapler staple "${APP_BUNDLE}"
|
||||||
|
|
||||||
|
echo "==> Rebuilding zip from the stapled app"
|
||||||
|
build_zip
|
||||||
|
SHA256="$(shasum -a 256 "${ZIP_PATH}" | awk '{print $1}')"
|
||||||
|
ZIP_BYTES="$(stat -f%z "${ZIP_PATH}")"
|
||||||
|
echo " Zip SHA256: ${SHA256}"
|
||||||
|
echo " Zip bytes: ${ZIP_BYTES}"
|
||||||
|
|
||||||
|
echo "==> Rebuilding DMG from the stapled app"
|
||||||
|
build_dmg
|
||||||
|
|
||||||
|
echo "==> Stapling ${DMG_PATH}"
|
||||||
|
xcrun stapler staple "${DMG_PATH}"
|
||||||
|
else
|
||||||
|
echo "==> REDLINE_NOTARY_KEY_ID/ISSUER/KEY_PATH (or REDLINE_NOTARY_PROFILE) not set"
|
||||||
|
echo "NOT NOTARIZED"
|
||||||
|
echo "==> Building manual installer DMG"
|
||||||
|
build_dmg
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Gatekeeper check: spctl -a -vv -t exec ${APP_BUNDLE}"
|
||||||
|
set +e
|
||||||
|
SPCTL_OUTPUT="$(spctl -a -vv -t exec "${APP_BUNDLE}" 2>&1)"
|
||||||
|
SPCTL_STATUS=$?
|
||||||
|
set -e
|
||||||
|
echo "${SPCTL_OUTPUT}"
|
||||||
|
if [[ "${SPCTL_STATUS}" -ne 0 ]] || ! grep -qi 'accepted' <<<"${SPCTL_OUTPUT}"; then
|
||||||
|
echo "spctl did not report accepted for ${APP_BUNDLE}." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TEAM_IDENTIFIER="$(codesign -dv "${APP_BUNDLE}" 2>&1 | awk -F= '/^TeamIdentifier=/{print $2}')"
|
||||||
|
|
||||||
echo "==> Writing ${APPCAST_PATH}"
|
echo "==> Writing ${APPCAST_PATH}"
|
||||||
python3 - "${VERSION}" "${ZIP_URL}" "${SHA256}" "${NOTES}" "${PUBDATE}" "${APPCAST_PATH}" <<'PY'
|
python3 - "${VERSION}" "${ZIP_URL}" "${SHA256}" "${NOTES}" "${PUBDATE}" "${APPCAST_PATH}" "${NOTARIZED}" "${TEAM_IDENTIFIER}" <<'PY'
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
version, zip_url, sha256, notes, pub_date, out_path = sys.argv[1:]
|
version, zip_url, sha256, notes, pub_date, out_path, notarized, team_identifier = sys.argv[1:]
|
||||||
payload = {
|
payload = {
|
||||||
"version": version,
|
"version": version,
|
||||||
"zipURL": zip_url,
|
"zipURL": zip_url,
|
||||||
"sha256": sha256,
|
"sha256": sha256,
|
||||||
"notes": notes,
|
"notes": notes,
|
||||||
"pubDate": pub_date,
|
"pubDate": pub_date,
|
||||||
|
"notarized": notarized == "1",
|
||||||
|
"teamIdentifier": team_identifier,
|
||||||
}
|
}
|
||||||
with open(out_path, "w", encoding="utf-8") as fh:
|
with open(out_path, "w", encoding="utf-8") as fh:
|
||||||
json.dump(payload, fh, indent=2)
|
json.dump(payload, fh, indent=2)
|
||||||
@@ -280,8 +447,16 @@ fi
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Published v${VERSION}"
|
echo "Published v${VERSION}"
|
||||||
echo " appcast: ${APPCAST_URL}"
|
echo " identity: ${SIGN_IDENTITY}"
|
||||||
echo " zip: ${ZIP_URL}"
|
if [[ "${NOTARIZED}" -eq 1 ]]; then
|
||||||
echo " sha256: ${SHA256}"
|
echo " notarized: yes"
|
||||||
echo " dmg: ${PUBLIC_DIR}/${DMG_NAME}"
|
else
|
||||||
echo " dmg: ${PUBLIC_DIR}/Redline.dmg"
|
echo " notarized: no"
|
||||||
|
fi
|
||||||
|
echo " spctl: ${SPCTL_OUTPUT}"
|
||||||
|
echo " team: ${TEAM_IDENTIFIER}"
|
||||||
|
echo " appcast: ${APPCAST_URL}"
|
||||||
|
echo " zip: ${ZIP_URL}"
|
||||||
|
echo " sha256: ${SHA256}"
|
||||||
|
echo " dmg: ${PUBLIC_DIR}/${DMG_NAME}"
|
||||||
|
echo " dmg: ${PUBLIC_DIR}/Redline.dmg"
|
||||||
|
|||||||
Reference in New Issue
Block a user