mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-20 20:15:54 +00:00
* feat(menubar): CI Developer ID signing, team-pinned installs, update feed - release-menubar.yml imports the Developer ID cert, signs with hardened runtime and timestamp, notarizes with an App Store Connect key, staples, and fails when the signing secrets are missing - the installer refuses bundles not signed by team XRVP7P7F9M or rejected by Gatekeeper - a rolling update-feeds prerelease carries menubar-latest.json, written last after the published zip is checked; installer and update check read it before scanning mac-v releases - the update pill skips the CLI step for the desktop app's bundled CLI or a CLI with no package manager, explains npm permission errors, and allows longer update timeouts - RELEASING.md matches what CI does and lists the required secrets * docs(menubar): DISTRIBUTION.md points at CI signing; retry the feed's asset check * fix(menubar): popover shows only the Update button; command footer appears only after a failed update
209 lines
8.3 KiB
Bash
Executable file
209 lines
8.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Builds a universal CodeBurnMenubar.app bundle from the SwiftPM target and drops a
|
|
# distributable zip alongside. Used by the GitHub release workflow; also runnable locally.
|
|
#
|
|
# Usage:
|
|
# mac/Scripts/package-app.sh [<version>]
|
|
# Defaults to `dev` if no version is given.
|
|
#
|
|
# Release builds set CODESIGN_IDENTITY (Developer ID Application) plus NOTARY_KEY_PATH,
|
|
# NOTARY_KEY_ID and NOTARY_ISSUER_ID (App Store Connect API key) to notarize and staple
|
|
# the app before it is zipped.
|
|
|
|
set -euo pipefail
|
|
|
|
VERSION="${1:-dev}"
|
|
ASSET_VERSION="${VERSION#mac-}"
|
|
BUNDLE_VERSION="${ASSET_VERSION#v}"
|
|
BUNDLE_NAME="CodeBurnMenubar.app"
|
|
BUNDLE_ID="org.agentseal.codeburn-menubar"
|
|
EXECUTABLE_NAME="CodeBurnMenubar"
|
|
MIN_MACOS="14.0"
|
|
|
|
repo_root() {
|
|
git rev-parse --show-toplevel 2>/dev/null || (cd "$(dirname "$0")/../.." && pwd)
|
|
}
|
|
|
|
ROOT=$(repo_root)
|
|
MAC_DIR="${ROOT}/mac"
|
|
DIST_DIR="${MAC_DIR}/.build/dist"
|
|
ICON_SOURCE="${ROOT}/assets/menubar-logo.png"
|
|
|
|
cd "${MAC_DIR}"
|
|
|
|
echo "▸ Cleaning previous dist..."
|
|
rm -rf "${DIST_DIR}"
|
|
mkdir -p "${DIST_DIR}"
|
|
|
|
echo "▸ Building universal binary (arm64 + x86_64)..."
|
|
swift build -c release --arch arm64 --arch x86_64
|
|
|
|
BIN_PATH=$(swift build -c release --arch arm64 --arch x86_64 --show-bin-path)
|
|
BUILT_BINARY="${BIN_PATH}/${EXECUTABLE_NAME}"
|
|
if [[ ! -x "${BUILT_BINARY}" ]]; then
|
|
echo "Binary not found at ${BUILT_BINARY}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "▸ Assembling ${BUNDLE_NAME}..."
|
|
BUNDLE="${DIST_DIR}/${BUNDLE_NAME}"
|
|
mkdir -p "${BUNDLE}/Contents/MacOS"
|
|
mkdir -p "${BUNDLE}/Contents/Resources"
|
|
cp "${BUILT_BINARY}" "${BUNDLE}/Contents/MacOS/${EXECUTABLE_NAME}"
|
|
cp "${ICON_SOURCE}" "${BUNDLE}/Contents/Resources/menubar-logo.png"
|
|
cp "${ROOT}/LICENSE" "${BUNDLE}/Contents/Resources/LICENSE.txt"
|
|
cp "${ROOT}/THIRD_PARTY_NOTICES.md" "${BUNDLE}/Contents/Resources/THIRD_PARTY_NOTICES.md"
|
|
|
|
# SwiftPM emits target resources as a sibling bundle; Bundle.module traps at
|
|
# runtime if it is missing from Contents/Resources, so ship it when present.
|
|
SPM_RESOURCE_BUNDLE="${BIN_PATH}/${EXECUTABLE_NAME}_${EXECUTABLE_NAME}.bundle"
|
|
if [[ -d "${SPM_RESOURCE_BUNDLE}" ]]; then
|
|
cp -R "${SPM_RESOURCE_BUNDLE}" "${BUNDLE}/Contents/Resources/"
|
|
fi
|
|
|
|
ICONSET="${DIST_DIR}/AppIcon.iconset"
|
|
rm -rf "${ICONSET}"
|
|
mkdir -p "${ICONSET}"
|
|
sips -z 16 16 "${ICON_SOURCE}" --out "${ICONSET}/icon_16x16.png" >/dev/null
|
|
sips -z 32 32 "${ICON_SOURCE}" --out "${ICONSET}/icon_16x16@2x.png" >/dev/null
|
|
sips -z 32 32 "${ICON_SOURCE}" --out "${ICONSET}/icon_32x32.png" >/dev/null
|
|
sips -z 64 64 "${ICON_SOURCE}" --out "${ICONSET}/icon_32x32@2x.png" >/dev/null
|
|
sips -z 128 128 "${ICON_SOURCE}" --out "${ICONSET}/icon_128x128.png" >/dev/null
|
|
sips -z 256 256 "${ICON_SOURCE}" --out "${ICONSET}/icon_128x128@2x.png" >/dev/null
|
|
sips -z 256 256 "${ICON_SOURCE}" --out "${ICONSET}/icon_256x256.png" >/dev/null
|
|
sips -z 512 512 "${ICON_SOURCE}" --out "${ICONSET}/icon_256x256@2x.png" >/dev/null
|
|
sips -z 512 512 "${ICON_SOURCE}" --out "${ICONSET}/icon_512x512.png" >/dev/null
|
|
cp "${ICON_SOURCE}" "${ICONSET}/icon_512x512@2x.png"
|
|
iconutil -c icns "${ICONSET}" -o "${BUNDLE}/Contents/Resources/AppIcon.icns"
|
|
rm -rf "${ICONSET}"
|
|
|
|
cat > "${BUNDLE}/Contents/Info.plist" <<PLIST
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>CFBundleDevelopmentRegion</key>
|
|
<string>en</string>
|
|
<!-- Advertises the localizations shipped in the SwiftPM resource bundle
|
|
(Sources/CodeBurnMenubar/Resources/<locale>.lproj). Without this the
|
|
app does not appear under System Settings > Language & Region > per-app
|
|
language, so users could not override the system language for it. -->
|
|
<key>CFBundleLocalizations</key>
|
|
<array>
|
|
<string>en</string>
|
|
<string>fr</string>
|
|
<string>ja</string>
|
|
<string>ko</string>
|
|
<string>zh-Hans</string>
|
|
<string>zh-Hant</string>
|
|
</array>
|
|
<key>CFBundleDisplayName</key>
|
|
<string>CodeBurn Menubar</string>
|
|
<key>CFBundleExecutable</key>
|
|
<string>${EXECUTABLE_NAME}</string>
|
|
<key>CFBundleIconFile</key>
|
|
<string>AppIcon</string>
|
|
<key>CFBundleIdentifier</key>
|
|
<string>${BUNDLE_ID}</string>
|
|
<key>CFBundleInfoDictionaryVersion</key>
|
|
<string>6.0</string>
|
|
<key>CFBundleName</key>
|
|
<string>${EXECUTABLE_NAME}</string>
|
|
<key>CFBundlePackageType</key>
|
|
<string>APPL</string>
|
|
<key>CFBundleShortVersionString</key>
|
|
<string>${BUNDLE_VERSION}</string>
|
|
<key>CFBundleVersion</key>
|
|
<string>${BUNDLE_VERSION}</string>
|
|
<key>LSMinimumSystemVersion</key>
|
|
<string>${MIN_MACOS}</string>
|
|
<key>LSUIElement</key>
|
|
<true/>
|
|
<key>NSHighResolutionCapable</key>
|
|
<true/>
|
|
<key>NSHumanReadableCopyright</key>
|
|
<string>© AgentSeal</string>
|
|
</dict>
|
|
</plist>
|
|
PLIST
|
|
|
|
cat > "${BUNDLE}/Contents/PkgInfo" <<'PKG'
|
|
APPL????
|
|
PKG
|
|
|
|
# Sign so macOS treats the bundle as internally consistent. Set CODESIGN_IDENTITY
|
|
# to a stable identity (Developer ID Application for distribution, or an Apple
|
|
# Development cert for local testing) so the TCC "access data from other apps"
|
|
# grant persists across rebuilds. Falls back to ad-hoc when unset (e.g. CI), which
|
|
# re-prompts on every build because each ad-hoc build has a fresh code identity.
|
|
CODESIGN_IDENTITY="${CODESIGN_IDENTITY:-}"
|
|
if [[ -n "${CODESIGN_IDENTITY}" ]]; then
|
|
echo "▸ Signing with identity: ${CODESIGN_IDENTITY}"
|
|
# --deep is enough: the only nested item is the resources-only SwiftPM bundle, no nested Mach-O.
|
|
codesign --force --sign "${CODESIGN_IDENTITY}" --options runtime --timestamp --deep "${BUNDLE}"
|
|
else
|
|
echo "▸ Ad-hoc signing (set CODESIGN_IDENTITY for a persistent TCC grant)..."
|
|
codesign --force --sign - --timestamp=none --deep "${BUNDLE}"
|
|
fi
|
|
codesign --verify --deep --strict "${BUNDLE}"
|
|
|
|
echo "▸ Verifying deployment target and libswift_errno absence..."
|
|
BUILT_EXE="${BUNDLE}/Contents/MacOS/${EXECUTABLE_NAME}"
|
|
MINOS_LINES=$(vtool -show-build "${BUILT_EXE}" 2>/dev/null | awk '/minos/{print $2}')
|
|
if [[ -z "${MINOS_LINES}" ]]; then
|
|
echo "Could not read a minos deployment target from ${BUILT_EXE} (vtool returned nothing)." >&2
|
|
exit 1
|
|
fi
|
|
BAD_MINOS=$(printf '%s\n' "${MINOS_LINES}" | grep -v '^14\.0$' || true)
|
|
if [[ -n "${BAD_MINOS}" ]]; then
|
|
echo "✗ Expected minos 14.0 for every arch slice, found: ${BAD_MINOS}" >&2
|
|
echo " Did Package.swift's platforms: [.macOS(...)] regress past .v14?" >&2
|
|
exit 1
|
|
fi
|
|
if otool -L "${BUILT_EXE}" | grep libswift_errno | grep -qv 'weak'; then
|
|
echo "✗ ${BUILT_EXE} links libswift_errno.dylib (macOS 15+ only), would fail on Sonoma with -10825." >&2
|
|
exit 1
|
|
fi
|
|
echo " minos 14.0 confirmed, no libswift_errno dependency."
|
|
|
|
if [[ -n "${NOTARY_KEY_PATH:-}" ]]; then
|
|
if [[ -z "${CODESIGN_IDENTITY}" || -z "${NOTARY_KEY_ID:-}" || -z "${NOTARY_ISSUER_ID:-}" ]]; then
|
|
echo "✗ NOTARY_KEY_PATH also needs CODESIGN_IDENTITY, NOTARY_KEY_ID and NOTARY_ISSUER_ID." >&2
|
|
exit 1
|
|
fi
|
|
NOTARY_AUTH=(--key "${NOTARY_KEY_PATH}" --key-id "${NOTARY_KEY_ID}" --issuer "${NOTARY_ISSUER_ID}")
|
|
NOTARY_ZIP="${DIST_DIR}/notarize-upload.zip"
|
|
echo "▸ Notarizing (usually a few minutes)..."
|
|
(cd "${DIST_DIR}" && /usr/bin/ditto -c -k --keepParent "${BUNDLE_NAME}" "$(basename "${NOTARY_ZIP}")")
|
|
NOTARY_RESULT=$(xcrun notarytool submit "${NOTARY_ZIP}" "${NOTARY_AUTH[@]}" --wait --output-format json || true)
|
|
rm -f "${NOTARY_ZIP}"
|
|
echo "${NOTARY_RESULT}"
|
|
NOTARY_STATUS=$(jq -r '.status // empty' <<< "${NOTARY_RESULT}" 2>/dev/null || true)
|
|
if [[ "${NOTARY_STATUS}" != "Accepted" ]]; then
|
|
NOTARY_ID=$(jq -r '.id // empty' <<< "${NOTARY_RESULT}" 2>/dev/null || true)
|
|
if [[ -n "${NOTARY_ID}" ]]; then
|
|
xcrun notarytool log "${NOTARY_ID}" "${NOTARY_AUTH[@]}" || true
|
|
fi
|
|
echo "✗ Notarization status: ${NOTARY_STATUS:-unknown}" >&2
|
|
exit 1
|
|
fi
|
|
xcrun stapler staple "${BUNDLE}"
|
|
xcrun stapler validate "${BUNDLE}"
|
|
spctl --assess --type execute -vv "${BUNDLE}"
|
|
fi
|
|
|
|
ZIP_NAME="CodeBurnMenubar-${ASSET_VERSION}.zip"
|
|
ZIP_PATH="${DIST_DIR}/${ZIP_NAME}"
|
|
echo "▸ Packaging ${ZIP_NAME}..."
|
|
(cd "${DIST_DIR}" && COPYFILE_DISABLE=1 /usr/bin/ditto -c -k --norsrc --keepParent "${BUNDLE_NAME}" "${ZIP_NAME}")
|
|
|
|
CHECKSUM_NAME="${ZIP_NAME}.sha256"
|
|
CHECKSUM_PATH="${DIST_DIR}/${CHECKSUM_NAME}"
|
|
echo "▸ Computing SHA-256 checksum..."
|
|
(cd "${DIST_DIR}" && shasum -a 256 "${ZIP_NAME}" > "${CHECKSUM_NAME}")
|
|
|
|
echo ""
|
|
echo "✓ Built ${ZIP_PATH}"
|
|
echo "✓ Checksum ${CHECKSUM_PATH}"
|
|
cat "${CHECKSUM_PATH}"
|
|
ls -la "${DIST_DIR}"
|