github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/step_changelog.go (about) 1 package step 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "regexp" 11 "sort" 12 "strings" 13 "text/template" 14 "time" 15 16 "github.com/olli-ai/jx/v2/pkg/builds" 17 18 "github.com/olli-ai/jx/v2/pkg/cmd/opts/step" 19 20 "github.com/olli-ai/jx/v2/pkg/dependencymatrix" 21 22 "github.com/olli-ai/jx/v2/pkg/cmd/helper" 23 "github.com/olli-ai/jx/v2/pkg/kube/naming" 24 25 "github.com/pkg/errors" 26 27 "github.com/olli-ai/jx/v2/pkg/users" 28 29 "github.com/ghodss/yaml" 30 jenkinsio "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io" 31 v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1" 32 "github.com/jenkins-x/jx-logging/pkg/log" 33 "github.com/olli-ai/jx/v2/pkg/cmd/opts" 34 "github.com/olli-ai/jx/v2/pkg/cmd/templates" 35 "github.com/olli-ai/jx/v2/pkg/gits" 36 "github.com/olli-ai/jx/v2/pkg/issues" 37 "github.com/olli-ai/jx/v2/pkg/kube" 38 "github.com/olli-ai/jx/v2/pkg/util" 39 "github.com/spf13/cobra" 40 "gopkg.in/src-d/go-git.v4/plumbing/object" 41 42 chgit "github.com/antham/chyle/chyle/git" 43 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 44 ) 45 46 // StepChangelogOptions contains the command line flags 47 type StepChangelogOptions struct { 48 step.StepOptions 49 50 PreviousRevision string 51 PreviousDate string 52 CurrentRevision string 53 TemplatesDir string 54 ReleaseYamlFile string 55 CrdYamlFile string 56 Dir string 57 Version string 58 Build string 59 Header string 60 HeaderFile string 61 Footer string 62 FooterFile string 63 OutputMarkdownFile string 64 OverwriteCRD bool 65 GenerateCRD bool 66 GenerateReleaseYaml bool 67 UpdateRelease bool 68 NoReleaseInDev bool 69 IncludeMergeCommits bool 70 FailIfFindCommits bool 71 State StepChangelogState 72 } 73 74 type StepChangelogState struct { 75 GitInfo *gits.GitRepository 76 GitProvider gits.GitProvider 77 Tracker issues.IssueProvider 78 FoundIssueNames map[string]bool 79 LoggedIssueKind bool 80 Release *v1.Release 81 } 82 83 const ( 84 ReleaseName = `{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}` 85 86 SpecName = `{{ .Chart.Name }}` 87 SpecVersion = `{{ .Chart.Version }}` 88 89 ReleaseCrdYaml = `apiVersion: apiextensions.k8s.io/v1beta1 90 kind: CustomResourceDefinition 91 metadata: 92 creationTimestamp: 2018-02-24T14:56:33Z 93 name: releases.jenkins.io 94 resourceVersion: "557150" 95 selfLink: /apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/releases.jenkins.io 96 uid: e77f4e08-1972-11e8-988e-42010a8401df 97 spec: 98 group: jenkins.io 99 names: 100 kind: Release 101 listKind: ReleaseList 102 plural: releases 103 shortNames: 104 - rel 105 singular: release 106 categories: 107 - all 108 scope: Namespaced 109 version: v1` 110 ) 111 112 var ( 113 GitAccessDescription = ` 114 115 By default jx commands look for a file '~/.jx/gitAuth.yaml' to find the API tokens for Git servers. You can use 'jx create git token' to create a Git token. 116 117 Alternatively if you are running this command inside a CI server you can use environment variables to specify the username and API token. 118 e.g. define environment variables GIT_USERNAME and GIT_API_TOKEN 119 ` 120 121 StepChangelogLong = templates.LongDesc(` 122 Generates a Changelog for the latest tag 123 124 This command will generate a Changelog as markdown for the git commit range given. 125 If you are using GitHub it will also update the GitHub Release with the changelog. You can disable that by passing'--update-release=false' 126 127 If you have just created a git tag this command will try default to the changes between the last tag and the previous one. You can always specify the exact Git references (tag/sha) directly via '--previous-rev' and '--rev' 128 129 The changelog is generated by parsing the git commits. It will also detect any text like 'fixes #123' to link to issue fixes. You can also use Conventional Commits notation: https://conventionalcommits.org/ to get a nicer formatted changelog. e.g. using commits like 'fix:(my feature) this my fix' or 'feat:(cheese) something' 130 131 This command also generates a Release Custom Resource Definition you can include in your helm chart to give metadata about the changelog of the application along with metadata about the release (git tag, url, commits, issues fixed etc). Including this metadata in a helm charts means we can do things like automatically comment on issues when they hit Staging or Production; or give detailed descriptions of what things have changed when using GitOps to update versions in an environment by referencing the fixed issues in the Pull Request. 132 133 You can opt out of the release YAML generation via the '--generate-yaml=false' option 134 135 To update the release notes on GitHub / Gitea this command needs a git API token. 136 137 `) + GitAccessDescription 138 139 StepChangelogExample = templates.Examples(` 140 # generate a changelog on the current source 141 jx step changelog 142 143 # specify the version to use 144 jx step changelog --version 1.2.3 145 146 # specify the version and a header template 147 jx step changelog --header-file docs/dev/changelog-header.md --version 1.2.3 148 149 `) 150 151 GitHubIssueRegex = regexp.MustCompile(`(\#\d+)`) 152 JIRAIssueRegex = regexp.MustCompile(`[A-Z][A-Z]+-(\d+)`) 153 ) 154 155 func NewCmdStepChangelog(commonOpts *opts.CommonOptions) *cobra.Command { 156 options := StepChangelogOptions{ 157 StepOptions: step.StepOptions{ 158 CommonOptions: commonOpts, 159 }, 160 } 161 cmd := &cobra.Command{ 162 Use: "changelog", 163 Short: "Creates a changelog for a git tag", 164 Aliases: []string{"changes"}, 165 Long: StepChangelogLong, 166 Example: StepChangelogExample, 167 Run: func(cmd *cobra.Command, args []string) { 168 options.Cmd = cmd 169 options.Args = args 170 err := options.Run() 171 helper.CheckErr(err) 172 }, 173 } 174 175 cmd.Flags().StringVarP(&options.PreviousRevision, "previous-rev", "p", "", "the previous tag revision") 176 cmd.Flags().StringVarP(&options.PreviousDate, "previous-date", "", "", "the previous date to find a revision in format 'MonthName dayNumber year'") 177 cmd.Flags().StringVarP(&options.CurrentRevision, "rev", "r", "", "the current tag revision") 178 cmd.Flags().StringVarP(&options.TemplatesDir, "templates-dir", "t", "", "the directory containing the helm chart templates to generate the resources") 179 cmd.Flags().StringVarP(&options.ReleaseYamlFile, "release-yaml-file", "", "release.yaml", "the name of the file to generate the Release YAML") 180 cmd.Flags().StringVarP(&options.CrdYamlFile, "crd-yaml-file", "", "release-crd.yaml", "the name of the file to generate the Release CustomResourceDefinition YAML") 181 cmd.Flags().StringVarP(&options.Version, "version", "v", "", "The version to release") 182 cmd.Flags().StringVarP(&options.Build, "build", "", "", "The Build number which is used to update the PipelineActivity. If not specified its defaulted from the '$BUILD_NUMBER' environment variable") 183 cmd.Flags().StringVarP(&options.Dir, "dir", "", "", "The directory of the Git repository. Defaults to the current working directory") 184 cmd.Flags().StringVarP(&options.OutputMarkdownFile, "output-markdown", "", "", "The file to generate for the changelog output if not updating a Git provider release") 185 cmd.Flags().BoolVarP(&options.OverwriteCRD, "overwrite", "o", false, "overwrites the Release CRD YAML file if it exists") 186 cmd.Flags().BoolVarP(&options.GenerateCRD, "crd", "c", false, "Generate the CRD in the chart") 187 cmd.Flags().BoolVarP(&options.GenerateReleaseYaml, "generate-yaml", "y", true, "Generate the Release YAML in the local helm chart") 188 cmd.Flags().BoolVarP(&options.UpdateRelease, "update-release", "", true, "Should we update the release on the Git repository with the changelog") 189 cmd.Flags().BoolVarP(&options.NoReleaseInDev, "no-dev-release", "", false, "Disables the generation of Release CRDs in the development namespace to track releases being performed") 190 cmd.Flags().BoolVarP(&options.IncludeMergeCommits, "include-merge-commits", "", false, "Include merge commits when generating the changelog") 191 cmd.Flags().BoolVarP(&options.FailIfFindCommits, "fail-if-no-commits", "", false, "Do we want to fail the build if we don't find any commits to generate the changelog") 192 193 cmd.Flags().StringVarP(&options.Header, "header", "", "", "The changelog header in markdown for the changelog. Can use go template expressions on the ReleaseSpec object: https://golang.org/pkg/text/template/") 194 cmd.Flags().StringVarP(&options.HeaderFile, "header-file", "", "", "The file name of the changelog header in markdown for the changelog. Can use go template expressions on the ReleaseSpec object: https://golang.org/pkg/text/template/") 195 cmd.Flags().StringVarP(&options.Footer, "footer", "", "", "The changelog footer in markdown for the changelog. Can use go template expressions on the ReleaseSpec object: https://golang.org/pkg/text/template/") 196 cmd.Flags().StringVarP(&options.FooterFile, "footer-file", "", "", "The file name of the changelog footer in markdown for the changelog. Can use go template expressions on the ReleaseSpec object: https://golang.org/pkg/text/template/") 197 198 return cmd 199 } 200 201 func (o *StepChangelogOptions) Run() error { 202 // lets enable batch mode if we detect we are inside a pipeline 203 if !o.BatchMode && builds.GetBuildNumber() != "" { 204 log.Logger().Info("Using batch mode as inside a pipeline") 205 o.BatchMode = true 206 } 207 208 dir := o.Dir 209 var err error 210 if dir == "" { 211 dir, err = os.Getwd() 212 if err != nil { 213 return err 214 } 215 } 216 217 // Ensure we don't have a shallow checkout in git 218 err = gits.Unshallow(dir, o.Git()) 219 if err != nil { 220 return errors.Wrapf(err, "error unshallowing git repo in %s", dir) 221 } 222 previousRev := o.PreviousRevision 223 if previousRev == "" { 224 previousDate := o.PreviousDate 225 if previousDate != "" { 226 previousRev, err = o.Git().GetRevisionBeforeDateText(dir, previousDate) 227 if err != nil { 228 return fmt.Errorf("Failed to find commits before date %s: %s", previousDate, err) 229 } 230 } 231 } 232 if previousRev == "" { 233 previousRev, _, err = o.Git().GetCommitPointedToByPreviousTag(dir) 234 if err != nil { 235 return err 236 } 237 if previousRev == "" { 238 // lets assume we are the first release 239 previousRev, err = o.Git().GetFirstCommitSha(dir) 240 if err != nil { 241 return errors.Wrap(err, "failed to find first commit after we found no previous releaes") 242 } 243 if previousRev == "" { 244 log.Logger().Info("no previous commit version found so change diff unavailable") 245 return nil 246 } 247 } 248 } 249 currentRev := o.CurrentRevision 250 if currentRev == "" { 251 currentRev, _, err = o.Git().GetCommitPointedToByLatestTag(dir) 252 if err != nil { 253 return err 254 } 255 } 256 257 templatesDir := o.TemplatesDir 258 if templatesDir == "" { 259 chartFile, err := o.FindHelmChart() 260 if err != nil { 261 return fmt.Errorf("Could not find helm chart %s", err) 262 } 263 path, _ := filepath.Split(chartFile) 264 templatesDir = filepath.Join(path, "templates") 265 } 266 err = os.MkdirAll(templatesDir, util.DefaultWritePermissions) 267 if err != nil { 268 return fmt.Errorf("Failed to create the templates directory %s due to %s", templatesDir, err) 269 } 270 271 log.Logger().Infof("Generating change log from git ref %s => %s", util.ColorInfo(previousRev), util.ColorInfo(currentRev)) 272 273 gitDir, gitConfDir, err := o.Git().FindGitConfigDir(dir) 274 if err != nil { 275 return err 276 } 277 if gitDir == "" || gitConfDir == "" { 278 log.Logger().Warnf("No git directory could be found from dir %s", dir) 279 return nil 280 } 281 282 gitUrl, err := o.Git().DiscoverUpstreamGitURL(gitConfDir) 283 if err != nil { 284 return err 285 } 286 gitInfo, err := gits.ParseGitURL(gitUrl) 287 if err != nil { 288 return err 289 } 290 o.State.GitInfo = gitInfo 291 292 tracker, err := o.CreateIssueProvider(dir) 293 if err != nil { 294 return err 295 } 296 o.State.Tracker = tracker 297 298 authConfigSvc, err := o.GitAuthConfigService() 299 if err != nil { 300 return err 301 } 302 jxClient, devNs, err := o.JXClientAndDevNamespace() 303 if err != nil { 304 return err 305 } 306 307 gitKind, err := o.GitServerKind(gitInfo) 308 foundGitProvider := true 309 ghOwner, err := o.GetGitHubAppOwner(gitInfo) 310 if err != nil { 311 return err 312 } 313 gitProvider, err := o.State.GitInfo.CreateProvider(o.InCluster(), authConfigSvc, gitKind, ghOwner, o.Git(), o.BatchMode, o.GetIOFileHandles()) 314 if err != nil { 315 foundGitProvider = false 316 log.Logger().Warnf("Could not create GitProvide so cannot update the release notes: %s", err) 317 } 318 o.State.GitProvider = gitProvider 319 o.State.FoundIssueNames = map[string]bool{} 320 321 commits, err := chgit.FetchCommits(gitDir, previousRev, currentRev) 322 if err != nil { 323 if o.FailIfFindCommits { 324 return err 325 } 326 log.Logger().Warnf("failed to find git commits between revision %s and %s due to: %s", previousRev, currentRev, err.Error()) 327 } 328 if commits != nil { 329 commits1 := *commits 330 if len(commits1) > 0 { 331 if strings.HasPrefix(commits1[0].Message, "release ") { 332 // remove the release commit from the log 333 tmp := commits1[1:] 334 commits = &tmp 335 } 336 } 337 log.Logger().Debugf("Found commits:") 338 for _, commit := range *commits { 339 log.Logger().Debugf(" commit %s", commit.Hash) 340 log.Logger().Debugf(" Author: %s <%s>", commit.Author.Name, commit.Author.Email) 341 log.Logger().Debugf(" Date: %s", commit.Committer.When.Format(time.ANSIC)) 342 log.Logger().Debugf(" %s\n\n\n", commit.Message) 343 } 344 } 345 version := o.Version 346 if version == "" { 347 version = SpecVersion 348 } 349 350 release := &v1.Release{ 351 TypeMeta: metav1.TypeMeta{ 352 Kind: "Release", 353 APIVersion: jenkinsio.GroupAndVersion, 354 }, 355 ObjectMeta: metav1.ObjectMeta{ 356 Name: ReleaseName, 357 CreationTimestamp: metav1.Time{ 358 Time: time.Now(), 359 }, 360 //ResourceVersion: "1", 361 DeletionTimestamp: &metav1.Time{}, 362 }, 363 Spec: v1.ReleaseSpec{ 364 Name: SpecName, 365 Version: version, 366 GitOwner: gitInfo.Organisation, 367 GitRepository: gitInfo.Name, 368 GitHTTPURL: gitInfo.HttpsURL(), 369 GitCloneURL: gits.HttpCloneURL(gitInfo, gitKind), 370 Commits: []v1.CommitSummary{}, 371 Issues: []v1.IssueSummary{}, 372 PullRequests: []v1.IssueSummary{}, 373 }, 374 } 375 376 resolver := users.GitUserResolver{ 377 GitProvider: gitProvider, 378 Namespace: devNs, 379 JXClient: jxClient, 380 } 381 if commits != nil && gitProvider != nil { 382 for _, commit := range *commits { 383 c := commit 384 if o.IncludeMergeCommits || len(commit.ParentHashes) <= 1 { 385 o.addCommit(&release.Spec, &c, &resolver) 386 } 387 } 388 } 389 390 release.Spec.DependencyUpdates = CollapseDependencyUpdates(release.Spec.DependencyUpdates) 391 392 // lets try to update the release 393 markdown, err := gits.GenerateMarkdown(&release.Spec, gitInfo) 394 if err != nil { 395 return err 396 } 397 header, err := o.getTemplateResult(&release.Spec, "header", o.Header, o.HeaderFile) 398 if err != nil { 399 return err 400 } 401 footer, err := o.getTemplateResult(&release.Spec, "footer", o.Footer, o.FooterFile) 402 if err != nil { 403 return err 404 } 405 markdown = header + markdown + footer 406 407 log.Logger().Debugf("Generated release notes:\n\n%s\n", markdown) 408 409 if version != "" && o.UpdateRelease && foundGitProvider { 410 tags, err := o.Git().FilterTags(o.Dir, version) 411 if err != nil { 412 return errors.Wrapf(err, "listing tags with pattern %s in %s", version, o.Dir) 413 } 414 vVersion := fmt.Sprintf("v%s", version) 415 vtags, err := o.Git().FilterTags(o.Dir, vVersion) 416 if err != nil { 417 return errors.Wrapf(err, "listing tags with pattern %s in %s", vVersion, o.Dir) 418 } 419 foundTag := false 420 foundVTag := false 421 422 for _, t := range tags { 423 if t == version { 424 foundTag = true 425 break 426 } 427 } 428 for _, t := range vtags { 429 if t == vVersion { 430 foundVTag = true 431 break 432 } 433 } 434 tagName := version 435 if foundVTag && !foundTag { 436 tagName = vVersion 437 } 438 releaseInfo := &gits.GitRelease{ 439 Name: version, 440 TagName: tagName, 441 Body: markdown, 442 } 443 url := releaseInfo.HTMLURL 444 if url == "" { 445 url = releaseInfo.URL 446 } 447 if url == "" { 448 url = util.UrlJoin(gitInfo.HttpsURL(), "releases/tag", tagName) 449 } 450 err = gitProvider.UpdateRelease(gitInfo.Organisation, gitInfo.Name, tagName, releaseInfo) 451 if err != nil { 452 log.Logger().Warnf("Failed to update the release at %s: %s", url, err) 453 return nil 454 } 455 release.Spec.ReleaseNotesURL = url 456 log.Logger().Infof("Updated the release information at %s", util.ColorInfo(url)) 457 458 // First, attach the current dependency matrix 459 dependencyMatrixFileName := filepath.Join(dir, dependencymatrix.DependencyMatrixDirName, dependencymatrix.DependencyMatrixYamlFileName) 460 if info, err := os.Stat(dependencyMatrixFileName); err != nil && os.IsNotExist(err) { 461 log.Logger().Debugf("Not adding dependency matrix %s as does not exist", dependencyMatrixFileName) 462 } else if err != nil { 463 return errors.Wrapf(err, "checking if %s exists", dependencyMatrixFileName) 464 } else if info.Size() == 0 { 465 log.Logger().Debugf("Not adding dependency matrix %s as has no content", dependencyMatrixFileName) 466 } else { 467 file, err := os.Open(dependencyMatrixFileName) 468 // The file will be closed by the release asset uploader 469 if err != nil { 470 return errors.Wrapf(err, "opening %s", dependencyMatrixFileName) 471 } 472 releaseAsset, err := gitProvider.UploadReleaseAsset(gitInfo.Organisation, gitInfo.Name, releaseInfo.ID, dependencymatrix.DependencyMatrixAssetName, file) 473 if err != nil { 474 return errors.Wrapf(err, "uploading %s to release %d of %s/%s", dependencyMatrixFileName, releaseInfo.ID, gitInfo.Organisation, gitInfo.Name) 475 } 476 log.Logger().Infof("Uploaded %s to release asset %s", dependencyMatrixFileName, releaseAsset.BrowserDownloadURL) 477 } 478 if len(release.Spec.DependencyUpdates) > 0 { 479 // Now, let's attach any dependency updates that were done as part of this release 480 file, err := ioutil.TempFile("", "") 481 if err != nil { 482 return errors.Wrapf(err, "creating temp file to write dependency updates to") 483 } 484 data := dependencymatrix.DependencyUpdates{ 485 Updates: release.Spec.DependencyUpdates, 486 } 487 bytes, err := yaml.Marshal(data) 488 if err != nil { 489 return errors.Wrapf(err, "marshaling %+v to yaml", data) 490 } 491 err = ioutil.WriteFile(file.Name(), bytes, 0600) 492 if err != nil { 493 return errors.Wrapf(err, "writing dependency update yaml to %s", file.Name()) 494 } 495 releaseAsset, err := gitProvider.UploadReleaseAsset(gitInfo.Organisation, gitInfo.Name, releaseInfo.ID, dependencymatrix.DependencyUpdatesAssetName, file) 496 if err != nil { 497 return errors.Wrapf(err, "uploading %s to release %d of %s/%s", dependencymatrix.DependencyUpdatesAssetName, releaseInfo.ID, gitInfo.Organisation, gitInfo.Name) 498 } 499 log.Logger().Infof("Uploaded %s to release asset %s", dependencymatrix.DependencyUpdatesAssetName, releaseAsset.BrowserDownloadURL) 500 } 501 502 } else if o.OutputMarkdownFile != "" { 503 err := ioutil.WriteFile(o.OutputMarkdownFile, []byte(markdown), util.DefaultWritePermissions) 504 if err != nil { 505 return err 506 } 507 log.Logger().Infof("\nGenerated Changelog: %s", util.ColorInfo(o.OutputMarkdownFile)) 508 } else { 509 log.Logger().Infof("\nGenerated Changelog:") 510 log.Logger().Infof("%s\n", markdown) 511 } 512 513 o.State.Release = release 514 // now lets marshal the release YAML 515 data, err := yaml.Marshal(release) 516 517 if err != nil { 518 return err 519 } 520 if data == nil { 521 return fmt.Errorf("Could not marshal release to yaml") 522 } 523 releaseFile := filepath.Join(templatesDir, o.ReleaseYamlFile) 524 crdFile := filepath.Join(templatesDir, o.CrdYamlFile) 525 if o.GenerateReleaseYaml { 526 err = ioutil.WriteFile(releaseFile, data, util.DefaultWritePermissions) 527 if err != nil { 528 return fmt.Errorf("Failed to save Release YAML file %s: %s", releaseFile, err) 529 } 530 log.Logger().Infof("generated: %s", util.ColorInfo(releaseFile)) 531 } 532 cleanVersion := strings.TrimPrefix(version, "v") 533 release.Spec.Version = cleanVersion 534 if o.GenerateCRD { 535 exists, err := util.FileExists(crdFile) 536 if err != nil { 537 return fmt.Errorf("Failed to check for CRD YAML file %s: %s", crdFile, err) 538 } 539 if o.OverwriteCRD || !exists { 540 err = ioutil.WriteFile(crdFile, []byte(ReleaseCrdYaml), util.DefaultWritePermissions) 541 if err != nil { 542 return fmt.Errorf("Failed to save Release CRD YAML file %s: %s", crdFile, err) 543 } 544 log.Logger().Infof("generated: %s", util.ColorInfo(crdFile)) 545 } 546 } 547 appName := "" 548 if gitInfo != nil { 549 appName = gitInfo.Name 550 } 551 if appName == "" { 552 appName = release.Spec.Name 553 } 554 if appName == "" { 555 appName = release.Spec.GitRepository 556 } 557 if !o.NoReleaseInDev { 558 devRelease := *release 559 devRelease.ResourceVersion = "" 560 devRelease.Namespace = devNs 561 devRelease.Name = naming.ToValidName(appName + "-" + cleanVersion) 562 devRelease.Spec.Name = appName 563 _, err := kube.GetOrCreateRelease(jxClient, devNs, &devRelease) 564 if err != nil { 565 log.Logger().Warnf("%s", err) 566 } else { 567 log.Logger().Infof("Created Release %s resource in namespace %s", devRelease.Name, devNs) 568 } 569 } 570 releaseNotesURL := release.Spec.ReleaseNotesURL 571 pipeline := "" 572 build := o.Build 573 pipeline, build = o.GetPipelineName(gitInfo, pipeline, build, appName) 574 if pipeline != "" && build != "" { 575 name := naming.ToValidName(pipeline + "-" + build) 576 // lets see if we can update the pipeline 577 activities := jxClient.JenkinsV1().PipelineActivities(devNs) 578 lastCommitSha := "" 579 lastCommitMessage := "" 580 lastCommitURL := "" 581 commits := release.Spec.Commits 582 if len(commits) > 0 { 583 lastCommit := commits[len(commits)-1] 584 lastCommitSha = lastCommit.SHA 585 lastCommitMessage = lastCommit.Message 586 lastCommitURL = lastCommit.URL 587 } 588 log.Logger().Infof("Updating PipelineActivity %s with version %s", name, cleanVersion) 589 590 key := &kube.PromoteStepActivityKey{ 591 PipelineActivityKey: kube.PipelineActivityKey{ 592 Name: name, 593 Pipeline: pipeline, 594 Build: build, 595 ReleaseNotesURL: releaseNotesURL, 596 LastCommitSHA: lastCommitSha, 597 LastCommitMessage: lastCommitMessage, 598 LastCommitURL: lastCommitURL, 599 Version: cleanVersion, 600 GitInfo: gitInfo, 601 }, 602 } 603 _, currentNamespace, err := o.KubeClientAndNamespace() 604 if err != nil { 605 return errors.Wrap(err, "getting current namespace") 606 } 607 a, created, err := key.GetOrCreate(jxClient, currentNamespace) 608 if err == nil && a != nil && !created { 609 _, err = activities.PatchUpdate(a) 610 if err != nil { 611 log.Logger().Warnf("Failed to update PipelineActivities %s: %s", name, err) 612 } else { 613 log.Logger().Infof("Updated PipelineActivities %s with release notes URL: %s", util.ColorInfo(name), util.ColorInfo(releaseNotesURL)) 614 } 615 } 616 } else { 617 log.Logger().Infof("No pipeline and build number available on $JOB_NAME and $BUILD_NUMBER so cannot update PipelineActivities with the ReleaseNotesURL") 618 } 619 return nil 620 } 621 622 func (o *StepChangelogOptions) addCommit(spec *v1.ReleaseSpec, commit *object.Commit, resolver *users.GitUserResolver) { 623 // TODO 624 url := "" 625 branch := "master" 626 627 var author, committer *v1.User 628 var err error 629 sha := commit.Hash.String() 630 if commit.Author.Email != "" && commit.Author.Name != "" { 631 author, err = resolver.GitSignatureAsUser(&commit.Author) 632 if err != nil { 633 log.Logger().Warnf("failed to enrich commit with issues, error getting git signature for git author %s: %v", commit.Author, err) 634 } 635 } 636 if commit.Committer.Email != "" && commit.Committer.Name != "" { 637 committer, err = resolver.GitSignatureAsUser(&commit.Committer) 638 if err != nil { 639 log.Logger().Warnf("failed to enrich commit with issues, error getting git signature for git committer %s: %v", commit.Committer, err) 640 } 641 } 642 var authorDetails, committerDetails v1.UserDetails 643 if author != nil { 644 authorDetails = author.Spec 645 } 646 if committer != nil { 647 committerDetails = committer.Spec 648 } 649 dependencyUpdate, upstreamUpdates, err := o.ParseDependencyUpdateMessage(commit.Message, spec.GitCloneURL) 650 if err != nil { 651 log.Logger().Infof("Parsing %s for dependency updates", commit.Message) 652 } 653 if dependencyUpdate != nil { 654 if spec.DependencyUpdates == nil { 655 spec.DependencyUpdates = make([]v1.DependencyUpdate, 0) 656 } 657 spec.DependencyUpdates = append(spec.DependencyUpdates, *dependencyUpdate) 658 } 659 if upstreamUpdates != nil { 660 for _, u := range upstreamUpdates.Updates { 661 spec.DependencyUpdates = append(spec.DependencyUpdates, u) 662 } 663 } 664 commitSummary := v1.CommitSummary{ 665 Message: commit.Message, 666 URL: url, 667 SHA: sha, 668 Author: &authorDetails, 669 Branch: branch, 670 Committer: &committerDetails, 671 } 672 673 err = o.addIssuesAndPullRequests(spec, &commitSummary, commit) 674 if err != nil { 675 log.Logger().Warnf("Failed to enrich commit %s with issues: %s", sha, err) 676 } 677 spec.Commits = append(spec.Commits, commitSummary) 678 679 } 680 681 func (o *StepChangelogOptions) addIssuesAndPullRequests(spec *v1.ReleaseSpec, commit *v1.CommitSummary, rawCommit *object.Commit) error { 682 tracker := o.State.Tracker 683 684 gitProvider := o.State.GitProvider 685 if gitProvider == nil || !gitProvider.HasIssues() { 686 return nil 687 } 688 regex := GitHubIssueRegex 689 issueKind := issues.GetIssueProvider(tracker) 690 if !o.State.LoggedIssueKind { 691 o.State.LoggedIssueKind = true 692 log.Logger().Infof("Finding issues in commit messages using %s format", issueKind) 693 } 694 if issueKind == issues.Jira { 695 regex = JIRAIssueRegex 696 } 697 message := fullCommitMessageText(rawCommit) 698 699 matches := regex.FindAllStringSubmatch(message, -1) 700 jxClient, ns, err := o.JXClientAndDevNamespace() 701 if err != nil { 702 return err 703 } 704 resolver := users.GitUserResolver{ 705 JXClient: jxClient, 706 Namespace: ns, 707 GitProvider: gitProvider, 708 } 709 for _, match := range matches { 710 for _, result := range match { 711 result = strings.TrimPrefix(result, "#") 712 if _, ok := o.State.FoundIssueNames[result]; !ok { 713 o.State.FoundIssueNames[result] = true 714 issue, err := tracker.GetIssue(result) 715 if err != nil { 716 log.Logger().Warnf("Failed to lookup issue %s in issue tracker %s due to %s", result, tracker.HomeURL(), err) 717 continue 718 } 719 if issue == nil { 720 log.Logger().Warnf("Failed to find issue %s for repository %s", result, tracker.HomeURL()) 721 continue 722 } 723 724 var user v1.UserDetails 725 if issue.User == nil { 726 log.Logger().Warnf("Failed to find user for issue %s repository %s", result, tracker.HomeURL()) 727 } else { 728 u, err := resolver.Resolve(issue.User) 729 if err != nil { 730 log.Logger().Warnf("Failed to resolve user %v for issue %s repository %s", issue.User, result, tracker.HomeURL()) 731 } else if u != nil { 732 user = u.Spec 733 } 734 } 735 736 var closedBy v1.UserDetails 737 if issue.ClosedBy == nil { 738 log.Logger().Warnf("Failed to find closedBy user for issue %s repository %s", result, tracker.HomeURL()) 739 } else { 740 u, err := resolver.Resolve(issue.User) 741 if err != nil { 742 log.Logger().Warnf("Failed to resolve closedBy user %v for issue %s repository %s", issue.User, result, tracker.HomeURL()) 743 } else if u != nil { 744 closedBy = u.Spec 745 } 746 } 747 748 var assignees []v1.UserDetails 749 if issue.Assignees == nil { 750 log.Logger().Warnf("Failed to find assignees for issue %s repository %s", result, tracker.HomeURL()) 751 } else { 752 u, err := resolver.GitUserSliceAsUserDetailsSlice(issue.Assignees) 753 if err != nil { 754 log.Logger().Warnf("Failed to resolve Assignees %v for issue %s repository %s", issue.Assignees, result, tracker.HomeURL()) 755 } 756 assignees = u 757 } 758 759 labels := toV1Labels(issue.Labels) 760 commit.IssueIDs = append(commit.IssueIDs, result) 761 issueSummary := v1.IssueSummary{ 762 ID: result, 763 URL: issue.URL, 764 Title: issue.Title, 765 Body: issue.Body, 766 User: &user, 767 CreationTimestamp: kube.ToMetaTime(issue.CreatedAt), 768 ClosedBy: &closedBy, 769 Assignees: assignees, 770 Labels: labels, 771 } 772 state := issue.State 773 if state != nil { 774 issueSummary.State = *state 775 } 776 if issue.IsPullRequest { 777 spec.PullRequests = append(spec.PullRequests, issueSummary) 778 } else { 779 spec.Issues = append(spec.Issues, issueSummary) 780 } 781 } 782 } 783 } 784 return nil 785 } 786 787 // toV1Labels converts git labels to IssueLabel 788 func toV1Labels(labels []gits.GitLabel) []v1.IssueLabel { 789 answer := []v1.IssueLabel{} 790 for _, label := range labels { 791 answer = append(answer, v1.IssueLabel{ 792 URL: label.URL, 793 Name: label.Name, 794 Color: label.Color, 795 }) 796 } 797 return answer 798 } 799 800 // fullCommitMessageText returns the commit message 801 func fullCommitMessageText(commit *object.Commit) string { 802 answer := commit.Message 803 fn := func(parent *object.Commit) error { 804 text := parent.Message 805 if text != "" { 806 sep := "\n" 807 if strings.HasSuffix(answer, "\n") { 808 sep = "" 809 } 810 answer += sep + text 811 } 812 return nil 813 } 814 fn(commit) //nolint:errcheck 815 return answer 816 817 } 818 819 func (o *StepChangelogOptions) getTemplateResult(releaseSpec *v1.ReleaseSpec, templateName string, templateText string, templateFile string) (string, error) { 820 if templateText == "" { 821 if templateFile == "" { 822 return "", nil 823 } 824 data, err := ioutil.ReadFile(templateFile) 825 if err != nil { 826 return "", err 827 } 828 templateText = string(data) 829 } 830 if templateText == "" { 831 return "", nil 832 } 833 tmpl, err := template.New(templateName).Parse(templateText) 834 if err != nil { 835 return "", err 836 } 837 var buffer bytes.Buffer 838 writer := bufio.NewWriter(&buffer) 839 err = tmpl.Execute(writer, releaseSpec) 840 writer.Flush() 841 return buffer.String(), err 842 } 843 844 //CollapseDependencyUpdates takes a raw set of dependencyUpdates, removes duplicates and collapses multiple updates to 845 // the same org/repo:components into a sungle update 846 func CollapseDependencyUpdates(dependencyUpdates []v1.DependencyUpdate) []v1.DependencyUpdate { 847 // Sort the dependency updates. This makes the outputs more readable, and it also allows us to more easily do duplicate removal and collapsing 848 849 sort.Slice(dependencyUpdates, func(i, j int) bool { 850 if dependencyUpdates[i].Owner == dependencyUpdates[j].Owner { 851 if dependencyUpdates[i].Repo == dependencyUpdates[j].Repo { 852 if dependencyUpdates[i].Component == dependencyUpdates[j].Component { 853 if dependencyUpdates[i].FromVersion == dependencyUpdates[j].FromVersion { 854 return dependencyUpdates[i].ToVersion < dependencyUpdates[j].ToVersion 855 } 856 return dependencyUpdates[i].FromVersion < dependencyUpdates[j].FromVersion 857 } 858 return dependencyUpdates[i].Component < dependencyUpdates[j].Component 859 } 860 return dependencyUpdates[i].Repo < dependencyUpdates[j].Repo 861 } 862 return dependencyUpdates[i].Owner < dependencyUpdates[j].Owner 863 }) 864 865 // Collapse entries 866 collapsed := make([]v1.DependencyUpdate, 0) 867 868 if len(dependencyUpdates) > 0 { 869 start := 0 870 for i := 1; i <= len(dependencyUpdates); i++ { 871 if i == len(dependencyUpdates) || dependencyUpdates[i-1].Owner != dependencyUpdates[i].Owner || dependencyUpdates[i-1].Repo != dependencyUpdates[i].Repo || dependencyUpdates[i-1].Component != dependencyUpdates[i].Component { 872 end := i - 1 873 collapsed = append(collapsed, v1.DependencyUpdate{ 874 DependencyUpdateDetails: v1.DependencyUpdateDetails{ 875 Owner: dependencyUpdates[start].Owner, 876 Repo: dependencyUpdates[start].Repo, 877 Component: dependencyUpdates[start].Component, 878 URL: dependencyUpdates[start].URL, 879 Host: dependencyUpdates[start].Host, 880 FromVersion: dependencyUpdates[start].FromVersion, 881 FromReleaseHTMLURL: dependencyUpdates[start].FromReleaseHTMLURL, 882 FromReleaseName: dependencyUpdates[start].FromReleaseName, 883 ToVersion: dependencyUpdates[end].ToVersion, 884 ToReleaseName: dependencyUpdates[end].ToReleaseName, 885 ToReleaseHTMLURL: dependencyUpdates[end].ToReleaseHTMLURL, 886 }, 887 }) 888 start = i 889 } 890 } 891 } 892 return collapsed 893 }