Skip to content

Commit 999197b

Browse files
authored
Add step to prepare the version for an artifact (#1343)
1 parent f482f43 commit 999197b

19 files changed

+1931
-3
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ targets/
1818
documentation/docs-gen
1919

2020
consumer-test/**/workspace
21+
.pipeline/commonPipelineEnvironment
2122

2223
*.code-workspace
2324
/piper

cmd/artifactPrepareVersion.go

+351
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"text/template"
9+
"time"
10+
11+
"github.com/SAP/jenkins-library/pkg/command"
12+
"github.com/SAP/jenkins-library/pkg/log"
13+
"github.com/SAP/jenkins-library/pkg/telemetry"
14+
"github.com/SAP/jenkins-library/pkg/versioning"
15+
"github.com/pkg/errors"
16+
17+
"github.com/go-git/go-git/v5"
18+
gitConfig "github.com/go-git/go-git/v5/config"
19+
"github.com/go-git/go-git/v5/plumbing"
20+
"github.com/go-git/go-git/v5/plumbing/object"
21+
"github.com/go-git/go-git/v5/plumbing/transport/http"
22+
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
23+
)
24+
25+
type gitRepository interface {
26+
CreateTag(string, plumbing.Hash, *git.CreateTagOptions) (*plumbing.Reference, error)
27+
CreateRemote(*gitConfig.RemoteConfig) (*git.Remote, error)
28+
DeleteRemote(string) error
29+
Push(*git.PushOptions) error
30+
Remote(string) (*git.Remote, error)
31+
ResolveRevision(plumbing.Revision) (*plumbing.Hash, error)
32+
Worktree() (*git.Worktree, error)
33+
}
34+
35+
type gitWorktree interface {
36+
Add(string) (plumbing.Hash, error)
37+
Checkout(*git.CheckoutOptions) error
38+
Commit(string, *git.CommitOptions) (plumbing.Hash, error)
39+
}
40+
41+
func getGitWorktree(repository gitRepository) (gitWorktree, error) {
42+
return repository.Worktree()
43+
}
44+
45+
func artifactPrepareVersion(config artifactPrepareVersionOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *artifactPrepareVersionCommonPipelineEnvironment) {
46+
c := command.Command{}
47+
// reroute command output to logging framework
48+
c.Stdout(log.Entry().Writer())
49+
c.Stderr(log.Entry().Writer())
50+
51+
// open local .git repository
52+
repository, err := openGit()
53+
if err != nil {
54+
log.Entry().WithError(err).Fatal("git repository required - none available")
55+
}
56+
57+
err = runArtifactPrepareVersion(&config, telemetryData, commonPipelineEnvironment, nil, &c, repository, getGitWorktree)
58+
if err != nil {
59+
log.Entry().WithError(err).Fatal("artifactPrepareVersion failed")
60+
}
61+
log.Entry().Info("SUCCESS")
62+
}
63+
64+
var sshAgentAuth = ssh.NewSSHAgentAuth
65+
66+
func runArtifactPrepareVersion(config *artifactPrepareVersionOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *artifactPrepareVersionCommonPipelineEnvironment, artifact versioning.Artifact, runner execRunner, repository gitRepository, getWorktree func(gitRepository) (gitWorktree, error)) error {
67+
68+
telemetryData.Custom1Label = "buildTool"
69+
telemetryData.Custom1 = config.BuildTool
70+
71+
// Options for artifact
72+
artifactOpts := versioning.Options{
73+
GlobalSettingsFile: config.GlobalSettingsFile,
74+
M2Path: config.M2Path,
75+
ProjectSettingsFile: config.ProjectSettingsFile,
76+
}
77+
78+
var err error
79+
if artifact == nil {
80+
artifact, err = versioning.GetArtifact(config.BuildTool, config.FilePath, &artifactOpts, runner)
81+
if err != nil {
82+
return errors.Wrap(err, "failed to retrieve artifact")
83+
}
84+
}
85+
86+
versioningType := config.VersioningType
87+
88+
// support former groovy versioning template and translate into new options
89+
if len(config.VersioningTemplate) > 0 {
90+
versioningType, _, config.IncludeCommitID = templateCompatibility(config.VersioningTemplate)
91+
}
92+
93+
version, err := artifact.GetVersion()
94+
if err != nil {
95+
return errors.Wrap(err, "failed to retrieve version")
96+
}
97+
log.Entry().Infof("Version before automatic versioning: %v", version)
98+
99+
gitCommit, err := getGitCommitID(repository)
100+
if err != nil {
101+
return err
102+
}
103+
gitCommitID := gitCommit.String()
104+
105+
newVersion := version
106+
107+
if versioningType == "cloud" {
108+
versioningTempl, err := versioningTemplate(artifact.VersioningScheme())
109+
if err != nil {
110+
return errors.Wrapf(err, "failed to get versioning template for scheme '%v'", artifact.VersioningScheme())
111+
}
112+
113+
now := time.Now()
114+
115+
newVersion, err = calculateNewVersion(versioningTempl, version, gitCommitID, config.IncludeCommitID, now)
116+
if err != nil {
117+
return errors.Wrap(err, "failed to calculate new version")
118+
}
119+
120+
worktree, err := getWorktree(repository)
121+
if err != nil {
122+
return errors.Wrap(err, "failed to retrieve git worktree")
123+
}
124+
125+
// opening repository does not seem to consider already existing files properly
126+
// behavior in case we do not run initializeWorktree:
127+
// git.Add(".") will add the complete workspace instead of only changed files
128+
err = initializeWorktree(gitCommit, worktree)
129+
if err != nil {
130+
return err
131+
}
132+
133+
// only update version in build descriptor if required in order to save prossing time (e.g. maven case)
134+
if newVersion != version {
135+
err = artifact.SetVersion(newVersion)
136+
if err != nil {
137+
return errors.Wrap(err, "failed to write version")
138+
}
139+
}
140+
141+
//ToDo: what about closure in current Groovy step. Discard the possibility or provide extension mechanism?
142+
143+
// commit changes and push to repository (including new version tag)
144+
gitCommitID, err = pushChanges(config, newVersion, repository, worktree, now)
145+
if err != nil {
146+
return errors.Wrapf(err, "failed to push changes for version '%v'", newVersion)
147+
}
148+
}
149+
150+
log.Entry().Infof("New version: '%v'", newVersion)
151+
152+
commonPipelineEnvironment.git.commitID = gitCommitID
153+
commonPipelineEnvironment.artifactVersion = newVersion
154+
155+
return nil
156+
}
157+
158+
func openGit() (gitRepository, error) {
159+
workdir, _ := os.Getwd()
160+
return git.PlainOpen(workdir)
161+
}
162+
163+
func getGitCommitID(repository gitRepository) (plumbing.Hash, error) {
164+
commitID, err := repository.ResolveRevision(plumbing.Revision("HEAD"))
165+
if err != nil {
166+
return plumbing.Hash{}, errors.Wrap(err, "failed to retrieve git commit ID")
167+
}
168+
return *commitID, nil
169+
}
170+
171+
func versioningTemplate(scheme string) (string, error) {
172+
// generally: timestamp acts as build number providing a proper order
173+
switch scheme {
174+
case "maven":
175+
// according to https://www.mojohaus.org/versions-maven-plugin/version-rules.html
176+
return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}_{{.CommitID}}{{end}}{{end}}", nil
177+
case "pep440":
178+
// according to https://www.python.org/dev/peps/pep-0440/
179+
return "{{.Version}}{{if .Timestamp}}.{{.Timestamp}}{{if .CommitID}}+{{.CommitID}}{{end}}{{end}}", nil
180+
case "semver2":
181+
// according to https://semver.org/spec/v2.0.0.html
182+
return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}+{{.CommitID}}{{end}}{{end}}", nil
183+
}
184+
return "", fmt.Errorf("versioning scheme '%v' not supported", scheme)
185+
}
186+
187+
func calculateNewVersion(versioningTemplate, currentVersion, commitID string, includeCommitID bool, t time.Time) (string, error) {
188+
tmpl, err := template.New("version").Parse(versioningTemplate)
189+
if err != nil {
190+
return "", errors.Wrapf(err, "failed to create version template: %v", versioningTemplate)
191+
}
192+
193+
buf := new(bytes.Buffer)
194+
versionParts := struct {
195+
Version string
196+
Timestamp string
197+
CommitID string
198+
}{
199+
Version: currentVersion,
200+
Timestamp: t.Format("20060102150405"),
201+
}
202+
203+
if includeCommitID {
204+
versionParts.CommitID = commitID
205+
}
206+
207+
err = tmpl.Execute(buf, versionParts)
208+
if err != nil {
209+
return "", errors.Wrapf(err, "failed to execute versioning template: %v", versioningTemplate)
210+
}
211+
212+
newVersion := buf.String()
213+
if len(newVersion) == 0 {
214+
return "", fmt.Errorf("failed calculate version, new version is '%v'", newVersion)
215+
}
216+
return buf.String(), nil
217+
}
218+
219+
func initializeWorktree(gitCommit plumbing.Hash, worktree gitWorktree) error {
220+
// checkout current revision in order to work on that
221+
err := worktree.Checkout(&git.CheckoutOptions{Hash: gitCommit, Keep: true})
222+
if err != nil {
223+
return errors.Wrap(err, "failed to initialize worktree")
224+
}
225+
226+
return nil
227+
}
228+
229+
func pushChanges(config *artifactPrepareVersionOptions, newVersion string, repository gitRepository, worktree gitWorktree, t time.Time) (string, error) {
230+
231+
var commitID string
232+
233+
commit, err := addAndCommit(worktree, newVersion, t)
234+
if err != nil {
235+
return commit.String(), err
236+
}
237+
238+
commitID = commit.String()
239+
240+
tag := fmt.Sprintf("%v%v", config.TagPrefix, newVersion)
241+
_, err = repository.CreateTag(tag, commit, nil)
242+
if err != nil {
243+
return commitID, err
244+
}
245+
246+
ref := gitConfig.RefSpec(fmt.Sprintf("refs/tags/%v:refs/tags/%v", tag, tag))
247+
248+
pushOptions := git.PushOptions{
249+
RefSpecs: []gitConfig.RefSpec{gitConfig.RefSpec(ref)},
250+
}
251+
252+
currentRemoteOrigin, err := repository.Remote("origin")
253+
if err != nil {
254+
return commitID, errors.Wrap(err, "failed to retrieve current remote origin")
255+
}
256+
var updatedRemoteOrigin *git.Remote
257+
258+
urls := originUrls(repository)
259+
if len(urls) == 0 {
260+
return commitID, fmt.Errorf("no remote url maintained")
261+
}
262+
if strings.HasPrefix(urls[0], "http") {
263+
if len(config.Username) == 0 || len(config.Password) == 0 {
264+
// handling compatibility: try to use ssh in case no credentials are available
265+
log.Entry().Info("git username/password missing - switching to ssh")
266+
267+
remoteURL := convertHTTPToSSHURL(urls[0])
268+
269+
// update remote origin url to point to ssh url instead of http(s) url
270+
err = repository.DeleteRemote("origin")
271+
if err != nil {
272+
return commitID, errors.Wrap(err, "failed to update remote origin - remove")
273+
}
274+
updatedRemoteOrigin, err = repository.CreateRemote(&gitConfig.RemoteConfig{Name: "origin", URLs: []string{remoteURL}})
275+
if err != nil {
276+
return commitID, errors.Wrap(err, "failed to update remote origin - create")
277+
}
278+
279+
pushOptions.Auth, err = sshAgentAuth("git")
280+
if err != nil {
281+
return commitID, errors.Wrap(err, "failed to retrieve ssh authentication")
282+
}
283+
log.Entry().Infof("using remote '%v'", remoteURL)
284+
} else {
285+
pushOptions.Auth = &http.BasicAuth{Username: config.Username, Password: config.Password}
286+
}
287+
} else {
288+
pushOptions.Auth, err = sshAgentAuth("git")
289+
if err != nil {
290+
return commitID, errors.Wrap(err, "failed to retrieve ssh authentication")
291+
}
292+
}
293+
294+
err = repository.Push(&pushOptions)
295+
if err != nil {
296+
return commitID, err
297+
}
298+
299+
if updatedRemoteOrigin != currentRemoteOrigin {
300+
err = repository.DeleteRemote("origin")
301+
if err != nil {
302+
return commitID, errors.Wrap(err, "failed to restore remote origin - remove")
303+
}
304+
_, err := repository.CreateRemote(currentRemoteOrigin.Config())
305+
if err != nil {
306+
return commitID, errors.Wrap(err, "failed to restore remote origin - create")
307+
}
308+
}
309+
310+
return commitID, nil
311+
}
312+
313+
func addAndCommit(worktree gitWorktree, newVersion string, t time.Time) (plumbing.Hash, error) {
314+
_, err := worktree.Add(".")
315+
if err != nil {
316+
return plumbing.Hash{}, errors.Wrap(err, "failed to execute 'git add .'")
317+
}
318+
319+
//maybe more options are required: https://github.com/go-git/go-git/blob/master/_examples/commit/main.go
320+
commit, err := worktree.Commit(fmt.Sprintf("update version %v", newVersion), &git.CommitOptions{Author: &object.Signature{Name: "Project Piper", When: t}})
321+
if err != nil {
322+
return commit, errors.Wrap(err, "failed to commit new version")
323+
}
324+
return commit, nil
325+
}
326+
327+
func originUrls(repository gitRepository) []string {
328+
remote, err := repository.Remote("origin")
329+
if err != nil || remote == nil {
330+
return []string{}
331+
}
332+
return remote.Config().URLs
333+
}
334+
335+
func convertHTTPToSSHURL(url string) string {
336+
sshURL := strings.Replace(url, "https://", "git@", 1)
337+
return strings.Replace(sshURL, "/", ":", 1)
338+
}
339+
340+
func templateCompatibility(groovyTemplate string) (versioningType string, useTimestamp bool, useCommitID bool) {
341+
useTimestamp = strings.Contains(groovyTemplate, "${timestamp}")
342+
useCommitID = strings.Contains(groovyTemplate, "${commitId")
343+
344+
versioningType = "library"
345+
346+
if useTimestamp {
347+
versioningType = "cloud"
348+
}
349+
350+
return
351+
}

0 commit comments

Comments
 (0)