github.com/xgoffin/jenkins-library@v1.154.0/cmd/githubPublishRelease.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"mime"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/SAP/jenkins-library/pkg/log"
    12  	"github.com/SAP/jenkins-library/pkg/telemetry"
    13  	"github.com/google/go-github/v32/github"
    14  	"github.com/pkg/errors"
    15  
    16  	piperGithub "github.com/SAP/jenkins-library/pkg/github"
    17  )
    18  
    19  type githubRepoClient interface {
    20  	CreateRelease(ctx context.Context, owner string, repo string, release *github.RepositoryRelease) (*github.RepositoryRelease, *github.Response, error)
    21  	DeleteReleaseAsset(ctx context.Context, owner string, repo string, id int64) (*github.Response, error)
    22  	GetLatestRelease(ctx context.Context, owner string, repo string) (*github.RepositoryRelease, *github.Response, error)
    23  	ListReleaseAssets(ctx context.Context, owner string, repo string, id int64, opt *github.ListOptions) ([]*github.ReleaseAsset, *github.Response, error)
    24  	UploadReleaseAsset(ctx context.Context, owner string, repo string, id int64, opt *github.UploadOptions, file *os.File) (*github.ReleaseAsset, *github.Response, error)
    25  }
    26  
    27  type githubIssueClient interface {
    28  	ListByRepo(ctx context.Context, owner string, repo string, opt *github.IssueListByRepoOptions) ([]*github.Issue, *github.Response, error)
    29  }
    30  
    31  func githubPublishRelease(config githubPublishReleaseOptions, telemetryData *telemetry.CustomData) {
    32  	//TODO provide parameter for trusted certs
    33  	ctx, client, err := piperGithub.NewClient(config.Token, config.APIURL, config.UploadURL, []string{})
    34  	if err != nil {
    35  		log.Entry().WithError(err).Fatal("Failed to get GitHub client.")
    36  	}
    37  
    38  	err = runGithubPublishRelease(ctx, &config, client.Repositories, client.Issues)
    39  	if err != nil {
    40  		log.Entry().WithError(err).Fatal("Failed to publish GitHub release.")
    41  	}
    42  }
    43  
    44  func runGithubPublishRelease(ctx context.Context, config *githubPublishReleaseOptions, ghRepoClient githubRepoClient, ghIssueClient githubIssueClient) error {
    45  
    46  	var publishedAt github.Timestamp
    47  
    48  	lastRelease, resp, err := ghRepoClient.GetLatestRelease(ctx, config.Owner, config.Repository)
    49  	if err != nil {
    50  		if resp != nil && resp.StatusCode == 404 {
    51  			//no previous release found -> first release
    52  			config.AddDeltaToLastRelease = false
    53  			log.Entry().Debug("This is the first release.")
    54  		} else {
    55  			return errors.Wrapf(err, "Error occurred when retrieving latest GitHub release (%v/%v)", config.Owner, config.Repository)
    56  		}
    57  	}
    58  	publishedAt = lastRelease.GetPublishedAt()
    59  	log.Entry().Debugf("Previous GitHub release published: '%v'", publishedAt)
    60  
    61  	//updating assets only supported on latest release
    62  	if len(config.AssetPath) > 0 && config.Version == "latest" {
    63  		return uploadReleaseAsset(ctx, lastRelease.GetID(), config, ghRepoClient)
    64  	}
    65  
    66  	releaseBody := ""
    67  
    68  	if len(config.ReleaseBodyHeader) > 0 {
    69  		releaseBody += config.ReleaseBodyHeader + "\n"
    70  	}
    71  
    72  	if config.AddClosedIssues {
    73  		releaseBody += getClosedIssuesText(ctx, publishedAt, config, ghIssueClient)
    74  	}
    75  
    76  	if config.AddDeltaToLastRelease {
    77  		releaseBody += getReleaseDeltaText(config, lastRelease)
    78  	}
    79  
    80  	prefixedTagName := config.TagPrefix + config.Version
    81  
    82  	release := github.RepositoryRelease{
    83  		TagName:         &prefixedTagName,
    84  		TargetCommitish: &config.Commitish,
    85  		Name:            &config.Version,
    86  		Body:            &releaseBody,
    87  		Prerelease:      &config.PreRelease,
    88  	}
    89  
    90  	createdRelease, _, err := ghRepoClient.CreateRelease(ctx, config.Owner, config.Repository, &release)
    91  	if err != nil {
    92  		return errors.Wrapf(err, "Creation of release '%v' failed", *release.TagName)
    93  	}
    94  	log.Entry().Infof("Release %v created on %v/%v", *createdRelease.TagName, config.Owner, config.Repository)
    95  
    96  	if len(config.AssetPath) > 0 {
    97  		return uploadReleaseAsset(ctx, createdRelease.GetID(), config, ghRepoClient)
    98  	}
    99  
   100  	return nil
   101  }
   102  
   103  func getClosedIssuesText(ctx context.Context, publishedAt github.Timestamp, config *githubPublishReleaseOptions, ghIssueClient githubIssueClient) string {
   104  	closedIssuesText := ""
   105  
   106  	options := github.IssueListByRepoOptions{
   107  		State:     "closed",
   108  		Direction: "asc",
   109  		Since:     publishedAt.Time,
   110  	}
   111  	if len(config.Labels) > 0 {
   112  		options.Labels = config.Labels
   113  	}
   114  	ghIssues, _, err := ghIssueClient.ListByRepo(ctx, config.Owner, config.Repository, &options)
   115  	if err != nil {
   116  		log.Entry().WithError(err).Error("Failed to get GitHub issues.")
   117  	}
   118  
   119  	prTexts := []string{"**List of closed pull-requests since last release**"}
   120  	issueTexts := []string{"**List of closed issues since last release**"}
   121  
   122  	for _, issue := range ghIssues {
   123  		if issue.IsPullRequest() && !isExcluded(issue, config.ExcludeLabels) {
   124  			prTexts = append(prTexts, fmt.Sprintf("[#%v](%v): %v", issue.GetNumber(), issue.GetHTMLURL(), issue.GetTitle()))
   125  			log.Entry().Debugf("Added PR #%v to release", issue.GetNumber())
   126  		} else if !issue.IsPullRequest() && !isExcluded(issue, config.ExcludeLabels) {
   127  			issueTexts = append(issueTexts, fmt.Sprintf("[#%v](%v): %v", issue.GetNumber(), issue.GetHTMLURL(), issue.GetTitle()))
   128  			log.Entry().Debugf("Added Issue #%v to release", issue.GetNumber())
   129  		}
   130  	}
   131  
   132  	if len(prTexts) > 1 {
   133  		closedIssuesText += "\n" + strings.Join(prTexts, "\n") + "\n"
   134  	}
   135  
   136  	if len(issueTexts) > 1 {
   137  		closedIssuesText += "\n" + strings.Join(issueTexts, "\n") + "\n"
   138  	}
   139  	return closedIssuesText
   140  }
   141  
   142  func getReleaseDeltaText(config *githubPublishReleaseOptions, lastRelease *github.RepositoryRelease) string {
   143  	releaseDeltaText := ""
   144  
   145  	//add delta link to previous release
   146  	releaseDeltaText += "\n**Changes**\n"
   147  	releaseDeltaText += fmt.Sprintf(
   148  		"[%v...%v](%v/%v/%v/compare/%v...%v)\n",
   149  		lastRelease.GetTagName(),
   150  		config.Version,
   151  		config.ServerURL,
   152  		config.Owner,
   153  		config.Repository,
   154  		lastRelease.GetTagName(), config.Version,
   155  	)
   156  
   157  	return releaseDeltaText
   158  }
   159  
   160  func uploadReleaseAsset(ctx context.Context, releaseID int64, config *githubPublishReleaseOptions, ghRepoClient githubRepoClient) error {
   161  
   162  	assets, _, err := ghRepoClient.ListReleaseAssets(ctx, config.Owner, config.Repository, releaseID, &github.ListOptions{})
   163  	if err != nil {
   164  		return errors.Wrap(err, "Failed to get list of release assets.")
   165  	}
   166  	var assetID int64
   167  	for _, a := range assets {
   168  		if a.GetName() == filepath.Base(config.AssetPath) {
   169  			assetID = a.GetID()
   170  			break
   171  		}
   172  	}
   173  	if assetID != 0 {
   174  		//asset needs to be deleted first since API does not allow for replacement
   175  		_, err := ghRepoClient.DeleteReleaseAsset(ctx, config.Owner, config.Repository, assetID)
   176  		if err != nil {
   177  			return errors.Wrap(err, "Failed to delete release asset.")
   178  		}
   179  	}
   180  
   181  	mediaType := mime.TypeByExtension(filepath.Ext(config.AssetPath))
   182  	if mediaType == "" {
   183  		mediaType = "application/octet-stream"
   184  	}
   185  	log.Entry().Debugf("Using mediaType '%v'", mediaType)
   186  
   187  	name := filepath.Base(config.AssetPath)
   188  	log.Entry().Debugf("Using file name '%v'", name)
   189  
   190  	opts := github.UploadOptions{
   191  		Name:      name,
   192  		MediaType: mediaType,
   193  	}
   194  	file, err := os.Open(config.AssetPath)
   195  	defer file.Close()
   196  	if err != nil {
   197  		return errors.Wrapf(err, "Failed to load release asset '%v'", config.AssetPath)
   198  	}
   199  
   200  	log.Entry().Info("Starting to upload release asset.")
   201  	asset, _, err := ghRepoClient.UploadReleaseAsset(ctx, config.Owner, config.Repository, releaseID, &opts, file)
   202  	if err != nil {
   203  		return errors.Wrap(err, "Failed to upload release asset.")
   204  	}
   205  	log.Entry().Infof("Done uploading asset '%v'.", asset.GetURL())
   206  
   207  	return nil
   208  }
   209  
   210  func isExcluded(issue *github.Issue, excludeLabels []string) bool {
   211  	//issue.Labels[0].GetName()
   212  	for _, ex := range excludeLabels {
   213  		for _, l := range issue.Labels {
   214  			if ex == l.GetName() {
   215  				return true
   216  			}
   217  		}
   218  	}
   219  	return false
   220  }