From 96a95e2dc7e89623a089845b5e108786093d267f Mon Sep 17 00:00:00 2001 From: RaviAnand Mohabir Date: Sat, 22 Jun 2024 16:19:00 +0200 Subject: [PATCH] feat: :sparkles: implement drone-gitea-release plugin with support for tag from latest git tag or version file and notes from settings or file --- README.md | 0 go.mod | 18 +++++ go.sum | 44 +++++++++++++ main.go | 56 ++++++++++++++++ plugin/pipeline.go | 149 ++++++++++++++++++++++++++++++++++++++++++ plugin/plugin.go | 142 ++++++++++++++++++++++++++++++++++++++++ plugin/plugin_test.go | 11 ++++ plugin/util.go | 47 +++++++++++++ 8 files changed, 467 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 plugin/pipeline.go create mode 100644 plugin/plugin.go create mode 100644 plugin/plugin_test.go create mode 100644 plugin/util.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9d01ea2 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module gitea.dikurium.ch/InnoPeak/drone-gitea-release + +go 1.22.3 + +require ( + code.gitea.io/sdk/gitea v0.18.0 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/pflag v1.0.5 +) + +require ( + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/sys v0.19.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..40f7995 --- /dev/null +++ b/go.sum @@ -0,0 +1,44 @@ +code.gitea.io/sdk/gitea v0.18.0 h1:+zZrwVmujIrgobt6wVBWCqITz6bn1aBjnCUHmpZrerI= +code.gitea.io/sdk/gitea v0.18.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a868902 --- /dev/null +++ b/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + + "gitea.dikurium.ch/InnoPeak/drone-gitea-release/plugin" + + "github.com/kelseyhightower/envconfig" + "github.com/sirupsen/logrus" + + flag "github.com/spf13/pflag" +) + +func main() { + logrus.SetFormatter(new(formatter)) + + var args plugin.Args + + if err := envconfig.Process("", &args); err != nil { + logrus.Fatalln(err) + } + + flag.StringVar(&args.TitleFormat, "title-format", "", "Format to use for the release title") + flag.StringVar(&args.TagFile, "tag-file", "", "File to check for tag to release") + flag.StringVar(&args.TagRegex, "tag-regex", "", "Regex pattern to look for tag in tag-file") + flag.StringVar(&args.Notes, "notes", "", "Notes to use in release") + flag.StringVar(&args.NotesFile, "notes-file", "", "File to use for release notes") + + flag.Parse() + + switch args.Level { + case "debug": + logrus.SetFormatter(textFormatter) + logrus.SetLevel(logrus.DebugLevel) + case "trace": + logrus.SetFormatter(textFormatter) + logrus.SetLevel(logrus.TraceLevel) + } + + if err := plugin.Exec(context.Background(), args); err != nil { + logrus.Fatalln(err) + } +} + +// default formatter that writes logs without including timestamp +// or level information. +type formatter struct{} + +func (*formatter) Format(entry *logrus.Entry) ([]byte, error) { + return []byte(entry.Message), nil +} + +// text formatter that writes logs with level information +var textFormatter = &logrus.TextFormatter{ + DisableTimestamp: true, +} diff --git a/plugin/pipeline.go b/plugin/pipeline.go new file mode 100644 index 0000000..af82a23 --- /dev/null +++ b/plugin/pipeline.go @@ -0,0 +1,149 @@ +// Copyright 2020 the Drone Authors. All rights reserved. +// Use of this source code is governed by the Blue Oak Model License +// that can be found in the LICENSE file. + +package plugin + +// Pipeline provides Pipeline metadata from the environment. +type Pipeline struct { + // Build provides build metadata. + Build struct { + Branch string `envconfig:"DRONE_BUILD_BRANCH"` + Number int `envconfig:"DRONE_BUILD_NUMBER"` + Parent int `envconfig:"DRONE_BUILD_PARENT"` + Event string `envconfig:"DRONE_BUILD_EVENT"` + Action string `envconfig:"DRONE_BUILD_ACTION"` + Status string `envconfig:"DRONE_BUILD_STATUS"` + Created int64 `envconfig:"DRONE_BUILD_CREATED"` + Started int64 `envconfig:"DRONE_BUILD_STARTED"` + Finished int64 `envconfig:"DRONE_BUILD_FINISHED"` + Link string `envconfig:"DRONE_BUILD_LINK"` + } + + // Calver provides the calver details parsed from the + // git tag. If the git tag is empty or is not a valid + // calver, the values will be empty. + Calver struct { + Version string `envconfig:"DRONE_CALVER"` + Short string `envconfig:"DRONE_CALVER_SHORT"` + MajorMinor string `envconfig:"DRONE_CALVER_MAJOR_MINOR"` + Major string `envconfig:"DRONE_CALVER_MAJOR"` + Minor string `envconfig:"DRONE_CALVER_MINOR"` + Micro string `envconfig:"DRONE_CALVER_MICRO"` + Modifier string `envconfig:"DRONE_CALVER_MODIFIER"` + } + + // Card provides adaptive card configuration options. + Card struct { + Path string `envconfig:"DRONE_CARD_PATH"` + } + + // Commit provides the commit metadata. + Commit struct { + Rev string `envconfig:"DRONE_COMMIT_SHA"` + Before string `envconfig:"DRONE_COMMIT_BEFORE"` + After string `envconfig:"DRONE_COMMIT_AFTER"` + Ref string `envconfig:"DRONE_COMMIT_REF"` + Branch string `envconfig:"DRONE_COMMIT_BRANCH"` + Source string `envconfig:"DRONE_COMMIT_SOURCE"` + Target string `envconfig:"DRONE_COMMIT_TARGET"` + Link string `envconfig:"DRONE_COMMIT_LINK"` + Message string `envconfig:"DRONE_COMMIT_MESSAGE"` + + Author struct { + Username string `envconfig:"DRONE_COMMIT_AUTHOR"` + Name string `envconfig:"DRONE_COMMIT_AUTHOR_NAME"` + Email string `envconfig:"DRONE_COMMIT_AUTHOR_EMAIL"` + Avatar string `envconfig:"DRONE_COMMIT_AUTHOR_AVATAR"` + } + } + + // Deploy provides the deployment metadata. + Deploy struct { + ID string `envconfig:"DRONE_DEPLOY_TO"` + Target string `envconfig:"DRONE_DEPLOY_ID"` + } + + // Failed provides a list of failed steps and failed stages + // for the current pipeline. + Failed struct { + Steps []string `envconfig:"DRONE_FAILED_STEPS"` + Stages []string `envconfig:"DRONE_FAILED_STAGES"` + } + + // Git provides the git repository metadata. + Git struct { + HTTPURL string `envconfig:"DRONE_GIT_HTTP_URL"` + SSHURL string `envconfig:"DRONE_GIT_SSH_URL"` + } + + // PullRequest provides the pull request metadata. + PullRequest struct { + Number int `envconfig:"DRONE_PULL_REQUEST"` + } + + // Repo provides the repository metadata. + Repo struct { + Branch string `envconfig:"DRONE_REPO_BRANCH"` + Link string `envconfig:"DRONE_REPO_LINK"` + Namespace string `envconfig:"DRONE_REPO_NAMESPACE"` + Name string `envconfig:"DRONE_REPO_NAME"` + Private bool `envconfig:"DRONE_REPO_PRIVATE"` + Remote string `envconfig:"DRONE_GIT_HTTP_URL"` + SCM string `envconfig:"DRONE_REPO_SCM"` + Slug string `envconfig:"DRONE_REPO"` + Visibility string `envconfig:"DRONE_REPO_VISIBILITY"` + } + + // Stage provides the stage metadata. + Stage struct { + Kind string `envconfig:"DRONE_STAGE_KIND"` + Type string `envconfig:"DRONE_STAGE_TYPE"` + Name string `envconfig:"DRONE_STAGE_NAME"` + Number int `envconfig:"DRONE_STAGE_NUMBER"` + Machine string `envconfig:"DRONE_STAGE_MACHINE"` + OS string `envconfig:"DRONE_STAGE_OS"` + Arch string `envconfig:"DRONE_STAGE_ARCH"` + Variant string `envconfig:"DRONE_STAGE_VARIANT"` + Status string `envconfig:"DRONE_STAGE_STATUS"` + Started int64 `envconfig:"DRONE_STAGE_STARTED"` + Finished int64 `envconfig:"DRONE_STAGE_FINISHED"` + DependsOn []string `envconfig:"DRONE_STAGE_DEPENDS_ON"` + } + + // Step provides the step metadata. + Step struct { + Number int `envconfig:"DRONE_STEP_NUMBER"` + Name string `envconfig:"DRONE_STEP_NAME"` + } + + // Semver provides the semver details parsed from the + // git tag. If the git tag is empty or is not a valid + // semver, the values will be empty and the error field + // will be populated with the parsing error. + Semver struct { + Version string `envconfig:"DRONE_SEMVER"` + Short string `envconfig:"DRONE_SEMVER_SHORT"` + Major string `envconfig:"DRONE_SEMVER_MAJOR"` + Minor string `envconfig:"DRONE_SEMVER_MINOR"` + Patch string `envconfig:"DRONE_SEMVER_PATCH"` + Build string `envconfig:"DRONE_SEMVER_BUILD"` + PreRelease string `envconfig:"DRONE_SEMVER_PRERELEASE"` + Error string `envconfig:"DRONE_SEMVER_ERROR"` + } + + // System provides the Drone system metadata, including + // the system version of details required to create the + // drone website address. + System struct { + Proto string `envconfig:"DRONE_SYSTEM_PROTO"` + Host string `envconfig:"DRONE_SYSTEM_HOST"` + Hostname string `envconfig:"DRONE_SYSTEM_HOSTNAME"` + Version string `envconfig:"DRONE_SYSTEM_VERSION"` + } + + // Tag provides the git tag details. + Tag struct { + Name string `envconfig:"DRONE_TAG"` + } +} diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 0000000..bdf73f6 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,142 @@ +package plugin + +import ( + "bytes" + "context" + "fmt" + "os" + "regexp" + "text/template" + + "code.gitea.io/sdk/gitea" +) + +var ( + defaultTitleFormat = "Release {{.Tag}}" + defaultTagRegex = regexp.MustCompile(`((?P[\d]+\.[\d]+\.[\w-]+))`) +) + +// Args provides plugin execution arguments. +type Args struct { + Pipeline + + // Level defines the plugin log level. + Level string `envconfig:"PLUGIN_LOG_LEVEL"` + + GiteaUrl string `envconfig:"PLUGIN_GITEA_URL"` + GiteaUsername string `envconfig:"PLUGIN_GITEA_USERNAME"` + GiteaPassword string `envconfig:"PLUGIN_GITEA_PASSWORD"` + Owner string `envconfig:"PLUGIN_OWNER"` + Repo string `envconfig:"PLUGIN_REPO"` + TitleFormat string `envconfig:"PLUGIN_TITLE_FORMAT"` + UseLatestGitTag bool `envconfig:"PLUGIN_USE_LATEST_GIT_TAG"` + TagFile string `envconfig:"PLUGIN_TAG_FILE"` + TagRegex string `envconfig:"PLUGIN_TAG_REGEX"` + NotesFile string `envconfig:"PLUGIN_NOTES_FILE"` + Notes string `envconfig:"PLUGIN_NOTES"` + IsPrerelease bool `envconfig:"PLUGIN_IS_PRERELEASE"` +} + +type TitleTemplateCtx struct { + Tag string + GiteaUrl string + Owner string + Repo string + IsPrerelease bool +} + +// Exec executes the plugin. +func Exec(ctx context.Context, args Args) error { + var ( + note string + tag string + title string + err error + ) + + if args.Notes != "" { + note = args.Notes + } else if args.NotesFile != "" { + content, err := os.ReadFile(args.NotesFile) + + if err != nil { + return fmt.Errorf("error reading notes file %w", err) + } + + note = string(content) + } else { + return fmt.Errorf("notes or notes file must be specified") + } + + if args.UseLatestGitTag { + tag, err = getLatestGitTag() + + if err != nil { + return fmt.Errorf("error getting git tag %w", err) + } + } else if args.TagFile != "" { + var pattern = defaultTagRegex + if args.TagRegex != "" { + pattern = regexp.MustCompile(args.TagRegex) + } + + content, err := os.ReadFile(args.TagFile) + + if err != nil { + return fmt.Errorf("error reading tag file %w", err) + } + + matches := pattern.FindStringSubmatch(string(content)) + + if len(matches) == 0 { + return fmt.Errorf("no matches found in tag file") + } + + for i, name := range pattern.SubexpNames() { + if name == "version" { + tag = matches[i] + } + } + } else { + return fmt.Errorf("latest git tag or tag file must be given") + } + + var titleTmpl *template.Template + if args.TitleFormat != "" { + titleTmpl, err = template.New("title").Parse(args.TitleFormat) + } else { + titleTmpl, err = template.New("title").Parse(defaultTitleFormat) + } + + if err != nil { + return fmt.Errorf("error reading template %w", err) + } + + var titleBytes []byte + var titleBuffer = bytes.NewBuffer(titleBytes) + + if err = titleTmpl.Execute(titleBuffer, TitleTemplateCtx{Tag: tag, GiteaUrl: args.GiteaUrl, Owner: args.Owner, Repo: args.Repo}); err != nil { + return fmt.Errorf("error reading template %w", err) + } + + title = titleBuffer.String() + + client, err := gitea.NewClient(args.GiteaUrl, gitea.SetBasicAuth(args.GiteaUsername, args.GiteaPassword)) + + if err != nil { + return fmt.Errorf("error creating Gitea client %w", err) + } + + _, _, err = client.CreateRelease(args.Owner, args.Repo, gitea.CreateReleaseOption{ + TagName: tag, + Title: title, + Note: note, + IsPrerelease: args.IsPrerelease, + }) + + if err != nil { + return fmt.Errorf("error creating Gitea release %w", err) + } + + return err +} diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go new file mode 100644 index 0000000..cf5d77a --- /dev/null +++ b/plugin/plugin_test.go @@ -0,0 +1,11 @@ +// Copyright 2020 the Drone Authors. All rights reserved. +// Use of this source code is governed by the Blue Oak Model License +// that can be found in the LICENSE file. + +package plugin + +import "testing" + +func TestPlugin(t *testing.T) { + t.Skip() +} diff --git a/plugin/util.go b/plugin/util.go new file mode 100644 index 0000000..41a48d1 --- /dev/null +++ b/plugin/util.go @@ -0,0 +1,47 @@ +// Copyright 2020 the Drone Authors. All rights reserved. +// Use of this source code is governed by the Blue Oak Model License +// that can be found in the LICENSE file. + +package plugin + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" +) + +func getLatestGitTag() (string, error) { + tag, err := exec.Command("git", "describe", "--tags", "--abbrev=0").Output() + + if err != nil { + return "", fmt.Errorf("error getting git tag %w", err) + } + + return string(tag), err +} + +func writeCard(path, schema string, card interface{}) { + data, _ := json.Marshal(map[string]interface{}{ + "schema": schema, + "data": card, + }) + switch { + case path == "/dev/stdout": + writeCardTo(os.Stdout, data) + case path == "/dev/stderr": + writeCardTo(os.Stderr, data) + case path != "": + os.WriteFile(path, data, 0644) + } +} + +func writeCardTo(out io.Writer, data []byte) { + encoded := base64.StdEncoding.EncodeToString(data) + io.WriteString(out, "\u001B]1338;") + io.WriteString(out, encoded) + io.WriteString(out, "\u001B]0m") + io.WriteString(out, "\n") +}