diff --git a/docs/local-web-contract.md b/docs/local-web-contract.md index 32dd3d3..544fbe4 100644 --- a/docs/local-web-contract.md +++ b/docs/local-web-contract.md @@ -31,15 +31,29 @@ wiki content. The app registers or repairs that helper through native setup UI and macOS Login Items & Extensions approval. The same flow trusts the local Caddy CA in the user's login keychain. Uninstall removes both. -The canonical product URL is: +The default browser-open product URL is: ```text -https://wiki.1context.localhost/your-context +https://localhost/your-context ``` -High-port HTTP (`http://wiki.1context.localhost:/your-context`) remains a -test and development harness mode only. Product code should not silently fall -back to it when the local HTTPS setup is missing. +The branded alias `https://wiki.1context.localhost/your-context` is still served +by Caddy and reported by diagnose, but product UI must not depend on that +multi-label `.localhost` name for either app readiness or default browser-open +behavior. Some macOS networking clients and browsers do not resolve it +consistently. + +Browser-visible high-port HTTPS +(`https://localhost:/your-context` or +`https://wiki.1context.localhost:/your-context`) remains a test and +development harness mode only. Product UI should not silently fall back to it +when the local HTTPS setup is missing. + +The app's internal health probe uses literal loopback +`https://127.0.0.1:/__1context/health` against the user-owned Caddy edge. +That keeps app readiness off local DNS entirely. Diagnose additionally probes +`https://localhost/__1context/health` through the privileged proxy and the +branded host health URL, but those probes are classified separately. The Swift daemon owns the local dynamic wiki API: @@ -103,7 +117,7 @@ should still load, with dynamic API calls degrading cleanly. The local adapter must not leak into the web contract: -- no browser-visible socket paths or loopback-only URLs +- no browser-visible socket paths or high-port backend URLs - no Caddy-specific behavior required by browser JavaScript - no render-on-request behavior from API routes - local-only capabilities must be explicit in API capability responses diff --git a/docs/macos-app-architecture.md b/docs/macos-app-architecture.md index b50ee36..57dcf93 100644 --- a/docs/macos-app-architecture.md +++ b/docs/macos-app-architecture.md @@ -50,7 +50,7 @@ stateDiagram-v2 WikiOpen --> PermissionsUI: setup later becomes stale or missing ``` -The required launch gate is Local Wiki Access because the app's primary wiki URL is `https://wiki.1context.localhost/your-context`. Future sensitive permissions should be added only with the feature that needs them, plus policy and tests for the exact user-facing prompt. +The required launch gate is Local Wiki Access because the app's primary wiki URL is `https://localhost/your-context`. The branded alias `https://wiki.1context.localhost/your-context` is served for diagnostics and compatibility observation, but the app does not depend on it for readiness or browser-open behavior. Future sensitive permissions should be added only with the feature that needs them, plus policy and tests for the exact user-facing prompt. ## Smoke Policy diff --git a/docs/macos-release-runbook.md b/docs/macos-release-runbook.md index 463dfee..14a7658 100644 --- a/docs/macos-release-runbook.md +++ b/docs/macos-release-runbook.md @@ -14,7 +14,8 @@ This is the current operating doc for shipping the 1Context macOS app. train. Prototype, private, and official `.app`/DMG builds must bundle their runtime inputs instead of resolving tools from Homebrew or host `PATH`. - Update engine: Sparkle from the installed `/Applications/1Context.app`. -- Canonical wiki URL: `https://wiki.1context.localhost/your-context`. +- Default wiki URL: `https://localhost/your-context`. +- Branded local alias: `https://wiki.1context.localhost/your-context`. - Required first setup: Local Wiki Access through native app setup. - User content root: `~/1Context`. - App machinery root: `~/Library/Application Support/1Context`. diff --git a/macos/Sources/OneContextCLI/main.swift b/macos/Sources/OneContextCLI/main.swift index 44aa05b..4531ded 100644 --- a/macos/Sources/OneContextCLI/main.swift +++ b/macos/Sources/OneContextCLI/main.swift @@ -139,6 +139,12 @@ struct OneContextCLI { print(" URL Mode: \(diagnostics.urlMode)") print(" Trust Mode: \(diagnostics.trustMode)") print(" Privileged Bind Required: \(yesNo(diagnostics.privilegedBindRequired))") + print(" Readiness Probe URL: \(diagnostics.readinessProbeURL)") + print(" Readiness Probe Health: \(diagnostics.readinessProbeHealth)") + print(" Privileged Proxy Probe URL: \(diagnostics.privilegedProxyProbeURL)") + print(" Privileged Proxy Probe Health: \(diagnostics.privilegedProxyProbeHealth)") + print(" Branded Host Probe URL: \(diagnostics.brandedProbeURL)") + print(" Branded Host Probe Health: \(diagnostics.brandedProbeHealth)") for line in LocalWebSetupDiagnostics.render(diagnostics.setup, redact: { displayPath($0, redact: redact) }) { print(line) } diff --git a/macos/Sources/OneContextLocalWeb/LocalWeb.swift b/macos/Sources/OneContextLocalWeb/LocalWeb.swift index 3b9415d..5cbbcda 100644 --- a/macos/Sources/OneContextLocalWeb/LocalWeb.swift +++ b/macos/Sources/OneContextLocalWeb/LocalWeb.swift @@ -33,6 +33,12 @@ public struct LocalWebDiagnostics: Codable, Equatable, Sendable { public var trustMode: String public var privilegedBindRequired: Bool public var setup: LocalWebSetupSnapshot + public var readinessProbeURL: String + public var readinessProbeHealth: String + public var privilegedProxyProbeURL: String + public var privilegedProxyProbeHealth: String + public var brandedProbeURL: String + public var brandedProbeHealth: String public var apiURL: String public var apiHealth: String public var apiPort: Int @@ -60,6 +66,12 @@ public struct LocalWebDiagnostics: Codable, Equatable, Sendable { trustMode: String, privilegedBindRequired: Bool, setup: LocalWebSetupSnapshot, + readinessProbeURL: String, + readinessProbeHealth: String, + privilegedProxyProbeURL: String, + privilegedProxyProbeHealth: String, + brandedProbeURL: String, + brandedProbeHealth: String, apiURL: String, apiHealth: String, apiPort: Int, @@ -86,6 +98,12 @@ public struct LocalWebDiagnostics: Codable, Equatable, Sendable { self.trustMode = trustMode self.privilegedBindRequired = privilegedBindRequired self.setup = setup + self.readinessProbeURL = readinessProbeURL + self.readinessProbeHealth = readinessProbeHealth + self.privilegedProxyProbeURL = privilegedProxyProbeURL + self.privilegedProxyProbeHealth = privilegedProxyProbeHealth + self.brandedProbeURL = brandedProbeURL + self.brandedProbeHealth = brandedProbeHealth self.apiURL = apiURL self.apiHealth = apiHealth self.apiPort = apiPort @@ -111,11 +129,17 @@ public struct LocalWebDiagnostics: Codable, Equatable, Sendable { public enum LocalWebDefaults { public static let wikiHost = "wiki.1context.localhost" + public static let browserHost = "localhost" public static let bindHost = "127.0.0.1" + // Keep app-internal readiness on literal loopback. The browser URL uses + // localhost because multi-label .localhost handling varies by macOS client. + public static let healthHost = bindHost + public static let privilegedProxyHealthHost = browserHost public static let wikiPort = 39191 public static let wikiAPIPort = 39192 public static let wikiRoute = "/your-context" - public static let defaultWikiURL = "https://\(wikiHost)\(wikiRoute)" + public static let defaultWikiURL = "https://\(browserHost)\(wikiRoute)" + public static let brandedWikiURL = "https://\(wikiHost)\(wikiRoute)" } public enum LocalWebURLMode: String, Codable, Sendable { @@ -288,10 +312,18 @@ public struct CaddyConfig: Equatable, Sendable { } public var url: String { - "https://\(host)\(LocalWebDefaults.wikiRoute)" + LocalWebDefaults.defaultWikiURL } public var healthURL: URL { + URL(string: "https://\(LocalWebDefaults.healthHost):\(port)/__1context/health")! + } + + public var privilegedProxyHealthURL: URL { + URL(string: "https://\(LocalWebDefaults.privilegedProxyHealthHost)/__1context/health")! + } + + public var brandedHealthURL: URL { URL(string: "https://\(host)/__1context/health")! } @@ -307,7 +339,7 @@ public struct CaddyConfig: Equatable, Sendable { auto_https disable_redirects } - https://\(host):\(port) { + \(siteAddresses()) { bind \(bindHost) root * "\(escape(siteRoot.path))" @@ -352,6 +384,15 @@ public struct CaddyConfig: Equatable, Sendable { .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") } + + private func siteAddresses() -> String { + let addresses = [ + "https://\(host):\(port)", + "https://\(LocalWebDefaults.healthHost):\(port)", + "https://\(LocalWebDefaults.privilegedProxyHealthHost):\(port)" + ] + return Array(Set(addresses)).sorted().joined(separator: ", ") + } } private struct CaddyState: Codable { @@ -465,7 +506,7 @@ public final class CaddyManager: @unchecked Sendable { rootCertificateSHA256: fingerprints.sha256, backendHost: LocalWebDefaults.bindHost, backendPort: port, - publicHost: host, + publicHost: LocalWebDefaults.browserHost, publicPort: LocalWebSetupConstants.privilegedHTTPSPort ) } @@ -473,12 +514,23 @@ public final class CaddyManager: @unchecked Sendable { public func diagnostics() -> LocalWebDiagnostics { let caddy = (try? caddyExecutable()) ?? URL(fileURLWithPath: "") let bundled = bundledCaddyURL() + let config = caddyConfig() + let setup = localWebSetupSnapshot() + let readinessProbeHealth = setup.ready ? probeHealth(config.healthURL) : "setup required" + let privilegedProxyProbeHealth = setup.ready ? probeHealth(config.privilegedProxyHealthURL) : "setup required" + let brandedProbeHealth = setup.ready ? probeHealth(config.brandedHealthURL) : "setup required" return LocalWebDiagnostics( snapshot: status(), urlMode: urlMode.rawValue, trustMode: urlMode.trustMode, privilegedBindRequired: urlMode.privilegedBindRequired, - setup: localWebSetupSnapshot(), + setup: setup, + readinessProbeURL: config.healthURL.absoluteString, + readinessProbeHealth: readinessProbeHealth, + privilegedProxyProbeURL: config.privilegedProxyHealthURL.absoluteString, + privilegedProxyProbeHealth: privilegedProxyProbeHealth, + brandedProbeURL: config.brandedHealthURL.absoluteString, + brandedProbeHealth: brandedProbeHealth, apiURL: apiConfig().healthURL.absoluteString, apiHealth: WikiLocalAPIProbe.health(config: apiConfig()), apiPort: apiConfig().port, @@ -753,25 +805,59 @@ public final class CaddyManager: @unchecked Sendable { } private func healthOK(_ url: URL) -> Bool { + probeHealth(url) == "OK" + } + + private func probeHealth(_ url: URL) -> String { var request = URLRequest(url: url) request.timeoutInterval = 0.75 let semaphore = DispatchSemaphore(value: 0) - nonisolated(unsafe) var ok = false - let task = URLSession.shared.dataTask(with: request) { data, _, _ in + nonisolated(unsafe) var health = "no response" + let task = URLSession.shared.dataTask(with: request) { data, _, error in defer { semaphore.signal() } + if let error { + health = Self.describeProbeError(error) + return + } guard let data, let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + health = "invalid response" return } - ok = object["status"] as? String == "ok" + health = object["status"] as? String == "ok" ? "OK" : "unhealthy response" } task.resume() if semaphore.wait(timeout: .now() + 1) == .timedOut { task.cancel() - return false + return "timeout" + } + return health + } + + private static func describeProbeError(_ error: Error) -> String { + let nsError = error as NSError + guard nsError.domain == NSURLErrorDomain else { + return error.localizedDescription + } + switch nsError.code { + case NSURLErrorCannotFindHost, NSURLErrorDNSLookupFailed: + return "dns failed" + case NSURLErrorCannotConnectToHost: + return "connection failed" + case NSURLErrorTimedOut: + return "timeout" + case NSURLErrorSecureConnectionFailed, + NSURLErrorServerCertificateHasBadDate, + NSURLErrorServerCertificateUntrusted, + NSURLErrorServerCertificateHasUnknownRoot, + NSURLErrorServerCertificateNotYetValid, + NSURLErrorClientCertificateRejected, + NSURLErrorClientCertificateRequired: + return "tls failed" + default: + return "url error \(nsError.code)" } - return ok } private func readState() -> CaddyState? { diff --git a/macos/Tests/OneContextLocalWebTests/LocalWebTests.swift b/macos/Tests/OneContextLocalWebTests/LocalWebTests.swift index 3f592b0..1c188c1 100644 --- a/macos/Tests/OneContextLocalWebTests/LocalWebTests.swift +++ b/macos/Tests/OneContextLocalWebTests/LocalWebTests.swift @@ -14,13 +14,17 @@ final class LocalWebTests: XCTestCase { XCTAssertTrue(text.contains("admin off")) XCTAssertTrue(text.contains("skip_install_trust")) XCTAssertTrue(text.contains("auto_https disable_redirects")) - XCTAssertTrue(text.contains("https://wiki.1context.localhost:39191 {")) + XCTAssertTrue(text.contains("https://127.0.0.1:39191")) + XCTAssertTrue(text.contains("https://localhost:39191")) + XCTAssertTrue(text.contains("https://wiki.1context.localhost:39191")) XCTAssertTrue(text.contains("bind 127.0.0.1")) XCTAssertTrue(text.contains("tls internal")) XCTAssertFalse(text.contains("auto_https off")) XCTAssertTrue(text.contains(":39191")) - XCTAssertEqual(config.url, "https://wiki.1context.localhost/your-context") - XCTAssertEqual(config.healthURL.absoluteString, "https://wiki.1context.localhost/__1context/health") + XCTAssertEqual(config.url, "https://localhost/your-context") + XCTAssertEqual(config.healthURL.absoluteString, "https://127.0.0.1:39191/__1context/health") + XCTAssertEqual(config.privilegedProxyHealthURL.absoluteString, "https://localhost/__1context/health") + XCTAssertEqual(config.brandedHealthURL.absoluteString, "https://wiki.1context.localhost/__1context/health") } func testDefaultURLModeRequiresProfessionalLocalHTTPSSetup() { @@ -32,7 +36,8 @@ final class LocalWebTests: XCTestCase { ) XCTAssertEqual(mode, .localHTTPSPortless) - XCTAssertEqual(LocalWebDefaults.defaultWikiURL, "https://wiki.1context.localhost/your-context") + XCTAssertEqual(LocalWebDefaults.defaultWikiURL, "https://localhost/your-context") + XCTAssertEqual(LocalWebDefaults.brandedWikiURL, "https://wiki.1context.localhost/your-context") XCTAssertEqual(config.url, LocalWebDefaults.defaultWikiURL) } @@ -94,6 +99,12 @@ final class LocalWebTests: XCTestCase { XCTAssertTrue(diagnostics.caddyfilePath.hasSuffix("Application Support/1Context/local-web/caddy/Caddyfile")) XCTAssertEqual(diagnostics.apiPort, LocalWebDefaults.wikiAPIPort) XCTAssertTrue(diagnostics.apiStatePath.hasSuffix("Application Support/1Context/local-web/wiki-browser-state.json")) + XCTAssertEqual(diagnostics.readinessProbeURL, "https://127.0.0.1:39191/__1context/health") + XCTAssertEqual(diagnostics.readinessProbeHealth, "setup required") + XCTAssertEqual(diagnostics.privilegedProxyProbeURL, "https://localhost/__1context/health") + XCTAssertEqual(diagnostics.privilegedProxyProbeHealth, "setup required") + XCTAssertEqual(diagnostics.brandedProbeURL, "https://wiki.1context.localhost/__1context/health") + XCTAssertEqual(diagnostics.brandedProbeHealth, "setup required") } func testDiagnosticsReportsLocalHTTPSURLMode() throws { @@ -113,7 +124,7 @@ final class LocalWebTests: XCTestCase { let diagnostics = manager.diagnostics() - XCTAssertEqual(diagnostics.snapshot.url, "https://wiki.1context.localhost/your-context") + XCTAssertEqual(diagnostics.snapshot.url, "https://localhost/your-context") XCTAssertEqual(diagnostics.urlMode, "local-https-portless") XCTAssertEqual(diagnostics.trustMode, "local-ca-required") XCTAssertTrue(diagnostics.privilegedBindRequired) @@ -137,7 +148,7 @@ final class LocalWebTests: XCTestCase { XCTAssertFalse(snapshot.running) XCTAssertEqual(snapshot.health, "setup required") - XCTAssertEqual(snapshot.url, "https://wiki.1context.localhost/your-context") + XCTAssertEqual(snapshot.url, "https://localhost/your-context") XCTAssertEqual(snapshot.lastError, "Local web setup required: Local HTTPS helper, Local certificate trust") } diff --git a/macos/Tests/OneContextSetupTests/AppSetupTests.swift b/macos/Tests/OneContextSetupTests/AppSetupTests.swift index b8614dc..712ff04 100644 --- a/macos/Tests/OneContextSetupTests/AppSetupTests.swift +++ b/macos/Tests/OneContextSetupTests/AppSetupTests.swift @@ -115,6 +115,12 @@ final class AppSetupTests: XCTestCase { trustMode: setup.urlMode == LocalWebURLMode.localHTTPSPortless.rawValue ? "local-ca-required" : "none", privilegedBindRequired: setup.urlMode == LocalWebURLMode.localHTTPSPortless.rawValue, setup: setup, + readinessProbeURL: "https://127.0.0.1:39191/__1context/health", + readinessProbeHealth: setup.ready ? "not running" : "setup required", + privilegedProxyProbeURL: "https://localhost/__1context/health", + privilegedProxyProbeHealth: setup.ready ? "not running" : "setup required", + brandedProbeURL: "https://wiki.1context.localhost/__1context/health", + brandedProbeHealth: setup.ready ? "not running" : "setup required", apiURL: "http://127.0.0.1:39192/__1context/api/health", apiHealth: "not running", apiPort: 39192, @@ -140,11 +146,11 @@ final class AppSetupTests: XCTestCase { private func localHTTPSSetup(ready: Bool, installedProxySHA256: String? = nil) -> LocalWebSetupSnapshot { LocalWebSetupSnapshot.localHTTPSPortless( - targetURL: "https://wiki.1context.localhost/your-context", + targetURL: LocalWebDefaults.defaultWikiURL, state: LocalWebSetupState( label: LocalWebSetupConstants.proxyLabel, targetHost: LocalWebDefaults.wikiHost, - targetURL: "https://wiki.1context.localhost/your-context", + targetURL: LocalWebDefaults.defaultWikiURL, backendHost: LocalWebDefaults.bindHost, backendPort: LocalWebDefaults.wikiPort, privilegedPort: LocalWebSetupConstants.privilegedHTTPSPort, diff --git a/scripts/test-release-app-product-https.sh b/scripts/test-release-app-product-https.sh index 4ed284a..5b66de5 100755 --- a/scripts/test-release-app-product-https.sh +++ b/scripts/test-release-app-product-https.sh @@ -9,7 +9,7 @@ if [[ "${ONECONTEXT_PRODUCT_HTTPS_SMOKE_INTERACTIVE:-0}" != "1" ]]; then Product HTTPS smoke is interactive because it validates real macOS setup: - user login-keychain certificate trust - ServiceManagement background helper approval - - portless HTTPS at https://wiki.1context.localhost + - portless HTTPS at https://localhost Complete Settings > Setup in the app once before running this smoke. @@ -46,6 +46,19 @@ assert_url_contains() { grep -q "$expected" <<<"$output" } +wait_url_contains() { + local url="$1" + local expected="$2" + local family="${3:-}" + for _ in {1..80}; do + if assert_url_contains "$url" "$expected" "$family"; then + return 0 + fi + sleep 0.25 + done + assert_url_contains "$url" "$expected" "$family" +} + export no_proxy="wiki.1context.localhost,localhost,127.0.0.1,::1" export NO_PROXY="$no_proxy" @@ -58,27 +71,23 @@ if ! grep -q "Setup Ready: yes" "$DIAGNOSE_OUTPUT"; then exit 1 fi -open -na "$APP" - -for _ in {1..80}; do - if assert_url_contains "https://wiki.1context.localhost/your-context" "Your Context"; then - break - fi - sleep 0.25 -done +open "$APP" -assert_url_contains "https://wiki.1context.localhost/your-context" "Your Context" -assert_url_contains "https://wiki.1context.localhost/for-you" "How This Page Works" -assert_url_contains "https://wiki.1context.localhost/api/wiki/health" "1context-wiki-api" -assert_url_contains "https://wiki.1context.localhost/__1context/health" "ok" "-4" -assert_url_contains "https://wiki.1context.localhost/__1context/health" "ok" "-6" -unknown_api_status="$(curl --silent --output /dev/null --write-out "%{http_code}" --noproxy '*' "https://wiki.1context.localhost/api/wiki/does-not-exist")" +wait_url_contains "https://localhost/your-context" "Your Context" +wait_url_contains "https://localhost/for-you" "How This Page Works" +wait_url_contains "https://localhost/api/wiki/health" "1context-wiki-api" +wait_url_contains "https://localhost/__1context/health" "ok" +wait_url_contains "https://wiki.1context.localhost/your-context" "Your Context" +wait_url_contains "https://wiki.1context.localhost/__1context/health" "ok" "-4" +wait_url_contains "https://wiki.1context.localhost/__1context/health" "ok" "-6" +unknown_api_status="$(curl --silent --output /dev/null --write-out "%{http_code}" --noproxy '*' "https://localhost/api/wiki/does-not-exist")" if [[ "$unknown_api_status" != "404" ]]; then echo "Unknown wiki API route returned $unknown_api_status instead of 404." >&2 exit 1 fi "$CLI" diagnose | grep -q "Local Wiki Access: Granted" -"$CLI" diagnose | grep -q "URL: https://wiki.1context.localhost/your-context" +"$CLI" diagnose | grep -q "URL: https://localhost/your-context" +"$CLI" diagnose | grep -q "Branded Host Probe URL: https://wiki.1context.localhost/__1context/health" echo "Product HTTPS smoke passed." diff --git a/scripts/test-release-train.sh b/scripts/test-release-train.sh index fc38994..ef00e73 100755 --- a/scripts/test-release-train.sh +++ b/scripts/test-release-train.sh @@ -8,6 +8,31 @@ VERSION="$(tr -d '[:space:]' < "$ROOT/VERSION")" eval "$("$ROOT/scripts/release-manifest.py" export-env)" PREVIOUS_VERSION="$ONECONTEXT_RELEASE_PREVIOUS_VERSION" +scan_text() { + local exclude="" + if [[ "${1:-}" == "--exclude" ]]; then + exclude="$2" + shift 2 + fi + local pattern="$1" + shift + + if command -v rg >/dev/null 2>&1; then + local args=(-n) + if [[ -n "$exclude" ]]; then + args+=(--glob "!$exclude") + fi + rg "${args[@]}" "$pattern" "$@" + return + fi + + local args=(-R -n -I -E) + if [[ -n "$exclude" ]]; then + args+=(--exclude "$exclude") + fi + grep "${args[@]}" "$pattern" "$@" +} + write_appcast() { local path="$1" local critical="$2" @@ -206,7 +231,7 @@ grep -q "./scripts/release-train.sh prove --runner-execute" "$ROOT/.github/workf grep -q "proof_reason:" "$ROOT/.github/workflows/self-hosted-mac-update-proof.yml" grep -q "./scripts/release-train.sh prove --channel private --runner-execute" "$ROOT/.github/workflows/self-hosted-mac-private-update-proof.yml" grep -q "proof_reason:" "$ROOT/.github/workflows/self-hosted-mac-private-update-proof.yml" -if rg -n '^\s+(old_version|new_version|staging_appcast_url|update_class|old_tag|old_dmg_url|update_timeout_seconds|steady_state_seconds|artifact_retention_days):' \ +if scan_text '^[[:space:]]+(old_version|new_version|staging_appcast_url|update_class|old_tag|old_dmg_url|update_timeout_seconds|steady_state_seconds|artifact_retention_days):' \ "$ROOT/.github/workflows/self-hosted-mac-update-proof.yml" \ "$ROOT/.github/workflows/self-hosted-mac-private-update-proof.yml" > "$TMP_DIR/proof-workflow-release-inputs.out" then @@ -225,7 +250,7 @@ fi grep -q "./scripts/release-train.sh build --channel official" "$ROOT/.github/workflows/release.yml" grep -q "ONECONTEXT_REMOTE_APPCAST_GITHUB_REPO" "$ROOT/scripts/release-train.sh" grep -q "ONECONTEXT_REMOTE_APPCAST_GITHUB_REPO" "$ROOT/scripts/release/internal/prove-remote-sparkle-update.sh" -if rg -n --glob '!test-release-train.sh' 'release-train\.sh package|ONECONTEXT_RUNTIME_ROOT|ONECONTEXT_REMOTE_UPDATE_VALIDATE_REPO_POLICY|dev-runtime-env|with-dev-runtime|release/update-policy' \ +if scan_text --exclude 'test-release-train.sh' 'release-train\.sh package|ONECONTEXT_RUNTIME_ROOT|ONECONTEXT_REMOTE_UPDATE_VALIDATE_REPO_POLICY|dev-runtime-env|with-dev-runtime|release/update-policy' \ "$ROOT/.github" "$ROOT/scripts" "$ROOT/docs/README.md" "$ROOT/docs/development.md" "$ROOT/docs/macos-release-runbook.md" "$ROOT/docs/ci/self-hosted-mac-runner.md" "$ROOT/release" \ > "$TMP_DIR/no-shim-scan.out" then @@ -233,7 +258,7 @@ then echo "active release surfaces must not mention deleted shims, old package commands, or old update-policy files." >&2 exit 1 fi -if rg -n '\bbrew (install|--prefix)\b|command -v caddy' \ +if scan_text '(^|[^[:alnum:]_])brew (install|--prefix)([^[:alnum:]_]|$)|command -v caddy' \ "$ROOT/.github/workflows/ci.yml" \ "$ROOT/.github/workflows/release.yml" \ "$ROOT/.github/workflows/self-hosted-mac-update-proof.yml" \ @@ -244,14 +269,14 @@ then echo "release app/DMG builds must not depend on Homebrew installs, Homebrew prefixes, or host caddy discovery." >&2 exit 1 fi -if rg -n 'ONECONTEXT_(SPARKLE_FEED_URL|UPDATE_OPTIONAL_PROMPT_TITLE|UPDATE_OPTIONAL_PROMPT_BODY|UPDATE_FAILURE_TITLE|UPDATE_FAILURE_BODY|UPDATE_POST_INSTALL_MESSAGE_ENABLED|UPDATE_POST_INSTALL_TITLE|UPDATE_POST_INSTALL_BODY|SPARKLE_SHOW_RELEASE_NOTES_IN_UPDATE_WINDOW):-' \ +if scan_text 'ONECONTEXT_(SPARKLE_FEED_URL|UPDATE_OPTIONAL_PROMPT_TITLE|UPDATE_OPTIONAL_PROMPT_BODY|UPDATE_FAILURE_TITLE|UPDATE_FAILURE_BODY|UPDATE_POST_INSTALL_MESSAGE_ENABLED|UPDATE_POST_INSTALL_TITLE|UPDATE_POST_INSTALL_BODY|SPARKLE_SHOW_RELEASE_NOTES_IN_UPDATE_WINDOW):-' \ "$ROOT/scripts/build-macos-app.sh" > "$TMP_DIR/build-update-env-overrides.out" then cat "$TMP_DIR/build-update-env-overrides.out" >&2 echo "build-macos-app.sh must read updater UI policy from release/release.toml, not caller env defaults." >&2 exit 1 fi -if rg -n '\b1context-cli (start|stop|quit|restart|status|logs|update|setup)\b|"\$CLI" (start|stop|quit|restart|status|logs|update|setup)\b|\$CLI (start|stop|quit|restart|status|logs|update|setup)\b' \ +if scan_text '(^|[^[:alnum:]_])1context-cli (start|stop|quit|restart|status|logs|update|setup)([^[:alnum:]_]|$)|"\$CLI" (start|stop|quit|restart|status|logs|update|setup)([^[:alnum:]_]|$)|\$CLI (start|stop|quit|restart|status|logs|update|setup)([^[:alnum:]_]|$)' \ "$ROOT/scripts/release" \ "$ROOT/scripts/release/internal/prove-remote-sparkle-update.sh" \ "$ROOT/scripts/release/internal/verify-macos-steady-state.sh" > "$TMP_DIR/deleted-cli-script-uses.out" @@ -384,7 +409,7 @@ mkdir "$dirty_repo" git -C "$dirty_repo" init -q printf 'clean\n' > "$dirty_repo/tracked.txt" git -C "$dirty_repo" add tracked.txt -git -C "$dirty_repo" -c user.name=Test -c user.email=test@example.com commit -qm init +git -C "$dirty_repo" -c user.name=Test -c user.email=test@example.com -c commit.gpgsign=false commit -qm init printf 'dirty\n' > "$dirty_repo/untracked.txt" if "$ROOT/scripts/release-manifest.py" check-clean-tree --root "$dirty_repo" > "$TMP_DIR/dirty-tree.out" 2>&1; then echo "clean-tree gate should fail on untracked release files." >&2 @@ -404,7 +429,7 @@ git -C "$helper_repo" init -q } > "$helper_repo/scripts/main.sh" chmod +x "$helper_repo/scripts/main.sh" git -C "$helper_repo" add scripts/main.sh -git -C "$helper_repo" -c user.name=Test -c user.email=test@example.com commit -qm init +git -C "$helper_repo" -c user.name=Test -c user.email=test@example.com -c commit.gpgsign=false commit -qm init printf '# helper\n' > "$helper_repo/scripts/helper.sh" if "$ROOT/scripts/release-manifest.py" check-sourced-helpers --root "$helper_repo" > "$TMP_DIR/helper.out" 2>&1; then echo "sourced-helper gate should fail when a sourced helper is untracked." >&2