Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Golang implementation of release note generating tool #434

Merged
merged 2 commits into from
Nov 21, 2017

Conversation

roycaihw
Copy link
Member

@roycaihw roycaihw commented Oct 8, 2017

To better produce human-readable release note for Kubernetes. We proposed a hierarchical release note (designe doc shared with kubernetes-dev@ mailing list) design.

The implementation is divided into two PRs for easier review & test. This PR contains a Golang implementation of existing release note generating tool relnotes. The second PR focus on the new hierarchical feature and can be reviewed after this one is merged.

Notes for reviewer:

  1. This PR contains three commits. The first two commits add vendor dependency and bazel build rule changes for Golang. The third commit focus on the implementation.

  2. Unit tests are implemented for util functions and main function.

  3. To build, run:
    dep ensure
    bazel run //:gazelle
    bazel build toolbox/relnotes:relnotes

  4. Some e2e tests against the existing shell script relnotes (assume currently in a kubernetes repo):
    i)
    On branch release-1.7 (run make quick-release to generate the release tarballs):

    Golang program (the program takes about 15 minutes to run):
    ../release/bazel-bin/toolbox/relnotes/relnotes --preview --htmlize-md --html-file /tmp/release-note-html-testfile --release-tars=_output/release-tars v1.7.0..v1.7.2
    Script program:
    ../release/relnotes v1.7.0..v1.7.2 --preview --htmlize-md --html-file=/tmp/relnotes-testfile.html --release-tars=_output/release-tars
    ii)
    On branch release-1.7:

    Golang program:
    ../release/bazel-bin/toolbox/relnotes/relnotes --preview --html-file /tmp/release-note-html-testfile --release-tars=_output/release-tars v1.7.0..v1.7.0
    Script program:
    ../release/relnotes v1.7.0..v1.7.0 --preview --html-file=/tmp/relnotes-testfile.html --release-tars=_output/release-tars
    iii)
    On branch release-1.6.3:

    Golang program:
    ../release/bazel-bin/toolbox/relnotes/relnotes --html-file /tmp/release-note-html-testfile --full
    Script program:
    ../release/relnotes --html-file=/tmp/relnotes-testfile.html --full

  5. To compare the output:
    vimdiff /tmp/relnotes-release-1.7.md /tmp/release-notes-release-1.7.md
    vimdiff /tmp/relnotes-release-1.6.3.md /tmp/release-notes-release-1.6.3.md
    vimdiff /tmp/release-note-html-testfile /tmp/relnotes-testfile.html

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Oct 8, 2017
@roycaihw
Copy link
Member Author

roycaihw commented Oct 8, 2017

cc @david-mcmahon @yutongz

@david-mcmahon
Copy link
Contributor

Excellent! Excited to see this so soon. A few quick comments on usage so far:

  1. More user-helpful output
  • It looks like there's a long-run operation between processing the inputs and generating release notes - can this be mentioned somehow, such as "Gathering notes from github..." or the like?
  • For particularly long-run operations, a status meter showing % or some useful metric?
  1. Output
  • I ran it with only --branch and a range and it finished without anything to stdout and no indication if there was any file created. stdout would be nice (with a --quiet to suppress) and if a file is required, it could indicate that upfront before beginning the full operation and also show that file at the end.
  1. Other projects/repos?
  • I didn't check yet, but is the current version of this PR ready to work with any project/repo?

@roycaihw
Copy link
Member Author

roycaihw commented Oct 10, 2017

@david-mcmahon

Thanks for the comments! I've pushed an update:

  1. Now the program has more user-helpful output, including a status meter for the time-consuming gathering-issues operation.

  2. Now it indicates the files created at beginning, shows the markdown file at the end, and support --quiet option.

  3. Mostly it is, but the current version has some kubernetes-specified (hard-)coding (can be generalized): [DONE]

  • In --full mode or in a minor release, the program looks for release draft at [project (here kubernetes)]/feature. The program will generate a draft template if it cannot find the release draft (so it's fine).
  • Also in --full mode or in a minor release, the program looks for [project]/[repo]/master/CHANGELOG.md to get previous releases' anchor. It's fine if it cannot find the file.
  • In generating the download table for release tars, the filename patterns for matching the tarballs are kubernetes-specified. These functions won't get executed if user doesn't specify --release-tars flag (so it's fine)
  • Some hard-coded URLs for htmlize-ing markdown file. I'll push another commit to address this one, but it shouldn't affect working with other project/repo. [DONE]

Copy link

@yutongz yutongz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's awesome, good job!

}

// If githubToken isn't specified in flag, use the GITHUB_TOKEN environment variable
if *githubToken == "" {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually we take the path of token file as input and read token from that file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yutongz This is following the convention from existing relnotes script @david-mcmahon

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flexibility here is good if you'd like to add it. In fact you could just switch the flag to use a file. I don't think anyone would notice or care. $GITHUB_TOKEN is used by the release workflow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// otherwise.
func IsVer(version string, t string) bool {
m := make(map[string]string)
m["release"] = "v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[a-zA-Z0-9]+)*\\.*(0|[1-9][0-9]*)?"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to add some comments with example string you want to match for each regexp.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

for _, table := range tables {
result := IsVer(table.v, table.t)
if result != table.isVer {
t.Errorf("%v %v: IsVer check failed, want: %v, got: %v", table.v, table.t, table.isVer, result)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may also want to add cases which are expected to fail the matches.

Copy link
Member Author

@roycaihw roycaihw Oct 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yutongz Yes the test has cases that fail the matches.


// Bootstrap notes for minor (new branch) releases
if *full || u.IsVer(releaseTag, "dotzero") {
draftURL := u.GithubRawURL + *owner + "/features/master/" + *branch + "/release-notes-draft.md"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use fmt.Sprintf()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

patchRelease(prFile, startTag, releasePRs, issueMap)
}

prFile.Close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to use defer functions to handle something like close files, it's a golang trick. Take a look at a example

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yutongz The problem is that I need to close those files early, for

  1. opening prFile again and copy the content into mdFile
  2. running sed on mdFile to htmlize it
  3. running pandoc on mdFile

I will rearrange main() to be able to call defer functions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

log.Printf("failed to generate HTML release note: %s", err)
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the end, you may want to print something like "Successfully generated release-note"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


// getCIJobStatus runs the script find_green_build and append CI job status to outputFile.
func getCIJobStatus(outputFile, branch string, htmlize bool) error {
log.Printf("Adding CI job status (this may take a while)...")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it's "getting" instead of "adding"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


for _, file := range files {
fn := extractFileName(file)
sha, _ := u.GetSha256(file)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should check the error code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

// extractFileName takes a string and returns the file name after last '/'.
func extractFileName(filePath string) string {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use filepath.Base() https://golang.org/pkg/path/filepath/#Base

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

// TODO: find a better way to tell failed response
if err == nil && (resp.StatusCode == 200 || resp.StatusCode == 304) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we consider 304 as a success?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yutongz I saw some times the file was cached and the status code was 304 when I was testing, but maybe that was on the web browser not the program. Not really sure about this one...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, make sure when you decide it failed, print out the err message or status code. Maybe we can find out some other magic. Actually, you may always make sure your program don't eat err message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status code 304 should not happen here, since we are doing an unconditional GET. I think it's fine to fail if it's anything other than 200.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@enisoc Done.

@roycaihw
Copy link
Member Author

@yutongz Thanks for the detailed review! It's very helpful. I've pushed a commit to address most of the problems.

@dims
Copy link
Member

dims commented Oct 11, 2017

Would this be a better fit in https://github.com/kubernetes/release repo? (Since we are trying to shrink the main k/k repo)

@roycaihw
Copy link
Member Author

@dims Yes we are checking this code into the kubernetes/release repo.

However it comes with a large vendor directory and there is no guideline about managing Golang dependency in the kubernetes/release (I think there are some guidelines in k/k repo). Is there any suggestion on this?

@david-mcmahon
Copy link
Contributor

@roycaihw what are the details behind the ~12 minute data collection times. That's a ~15x increase in runtime. Is there anything we can do to optimize this?

@enisoc
Copy link
Member

enisoc commented Oct 11, 2017

@roycaihw What tool did you use to populate the vendor directory? I didn't see a checked-in metadata file describing dependencies.

I tried dep recently and it works pretty well. Would it make sense to avoid checking in vendor/ at all, and just have bazel run dep ensure as part of building this tool? Or is there some other benefit to checking in the vendor dir?

@roycaihw
Copy link
Member Author

what are the details behind the ~12 minute data collection times. That's a ~15x increase in runtime. Is there anything we can do to optimize this?

@david-mcmahon In existing script we use github search API to search for PRs with "release-note" label, but github search API

  1. only provides up to 1,000 results for each search;
  2. has a rate limit about 30 requests per minute (compared with list API can do 5,000 requests per minute).

The 1,000-result limit makes us lose old (but related) PRs (for example when you want to search for "release-note" labelled PRs between v1.5.0..v1.5.2, you may only get PRs after kubernetes/kubernetes#46247 (v1.6)). So we decided to use github list API to fetch all issue&PRs from the repo, which takes ~12 minutes to get 50k issue/PRs (~500 requests). Listing all issues is also good for us to add the new feature, as we don't need to send one request for each issue fixed by one release-note PR.

To optimize:

  1. We could cache the issues locally at the first time.
  2. If we assume all PRs we care about are created after [Federation][kubefed]: Add support for etcd image override kubernetes#46247 (as for today), we could still use the search API.

@roycaihw
Copy link
Member Author

roycaihw commented Oct 11, 2017

What tool did you use to populate the vendor directory? I didn't see a checked-in metadata file describing dependencies.

@enisoc I didn't use any tool. I just copied the vendor directory (my apologies)

I tried dep recently and it works pretty well. Would it make sense to avoid checking in vendor/ at all, and just have bazel run dep ensure as part of building this tool? Or is there some other benefit to checking in the vendor dir?

Sounds handy. I will try that. Thanks!

@david-mcmahon
Copy link
Contributor

@roycaihw Yes, if there's a way to bound or otherwise restrict by some range (or date), such as using the date range between two tags if tag boundaries are not accepted.
And/or maybe switch to search API for "earlier" ranges within the 1000 limit and extend to list API for older? I know that's more moving parts, but the ~15x hit is non-insignificant especially during the release workflow and I suspect also for those using the tool to get previews.

@enisoc
Copy link
Member

enisoc commented Oct 11, 2017

the ~15x hit is non-insignificant especially during the release workflow and I suspect also for those using the tool to get previews

As a release manager, I can confirm that making this script as fast as possible for common use cases (e.g. looking only at things relevant to the current release) would be greatly appreciated.

I often run the current tool to look for PRs whose release notes are wrong or low quality. Then I fix up the PRs and run the tool again to make sure the changes worked as expected.

@roycaihw
Copy link
Member Author

@david-mcmahon @enisoc Got it. I'm working on using the search API.

@roycaihw
Copy link
Member Author

@david-mcmahon @enisoc I've pushed an update. Now we can use the search API to get >1,000 results, and the program sleeps for 30 seconds when it hits the rate limit. On my machine it takes 1.5 minutes to run ../release/bazel-bin/toolbox/relnotes/release_note_collector -preview -htmlize-md --html-file /tmp/release-note-html-testfile --release-tars=_output/release-tars v1.7.0..v1.7.2 now (used to be 10~15 minutes).

@roycaihw
Copy link
Member Author

@enisoc I've pushed an update to use dep and remove vendor/, but I'm not sure how to have bazel run dep commands. Now you have to manually run dep ensure and bazel run //:gazelle (to generate BUILD file in vendor directories) before compile. And the compile target is now bazel build toolbox/relnotes:relnotes.

@david-mcmahon
Copy link
Contributor

@roycaihw Awesome optimization using the search api!

No immediate rush on this but can you look at the changes in #437 to make this work for Istio as well? I think your implementation is even closer out the gate than the original relnotes was before #437.

At some point if we further generalize this, we should move it out of the kubernetes org, but that can happen after this PR goes in.

@roycaihw
Copy link
Member Author

@david-mcmahon Sounds good to me.

@enisoc @yutongz I've pushed a commit to address some remaining problems on my mind. Please take a look. Let me know when you want me to squash the commits.


// LastReleases looks up the list of releases on github and puts the last release per branch
// into a branch-indexed dictionary.
func LastReleases(c *github.Client, owner, repo string) (map[string]string, error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the OO design, I suggest you make a structure here. You can create something like this https://github.com/istio/test-infra/blob/master/toolbox/util/githubClient.go#L44. And most of functions here can be a method of this structure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

// GetCommitDate gets commit time for given tag/commit, provided with repository tags and commits.
// The function returns ok as false if input tag/commit cannot be found in the repository.
func GetCommitDate(c *github.Client, owner, repo, tagCommit string, tags []*github.RepositoryTag) (date time.Time, ok bool) {
// If input string is a tag, convert it into SHA
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you are given both tagCommit and tags. Do you have a perference?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input tagCommit can be either a tag or a SHA. The input tags is a list of all the tags in that repo and is used to look up a tag and get the corresponding SHA. This is because github API's git service can only get one commit by SHA (not by tag).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is your way, which is the proper way to call this function. But what if people call it with the illegal inputs, say giving you both tagCommit and tags, or tags which are not pointing to the same SHA, you need to tell this issue, log out message or/and throw out the error.
You are not the only one who is going to use this function later on, so it's your responsibility to make sure your function will not misbehavior with some wrong input.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Updated.

// The function returns ok as false if input tag/commit cannot be found in the repository.
func GetCommitDate(c *github.Client, owner, repo, tagCommit string, tags []*github.RepositoryTag) (date time.Time, ok bool) {
// If input string is a tag, convert it into SHA
for _, t := range tags {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if those tags are not pointing to the same SHA?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

return
}

func readGithubToken(filename string) string {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change it to ReadToken() and put it in one of the util files

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

commitPRs, err := parsePRFromCommit(releaseCommits)
if err != nil {
log.Printf("failed to parse release commits: %s", err)
os.Exit(1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should only use os.Exit() in main(), use return (with/without err)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}()

// Bootstrap notes for minor (new branch) releases
if *full || u.IsVer(info.releaseTag, "dotzero") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make "dotzero" as a const

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

// TODO: find a better way to tell failed response
if err == nil && (resp.StatusCode == 200 || resp.StatusCode == 304) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, make sure when you decide it failed, print out the err message or status code. Maybe we can find out some other magic. Actually, you may always make sure your program don't eat err message.

}
f.WriteString("\n")
} else {
log.Printf("No draft found - creating generic template...")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"No draft found" sounds like the error reason is "There is not such thing." What if it failed because of timeout, or authentication issue? You better say "Failed to do ..." or "Error when ..." plus printing out actual error message you get. Or you can return this error to upper level, and print it out there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
}
f.WriteString("\n")
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if err != nil. Should we have some message out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Printed out error message/status code

}

// extractReleaseNote tries to fetch release note from PR body, otherwise uses PR title.
func extractReleaseNote(pr *github.Issue) string {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change to name to "extractReleaseNoteFromPR"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@yutongz
Copy link

yutongz commented Oct 18, 2017

PTAL

@enisoc
Copy link
Member

enisoc commented Oct 19, 2017

I tried this out with my usual flow, and it LGTM.

One caveat to be aware of is that it seems to ignore flags that come after the first positional arg. I think this is a Go stdlib limitation that we can't avoid. We just need to be careful when substituting it in for the script, to make sure positional args come at the end.

@yutongz
Copy link

yutongz commented Oct 20, 2017

LGTM Thanks to make this!


var (
// Flags
// TODO: golang flags and parameters syntex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/syntex/syntax

var err error
*branch, err = u.GetCurrentBranch()
if err != nil {
log.Printf("not a git repository: %s", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the log leading by "not a git repository"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and should we change the placeholder from %s to %v for err in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log "not a git repository" follows the existing shell script. I will change it into "failed to get current branch".

I'm new to Golang so I don't know the convention about placeholder for error. I will make this change.

}

// Generating release note...
log.Printf("Generating release notes...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Print would be ok.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

// Start generating markdown file
log.Printf("Preparing layout...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above


if *githubToken == "" {
// If githubToken isn't specified in flag, use the GITHUB_TOKEN environment variable
*githubToken = os.Getenv("GITHUB_TOKEN")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"GITHUB_TOKEN" might be empty, so do we need to check *githubToken == "" again before line 113 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The release note generator doesn't need certain access authentication, but the program will experience bad request rate limit and can fail without Github token. Added check for *githubToken.

return nil, fmt.Errorf("failed to parse release commits: %s", err)
}

log.Printf("Gathering \"release-note\" labelled PRs using Github search API. This may take a while...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

if err != nil {
return nil, fmt.Errorf("failed to search release-note labelled PRs: %s", err)
}
log.Printf("\"release-note\" labelled PRs gathered.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

}
log.Printf("\"release-note\" labelled PRs gathered.")

log.Printf("Gathering \"release-note-action-required\" labelled PRs using Github search API.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

if err != nil {
return nil, fmt.Errorf("failed to search release-note-action-required labelled PRs: %s", err)
}
log.Printf("\"release-note-action-required\" labelled PRs gathered.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

// Bootstrap notes for minor (new branch) releases
if *full || u.IsVer(info.releaseTag, verDotzero) {
draftURL := fmt.Sprintf("%s%s/features/master/%s/release-notes-draft.md", u.GithubRawURL, *owner, *branch)
changelogURL := fmt.Sprintf("%s%s/%s/master/CHANGELOG.md", u.GithubRawURL, *owner, *repo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGELOG-1.x.md

Copy link
Contributor

@xiangpengzhao xiangpengzhao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The s/Printf/Print stuffs are from my IDE's complaining. Maybe we don't need to address them.


// getPendingPRs gets pending PRs on given branch in the repo.
func getPendingPRs(g *u.GithubClient, f *os.File, owner, repo, branch string) error {
log.Printf("Getting pending PR status...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

// createHTMLNote generates HTML release note based on the input markdown release note.
func createHTMLNote(htmlFileName, mdFileName string) error {
var result error
log.Printf("Generating HTML release note...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

// before running this function.
func getCIJobStatus(outputFile, branch string, htmlize bool) error {
var result error
log.Printf("Getting CI job status (this may take a while)...")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

f.WriteString(content)
f.WriteString("```\n")

log.Printf("CI job status fetched.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

if err != nil {
return fmt.Errorf("failed to create downloads table: %s", err)
}
err = createDownloadsTable(f, releaseTag, "Node Binaries", releaseTars+"/kubernetes-node*.tar.gz")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be refactored to use a table-driven for loop for these cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// minorReleases performs a minor (vX.Y.0) release by fetching the release template and aggregate
// previous release in series.
func minorRelease(f *os.File, release, draftURL, changelogURL string) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra line


if *releaseBucket == "kubernetes-release" {
urlPrefix = k8sReleaseURLPrefix
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if *releaseBucket == "" ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag has default value "kubernetes-release" and user can specify alternative google storage bucket to point to in generated notes. The download-table-creation logic is Kubernetes-specified and doesn't apply to other projects yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if user specifies the flag as ""? We will get https://storage.googleapis.com//release

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it only affect the URL text displayed in generated notes (the program doesn't actually try to fetch resources from that URL), and it's user's responsibility to provide correct address. I don't know if there are cases other than https://dl.k8s.io and https://storage.googleapis.com/<some bucket>/release. In that case user may manually modify the generated text.

I feel like we should keep the program running even if user specifies the flag as "". We can have the program log an error message when *releaseBucket == "", but it won't cover cases if user specifies some invalid non-existing google storage bucket.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed :)

// Regexp Example:
// Assume the release tag is v1.7.0, this regexp matches "- [v1.7.0-" in
// "- [v1.7.0-rc.1](#v170-rc1)"
// "- [v1.7.0-beta.2](#v170-beta2)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also alpha version something like "- v1.7.0-alpha.3", right?

}
f.WriteString("\n")
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also see in the CHANGELOG we have a section Deprecations. How is it generated? I guess we may want a new release-note-deprecation ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's from the release note drafts in kubernetes/features repo. I don't know how are the drafts generated though @david-mcmahon

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Release Notes for new minor (x.y.0) releases are populated from their respective feature branch repos, such as https://github.com/kubernetes/features/blob/master/release-1.7/release-notes-draft.md. These files are updated throughout the release cycle leading up to the .0 release.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new label release-note-deprecation will be helpful to auto-generate the section Deprecations. The items in this section are from the release-note-action-required PRs and then trriaged to this section manually I guess.

if note := re.FindStringSubmatch(*pr.Body); note != nil {
return note[1]
}
return *pr.Title
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still have this case now? i.e., a PR has a release-note label but without release-note block in PR body? @cjwagner

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the PR has the release-note label then there must be a release-note block. The only release-note-* label that does not require a release-note block is release-note-none.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find an example PR which has the release-note label but without a release-note block: kubernetes/kubernetes#54007 (comment)
The author (he is a repo maintainer) manually added the release-note label...

So the code here is correct...

// https://github.com/kubernetes/kubernetes/blob/master/.github/PULL_REQUEST_TEMPLATE.md
re, _ := regexp.Compile("```release-note\r\n(.+)\r\n```")
if note := re.FindStringSubmatch(*pr.Body); note != nil {
return note[1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may also want to exclude some corner cases such as a PR with release note NONE. (the PR will get a trelease-note label). I saw many such cases in changelogs. Yeah maybe it's label plugin's responsibility.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean

`NONE`

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xiangpengzhao
Copy link
Contributor

@roycaihw does this PR update TOC? I don't find associated code here. I might miss something...

@xiangpengzhao
Copy link
Contributor

Another thing, we may also need to update https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG.md.

Below is its content. For example, when we release 1.9, we need to change Current release to 1.9 and Development releases to 1.10, and add 1.8 to Older releases. This md can be updated manually though.

Development releases:

Current release:

Older releases:

@roycaihw
Copy link
Member Author

does this PR update TOC? I don't find associated code here. I might miss something...

@xiangpengzhao which table do you mean? This tool collects release note and generates markdown/HTML file locally (e.g. at /tmp/release-notes-release-1.9.md). Updating the Github repo is out of this tool's scope.

For the CHANGELOG.md, yes it's easier to do it manually.

@david-mcmahon
Copy link
Contributor

@roycaihw I believe @xiangpengzhao is referring to the call to common::mdtoc(). I haven't verified this myself either in the current implementation.

@xiangpengzhao
Copy link
Contributor

yeah @david-mcmahon mentioned is what I meant :)

@roycaihw
Copy link
Member Author

I see a mix of "-" and "--" for flags. "--" is standard. Can we ensure the docs and parsers are using "--"?

@david-mcmahon Updated.

@xiangpengzhao
Copy link
Contributor

I ran ../release/bazel-bin/toolbox/relnotes/relnotes --preview --htmlize-md --html-file /tmp/release-note-html-testfile --release-tars=_output/release-tars v1.7.0..v1.7.2 and got below result.

There might be two nits here:

  • # Branch v1.7.2 : in current CHANGELOG file in K/K, there isn't the word Branch. Do we need it?
  • the link of Documentation is https://docs.k8s.io, do we need to change it to the release's specific link:https://v1-7.docs.kubernetes.io for each release. But there might be an issue here, AFAIK, the latest release's doc branch is published after the release is published. So when we generate CHANGELOG for the latest release (e.g., 1.9), the doc release for it (1.9) isn't published yet. cc @steveperry-53 @zacharysarah @Bradamant3

RESULT BELOW

cloud@ubuntu:/tmp$ cat release-notes-release-1.7.md
Release Note Preview - generated on Wed Nov 1 01:42:23 CST 2017

Branch v1.7.2

Documentation & Examples

Downloads for Branch v1.7.2

I omitted

Changelog since v1.7.0

Other notable changes

I omitted


PENDING PRs on the release-1.7 branch

PR Milestone User Date Commit Message

*I omitted

State of release-1.7 branch

NOT READY

Details

find_green_build: BEGIN main on ubuntu Wed Nov  1 01:42:29 CST 2017

Checking for a valid github API token: OK
Checking required system packages: OK
Checking required PIP packages: OK
Checking/setting cloud tools: FAILED


Signal ERR caught!

Traceback (line function script):
894 main /home/cloud/go/src/k8s.io/release/find_green_build

Exiting...


find_green_build: DONE main on ubuntu Wed Nov  1 01:42:31 CST 2017 in 2s

@roycaihw
Copy link
Member Author

roycaihw commented Nov 1, 2017

# Branch v1.7.2 : in current CHANGELOG file in K/K, there isn't the word Branch. Do we need it?

@xiangpengzhao The prefix "Branch " shows up if the tool runs in preview mode. This follows the existing tool's behavior.

@xiangpengzhao
Copy link
Contributor

@roycaihw thanks for clarification!

@xiangpengzhao
Copy link
Contributor

@roycaihw is this PR ready to merge?

@roycaihw
Copy link
Member Author

is this PR ready to merge?

@xiangpengzhao @david-mcmahon I've squashed the commits. The original commits can be found here. This PR is ready to merge.

@xiangpengzhao
Copy link
Contributor

@roycaihw will this go version release note tool be invoked by anago the same way as the script version tool is invoked?

@roycaihw
Copy link
Member Author

will this go version release note tool be invoked by anago the same way as the script version tool is invoked?

@xiangpengzhao Yes, it supports the same flags. The only difference is as @enisoc pointed out:

One caveat to be aware of is that it seems to ignore flags that come after the first positional arg. I think this is a Go stdlib limitation that we can't avoid. We just need to be careful when substituting it in for the script, to make sure positional args come at the end.

@david-mcmahon
Copy link
Contributor

As the new tool is golang and needs to be built during the execution of anago, I'm trying to find the least costly, least invasive way of doing this so it just runs inline without any disruption to the current workflow. If anyone has a thought on this, take a stab at it and send me a PR.

@enisoc
Copy link
Member

enisoc commented Nov 17, 2017

@xiangpengzhao Are you ready to give an official LGTM for this?

@david-mcmahon After this merges, I can try to work on something. Are we allowed to invoke docker in all the various environments in which anago runs?

@david-mcmahon
Copy link
Contributor

@enisoc depends on your definition of "invoke". We explicitly have to work around running docker-in-docker to operate within the containerized GCB environment to build k8s there. Rather than do this, we had to run outside of the build-image.

@xiangpengzhao
Copy link
Contributor

@enisoc I'd like this PR to get merged for now and we try it out in the next alpha/beta publishing :)
cc @dashpole
/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Nov 18, 2017
@enisoc
Copy link
Member

enisoc commented Nov 21, 2017

I'm merging this now, but I think there's no hurry to get this integrated in anago. My priority is just to be able to run it manually with the hierarchical changes, so we can use it as a starting point for the hand-written release notes.

@enisoc enisoc merged commit fed9f68 into kubernetes:master Nov 21, 2017
@xiangpengzhao
Copy link
Contributor

@enisoc agreed.

@roycaihw roycaihw deleted the relnotes-release branch March 13, 2018 20:36
marpaia pushed a commit to marpaia/release that referenced this pull request Feb 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants