Skip to content

Commit

Permalink
Merge pull request #101 from WeTransfer/feature/fix-release-notes
Browse files Browse the repository at this point in the history
Make sure GH release notes are used correctly with previous tag
  • Loading branch information
AvdLee authored Jun 14, 2023
2 parents 9bd34e0 + 8d820fa commit 72add0d
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"repositoryURL": "https://github.com/WeTransfer/octokit.swift",
"state": {
"branch": "main",
"revision": "3d0fea9587af530cb13ef5801a3cb90186fce43e",
"revision": "d3706890a06d2f9afc1de4665191167058438153",
"version": null
}
},
Expand Down
51 changes: 50 additions & 1 deletion Sources/GitBuddyCore/Changelog/ChangelogProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,44 @@ final class ChangelogProducer: URLSessionInjectable {
}

private lazy var octoKit: Octokit = .init()

/// The base branch to compare with. Defaults to master.
let baseBranch: Branch

/// "The tag to use as a base. Defaults to the latest tag.
let since: Since

let from: Date
let to: Date

/// The GIT Project to create a changelog for.
let project: GITProject

init(since: Since = .latestTag, to: Date = Date(), baseBranch: Branch?) throws {
/// If `true`, release notes will be generated by GitHub.
/// Defaults to false, which will lead to a changelog based on PR and issue titles matching the tag changes.
let useGitHubReleaseNotes: Bool

/// The name of the tag to use as base for the changelog comparison.
let tagName: String?

/// Specifies the commitish value that will be the target for the release's tag.
/// Required if the supplied tagName does not reference an existing tag. Ignored if the tagName already exists.
/// While we're talking about creating tags, the changelog producer will only use these values for GitHub release
/// rotes generation.
let targetCommitish: String?

/// The previous tag to compare against. Will only be used for GitHub release notes generation.
let previousTagName: String?

init(
since: Since = .latestTag,
to: Date = Date(),
baseBranch: Branch?,
useGitHubReleaseNotes: Bool = false,
tagName: String? = nil,
targetCommitish: String? = nil,
previousTagName: String? = nil
) throws {
try Octokit.authenticate()

self.to = to
Expand All @@ -57,9 +88,27 @@ final class ChangelogProducer: URLSessionInjectable {
Log.debug("Getting all changes between \(self.from) and \(self.to)")
self.baseBranch = baseBranch ?? "master"
project = GITProject.current()
self.useGitHubReleaseNotes = useGitHubReleaseNotes
self.tagName = tagName
self.targetCommitish = targetCommitish
self.previousTagName = previousTagName
}

@discardableResult public func run(isSectioned: Bool) throws -> Changelog {
if useGitHubReleaseNotes, let tagName, let targetCommitish, let previousTagName {
return try GitHubReleaseNotesGenerator(
octoKit: octoKit,
project: project,
tagName: tagName,
targetCommitish: targetCommitish,
previousTagName: previousTagName
).generate(using: urlSession)
} else {
return try generateChangelogUsingPRsAndIssues(isSectioned: isSectioned)
}
}

private func generateChangelogUsingPRsAndIssues(isSectioned: Bool) throws -> Changelog {
let pullRequestsFetcher = PullRequestFetcher(octoKit: octoKit, baseBranch: baseBranch, project: project)
let pullRequests = try pullRequestsFetcher.fetchAllBetween(from, and: to, using: urlSession)

Expand Down
2 changes: 1 addition & 1 deletion Sources/GitBuddyCore/Commands/ReleaseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ struct ReleaseCommand: ParsableCommand {
@Flag(name: .customLong("sections"), help: "Whether the changelog should be split into sections. Defaults to false.")
private var isSectioned: Bool = false

@Flag(name: .customLong("useGitHubReleaseNotes"), help: "If `true`, release notes will be generated by GitHub. Defaults to false, which will lead to a changelog based on PR and issue titles matching the tag changes.")
@Flag(name: .customLong("use-github-release-notes"), help: "If `true`, release notes will be generated by GitHub. Defaults to false, which will lead to a changelog based on PR and issue titles matching the tag changes.")
private var useGitHubReleaseNotes: Bool = false

@Flag(name: .customLong("json"), help: "Whether the release output should be in JSON, containing more details. Defaults to false.")
Expand Down
45 changes: 45 additions & 0 deletions Sources/GitBuddyCore/GitHub/GitHubReleaseNotesGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// GitHubReleaseNotesGenerator.swift
//
//
// Created by Antoine van der Lee on 13/06/2023.
// Copyright © 2023 WeTransfer. All rights reserved.
//

import Foundation
import OctoKit

struct GitHubReleaseNotesGenerator {
let octoKit: Octokit
let project: GITProject
let tagName: String
let targetCommitish: String
let previousTagName: String

func generate(using session: URLSession = URLSession.shared) throws -> ReleaseNotes {
let group = DispatchGroup()
group.enter()

var result: Result<ReleaseNotes, Swift.Error>!

octoKit.generateReleaseNotes(
session,
owner: project.organisation,
repository: project.repository,
tagName: tagName,
targetCommitish: targetCommitish,
previousTagName: previousTagName) { response in
switch response {
case .success(let releaseNotes):
result = .success(releaseNotes)
case .failure(let error):
result = .failure(OctoKitError(error: error))
}
group.leave()
}

group.wait()

return try result.get()
}
}
8 changes: 8 additions & 0 deletions Sources/GitBuddyCore/Models/Changelog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ protocol Changelog: CustomStringConvertible {
var itemIdentifiers: [PullRequestID: [IssueID]] { get }
}

extension ReleaseNotes: Changelog {
var itemIdentifiers: [PullRequestID : [IssueID]] {
[:]
}

public var description: String { body }
}

/// Represents a changelog with a single section of changelog items.
struct SingleSectionChangelog: Changelog {
let description: String
Expand Down
46 changes: 43 additions & 3 deletions Sources/GitBuddyCore/Release/ReleaseProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,50 @@ final class ReleaseProducer: URLSessionInjectable, ShellInjectable {
}

private lazy var octoKit: Octokit = .init()

/// The path to the Changelog to update it with the latest changes.
let changelogURL: Foundation.URL?

/// Disable commenting on issues and PRs about the new release.
let skipComments: Bool

/// Create the release as a pre-release.
let isPrerelease: Bool

/*
Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA.
Unused if the Git tag already exists.

Default: the repository's default branch (usually main).
*/
let targetCommitish: String?

/*
The name of the tag. If set, `changelogToTag` is required too.

Default: takes the last created tag to publish as a GitHub release.
*/
let tagName: String?

/// The title of the release. Default: uses the tag name.
let releaseTitle: String?

/// The last release tag to use as a base for the changelog creation. Default: previous tag.
let lastReleaseTag: String?

/*
If set, the date of this tag will be used as the limit for the changelog creation.
This variable should be passed when `tagName` is set.

Default: latest tag.
*/
let changelogToTag: String?

/// The base branch to compare with for generating the changelog. Defaults to master.
let baseBranch: String

/// If `true`, release notes will be generated by GitHub.
/// Defaults to false, which will lead to a changelog based on PR and issue titles matching the tag changes.
let useGitHubReleaseNotes: Bool

init(
Expand Down Expand Up @@ -69,17 +104,21 @@ final class ReleaseProducer: URLSessionInjectable, ShellInjectable {

/// We're adding 60 seconds to make sure the tag commit itself is included in the changelog as well.
let adjustedChangelogToDate = changelogToDate.addingTimeInterval(60)
let tagName = try tagName ?? Tag.latest().name

let changelogSinceTag = lastReleaseTag ?? Self.shell.execute(.previousTag)
let changelogProducer = try ChangelogProducer(
since: .tag(tag: changelogSinceTag),
to: adjustedChangelogToDate,
baseBranch: baseBranch
baseBranch: baseBranch,
useGitHubReleaseNotes: useGitHubReleaseNotes,
tagName: tagName,
targetCommitish: targetCommitish,
previousTagName: changelogSinceTag
)
let changelog = try changelogProducer.run(isSectioned: isSectioned)
Log.debug("\(changelog)\n")

let tagName = try tagName ?? Tag.latest().name
try updateChangelogFile(adding: changelog.description, for: tagName)

let repositoryName = Self.shell.execute(.repositoryName)
Expand Down Expand Up @@ -206,7 +245,8 @@ final class ReleaseProducer: URLSessionInjectable, ShellInjectable {
body: body,
prerelease: isPrerelease,
draft: false,
generateReleaseNotes: useGitHubReleaseNotes
/// Since GitHub's API does not support setting `previous_tag_name`, we manually call the API to generate automated GH changelogs.
generateReleaseNotes: false
) { response in
switch response {
case .success(let release):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class ChangelogItemsFactoryTests: XCTestCase {

/// It should return the pull request only if no referencing issues are found.
func testCreatingItems() {
let pullRequest = PullRequestsJSON.data(using: .utf8)!.mapJSON(to: [PullRequest].self).first!
let pullRequest = Data(PullRequestsJSON.utf8).mapJSON(to: [PullRequest].self).first!
let factory = ChangelogItemsFactory(octoKit: octoKit, pullRequests: [pullRequest], project: project)
let items = factory.items(using: urlSession)
XCTAssertEqual(items.count, 1)
Expand All @@ -41,8 +41,8 @@ final class ChangelogItemsFactoryTests: XCTestCase {

/// It should return the referencing issue with the pull request.
func testReferencingIssue() {
let pullRequest = PullRequestsJSON.data(using: .utf8)!.mapJSON(to: [PullRequest].self).last!
let issue = IssueJSON.data(using: .utf8)!.mapJSON(to: Issue.self)
let pullRequest = Data(PullRequestsJSON.utf8).mapJSON(to: [PullRequest].self).last!
let issue = Data(IssueJSON.utf8).mapJSON(to: Issue.self)
let factory = ChangelogItemsFactory(octoKit: octoKit, pullRequests: [pullRequest], project: project)
Mocker.mockForIssueNumber(39)
let items = factory.items(using: urlSession)
Expand Down
4 changes: 2 additions & 2 deletions Tests/GitBuddyTests/Models/ChangelogItemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ final class ChangelogItemTests: XCTestCase {

/// It should show the user if possible.
func testUser() {
let input = PullRequestsJSON.data(using: .utf8)!.mapJSON(to: [PullRequest].self).first!
let input = Data(PullRequestsJSON.utf8).mapJSON(to: [PullRequest].self).first!
input.htmlURL = nil
let item = ChangelogItem(input: input, closedBy: input)
XCTAssertEqual(item.title, "Add charset utf-8 to html head via [@AvdLee](https://github.com/AvdLee)")
}

/// It should fallback to the assignee if the user is nil for Pull Requests.
func testAssigneeFallback() {
let input = PullRequestsJSON.data(using: .utf8)!.mapJSON(to: [PullRequest].self).first!
let input = Data(PullRequestsJSON.utf8).mapJSON(to: [PullRequest].self).first!
input.user = nil
input.htmlURL = nil
let item = ChangelogItem(input: input, closedBy: input)
Expand Down
22 changes: 22 additions & 0 deletions Tests/GitBuddyTests/Release/ReleaseProducerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ final class ReleaseProducerTests: XCTestCase {
wait(for: [mockExpectation], timeout: 0.3)
}

/// It should set the parameters correctly.
func testPostBodyArgumentsGitHubReleaseNotes() throws {
let mockExpectation = expectation(description: "Mocks should be called")
Mocker.mockReleaseNotes()
var mock = Mocker.mockRelease()
mock.onRequest = { _, parameters in
guard let parameters = try? XCTUnwrap(parameters) else { return }
XCTAssertEqual(parameters["prerelease"] as? Bool, false)
XCTAssertEqual(parameters["draft"] as? Bool, false)
XCTAssertEqual(parameters["tag_name"] as? String, "1.0.1")
XCTAssertEqual(parameters["name"] as? String, "1.0.1")
XCTAssertEqual(parameters["body"] as? String, """
##Changes in Release v1.0.0 ... ##Contributors @monalisa
""")
mockExpectation.fulfill()
}
mock.register()

try executeCommand("gitbuddy release -s --use-github-release-notes -t develop")
wait(for: [mockExpectation], timeout: 0.3)
}

/// It should update the changelog file if the argument is set.
func testChangelogUpdating() throws {
let existingChangelog = """
Expand Down
24 changes: 18 additions & 6 deletions Tests/GitBuddyTests/TestHelpers/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ extension Mocker {
URLQueryItem(name: "state", value: "closed")
]

let pullRequestJSONData = PullRequestsJSON.data(using: .utf8)!
let pullRequestJSONData = Data(PullRequestsJSON.utf8)
let mock = Mock(url: urlComponents.url!, dataType: .json, statusCode: 200, data: [.get: pullRequestJSONData])
mock.register()
}
Expand All @@ -101,19 +101,19 @@ extension Mocker {
URLQueryItem(name: "state", value: "closed")
]

let data = IssuesJSON.data(using: .utf8)!
let data = Data(IssuesJSON.utf8)
let mock = Mock(url: urlComponents.url!, dataType: .json, statusCode: 200, data: [.get: data])
mock.register()
}

static func mockForIssueNumber(_ issueNumber: Int) {
let urlComponents = URLComponents(string: "https://api.github.com/repos/WeTransfer/Diagnostics/issues/\(issueNumber)")!
let issueJSONData = IssueJSON.data(using: .utf8)!
let issueJSONData = Data(IssueJSON.utf8)
Mock(url: urlComponents.url!, dataType: .json, statusCode: 200, data: [.get: issueJSONData]).register()
}

@discardableResult static func mockRelease() -> Mock {
let releaseJSONData = ReleaseJSON.data(using: .utf8)!
let releaseJSONData = Data(ReleaseJSON.utf8)
let mock = Mock(
url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/releases")!,
dataType: .json,
Expand All @@ -124,8 +124,20 @@ extension Mocker {
return mock
}

@discardableResult static func mockReleaseNotes() -> Mock {
let releaseNotesJSONData = Data(ReleaseNotesJSON.utf8)
let mock = Mock(
url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/releases/generate-notes")!,
dataType: .json,
statusCode: 200,
data: [.post: releaseNotesJSONData]
)
mock.register()
return mock
}

@discardableResult static func mockListReleases() -> Mock {
let releaseJSONData = ListReleasesJSON.data(using: .utf8)!
let releaseJSONData = Data(ListReleasesJSON.utf8)
let mock = Mock(
url: URL(string: "https://api.github.com/repos/WeTransfer/Diagnostics/releases?per_page=100")!,
dataType: .json,
Expand Down Expand Up @@ -160,7 +172,7 @@ extension Mocker {

static func mockForCommentingOn(issueNumber: Int) -> Mock {
let urlComponents = URLComponents(string: "https://api.github.com/repos/WeTransfer/Diagnostics/issues/\(issueNumber)/comments")!
let commentJSONData = CommentJSON.data(using: .utf8)!
let commentJSONData = Data(CommentJSON.utf8)
return Mock(url: urlComponents.url!, dataType: .json, statusCode: 201, data: [.post: commentJSONData])
}
}
Expand Down
18 changes: 18 additions & 0 deletions Tests/GitBuddyTests/TestHelpers/ReleaseNotesJSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// ReleaseNotesJSON.swift
//
//
// Created by Antoine van der Lee on 13/06/2023.
// Copyright © 2023 WeTransfer. All rights reserved.
//

import Foundation

// swiftlint:disable line_length
let ReleaseNotesJSON = """
{
"name": "Release v1.0.0 is now available!",
"body": "##Changes in Release v1.0.0 ... ##Contributors @monalisa"
}
"""

0 comments on commit 72add0d

Please sign in to comment.