Skip to content

Commit

Permalink
feat: Service client release notes (#1780)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
sichanyoo and Sichan Yoo authored Oct 4, 2024
1 parent 864b9b6 commit bfb7379
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
50 changes: 50 additions & 0 deletions AWSSDKSwiftCLI/Sources/AWSSDKSwiftCLI/Models/Features.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class PackageManifestBuilderTests: XCTestCase {

let expected = """
<contents of prefix>
// MARK: - Dynamic Content
let clientRuntimeVersion: Version = "1.2.3"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
)
)
}
}

0 comments on commit bfb7379

Please sign in to comment.