updated deployer code

This commit is contained in:
Mark Gregory
2026-09-30 10:27:14 +01:00
parent e8b0a1ce57
commit 1a7d026edd
6 changed files with 166 additions and 16 deletions
+2
View File
@@ -11,6 +11,8 @@
<key>CFBundleShortVersionString</key><string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key><string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSApplicationCategoryType</key><string>public.app-category.developer-tools</string>
<key>SonarDeskEnvironment</key><string>$(SONARDESK_ENVIRONMENT)</string>
<key>SonarDeskLicenseBaseURL</key><string>$(SONARDESK_LICENSE_BASE_URL)</string>
<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.net.absdev.SonarDesk</key>
+88 -6
View File
@@ -119,7 +119,7 @@
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
A12 /* Debug */ = {
A12 /* Develop */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@@ -160,8 +160,52 @@
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
name = Develop;
};
A16 /* Stage */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 39R6U3437D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_VERSION = 5.0;
};
name = Stage;
};
A13 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -203,7 +247,7 @@
};
name = Release;
};
A14 /* Debug */ = {
A14 /* Develop */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
@@ -226,12 +270,46 @@
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Develop;
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.vm";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_VERSION = 5.0;
};
name = Debug;
name = Develop;
};
A18 /* Stage */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = SonarDesk/SonarDesk.entitlements;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Configuration/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SonarDesk;
INFOPLIST_KEY_CFBundleIconName = AppIcon;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Stage;
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.stage.absdev.net";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_VERSION = 5.0;
};
name = Stage;
};
A15 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -255,6 +333,8 @@
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Production;
SONARDESK_LICENSE_BASE_URL = "https://sonardsk.absdev.net";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
@@ -270,7 +350,8 @@
A2 /* Build configuration list for PBXProject "SonarDesk" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A12 /* Debug */,
A12 /* Develop */,
A16 /* Stage */,
A13 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -279,7 +360,8 @@
A8 /* Build configuration list for PBXNativeTarget "SonarDesk" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A14 /* Debug */,
A14 /* Develop */,
A18 /* Stage */,
A15 /* Release */,
);
defaultConfigurationIsVisible = 0;
+18 -7
View File
@@ -13,6 +13,7 @@ struct SonarDeskApp: App {
@NSApplicationDelegateAdaptor(SonarDeskAppDelegate.self) private var appDelegate
@StateObject private var appState = AppState()
@StateObject private var reportStore = ReportStore()
@StateObject private var licenseStore = LicenseStore()
@State private var launchPhase: LaunchPhase = .splash
private enum LaunchPhase {
@@ -28,10 +29,17 @@ struct SonarDeskApp: App {
LaunchView()
.task { await startApplication() }
case .application:
RootView()
.environmentObject(appState)
.environmentObject(reportStore)
.transition(.opacity)
Group {
if licenseStore.isLicensed {
RootView()
.environmentObject(appState)
.environmentObject(reportStore)
} else {
LicenseRequiredView()
}
}
.environmentObject(licenseStore)
.transition(.opacity)
}
}
.frame(minWidth: 1040, minHeight: 680)
@@ -51,7 +59,8 @@ struct SonarDeskApp: App {
SettingsView()
.environmentObject(appState)
.environmentObject(reportStore)
.frame(width: 620, height: 430)
.environmentObject(licenseStore)
.frame(width: 720, height: 560)
}
}
@@ -60,11 +69,13 @@ struct SonarDeskApp: App {
// Keep the branded launch experience visible long enough to be perceptible,
// while using the time to perform the first SonarQube refresh.
async let minimumSplash: Void = Task.sleep(for: .seconds(1.6))
async let licenceRefresh: Void = licenseStore.refresh()
if appState.projects.isEmpty, appState.selectedServer != nil {
_ = await licenceRefresh
if licenseStore.isLicensed, appState.projects.isEmpty, appState.selectedServer != nil {
await appState.refresh()
}
_ = try? await minimumSplash
withAnimation(.easeInOut(duration: 0.32)) {
launchPhase = .application
+30
View File
@@ -79,3 +79,33 @@ enum KeychainError: LocalizedError {
}
}
}
extension KeychainService {
private static let licenseTokenAccount = "sonardesk.license-token"
static func saveLicenseToken(_ token: String) throws {
let clean = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !clean.isEmpty else { throw KeychainError.emptyToken }
let data = Data(clean.utf8)
SecItemDelete(query(account: licenseTokenAccount, synchronizable: kSecAttrSynchronizableAny) as CFDictionary)
var add = query(account: licenseTokenAccount, synchronizable: kCFBooleanFalse)
add[kSecValueData as String] = data
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
let status = SecItemAdd(add as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.status(status) }
}
static func licenseToken() -> String? {
var lookup = query(account: licenseTokenAccount, synchronizable: kSecAttrSynchronizableAny)
lookup[kSecReturnData as String] = true
lookup[kSecMatchLimit as String] = kSecMatchLimitOne
var item: CFTypeRef?
guard SecItemCopyMatching(lookup as CFDictionary, &item) == errSecSuccess,
let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
static func deleteLicenseToken() {
SecItemDelete(query(account: licenseTokenAccount, synchronizable: kSecAttrSynchronizableAny) as CFDictionary)
}
}
@@ -9,6 +9,7 @@ struct ProjectDetailView: View {
@State private var credentialError: String?
@State private var generatingCredentials = false
@State private var exportingAI = false
@State private var exportingPDF = false
@State private var exportError: String?
private var item: ProjectSnapshot? { app.projects.first { $0.project.key == projectKey } }
@@ -18,9 +19,15 @@ struct ProjectDetailView: View {
HStack {
VStack(alignment: .leading, spacing: 4) { Text(item.project.name).font(.system(size: 30, weight: .bold)); Text(item.project.key).foregroundStyle(.secondary) }
Spacer()
Button { Task { await exportAIReport(item) } } label: {
if exportingAI { ProgressView().controlSize(.small) } else { Label("Export for AI Analysis…", systemImage: "brain.head.profile") }
}.buttonStyle(.bordered).disabled(exportingAI)
Menu {
Button { Task { await exportPDFReport(item) } } label: { Label("PDF Report…", systemImage: "doc.richtext") }
Button { Task { await exportAIReport(item) } } label: { Label("AI Analysis Export…", systemImage: "brain.head.profile") }
} label: {
if exportingPDF || exportingAI { ProgressView().controlSize(.small) } else { Label("Export Report…", systemImage: "square.and.arrow.up") }
}
.menuStyle(.borderlessButton)
.fixedSize()
.disabled(exportingPDF || exportingAI)
GateBadge(status: item.qualityGate)
}.padding(.horizontal, 26).padding(.top, 22).padding(.bottom, 14)
@@ -172,6 +179,20 @@ struct ProjectDetailView: View {
catch { credentialError = error.localizedDescription }
}
@MainActor private func exportPDFReport(_ item: ProjectSnapshot) async {
exportingPDF = true; exportError = nil
defer { exportingPDF = false }
do {
let issues = try await app.allIssues(projectKey: item.project.key)
try ProjectPDFReportExporter.export(
project: item,
issues: issues,
serverURL: app.selectedServer?.baseURL ?? "Unknown",
serverVersion: app.serverVersion
)
} catch { exportError = error.localizedDescription }
}
@MainActor private func exportAIReport(_ item: ProjectSnapshot) async {
exportingAI = true; exportError = nil
defer { exportingAI = false }
@@ -3,6 +3,7 @@ import SwiftUI
struct SettingsView: View {
@EnvironmentObject private var app: AppState
@EnvironmentObject private var reportStore: ReportStore
@EnvironmentObject private var license: LicenseStore
@State private var editingServer: SonarServer?
@State private var showingAddServer = false
@@ -10,6 +11,9 @@ struct SettingsView: View {
TabView {
serversPage
.tabItem { Label("Servers", systemImage: "server.rack") }
LicenseSettingsView()
.environmentObject(license)
.tabItem { Label("Licence", systemImage: "key.fill") }
reportsPage
.tabItem { Label("Reports", systemImage: "doc.text") }
aboutPage