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