2020-04-03 16:34:40 +02:00
package cmd
import (
"bytes"
"fmt"
"os"
"strings"
"text/template"
"time"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/versioning"
"github.com/pkg/errors"
"github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
)
type gitRepository interface {
2020-05-07 11:21:29 +02:00
CommitObject ( plumbing . Hash ) ( * object . Commit , error )
2020-04-03 16:34:40 +02:00
CreateTag ( string , plumbing . Hash , * git . CreateTagOptions ) ( * plumbing . Reference , error )
CreateRemote ( * gitConfig . RemoteConfig ) ( * git . Remote , error )
DeleteRemote ( string ) error
Push ( * git . PushOptions ) error
Remote ( string ) ( * git . Remote , error )
ResolveRevision ( plumbing . Revision ) ( * plumbing . Hash , error )
Worktree ( ) ( * git . Worktree , error )
}
type gitWorktree interface {
Checkout ( * git . CheckoutOptions ) error
Commit ( string , * git . CommitOptions ) ( plumbing . Hash , error )
}
func getGitWorktree ( repository gitRepository ) ( gitWorktree , error ) {
return repository . Worktree ( )
}
func artifactPrepareVersion ( config artifactPrepareVersionOptions , telemetryData * telemetry . CustomData , commonPipelineEnvironment * artifactPrepareVersionCommonPipelineEnvironment ) {
c := command . Command { }
// reroute command output to logging framework
2020-05-06 13:35:40 +02:00
c . Stdout ( log . Writer ( ) )
c . Stderr ( log . Writer ( ) )
2020-04-03 16:34:40 +02:00
// open local .git repository
repository , err := openGit ( )
if err != nil {
log . Entry ( ) . WithError ( err ) . Fatal ( "git repository required - none available" )
}
err = runArtifactPrepareVersion ( & config , telemetryData , commonPipelineEnvironment , nil , & c , repository , getGitWorktree )
if err != nil {
log . Entry ( ) . WithError ( err ) . Fatal ( "artifactPrepareVersion failed" )
}
}
var sshAgentAuth = ssh . NewSSHAgentAuth
2020-07-01 11:28:16 +02:00
func runArtifactPrepareVersion ( config * artifactPrepareVersionOptions , telemetryData * telemetry . CustomData , commonPipelineEnvironment * artifactPrepareVersionCommonPipelineEnvironment , artifact versioning . Artifact , runner command . ExecRunner , repository gitRepository , getWorktree func ( gitRepository ) ( gitWorktree , error ) ) error {
2020-04-03 16:34:40 +02:00
telemetryData . Custom1Label = "buildTool"
telemetryData . Custom1 = config . BuildTool
2020-04-15 13:12:43 +02:00
telemetryData . Custom2Label = "filePath"
telemetryData . Custom2 = config . FilePath
2020-04-03 16:34:40 +02:00
// Options for artifact
artifactOpts := versioning . Options {
GlobalSettingsFile : config . GlobalSettingsFile ,
M2Path : config . M2Path ,
ProjectSettingsFile : config . ProjectSettingsFile ,
2020-04-24 20:52:16 +02:00
VersionField : config . CustomVersionField ,
2020-04-15 13:12:43 +02:00
VersionSection : config . CustomVersionSection ,
2020-04-24 20:52:16 +02:00
VersioningScheme : config . CustomVersioningScheme ,
2020-05-06 22:07:27 +02:00
VersionSource : config . DockerVersionSource ,
2020-04-30 17:32:18 +02:00
}
2020-04-03 16:34:40 +02:00
var err error
if artifact == nil {
artifact , err = versioning . GetArtifact ( config . BuildTool , config . FilePath , & artifactOpts , runner )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrap ( err , "failed to retrieve artifact" )
}
}
versioningType := config . VersioningType
// support former groovy versioning template and translate into new options
if len ( config . VersioningTemplate ) > 0 {
versioningType , _ , config . IncludeCommitID = templateCompatibility ( config . VersioningTemplate )
}
version , err := artifact . GetVersion ( )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrap ( err , "failed to retrieve version" )
}
log . Entry ( ) . Infof ( "Version before automatic versioning: %v" , version )
2020-05-07 11:21:29 +02:00
gitCommit , gitCommitMessage , err := getGitCommitID ( repository )
2020-04-03 16:34:40 +02:00
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return err
}
gitCommitID := gitCommit . String ( )
newVersion := version
2020-04-07 13:27:36 +02:00
if versioningType == "cloud" || versioningType == "cloud_noTag" {
2020-04-03 16:34:40 +02:00
versioningTempl , err := versioningTemplate ( artifact . VersioningScheme ( ) )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrapf ( err , "failed to get versioning template for scheme '%v'" , artifact . VersioningScheme ( ) )
}
now := time . Now ( )
2020-05-06 22:07:27 +02:00
newVersion , err = calculateNewVersion ( versioningTempl , version , gitCommitID , config . IncludeCommitID , config . ShortCommitID , config . UnixTimestamp , now )
2020-04-03 16:34:40 +02:00
if err != nil {
return errors . Wrap ( err , "failed to calculate new version" )
}
worktree , err := getWorktree ( repository )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrap ( err , "failed to retrieve git worktree" )
}
// opening repository does not seem to consider already existing files properly
// behavior in case we do not run initializeWorktree:
// git.Add(".") will add the complete workspace instead of only changed files
err = initializeWorktree ( gitCommit , worktree )
if err != nil {
return err
}
// only update version in build descriptor if required in order to save prossing time (e.g. maven case)
if newVersion != version {
err = artifact . SetVersion ( newVersion )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return errors . Wrap ( err , "failed to write version" )
}
}
//ToDo: what about closure in current Groovy step. Discard the possibility or provide extension mechanism?
2020-04-07 13:27:36 +02:00
if versioningType == "cloud" {
// commit changes and push to repository (including new version tag)
gitCommitID , err = pushChanges ( config , newVersion , repository , worktree , now )
if err != nil {
return errors . Wrapf ( err , "failed to push changes for version '%v'" , newVersion )
}
2020-04-03 16:34:40 +02:00
}
}
log . Entry ( ) . Infof ( "New version: '%v'" , newVersion )
commonPipelineEnvironment . git . commitID = gitCommitID
commonPipelineEnvironment . artifactVersion = newVersion
2020-05-28 10:05:22 +02:00
commonPipelineEnvironment . originalArtifactVersion = version
2020-05-07 11:21:29 +02:00
commonPipelineEnvironment . git . commitMessage = gitCommitMessage
2020-04-03 16:34:40 +02:00
return nil
}
func openGit ( ) ( gitRepository , error ) {
workdir , _ := os . Getwd ( )
return git . PlainOpen ( workdir )
}
2020-05-07 11:21:29 +02:00
func getGitCommitID ( repository gitRepository ) ( plumbing . Hash , string , error ) {
2020-04-03 16:34:40 +02:00
commitID , err := repository . ResolveRevision ( plumbing . Revision ( "HEAD" ) )
if err != nil {
2020-05-07 11:21:29 +02:00
return plumbing . Hash { } , "" , errors . Wrap ( err , "failed to retrieve git commit ID" )
2020-04-03 16:34:40 +02:00
}
2020-05-07 11:21:29 +02:00
// ToDo not too elegant to retrieve the commit message here, must be refactored sooner than later
// but to quickly address https://github.com/SAP/jenkins-library/pull/1515 let's revive this
commitObject , err := repository . CommitObject ( * commitID )
if err != nil {
return * commitID , "" , errors . Wrap ( err , "failed to retrieve git commit message" )
}
return * commitID , commitObject . Message , nil
2020-04-03 16:34:40 +02:00
}
func versioningTemplate ( scheme string ) ( string , error ) {
// generally: timestamp acts as build number providing a proper order
switch scheme {
2020-05-06 22:07:27 +02:00
case "docker" :
// from Docker documentation:
// A tag name must be valid ASCII and may contain lowercase and uppercase letters, digits, underscores, periods and dashes.
// A tag name may not start with a period or a dash and may contain a maximum of 128 characters.
return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}-{{.CommitID}}{{end}}{{end}}" , nil
2020-04-03 16:34:40 +02:00
case "maven" :
// according to https://www.mojohaus.org/versions-maven-plugin/version-rules.html
return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}_{{.CommitID}}{{end}}{{end}}" , nil
case "pep440" :
// according to https://www.python.org/dev/peps/pep-0440/
return "{{.Version}}{{if .Timestamp}}.{{.Timestamp}}{{if .CommitID}}+{{.CommitID}}{{end}}{{end}}" , nil
case "semver2" :
// according to https://semver.org/spec/v2.0.0.html
return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}+{{.CommitID}}{{end}}{{end}}" , nil
}
return "" , fmt . Errorf ( "versioning scheme '%v' not supported" , scheme )
}
2020-05-06 22:07:27 +02:00
func calculateNewVersion ( versioningTemplate , currentVersion , commitID string , includeCommitID , shortCommitID , unixTimestamp bool , t time . Time ) ( string , error ) {
2020-04-03 16:34:40 +02:00
tmpl , err := template . New ( "version" ) . Parse ( versioningTemplate )
if err != nil {
return "" , errors . Wrapf ( err , "failed to create version template: %v" , versioningTemplate )
}
2020-05-06 22:07:27 +02:00
timestamp := t . Format ( "20060102150405" )
if unixTimestamp {
timestamp = fmt . Sprint ( t . Unix ( ) )
}
2020-04-03 16:34:40 +02:00
buf := new ( bytes . Buffer )
versionParts := struct {
Version string
Timestamp string
CommitID string
} {
Version : currentVersion ,
2020-05-06 22:07:27 +02:00
Timestamp : timestamp ,
2020-04-03 16:34:40 +02:00
}
if includeCommitID {
versionParts . CommitID = commitID
2020-05-06 22:07:27 +02:00
if shortCommitID {
versionParts . CommitID = commitID [ 0 : 7 ]
}
2020-04-03 16:34:40 +02:00
}
err = tmpl . Execute ( buf , versionParts )
if err != nil {
return "" , errors . Wrapf ( err , "failed to execute versioning template: %v" , versioningTemplate )
}
newVersion := buf . String ( )
if len ( newVersion ) == 0 {
return "" , fmt . Errorf ( "failed calculate version, new version is '%v'" , newVersion )
}
return buf . String ( ) , nil
}
func initializeWorktree ( gitCommit plumbing . Hash , worktree gitWorktree ) error {
// checkout current revision in order to work on that
err := worktree . Checkout ( & git . CheckoutOptions { Hash : gitCommit , Keep : true } )
if err != nil {
return errors . Wrap ( err , "failed to initialize worktree" )
}
return nil
}
func pushChanges ( config * artifactPrepareVersionOptions , newVersion string , repository gitRepository , worktree gitWorktree , t time . Time ) ( string , error ) {
var commitID string
2020-04-08 09:28:03 +02:00
commit , err := addAndCommit ( config , worktree , newVersion , t )
2020-04-03 16:34:40 +02:00
if err != nil {
return commit . String ( ) , err
}
commitID = commit . String ( )
tag := fmt . Sprintf ( "%v%v" , config . TagPrefix , newVersion )
_ , err = repository . CreateTag ( tag , commit , nil )
if err != nil {
return commitID , err
}
ref := gitConfig . RefSpec ( fmt . Sprintf ( "refs/tags/%v:refs/tags/%v" , tag , tag ) )
pushOptions := git . PushOptions {
RefSpecs : [ ] gitConfig . RefSpec { gitConfig . RefSpec ( ref ) } ,
}
currentRemoteOrigin , err := repository . Remote ( "origin" )
if err != nil {
return commitID , errors . Wrap ( err , "failed to retrieve current remote origin" )
}
var updatedRemoteOrigin * git . Remote
urls := originUrls ( repository )
if len ( urls ) == 0 {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return commitID , fmt . Errorf ( "no remote url maintained" )
}
if strings . HasPrefix ( urls [ 0 ] , "http" ) {
if len ( config . Username ) == 0 || len ( config . Password ) == 0 {
// handling compatibility: try to use ssh in case no credentials are available
log . Entry ( ) . Info ( "git username/password missing - switching to ssh" )
remoteURL := convertHTTPToSSHURL ( urls [ 0 ] )
// update remote origin url to point to ssh url instead of http(s) url
err = repository . DeleteRemote ( "origin" )
if err != nil {
return commitID , errors . Wrap ( err , "failed to update remote origin - remove" )
}
updatedRemoteOrigin , err = repository . CreateRemote ( & gitConfig . RemoteConfig { Name : "origin" , URLs : [ ] string { remoteURL } } )
if err != nil {
return commitID , errors . Wrap ( err , "failed to update remote origin - create" )
}
pushOptions . Auth , err = sshAgentAuth ( "git" )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return commitID , errors . Wrap ( err , "failed to retrieve ssh authentication" )
}
log . Entry ( ) . Infof ( "using remote '%v'" , remoteURL )
} else {
pushOptions . Auth = & http . BasicAuth { Username : config . Username , Password : config . Password }
}
} else {
pushOptions . Auth , err = sshAgentAuth ( "git" )
if err != nil {
2020-07-31 14:55:22 +02:00
log . SetErrorCategory ( log . ErrorConfiguration )
2020-04-03 16:34:40 +02:00
return commitID , errors . Wrap ( err , "failed to retrieve ssh authentication" )
}
}
err = repository . Push ( & pushOptions )
if err != nil {
2020-08-06 11:13:19 +02:00
errText := fmt . Sprint ( err )
switch {
case strings . Contains ( errText , "ssh: handshake failed" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
case strings . Contains ( errText , "Permission" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
2020-08-19 20:20:00 +02:00
case strings . Contains ( errText , "authorization failed" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
case strings . Contains ( errText , "authentication required" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
2020-09-21 14:51:02 +02:00
case strings . Contains ( errText , "knownhosts:" ) :
2020-08-06 11:13:19 +02:00
err = errors . Wrap ( err , "known_hosts file seems invalid" )
log . SetErrorCategory ( log . ErrorConfiguration )
case strings . Contains ( errText , "unable to find any valid known_hosts file" ) :
log . SetErrorCategory ( log . ErrorConfiguration )
case strings . Contains ( errText , "connection timed out" ) :
log . SetErrorCategory ( log . ErrorInfrastructure )
}
2020-04-03 16:34:40 +02:00
return commitID , err
}
if updatedRemoteOrigin != currentRemoteOrigin {
err = repository . DeleteRemote ( "origin" )
if err != nil {
return commitID , errors . Wrap ( err , "failed to restore remote origin - remove" )
}
_ , err := repository . CreateRemote ( currentRemoteOrigin . Config ( ) )
if err != nil {
return commitID , errors . Wrap ( err , "failed to restore remote origin - create" )
}
}
return commitID , nil
}
2020-04-08 09:28:03 +02:00
func addAndCommit ( config * artifactPrepareVersionOptions , worktree gitWorktree , newVersion string , t time . Time ) ( plumbing . Hash , error ) {
2020-04-03 16:34:40 +02:00
//maybe more options are required: https://github.com/go-git/go-git/blob/master/_examples/commit/main.go
2020-05-14 17:20:13 +02:00
commit , err := worktree . Commit ( fmt . Sprintf ( "update version %v" , newVersion ) , & git . CommitOptions { All : true , Author : & object . Signature { Name : config . CommitUserName , When : t } } )
2020-04-03 16:34:40 +02:00
if err != nil {
return commit , errors . Wrap ( err , "failed to commit new version" )
}
return commit , nil
}
func originUrls ( repository gitRepository ) [ ] string {
remote , err := repository . Remote ( "origin" )
if err != nil || remote == nil {
return [ ] string { }
}
return remote . Config ( ) . URLs
}
func convertHTTPToSSHURL ( url string ) string {
sshURL := strings . Replace ( url , "https://" , "git@" , 1 )
return strings . Replace ( sshURL , "/" , ":" , 1 )
}
func templateCompatibility ( groovyTemplate string ) ( versioningType string , useTimestamp bool , useCommitID bool ) {
useTimestamp = strings . Contains ( groovyTemplate , "${timestamp}" )
useCommitID = strings . Contains ( groovyTemplate , "${commitId" )
versioningType = "library"
if useTimestamp {
versioningType = "cloud"
}
return
}