diff --git a/.github/workflows/automa-backend-testing.yml b/.github/workflows/automa-backend-testing.yml index 8dbaaedf..20b76ec5 100644 --- a/.github/workflows/automa-backend-testing.yml +++ b/.github/workflows/automa-backend-testing.yml @@ -3,8 +3,7 @@ name: Run `AutomaBackend` Tests on: pull_request: paths: - - 'Backend/**/*.swift' - + - "Backend/**/*.swift" jobs: unit-tests: @@ -14,7 +13,7 @@ jobs: - uses: SwiftyLab/setup-swift@latest with: - swift-version: "6.1" + swift-version: "6.1" - uses: useblacksmith/cache@v5 with: @@ -27,6 +26,7 @@ jobs: compose: "false" working-directory: "Backend" swift_test_extra_args: "--filter '.*UnitTests.*'" + gh_pat: ${{ secrets.GET_AUTOMA_UTILITIES_GH_PAT }} integration-tests: runs-on: blacksmith-4vcpu-ubuntu-2404 @@ -35,13 +35,12 @@ jobs: - uses: SwiftyLab/setup-swift@latest with: - swift-version: "6.1" + swift-version: "6.1" - uses: useblacksmith/cache@v5 with: path: Backend/.build key: ${{ runner.os }}-backend-integrationtests-${{ hashFiles('**/Package.resolved') }} - - name: Run Integration Tests uses: GetAutomaApp/opensource-actions/swifttesting@main with: @@ -50,6 +49,7 @@ jobs: swift_test_extra_args: "--filter '.*IntegrationTests.*'" required_healthy_services_docker_compose: '["postgres", "localstack"]' compose_services_to_startup: '["postgres", "localstack"]' + gh_pat: ${{ secrets.GET_AUTOMA_UTILITIES_GH_PAT }} env: OWNER: ${{ secrets.OWNER }} REPO: ${{ secrets.REPO }} diff --git a/Backend/.gitignore b/Backend/.gitignore index 44272c4d..89af0c74 100644 --- a/Backend/.gitignore +++ b/Backend/.gitignore @@ -11,3 +11,4 @@ db.sqlite ! .env.example .vscode .fly.toml +infra/docker-secrets/* diff --git a/Backend/DataTypes/Sources/DataTypes/GenericErrors.swift b/Backend/DataTypes/Sources/DataTypes/GenericErrors.swift index d42d51bf..8c20eebf 100644 --- a/Backend/DataTypes/Sources/DataTypes/GenericErrors.swift +++ b/Backend/DataTypes/Sources/DataTypes/GenericErrors.swift @@ -13,8 +13,6 @@ public enum GenericErrors: String, Error, Decodable, Encodable { /// Error related to Alamofire networking operations case alamofireError /// Error when sending a Discord webhook message fails - case discordWebhookMessageFailed - /// Error when response decoding fails case failedToDecodeResponse /// Error when response encoding fails case failedToEncodeResponse @@ -61,12 +59,10 @@ public enum GenericErrors: String, Error, Decodable, Encodable { "You're Authentication Token is Invalid!" case .invalidUserId: "You're UserID isn't a valid UUID. We're Investigating" - case .discordWebhookMessageFailed: - "Sorry, We couldn't send a message through the discord webhook" case .smsMessageFailed: "We are having some technical difficulties sending sms messages!" case .missingImage: - "" + "Image generation was unsuccessful" case .failedToDecodeResponse: "An Invalid Response Object was sent down to the client!" case .failedToEncodeResponse: diff --git a/Backend/DevDockerfile b/Backend/DevDockerfile index 626a502c..18136559 100644 --- a/Backend/DevDockerfile +++ b/Backend/DevDockerfile @@ -12,4 +12,10 @@ WORKDIR /app EXPOSE 8080 +EXPOSE 6834 + +RUN --mount=type=secret,id=GITHUB_SSH_AUTHENTICATION_TOKEN \ + TOKEN=$(cat /run/secrets/GITHUB_SSH_AUTHENTICATION_TOKEN) && \ + git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" + CMD bash -c "nodemon -w ./ -w .env -e '.' --ignore ./.build --exec 'swift run App --env local $RUN_COMMAND'" diff --git a/Backend/Dockerfile b/Backend/Dockerfile index e9e9fa02..a2eeb6ed 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -5,10 +5,10 @@ FROM swift:6.1.0-jammy AS build # Install OS updates RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ - && apt-get -q update \ - && apt-get -q dist-upgrade -y \ - && apt-get install -y libjemalloc-dev \ - && apt-get install -y openssl libssl-dev + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get install -y libjemalloc-dev \ + && apt-get install -y openssl libssl-dev # Set up a build area WORKDIR /build @@ -18,6 +18,10 @@ WORKDIR /build # as long as your Package.swift/Package.resolved # files do not change. +RUN --mount=type=secret,id=GITHUB_SSH_AUTHENTICATION_TOKEN \ + TOKEN=$(cat /run/secrets/GITHUB_SSH_AUTHENTICATION_TOKEN) && \ + git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/" + COPY Package.swift . COPY Package.resolved . @@ -29,8 +33,8 @@ COPY . . # Build everything, with optimizations, with static linking, and using jemalloc # N.B.: The static version of jemalloc is incompatible with the static Swift runtime. RUN --mount=type=cache,target=/build/.build swift build -c release \ - --static-swift-stdlib \ - -Xlinker -ljemalloc + --static-swift-stdlib \ + -Xlinker -ljemalloc # Switch to the staging area WORKDIR /staging @@ -56,15 +60,15 @@ FROM ubuntu:jammy # Make sure all system packages are up to date, and install only essential packages. RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ - && apt-get -q update \ - && apt-get -q dist-upgrade -y \ - && apt-get -q install -y \ - libjemalloc2 \ - ca-certificates \ - tzdata \ - && apt-get install bash \ - && apt-get install -y openssl libssl-dev libcurl4 libxml2 \ - && rm -r /var/lib/apt/lists/* + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + libjemalloc2 \ + ca-certificates \ + tzdata \ + && apt-get install bash \ + && apt-get install -y openssl libssl-dev libcurl4 libxml2 \ + && rm -r /var/lib/apt/lists/* # Create a vapor user and group with /app as its home directory RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor @@ -84,4 +88,7 @@ USER vapor:vapor # Let Docker bind to port 8080 EXPOSE 8080 +# Expose PrometheusService server port +EXPOSE 6834 + CMD /bin/bash -c "eval './App $RUN_COMMAND'" diff --git a/Backend/Package.resolved b/Backend/Package.resolved index 50cfb276..6292837f 100644 --- a/Backend/Package.resolved +++ b/Backend/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fd696153ea81efb825467272117f6b70b5b5fa52dfde9a8035d412d86626d6d3", + "originHash" : "1d8db4abc125868abd7c0e97eb84f6246d39ee42a10a5f1676c43785a2426b91", "pins" : [ { "identity" : "alamofire", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "333f51104b75d1a5b94cb3b99e4c58a3b442c9f7", - "version" : "1.25.2" + "revision" : "8430dd49d4e2b417f472141805c9691ec2923cb8", + "version" : "1.29.0" } }, { @@ -24,8 +24,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/async-kit.git", "state" : { - "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", - "version" : "1.20.0" + "revision" : "6f3615ccf2ac3c2ae0c8087d527546e9544a43dd", + "version" : "1.21.0" + } + }, + { + "identity" : "automautilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GetAutomaApp/AutomaUtilities", + "state" : { + "branch" : "main", + "revision" : "e9876286ece603e964a8ef1c9bdc9802d4f680ed" } }, { @@ -82,6 +91,15 @@ "version" : "2.10.1" } }, + { + "identity" : "flyingfox", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swhitty/FlyingFox.git", + "state" : { + "revision" : "76b2d2d0eb54f0b53e8b661cc8007c487d94f8da", + "version" : "0.25.0" + } + }, { "identity" : "jmespath.swift", "kind" : "remoteSourceControl", @@ -168,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/postgres-nio.git", "state" : { - "revision" : "5d817be55cae8b00003b7458944954558302d006", - "version" : "1.25.0" + "revision" : "312444ea512ac9ed77fe58dcf737265ee48503cf", + "version" : "1.28.0" } }, { @@ -285,8 +303,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-distributed-tracing.git", "state" : { - "revision" : "a64a0abc2530f767af15dd88dda7f64d5f1ff9de", - "version" : "1.2.0" + "revision" : "baa932c1336f7894145cbaafcd34ce2dd0b77c97", + "version" : "1.3.1" } }, { @@ -420,8 +438,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/swift-service-lifecycle.git", "state" : { - "revision" : "e7187309187695115033536e8fc9b2eb87fd956d", - "version" : "2.8.0" + "revision" : "0fcc4c9c2d58dd98504c06f7308c86de775396ff", + "version" : "2.9.0" } }, { diff --git a/Backend/Package.swift b/Backend/Package.swift index 42785c0d..086041ee 100644 --- a/Backend/Package.swift +++ b/Backend/Package.swift @@ -1,7 +1,7 @@ // swift-tools-version:6.0 import PackageDescription -/// INitializes Package +/// Initializes Package public let package = Package( name: "Backend", platforms: [ diff --git a/Backend/README.md b/Backend/README.md new file mode 100644 index 00000000..351eafa9 --- /dev/null +++ b/Backend/README.md @@ -0,0 +1,2 @@ +1. Create `./infra/docker-secrets/` directory with the following files and secrets: + 1. "GITHUB_SSH_AUTHENTICATION_TOKEN": A Github fine-grained token that allows cloning AutomaUtilities repository (private) diff --git a/Backend/Sources/App/Clients/AutomaWebCoreClient/AutomaWebCoreClient.swift b/Backend/Sources/App/Clients/AutomaWebCoreClient/AutomaWebCoreClient.swift new file mode 100644 index 00000000..f89b5803 --- /dev/null +++ b/Backend/Sources/App/Clients/AutomaWebCoreClient/AutomaWebCoreClient.swift @@ -0,0 +1,25 @@ +// AutomaWebCoreClient.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import AutomaUtilities +import Vapor + +internal struct AutomaWebCoreClient { + private let client: any Client + private let baseURL: URL + + public init(client: any Client) throws { + self.client = client + let urlString = try Environment.getOrThrow("AUTOMA_WEB_CORE_API_BASE_URL") + baseURL = try URL.fromString(payload: .init(string: urlString)) + } + + public func getWebsiteHTML(payload: AutomaWebCoreAPIEndpointPayload) async throws -> String { + let res = try await client.get("\(baseURL.absoluteString)/api") { req in + try req.content.encode(payload) + } + return try res.content.decode(String.self) + } +} diff --git a/Backend/Sources/App/Controllers/PrometheusController/PrometheusController.swift b/Backend/Sources/App/Controllers/PrometheusController/PrometheusController.swift deleted file mode 100644 index 44a0ce1f..00000000 --- a/Backend/Sources/App/Controllers/PrometheusController/PrometheusController.swift +++ /dev/null @@ -1,63 +0,0 @@ -// PrometheusController.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import AutomaUtilities -import Fluent -import Prometheus -import Vapor - -/// Controller for handling Prometheus metrics requests. -internal struct PrometheusController: RouteCollection { - /// Registers routes for Prometheus operations. - /// - Parameter routes: The routes builder to register routes on. - public func boot(routes: RoutesBuilder) throws { - let prometheusRoute = routes.grouped("Prometheus") - - prometheusRoute.get("metrics", use: metrics) - } - - /// Retrieves Prometheus metrics. - /// - Parameter req: The request object. - /// - Returns: A string containing the metrics data. - /// - Throws: An error if metrics retrieval or conversion fails. - @Sendable - public func metrics(req: Request) throws -> String { - try validate(req: req) - guard - let metrics = String(data: MetricsService.global.emit(), encoding: .utf8) - else { - try MessageService().sendDiscordAlert( - alertTitle: "Could not convert metrics to string.", - error: PrometheusControllerError.couldNotConvertMetricsToData, - logger: req.logger - ) - throw PrometheusControllerError.couldNotConvertMetricsToData - } - return metrics - } - - /// Validates the request for Prometheus metrics. - /// - Parameter req: The request object. - /// - Throws: An error if the authentication token is invalid. - private func validate(req: Request) throws { - let query = try req.query.decode(PrometheusRouteQuery.self) - let token = try Environment.getOrThrow("FLY_METRICS_TOKEN") - - guard query.authToken == token else { - throw PrometheusControllerError.invalidAuthToken - } - } -} - -/// Represents the query parameters for Prometheus routes. -internal struct PrometheusRouteQuery: Content { - /// The authentication token for accessing Prometheus metrics. - public let authToken: String - - /// Coding keys to map the JSON keys to the struct properties. - public enum CodingKeys: String, CodingKey { - case authToken = "auth_token" - } -} diff --git a/Backend/Sources/App/Controllers/PrometheusController/PrometheusControllerError.swift b/Backend/Sources/App/Controllers/PrometheusController/PrometheusControllerError.swift deleted file mode 100644 index 83ed3556..00000000 --- a/Backend/Sources/App/Controllers/PrometheusController/PrometheusControllerError.swift +++ /dev/null @@ -1,13 +0,0 @@ -// PrometheusControllerError.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -/// Represents errors that can occur in the Prometheus controller. -internal enum PrometheusControllerError: Error { - /// Error indicating failure to convert metrics data to a string. - case couldNotConvertMetricsToData - - /// Error indicating an invalid authentication token. - case invalidAuthToken -} diff --git a/Backend/Sources/App/Services/MetricsService/MetricsService.swift b/Backend/Sources/App/DataTypes/BackendMetric.swift similarity index 68% rename from Backend/Sources/App/Services/MetricsService/MetricsService.swift rename to Backend/Sources/App/DataTypes/BackendMetric.swift index 78ad740f..d09d89f5 100644 --- a/Backend/Sources/App/Services/MetricsService/MetricsService.swift +++ b/Backend/Sources/App/DataTypes/BackendMetric.swift @@ -1,114 +1,67 @@ -// MetricsService.swift +// BackendMetric.swift // Copyright (c) 2025 GetAutomaApp // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +import AutomaUtilities import Foundation -import Metrics import Prometheus -/// Service for managing and emitting metrics. -internal struct MetricsService { - /// Global instance of the `MetricsService`. - public static let global = Self() - - /// Prometheus collector registry for managing metrics. - private var prometheus: PrometheusCollectorRegistry - - /// Initializes a new instance of `MetricsService`. - private init() { - let prometheusRegistry = PrometheusCollectorRegistry() - let myProm = PrometheusMetricsFactory(registry: prometheusRegistry) - MetricsSystem.bootstrap(myProm) - - prometheus = prometheusRegistry - } - - /// Emits metrics into a buffer and returns the data. - /// - Returns: A `Data` object containing the emitted metrics. - public func emit() -> Data { - var buffer = [UInt8]() - prometheus.emit(into: &buffer) - let data = String(decoding: buffer, as: Unicode.UTF8.self) - return Data(data.utf8) - } - - /// Creates a counter type metric. - /// - Parameters: - /// - name: The name of the counter. - /// - labels: Optional labels for the counter. - /// - Returns: A `Prometheus.Counter` object. - public func makeCounter(name: String, labels: [String: String] = [:]) -> Prometheus.Counter { - prometheus - .makeCounter( - name: name, - labels: convertStringDictionaryToStringMap(labels) - ) - } - - /// Converts a dictionary of strings to a tuple array. - /// - Parameter dictionary: The dictionary to convert. - /// - Returns: An array of tuples representing the dictionary. - private func convertStringDictionaryToStringMap(_ dictionary: [String: String]) -> [(String, String)] { - zip(dictionary.keys, dictionary.values).map { ($0.0, $0.1) } - } -} - /// Enum for managing backend metrics. internal enum BackendMetric { /// Counter for successful verification codes sent. public static let totalSuccessfulVerificationCodesSent = MetricsService.global.makeCounter( name: "total_verification_codes_sent", - labels: ["status": Self.MetricStatus.success.rawValue] + labels: ["status": MetricStatus.success.rawValue] ) /// Counter for failed verification codes sent. public static let totalFailedVerificationCodesSent = MetricsService.global.makeCounter( name: "total_verification_codes_sent", - labels: ["status": Self.MetricStatus.fail.rawValue] + labels: ["status": MetricStatus.fail.rawValue] ) /// Counter for total users created. public static let totalUsersCreated = MetricsService.global.makeCounter( name: "total_users_created", - labels: ["status": Self.MetricStatus.success.rawValue] + labels: ["status": MetricStatus.success.rawValue] ) /// Counter for users that already exist. public static let totalUsersAlreadyExists = MetricsService.global.makeCounter( name: "total_users_created", - labels: ["status": Self.MetricStatus.alreadyExists.rawValue] + labels: ["status": MetricStatus.alreadyExists.rawValue] ) /// Counter for successful token refresh attempts. public static let totalSuccessfulTokensRefreshed = MetricsService.global.makeCounter( name: "total_token_refresh_attempts", - labels: ["status": Self.MetricStatus.success.rawValue] + labels: ["status": MetricStatus.success.rawValue] ) /// Counter for failed token refresh attempts. public static let totalFailedTokensRefreshed = MetricsService.global.makeCounter( name: "total_token_refresh_attempts", - labels: ["status": Self.MetricStatus.fail.rawValue] + labels: ["status": MetricStatus.fail.rawValue] ) /// Counter for logout attempts. public static let totalLogoutAttempted = MetricsService.global.makeCounter( name: "total_logout_attempts", - labels: ["status": Self.MetricStatus.success.rawValue] + labels: ["status": MetricStatus.success.rawValue] ) /// Counter for failed logout attempts. public static let totalFailedLogoutAttempted = MetricsService.global.makeCounter( name: "total_logout_attempts", - labels: ["status": Self.MetricStatus.fail.rawValue] + labels: ["status": MetricStatus.fail.rawValue] ) /// Counter for profile pictures generated. public static let totalProfilePicturesGenerated = MetricsService.global.makeCounter( name: "total_profile_pictures_generated", labels: [ - "status": Self.MetricStatus.success.rawValue, + "status": MetricStatus.success.rawValue, ] ) @@ -116,7 +69,7 @@ internal enum BackendMetric { public static let totalProfilePicturesGenerationFailed = MetricsService.global.makeCounter( name: "total_profile_pictures_generated", labels: [ - "status": Self.MetricStatus.fail.rawValue, + "status": MetricStatus.fail.rawValue, ] ) @@ -124,7 +77,7 @@ internal enum BackendMetric { public static let totalTextMessagesSent = MetricsService.global.makeCounter( name: "total_text_messages_sent", labels: [ - "status": Self.MetricStatus.success.rawValue, + "status": MetricStatus.success.rawValue, ] ) @@ -132,15 +85,7 @@ internal enum BackendMetric { public static let totalTextMessagesSentFailed = MetricsService.global.makeCounter( name: "total_text_messages_sent", labels: [ - "status": Self.MetricStatus.fail.rawValue, - ] - ) - - /// Counter for Discord webhook messages sent. - public static let totalDiscordWebhookMessagesSent = MetricsService.global.makeCounter( - name: "total_discord_webhook_messages_sent", - labels: [ - "status": Self.MetricStatus.success.rawValue, + "status": MetricStatus.fail.rawValue, ] ) @@ -153,7 +98,7 @@ internal enum BackendMetric { /// - Parameter status: The status of the request. /// - Returns: A `Prometheus.Counter` object. public static func openAIImageGenerationRequest( - status: Self.MetricStatus + status: MetricStatus ) -> Prometheus.Counter { MetricsService.global.makeCounter( name: "openai_image_generation_requests", @@ -167,7 +112,7 @@ internal enum BackendMetric { /// - Parameter status: The status of the OAuth request (success/fail/etc). /// - Returns: A Prometheus counter for Twitter OAuth requests with the given status. public static func twitterOAuthRequest( - status: Self.MetricStatus + status: MetricStatus ) -> Prometheus.Counter { MetricsService.global.makeCounter( name: "twitter_oauth_requests", @@ -181,7 +126,7 @@ internal enum BackendMetric { /// - Parameter status: The status of the token conversion (success/fail/etc). /// - Returns: A Prometheus counter for Twitter token conversions with the given status. public static func twitterUserTokensConverted( - status: Self.MetricStatus + status: MetricStatus ) -> Prometheus.Counter { MetricsService.global.makeCounter( name: "twitter_user_tokens_converted", @@ -195,7 +140,7 @@ internal enum BackendMetric { /// - Parameter status: The status of posting the tweet (success/fail/etc). /// - Returns: A Prometheus counter for tweet posts with the given status. public static func twitterPostTweet( - status: Self.MetricStatus + status: MetricStatus ) -> Prometheus.Counter { MetricsService.global.makeCounter( name: "twitter_post_tweet", @@ -213,7 +158,7 @@ internal enum BackendMetric { /// - didThrowOnFeedInitialization: Flag indicating if an error occurred during feed initialization. /// - Returns: A Prometheus counter for RSS feed reads with the given status. public static func rssFeedReaderMetric( - status: Self.MetricStatus, + status: MetricStatus, url: URL, isRSSFeed: Bool? = nil, didThrowOnFeedInitialization: Bool = false @@ -240,7 +185,7 @@ internal enum BackendMetric { public static func chatCompletionServiceCall( platform: ChatCompletionPlatform, model: ChatCompletionModel, - status: Self.MetricStatus + status: MetricStatus ) -> Prometheus.Counter { MetricsService.global.makeCounter( name: "chat_completion_service_call", @@ -256,7 +201,7 @@ internal enum BackendMetric { public static let totalMediaFilesUploadedToTigris = MetricsService.global.makeCounter( name: "total_media_files_uploaded_to_tigris", labels: [ - "status": Self.MetricStatus.success.rawValue, + "status": MetricStatus.success.rawValue, ] ) @@ -264,7 +209,7 @@ internal enum BackendMetric { public static let totalMediaFilesUploadedToTigrisFailed = MetricsService.global.makeCounter( name: "total_media_files_uploaded_to_tigris", labels: [ - "status": Self.MetricStatus.fail.rawValue, + "status": MetricStatus.fail.rawValue, ] ) @@ -274,7 +219,7 @@ internal enum BackendMetric { /// - url: The URL being scraped. /// - Returns: A Prometheus counter for Firecrawl markdown scraping with the given status. public static func firecrawlScrapeMarkdown( - status: Self.MetricStatus, + status: MetricStatus, url: String ) -> Prometheus.Counter { MetricsService.global.makeCounter( @@ -285,16 +230,4 @@ internal enum BackendMetric { ] ) } - - /// Enum representing the status of a metric. - public enum MetricStatus: String, Codable { - /// Indicates that the metric already exists. - case alreadyExists - /// Indicates a failure status for the metric. - case fail - /// Indicates a start status for the metric. - case start - /// Indicates a success status for the metric. - case success - } } diff --git a/Backend/Sources/App/Extensions/Foundation/TaskExtensions.swift b/Backend/Sources/App/Extensions/Foundation/TaskExtensions.swift deleted file mode 100644 index fbd543dc..00000000 --- a/Backend/Sources/App/Extensions/Foundation/TaskExtensions.swift +++ /dev/null @@ -1,38 +0,0 @@ -// TaskExtensions.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import Vapor - -/// Extension on Task to start a detached task & log raw output -internal extension Task where Success == Void, Failure == any Error { - /// Executes & logs success / error on end - static func detachedLogOnError( - destination: String, - logger: Logger, - onError: @escaping @Sendable (Error) async throws -> Void = { _ in }, - onSuccess: @escaping @Sendable () async throws -> Void = {}, - method: @escaping @Sendable () async throws -> Void - ) { - Task.detached { - do { - try await method() - } catch { - logger.critical( - "Error occurred while running detached task", - metadata: [ - "destination": .array([ - .string(destination), - .string("Task.detachedLogOnError"), - .string(error.localizedDescription), - ]), - ] - ) - try await onError(error) - } - - try await onSuccess() - } - } -} diff --git a/Backend/Sources/App/Middleware/Authentication/RequestIsAuthenticatedMiddleware.swift b/Backend/Sources/App/Middleware/Authentication/RequestIsAuthenticatedMiddleware.swift index 422a985c..255c8126 100644 --- a/Backend/Sources/App/Middleware/Authentication/RequestIsAuthenticatedMiddleware.swift +++ b/Backend/Sources/App/Middleware/Authentication/RequestIsAuthenticatedMiddleware.swift @@ -3,6 +3,7 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +import AutomaUtilities import DataTypes import Vapor diff --git a/Backend/Sources/App/Procs/Jobs/TransactionalMessageAsyncJob/TransactionalMessageAsyncJob.swift b/Backend/Sources/App/Procs/Jobs/TransactionalMessageAsyncJob/TransactionalMessageAsyncJob.swift index ee9a70b5..598ea6e9 100644 --- a/Backend/Sources/App/Procs/Jobs/TransactionalMessageAsyncJob/TransactionalMessageAsyncJob.swift +++ b/Backend/Sources/App/Procs/Jobs/TransactionalMessageAsyncJob/TransactionalMessageAsyncJob.swift @@ -25,10 +25,10 @@ internal struct TransactionalMessageAsyncJob: AsyncJob { /// - payload: The input payload containing message data. /// - Throws: Throws an error if the message sending fails. public func dequeue(_ context: QueueContext, _ payload: TransactionalMessageJobInput) async throws { - let messageService = MessageService() + let snsService = SNSService() // Send the SMS message - _ = try await messageService + _ = try await snsService .sendSmS( to: payload.toPhoneNumber, message: payload.content, diff --git a/Backend/Sources/App/Services/AuthenticationService/AuthenticationService.swift b/Backend/Sources/App/Services/AuthenticationService/AuthenticationService.swift index a0fe2f51..0cd29453 100644 --- a/Backend/Sources/App/Services/AuthenticationService/AuthenticationService.swift +++ b/Backend/Sources/App/Services/AuthenticationService/AuthenticationService.swift @@ -3,6 +3,7 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +import AutomaUtilities import DataTypes import Fluent import JWT @@ -239,12 +240,12 @@ internal protocol AuthenticationService { } internal protocol AuthenticationServiceConfig { - /// Db w/ write access - var writeDb: Database { get } - /// Db w/ readonly access - var readDb: Database { get } - /// Logger - var logger: Logger { get } + /// Db w/ write access + var writeDb: Database { get } + /// Db w/ readonly access + var readDb: Database { get } + /// Logger + var logger: Logger { get } } internal struct RootAuthenticationServiceConfig: AuthenticationServiceConfig { diff --git a/Backend/Sources/App/Services/AuthenticationService/UserLoginService.swift b/Backend/Sources/App/Services/AuthenticationService/UserLoginService.swift index a815561c..546a2d19 100644 --- a/Backend/Sources/App/Services/AuthenticationService/UserLoginService.swift +++ b/Backend/Sources/App/Services/AuthenticationService/UserLoginService.swift @@ -3,6 +3,7 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +import AutomaUtilities import DataTypes import Fluent import JWT diff --git a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift index 96a79888..e0a44ece 100644 --- a/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift +++ b/Backend/Sources/App/Services/AuthenticationService/UserRegistrationService.swift @@ -3,6 +3,7 @@ // All source code and related assets are the property of GetAutomaApp. // All rights reserved. +import AutomaUtilities import DataTypes import Fluent import JWT diff --git a/Backend/Sources/App/Services/Formatters/MessageFormatterService/MessageFormatterService.swift b/Backend/Sources/App/Services/Formatters/MessageFormatterService/MessageFormatterService.swift deleted file mode 100644 index 28d40449..00000000 --- a/Backend/Sources/App/Services/Formatters/MessageFormatterService/MessageFormatterService.swift +++ /dev/null @@ -1,42 +0,0 @@ -// MessageFormatterService.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import DataTypes -import Fluent -import Vapor - -/// Service for formatting messages. -internal enum MessageFormatterService { - /// Crafts a verification code message. - /// - Parameter code: The verification code to include in the message. - /// - Returns: A formatted string containing the verification code. - public static func craftVerificationCodeMessage( - code: String - ) -> String { - "your automa verification code is: \"\(code)\"" - } - - /// Crafts a Discord webhook message for user events. - /// - Parameters: - /// - input: The input string for the message. - /// - event: The event description. - /// - imageUrl: Optional URL for an image to include in the message. - /// - Returns: A `DiscordWebhookMessage` configured with the provided details. - public static func craftUserEventDiscordWebhookMessage( - input: String, - event: String, - imageUrl: String? = nil - ) -> DiscordWebhookMessage { - .init( - embeds: [ - .init( - title: "**[\(input)]**", - description: "\(event)", - image: .init(url: imageUrl) - ), - ] - ) - } -} diff --git a/Backend/Sources/App/Services/MessageService/DiscordWebhookMessage.swift b/Backend/Sources/App/Services/MessageService/DiscordWebhookMessage.swift deleted file mode 100644 index dd700361..00000000 --- a/Backend/Sources/App/Services/MessageService/DiscordWebhookMessage.swift +++ /dev/null @@ -1,109 +0,0 @@ -// DiscordWebhookMessage.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import Foundation - -/// Represents a Discord webhook message. -internal struct DiscordWebhookMessage: Codable { - /// The content of the message. - public var content: String? - /// The username to display for the message. - public var username: String? - /// The URL of the avatar to display for the message. - public var avatarURL: String? - /// The embeds to include in the message. - public var embeds: [DiscordEmbed]? - - public enum CodingKeys: String, CodingKey { - case avatarURL = "avatar_url" - } -} - -/// Represents an embed in a Discord webhook message. -internal struct DiscordEmbed: Codable { - /// The title of the embed. - public var title: String? - /// The description of the embed. - public var description: String? - /// The URL associated with the embed. - public var url: String? - /// The timestamp of the embed. - public var timestamp: String? - /// The color of the embed. - public var color: Int? - /// The fields to include in the embed. - public var fields: [DiscordEmbedField]? - /// The footer of the embed. - public var footer: DiscordEmbedFooter? - /// The image to include in the embed. - public var image: DiscordEmbedImage? - /// The thumbnail to include in the embed. - public var thumbnail: DiscordEmbedImage? - /// The author of the embed. - public var author: DiscordEmbedAuthor? - /// The provider of the embed. - public var provider: DiscordEmbedProvider? - /// The video to include in the embed. - public var video: DiscordEmbedVideo? -} - -/// Represents a field in a Discord embed. -internal struct DiscordEmbedField: Codable { - /// The name of the field. - public var name: String - /// The value of the field. - public var value: String - /// Whether the field should be displayed inline. - public var inline: Bool -} - -/// Represents the footer of a Discord embed. -internal struct DiscordEmbedFooter: Codable { - /// The text of the footer. - public var text: String - /// The URL of the icon to display in the footer. - public var iconURL: String? - - public enum CodingKeys: String, CodingKey { - case iconURL = "icon_url" - case text - } -} - -/// Represents an image in a Discord embed. -internal struct DiscordEmbedImage: Codable { - /// The URL of the image. - public var url: String? -} - -/// Represents the author of a Discord embed. -internal struct DiscordEmbedAuthor: Codable { - /// The name of the author. - public var name: String - /// The URL associated with the author. - public var url: String? - /// The URL of the icon to display for the author. - public var iconURL: String? - - public enum CodingKeys: String, CodingKey { - case iconURL = "icon_url" - case url - case name - } -} - -/// Represents the provider of a Discord embed. -internal struct DiscordEmbedProvider: Codable { - /// The name of the provider. - public var name: String - /// The URL associated with the provider. - public var url: String -} - -/// Represents a video in a Discord embed. -internal struct DiscordEmbedVideo: Codable { - /// The URL of the video. - public var url: String -} diff --git a/Backend/Sources/App/Services/MessageService/MessageService.swift b/Backend/Sources/App/Services/MessageService/MessageService.swift deleted file mode 100644 index dc271ad0..00000000 --- a/Backend/Sources/App/Services/MessageService/MessageService.swift +++ /dev/null @@ -1,217 +0,0 @@ -// MessageService.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -import AutomaUtilities -import DataTypes -import Fluent -import Foundation -import SotoSNS -import Vapor - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -internal struct MessageService: Decodable { - // MARK: - Public API - - public func sendSmS( - to phoneNumber: String, - message: String, - logger: Logger - ) async throws -> String { - do { - let snsClient = try createSNSClient() - let output = try await snsClient.publish(.init(message: message, phoneNumber: phoneNumber)) - - try logSmsSentEvent(to: phoneNumber, message: message, logger: logger) - - return try extractMessageId(from: output, phoneNumber: phoneNumber, message: message, logger: logger) - } catch { - handleSmsError(error, to: phoneNumber, message: message, logger: logger) - throw error - } - } - - public func sendWebhookMessage( - webhookURL: URL, - message: DiscordWebhookMessage, - logger: Logger - ) async throws { - BackendMetric.totalDiscordWebhookMessagesSent.increment() - - guard try Environment.getOrThrow("ENVIRONMENT") != "local" else { return } - - do { - let request = try buildWebhookRequest(url: webhookURL, message: message) - let (data, response) = try await URLSession.shared.data(for: request) - - try validateWebhookResponse(response, data: data) - - logger.info("Sent Discord webhook message successfully", metadata: [ - "to": .string("MessageService.sendWebhookMessage"), - "webhook": .string(webhookURL.absoluteString), - "response": .string(String(data: data, encoding: .utf8) ?? "") - ]) - } catch { - logger.error("Failed to send Discord webhook message", metadata: [ - "to": .string("MessageService.sendWebhookMessage"), - "webhook": .string(webhookURL.absoluteString), - "error": .string(error.localizedDescription) - ]) - throw GenericErrors.discordWebhookMessageFailed - } - } - - public func sendDiscordWebhookAppEvent( - input: String, - event: String, - imageUrl: String? = nil, - logger: Logger, - withUrl: URL? = nil - ) throws { - guard let url = try resolveWebhookURL(override: withUrl, logger: logger, input: input, event: event) else { - return - } - - Task.detachedLogOnError(destination: "MessageService.sendDiscordWebhookAppEvent", logger: logger) { - try await sendWebhookMessage( - webhookURL: url, - message: MessageFormatterService.craftUserEventDiscordWebhookMessage( - input: input, - event: event, - imageUrl: imageUrl - ), - logger: logger - ) - } - } - - public func sendDiscordAlert( - alertTitle: String, - error: Error, - logger: Logger - ) throws { - guard - let webhookUrl = try URL(string: Environment.getOrThrow("DISCORD_AUTOMA_ALERTS_WEBHOOK_URL")) - else { - throwWebhookURLError(logger: logger, title: alertTitle) - } - - try sendDiscordWebhookAppEvent( - input: "Critical Error Occurred - \(alertTitle)", - event: "\(error) - \(error.localizedDescription)", - logger: logger, - withUrl: webhookUrl - ) - } - - // MARK: - Private Helpers - - private func createSNSClient() throws -> SNS { - let clientAuth = try AWSClient( - credentialProvider: .static( - accessKeyId: Environment.getOrThrow("AWS_ACCESS_KEY_ID"), - secretAccessKey: Environment.getOrThrow("AWS_SECRET_ACCESS_KEY") - ) - ) - let region = try Environment.getOrThrow("AWS_DEFAULT_REGION") - return SNS(client: clientAuth, region: .other(region)) - } - - private func extractMessageId( - from output: SNS.PublishResponse, - phoneNumber: String, - message: String, - logger: Logger - ) throws -> String { - guard let messageId = output.messageId else { - logger.error("Failed to send message to user", metadata: [ - "to": .string("MessageService.sendSmS"), - "phoneNumber": .string(phoneNumber), - "message": .string(message), - ]) - throw GenericErrors.smsMessageFailed - } - - logger.info("Sent SMS message to user", metadata: [ - "to": .string("MessageService.sendSmS"), - "messageId": .string(messageId), - "phoneNumber": .string(phoneNumber), - "message": .string(message), - "sequenceNumber": .string(output.sequenceNumber ?? "") - ]) - BackendMetric.totalTextMessagesSent.increment() - - return messageId - } - - private func handleSmsError( - _ error: Error, - to phoneNumber: String, - message: String, - logger: Logger - ) { - logger.error("Failed to send message", metadata: [ - "to": .string("MessageService.sendSmS"), - "phoneNumber": .string(phoneNumber), - "message": .string(message), - "error": .string(error.localizedDescription) - ]) - BackendMetric.totalTextMessagesSentFailed.increment() - } - - private func buildWebhookRequest(url: URL, message: DiscordWebhookMessage) throws -> URLRequest { - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSONEncoder().encode(message) - return request - } - - private func validateWebhookResponse(_ response: URLResponse, data _: Data) throws { - if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 { - throw GenericErrors.discordWebhookMessageFailed - } - } - - private func resolveWebhookURL( - override: URL?, - logger: Logger, - input: String, - event: String - ) throws -> URL? { - if let url = override { - return url - } - - guard let fallbackUrl = try? URL(string: Environment.getOrThrow("DISCORD_APP_EVENTS_URL")) else { - logger.error("Could not resolve Discord webhook URL", metadata: [ - "to": .string("MessageService.sendDiscordWebhookAppEvent"), - "event": .string(event), - "input": .string(input) - ]) - throw Abort(.internalServerError) - } - - return fallbackUrl - } - - private func throwWebhookURLError(logger: Logger, title: String) -> Never { - logger.error("Could not resolve Discord webhook URL", metadata: [ - "to": .string("MessageService.sendDiscordAlert"), - "alert_title": .string(title) - ]) - fatalError("Invalid webhook URL") // Or throw Abort(.internalServerError) - } - - private func logSmsSentEvent(to phoneNumber: String, message: String, logger: Logger) throws { - try sendDiscordWebhookAppEvent( - input: "random -> \(phoneNumber)", - event: "sending message: `\(message)`", - logger: logger - ) - } -} diff --git a/Backend/Sources/App/Services/SNSService/SNSService.swift b/Backend/Sources/App/Services/SNSService/SNSService.swift new file mode 100644 index 00000000..57aa369b --- /dev/null +++ b/Backend/Sources/App/Services/SNSService/SNSService.swift @@ -0,0 +1,101 @@ +// SNSService.swift +// Copyright (c) 2025 GetAutomaApp +// All source code and related assets are the property of GetAutomaApp. +// All rights reserved. + +import AutomaUtilities +import DataTypes +import Foundation +import SotoSNS +import Vapor + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +internal struct SNSService { + private let messageService: MessageService + + public init() { + messageService = MessageService() + } + + public func sendSmS( + to phoneNumber: String, + message: String, + logger: Logger + ) async throws -> String { + do { + let snsClient = try createSNSClient() + let output = try await snsClient.publish(.init(message: message, phoneNumber: phoneNumber)) + + try logSmsSentEvent(to: phoneNumber, message: message, logger: logger) + + return try extractMessageId(from: output, phoneNumber: phoneNumber, message: message, logger: logger) + } catch { + handleSmsError(error, to: phoneNumber, message: message, logger: logger) + throw error + } + } + + private func createSNSClient() throws -> SNS { + let clientAuth = try AWSClient( + credentialProvider: .static( + accessKeyId: Environment.getOrThrow("AWS_ACCESS_KEY_ID"), + secretAccessKey: Environment.getOrThrow("AWS_SECRET_ACCESS_KEY") + ) + ) + let region = try Environment.getOrThrow("AWS_DEFAULT_REGION") + return SNS(client: clientAuth, region: .other(region)) + } + + private func extractMessageId( + from output: SNS.PublishResponse, + phoneNumber: String, + message: String, + logger: Logger + ) throws -> String { + guard let messageId = output.messageId else { + logger.error("Failed to send message to user", metadata: [ + "to": .string("MessageService.sendSmS"), + "phoneNumber": .string(phoneNumber), + "message": .string(message), + ]) + throw GenericErrors.smsMessageFailed + } + + logger.info("Sent SMS message to user", metadata: [ + "to": .string("MessageService.sendSmS"), + "messageId": .string(messageId), + "phoneNumber": .string(phoneNumber), + "message": .string(message), + "sequenceNumber": .string(output.sequenceNumber ?? "") + ]) + BackendMetric.totalTextMessagesSent.increment() + + return messageId + } + + private func handleSmsError( + _ error: Error, + to phoneNumber: String, + message: String, + logger: Logger + ) { + logger.error("Failed to send message", metadata: [ + "to": .string("MessageService.sendSmS"), + "phoneNumber": .string(phoneNumber), + "message": .string(message), + "error": .string(error.localizedDescription) + ]) + BackendMetric.totalTextMessagesSentFailed.increment() + } + + private func logSmsSentEvent(to phoneNumber: String, message: String, logger: Logger) throws { + try messageService.sendDiscordWebhookAppEvent( + input: "random -> \(phoneNumber)", + event: "sending message: `\(message)`", + logger: logger + ) + } +} diff --git a/Backend/Sources/App/configure.swift b/Backend/Sources/App/configure.swift index 4fb9ab64..ed45c8ee 100644 --- a/Backend/Sources/App/configure.swift +++ b/Backend/Sources/App/configure.swift @@ -49,6 +49,7 @@ internal struct AppConfigurator { private func configureWhenDatabaseURLsAvailable() async throws { try await DatabaseConfigurator(app: app).configureDatabases() try registerControllers() + try await PrometheusService().startServer() try await addAuthenticationJWTKey() configureQueues() addJobsToQueue() @@ -61,7 +62,6 @@ internal struct AppConfigurator { try app.register(collection: TwitterController()) try app.register(collection: ChatCompletionController()) try app.register(collection: FirecrawlTestController()) - try app.register(collection: PrometheusController()) } private func addAuthenticationJWTKey() async throws { diff --git a/Backend/Sources/App/entrypoint.swift b/Backend/Sources/App/entrypoint.swift index 4c3bdcee..1e204e22 100644 --- a/Backend/Sources/App/entrypoint.swift +++ b/Backend/Sources/App/entrypoint.swift @@ -4,7 +4,6 @@ // All rights reserved. import Logging -import Metrics import NIOCore import NIOPosix import Vapor diff --git a/Backend/Tests/AppTests/Controllers/PrometheusControllerTests/PrometheusControllerIntegrationTests.swift b/Backend/Tests/AppTests/Controllers/PrometheusControllerTests/PrometheusControllerIntegrationTests.swift deleted file mode 100644 index 86504856..00000000 --- a/Backend/Tests/AppTests/Controllers/PrometheusControllerTests/PrometheusControllerIntegrationTests.swift +++ /dev/null @@ -1,52 +0,0 @@ -// PrometheusControllerIntegrationTests.swift -// Copyright (c) 2025 GetAutomaApp -// All source code and related assets are the property of GetAutomaApp. -// All rights reserved. - -@testable import App -import AutomaUtilities -import Testing -import VaporTesting - -/// Integration tests for the `PrometheusController`. -/// These tests verify the controller's ability to handle requests and return metrics data. -@Suite("PrometheusControllerIntegrationTests") -internal struct PrometheusControllerIntegrationTests { - /// Helper method to create a test application instance for each test. - /// This method handles proper setup and teardown of the application. - /// - /// - Parameter test: A closure that takes an `Application` instance and performs test operations. - /// - Throws: Any errors that occur during test execution or application setup/teardown. - private func withApp(_ test: (Application) async throws -> Void) async throws { - let app = try await Application.make(.testing) - do { - // Register the `PrometheusController` with the application - try app.register(collection: PrometheusController()) - // Execute the test closure with the application instance - try await test(app) - } catch { - // Shut down the application in case of errors - try await app.asyncShutdown() - throw error - } - // Ensure proper cleanup by shutting down the application - try await app.asyncShutdown() - } - - /// Tests the ability to request metrics data from the Prometheus controller. - /// Verifies that the response status is OK and the metrics data is returned. - /// - /// - Throws: Any errors that occur during test execution or request handling. - @Test("Test Request") - public func request() async throws { - try await withApp { app in - // Retrieve the Fly metrics token from the environment - let token = try Environment.getOrThrow("FLY_METRICS_TOKEN") - // Send a GET request to the Prometheus metrics endpoint - try await app.testing().test(.GET, "Prometheus/metrics?auth_token=\(token)") { res async in - // Expect the response status to be OK - #expect(res.status == .ok) - } - } - } -} diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index 5db77360..4f61fe9f 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -6,6 +6,8 @@ services: build: context: . dockerfile: ./DevDockerfile + secrets: + - GITHUB_SSH_AUTHENTICATION_TOKEN depends_on: postgres: condition: service_healthy @@ -68,3 +70,7 @@ volumes: driver: local worker_build: driver: local + +secrets: + GITHUB_SSH_AUTHENTICATION_TOKEN: + file: ./infra/docker-secrets/GITHUB_SSH_AUTHENTICATION_TOKEN diff --git a/Backend/infra/fly/fly-jobs.toml b/Backend/infra/fly/fly-jobs.toml index 3d05bd68..273fe1fb 100644 --- a/Backend/infra/fly/fly-jobs.toml +++ b/Backend/infra/fly/fly-jobs.toml @@ -18,5 +18,5 @@ cpus = 2 min_machines_running = 1 [metrics] -port = 8080 -path = "/Prometheus/metrics?auth_token=__FLY_METRICS_TOKEN__" +port = 6834 +path = "/metrics" diff --git a/Backend/infra/fly/fly.toml b/Backend/infra/fly/fly.toml index b8644bc4..8b12b29e 100644 --- a/Backend/infra/fly/fly.toml +++ b/Backend/infra/fly/fly.toml @@ -25,5 +25,5 @@ cpu_kind = 'shared' cpus = 4 [metrics] -port = 8080 -path = "/Prometheus/metrics?auth_token=__FLY_METRICS_TOKEN__" +port = 6834 +path = "/metrics" diff --git a/package.json b/package.json index 988c64ac..01da9961 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "deploy:backend:sandbox": "npm run fly:config -- fly sandbox | npm run deploy:to:fly", "deploy:backend:staging": "npm run fly:config -- fly staging | npm run deploy:to:fly", "deploy:backend:production": "npm run fly:config -- fly production | npm run deploy:to:fly", - "deploy:to:fly": "tail -n 1 | xargs -Ixx cp xx Backend/.fly.toml && cd Backend && flyctl deploy --config=.fly.toml", + "deploy:to:fly": "tail -n 1 | xargs -Ixx cp xx Backend/.fly.toml && cd Backend && flyctl deploy --ha=false --config=.fly.toml --build-secret GITHUB_SSH_AUTHENTICATION_TOKEN=$(cat ./infra/docker-secrets/GITHUB_SSH_AUTHENTICATION_TOKEN)", "deploy:worker:sandbox": "npm run fly:config -- fly-jobs sandbox | npm run deploy:to:fly", "deploy:worker:production": "npm run fly:config -- fly-jobs production | npm run deploy:to:fly", "build:all": "npx npm-run-all --parallel build:ui-kit build:backend",