7 Commits

Author SHA1 Message Date
3743eefa69 fix: Update to release workflow
Some checks failed
CI / Run Tests (push) Has been cancelled
Create and publish a Docker image / build-and-push-image (push) Failing after 6s
2024-11-19 14:02:10 -05:00
845d566c60 feat: Adds release workflow to gitea
Some checks failed
CI / Run Tests (push) Has been cancelled
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2024-11-19 13:43:04 -05:00
99f39b91af feat: More cli client tests and documentation.
All checks were successful
CI / Run Tests (push) Successful in 4m57s
2024-11-18 22:55:54 -05:00
55ea88a29f feat: Switch to old commit to checkout xcodebuild 2024-11-18 18:45:03 -05:00
756fd0bccf feat: cli client test updates
Some checks failed
CI / Run Tests (push) Failing after 3h8m1s
2024-11-18 17:16:44 -05:00
24f2ad63a7 feat: Moved cli client tests to XCTest to work in docker
Some checks failed
CI / Run Tests (push) Has been cancelled
2024-11-18 15:53:23 -05:00
ce18c44363 feat: Working on cli client and tests
Some checks failed
CI / Run Tests (push) Failing after 3m7s
2024-11-17 22:23:44 -05:00
25 changed files with 575 additions and 608 deletions

66
.gitea/workflows/release.yml Executable file
View File

@@ -0,0 +1,66 @@
#
name: Create and publish a Docker image
# Configures this workflow to run every time a change is pushed to the branch called `release`.
on:
push:
#branches: ['release']
tags:
- '*'
workflow_dispatch:
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: git.housh.dev
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
attestations: write
id-token: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ gitea.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

2
.gitignore vendored
View File

@@ -5,7 +5,7 @@
xcuserdata/ xcuserdata/
DerivedData/ DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.dewPoint-env .dewPoint-env*
.topics .topics
mqtt_password.txt mqtt_password.txt
.env .env

View File

@@ -1,2 +1,9 @@
disabled_rules: disabled_rules:
- closing_brace - closing_brace
- fuction_body_length
included:
- Sources
- Tests
ignore_multiline_statement_conditions: true

View File

@@ -17,7 +17,6 @@
}, },
"testTargets" : [ "testTargets" : [
{ {
"parallelizable" : true,
"target" : { "target" : {
"containerPath" : "container:", "containerPath" : "container:",
"identifier" : "IntegrationTests", "identifier" : "IntegrationTests",

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MQTTConnectionService"
BuildableName = "MQTTConnectionService"
BlueprintName = "MQTTConnectionService"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MQTTConnectionService"
BuildableName = "MQTTConnectionService"
BlueprintName = "MQTTConnectionService"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MQTTManager"
BuildableName = "MQTTManager"
BlueprintName = "MQTTManager"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MQTTManager"
BuildableName = "MQTTManager"
BlueprintName = "MQTTManager"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1330"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Models"
BuildableName = "Models"
BlueprintName = "Models"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Models"
BuildableName = "Models"
BlueprintName = "Models"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SensorsService"
BuildableName = "SensorsService"
BlueprintName = "SensorsService"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SensorsService"
BuildableName = "SensorsService"
BlueprintName = "SensorsService"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,177 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MQTTConnectionService"
BuildableName = "MQTTConnectionService"
BlueprintName = "MQTTConnectionService"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MQTTManager"
BuildableName = "MQTTManager"
BlueprintName = "MQTTManager"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Models"
BuildableName = "Models"
BlueprintName = "Models"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SensorsService"
BuildableName = "SensorsService"
BlueprintName = "SensorsService"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "dewpoint-controller"
BuildableName = "dewpoint-controller"
BlueprintName = "dewpoint-controller"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "IntegrationTests"
BuildableName = "IntegrationTests"
BlueprintName = "IntegrationTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CliClient"
BuildableName = "CliClient"
BlueprintName = "CliClient"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:.swiftpm/dewpoint-controller-Package.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "IntegrationTests"
BuildableName = "IntegrationTests"
BlueprintName = "IntegrationTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "dewpoint-controller"
BuildableName = "dewpoint-controller"
BlueprintName = "dewpoint-controller"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "dewpoint-controller"
BuildableName = "dewpoint-controller"
BlueprintName = "dewpoint-controller"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "dewpoint-controller"
BuildableName = "dewpoint-controller"
BlueprintName = "dewpoint-controller"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "dewpoint-controller"
BuildableName = "dewpoint-controller"
BlueprintName = "dewpoint-controller"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "dewpoint-controller"
BuildableName = "dewpoint-controller"
BlueprintName = "dewpoint-controller"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -42,6 +42,16 @@ let package = Package(
.product(name: "MQTTNIO", package: "mqtt-nio") .product(name: "MQTTNIO", package: "mqtt-nio")
] ]
), ),
.testTarget(
name: "CliClientTests",
dependencies: [
"CliClient"
],
resources: [
.copy("test.env"),
.copy("test-env.json")
]
),
.executableTarget( .executableTarget(
name: "DewPointController", name: "DewPointController",
dependencies: [ dependencies: [

View File

@@ -14,25 +14,49 @@ public extension DependencyValues {
} }
} }
/// Represents the interface needed for the command line application.
///
///
@DependencyClient @DependencyClient
public struct CliClient { public struct CliClient {
/// Parse a log level from the given `EnvVars`.
public var logLevel: @Sendable (EnvVars) -> Logger.Level = { _ in .debug } public var logLevel: @Sendable (EnvVars) -> Logger.Level = { _ in .debug }
/// Generate the `EnvVars` with the given parameters.
public var makeEnvVars: @Sendable (EnvVarsRequest) async throws -> EnvVars public var makeEnvVars: @Sendable (EnvVarsRequest) async throws -> EnvVars
/// Generate the `MQTTClient` with the given parameters.
public var makeClient: @Sendable (ClientRequest) throws -> MQTTClient public var makeClient: @Sendable (ClientRequest) throws -> MQTTClient
/// Attempt to parse a string to an `MQTTClient.Version`.
public var parseMqttClientVersion: @Sendable (String) -> MQTTClient.Version? public var parseMqttClientVersion: @Sendable (String) -> MQTTClient.Version?
/// Represents the parameters needed to create an `MQTTClient`.
///
public struct ClientRequest: Sendable { public struct ClientRequest: Sendable {
public let envVars: EnvVars
/// The environment variables used to create the client.
public let environment: EnvVars
/// The event loop group for the client.
public let eventLoopGroup: MultiThreadedEventLoopGroup public let eventLoopGroup: MultiThreadedEventLoopGroup
/// A logger to use with the client.
public let logger: Logger? public let logger: Logger?
/// Create a new client request.
///
/// - Parameters:
/// - environment: The environment variables to use.
/// - eventLoopGroup: The event loop group to use on the client.
/// - logger: An optional logger to use on the client.
public init( public init(
envVars: EnvVars, environment: EnvVars,
eventLoopGroup: MultiThreadedEventLoopGroup, eventLoopGroup: MultiThreadedEventLoopGroup,
logger: Logger? logger: Logger?
) { ) {
self.envVars = envVars self.environment = environment
self.eventLoopGroup = eventLoopGroup self.eventLoopGroup = eventLoopGroup
self.logger = logger self.logger = logger
} }
@@ -54,6 +78,7 @@ public struct CliClient {
self.logger = logger self.logger = logger
self.mqttClientVersion = mqttClientVersion self.mqttClientVersion = mqttClientVersion
} }
} }
} }
@@ -63,7 +88,7 @@ extension CliClient: DependencyKey {
Self( Self(
logLevel: { Logger.Level.from(environment: $0) }, logLevel: { Logger.Level.from(environment: $0) },
makeEnvVars: { makeEnvVars: {
try EnvVars.load( try await EnvVars.load(
dotEnvFile: $0.envFilePath, dotEnvFile: $0.envFilePath,
logger: $0.logger, logger: $0.logger,
version: $0.mqttClientVersion version: $0.mqttClientVersion
@@ -71,7 +96,7 @@ extension CliClient: DependencyKey {
}, },
makeClient: { makeClient: {
MQTTClient( MQTTClient(
envVars: $0.envVars, environment: $0.environment,
eventLoopGroup: $0.eventLoopGroup, eventLoopGroup: $0.eventLoopGroup,
logger: $0.logger logger: $0.logger
) )
@@ -81,35 +106,46 @@ extension CliClient: DependencyKey {
} }
} }
// MARK: - Helpers
extension EnvironmentDependency {
func dotEnvDict(path: String?) async throws -> [String: String] {
guard let path,
let file = FileType(path: path)
else { return [:] }
return try await load(file)
}
}
extension EnvVars { extension EnvVars {
/// Load the `EnvVars` from the environment. /// Load the `EnvVars` from the environment.
/// ///
/// - Paramaters: /// - Paramaters:
/// - dotEnvFile: An optional environment file to load.
/// - logger: An optional logger to use for debugging. /// - logger: An optional logger to use for debugging.
/// - version: A version that is specified from command line, ignoring any environment variable. /// - version: A version that is specified from command line, ignoring any environment variable.
static func load( static func load(
dotEnvFile: String?, dotEnvFile: String?,
logger: Logger?, logger: Logger?,
version: String? version: String?
) throws -> EnvVars { ) async throws -> EnvVars {
@Dependency(\.environment) var environment
let defaultEnvVars = EnvVars() let defaultEnvVars = EnvVars()
let encoder = JSONEncoder() let coders = environment.coders()
let decoder = JSONDecoder()
if let dotEnvFile { let defaultEnvDict = (try? coders.encode(defaultEnvVars))
try DotEnv.load(path: dotEnvFile) .flatMap { try? coders.decode([String: String].self, from: $0) }
}
let defaultEnvDict = (try? encoder.encode(defaultEnvVars))
.flatMap { try? decoder.decode([String: String].self, from: $0) }
?? [:] ?? [:]
let dotEnvDict = try await environment.dotEnvDict(path: dotEnvFile)
let envVarsDict = defaultEnvDict let envVarsDict = defaultEnvDict
.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 }) .merging(environment.processInfo(), uniquingKeysWith: { $1 })
.merging(dotEnvDict, uniquingKeysWith: { $1 })
var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict)) var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict))
.flatMap { try? decoder.decode(EnvVars.self, from: $0) } .flatMap { try? coders.decode(EnvVars.self, from: $0) }
?? defaultEnvVars ?? defaultEnvVars
if let version { if let version {
@@ -125,7 +161,7 @@ extension EnvVars {
@_spi(Internal) @_spi(Internal)
public extension MQTTClient { public extension MQTTClient {
convenience init( convenience init(
envVars: EnvVars, environment envVars: EnvVars,
eventLoopGroup: EventLoopGroup, eventLoopGroup: EventLoopGroup,
logger: Logger? logger: Logger?
) { ) {
@@ -136,7 +172,7 @@ public extension MQTTClient {
eventLoopGroupProvider: .shared(eventLoopGroup), eventLoopGroupProvider: .shared(eventLoopGroup),
logger: logger, logger: logger,
configuration: .init( configuration: .init(
version: .parseOrDefualt(string: envVars.version), version: .parseOrDefault(string: envVars.version),
disablePing: false, disablePing: false,
userName: envVars.userName, userName: envVars.userName,
password: envVars.password password: envVars.password
@@ -145,10 +181,11 @@ public extension MQTTClient {
} }
} }
extension MQTTClient.Version { @_spi(Internal)
public extension MQTTClient.Version {
static let `default` = Self.v3_1_1 static let `default` = Self.v3_1_1
static func parseOrDefualt(string: String?) -> Self { static func parseOrDefault(string: String?) -> Self {
guard let string, let value = Self(string: string) else { guard let string, let value = Self(string: string) else {
return .default return .default
} }
@@ -166,8 +203,7 @@ extension MQTTClient.Version {
} }
} }
@_spi(Internal) extension Logger.Level {
public extension Logger.Level {
/// Parse a `Logger.Level` from the loaded `EnvVars`. /// Parse a `Logger.Level` from the loaded `EnvVars`.
static func from(environment envVars: EnvVars) -> Self { static func from(environment envVars: EnvVars) -> Self {

View File

@@ -0,0 +1,134 @@
import Dependencies
import DependenciesMacros
import DotEnv
import Foundation
import Models
@_spi(Internal)
public extension DependencyValues {
/// A dependecy responsible for loding environment variables.
///
/// This is just used internally of this module, but is useful to
/// override for testing purposes, so import using `@_spi(Internal)`.
var environment: EnvironmentDependency {
get { self[EnvironmentDependency.self] }
set { self[EnvironmentDependency.self] = newValue }
}
}
/// Responsible for loading environment variables and files.
///
///
@_spi(Internal)
@DependencyClient
public struct EnvironmentDependency: Sendable {
public var coders: @Sendable () -> any Coderable = { JSONCoders() }
/// Load the variables based on the request.
public var load: @Sendable (FileType) async throws -> [String: String] = { _ in [:] }
/// Load the environment variables based on the current process environment.
///
/// You can override this to use an empty environment, which is useful for testing purposes.
public var processInfo: @Sendable () -> [String: String] = { [:] }
/// Represents the types of files that can be loaded and decoded into
/// the environment.
public enum FileType: Equatable {
case dotEnv(path: String)
case json(path: String)
public init?(path: String) {
let strings = path.split(separator: ".")
guard let ext = strings.last else {
return nil
}
switch ext {
case "env":
self = .dotEnv(path: path)
case "json":
self = .json(path: path)
default:
return nil
}
}
}
}
struct DecodeError: Error {}
@_spi(Internal)
extension EnvironmentDependency: DependencyKey {
public static let testValue: EnvironmentDependency = Self()
public static func live(
decoder: JSONDecoder = .init(),
encoder: JSONEncoder = .init()
) -> Self {
Self(
coders: { JSONCoders(decoder: decoder, encoder: encoder) },
load: { file in
switch file {
case let .dotEnv(path: path):
let file = try DotEnv.read(path: path)
return file.lines.reduce(into: [String: String]()) { partialResult, line in
partialResult[line.key] = line.value
}
case let .json(path: path):
let url = url(for: path)
return try decoder.decode(
[String: String].self,
from: Data(contentsOf: url)
)
}
},
processInfo: { ProcessInfo.processInfo.environment }
)
}
public static let liveValue: EnvironmentDependency = .live()
}
/// A type that encode and decode values.
///
/// This is really just here to override tests with coders that will throw an error,
/// instead of encoding or decoding data.
///
@_spi(Internal)
public protocol Coderable {
func encode<T: Encodable>(_ instance: T) throws -> Data
func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T
}
struct JSONCoders: Coderable {
let decoder: JSONDecoder
let encoder: JSONEncoder
init(
decoder: JSONDecoder = .init(),
encoder: JSONEncoder = .init()
) {
self.decoder = decoder
self.encoder = encoder
}
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
try encoder.encode(instance)
}
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
try decoder.decode(T.self, from: data)
}
}
private func url(for path: String) -> URL {
#if os(Linux)
return URL(fileURLWithPath: path)
#else
return URL(filePath: path)
#endif
}

View File

@@ -13,11 +13,11 @@ extension Application {
static let configuration: CommandConfiguration = .init( static let configuration: CommandConfiguration = .init(
commandName: "debug", commandName: "debug",
abstract: "Debug the environment variables." abstract: "Debug the environment variables and command line arguments."
) )
@OptionGroup @OptionGroup
var shared: SharedOptions var options: SharedOptions
@Flag( @Flag(
name: [.customLong("show-password")], name: [.customLong("show-password")],
@@ -31,7 +31,7 @@ extension Application {
print("--------------------------") print("--------------------------")
print("Running debug command...") print("Running debug command...")
if let envFile = shared.envFile { if let envFile = options.envFile {
print("Reading env file: \(envFile)") print("Reading env file: \(envFile)")
print("--------------------------") print("--------------------------")
} else { } else {
@@ -41,12 +41,12 @@ extension Application {
print("Loading EnvVars") print("Loading EnvVars")
print("--------------------------") print("--------------------------")
let envVars = try await client.makeEnvVars(shared.envVarsRequest(logger: logger)) let envVars = try await client.makeEnvVars(options.envVarsRequest(logger: logger))
printEnvVars(envVars: envVars, showPassword: showPassword) printEnvVars(envVars: envVars, showPassword: showPassword)
print("--------------------------") print("--------------------------")
if let logLevel = shared.logLevel, let level = logLevel() { if let logLevel = options.logLevel {
print("Log Level option: \(level)") print("Log Level option: \(logLevel)")
print("--------------------------") print("--------------------------")
} else { } else {
print("Log Level option: nil") print("Log Level option: nil")

View File

@@ -23,14 +23,13 @@ extension Application {
) )
@OptionGroup @OptionGroup
var shared: SharedOptions var options: SharedOptions
mutating func run() async throws { mutating func run() async throws {
@Dependency(\.cliClient) var cliClient @Dependency(\.cliClient) var cliClient
let eventloopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) let (mqtt, logger) = try await cliClient.setupRun(options: options)
var logger = Logger(label: "dewpoint-controller") logger.info("Setting up environment...")
let mqtt = try await setup(eventLoopGroup: eventloopGroup, logger: &logger)
do { do {
try await withDependencies { try await withDependencies {
@@ -48,6 +47,9 @@ extension Application {
gracefulShutdownSignals: [.sigterm, .sigint], gracefulShutdownSignals: [.sigterm, .sigint],
logger: logger logger: logger
) )
// These settings prevent services from running forever after we've
// received a shutdown signal. In general it should not needed unless the
// services don't shutdown their async streams properly.
serviceGroupConfiguration.maximumCancellationDuration = .seconds(5) serviceGroupConfiguration.maximumCancellationDuration = .seconds(5)
serviceGroupConfiguration.maximumGracefulShutdownDuration = .seconds(10) serviceGroupConfiguration.maximumGracefulShutdownDuration = .seconds(10)
@@ -57,30 +59,32 @@ extension Application {
try await serviceGroup.run() try await serviceGroup.run()
} }
// Here we've received a shutdown signal and shutdown all the
// services.
try await mqtt.shutdown() try await mqtt.shutdown()
try await eventloopGroup.shutdownGracefully()
} catch { } catch {
try await eventloopGroup.shutdownGracefully() // If something fails, shutdown the mqtt client.
try await mqtt.shutdown()
} }
} }
}
}
private func setup( private extension CliClient {
eventLoopGroup: MultiThreadedEventLoopGroup, func setupRun(
logger: inout Logger eventLoopGroup: MultiThreadedEventLoopGroup = .init(numberOfThreads: 1),
) async throws -> MQTTClient { loggerLabel: String = "dewpoint-controller",
@Dependency(\.cliClient) var cliClient options: Application.SharedOptions
) async throws -> (MQTTClient, Logger) {
let environment = try await cliClient.makeEnvVars(shared.envVarsRequest(logger: logger)) var logger = Logger(label: loggerLabel)
logger.logLevel = cliClient.logLevel(environment) let environment = try await makeEnvVars(options.envVarsRequest(logger: logger))
logger.logLevel = logLevel(environment)
return try cliClient.makeClient(.init( let client = try makeClient(.init(
envVars: environment, environment: environment,
eventLoopGroup: eventLoopGroup, eventLoopGroup: eventLoopGroup,
logger: logger logger: logger
)) ))
}
}
return (client, logger)
}
} }
// MARK: - Helpers

View File

@@ -17,7 +17,7 @@ extension Application {
name: [.short, .customLong("log-level")], name: [.short, .customLong("log-level")],
help: "Set the logging level." help: "Set the logging level."
) )
var logLevel: LogLevelContainer? var logLevelContainer: LogLevelContainer?
@Option( @Option(
name: [.short, .long], name: [.short, .long],
@@ -29,6 +29,8 @@ extension Application {
.init(envFilePath: envFile, logger: logger, version: version) .init(envFilePath: envFile, logger: logger, version: version)
} }
var logLevel: Logger.Level? { logLevelContainer?.logLevel }
} }
} }

View File

@@ -1,10 +0,0 @@
import Models
@_spi(Internal)
public extension Array where Element == TemperatureAndHumiditySensor {
static var live: Self {
TemperatureAndHumiditySensor.Location.allCases.map { location in
TemperatureAndHumiditySensor(location: location)
}
}
}

View File

@@ -139,3 +139,11 @@ public struct TemperatureAndHumiditySensor: Identifiable, Sendable {
} }
} }
} }
public extension Array where Element == TemperatureAndHumiditySensor {
static var live: Self {
TemperatureAndHumiditySensor.Location.allCases.map {
TemperatureAndHumiditySensor(location: $0)
}
}
}

View File

@@ -0,0 +1,212 @@
@_spi(Internal) import CliClient
import Dependencies
import Foundation
import Logging
import Models
import MQTTNIO
import XCTest
final class CliClientTests: XCTestCase {
override func invokeTest() {
withDependencies {
$0.cliClient = .liveValue
$0.environment = .liveValue
$0.environment.processInfo = { [:] }
} operation: {
super.invokeTest()
}
}
func testParsingMQTTVersion() {
@Dependency(\.cliClient) var cliClient
let arguments = [
(MQTTClient.Version.v3_1_1, ["3", "3.1", "3.1.1", "00367894"]),
(MQTTClient.Version.v5_0, ["5", "5.1", "5.1.1", "00000500012"]),
(nil, ["0", "0.1", "0.1.1", "0000000001267894", "blob"])
]
for (version, strings) in arguments {
for string in strings {
XCTAssertEqual(cliClient.parseMqttClientVersion(string), version)
}
}
XCTAssertEqual(MQTTClient.Version.parseOrDefault(string: nil), .v3_1_1)
}
func testLogLevelFromEnvironment() {
@Dependency(\.cliClient) var cliClient
let arguments = [
(Logger.Level.debug, EnvVars(appEnv: .staging, logLevel: nil)),
(Logger.Level.debug, EnvVars(appEnv: .development, logLevel: nil)),
(Logger.Level.info, EnvVars(appEnv: .production, logLevel: nil)),
(Logger.Level.trace, EnvVars(appEnv: .testing, logLevel: nil)),
(Logger.Level.info, EnvVars(appEnv: .staging, logLevel: .info)),
(Logger.Level.trace, EnvVars(appEnv: .development, logLevel: .trace)),
(Logger.Level.warning, EnvVars(appEnv: .production, logLevel: .warning)),
(Logger.Level.debug, EnvVars(appEnv: .testing, logLevel: .debug))
]
for (expected, envVars) in arguments {
XCTAssertEqual(expected, cliClient.logLevel(envVars))
}
}
func testMakeEnvVars() async throws {
@Dependency(\.cliClient) var cliClient
@Dependency(\.environment) var environment
let arguments = [
(
CliClient.EnvVarsRequest(envFilePath: nil, logger: nil, version: nil),
EnvVars()
),
(
CliClient.EnvVarsRequest(envFilePath: nil, logger: nil, version: "3"),
EnvVars(version: "3")
),
(
CliClient.EnvVarsRequest(
envFilePath: "test.env", // Needs to be a bundled resource.
logger: nil,
version: nil
),
EnvVars.test
),
(
CliClient.EnvVarsRequest(
envFilePath: "test-env.json", // Needs to be a bundled resource.
logger: nil,
version: nil
),
EnvVars.test
)
]
for (request, expectedEnvVars) in arguments {
var request = request
if let file = request.envFilePath {
request = .init(
envFilePath: cleanFilePath(file),
logger: request.logger,
version: request.mqttClientVersion
)
}
let result = try await cliClient.makeEnvVars(request)
XCTAssertEqual(result, expectedEnvVars)
}
}
func testMakeEnvVarsWithFailingDecoder() async throws {
try await withDependencies {
$0.environment.coders = { ThrowingDecoder() }
} operation: {
@Dependency(\.cliClient) var cliClient
let envVars = try await cliClient.makeEnvVars(.init())
XCTAssertEqual(envVars, EnvVars())
}
}
func testMakeEnvVarsWithFailingEncoder() async throws {
try await withDependencies {
$0.environment.coders = { ThrowingEncoder() }
} operation: {
@Dependency(\.cliClient) var cliClient
let envVars = try await cliClient.makeEnvVars(.init())
XCTAssertEqual(envVars, EnvVars())
}
}
func testFileType() {
let arguments = [
(EnvironmentDependency.FileType.dotEnv(path: "test.env"), "test.env"),
(EnvironmentDependency.FileType.json(path: "test.json"), "test.json"),
(nil, "test"),
(nil, "")
]
for (expected, file) in arguments {
XCTAssertEqual(EnvironmentDependency.FileType(path: file), expected)
}
}
func testEnvironmentLiveValueProcessInfo() {
let environment = EnvironmentDependency.liveValue
XCTAssertEqual(environment.processInfo(), ProcessInfo.processInfo.environment)
}
func testMakeClient() throws {
@Dependency(\.cliClient) var cliClient
let envVars = EnvVars.test
let client = try cliClient.makeClient(.init(
environment: envVars,
eventLoopGroup: .init(numberOfThreads: 1),
logger: nil
))
XCTAssertEqual(client.host, envVars.host)
XCTAssertEqual(client.port, Int(envVars.port!))
XCTAssertEqual(client.identifier, envVars.identifier)
XCTAssertEqual(client.configuration.version, .v5_0)
XCTAssertEqual(client.configuration.userName, envVars.userName)
XCTAssertEqual(client.configuration.password, envVars.password)
try client.syncShutdownGracefully()
}
}
// - MARK: Helper
private func cleanFilePath(_ path: String) -> String {
#if os(Linux)
return "Tests/CliClientTests/\(path)"
#else
let split = path.split(separator: ".")
let fileName = split.first!
let ext = split.last!
let url = Bundle.module.url(forResource: String(fileName), withExtension: String(ext))!.absoluteString
let cleaned = url.split(separator: "file://").last!
return String(cleaned)
#endif
}
extension EnvVars {
static let test = EnvVars(
appEnv: .testing,
host: "test.mqtt",
port: "1234",
identifier: "testing-mqtt",
userName: "test-user",
password: "super-secret",
logLevel: .debug,
version: "5.0"
)
}
struct ThrowingDecoder: Coderable {
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
try JSONEncoder().encode(instance)
}
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
throw DecodeError()
}
}
struct ThrowingEncoder: Coderable {
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
throw EncodeError()
}
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
try JSONDecoder().decode(T.self, from: data)
}
}
struct DecodeError: Error {}
struct EncodeError: Error {}

View File

@@ -0,0 +1,10 @@
{
"APP_ENV": "testing",
"MQTT_HOST": "test.mqtt",
"MQTT_PORT": "1234",
"MQTT_IDENTIFIER": "testing-mqtt",
"MQTT_USERNAME": "test-user",
"MQTT_PASSWORD": "super-secret",
"LOG_LEVEL": "debug",
"MQTT_VERSION": "5.0"
}

View File

@@ -0,0 +1,8 @@
APP_ENV="testing"
MQTT_HOST="test.mqtt"
MQTT_PORT="1234"
MQTT_IDENTIFIER="testing-mqtt"
MQTT_USERNAME="test-user"
MQTT_PASSWORD="super-secret"
LOG_LEVEL="debug"
MQTT_VERSION="5.0"

View File

@@ -14,7 +14,6 @@ import ServiceLifecycleTestKit
import XCTest import XCTest
final class IntegrationTests: XCTestCase { final class IntegrationTests: XCTestCase {
static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost" static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost"
static let logger: Logger = { static let logger: Logger = {
@@ -25,7 +24,6 @@ final class IntegrationTests: XCTestCase {
override func invokeTest() { override func invokeTest() {
let client = createClient(identifier: "\(Self.self)") let client = createClient(identifier: "\(Self.self)")
withDependencies { withDependencies {
$0.mqtt = .live(client: client, logger: Self.logger) $0.mqtt = .live(client: client, logger: Self.logger)
$0.psychrometricClient = PsychrometricClient.liveValue $0.psychrometricClient = PsychrometricClient.liveValue
@@ -36,7 +34,7 @@ final class IntegrationTests: XCTestCase {
func testConnectionServiceShutdown() async throws { func testConnectionServiceShutdown() async throws {
@Dependency(\.mqtt) var mqtt @Dependency(\.mqtt) var mqtt
do {
let service = MQTTConnectionService(logger: Self.logger) let service = MQTTConnectionService(logger: Self.logger)
let task = Task { try await service.run() } let task = Task { try await service.run() }
defer { task.cancel() } defer { task.cancel() }
@@ -50,11 +48,14 @@ final class IntegrationTests: XCTestCase {
mqtt.shutdown() mqtt.shutdown()
try await Task.sleep(for: .milliseconds(500)) try await Task.sleep(for: .milliseconds(500))
// check the connection is active here. // check the connection is active here.
try await mqtt.withClient { client in try await mqtt.withClient { client in
XCTAssertFalse(client.isActive()) XCTAssertFalse(client.isActive())
} }
} catch {
mqtt.shutdown()
try await Task.sleep(for: .milliseconds(500))
}
} }
func testMQTTConnectionStream() async throws { func testMQTTConnectionStream() async throws {
@@ -188,11 +189,9 @@ final class IntegrationTests: XCTestCase {
configuration: config configuration: config
) )
} }
} }
// - MARK: Helpers // - MARK: Helpers
struct TopicNotFoundError: Error {} struct TopicNotFoundError: Error {}
actor ResultContainer: Sendable { actor ResultContainer: Sendable {

View File

@@ -1,5 +1,6 @@
# Used to build a test image. # Used to build a test image.
FROM swift:5.10 ARG SWIFT_IMAGE_VERSION="5.10"
FROM swift:${SWIFT_IMAGE_VERSION}
WORKDIR /app WORKDIR /app
COPY ./Package.* ./ COPY ./Package.* ./
RUN swift package resolve RUN swift package resolve

View File

@@ -1,4 +1,6 @@
# run this with docker-compose run test # run this with docker-compose run test
name: swift-mqtt-dewpoint-test
services: services:
test: test:
build: build:

View File

@@ -1,4 +1,6 @@
# run this with docker-compose run dewpoint_controller # run this with docker-compose run dewpoint_controller
name: swift-mqtt-dewpoint
services: services:
dewpoint_controller: dewpoint_controller:
container_name: dewpoint-controller container_name: dewpoint-controller