From bfb7379a6554d7e2aeb3b76bb20713a3ae02b9bf Mon Sep 17 00:00:00 2001 From: Chan <55515281+sichanyoo@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:45:07 -0700 Subject: [PATCH] feat: Service client release notes (#1780) * Add features struct and feature struct for grabbing info we need from build request and feature to service name mapping JSON files. * Add service feature & service documentation sections to release notes generation. Also, refactor it for consistent spacing between the lines even when sections are omitted. * Update existing code that uses ReleaseNotesBuilder to reflect new changes. * Update test that fails; possibly from previous package manifest change. * Add regression tests for release notes builder. * Only build service client sections in release notes if the repo is aws-sdk-swift. * Add test flag (grr) to ReleaseNotesBuilder to allow tests. In real scenarios the JSON files we need are located in parent directory of aws-sdk-swift, but due to sandboxing, we can't save the dummy files to parent directory and instead can only save it in current directory. Use the flag to differentiate the path to retrieve JSON files from. * Resolve Josh's PR comments * Temporarily comment out repo has change check for running E2E test. * Uncomment temporarily commented logic for generating empty manfiest if repo has no changes. * Change SDK changes section name from AWS SDK for Swift to Miscellaneous & change commit filter criteria from discaring chore and Update, to including feat and fix. --------- Co-authored-by: Sichan Yoo --- .../Subcommands/PrepareRelease.swift | 2 +- .../AWSSDKSwiftCLI/Models/Features.swift | 50 +++++ .../Models/ReleaseNotesBuilder.swift | 66 +++++-- .../Commands/PrepareReleaseTests.swift | 10 + .../Models/PackageManifestBuilderTests.swift | 1 - .../Models/ReleaseNotesBuilderTests.swift | 187 ++++++++++++++++++ 6 files changed, 300 insertions(+), 16 deletions(-) create mode 100644 AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift create mode 100644 AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift 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" + ) + ) + } +}