From dc80f195ccc03c88139e996668a1fd877cf6e260 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 27 Sep 2024 15:42:07 -0700 Subject: [PATCH 01/14] Add features struct and feature struct for grabbing info we need from build request and feature to service name mapping JSON files. --- .../AWSSDKSwiftCLI/Models/Features.swift | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift new file mode 100644 index 00000000000..4dc9f1aedca --- /dev/null +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift @@ -0,0 +1,72 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import AWSCLIUtils + +struct Features: Decodable { + let features: [Feature] + + static func fromFile(_ filePath: String) throws -> Self { + var fileContents: Data + do { + fileContents = try FileManager.default.loadContents(atPath: filePath) + } catch { + throw Error("Failed to load build-request.json.") + } + do { + return try JSONDecoder().decode(Self.self, from: fileContents) + } catch { + throw Error("Failed to decode build-request.json.") + } + } + + static func mappingFromFile(_ filePath: String) throws -> [String: String] { + var fileContents: Data + do { + fileContents = try FileManager.default.loadContents(atPath: filePath) + } catch { + throw Error("Failed to load feature-service-id.json.") + } + do { + return try JSONDecoder().decode([String: String].self, from: fileContents) + } catch { + throw Error("Failed to decode feature-service-id.json.") + } + } +} + +struct Feature: Decodable { + let featureId: String + let featureType: String + let releaseNotes: String + + // Custom coding keys for decoding nested values into top level fields in Feature struct + private enum CodingKeys: String, CodingKey { + case releaseNotes + case featureMetadata + + enum FeatureMetadataKeys: String, CodingKey { + case trebuchet + + enum TrebuchetKeys: String, CodingKey { + case featureId, featureType + } + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + releaseNotes = try container.decode(String.self, forKey: .releaseNotes) + + let featureMetadata = try container.nestedContainer(keyedBy: CodingKeys.FeatureMetadataKeys.self, forKey: .featureMetadata) + let trebuchet = try featureMetadata.nestedContainer(keyedBy: CodingKeys.FeatureMetadataKeys.TrebuchetKeys.self, forKey: .trebuchet) + + featureId = try trebuchet.decode(String.self, forKey: .featureId) + featureType = try trebuchet.decode(String.self, forKey: .featureType) + } +} From e117102522aa9cf6cdd0e8b2aecd14fb6b12f6f1 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 27 Sep 2024 15:43:43 -0700 Subject: [PATCH 02/14] Add service feature & service documentation sections to release notes generation. Also, refactor it for consistent spacing between the lines even when sections are omitted. --- .../Models/ReleaseNotesBuilder.swift | 65 +++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift index da4451fe660..823e62b5bbf 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift @@ -18,21 +18,60 @@ struct ReleaseNotesBuilder { // 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 contents: [String] = ( + ["## What's Changed"] + + buildSDKChangeSection() + + (try buildServiceChangeSection()) + + ["\n**Full Changelog**: https://github.com/\(repoOrg.rawValue)/\(repoType.rawValue)/compare/\(previousVersion)...\(newVersion)"] + ) 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("chore") && !$0.hasPrefix("Update") } + .map { "* \($0)" } + .joined(separator: .newline) + if (!formattedCommits.isEmpty) { + return ["### AWS SDK for Swift", formattedCommits] + } + return [] + } + + func buildServiceChangeSection() throws -> [String] { + var features: Features + var mapping: [String: String] + features = try Features.fromFile("build-request.json") + mapping = try Features.mappingFromFile("feature-service-id.json") + return buildServiceFeatureSection(features, mapping) + buildServiceDocSection(features, mapping) + } + + private func buildServiceFeatureSection( + _ features: Features, + _ mapping: [String: String] + ) -> [String] { + let formattedFeatures = features.features + .filter { $0.featureType == "NEW_FEATURE" } + .map { "* **AWS \(mapping[$0.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.featureType == "DOC_UPDATE" } + .map { "* **AWS \(mapping[$0.featureId]!)**: \($0.releaseNotes)" } .joined(separator: .newline) + if (!formattedDocUpdates.isEmpty) { + return ["### Service Documentation", formattedDocUpdates] + } + return [] } } From eb4fed984784a2ac2db193e5fe8aea79bcd66d6a Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 27 Sep 2024 15:45:05 -0700 Subject: [PATCH 03/14] Update existing code that uses ReleaseNotesBuilder to reflect new changes. --- .../AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift | 2 +- .../Commands/PrepareReleaseTests.swift | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) 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/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() From 51d58ec2087024179991bf1aabb6f815b0dae75b Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 27 Sep 2024 15:46:36 -0700 Subject: [PATCH 04/14] Update test that fails; possibly from previous package manifest change. --- .../AWSSDKSwiftCLITests/Models/PackageManifestBuilderTests.swift | 1 - 1 file changed, 1 deletion(-) 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" From c8891eee876b93dd236623d4bcb6f9d4b7a9d8c2 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 27 Sep 2024 15:47:04 -0700 Subject: [PATCH 05/14] Add regression tests for release notes builder. --- .../Models/ReleaseNotesBuilderTests.swift | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift diff --git a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift new file mode 100644 index 00000000000..cf783e7a4ba --- /dev/null +++ b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift @@ -0,0 +1,179 @@ +// +// 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 + ### AWS SDK for Swift + * fix: Fix X + * feat: Feat Y + ### 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 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 + ### AWS SDK for Swift + * fix: Fix X + * feat: Feat Y + ### 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 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 + ### AWS SDK for Swift + * fix: Fix X + * feat: Feat Y + ### Service Features + * **AWS Service 1**: New feature description A. + * **AWS Service 2**: New feature description B. + + **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) { + 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 + ) + } +} From 55146a62fcfc14b40190c0ea9db4bf80c52d1f5e Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 27 Sep 2024 16:19:58 -0700 Subject: [PATCH 06/14] Only build service client sections in release notes if the repo is aws-sdk-swift. --- .../AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift index 823e62b5bbf..2cec02cd058 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift @@ -19,12 +19,15 @@ struct ReleaseNotesBuilder { // MARK: - Build func build() throws -> String { - let contents: [String] = ( + let baseContent: [String] = ( ["## What's Changed"] + - buildSDKChangeSection() + - (try buildServiceChangeSection()) + - ["\n**Full Changelog**: https://github.com/\(repoOrg.rawValue)/\(repoType.rawValue)/compare/\(previousVersion)...\(newVersion)"] + buildSDKChangeSection() ) + let serviceClientChanges = repoType == .awsSdkSwift ? (try buildServiceChangeSection()) : [] + let fullCommitLogLink = [ + "\n**Full Changelog**: https://github.com/\(repoOrg.rawValue)/\(repoType.rawValue)/compare/\(previousVersion)...\(newVersion)" + ] + let contents = baseContent + serviceClientChanges + fullCommitLogLink return contents.joined(separator: .newline) } From 956e8ff28e59de6adf805332ff95c3ab81e0431b Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 27 Sep 2024 17:02:18 -0700 Subject: [PATCH 07/14] 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. --- .../AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift | 11 +++++++++-- .../Models/ReleaseNotesBuilderTests.swift | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift index 2cec02cd058..2aacae89d9b 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift @@ -15,6 +15,7 @@ struct ReleaseNotesBuilder { let repoOrg: PrepareRelease.Org let repoType: PrepareRelease.Repo let commits: [String] + var isTest: Bool = false // MARK: - Build @@ -45,8 +46,14 @@ struct ReleaseNotesBuilder { func buildServiceChangeSection() throws -> [String] { var features: Features var mapping: [String: String] - features = try Features.fromFile("build-request.json") - mapping = try Features.mappingFromFile("feature-service-id.json") + // At this point, FileManager.default's current working directory is aws-sdk-swift/. + // The JSON files we need are located one level above it, in the workspace directory. + // For tests, due to sandboxing, the dummy files are created in current directory instead of + // in parent directory. Therefore, fetch them from current directory for tests. + let buildRequestLocation = isTest ? "build-request.json" : "../build-request.json" + let mappingLocation = isTest ? "feature-service-id.json" : "../feature-service-id.json" + features = try Features.fromFile(buildRequestLocation) + mapping = try Features.mappingFromFile(mappingLocation) return buildServiceFeatureSection(features, mapping) + buildServiceDocSection(features, mapping) } diff --git a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift index cf783e7a4ba..bae246d100c 100644 --- a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift +++ b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift @@ -173,7 +173,8 @@ class ReleaseNotesBuilderTests: XCTestCase { newVersion: Version("1.0.1"), repoOrg: .awslabs, repoType: .awsSdkSwift, - commits: testCommits + commits: testCommits, + isTest: true ) } } From 21177effc677d2f0d613551d74f3b28502f2b9a7 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Thu, 3 Oct 2024 15:47:54 -0700 Subject: [PATCH 08/14] Resolve Josh's PR comments --- .../AWSSDKSwiftCLI/Models/Features.swift | 76 +++++++------------ .../Models/ReleaseNotesBuilder.swift | 33 +++----- .../Models/ReleaseNotesBuilderTests.swift | 25 +++--- 3 files changed, 54 insertions(+), 80 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift index 4dc9f1aedca..526928fa588 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift @@ -8,65 +8,43 @@ import Foundation import AWSCLIUtils -struct Features: Decodable { - let features: [Feature] +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 + } - static func fromFile(_ filePath: String) throws -> Self { - var fileContents: Data - do { - fileContents = try FileManager.default.loadContents(atPath: filePath) - } catch { - throw Error("Failed to load build-request.json.") - } - do { - return try JSONDecoder().decode(Self.self, from: fileContents) - } catch { - throw Error("Failed to decode build-request.json.") - } + public func getFeaturesFromFile() throws -> Features { + let fileContents = try FileManager.default.loadContents(atPath: requestFilePath) + return try JSONDecoder().decode(Features.self, from: fileContents) } - static func mappingFromFile(_ filePath: String) throws -> [String: String] { - var fileContents: Data - do { - fileContents = try FileManager.default.loadContents(atPath: filePath) - } catch { - throw Error("Failed to load feature-service-id.json.") - } - do { - return try JSONDecoder().decode([String: String].self, from: fileContents) - } catch { - throw Error("Failed to decode feature-service-id.json.") - } + 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 featureId: String - let featureType: String let releaseNotes: String + let featureMetadata: FeatureMetadata - // Custom coding keys for decoding nested values into top level fields in Feature struct - private enum CodingKeys: String, CodingKey { - case releaseNotes - case featureMetadata - - enum FeatureMetadataKeys: String, CodingKey { - case trebuchet + struct FeatureMetadata: Decodable { + let trebuchet: Trebuchet - enum TrebuchetKeys: String, CodingKey { - case featureId, featureType - } + struct Trebuchet: Decodable { + let featureId: String + let featureType: String } } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - releaseNotes = try container.decode(String.self, forKey: .releaseNotes) - - let featureMetadata = try container.nestedContainer(keyedBy: CodingKeys.FeatureMetadataKeys.self, forKey: .featureMetadata) - let trebuchet = try featureMetadata.nestedContainer(keyedBy: CodingKeys.FeatureMetadataKeys.TrebuchetKeys.self, forKey: .trebuchet) - - featureId = try trebuchet.decode(String.self, forKey: .featureId) - featureType = try trebuchet.decode(String.self, forKey: .featureType) - } } diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift index 2aacae89d9b..da91be411f0 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift @@ -15,20 +15,17 @@ struct ReleaseNotesBuilder { let repoOrg: PrepareRelease.Org let repoType: PrepareRelease.Repo let commits: [String] - var isTest: Bool = false - + var featuresReader: FeaturesReader = FeaturesReader() + // MARK: - Build - + func build() throws -> String { - let baseContent: [String] = ( - ["## What's Changed"] + - buildSDKChangeSection() - ) + 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 = baseContent + serviceClientChanges + fullCommitLogLink + let contents = ["## What's Changed"] + serviceClientChanges + sdkChanges + fullCommitLogLink return contents.joined(separator: .newline) } @@ -44,16 +41,8 @@ struct ReleaseNotesBuilder { } func buildServiceChangeSection() throws -> [String] { - var features: Features - var mapping: [String: String] - // At this point, FileManager.default's current working directory is aws-sdk-swift/. - // The JSON files we need are located one level above it, in the workspace directory. - // For tests, due to sandboxing, the dummy files are created in current directory instead of - // in parent directory. Therefore, fetch them from current directory for tests. - let buildRequestLocation = isTest ? "build-request.json" : "../build-request.json" - let mappingLocation = isTest ? "feature-service-id.json" : "../feature-service-id.json" - features = try Features.fromFile(buildRequestLocation) - mapping = try Features.mappingFromFile(mappingLocation) + let features = try featuresReader.getFeaturesFromFile() + let mapping = try featuresReader.getFeaturesIDToServiceNameDictFromFile() return buildServiceFeatureSection(features, mapping) + buildServiceDocSection(features, mapping) } @@ -62,8 +51,8 @@ struct ReleaseNotesBuilder { _ mapping: [String: String] ) -> [String] { let formattedFeatures = features.features - .filter { $0.featureType == "NEW_FEATURE" } - .map { "* **AWS \(mapping[$0.featureId]!)**: \($0.releaseNotes)" } + .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] @@ -76,8 +65,8 @@ struct ReleaseNotesBuilder { _ mapping: [String: String] ) -> [String] { let formattedDocUpdates = features.features - .filter { $0.featureType == "DOC_UPDATE" } - .map { "* **AWS \(mapping[$0.featureId]!)**: \($0.releaseNotes)" } + .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] diff --git a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift index bae246d100c..00cb2f56a47 100644 --- a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift +++ b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift @@ -72,14 +72,14 @@ class ReleaseNotesBuilderTests: XCTestCase { let releaseNotes = try builder.build() let expected = """ ## What's Changed - ### AWS SDK for Swift - * fix: Fix X - * feat: Feat Y ### 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. + ### AWS SDK for Swift + * fix: Fix X + * feat: Feat Y **Full Changelog**: https://github.com/awslabs/aws-sdk-swift/compare/1.0.0...1.0.1 """ @@ -95,11 +95,11 @@ class ReleaseNotesBuilderTests: XCTestCase { let releaseNotes = try builder.build() let expected = """ ## What's Changed + ### Service Documentation + * **AWS Service 3**: Doc update description C. ### AWS SDK for Swift * fix: Fix X * feat: Feat Y - ### 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 """ @@ -115,12 +115,12 @@ class ReleaseNotesBuilderTests: XCTestCase { let releaseNotes = try builder.build() let expected = """ ## What's Changed - ### AWS SDK for Swift - * fix: Fix X - * feat: Feat Y ### Service Features * **AWS Service 1**: New feature description A. * **AWS Service 2**: New feature description B. + ### AWS SDK for Swift + * fix: Fix X + * feat: Feat Y **Full Changelog**: https://github.com/awslabs/aws-sdk-swift/compare/1.0.0...1.0.1 """ @@ -163,6 +163,9 @@ class ReleaseNotesBuilderTests: XCTestCase { } 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)) } @@ -174,7 +177,11 @@ class ReleaseNotesBuilderTests: XCTestCase { repoOrg: .awslabs, repoType: .awsSdkSwift, commits: testCommits, - isTest: true + // Parametrize behavior of FeaturesReader with paths used to create JSON test files + featuresReader: FeaturesReader( + requestFilePath: "build-request.json", + mappingFilePath: "feature-service-id.json" + ) ) } } From bda2b0e03fefe9a8720deafdc80a9266c0794766 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 4 Oct 2024 10:20:18 -0700 Subject: [PATCH 09/14] Temporarily comment out repo has change check for running E2E test. --- .../Subcommands/PrepareRelease.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift index 1d21c45ea1c..b1f58b72234 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift @@ -81,16 +81,16 @@ struct PrepareRelease { try FileManager.default.changeWorkingDirectory(repoPath) let previousVersion = try getPreviousVersion() - guard try repoHasChanges(previousVersion) else { - /// If repo has no changes, create an empty release-manifest.json file. - /// Empty manifest file makes GitHubReleasePublisher be no-op. - /// The manifest file is required regardless of whether there should - /// be a release or not. - try createEmptyReleaseManifest() - /// Return without creating new commit or tag in local repos. - /// This makes GitPublisher be no-op. - return - } +// guard try repoHasChanges(previousVersion) else { +// /// If repo has no changes, create an empty release-manifest.json file. +// /// Empty manifest file makes GitHubReleasePublisher be no-op. +// /// The manifest file is required regardless of whether there should +// /// be a release or not. +// try createEmptyReleaseManifest() +// /// Return without creating new commit or tag in local repos. +// /// This makes GitPublisher be no-op. +// return +// } let newVersion = try createNewVersion(previousVersion) try stageFiles() From 4dc331d4c00513bcb880cbe8dcd9387286b9945e Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 4 Oct 2024 11:51:22 -0700 Subject: [PATCH 10/14] Uncomment temporarily commented logic for generating empty manfiest if repo has no changes. --- .../Subcommands/PrepareRelease.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift index b1f58b72234..1d21c45ea1c 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift @@ -81,16 +81,16 @@ struct PrepareRelease { try FileManager.default.changeWorkingDirectory(repoPath) let previousVersion = try getPreviousVersion() -// guard try repoHasChanges(previousVersion) else { -// /// If repo has no changes, create an empty release-manifest.json file. -// /// Empty manifest file makes GitHubReleasePublisher be no-op. -// /// The manifest file is required regardless of whether there should -// /// be a release or not. -// try createEmptyReleaseManifest() -// /// Return without creating new commit or tag in local repos. -// /// This makes GitPublisher be no-op. -// return -// } + guard try repoHasChanges(previousVersion) else { + /// If repo has no changes, create an empty release-manifest.json file. + /// Empty manifest file makes GitHubReleasePublisher be no-op. + /// The manifest file is required regardless of whether there should + /// be a release or not. + try createEmptyReleaseManifest() + /// Return without creating new commit or tag in local repos. + /// This makes GitPublisher be no-op. + return + } let newVersion = try createNewVersion(previousVersion) try stageFiles() From f2c6d4f27385d45952d9f5cc1ed5e0cdf666f2c0 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 4 Oct 2024 12:58:14 -0700 Subject: [PATCH 11/14] 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. --- .../Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift | 4 ++-- .../Models/ReleaseNotesBuilderTests.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift index da91be411f0..5a9a4eb5828 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift @@ -31,11 +31,11 @@ struct ReleaseNotesBuilder { func buildSDKChangeSection() -> [String] { let formattedCommits = commits - .filter { !$0.hasPrefix("chore") && !$0.hasPrefix("Update") } + .filter { $0.hasPrefix("feat") || $0.hasPrefix("fix") } .map { "* \($0)" } .joined(separator: .newline) if (!formattedCommits.isEmpty) { - return ["### AWS SDK for Swift", formattedCommits] + return ["### Miscellaneous", formattedCommits] } return [] } diff --git a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift index 00cb2f56a47..c4c96ccbb3c 100644 --- a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift +++ b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift @@ -77,7 +77,7 @@ class ReleaseNotesBuilderTests: XCTestCase { * **AWS Service 2**: New feature description B. ### Service Documentation * **AWS Service 3**: Doc update description C. - ### AWS SDK for Swift + ### Miscellaneous * fix: Fix X * feat: Feat Y @@ -97,7 +97,7 @@ class ReleaseNotesBuilderTests: XCTestCase { ## What's Changed ### Service Documentation * **AWS Service 3**: Doc update description C. - ### AWS SDK for Swift + ### Miscellaneous * fix: Fix X * feat: Feat Y @@ -118,7 +118,7 @@ class ReleaseNotesBuilderTests: XCTestCase { ### Service Features * **AWS Service 1**: New feature description A. * **AWS Service 2**: New feature description B. - ### AWS SDK for Swift + ### Miscellaneous * fix: Fix X * feat: Feat Y From 0909578376de3c234af491600f27373cd6942d6d Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Fri, 4 Oct 2024 16:46:25 -0700 Subject: [PATCH 12/14] Handle null value for releaseNotes and add test for it. --- .../AWSSDKSwiftCLI/Models/Features.swift | 2 +- .../Models/ReleaseNotesBuilder.swift | 4 +-- .../Models/ReleaseNotesBuilderTests.swift | 33 ++++++++++++++++++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift index 526928fa588..93e749c74ed 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift @@ -36,7 +36,7 @@ struct Features: Decodable { } struct Feature: Decodable { - let releaseNotes: String + let releaseNotes: String? let featureMetadata: FeatureMetadata struct FeatureMetadata: Decodable { diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift index 5a9a4eb5828..43ce814578b 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/ReleaseNotesBuilder.swift @@ -52,7 +52,7 @@ struct ReleaseNotesBuilder { ) -> [String] { let formattedFeatures = features.features .filter { $0.featureMetadata.trebuchet.featureType == "NEW_FEATURE" } - .map { "* **AWS \(mapping[$0.featureMetadata.trebuchet.featureId]!)**: \($0.releaseNotes)" } + .map { "* **AWS \(mapping[$0.featureMetadata.trebuchet.featureId]!)**: \($0.releaseNotes ?? "No description provided.")" } .joined(separator: .newline) if (!formattedFeatures.isEmpty) { return ["### Service Features", formattedFeatures] @@ -66,7 +66,7 @@ struct ReleaseNotesBuilder { ) -> [String] { let formattedDocUpdates = features.features .filter { $0.featureMetadata.trebuchet.featureType == "DOC_UPDATE" } - .map { "* **AWS \(mapping[$0.featureMetadata.trebuchet.featureId]!)**: \($0.releaseNotes)" } + .map { "* **AWS \(mapping[$0.featureMetadata.trebuchet.featureId]!)**: \($0.releaseNotes ?? "No description provided.")" } .joined(separator: .newline) if (!formattedDocUpdates.isEmpty) { return ["### Service Documentation", formattedDocUpdates] diff --git a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift index c4c96ccbb3c..ee1ee2f8958 100644 --- a/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift +++ b/AWSSDKSwiftCLI/Tests/AWSSDKSwiftCLITests/Models/ReleaseNotesBuilderTests.swift @@ -54,12 +54,26 @@ class ReleaseNotesBuilderTests: XCTestCase { } """ + // Feature with null releaseNotes field + private let feature4 = """ + { + "releaseNotes": null, + "featureMetadata": { + "trebuchet": { + "featureId": "feature-id-d", + "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" + "feature-id-c": "Service 3", + "feature-id-d": "Service 4" } """ @@ -162,6 +176,23 @@ class ReleaseNotesBuilderTests: XCTestCase { XCTAssertEqual(releaseNotes, expected) } + func testNullReleaseNotesFieldGetsHandledWithoutError() throws { + let buildRequest = """ + { "features": [\(feature4)] } + """ + setUpBuildRequestAndMappingJSONs(buildRequest, mapping) + let builder = try setUpBuilder() + let releaseNotes = try builder.build() + let expected = """ + ## What's Changed + ### Service Documentation + * **AWS Service 4**: No description provided. + + **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 From be86e44d2fe5050227af1dc7cba38a6facf92e60 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Mon, 7 Oct 2024 10:22:05 -0700 Subject: [PATCH 13/14] Temporarily disable diff checker for running E2E tests --- .../Subcommands/PrepareRelease.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift index 1d21c45ea1c..b1f58b72234 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift @@ -81,16 +81,16 @@ struct PrepareRelease { try FileManager.default.changeWorkingDirectory(repoPath) let previousVersion = try getPreviousVersion() - guard try repoHasChanges(previousVersion) else { - /// If repo has no changes, create an empty release-manifest.json file. - /// Empty manifest file makes GitHubReleasePublisher be no-op. - /// The manifest file is required regardless of whether there should - /// be a release or not. - try createEmptyReleaseManifest() - /// Return without creating new commit or tag in local repos. - /// This makes GitPublisher be no-op. - return - } +// guard try repoHasChanges(previousVersion) else { +// /// If repo has no changes, create an empty release-manifest.json file. +// /// Empty manifest file makes GitHubReleasePublisher be no-op. +// /// The manifest file is required regardless of whether there should +// /// be a release or not. +// try createEmptyReleaseManifest() +// /// Return without creating new commit or tag in local repos. +// /// This makes GitPublisher be no-op. +// return +// } let newVersion = try createNewVersion(previousVersion) try stageFiles() From 6898fe32fa5f21c5ff2193c256d070127d27a138 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Mon, 7 Oct 2024 12:35:00 -0700 Subject: [PATCH 14/14] Uncomment temporarily commented out diff logic. --- .../Subcommands/PrepareRelease.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift index b1f58b72234..1d21c45ea1c 100644 --- a/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift +++ b/AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Commands/AWSSDKSwiftCLI/Subcommands/PrepareRelease.swift @@ -81,16 +81,16 @@ struct PrepareRelease { try FileManager.default.changeWorkingDirectory(repoPath) let previousVersion = try getPreviousVersion() -// guard try repoHasChanges(previousVersion) else { -// /// If repo has no changes, create an empty release-manifest.json file. -// /// Empty manifest file makes GitHubReleasePublisher be no-op. -// /// The manifest file is required regardless of whether there should -// /// be a release or not. -// try createEmptyReleaseManifest() -// /// Return without creating new commit or tag in local repos. -// /// This makes GitPublisher be no-op. -// return -// } + guard try repoHasChanges(previousVersion) else { + /// If repo has no changes, create an empty release-manifest.json file. + /// Empty manifest file makes GitHubReleasePublisher be no-op. + /// The manifest file is required regardless of whether there should + /// be a release or not. + try createEmptyReleaseManifest() + /// Return without creating new commit or tag in local repos. + /// This makes GitPublisher be no-op. + return + } let newVersion = try createNewVersion(previousVersion) try stageFiles()