diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift index f53b6841f05..1d21c45ea1c 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift @@ -236,7 +236,7 @@ struct PrepareRelease { ) throws { let commits = try Process.git.listOfCommitsBetween("HEAD", "\(previousVersion)") - let releaseNotes = ReleaseNotesBuilder( + let releaseNotes = try ReleaseNotesBuilder( previousVersion: previousVersion, newVersion: newVersion, repoOrg: repoOrg, diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift new file mode 100644 index 00000000000..526928fa588 --- /dev/null +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift @@ -0,0 +1,50 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import AWSCLIUtils + +struct FeaturesReader: Decodable { + private let requestFilePath: String + private let mappingFilePath: String + + public init( + requestFilePath: String = "../build-request.json", + mappingFilePath: String = "../feature-service-id.json" + ) { + self.requestFilePath = requestFilePath + self.mappingFilePath = mappingFilePath + } + + public func getFeaturesFromFile() throws -> Features { + let fileContents = try FileManager.default.loadContents(atPath: requestFilePath) + return try JSONDecoder().decode(Features.self, from: fileContents) + } + + public func getFeaturesIDToServiceNameDictFromFile() throws -> [String: String] { + let fileContents = try FileManager.default.loadContents(atPath: mappingFilePath) + return try JSONDecoder().decode([String: String].self, from: fileContents) + } +} + +struct Features: Decodable { + let features: [Feature] +} + +struct Feature: Decodable { + let releaseNotes: String + let featureMetadata: FeatureMetadata + + struct FeatureMetadata: Decodable { + let trebuchet: Trebuchet + + struct Trebuchet: Decodable { + let featureId: String + let featureType: String + } + } +} diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift index da4451fe660..5a9a4eb5828 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift @@ -15,24 +15,62 @@ struct ReleaseNotesBuilder { let repoOrg: PrepareRelease.Org let repoType: PrepareRelease.Repo let commits: [String] - + var featuresReader: FeaturesReader = FeaturesReader() + // MARK: - Build - - func build() -> String { - let contents = [ - "## What's Changed", - buildCommits(), - .newline, - "**Full Changelog**: https://github.com/\(repoOrg.rawValue)/\(repoType.rawValue)/compare/\(previousVersion)...\(newVersion)" + + func build() throws -> String { + let sdkChanges: [String] = buildSDKChangeSection() + let serviceClientChanges = repoType == .awsSdkSwift ? (try buildServiceChangeSection()) : [] + let fullCommitLogLink = [ + "\n**Full Changelog**: https://github.com/\(repoOrg.rawValue)/\(repoType.rawValue)/compare/\(previousVersion)...\(newVersion)" ] + let contents = ["## What's Changed"] + serviceClientChanges + sdkChanges + fullCommitLogLink return contents.joined(separator: .newline) } - - // Adds a preceding `*` to each commit string - // This renders the list of commits as a list in markdown - func buildCommits() -> String { - commits - .map { "* \($0)"} + + func buildSDKChangeSection() -> [String] { + let formattedCommits = commits + .filter { $0.hasPrefix("feat") || $0.hasPrefix("fix") } + .map { "* \($0)" } + .joined(separator: .newline) + if (!formattedCommits.isEmpty) { + return ["### Miscellaneous", formattedCommits] + } + return [] + } + + func buildServiceChangeSection() throws -> [String] { + let features = try featuresReader.getFeaturesFromFile() + let mapping = try featuresReader.getFeaturesIDToServiceNameDictFromFile() + return buildServiceFeatureSection(features, mapping) + buildServiceDocSection(features, mapping) + } + + private func buildServiceFeatureSection( + _ features: Features, + _ mapping: [String: String] + ) -> [String] { + let formattedFeatures = features.features + .filter { $0.featureMetadata.trebuchet.featureType == "NEW_FEATURE" } + .map { "* **AWS \(mapping[$0.featureMetadata.trebuchet.featureId]!)**: \($0.releaseNotes)" } + .joined(separator: .newline) + if (!formattedFeatures.isEmpty) { + return ["### Service Features", formattedFeatures] + } + return [] + } + + private func buildServiceDocSection( + _ features: Features, + _ mapping: [String: String] + ) -> [String] { + let formattedDocUpdates = features.features + .filter { $0.featureMetadata.trebuchet.featureType == "DOC_UPDATE" } + .map { "* **AWS \(mapping[$0.featureMetadata.trebuchet.featureId]!)**: \($0.releaseNotes)" } .joined(separator: .newline) + if (!formattedDocUpdates.isEmpty) { + return ["### Service Documentation", formattedDocUpdates] + } + return [] } } diff --git a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Commands/PrepareReleaseTests.swift b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Commands/PrepareReleaseTests.swift index c8b35cc1d4c..fac35a2bc75 100644 --- a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Commands/PrepareReleaseTests.swift +++ b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Commands/PrepareReleaseTests.swift @@ -45,6 +45,16 @@ class PrepareReleaseTests: CLITestCase { createPackageVersion(previousVersion) createNextPackageVersion(newVersion) + let buildRequest = """ + { + "features": [] + } + """ + FileManager.default.createFile(atPath: "build-request.json", contents: Data(buildRequest.utf8)) + + let mapping = "{}" + FileManager.default.createFile(atPath: "feature-service-id.json", contents: Data(mapping.utf8)) + let subject = PrepareRelease.mock(repoType: .awsSdkSwift, diffChecker: { _,_ in true }) try! subject.run() diff --git a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/PackageManifestBuilderTests.swift b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/PackageManifestBuilderTests.swift index 16f8b836ac1..ecb12da89a5 100644 --- a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/PackageManifestBuilderTests.swift +++ b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/PackageManifestBuilderTests.swift @@ -12,7 +12,6 @@ class PackageManifestBuilderTests: XCTestCase { let expected = """ - // MARK: - Dynamic Content let clientRuntimeVersion: Version = "1.2.3" diff --git a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift new file mode 100644 index 00000000000..c4c96ccbb3c --- /dev/null +++ b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift @@ -0,0 +1,187 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@testable import AWSSDKSwiftCLI +import AWSCLIUtils +import XCTest + +/* + * Regression tests for protection against change in generated release notes markdown content. + */ +class ReleaseNotesBuilderTests: XCTestCase { + /* Reusable feature strings */ + + // New feature 1 + private let feature1 = """ + { + "releaseNotes": "New feature description A.", + "featureMetadata": { + "trebuchet": { + "featureId": "feature-id-a", + "featureType": "NEW_FEATURE", + } + } + } + """ + + // New feature 2 + private let feature2 = """ + { + "releaseNotes": "New feature description B.", + "featureMetadata": { + "trebuchet": { + "featureId": "feature-id-b", + "featureType": "NEW_FEATURE", + } + } + } + """ + + // Documentation update + private let feature3 = """ + { + "releaseNotes": "Doc update description C.", + "featureMetadata": { + "trebuchet": { + "featureId": "feature-id-c", + "featureType": "DOC_UPDATE", + } + } + } + """ + + // Dictionary of feature ID to name of the service + private let mapping = """ + { + "feature-id-a": "Service 1", + "feature-id-b": "Service 2", + "feature-id-c": "Service 3" + } + """ + + func testAllSectionsPresent() throws { + let buildRequest = """ + { "features": [\(feature1), \(feature2), \(feature3)] } + """ + setUpBuildRequestAndMappingJSONs(buildRequest, mapping) + let builder = try setUpBuilder(testCommits: ["fix: Fix X", "feat: Feat Y"]) + let releaseNotes = try builder.build() + let expected = """ + ## What's Changed + ### Service Features + * **AWS Service 1**: New feature description A. + * **AWS Service 2**: New feature description B. + ### Service Documentation + * **AWS Service 3**: Doc update description C. + ### Miscellaneous + * fix: Fix X + * feat: Feat Y + + **Full Changelog**: https://github.com/awslabs/aws-sdk-swift/compare/1.0.0...1.0.1 + """ + XCTAssertEqual(releaseNotes, expected) + } + + func testNoServiceFeatureSectionPresent() throws { + let buildRequest = """ + { "features": [\(feature3)] } + """ + setUpBuildRequestAndMappingJSONs(buildRequest, mapping) + let builder = try setUpBuilder(testCommits: ["fix: Fix X", "feat: Feat Y"]) + let releaseNotes = try builder.build() + let expected = """ + ## What's Changed + ### Service Documentation + * **AWS Service 3**: Doc update description C. + ### Miscellaneous + * fix: Fix X + * feat: Feat Y + + **Full Changelog**: https://github.com/awslabs/aws-sdk-swift/compare/1.0.0...1.0.1 + """ + XCTAssertEqual(releaseNotes, expected) + } + + func testNoServiceDocSectionPresent() throws { + let buildRequest = """ + { "features": [\(feature1), \(feature2)] } + """ + setUpBuildRequestAndMappingJSONs(buildRequest, mapping) + let builder = try setUpBuilder(testCommits: ["fix: Fix X", "feat: Feat Y"]) + let releaseNotes = try builder.build() + let expected = """ + ## What's Changed + ### Service Features + * **AWS Service 1**: New feature description A. + * **AWS Service 2**: New feature description B. + ### Miscellaneous + * fix: Fix X + * feat: Feat Y + + **Full Changelog**: https://github.com/awslabs/aws-sdk-swift/compare/1.0.0...1.0.1 + """ + XCTAssertEqual(releaseNotes, expected) + } + + func testNoSDKChangeSectionPresent() throws { + let buildRequest = """ + { "features": [\(feature1), \(feature2), \(feature3)] } + """ + setUpBuildRequestAndMappingJSONs(buildRequest, mapping) + let builder = try setUpBuilder() + let releaseNotes = try builder.build() + let expected = """ + ## What's Changed + ### Service Features + * **AWS Service 1**: New feature description A. + * **AWS Service 2**: New feature description B. + ### Service Documentation + * **AWS Service 3**: Doc update description C. + + **Full Changelog**: https://github.com/awslabs/aws-sdk-swift/compare/1.0.0...1.0.1 + """ + XCTAssertEqual(releaseNotes, expected) + } + + func testNoSectionsPresentAndIrrelevantCommitsAreFiltered() throws { + let buildRequest = """ + { "features":[] } + """ + setUpBuildRequestAndMappingJSONs(buildRequest, mapping) + let builder = try setUpBuilder(testCommits: ["chore: Commit A", "Update X"]) + let releaseNotes = try builder.build() + let expected = """ + ## What's Changed + + **Full Changelog**: https://github.com/awslabs/aws-sdk-swift/compare/1.0.0...1.0.1 + """ + XCTAssertEqual(releaseNotes, expected) + } + + private func setUpBuildRequestAndMappingJSONs(_ buildRequest: String, _ mapping: String) { + // In real scenario, the JSON files we need are located one level above, in the workspace directory. + // For tests, due to sandboxing, the dummy files are created in current directory instead of + // in parent directory. + FileManager.default.createFile(atPath: "build-request.json", contents: Data(buildRequest.utf8)) + FileManager.default.createFile(atPath: "feature-service-id.json", contents: Data(mapping.utf8)) + } + + private func setUpBuilder(testCommits: [String] = []) throws -> ReleaseNotesBuilder { + return try ReleaseNotesBuilder( + previousVersion: Version("1.0.0"), + newVersion: Version("1.0.1"), + repoOrg: .awslabs, + repoType: .awsSdkSwift, + commits: testCommits, + // Parametrize behavior of FeaturesReader with paths used to create JSON test files + featuresReader: FeaturesReader( + requestFilePath: "build-request.json", + mappingFilePath: "feature-service-id.json" + ) + ) + } +}