From e44ec24b5ac9b1730439fe57395bcc2492407ea6 Mon Sep 17 00:00:00 2001 From: LMZ Date: Mon, 30 Mar 2026 16:47:12 +0800 Subject: [PATCH] feat: harden releases and add diagnostics --- .github/workflows/ci.yml | 5 ++ RELEASING.md | 9 +- Sources/ProcessBarMonitor/Diagnostics.swift | 40 +++++++++ .../ProcessBarMonitor/MonitorViewModel.swift | 78 ++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 3 + .../zh-Hans.lproj/Localizable.strings | 3 + Sources/ProcessBarMonitor/Views.swift | 4 + build_app.sh | 88 ++++++++++++++++--- release.sh | 73 +++++++++++++-- scripts/validate_app_bundle.sh | 62 +++++++++++++ 10 files changed, 344 insertions(+), 21 deletions(-) create mode 100644 Sources/ProcessBarMonitor/Diagnostics.swift create mode 100755 scripts/validate_app_bundle.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61221be..cb526a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,10 +25,14 @@ jobs: - name: Build app bundle run: ./build_app.sh + - name: Validate app bundle + run: ./scripts/validate_app_bundle.sh dist/ProcessBarMonitor.app + - name: Archive app bundle run: | mkdir -p release ditto -c -k --sequesterRsrc --keepParent dist/ProcessBarMonitor.app release/ProcessBarMonitor-ci-macOS.zip + shasum -a 256 release/ProcessBarMonitor-ci-macOS.zip > release/ProcessBarMonitor-ci-SHA256SUMS.txt - name: Upload app artifact uses: actions/upload-artifact@v4 @@ -37,6 +41,7 @@ jobs: path: | dist/ProcessBarMonitor.app release/ProcessBarMonitor-ci-macOS.zip + release/ProcessBarMonitor-ci-SHA256SUMS.txt release-validation: if: startsWith(github.ref, 'refs/tags/v') diff --git a/RELEASING.md b/RELEASING.md index 821eb34..c1a5eae 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -21,12 +21,14 @@ Examples: ## What the script does -- builds the app bundle +- builds the app bundle in `release` configuration - stamps version/build into `Info.plist` +- validates the generated `.app` bundle contents - zips `dist/ProcessBarMonitor.app` +- writes `SHA256SUMS` for the zip - creates or updates the Git tag - creates or updates the GitHub Release -- uploads the zipped release asset +- uploads the zipped release asset and checksum file - marks the release as latest ## Preconditions @@ -34,9 +36,12 @@ Examples: - working tree must be clean - `gh` must be installed and authenticated - you need permission to push tags and create releases for the repo +- default release branch is `main` (override intentionally with `ALLOW_NON_MAIN_RELEASE=1`) ## Notes - release assets are written to `release/` - release notes are generated into `release/release-notes-vX.Y.Z.md` +- checksum file is generated as `release/ProcessBarMonitor-vX.Y.Z-SHA256SUMS.txt` - current release helper script: `release.sh` +- bundle validation helper: `scripts/validate_app_bundle.sh` diff --git a/Sources/ProcessBarMonitor/Diagnostics.swift b/Sources/ProcessBarMonitor/Diagnostics.swift new file mode 100644 index 0000000..9677e47 --- /dev/null +++ b/Sources/ProcessBarMonitor/Diagnostics.swift @@ -0,0 +1,40 @@ +import Foundation + +struct ProcessSnapshotDiagnostics { + var attemptCount: Int = 0 + var successCount: Int = 0 + var failureCount: Int = 0 + var lastAttemptAt: Date? + var lastSuccessAt: Date? + var lastFailureAt: Date? + var lastFailureMessage: String? + var lastFailureDetails: String? + var lastSnapshotProcessCount: Int = 0 + var lastTopCPUCount: Int = 0 + var lastTopMemoryCount: Int = 0 + + mutating func markAttempt(at date: Date = Date()) { + attemptCount += 1 + lastAttemptAt = date + } + + mutating func markSuccess( + processCount: Int, + topCPUCount: Int, + topMemoryCount: Int, + at date: Date = Date() + ) { + successCount += 1 + lastSuccessAt = date + lastSnapshotProcessCount = processCount + lastTopCPUCount = topCPUCount + lastTopMemoryCount = topMemoryCount + } + + mutating func markFailure(message: String, details: String, at date: Date = Date()) { + failureCount += 1 + lastFailureAt = date + lastFailureMessage = message + lastFailureDetails = details + } +} diff --git a/Sources/ProcessBarMonitor/MonitorViewModel.swift b/Sources/ProcessBarMonitor/MonitorViewModel.swift index 792d9d4..a073c7a 100644 --- a/Sources/ProcessBarMonitor/MonitorViewModel.swift +++ b/Sources/ProcessBarMonitor/MonitorViewModel.swift @@ -1,5 +1,7 @@ import Foundation import Combine +import AppKit +import OSLog private struct SettingsStore { let defaults: UserDefaults @@ -33,6 +35,7 @@ final class MonitorViewModel: ObservableObject { @Published private(set) var temperatureHistory: [MetricPoint] = [] @Published private(set) var isMenuExpanded = false @Published var statusMessage: String? + @Published private(set) var processDiagnostics = ProcessSnapshotDiagnostics() @Published var searchText = "" @Published var processLimit: Int { @@ -48,8 +51,11 @@ final class MonitorViewModel: ObservableObject { private let metricsProvider = SystemMetricsProvider() private let processProvider = ProcessSnapshotProvider() private let settings: SettingsStore + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "ai.openclaw.ProcessBarMonitor", category: "process-snapshot") private var refreshTask: Task? private var lastProcessRefresh = Date.distantPast + private var lastLoggedFailureSignature: String? + private var lastLoggedFailureAt = Date.distantPast private enum Keys { static let processLimit = "processLimit" @@ -142,13 +148,23 @@ final class MonitorViewModel: ObservableObject { appendHistory(cpu: snapshotSummary.cpuPercent, memory: snapshotSummary.memoryPressurePercent, temperature: snapshotSummary.cpuTemperatureC) if let processResult = await processTask { + processDiagnostics.markAttempt() switch processResult { case .success(let processes): allProcesses = processes lastProcessRefresh = Date() recomputeVisibleProcesses() + processDiagnostics.markSuccess( + processCount: processes.count, + topCPUCount: topCPUProcesses.count, + topMemoryCount: topMemoryProcesses.count + ) statusMessage = nil case .failure(let error): + let message = error.localizedDescription + let details = String(describing: error) + processDiagnostics.markFailure(message: message, details: details) + logProcessSnapshotFailureIfNeeded(message: message, details: details) statusMessage = L10n.format("status.failed_to_load_top_apps", error.localizedDescription) } } @@ -207,6 +223,17 @@ final class MonitorViewModel: ObservableObject { statusMessage = nil } + func copyDiagnosticsToPasteboard() { + let report = diagnosticsReport() + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + if pasteboard.setString(report, forType: .string) { + statusMessage = L10n.string("status.diagnostics_copied") + } else { + statusMessage = L10n.string("status.diagnostics_copy_failed") + } + } + func thermalText(_ state: ProcessInfo.ThermalState) -> String { switch state { case .nominal: return L10n.string("thermal.nominal") @@ -216,4 +243,55 @@ final class MonitorViewModel: ObservableObject { @unknown default: return L10n.string("thermal.unknown") } } + + private func logProcessSnapshotFailureIfNeeded(message: String, details: String) { + let signature = "\(message)|\(details)" + let now = Date() + let minInterval: TimeInterval = 60 + + if signature == lastLoggedFailureSignature, now.timeIntervalSince(lastLoggedFailureAt) < minInterval { + return + } + + lastLoggedFailureSignature = signature + lastLoggedFailureAt = now + logger.error("Process snapshot failed. attempts=\(self.processDiagnostics.attemptCount, privacy: .public) failures=\(self.processDiagnostics.failureCount, privacy: .public) message=\(message, privacy: .public) details=\(details, privacy: .public)") + } + + private func diagnosticsReport() -> String { + let info = Bundle.main.infoDictionary ?? [:] + let appVersion = (info["CFBundleShortVersionString"] as? String) ?? "unknown" + let appBuild = (info["CFBundleVersion"] as? String) ?? "unknown" + let osVersion = ProcessInfo.processInfo.operatingSystemVersionString + let iso = ISO8601DateFormatter() + iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let now = iso.string(from: Date()) + + func format(_ date: Date?) -> String { + guard let date else { return "n/a" } + return iso.string(from: date) + } + + return """ + ProcessBarMonitor Diagnostics + generated_at: \(now) + app_version: \(appVersion) + app_build: \(appBuild) + os: \(osVersion) + process_snapshot_attempts: \(processDiagnostics.attemptCount) + process_snapshot_successes: \(processDiagnostics.successCount) + process_snapshot_failures: \(processDiagnostics.failureCount) + process_snapshot_last_attempt: \(format(processDiagnostics.lastAttemptAt)) + process_snapshot_last_success: \(format(processDiagnostics.lastSuccessAt)) + process_snapshot_last_failure: \(format(processDiagnostics.lastFailureAt)) + process_snapshot_last_process_count: \(processDiagnostics.lastSnapshotProcessCount) + top_cpu_rows: \(processDiagnostics.lastTopCPUCount) + top_memory_rows: \(processDiagnostics.lastTopMemoryCount) + process_snapshot_last_error: \(processDiagnostics.lastFailureMessage ?? "n/a") + process_snapshot_last_error_detail: \(processDiagnostics.lastFailureDetails ?? "n/a") + status_message: \(statusMessage ?? "n/a") + menu_expanded: \(isMenuExpanded) + search_text: \(searchText) + """ + } } diff --git a/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings b/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings index a2c9c13..da71afa 100644 --- a/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings +++ b/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings @@ -57,3 +57,6 @@ "note.temperature.arm64_unavailable" = "Apple Silicon HID sensors were checked for %@, but no valid value was exposed right now."; "note.temperature.intel_hint" = "Intel: install osx-cpu-temp or istats to show a real CPU temperature."; "note.temperature.no_source" = "No valid CPU temperature source found."; +"button.copy_diagnostics" = "Copy Diagnostics"; +"status.diagnostics_copied" = "Diagnostics copied to clipboard."; +"status.diagnostics_copy_failed" = "Failed to copy diagnostics."; diff --git a/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings b/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings index 0734318..9643c4b 100644 --- a/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings @@ -57,3 +57,6 @@ "note.temperature.arm64_unavailable" = "已检查 %@ 的 Apple Silicon HID 传感器,但当前未返回有效值。"; "note.temperature.intel_hint" = "Intel 机型:安装 osx-cpu-temp 或 istats 以显示真实 CPU 温度。"; "note.temperature.no_source" = "未找到有效的 CPU 温度来源。"; +"button.copy_diagnostics" = "复制诊断信息"; +"status.diagnostics_copied" = "诊断信息已复制到剪贴板。"; +"status.diagnostics_copy_failed" = "复制诊断信息失败。"; diff --git a/Sources/ProcessBarMonitor/Views.swift b/Sources/ProcessBarMonitor/Views.swift index deab2f8..51ee9a6 100644 --- a/Sources/ProcessBarMonitor/Views.swift +++ b/Sources/ProcessBarMonitor/Views.swift @@ -314,6 +314,10 @@ struct MenuBarContentView: View { } .disabled(viewModel.isRefreshing) + Button(L10n.string("button.copy_diagnostics")) { + viewModel.copyDiagnosticsToPasteboard() + } + Button(L10n.string("button.quit")) { quitApplication() } diff --git a/build_app.sh b/build_app.sh index cc8fd16..f52a275 100755 --- a/build_app.sh +++ b/build_app.sh @@ -3,25 +3,92 @@ set -euo pipefail APP_NAME="ProcessBarMonitor" BUNDLE_ID="ai.openclaw.ProcessBarMonitor" -VERSION="${1:-1.0.0}" -BUILD_NUMBER="${2:-1}" ROOT="$(cd "$(dirname "$0")" && pwd)" APP_DIR="$ROOT/dist/$APP_NAME.app" MACOS_DIR="$APP_DIR/Contents/MacOS" RESOURCES_DIR="$APP_DIR/Contents/Resources" -BUILD_DIR="$ROOT/.build/arm64-apple-macosx/debug" -RESOURCE_BUNDLE="$BUILD_DIR/${APP_NAME}_${APP_NAME}.bundle" +VERSION="1.0.0" +BUILD_NUMBER="1" +CONFIGURATION="${CONFIGURATION:-release}" +CLEAN_BUILD=0 + +usage() { + cat <] [--clean] + +Examples: + ./build_app.sh + ./build_app.sh 1.0.2 3 + ./build_app.sh 1.0.2 3 --configuration debug +EOF +} + +POSITIONAL=() +while [[ $# -gt 0 ]]; do + case "$1" in + --configuration) + [[ $# -ge 2 ]] || { echo "--configuration requires a value"; exit 2; } + CONFIGURATION="$2" + shift 2 + ;; + --configuration=*) + CONFIGURATION="${1#*=}" + shift + ;; + --clean) + CLEAN_BUILD=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + echo "Unknown option: $1" + usage + exit 2 + ;; + *) + POSITIONAL+=("$1") + shift + ;; + esac +done + +if [[ ${#POSITIONAL[@]} -ge 1 ]]; then VERSION="${POSITIONAL[1]}"; fi +if [[ ${#POSITIONAL[@]} -ge 2 ]]; then BUILD_NUMBER="${POSITIONAL[2]}"; fi +if [[ ${#POSITIONAL[@]} -gt 2 ]]; then + echo "Too many positional arguments." + usage + exit 2 +fi + +if [[ "$CONFIGURATION" != "release" && "$CONFIGURATION" != "debug" ]]; then + echo "Invalid configuration: $CONFIGURATION (expected: release or debug)" + exit 2 +fi cd "$ROOT" +if [[ "$CLEAN_BUILD" -eq 1 ]]; then + rm -rf "$ROOT/dist" +fi + swift scripts/generate_icon.swift -swift build +swift build --configuration "$CONFIGURATION" + +BIN_DIR="$(swift build --configuration "$CONFIGURATION" --show-bin-path)" +EXECUTABLE_PATH="$BIN_DIR/$APP_NAME" +RESOURCE_BUNDLE="$(find "$BIN_DIR" -maxdepth 1 -type d -name "*_${APP_NAME}.bundle" | head -n 1)" + +[[ -x "$EXECUTABLE_PATH" ]] || { echo "Missing built executable: $EXECUTABLE_PATH"; exit 1; } +[[ -f "$ROOT/Resources/ProcessBarMonitor.icns" ]] || { echo "Missing icon file."; exit 1; } +[[ -n "$RESOURCE_BUNDLE" ]] || { echo "Missing SwiftPM resource bundle in $BIN_DIR"; exit 1; } + rm -rf "$APP_DIR" mkdir -p "$MACOS_DIR" "$RESOURCES_DIR" -cp "$BUILD_DIR/$APP_NAME" "$MACOS_DIR/$APP_NAME" +cp "$EXECUTABLE_PATH" "$MACOS_DIR/$APP_NAME" cp "$ROOT/Resources/ProcessBarMonitor.icns" "$RESOURCES_DIR/ProcessBarMonitor.icns" -if [ -d "$RESOURCE_BUNDLE" ]; then - cp -R "$RESOURCE_BUNDLE" "$RESOURCES_DIR/$(basename "$RESOURCE_BUNDLE")" -fi +cp -R "$RESOURCE_BUNDLE" "$RESOURCES_DIR/$(basename "$RESOURCE_BUNDLE")" cat > "$APP_DIR/Contents/Info.plist" < @@ -43,6 +110,7 @@ cat > "$APP_DIR/Contents/Info.plist" < EOF +plutil -lint "$APP_DIR/Contents/Info.plist" >/dev/null echo "Built app bundle at: $APP_DIR" -echo "Version: $VERSION ($BUILD_NUMBER)" +echo "Version: $VERSION ($BUILD_NUMBER), configuration: $CONFIGURATION" diff --git a/release.sh b/release.sh index 6926e10..23f8424 100755 --- a/release.sh +++ b/release.sh @@ -16,7 +16,31 @@ ROOT="$(cd "$(dirname "$0")" && pwd)" ASSET_DIR="$ROOT/release" ASSET_NAME="$APP_NAME-$TAG-macOS.zip" NOTES_FILE="$ASSET_DIR/release-notes-$TAG.md" -REPO="$(gh api user --jq .login)/$APP_NAME" +CHECKSUM_FILE="$ASSET_DIR/$APP_NAME-$TAG-SHA256SUMS.txt" + +required_commands=(git gh swift shasum ditto) +for cmd in "${required_commands[@]}"; do + command -v "$cmd" >/dev/null 2>&1 || { + echo "Missing required command: $cmd" + exit 1 + } +done + +if ! gh auth status >/dev/null 2>&1; then + echo "GitHub CLI is not authenticated. Run: gh auth login" + exit 1 +fi + +if [[ ! "$VERSION" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid version: $VERSION_RAW" + echo "Expected format: X.Y.Z or vX.Y.Z (optional prerelease/build suffix)" + exit 2 +fi + +if [[ ! "$BUILD_NUMBER" =~ ^[0-9]+$ ]]; then + echo "Build number must be an integer. Received: $BUILD_NUMBER" + exit 2 +fi cd "$ROOT" @@ -30,10 +54,39 @@ git diff --cached --quiet || { exit 1 } -./build_app.sh "$VERSION" "$BUILD_NUMBER" +CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)" +if [[ "$CURRENT_BRANCH" != "main" && "${ALLOW_NON_MAIN_RELEASE:-0}" != "1" ]]; then + echo "Releases are restricted to main by default. Current branch: $CURRENT_BRANCH" + echo "Set ALLOW_NON_MAIN_RELEASE=1 to override intentionally." + exit 1 +fi + +TAG_EXISTS_LOCAL=0 +if git rev-parse "$TAG" >/dev/null 2>&1; then + TAG_EXISTS_LOCAL=1 + TAG_COMMIT="$(git rev-parse "$TAG^{commit}")" + HEAD_COMMIT="$(git rev-parse HEAD)" + if [[ "$TAG_COMMIT" != "$HEAD_COMMIT" ]]; then + echo "Tag $TAG already exists but does not point to HEAD." + exit 1 + fi +fi + +REMOTE_URL="$(git remote get-url origin)" +if [[ "$REMOTE_URL" =~ github\\.com[:/]([^/]+)/([^/.]+)(\\.git)?$ ]]; then + REPO="${match[1]}/${match[2]}" +else + echo "Unable to derive GitHub repo from origin URL: $REMOTE_URL" + exit 1 +fi + +./build_app.sh "$VERSION" "$BUILD_NUMBER" --configuration release +./scripts/validate_app_bundle.sh "$ROOT/dist/$APP_NAME.app" + mkdir -p "$ASSET_DIR" /usr/bin/ditto -c -k --sequesterRsrc --keepParent "$ROOT/dist/$APP_NAME.app" "$ASSET_DIR/$ASSET_NAME" SHA=$(shasum -a 256 "$ASSET_DIR/$ASSET_NAME" | awk '{print $1}') +printf "%s %s\n" "$SHA" "$ASSET_NAME" > "$CHECKSUM_FILE" cat > "$NOTES_FILE" </dev/null 2>&1; then - echo "Tag $TAG already exists locally." -else +if [[ "$TAG_EXISTS_LOCAL" -eq 0 ]]; then git tag -a "$TAG" -m "Release $TAG" fi -git push origin main -git push origin "$TAG" +if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then + echo "Tag $TAG already exists on origin." +else + git push origin "$TAG" +fi if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then - gh release upload "$TAG" "$ASSET_DIR/$ASSET_NAME" --clobber --repo "$REPO" + gh release upload "$TAG" "$ASSET_DIR/$ASSET_NAME" "$CHECKSUM_FILE" --clobber --repo "$REPO" gh release edit "$TAG" --title "$APP_NAME $TAG" --notes-file "$NOTES_FILE" --latest --repo "$REPO" else - gh release create "$TAG" "$ASSET_DIR/$ASSET_NAME" --title "$APP_NAME $TAG" --notes-file "$NOTES_FILE" --latest --repo "$REPO" + gh release create "$TAG" "$ASSET_DIR/$ASSET_NAME" "$CHECKSUM_FILE" --title "$APP_NAME $TAG" --notes-file "$NOTES_FILE" --latest --repo "$REPO" fi echo "Released $TAG" diff --git a/scripts/validate_app_bundle.sh b/scripts/validate_app_bundle.sh new file mode 100755 index 0000000..1cd1190 --- /dev/null +++ b/scripts/validate_app_bundle.sh @@ -0,0 +1,62 @@ +#!/bin/zsh +set -euo pipefail + +APP_PATH="${1:-dist/ProcessBarMonitor.app}" + +fail() { + echo "Validation failed: $1" >&2 + exit 1 +} + +require_file() { + local path="$1" + [[ -f "$path" ]] || fail "Missing file: $path" +} + +require_dir() { + local path="$1" + [[ -d "$path" ]] || fail "Missing directory: $path" +} + +plist_value() { + local key="$1" + local plist="$2" + /usr/libexec/PlistBuddy -c "Print $key" "$plist" 2>/dev/null || true +} + +require_dir "$APP_PATH" +INFO_PLIST="$APP_PATH/Contents/Info.plist" +EXECUTABLE="$APP_PATH/Contents/MacOS/ProcessBarMonitor" +ICON_FILE="$APP_PATH/Contents/Resources/ProcessBarMonitor.icns" + +require_file "$INFO_PLIST" +require_file "$EXECUTABLE" +require_file "$ICON_FILE" +[[ -x "$EXECUTABLE" ]] || fail "Executable is not marked executable: $EXECUTABLE" + +CF_BUNDLE_NAME="$(plist_value "CFBundleName" "$INFO_PLIST")" +CF_BUNDLE_EXEC="$(plist_value "CFBundleExecutable" "$INFO_PLIST")" +CF_BUNDLE_ID="$(plist_value "CFBundleIdentifier" "$INFO_PLIST")" +CF_BUNDLE_SHORT_VERSION="$(plist_value "CFBundleShortVersionString" "$INFO_PLIST")" +CF_BUNDLE_VERSION="$(plist_value "CFBundleVersion" "$INFO_PLIST")" +LS_UI_ELEMENT="$(plist_value "LSUIElement" "$INFO_PLIST")" + +[[ "$CF_BUNDLE_NAME" == "ProcessBarMonitor" ]] || fail "Unexpected CFBundleName: $CF_BUNDLE_NAME" +[[ "$CF_BUNDLE_EXEC" == "ProcessBarMonitor" ]] || fail "Unexpected CFBundleExecutable: $CF_BUNDLE_EXEC" +[[ "$CF_BUNDLE_ID" == "ai.openclaw.ProcessBarMonitor" ]] || fail "Unexpected CFBundleIdentifier: $CF_BUNDLE_ID" +[[ -n "$CF_BUNDLE_SHORT_VERSION" ]] || fail "Missing CFBundleShortVersionString" +[[ -n "$CF_BUNDLE_VERSION" ]] || fail "Missing CFBundleVersion" +[[ "$LS_UI_ELEMENT" == "true" ]] || fail "LSUIElement must be true for menu bar app" + +RESOURCE_BUNDLE="$(find "$APP_PATH/Contents/Resources" -maxdepth 1 -type d -name "*_ProcessBarMonitor.bundle" | head -n 1)" +[[ -n "$RESOURCE_BUNDLE" ]] || fail "SwiftPM resource bundle was not packaged in app resources" + +require_file "$RESOURCE_BUNDLE/Info.plist" +require_file "$RESOURCE_BUNDLE/en.lproj/Localizable.strings" +if [[ ! -f "$RESOURCE_BUNDLE/zh-Hans.lproj/Localizable.strings" && ! -f "$RESOURCE_BUNDLE/zh-hans.lproj/Localizable.strings" ]]; then + fail "Missing zh-Hans localization file in resource bundle" +fi + +echo "Validation passed: $APP_PATH" +echo "Version: $CF_BUNDLE_SHORT_VERSION ($CF_BUNDLE_VERSION)" +echo "Resource bundle: $(basename "$RESOURCE_BUNDLE")"