Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0586443
chore: Opt-in service client tests
jbelkins Nov 17, 2025
18d5df5
Enable service tests on CI
jbelkins Nov 17, 2025
1da879e
Move dependency JSON in from SDK as a SwiftIntegration
jbelkins Nov 17, 2025
ecf5a57
Fix ktlint
jbelkins Nov 17, 2025
d0abf40
Add Dependencies.json for internal clients
jbelkins Nov 17, 2025
a62423a
Refactor CLI, fix CLI tests
jbelkins Nov 17, 2025
01184eb
Merge branch 'main' into jbe/remove_service_client_tests
jbelkins Nov 17, 2025
395de59
Don't publish internal modules
jbelkins Nov 18, 2025
c1be323
Fix protocol tests
jbelkins Nov 18, 2025
63fedc1
Don't test internal clients
jbelkins Nov 18, 2025
565b61b
Make ServiceClientData Sendable
jbelkins Nov 18, 2025
f70827e
Move package JSON generator to last integration run
jbelkins Nov 18, 2025
77740b8
Fix Swift 6 language mode in manifest
jbelkins Nov 18, 2025
ed6e759
Omit SmithyTestUtil from dependencies
jbelkins Nov 18, 2025
0d653a1
Refactor isInternal bool to an enum
jbelkins Nov 18, 2025
bc4dbfa
Fix tests
jbelkins Nov 18, 2025
0358490
Revert continuous-integration workflow
jbelkins Nov 19, 2025
bb3b0cb
Merge branch 'main' into jbe/remove_service_client_tests
jbelkins Nov 19, 2025
5090eb9
Merge branch 'main' into jbe/remove_service_client_tests
jbelkins Nov 25, 2025
cc6fb28
Correct caps in AWSSignin
jbelkins Nov 25, 2025
1aa9c18
Merge branch 'main' into jbe/remove_service_client_tests
jbelkins Nov 25, 2025
da5a648
Fix PrepareRelease CLI tests
jbelkins Nov 25, 2025
128b5cf
Merge branch 'main' into jbe/remove_service_client_tests
jbelkins Dec 2, 2025
0d41c80
Refactor service client logic, sort services
jbelkins Dec 2, 2025
39a5572
Update Package.swift
jbelkins Dec 2, 2025
2b217b3
Merge branch 'main' into jbe/remove_service_client_tests
jbelkins Dec 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/codegen-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:

env:
AWS_SWIFT_SDK_USE_LOCAL_DEPS: 1
AWS_SWIFT_SDK_ENABLE_SERVICE_TESTS: 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codegen-build-test is where all service tests run in CI.


jobs:
codegen-build-test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,43 @@ struct GeneratePackageManifest {
///
/// - Returns: The contents of the generated package manifest.
func generatePackageManifestContents() throws -> String {

struct ServiceClientInfo: Decodable {
let modelPath: String
let dependencies: [String]
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above is the decodable Swift type used to read the contents of Dependencies.json

Below is code that reads Dependencies.json and uses its contents to construct a PackageManifestBuilder.Service for each published and internal service.

The combined list of services is then passed into the manifest builder for inclusion in the text of the manifest.


let versions = try resolveVersions()
let services = try resolveServices().map { PackageManifestBuilder.Service(name: $0) }
let serviceNames = try resolveServices()
let services = serviceNames.map { name in
let fileURL = URL(fileURLWithPath: "Sources/Services/\(name)/Dependencies.json")
let data = try! Data(contentsOf: fileURL)
let info = try! JSONDecoder().decode(ServiceClientInfo.self, from: data)
let modelFileName = URL(fileURLWithPath: info.modelPath).lastPathComponent
return PackageManifestBuilder.Service(
moduleName: name,
codegenName: modelFileName,
dependencies: info.dependencies,
isInternal: false
)
}

let internalServiceNames = ["AWSSTS", "AWSSSO", "AWSSSOOIDC", "AWSCognitoIdentity", "AWSSignin"]
let internalServices = internalServiceNames.map { name in
let fileURL = URL(fileURLWithPath: "Sources/Core/AWSSDKIdentity/InternalClients/Internal\(name)/Dependencies.json")
let data = try! Data(contentsOf: fileURL)
let info = try! JSONDecoder().decode(ServiceClientInfo.self, from: data)
let modelFileName = URL(fileURLWithPath: info.modelPath).lastPathComponent
return PackageManifestBuilder.Service(
moduleName: "Internal\(name)",
codegenName: modelFileName,
dependencies: info.dependencies,
isInternal: true
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor to reduce code duplication + eliminate !

func loadService(name: String, basePath: String, isInternal: Bool) throws -> PackageManifestBuilder.Service {
    let fileURL = URL(fileURLWithPath: "\(basePath)/\(name)/Dependencies.json")
    let data = try Data(contentsOf: fileURL)
    let info = try JSONDecoder().decode(ServiceClientInfo.self, from: data)
    let modelFileName = URL(fileURLWithPath: info.modelPath).lastPathComponent
    return PackageManifestBuilder.Service(
        moduleName: name,
        codegenName: modelFileName,
        dependencies: info.dependencies,
        isInternal: isInternal
    )
}

let serviceNames = try resolveServices()
let services = try serviceNames.map { try loadService(name: $0, basePath: "Sources/Services", isInternal: false) }

let internalServiceNames = ["AWSSTS", "AWSSSO", "AWSSSOOIDC", "AWSCognitoIdentity", "AWSSignin"]
let internalServices = try internalServiceNames.map {
    try loadService(name: "Internal\($0)", basePath: "Sources/Core/AWSSDKIdentity/InternalClients", isInternal: true)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored as requested. I also added .sorted() to the two lists of services to ensure the order is determinate.


log("Creating package manifest contents...")
let contents = try buildPackageManifest(versions.clientRuntime, versions.crt, services)
let contents = try buildPackageManifest(versions.clientRuntime, versions.crt, services + internalServices)
log("Successfully created package manifest contents")
return contents
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import AWSCLIUtils

/// Builds the contents of the package manifest file.
struct PackageManifestBuilder {

struct Service {
let name: String
let moduleName: String
let codegenName: String
let dependencies: [String]
let isInternal: Bool
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An array of this data structure is passed in, with info about every service client to be included in the manifest.


let clientRuntimeVersion: Version
Expand Down Expand Up @@ -107,13 +111,6 @@ struct PackageManifestBuilder {
// Add the generated content that defines the list of services to include
buildServiceTargets(),
"",
// Add the dependencies for the internal clients
buildInternalAWSSTSDependencies(),
buildInternalAWSSSODependencies(),
buildInternalAWSSSOOIDCDependencies(),
buildInternalAWSCognitoIdentityDependencies(),
buildInternalAWSSignInDependencies(),
"",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here & below, special code for internal services is removed since they are now handled like all other services and filtered as needed into public/internal at runtime.

]
return contents.joined(separator: .newline)
}
Expand Down Expand Up @@ -147,51 +144,32 @@ struct PackageManifestBuilder {
/// and calls the `addServiceTarget` for each item.
private func buildServiceTargets() -> String {
guard !services.isEmpty else {
return "let serviceTargets: [String: [Target.Dependency]] = [:]"
return "private let serviceClientData: [ServiceClientData] = []"
}
var lines: [String] = []
lines += ["let serviceTargets: [String: [Target.Dependency]] = ["]
lines += services.map {
let jsonFilePath = "Sources/Services/\($0.name)/Dependencies.json"
let dependencies = clientDependencies(service: $0, jsonFilePath: jsonFilePath)
return " \($0.name.wrappedInQuotes()): \(dependencies),"
}
lines += ["private let serviceClientData: [ServiceClientData] = ["]
lines += services.map { serviceTargetData(service: $0) }
lines += ["]"]
return lines.joined(separator: .newline)
}

private func buildInternalAWSSTSDependencies() -> String {
buildInternalClientDependencies(name: "AWSSTS")
}

private func buildInternalAWSSSODependencies() -> String {
buildInternalClientDependencies(name: "AWSSSO")
}

private func buildInternalAWSSSOOIDCDependencies() -> String {
buildInternalClientDependencies(name: "AWSSSOOIDC")
}

private func buildInternalAWSCognitoIdentityDependencies() -> String {
buildInternalClientDependencies(name: "AWSCognitoIdentity")
}

private func buildInternalAWSSignInDependencies() -> String {
buildInternalClientDependencies(name: "AWSSignin")
}

private func buildInternalClientDependencies(name: String) -> String {
let jsonFilePath = "Sources/Core/AWSSDKIdentity/InternalClients/Internal\(name)/Dependencies.json"
let service = Service(name: name)
let dependencies = clientDependencies(service: service, jsonFilePath: jsonFilePath)
return "let internal\(name)Dependencies: [Target.Dependency] = \(dependencies)"
private func serviceTargetData(service: Service) -> String {
var lines = [String]()
lines += [" .init("]
lines += [" \(service.moduleName.wrappedInQuotes()),"]
lines += [" \(service.codegenName.wrappedInQuotes()),"]
if service.isInternal {
lines += [" \(clientDependencies(service: service)),"]
lines += [" .internalUse"]
} else {
lines += [" \(clientDependencies(service: service))"]
}
lines += [" ),"]
return lines.joined(separator: "\n")
}

private func clientDependencies(service: Service, jsonFilePath: String) -> String {
let jsonFileData = FileManager.default.contents(atPath: jsonFilePath) ?? Data("[]".utf8)
let dependencies = try! JSONDecoder().decode([String].self, from: jsonFileData)
.filter { $0 != "SmithyTestUtil" } // links to test files only
return "[" + dependencies.map { ".\($0)" }.joined(separator: ", ") + "]"
private func clientDependencies(service: Service) -> String {
return "[" + service.dependencies.map { ".\($0)" }.joined(separator: ", ") + "]"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code for reading the JSON file has been moved from here back up to the subcommand. Instead, the array of Service that is passed into this type contains the dependency info for each service.

}
}

107 changes: 65 additions & 42 deletions AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Resources/Package.Base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ let package = Package(
],
products:
runtimeProducts +
serviceTargets.map(productForService(_:_:)),
serviceProducts,
dependencies:
[smithySwiftDependency, crtDependency, doccDependencyOrNil].compactMap { $0 },
dependencies,
targets:
runtimeTargets +
runtimeTestTargets +
serviceTargets.map(target(_:_:)) +
serviceTargets.map(unitTestTarget(_:_:))
serviceTargets +
serviceTestTargets
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the logic in the Package initializer above has been extracted to helper methods below.

)

// MARK: Products
Expand All @@ -78,12 +78,20 @@ private var runtimeProducts: [Product] {
].map { .library(name: $0, targets: [$0]) }
}

private func productForService(_ service: String, _ dependencies: [Target.Dependency]) -> Product {
.library(name: service, targets: [service])
private var serviceProducts: [Product] {
serviceClientData.filter { $0.serviceType == .awsServiceClient }.map(productForService(_:))
}

private func productForService(_ service: ServiceClientData) -> Product {
.library(name: service.name, targets: [service.name])
}

// MARK: Dependencies

private var dependencies: [Package.Dependency] {
[smithySwiftDependency, crtDependency, doccDependencyOrNil].compactMap { $0 }
}

private var smithySwiftDependency: Package.Dependency {
let previewPath = "./smithy-swift"
let developmentPath = "../smithy-swift"
Expand Down Expand Up @@ -196,31 +204,6 @@ private var runtimeTargets: [Target] {
],
path: "Sources/Core/AWSSDKIdentity/Sources/AWSSDKIdentity"
),
.target(
name: "InternalAWSSTS",
dependencies: internalAWSSTSDependencies,
path: "Sources/Core/AWSSDKIdentity/InternalClients/InternalAWSSTS/Sources/InternalAWSSTS"
),
.target(
name: "InternalAWSSSO",
dependencies: internalAWSSSODependencies,
path: "Sources/Core/AWSSDKIdentity/InternalClients/InternalAWSSSO/Sources/InternalAWSSSO"
),
.target(
name: "InternalAWSSSOOIDC",
dependencies: internalAWSSSOOIDCDependencies,
path: "Sources/Core/AWSSDKIdentity/InternalClients/InternalAWSSSOOIDC/Sources/InternalAWSSSOOIDC"
),
.target(
name: "InternalAWSCognitoIdentity",
dependencies: internalAWSCognitoIdentityDependencies,
path: "Sources/Core/AWSSDKIdentity/InternalClients/InternalAWSCognitoIdentity/Sources/InternalAWSCognitoIdentity"
),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hard-coded internal client targets are removed, since internal clients are now listed along with the public ones.

Instead, the internal services are filtered from the main list of services, and the targets for them are appended to runtime targets below.

.target(
name: "InternalAWSSignin",
dependencies: internalAWSSigninDependencies,
path: "Sources/Core/AWSSDKIdentity/InternalClients/InternalAWSSignin/Sources/InternalAWSSignin"
),
.target(
name: "AWSSDKChecksums",
dependencies: [
Expand All @@ -235,7 +218,11 @@ private var runtimeTargets: [Target] {
name: "AWSSDKDynamic",
path: "Sources/Core/AWSSDKDynamic/Sources/AWSSDKDynamic"
),
]
] + internalServiceTargets
}

private var internalServiceTargets: [Target] {
serviceClientData.filter { $0.serviceType == .internalUse }.map(target(_:))
}

private var runtimeTestTargets: [Target] {
Expand Down Expand Up @@ -288,24 +275,60 @@ private var runtimeTestTargets: [Target] {
]
}

private func target(_ service: String, _ dependencies: [Target.Dependency]) -> Target {
.target(
name: service,
dependencies: dependencies,
path: "Sources/Services/\(service)/Sources/\(service)"
)
private var serviceTargets: [Target] {
serviceClientData.filter { $0.serviceType == .awsServiceClient }.map(target(_:))
}

private func target(_ service: ServiceClientData) -> Target {
.target(name: service.name, dependencies: service.dependencies, path: service.sourcePath)
}

private func unitTestTarget(_ service: String, _ dependencies: [Target.Dependency]) -> Target {
let testName = "\(service)Tests"
private var serviceTestTargets: [Target] {
guard ProcessInfo.processInfo.environment["AWS_SWIFT_SDK_ENABLE_SERVICE_TESTS"] != nil else { return [] }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service test targets now default to [] if the new env var isn't set.

return serviceClientData.filter { $0.serviceType == .awsServiceClient }.map(unitTestTarget(_:))
}

private func unitTestTarget(_ service: ServiceClientData) -> Target {
let testName = "\(service.name)Tests"
let modelName = URL(fileURLWithPath: service.modelPath).lastPathComponent
return .testTarget(
name: "\(testName)",
dependencies: [
.byName(name: service),
.byName(name: service.name),
.ClientRuntime,
.AWSClientRuntime,
.SmithyTestUtil,
],
path: "Sources/Services/\(service)/Tests/\(testName)"
path: "codegen/sdk-codegen/build/smithyprojections/sdk-codegen/\(modelName)/swift-codegen/Tests/\(testName)"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path for tests now points to the Smithy build folder, where the code generator places them at creation, since they are no longer copied into the project.

)
}

// As of Swift 6.2, @unchecked is not needed, but for Swift 6.0 it is
Copy link
Contributor Author

@jbelkins jbelkins Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Swift 6.1 or 6.2 (not sure which), Target.Dependency was marked Sendable. No code changes were needed to do so.

We mark ServiceClientData as @unchecked Sendable because in the earlier Swift versions we support, Target.Dependency is not Sendable so compile breaks if we try to mark this type as Sendable and Swift 6 language mode is in use.

It's just to suppress the Swift 6 compile error though, because there is no concurrency in the Package.swift file anyway.

private struct ServiceClientData: @unchecked Sendable {

enum ServiceType {
case awsServiceClient
case internalUse
}

let name: String
let modelPath: String
let dependencies: [Target.Dependency]
let serviceType: ServiceType

init(_ name: String, _ modelPath: String, _ dependencies: [Target.Dependency], _ serviceType: ServiceType = .awsServiceClient) {
self.name = name
self.modelPath = modelPath
self.dependencies = dependencies
self.serviceType = serviceType
}

var sourcePath: String {
switch serviceType {
case .awsServiceClient:
return "Sources/Services/\(name)/Sources/\(name)"
case .internalUse:
return "Sources/Core/AWSSDKIdentity/InternalClients/\(name)/Sources/\(name)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,40 @@ class GeneratePackageManifestTests: CLITestCase {

func createServiceFolders(_ services: [String]) {
services.forEach {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test setup below is expanded to write Dependencies.json for each of the mocked services, since that file is required on every service to successfully create a manifest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the internal clients get mocked similarly as well.

let path = "Sources/Services/\($0)"
try! FileManager.default.createDirectory(
atPath: "Sources/Services/\($0)",
atPath: path,
withIntermediateDirectories: true
)
createDependencyJSON(service: $0, path: path)
}

let internalServices = ["AWSSTS", "AWSSSO", "AWSSSOOIDC", "AWSCognitoIdentity", "AWSSignin"]
internalServices.forEach {
let path = "Sources/Core/AWSSDKIdentity/InternalClients/Internal\($0)"
try! FileManager.default.createDirectory(
atPath: path,
withIntermediateDirectories: true
)
createDependencyJSON(service: $0, path: path)
}

try! FileManager.default.createDirectory(
atPath: "IntegrationTests/Services",
withIntermediateDirectories: true
)
}


func createDependencyJSON(service: String, path: String) {
let json = Data("""
{
"modelPath": "codegen/sdk-codegen/aws-models/\(service.lowercased()).json",
"dependencies": []
}
""".utf8)
FileManager.default.createFile(atPath: "\(path)/Dependencies.json", contents: json)
}

// MARK: - Tests

// MARK: Golden Path
Expand All @@ -50,13 +73,13 @@ class GeneratePackageManifestTests: CLITestCase {
clientRuntimeVersion: clientRuntimeVersion
)
createServiceFolders(services)

let subject = GeneratePackageManifest.mock(buildPackageManifest: { _clientRuntimeVersion, _crtVersion, services in
"\(_clientRuntimeVersion)-\(_crtVersion)-\(services.map(\.name).joined(separator: "-"))"
"\(_clientRuntimeVersion)-\(_crtVersion)-\(services.map(\.moduleName).joined(separator: "-"))"
})
try! subject.run()
let result = try! String(contentsOfFile: "Package.swift", encoding: .utf8)
XCTAssertEqual(result, "1.2.3-3.2.1-EC2-S3")
XCTAssertEqual(result, "1.2.3-3.2.1-EC2-S3-InternalAWSSTS-InternalAWSSSO-InternalAWSSSOOIDC-InternalAWSCognitoIdentity-InternalAWSSignin")
}

// MARK: resolveVersions()
Expand Down
Loading
Loading