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