├── docs └── .gitkeep ├── .doc-hunt ├── logo ├── icon.png ├── verticalversion.png └── horizontalversion.png ├── .golangci.yaml ├── main.go ├── prompt ├── internal │ ├── builder │ │ ├── store.go │ │ ├── config.go │ │ ├── generic_prompt.go │ │ ├── prompt_env.go │ │ ├── switch_prompt_test.go │ │ ├── switch_prompt.go │ │ ├── prompt_group_env.go │ │ ├── prompt_group_env_test.go │ │ └── prompt_env_test.go │ └── counter │ │ ├── counter_test.go │ │ └── counter.go ├── prompt.go ├── menu.go ├── mandatory.go ├── extractor.go ├── common.go ├── matcher.go └── sender.go ├── cmd ├── exit.go ├── version.go ├── ui.go ├── config.go ├── create.go ├── root_test.go ├── config_test.go ├── root.go ├── setup_test.go └── create_test.go ├── .github ├── dependabot.yml └── workflows │ ├── create-release.yml │ └── build.yml ├── chyle ├── types │ └── changelog.go ├── matchers │ ├── type_test.go │ ├── author.go │ ├── committer.go │ ├── config.go │ ├── message.go │ ├── type.go │ ├── message_test.go │ └── matcher.go ├── extractors │ ├── config.go │ ├── regexp.go │ ├── extractor.go │ └── extractor_test.go ├── errh │ └── error.go ├── senders │ ├── config.go │ ├── sender.go │ ├── stdout.go │ ├── custom_api.go │ ├── stdout_test.go │ ├── fixtures │ │ ├── github-tag-creation-response.json │ │ └── github-release-fetch-response.json │ ├── sender_test.go │ ├── custom_api_test.go │ ├── github_release.go │ └── github_release_test.go ├── config │ ├── primitives_test.go │ ├── custom_api_sender.go │ ├── jira_issue_decorator.go │ ├── github_issue_decorator.go │ ├── stdout_sender.go │ ├── env_decorator.go │ ├── github_release_sender.go │ ├── custom_api_decorator.go │ ├── extractors.go │ ├── shell_decorator.go │ ├── matchers.go │ ├── primitives.go │ ├── config.go │ └── api_decorator.go ├── decorators │ ├── config.go │ ├── env_test.go │ ├── env.go │ ├── json_helper.go │ ├── jira_issue.go │ ├── custom_api.go │ ├── github_issue.go │ ├── shell.go │ ├── shell_test.go │ ├── decorator.go │ ├── github_issue_test.go │ ├── decorator_test.go │ ├── fixtures │ │ └── github-issue-fetch-response.json │ ├── jira_issue_test.go │ └── custom_api_test.go ├── setup_test.go ├── chyle.go ├── tmplh │ ├── template.go │ └── template_test.go ├── convh │ ├── converter_test.go │ └── converter.go ├── process.go ├── apih │ └── http.go ├── process_test.go ├── git │ ├── git.go │ └── git_test.go └── chyle_test.go ├── .goreleaser.yaml ├── setup-tests.sh ├── .gommit.toml ├── features ├── init.sh └── merge-commits.sh ├── .gitignore ├── LICENSE.txt ├── go.mod └── README.md /docs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.doc-hunt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antham/chyle/HEAD/.doc-hunt -------------------------------------------------------------------------------- /logo/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antham/chyle/HEAD/logo/icon.png -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | formatters: 3 | enable: 4 | - gofumpt 5 | -------------------------------------------------------------------------------- /logo/verticalversion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antham/chyle/HEAD/logo/verticalversion.png -------------------------------------------------------------------------------- /logo/horizontalversion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antham/chyle/HEAD/logo/horizontalversion.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/antham/chyle/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /prompt/internal/builder/store.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | // Store is a storage for environment variable defined through prompt 4 | type Store map[string]string 5 | -------------------------------------------------------------------------------- /cmd/exit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | var exitError = func() { 8 | os.Exit(1) 9 | } 10 | 11 | var exitSuccess = func() { 12 | os.Exit(0) 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | pull-request-branch-name: 8 | separator: "-" 9 | open-pull-requests-limit: 10 10 | -------------------------------------------------------------------------------- /chyle/types/changelog.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Changelog represents a changelog entry with datas extracted and metadatas 4 | type Changelog struct { 5 | Datas []map[string]any `json:"datas"` 6 | Metadatas map[string]any `json:"metadatas"` 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create the release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | call-workflow: 13 | uses: antham/go-workflow-github-action/.github/workflows/create-release.yml@master 14 | -------------------------------------------------------------------------------- /chyle/matchers/type_test.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewType(t *testing.T) { 10 | assert.Equal(t, regularCommit{}, newType(regularType)) 11 | assert.Equal(t, mergeCommit{}, newType(mergeType)) 12 | } 13 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - linux 9 | - windows 10 | - darwin 11 | goarch: 12 | - amd64 13 | ldflags: 14 | - -X 'github.com/antham/chyle/cmd.version={{.Version}}' 15 | -------------------------------------------------------------------------------- /prompt/internal/counter/counter_test.go: -------------------------------------------------------------------------------- 1 | package counter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCounter(t *testing.T) { 10 | c := Counter{} 11 | assert.Equal(t, c.Get(), "0") 12 | 13 | c.Increment() 14 | assert.Equal(t, c.Get(), "1") 15 | } 16 | -------------------------------------------------------------------------------- /chyle/extractors/config.go: -------------------------------------------------------------------------------- 1 | package extractors 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | // Config centralizes config needed for each extractor 8 | type Config map[string]struct { 9 | ORIGKEY string 10 | DESTKEY string 11 | REG *regexp.Regexp 12 | } 13 | 14 | // Features gives tell if extractors are enabled 15 | type Features struct { 16 | ENABLED bool 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | - main 10 | pull_request: 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | call-workflow: 16 | uses: antham/go-workflow-github-action/.github/workflows/build.yml@master 17 | secrets: 18 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 19 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var version = "" 8 | 9 | // versionCmd represents the version command 10 | var versionCmd = &cobra.Command{ 11 | Use: "version", 12 | Short: "App version", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | printWithNewLine(version) 15 | }, 16 | } 17 | 18 | func init() { 19 | RootCmd.AddCommand(versionCmd) 20 | } 21 | -------------------------------------------------------------------------------- /cmd/ui.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | func failure(err error) { 11 | c := color.New(color.FgRed) 12 | if _, ferr := c.Fprintf(writer, "%s\n", err.Error()); ferr != nil { 13 | log.Fatal(ferr) 14 | } 15 | } 16 | 17 | func printWithNewLine(str string) { 18 | if _, err := fmt.Fprintf(writer, "%s\n", str); err != nil { 19 | log.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /prompt/internal/builder/config.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | // EnvConfig is common config for all environments variables prompts builder 4 | type EnvConfig struct { 5 | ID string 6 | NextID string 7 | Env string 8 | PromptString string 9 | Validator func(value string) error 10 | DefaultValue string 11 | RunBeforeNextPrompt func(value string, store *Store) 12 | } 13 | -------------------------------------------------------------------------------- /chyle/matchers/author.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/go-git/go-git/v5/plumbing/object" 7 | ) 8 | 9 | // author is commit author matcher 10 | type author struct { 11 | regexp *regexp.Regexp 12 | } 13 | 14 | func (a author) Match(commit *object.Commit) bool { 15 | return a.regexp.MatchString(commit.Author.String()) 16 | } 17 | 18 | func newAuthor(re *regexp.Regexp) Matcher { 19 | return author{re} 20 | } 21 | -------------------------------------------------------------------------------- /setup-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rootPath=$(pwd) 4 | 5 | cd "$rootPath/cmd" && sh "$rootPath/features/init.sh" 6 | cd "$rootPath/cmd" && sh "$rootPath/features/merge-commits.sh" 7 | cd "$rootPath/chyle" && sh "$rootPath/features/init.sh" 8 | cd "$rootPath/chyle" && sh "$rootPath/features/merge-commits.sh" 9 | cd "$rootPath/chyle/git" && sh "$rootPath/features/init.sh" 10 | cd "$rootPath/chyle/git" && sh "$rootPath/features/merge-commits.sh" 11 | -------------------------------------------------------------------------------- /.gommit.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | exclude-merge-commits=true 3 | check-summary-length=true 4 | summary-length=72 5 | 6 | [matchers] 7 | simple=".+? : [a-z0-9].+(?:\n)?" 8 | extended=".+? : [a-z0-9].+?\n(?:\n?.+)+(?:\n)?" 9 | dependabot="Bump.+?\n(?:\n?.+)+(?:\n)?" 10 | 11 | [examples] 12 | a_simple_commit=""" 13 | module : a commit message 14 | """ 15 | an_extended_commit=""" 16 | module : a commit message 17 | 18 | * first line 19 | * second line 20 | * and so on... 21 | """ 22 | -------------------------------------------------------------------------------- /chyle/matchers/committer.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/go-git/go-git/v5/plumbing/object" 7 | ) 8 | 9 | // committer is commit committer matcher 10 | type committer struct { 11 | regexp *regexp.Regexp 12 | } 13 | 14 | func (c committer) Match(commit *object.Commit) bool { 15 | return c.regexp.MatchString(commit.Committer.String()) 16 | } 17 | 18 | func newCommitter(re *regexp.Regexp) Matcher { 19 | return committer{re} 20 | } 21 | -------------------------------------------------------------------------------- /chyle/errh/error.go: -------------------------------------------------------------------------------- 1 | package errh 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type errWrapper struct { 8 | msg string 9 | err error 10 | } 11 | 12 | func (e errWrapper) Error() string { 13 | return fmt.Sprintf("%s : %s", e.msg, e.err) 14 | } 15 | 16 | // AddCustomMessageToError appends a string message to an error 17 | // by creating a brand new error 18 | func AddCustomMessageToError(msg string, err error) error { 19 | if err == nil { 20 | return nil 21 | } 22 | 23 | return errWrapper{msg, err} 24 | } 25 | -------------------------------------------------------------------------------- /chyle/senders/config.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | // codebeat:disable[TOO_MANY_IVARS] 4 | 5 | // Config centralizes config needed for each sender 6 | type Config struct { 7 | STDOUT stdoutConfig 8 | GITHUBRELEASE githubReleaseConfig 9 | CUSTOMAPI customAPIConfig 10 | } 11 | 12 | // Features gives which senders are enabled 13 | type Features struct { 14 | ENABLED bool 15 | GITHUBRELEASE bool 16 | STDOUT bool 17 | CUSTOMAPI bool 18 | } 19 | 20 | // codebeat:enable[TOO_MANY_IVARS] 21 | -------------------------------------------------------------------------------- /features/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gitRepositoryPath=testing-repository 4 | 5 | if [ ! -z "${TRAVIS+x}" ]; 6 | then 7 | git config --global user.name "whatever"; 8 | git config --global user.email "whatever@example.com"; 9 | fi 10 | 11 | # Configure name 12 | 13 | # Init 14 | rm -rf $gitRepositoryPath > /dev/null; 15 | git init --quiet $gitRepositoryPath; 16 | 17 | cd $gitRepositoryPath || exit 1; 18 | 19 | git config --local user.name "whatever"; 20 | git config --local user.email "whatever@example.com"; 21 | -------------------------------------------------------------------------------- /prompt/internal/counter/counter.go: -------------------------------------------------------------------------------- 1 | // Package counter provides a way to create a singleton to increment and store a counter 2 | package counter 3 | 4 | import ( 5 | "strconv" 6 | ) 7 | 8 | // Counter provides a way to generate and store a incremented id 9 | type Counter struct { 10 | counter int 11 | } 12 | 13 | // Get returns current counter value 14 | func (c *Counter) Get() string { 15 | return strconv.Itoa(c.counter) 16 | } 17 | 18 | // Increment adds one to actual counter value 19 | func (c *Counter) Increment() { 20 | c.counter++ 21 | } 22 | -------------------------------------------------------------------------------- /chyle/matchers/config.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | // codebeat:disable[TOO_MANY_IVARS] 8 | 9 | // Config centralizes config needed for each matcher 10 | type Config struct { 11 | MESSAGE *regexp.Regexp 12 | COMMITTER *regexp.Regexp 13 | AUTHOR *regexp.Regexp 14 | TYPE string 15 | } 16 | 17 | // Features gives which matchers are enabled 18 | type Features struct { 19 | ENABLED bool 20 | MESSAGE bool 21 | COMMITTER bool 22 | AUTHOR bool 23 | TYPE bool 24 | } 25 | 26 | // codebeat:enable[TOO_MANY_IVARS] 27 | -------------------------------------------------------------------------------- /chyle/config/primitives_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMissingEnvError(t *testing.T) { 10 | e := MissingEnvError{envs: []string{"TEST"}} 11 | 12 | assert.Equal(t, `environment variable missing : "TEST"`, e.Error()) 13 | 14 | assert.Equal(t, []string{"TEST"}, e.Envs()) 15 | 16 | e = MissingEnvError{envs: []string{"TEST", "TEST1"}} 17 | 18 | assert.Equal(t, `environments variables missing : "TEST", "TEST1"`, e.Error()) 19 | 20 | assert.Equal(t, []string{"TEST", "TEST1"}, e.Envs()) 21 | } 22 | -------------------------------------------------------------------------------- /chyle/decorators/config.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | // codebeat:disable[TOO_MANY_IVARS] 4 | 5 | // Config centralizes config needed for each decorator 6 | type Config struct { 7 | CUSTOMAPI customAPIConfig 8 | GITHUBISSUE githubIssueConfig 9 | JIRAISSUE jiraIssueConfig 10 | ENV envConfig 11 | SHELL shellConfig 12 | } 13 | 14 | // Features gives which decorators are enabled 15 | type Features struct { 16 | ENABLED bool 17 | CUSTOMAPI bool 18 | JIRAISSUE bool 19 | GITHUBISSUE bool 20 | ENV bool 21 | SHELL bool 22 | } 23 | 24 | // codebeat:enable[TOO_MANY_IVARS] 25 | -------------------------------------------------------------------------------- /chyle/decorators/env_test.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestEnvs(t *testing.T) { 11 | err := os.Setenv("TESTENVDECORATOR", "this is a test") 12 | 13 | assert.NoError(t, err) 14 | 15 | envs := map[string]struct { 16 | DESTKEY string 17 | VARNAME string 18 | }{ 19 | "WHATEVER": { 20 | "envTesting", 21 | "TESTENVDECORATOR", 22 | }, 23 | } 24 | 25 | metadatas := map[string]any{} 26 | 27 | e := newEnvs(envs) 28 | m, err := e[0].Decorate(&metadatas) 29 | 30 | assert.NoError(t, err) 31 | assert.Equal(t, map[string]any{"envTesting": "this is a test"}, *m) 32 | } 33 | -------------------------------------------------------------------------------- /chyle/matchers/message.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/go-git/go-git/v5/plumbing/object" 7 | ) 8 | 9 | // message is commit message matcher 10 | type message struct { 11 | regexp *regexp.Regexp 12 | } 13 | 14 | func (m message) Match(commit *object.Commit) bool { 15 | return m.regexp.MatchString(commit.Message) 16 | } 17 | 18 | func newMessage(re *regexp.Regexp) Matcher { 19 | return message{re} 20 | } 21 | 22 | // removePGPKey fix library issue that don't trim PGP key from message 23 | func removePGPKey(message string) string { 24 | return regexp.MustCompile("(?s).*?-----END PGP SIGNATURE-----\n\n").ReplaceAllString(message, "") 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Output of the go coverage tool, specifically when used with LiteIDE 27 | *.out 28 | 29 | # external packages folder 30 | chyle/git/shallow-repository-test/ 31 | /chyle/git/testing-repository/ 32 | /chyle/testing-repository/ 33 | /cmd/testing-repository/ 34 | /testing-repository/ 35 | vendor/ 36 | .idea -------------------------------------------------------------------------------- /chyle/decorators/env.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type envConfig map[string]struct { 8 | DESTKEY string 9 | VARNAME string 10 | } 11 | 12 | // env dumps an environment variable into metadatas 13 | type env struct { 14 | varName string 15 | destKey string 16 | } 17 | 18 | func (e env) Decorate(metadatas *map[string]any) (*map[string]any, error) { 19 | (*metadatas)[e.destKey] = os.Getenv(e.varName) 20 | 21 | return metadatas, nil 22 | } 23 | 24 | func newEnvs(configs envConfig) []Decorater { 25 | results := []Decorater{} 26 | 27 | for _, config := range configs { 28 | results = append(results, env{ 29 | config.VARNAME, 30 | config.DESTKEY, 31 | }) 32 | } 33 | 34 | return results 35 | } 36 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/antham/chyle/prompt" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // configCmd represents the config command 11 | var configCmd = &cobra.Command{ 12 | Use: "config", 13 | Short: "Configuration prompt", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | prompts := prompt.New(reader, writer) 16 | 17 | p := prompts.Run() 18 | 19 | printWithNewLine("") 20 | printWithNewLine("Generated configuration :") 21 | printWithNewLine("") 22 | 23 | for key, value := range (map[string]string)(p) { 24 | printWithNewLine(fmt.Sprintf(`export %s="%s"`, key, value)) 25 | } 26 | }, 27 | } 28 | 29 | func init() { 30 | RootCmd.AddCommand(configCmd) 31 | } 32 | -------------------------------------------------------------------------------- /chyle/setup_test.go: -------------------------------------------------------------------------------- 1 | package chyle 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/antham/envh" 9 | ) 10 | 11 | var envs map[string]string 12 | 13 | func TestMain(m *testing.M) { 14 | saveExistingEnvs() 15 | code := m.Run() 16 | os.Exit(code) 17 | } 18 | 19 | func saveExistingEnvs() { 20 | var err error 21 | env := envh.NewEnv() 22 | 23 | envs, err = env.FindEntries(".*") 24 | if err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | } 29 | 30 | func restoreEnvs() { 31 | os.Clearenv() 32 | 33 | if len(envs) != 0 { 34 | for key, value := range envs { 35 | setenv(key, value) 36 | } 37 | } 38 | } 39 | 40 | func setenv(key string, value string) { 41 | err := os.Setenv(key, value) 42 | if err != nil { 43 | fmt.Println(err) 44 | os.Exit(1) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/antham/chyle/chyle" 7 | ) 8 | 9 | var createCmd = &cobra.Command{ 10 | Use: "create", 11 | Short: "Create a new changelog", 12 | Long: `Create a new changelog according to what is defined as config. 13 | 14 | Changelog creation follows this process : 15 | 16 | 1 - fetch commits 17 | 2 - filter relevant commits 18 | 3 - extract informations from commits fields and publish them to new fields 19 | 4 - enrich extracted datas with external apps 20 | 5 - publish datas`, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | err := chyle.BuildChangelog(envTree) 23 | if err != nil { 24 | failure(err) 25 | 26 | exitError() 27 | } 28 | 29 | exitSuccess() 30 | }, 31 | } 32 | 33 | func init() { 34 | RootCmd.AddCommand(createCmd) 35 | } 36 | -------------------------------------------------------------------------------- /chyle/extractors/regexp.go: -------------------------------------------------------------------------------- 1 | package extractors 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/antham/chyle/chyle/convh" 7 | ) 8 | 9 | // regex uses a regexp to extract data 10 | type regex struct { 11 | index string 12 | identifier string 13 | re *regexp.Regexp 14 | } 15 | 16 | func (r regex) Extract(commitMap *map[string]any) *map[string]any { 17 | var mapValue any 18 | var ok bool 19 | 20 | if mapValue, ok = (*commitMap)[r.index]; !ok { 21 | return commitMap 22 | } 23 | 24 | var value string 25 | 26 | value, ok = mapValue.(string) 27 | 28 | if !ok { 29 | return commitMap 30 | } 31 | 32 | var result string 33 | 34 | results := r.re.FindStringSubmatch(value) 35 | 36 | if len(results) > 1 { 37 | result = results[1] 38 | } 39 | 40 | (*commitMap)[r.identifier] = convh.GuessPrimitiveType(result) 41 | 42 | return commitMap 43 | } 44 | -------------------------------------------------------------------------------- /chyle/chyle.go: -------------------------------------------------------------------------------- 1 | package chyle 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/antham/chyle/chyle/config" 8 | "github.com/antham/chyle/chyle/git" 9 | 10 | "github.com/antham/envh" 11 | ) 12 | 13 | var logger *log.Logger 14 | 15 | func init() { 16 | logger = log.New(os.Stdout, "CHYLE - ", log.Ldate|log.Ltime) 17 | } 18 | 19 | // EnableDebugging activates step logging 20 | var EnableDebugging = false 21 | 22 | // BuildChangelog creates a changelog from defined configuration 23 | func BuildChangelog(envConfig *envh.EnvTree) error { 24 | conf, err := config.Create(envConfig) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if EnableDebugging { 30 | config.Debug(conf, logger) 31 | } 32 | 33 | commits, err := git.FetchCommits(conf.GIT.REPOSITORY.PATH, conf.GIT.REFERENCE.FROM, conf.GIT.REFERENCE.TO) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return proceed(newProcess(conf), commits) 39 | } 40 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestExecute(t *testing.T) { 14 | var code int 15 | var w sync.WaitGroup 16 | 17 | exitError = func() { 18 | panic(1) 19 | } 20 | 21 | exitSuccess = func() { 22 | panic(0) 23 | } 24 | 25 | writer = &bytes.Buffer{} 26 | 27 | w.Add(1) 28 | 29 | go func() { 30 | defer func() { 31 | if r := recover(); r != nil { 32 | code = r.(int) 33 | } 34 | 35 | w.Done() 36 | }() 37 | 38 | os.Args = []string{"", "whatever"} 39 | 40 | Execute() 41 | }() 42 | 43 | w.Wait() 44 | 45 | output, err := io.ReadAll(writer.(*bytes.Buffer)) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | assert.Contains(t, string(output), `unknown command "whatever" for "chyle"`) 51 | assert.EqualValues(t, 1, code, "Must exit with an errors (exit 1)") 52 | } 53 | -------------------------------------------------------------------------------- /prompt/internal/builder/generic_prompt.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | // GenericPrompt defines a generic overridable prompt 4 | type GenericPrompt struct { 5 | PromptID string 6 | PromptStr string 7 | OnSuccess func(string) string 8 | OnError func(error) string 9 | ParseValue func(string) error 10 | } 11 | 12 | // ID return ID of current prompt 13 | func (t *GenericPrompt) ID() string { 14 | return t.PromptID 15 | } 16 | 17 | // PromptString returns string given by prompt 18 | func (t *GenericPrompt) PromptString() string { 19 | return t.PromptStr 20 | } 21 | 22 | // Parse handles prompt value 23 | func (t *GenericPrompt) Parse(value string) error { 24 | return t.ParseValue(value) 25 | } 26 | 27 | // NextOnSuccess returns the next prompt to reach when succeed 28 | func (t *GenericPrompt) NextOnSuccess(value string) string { 29 | return t.OnSuccess(value) 30 | } 31 | 32 | // NextOnError returns an error when something wrong occurred 33 | func (t *GenericPrompt) NextOnError(err error) string { 34 | return t.OnError(err) 35 | } 36 | -------------------------------------------------------------------------------- /chyle/decorators/json_helper.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | 7 | "github.com/tidwall/gjson" 8 | 9 | "github.com/antham/chyle/chyle/apih" 10 | ) 11 | 12 | // jSONResponse extracts datas from a JSON api using defined keys 13 | // and add it to final commitMap data structure 14 | type jSONResponse struct { 15 | client *http.Client 16 | request *http.Request 17 | pairs map[string]struct { 18 | DESTKEY string 19 | FIELD string 20 | } 21 | } 22 | 23 | func (j jSONResponse) Decorate(commitMap *map[string]any) (*map[string]any, error) { 24 | statusCode, body, err := apih.SendRequest(j.client, j.request) 25 | 26 | if statusCode == 404 { 27 | return commitMap, nil 28 | } 29 | 30 | if err != nil { 31 | return commitMap, err 32 | } 33 | 34 | buf := bytes.NewBuffer(body) 35 | 36 | for _, pair := range j.pairs { 37 | if gjson.Get(buf.String(), pair.FIELD).Exists() { 38 | (*commitMap)[pair.DESTKEY] = gjson.Get(buf.String(), pair.FIELD).Value() 39 | } 40 | } 41 | 42 | return commitMap, nil 43 | } 44 | -------------------------------------------------------------------------------- /chyle/matchers/type.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "github.com/go-git/go-git/v5/plumbing/object" 5 | ) 6 | 7 | const ( 8 | regularType = "regular" 9 | mergeType = "merge" 10 | ) 11 | 12 | // mergeCommit match merge commit message 13 | type mergeCommit struct{} 14 | 15 | func (m mergeCommit) Match(commit *object.Commit) bool { 16 | return commit.NumParents() == 2 17 | } 18 | 19 | // regularCommit match regular commit message 20 | type regularCommit struct{} 21 | 22 | func (r regularCommit) Match(commit *object.Commit) bool { 23 | return commit.NumParents() == 1 || commit.NumParents() == 0 24 | } 25 | 26 | func newType(key string) Matcher { 27 | if key == regularType { 28 | return regularCommit{} 29 | } 30 | 31 | return mergeCommit{} 32 | } 33 | 34 | func solveType(commit *object.Commit) string { 35 | if commit.NumParents() == 2 { 36 | return mergeType 37 | } 38 | 39 | return regularType 40 | } 41 | 42 | // GetTypes returns all defined matchers types 43 | func GetTypes() []string { 44 | return []string{regularType, mergeType} 45 | } 46 | -------------------------------------------------------------------------------- /chyle/senders/sender.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "github.com/antham/chyle/chyle/types" 5 | ) 6 | 7 | // Sender defines where the changelog produced must be sent 8 | type Sender interface { 9 | Send(changelog *types.Changelog) error 10 | } 11 | 12 | // Send forwards changelog to senders 13 | func Send(senders *[]Sender, changelog *types.Changelog) error { 14 | for _, sender := range *senders { 15 | err := sender.Send(changelog) 16 | if err != nil { 17 | return err 18 | } 19 | } 20 | 21 | return nil 22 | } 23 | 24 | // Create builds senders from a config 25 | func Create(features Features, senders Config) *[]Sender { 26 | results := []Sender{} 27 | 28 | if !features.ENABLED { 29 | return &results 30 | } 31 | 32 | if features.GITHUBRELEASE { 33 | results = append(results, newGithubRelease(senders.GITHUBRELEASE)) 34 | } 35 | 36 | if features.CUSTOMAPI { 37 | results = append(results, newCustomAPI(senders.CUSTOMAPI)) 38 | } 39 | 40 | if features.STDOUT { 41 | results = append(results, newStdout(senders.STDOUT)) 42 | } 43 | 44 | return &results 45 | } 46 | -------------------------------------------------------------------------------- /cmd/config_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestConfig(t *testing.T) { 14 | var code int 15 | var wg sync.WaitGroup 16 | 17 | exitError = func() { 18 | panic(1) 19 | } 20 | 21 | exitSuccess = func() { 22 | panic(0) 23 | } 24 | 25 | wg.Add(1) 26 | 27 | reader = bytes.NewBufferString("test\ntest\ntest\nq\n") 28 | writer = &bytes.Buffer{} 29 | 30 | go func() { 31 | defer func() { 32 | if r := recover(); r != nil { 33 | code = r.(int) 34 | } 35 | 36 | wg.Done() 37 | }() 38 | 39 | os.Args = []string{"", "config"} 40 | 41 | Execute() 42 | }() 43 | 44 | wg.Wait() 45 | 46 | promptRecord, err := io.ReadAll(writer.(*bytes.Buffer)) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | assert.EqualValues(t, 0, code, "Must exit with no errors (exit 0)") 52 | assert.Contains(t, string(promptRecord), "Enter a git commit ID that start your range") 53 | assert.Contains(t, string(promptRecord), `CHYLE_GIT_REFERENCE_TO="test"`) 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 antham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /prompt/prompt.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/antham/strumt/v2" 7 | 8 | "github.com/antham/chyle/prompt/internal/builder" 9 | ) 10 | 11 | // Prompts held prompts 12 | type Prompts struct { 13 | prompts strumt.Prompts 14 | } 15 | 16 | // New creates a new prompt chain 17 | func New(reader io.Reader, writer io.Writer) Prompts { 18 | return Prompts{strumt.NewPromptsFromReaderAndWriter(reader, writer)} 19 | } 20 | 21 | func (p *Prompts) populatePrompts(prompts []strumt.Prompter) { 22 | for _, item := range prompts { 23 | switch prompt := item.(type) { 24 | case strumt.LinePrompter: 25 | p.prompts.AddLinePrompter(prompt) 26 | } 27 | } 28 | } 29 | 30 | // Run starts a prompt session 31 | func (p *Prompts) Run() builder.Store { 32 | store := &builder.Store{} 33 | prompts := mergePrompters( 34 | newMainMenu(), 35 | newMandatoryOption(store), 36 | newMatchers(store), 37 | newExtractors(store), 38 | newDecorators(store), 39 | newSenders(store), 40 | ) 41 | 42 | p.populatePrompts(prompts) 43 | 44 | p.prompts.SetFirst("referenceFrom") 45 | p.prompts.Run() 46 | 47 | return *store 48 | } 49 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/antham/chyle/chyle" 10 | "github.com/antham/envh" 11 | ) 12 | 13 | var ( 14 | envTree *envh.EnvTree 15 | debug bool 16 | ) 17 | 18 | var ( 19 | writer io.Writer 20 | reader io.Reader 21 | ) 22 | 23 | // RootCmd represents initial cobra command 24 | var RootCmd = &cobra.Command{ 25 | Use: "chyle", 26 | Short: "Create a changelog from your commit history", 27 | } 28 | 29 | // Execute adds all child commands to the root command sets flags appropriately. 30 | // This is called by main.main(). It only needs to happen once to the rootCmd. 31 | func Execute() { 32 | if err := RootCmd.Execute(); err != nil { 33 | failure(err) 34 | exitError() 35 | } 36 | } 37 | 38 | func init() { 39 | reader = os.Stdin 40 | writer = os.Stdout 41 | 42 | cobra.OnInitialize(initConfig) 43 | 44 | RootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debugging") 45 | } 46 | 47 | func initConfig() { 48 | e, err := envh.NewEnvTree("CHYLE", "_") 49 | if err != nil { 50 | failure(err) 51 | exitError() 52 | } 53 | 54 | envTree = &e 55 | 56 | chyle.EnableDebugging = debug 57 | } 58 | -------------------------------------------------------------------------------- /cmd/setup_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/antham/envh" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var gitRepositoryPath = "testing-repository" 14 | 15 | var envs map[string]string 16 | 17 | func TestMain(m *testing.M) { 18 | saveExistingEnvs() 19 | code := m.Run() 20 | os.Exit(code) 21 | } 22 | 23 | func saveExistingEnvs() { 24 | var err error 25 | env := envh.NewEnv() 26 | 27 | envs, err = env.FindEntries(".*") 28 | if err != nil { 29 | fmt.Println(err) 30 | os.Exit(1) 31 | } 32 | } 33 | 34 | func restoreEnvs() { 35 | os.Clearenv() 36 | 37 | if len(envs) != 0 { 38 | for key, value := range envs { 39 | setenv(key, value) 40 | } 41 | } 42 | } 43 | 44 | func setenv(key string, value string) { 45 | err := os.Setenv(key, value) 46 | if err != nil { 47 | fmt.Println(err) 48 | os.Exit(1) 49 | } 50 | } 51 | 52 | func getCommitFromRef(ref string) string { 53 | cmd := exec.Command("git", "rev-parse", ref) 54 | cmd.Dir = gitRepositoryPath 55 | 56 | ID, err := cmd.Output() 57 | ID = ID[:len(ID)-1] 58 | 59 | if err != nil { 60 | logrus.WithField("ID", string(ID)).Fatal(err) 61 | } 62 | 63 | return string(ID) 64 | } 65 | -------------------------------------------------------------------------------- /chyle/extractors/extractor.go: -------------------------------------------------------------------------------- 1 | package extractors 2 | 3 | import ( 4 | "github.com/antham/chyle/chyle/types" 5 | ) 6 | 7 | // Extracter describes a way to extract data from a commit hashmap summary 8 | type Extracter interface { 9 | Extract(*map[string]any) *map[string]any 10 | } 11 | 12 | // Extract parses commit fields to extract datas 13 | func Extract(extractors *[]Extracter, commitMaps *[]map[string]any) *types.Changelog { 14 | results := []map[string]any{} 15 | 16 | for _, commitMap := range *commitMaps { 17 | result := &commitMap 18 | 19 | for _, extractor := range *extractors { 20 | result = extractor.Extract(result) 21 | } 22 | 23 | results = append(results, *result) 24 | } 25 | 26 | changelog := types.Changelog{} 27 | changelog.Datas = results 28 | changelog.Metadatas = map[string]any{} 29 | 30 | return &changelog 31 | } 32 | 33 | // Create builds extracters from a config 34 | func Create(features Features, extractors Config) *[]Extracter { 35 | results := []Extracter{} 36 | 37 | if !features.ENABLED { 38 | return &results 39 | } 40 | 41 | for _, extractor := range extractors { 42 | results = append(results, regex{ 43 | extractor.ORIGKEY, 44 | extractor.DESTKEY, 45 | extractor.REG, 46 | }) 47 | } 48 | 49 | return &results 50 | } 51 | -------------------------------------------------------------------------------- /chyle/tmplh/template.go: -------------------------------------------------------------------------------- 1 | package tmplh 2 | 3 | import ( 4 | "bytes" 5 | tmpl "html/template" 6 | 7 | "github.com/Masterminds/sprig" 8 | 9 | "github.com/antham/chyle/chyle/errh" 10 | ) 11 | 12 | var store = map[string]any{} 13 | 14 | func isset(key string) bool { 15 | _, ok := store[key] 16 | 17 | return ok 18 | } 19 | 20 | func set(key string, value any) string { 21 | store[key] = value 22 | 23 | return "" 24 | } 25 | 26 | func get(key string) any { 27 | return store[key] 28 | } 29 | 30 | // Parse creates a template instance from string template 31 | func Parse(ID string, template string) (*tmpl.Template, error) { 32 | funcMap := sprig.FuncMap() 33 | funcMap["isset"] = isset 34 | funcMap["set"] = set 35 | funcMap["get"] = get 36 | 37 | return tmpl.New(ID).Funcs(funcMap).Parse(template) 38 | } 39 | 40 | // Build creates a template instance and runs it against datas to get 41 | // final resolved string 42 | func Build(ID string, template string, data any) (string, error) { 43 | t, err := Parse(ID, template) 44 | if err != nil { 45 | return "", errh.AddCustomMessageToError("check your template is well-formed", err) 46 | } 47 | 48 | b := bytes.Buffer{} 49 | 50 | if err = t.Execute(&b, data); err != nil { 51 | return "", err 52 | } 53 | 54 | return b.String(), nil 55 | } 56 | -------------------------------------------------------------------------------- /chyle/convh/converter_test.go: -------------------------------------------------------------------------------- 1 | package convh 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGuessPrimitiveType(t *testing.T) { 11 | tests := []struct { 12 | str string 13 | expected any 14 | }{ 15 | { 16 | "test", 17 | "test", 18 | }, 19 | { 20 | "true", 21 | true, 22 | }, 23 | { 24 | "3.4", 25 | float64(3.4), 26 | }, 27 | { 28 | "13", 29 | int64(13), 30 | }, 31 | } 32 | 33 | for _, test := range tests { 34 | assert.EqualValues(t, test.expected, GuessPrimitiveType(test.str)) 35 | } 36 | } 37 | 38 | func TestConvertToString(t *testing.T) { 39 | tests := []struct { 40 | value any 41 | expected string 42 | err error 43 | }{ 44 | { 45 | "test", 46 | "test", 47 | nil, 48 | }, 49 | { 50 | true, 51 | "true", 52 | nil, 53 | }, 54 | { 55 | float64(3.4), 56 | "3.4", 57 | nil, 58 | }, 59 | { 60 | int(13), 61 | "13", 62 | nil, 63 | }, 64 | { 65 | struct{}{}, 66 | "", 67 | fmt.Errorf("value can't be converted to string"), 68 | }, 69 | } 70 | 71 | for _, test := range tests { 72 | v, err := ConvertToString(test.value) 73 | 74 | assert.Equal(t, test.err, err) 75 | assert.EqualValues(t, test.expected, v) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /chyle/senders/stdout.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/antham/chyle/chyle/tmplh" 10 | "github.com/antham/chyle/chyle/types" 11 | ) 12 | 13 | type stdoutConfig struct { 14 | FORMAT string 15 | TEMPLATE string 16 | } 17 | 18 | // jSONStdout output commit payload as JSON on stdout 19 | type jSONStdout struct { 20 | stdout io.Writer 21 | } 22 | 23 | func (j jSONStdout) Send(changelog *types.Changelog) error { 24 | return json.NewEncoder(j.stdout).Encode(changelog) 25 | } 26 | 27 | type templateStdout struct { 28 | stdout io.Writer 29 | template string 30 | } 31 | 32 | func (t templateStdout) Send(changelog *types.Changelog) error { 33 | datas, err := tmplh.Build("stdout-template", t.template, changelog) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | _, err = fmt.Fprint(t.stdout, datas) 39 | 40 | return err 41 | } 42 | 43 | func newStdout(config stdoutConfig) Sender { 44 | if config.FORMAT == "json" { 45 | return newJSONStdout() 46 | } 47 | 48 | return newTemplateStdout(config.TEMPLATE) 49 | } 50 | 51 | func newJSONStdout() Sender { 52 | return jSONStdout{ 53 | os.Stdout, 54 | } 55 | } 56 | 57 | func newTemplateStdout(template string) Sender { 58 | return templateStdout{ 59 | os.Stdout, 60 | template, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /prompt/menu.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "github.com/antham/strumt/v2" 5 | 6 | "github.com/antham/chyle/prompt/internal/builder" 7 | ) 8 | 9 | var mainMenu = []strumt.Prompter{ 10 | builder.NewSwitchPrompt( 11 | "mainMenu", 12 | addQuitChoice( 13 | []builder.SwitchConfig{ 14 | { 15 | Choice: "1", 16 | PromptString: "Add a matcher, they are used to filters git commit according to various criterias (message pattern, author, and so on)", 17 | NextPromptID: "matcherChoice", 18 | }, 19 | { 20 | Choice: "2", 21 | PromptString: "Add an extractor, they are used to extract datas from commit fields, for instance extracting PR number from a commit message", 22 | NextPromptID: "extractorOrigKey", 23 | }, 24 | { 25 | Choice: "3", 26 | PromptString: "Add a decorator, they are used to add datas from external sources, for instance contacting github issue api to add ticket title", 27 | NextPromptID: "decoratorChoice", 28 | }, 29 | { 30 | Choice: "4", 31 | PromptString: "Add a sender, they are used to send the result to an external source, for instance dumping the result as markdown on stdout", 32 | NextPromptID: "senderChoice", 33 | }, 34 | }, 35 | ), 36 | ), 37 | } 38 | 39 | func newMainMenu() []strumt.Prompter { 40 | return mainMenu 41 | } 42 | -------------------------------------------------------------------------------- /chyle/matchers/message_test.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRemovePGPKey(t *testing.T) { 10 | type g struct { 11 | originalValue string 12 | expected string 13 | } 14 | 15 | tests := []g{ 16 | { 17 | " kDBcBAABCAAGBQJYRXDpAAoJEP6+b85LBShD46IP+gNG/HO+BMDm76SZVIIkOMnS\n qw/DhStoxYgdjbwKxiMVISQ5fHJqX4+PbRv2pcMtWy74cK79qK1OgxLCWWLf4zft\n FiRfp/Wq92ChglsN95GI0IrbehloqdP1wzSMo99WtNGb8uacsnO1P9pDF6PzITUn\n nhpCmIek6AUP5iUZ5E1lF2QuTbc9zM3q5Lq0G2RUZ9AGQNM5HUEql6/zvsqlx5Xl\n gdPf9daDFB6W/rpFEAU5VskcUTYKgSKqvpNDE13Hz56F4J0Z4mRb7fUk6bMqW8bH\n 68cWhbRV55qgdNDoIHMaQavexkBeR5sZ/Czs8/ajNS6Z3hv7qy57YMw3Z5Hqkn57\n 3JKmM56YpkhBM6eQTEoRjWRMOeI8QlxgaXrLPB8WZzf9J7E7R85MWF7iuWxeBHAq\n Fo2aK35Sbxa2hsZUv+cCH7tJHtDSTnSgORC+vXeBL7PzLKYQ1fwJA0buJBdU+CNX\n 8SyDoOR44u58HksxUZecqXKgOTyyJer5hkGY8IlxIBaqLDkV/TyDKQCHCqNTAi7a\n DTYG+qvTVBnFuRv3vaYOMALKsiEFQPUtEK+lLc/TGlfyp4hSY3VC6Gggx4WUUPG+\n Mb+FdfpuEVPp/lBMcIIveolM29Pf66Cs/bYoJoFC/lbkBKBAEdE4PlUC9l0S7gLF\n xVbg93wF3uLMJtF63j0f\n =IaBk\n -----END PGP SIGNATURE-----\n\ntest :whatever\n", 18 | "test :whatever\n", 19 | }, 20 | { 21 | "test\n\ntest : whatever\n", 22 | "test\n\ntest : whatever\n", 23 | }, 24 | } 25 | 26 | for _, test := range tests { 27 | assert.Equal(t, test.expected, removePGPKey(test.originalValue)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chyle/decorators/jira_issue.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type jiraIssueConfig struct { 9 | CREDENTIALS struct { 10 | USERNAME string 11 | PASSWORD string 12 | } 13 | ENDPOINT struct { 14 | URL string 15 | } 16 | KEYS map[string]struct { 17 | DESTKEY string 18 | FIELD string 19 | } 20 | } 21 | 22 | // jiraIssue fetch data using jira issue api 23 | type jiraIssue struct { 24 | client http.Client 25 | config jiraIssueConfig 26 | } 27 | 28 | func (j jiraIssue) Decorate(commitMap *map[string]any) (*map[string]any, error) { 29 | var ID string 30 | 31 | switch v := (*commitMap)["jiraIssueId"].(type) { 32 | case string: 33 | ID = v 34 | case int64: 35 | ID = fmt.Sprintf("%d", v) 36 | default: 37 | return commitMap, nil 38 | } 39 | 40 | if ID == "" { 41 | return commitMap, nil 42 | } 43 | 44 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/rest/api/2/issue/%s", j.config.ENDPOINT.URL, ID), nil) 45 | if err != nil { 46 | return commitMap, err 47 | } 48 | 49 | req.SetBasicAuth(j.config.CREDENTIALS.USERNAME, j.config.CREDENTIALS.PASSWORD) 50 | req.Header.Set("Content-Type", "application/json") 51 | 52 | return jSONResponse{&j.client, req, j.config.KEYS}.Decorate(commitMap) 53 | } 54 | 55 | func newJiraIssue(config jiraIssueConfig) Decorater { 56 | return jiraIssue{http.Client{}, config} 57 | } 58 | -------------------------------------------------------------------------------- /chyle/decorators/custom_api.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "regexp" 7 | 8 | "github.com/antham/chyle/chyle/apih" 9 | ) 10 | 11 | type customAPIConfig struct { 12 | CREDENTIALS struct { 13 | TOKEN string 14 | } 15 | ENDPOINT struct { 16 | URL string 17 | } 18 | KEYS map[string]struct { 19 | DESTKEY string 20 | FIELD string 21 | } 22 | } 23 | 24 | // customAPI fetch data using a provided custom HTTP api 25 | type customAPI struct { 26 | client http.Client 27 | config customAPIConfig 28 | } 29 | 30 | func (c customAPI) Decorate(commitMap *map[string]any) (*map[string]any, error) { 31 | var ID string 32 | 33 | switch v := (*commitMap)["customApiId"].(type) { 34 | case string: 35 | ID = v 36 | case int64: 37 | ID = fmt.Sprintf("%d", v) 38 | default: 39 | return commitMap, nil 40 | } 41 | 42 | req, err := http.NewRequest("GET", regexp.MustCompile(`{{\s*ID\s*}}`).ReplaceAllString(c.config.ENDPOINT.URL, ID), nil) 43 | 44 | apih.SetHeaders(req, map[string]string{ 45 | "Authorization": "token " + c.config.CREDENTIALS.TOKEN, 46 | "Content-Type": "application/json", 47 | }) 48 | 49 | if err != nil { 50 | return commitMap, err 51 | } 52 | 53 | return jSONResponse{&c.client, req, c.config.KEYS}.Decorate(commitMap) 54 | } 55 | 56 | func newCustomAPI(config customAPIConfig) Decorater { 57 | return customAPI{http.Client{}, config} 58 | } 59 | -------------------------------------------------------------------------------- /chyle/config/custom_api_sender.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/antham/envh" 5 | ) 6 | 7 | type customAPISenderConfigurator struct { 8 | config *envh.EnvTree 9 | } 10 | 11 | func (g *customAPISenderConfigurator) process(config *CHYLE) (bool, error) { 12 | if g.isDisabled() { 13 | return false, nil 14 | } 15 | 16 | config.FEATURES.SENDERS.ENABLED = true 17 | config.FEATURES.SENDERS.CUSTOMAPI = true 18 | 19 | for _, f := range []func() error{ 20 | g.validateCredentials, 21 | g.validateMandatoryFields, 22 | g.validateURL, 23 | } { 24 | if err := f(); err != nil { 25 | return false, err 26 | } 27 | } 28 | 29 | return false, nil 30 | } 31 | 32 | func (g *customAPISenderConfigurator) isDisabled() bool { 33 | return !g.config.IsExistingSubTree("CHYLE", "SENDERS", "CUSTOMAPI") 34 | } 35 | 36 | func (g *customAPISenderConfigurator) validateCredentials() error { 37 | return validateEnvironmentVariablesDefinition(g.config, [][]string{{"CHYLE", "SENDERS", "CUSTOMAPI", "CREDENTIALS", "TOKEN"}}) 38 | } 39 | 40 | func (g *customAPISenderConfigurator) validateMandatoryFields() error { 41 | return validateEnvironmentVariablesDefinition(g.config, [][]string{{"CHYLE", "SENDERS", "CUSTOMAPI", "ENDPOINT", "URL"}}) 42 | } 43 | 44 | func (g *customAPISenderConfigurator) validateURL() error { 45 | return validateURL(g.config, []string{"CHYLE", "SENDERS", "CUSTOMAPI", "ENDPOINT", "URL"}) 46 | } 47 | -------------------------------------------------------------------------------- /chyle/process.go: -------------------------------------------------------------------------------- 1 | package chyle 2 | 3 | import ( 4 | "github.com/antham/chyle/chyle/config" 5 | "github.com/antham/chyle/chyle/decorators" 6 | "github.com/antham/chyle/chyle/extractors" 7 | "github.com/antham/chyle/chyle/matchers" 8 | "github.com/antham/chyle/chyle/senders" 9 | 10 | "github.com/go-git/go-git/v5/plumbing/object" 11 | ) 12 | 13 | // process represents all steps executed 14 | // when creating a changelog 15 | type process struct { 16 | matchers *[]matchers.Matcher 17 | extractors *[]extractors.Extracter 18 | decorators *map[string][]decorators.Decorater 19 | senders *[]senders.Sender 20 | } 21 | 22 | // newProcess creates process entity from defined configuration 23 | func newProcess(conf *config.CHYLE) *process { 24 | return &process{ 25 | matchers.Create(conf.FEATURES.MATCHERS, conf.MATCHERS), 26 | extractors.Create(conf.FEATURES.EXTRACTORS, conf.EXTRACTORS), 27 | decorators.Create(conf.FEATURES.DECORATORS, conf.DECORATORS), 28 | senders.Create(conf.FEATURES.SENDERS, conf.SENDERS), 29 | } 30 | } 31 | 32 | // proceed extracts datas from a set of commits 33 | func proceed(process *process, commits *[]object.Commit) error { 34 | changelog, err := decorators.Decorate(process.decorators, 35 | extractors.Extract(process.extractors, 36 | matchers.Filter(process.matchers, commits))) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return senders.Send(process.senders, changelog) 42 | } 43 | -------------------------------------------------------------------------------- /chyle/decorators/github_issue.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/antham/chyle/chyle/apih" 8 | ) 9 | 10 | type githubIssueConfig struct { 11 | CREDENTIALS struct { 12 | OAUTHTOKEN string 13 | OWNER string 14 | } 15 | REPOSITORY struct { 16 | NAME string 17 | } 18 | KEYS map[string]struct { 19 | DESTKEY string 20 | FIELD string 21 | } 22 | } 23 | 24 | // githubIssue fetch data using github issue api 25 | type githubIssue struct { 26 | client http.Client 27 | config githubIssueConfig 28 | } 29 | 30 | func (g githubIssue) Decorate(commitMap *map[string]any) (*map[string]any, error) { 31 | var ID int64 32 | var ok bool 33 | 34 | if ID, ok = (*commitMap)["githubIssueId"].(int64); !ok { 35 | return commitMap, nil 36 | } 37 | 38 | req, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/%s/issues/%d", g.config.CREDENTIALS.OWNER, g.config.REPOSITORY.NAME, ID), nil) 39 | if err != nil { 40 | return commitMap, err 41 | } 42 | 43 | apih.SetHeaders(req, map[string]string{ 44 | "Authorization": "token " + g.config.CREDENTIALS.OAUTHTOKEN, 45 | "Content-Type": "application/json", 46 | "Accept": "application/vnd.github.v3+json", 47 | }) 48 | 49 | return jSONResponse{&g.client, req, g.config.KEYS}.Decorate(commitMap) 50 | } 51 | 52 | func newGithubIssue(config githubIssueConfig) Decorater { 53 | return githubIssue{http.Client{}, config} 54 | } 55 | -------------------------------------------------------------------------------- /prompt/internal/builder/prompt_env.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/antham/strumt/v2" 5 | ) 6 | 7 | // NewEnvPrompts creates several prompts at once to populate environments variables 8 | func NewEnvPrompts(configs []EnvConfig, store *Store) []strumt.Prompter { 9 | results := []strumt.Prompter{} 10 | 11 | for _, config := range configs { 12 | results = append(results, NewEnvPrompt(config, store)) 13 | } 14 | 15 | return results 16 | } 17 | 18 | // NewEnvPrompt creates a prompt to populate an environment variable 19 | func NewEnvPrompt(config EnvConfig, store *Store) strumt.Prompter { 20 | return &GenericPrompt{ 21 | config.ID, 22 | config.PromptString, 23 | func(value string) string { 24 | config.RunBeforeNextPrompt(value, store) 25 | 26 | return config.NextID 27 | }, 28 | func(error) string { return config.ID }, 29 | ParseEnv(config.Validator, config.Env, config.DefaultValue, store), 30 | } 31 | } 32 | 33 | // ParseEnv provides an env parser callback 34 | func ParseEnv(validator func(string) error, env string, defaultValue string, store *Store) func(value string) error { 35 | return func(value string) error { 36 | if value == "" && defaultValue != "" { 37 | (*store)[env] = defaultValue 38 | 39 | return nil 40 | } 41 | 42 | if err := validator(value); err != nil { 43 | return err 44 | } 45 | 46 | if value != "" { 47 | (*store)[env] = value 48 | } 49 | 50 | return nil 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /chyle/convh/converter.go: -------------------------------------------------------------------------------- 1 | package convh 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // parseBool replaces original ParseBool functions from strconv 9 | // package to only convert "true" and "false" strings and nothing more 10 | func parseBool(str string) (bool, error) { 11 | b, err := strconv.ParseBool(str) 12 | 13 | switch str { 14 | case "1", "t", "T", "TRUE", "True", "0", "f", "F", "FALSE", "False": 15 | return false, fmt.Errorf("can't convert %s to boolean", str) 16 | } 17 | 18 | return b, err 19 | } 20 | 21 | // GuessPrimitiveType extracts underlying primitive type from a string 22 | // ,"true" will be translated as a boolean value for instance 23 | func GuessPrimitiveType(str string) any { 24 | if b, err := parseBool(str); err == nil { 25 | return b 26 | } 27 | 28 | if i, err := strconv.ParseInt(str, 10, 64); err == nil { 29 | return i 30 | } 31 | 32 | if f, err := strconv.ParseFloat(str, 64); err == nil { 33 | return f 34 | } 35 | 36 | return str 37 | } 38 | 39 | // ConvertToString extracts string value from an interface 40 | func ConvertToString(value any) (string, error) { 41 | switch v := value.(type) { 42 | case int: 43 | return strconv.Itoa(v), nil 44 | case float64: 45 | return strconv.FormatFloat(v, 'f', -1, 64), nil 46 | case bool: 47 | return strconv.FormatBool(v), nil 48 | case string: 49 | return v, nil 50 | default: 51 | return "", fmt.Errorf("value can't be converted to string") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /prompt/internal/builder/switch_prompt_test.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/antham/strumt/v2" 11 | ) 12 | 13 | func TestCreateSwitchChoice(t *testing.T) { 14 | sws := []strumt.Prompter{ 15 | &switchPrompt{ 16 | "test", 17 | []SwitchConfig{ 18 | { 19 | "1", 20 | "1 - Choice number 1", 21 | "test", 22 | }, 23 | { 24 | "2", 25 | "2 - Choice number 2", 26 | "test", 27 | }, 28 | { 29 | "3", 30 | "3 - Choice number 3", 31 | "", 32 | }, 33 | }, 34 | }, 35 | } 36 | 37 | var stdout bytes.Buffer 38 | buf := "1\n2\n4\n3\n" 39 | 40 | p := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 41 | for _, sw := range sws { 42 | p.AddLinePrompter(sw.(strumt.LinePrompter)) 43 | } 44 | 45 | p.SetFirst("test") 46 | p.Run() 47 | 48 | scenario := p.Scenario() 49 | 50 | steps := []struct { 51 | input string 52 | err error 53 | }{ 54 | { 55 | "1", 56 | nil, 57 | }, 58 | { 59 | "2", 60 | nil, 61 | }, 62 | { 63 | "4", 64 | fmt.Errorf("this choice doesn't exist"), 65 | }, 66 | { 67 | "3", 68 | nil, 69 | }, 70 | } 71 | 72 | for i, step := range steps { 73 | assert.Len(t, scenario[i].Inputs(), 1) 74 | assert.Equal(t, scenario[i].Inputs()[0], step.input) 75 | assert.Equal(t, scenario[i].Error(), step.err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /chyle/apih/http.go: -------------------------------------------------------------------------------- 1 | package apih 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | // errResponse is triggered when status code is greater or equal to 400 11 | type errResponse struct { 12 | request *http.Request 13 | response *http.Response 14 | body []byte 15 | } 16 | 17 | // Error output error as string 18 | func (e errResponse) Error() string { 19 | return fmt.Sprintf("an error occurred when contacting remote api through %s, status code %d, body %s", e.request.URL, e.response.StatusCode, e.body) 20 | } 21 | 22 | // SetHeaders setup headers on request from a map header key -> header value 23 | func SetHeaders(request *http.Request, headers map[string]string) { 24 | for k, v := range headers { 25 | request.Header.Set(k, v) 26 | } 27 | } 28 | 29 | // SendRequest picks a request and send it with given client 30 | func SendRequest(client *http.Client, request *http.Request) (int, []byte, error) { 31 | response, err := client.Do(request) 32 | if err != nil { 33 | return 0, []byte{}, err 34 | } 35 | 36 | defer func() { 37 | err = response.Body.Close() 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | }() 42 | 43 | b, err := io.ReadAll(response.Body) 44 | if err != nil { 45 | return response.StatusCode, b, errResponse{request, response, b} 46 | } 47 | 48 | if response.StatusCode >= 400 { 49 | return response.StatusCode, b, errResponse{request, response, b} 50 | } 51 | 52 | return response.StatusCode, b, nil 53 | } 54 | -------------------------------------------------------------------------------- /chyle/senders/custom_api.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/antham/chyle/chyle/apih" 9 | "github.com/antham/chyle/chyle/errh" 10 | "github.com/antham/chyle/chyle/types" 11 | ) 12 | 13 | type customAPIConfig struct { 14 | CREDENTIALS struct { 15 | TOKEN string 16 | } 17 | ENDPOINT struct { 18 | URL string 19 | } 20 | } 21 | 22 | // customAPI fetch data using a provided custom HTTP api 23 | type customAPI struct { 24 | client *http.Client 25 | config customAPIConfig 26 | } 27 | 28 | func (c customAPI) createRequest(changelog *types.Changelog) (*http.Request, error) { 29 | payload, err := json.Marshal(changelog) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | req, err := http.NewRequest("POST", c.config.ENDPOINT.URL, bytes.NewBuffer(payload)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | apih.SetHeaders(req, map[string]string{ 40 | "Authorization": "token " + c.config.CREDENTIALS.TOKEN, 41 | "Content-Type": "application/json", 42 | }) 43 | 44 | return req, nil 45 | } 46 | 47 | func (c customAPI) Send(changelog *types.Changelog) error { 48 | errMsg := "can't call custom api to send release" 49 | 50 | req, err := c.createRequest(changelog) 51 | if err != nil { 52 | return errh.AddCustomMessageToError(errMsg, err) 53 | } 54 | 55 | _, _, err = apih.SendRequest(c.client, req) 56 | 57 | return errh.AddCustomMessageToError(errMsg, err) 58 | } 59 | 60 | func newCustomAPI(config customAPIConfig) Sender { 61 | return customAPI{&http.Client{}, config} 62 | } 63 | -------------------------------------------------------------------------------- /prompt/internal/builder/switch_prompt.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/antham/strumt/v2" 7 | ) 8 | 9 | // NewSwitchPrompt creates a new prompt used to provides several choices, like a menu can do 10 | func NewSwitchPrompt(ID string, choices []SwitchConfig) strumt.Prompter { 11 | return &switchPrompt{ID, choices} 12 | } 13 | 14 | // SwitchConfig provides a configuration to switch prompt 15 | type SwitchConfig struct { 16 | Choice string 17 | PromptString string 18 | NextPromptID string 19 | } 20 | 21 | type switchPrompt struct { 22 | iD string 23 | choices []SwitchConfig 24 | } 25 | 26 | func (s *switchPrompt) ID() string { 27 | return s.iD 28 | } 29 | 30 | func (s *switchPrompt) PromptString() string { 31 | out := "Choose one of this option and press enter:\n" 32 | 33 | for _, choice := range s.choices { 34 | out += fmt.Sprintf("%s - %s\n", choice.Choice, choice.PromptString) 35 | } 36 | 37 | return out 38 | } 39 | 40 | func (s *switchPrompt) Parse(value string) error { 41 | if value == "" { 42 | return fmt.Errorf("no value given") 43 | } 44 | 45 | for _, choice := range s.choices { 46 | if choice.Choice == value { 47 | return nil 48 | } 49 | } 50 | 51 | return fmt.Errorf("this choice doesn't exist") 52 | } 53 | 54 | func (s *switchPrompt) NextOnSuccess(value string) string { 55 | for _, choice := range s.choices { 56 | if choice.Choice == value { 57 | return choice.NextPromptID 58 | } 59 | } 60 | 61 | return "" 62 | } 63 | 64 | func (s *switchPrompt) NextOnError(err error) string { 65 | return s.iD 66 | } 67 | -------------------------------------------------------------------------------- /prompt/mandatory.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "github.com/antham/strumt/v2" 5 | 6 | "github.com/antham/chyle/prompt/internal/builder" 7 | ) 8 | 9 | func newMandatoryOption(store *builder.Store) []strumt.Prompter { 10 | return builder.NewEnvPrompts(mandatoryOption, store) 11 | } 12 | 13 | var mandatoryOption = []builder.EnvConfig{ 14 | { 15 | ID: "referenceFrom", 16 | NextID: "referenceTo", 17 | Env: "CHYLE_GIT_REFERENCE_FROM", 18 | PromptString: "Enter a git commit ID that start your range, this value will likely vary if you want to integrate it to a CI tool so you will need to generate this value according to your context", 19 | Validator: validateDefinedValue, 20 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 21 | }, 22 | { 23 | ID: "referenceTo", 24 | NextID: "gitPath", 25 | Env: "CHYLE_GIT_REFERENCE_TO", 26 | PromptString: "Enter a git commit ID that finish your range, this value will likely vary if you want to integrate it to a CI tool so you will need to generate this value according to your context", 27 | Validator: validateDefinedValue, 28 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 29 | }, 30 | { 31 | ID: "gitPath", 32 | NextID: "mainMenu", 33 | Env: "CHYLE_GIT_REPOSITORY_PATH", 34 | PromptString: "Enter the location of your git path repository", 35 | Validator: validateDefinedValue, 36 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /chyle/config/jira_issue_decorator.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/antham/envh" 5 | ) 6 | 7 | func getJiraIssueDecoratorMandatoryParamsRefs() []ref { 8 | return []ref{ 9 | { 10 | &chyleConfig.DECORATORS.JIRAISSUE.ENDPOINT.URL, 11 | []string{"CHYLE", "DECORATORS", "JIRAISSUE", "ENDPOINT", "URL"}, 12 | }, 13 | { 14 | &chyleConfig.DECORATORS.JIRAISSUE.CREDENTIALS.USERNAME, 15 | []string{"CHYLE", "DECORATORS", "JIRAISSUE", "CREDENTIALS", "USERNAME"}, 16 | }, 17 | { 18 | &chyleConfig.DECORATORS.JIRAISSUE.CREDENTIALS.PASSWORD, 19 | []string{"CHYLE", "DECORATORS", "JIRAISSUE", "CREDENTIALS", "PASSWORD"}, 20 | }, 21 | } 22 | } 23 | 24 | func getJiraIssueDecoratorFeatureRefs() []*bool { 25 | return []*bool{ 26 | &chyleConfig.FEATURES.DECORATORS.ENABLED, 27 | &chyleConfig.FEATURES.DECORATORS.JIRAISSUE, 28 | } 29 | } 30 | 31 | func getJiraIssueDecoratorCustomValidationFuncs(config *envh.EnvTree) []func() error { 32 | return []func() error{} 33 | } 34 | 35 | func getJiraIssueDecoratorCustomSettersFuncs() []func(*CHYLE) { 36 | return []func(*CHYLE){} 37 | } 38 | 39 | func newJiraIssueDecoratorConfigurator(config *envh.EnvTree) configurater { 40 | return &apiDecoratorConfigurator{ 41 | config: config, 42 | apiDecoratorConfig: apiDecoratorConfig{ 43 | "JIRAISSUEID", 44 | "jiraIssueId", 45 | "JIRAISSUE", 46 | &chyleConfig.DECORATORS.JIRAISSUE.KEYS, 47 | getJiraIssueDecoratorMandatoryParamsRefs(), 48 | getJiraIssueDecoratorFeatureRefs(), 49 | getJiraIssueDecoratorCustomValidationFuncs(config), 50 | getJiraIssueDecoratorCustomSettersFuncs(), 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /chyle/decorators/shell.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/antham/chyle/chyle/convh" 9 | ) 10 | 11 | type shellConfig map[string]struct { 12 | COMMAND string 13 | ORIGKEY string 14 | DESTKEY string 15 | } 16 | 17 | // shell pipes a shell command on field content and dump the 18 | // result into a new field 19 | type shell struct { 20 | COMMAND string 21 | ORIGKEY string 22 | DESTKEY string 23 | } 24 | 25 | func (s shell) Decorate(commitMap *map[string]any) (*map[string]any, error) { 26 | var tmp any 27 | var value string 28 | var ok bool 29 | var err error 30 | 31 | if tmp, ok = (*commitMap)[s.ORIGKEY]; !ok { 32 | return commitMap, nil 33 | } 34 | 35 | if value, err = convh.ConvertToString(tmp); err != nil { 36 | return commitMap, nil 37 | } 38 | 39 | if (*commitMap)[s.DESTKEY], err = s.execute(value); err != nil { 40 | return commitMap, err 41 | } 42 | 43 | return commitMap, nil 44 | } 45 | 46 | func (s shell) execute(value string) (string, error) { 47 | var result []byte 48 | var err error 49 | 50 | command := fmt.Sprintf(`echo "%s"|%s`, strings.ReplaceAll(value, `"`, `\"`), s.COMMAND) 51 | 52 | /* #nosec */ 53 | if result, err = exec.Command("sh", "-c", command).Output(); err != nil { 54 | return "", fmt.Errorf("%s : command failed", command) 55 | } 56 | 57 | return string(result[:len(result)-1]), nil 58 | } 59 | 60 | func newShell(configs shellConfig) []Decorater { 61 | results := []Decorater{} 62 | 63 | for _, config := range configs { 64 | results = append(results, shell(config)) 65 | } 66 | 67 | return results 68 | } 69 | -------------------------------------------------------------------------------- /chyle/config/github_issue_decorator.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/antham/envh" 5 | ) 6 | 7 | func getGithubIssueDecoratorMandatoryParamsRefs() []ref { 8 | return []ref{ 9 | { 10 | &chyleConfig.DECORATORS.GITHUBISSUE.CREDENTIALS.OAUTHTOKEN, 11 | []string{"CHYLE", "DECORATORS", "GITHUBISSUE", "CREDENTIALS", "OAUTHTOKEN"}, 12 | }, 13 | { 14 | &chyleConfig.DECORATORS.GITHUBISSUE.CREDENTIALS.OWNER, 15 | []string{"CHYLE", "DECORATORS", "GITHUBISSUE", "CREDENTIALS", "OWNER"}, 16 | }, 17 | { 18 | &chyleConfig.DECORATORS.GITHUBISSUE.REPOSITORY.NAME, 19 | []string{"CHYLE", "DECORATORS", "GITHUBISSUE", "REPOSITORY", "NAME"}, 20 | }, 21 | } 22 | } 23 | 24 | func getGithubIssueDecoratorFeatureRefs() []*bool { 25 | return []*bool{ 26 | &chyleConfig.FEATURES.DECORATORS.ENABLED, 27 | &chyleConfig.FEATURES.DECORATORS.GITHUBISSUE, 28 | } 29 | } 30 | 31 | func getGithubIssueDecoratorCustomValidationFuncs() []func() error { 32 | return []func() error{} 33 | } 34 | 35 | func getGithubIssueDecoratorCustomSettersFuncs() []func(*CHYLE) { 36 | return []func(*CHYLE){} 37 | } 38 | 39 | func newGithubIssueDecoratorConfigurator(config *envh.EnvTree) configurater { 40 | return &apiDecoratorConfigurator{ 41 | config: config, 42 | apiDecoratorConfig: apiDecoratorConfig{ 43 | "GITHUBISSUEID", 44 | "githubIssueId", 45 | "GITHUBISSUE", 46 | &chyleConfig.DECORATORS.GITHUBISSUE.KEYS, 47 | getGithubIssueDecoratorMandatoryParamsRefs(), 48 | getGithubIssueDecoratorFeatureRefs(), 49 | getGithubIssueDecoratorCustomValidationFuncs(), 50 | getGithubIssueDecoratorCustomSettersFuncs(), 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /chyle/senders/stdout_test.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/antham/chyle/chyle/types" 11 | ) 12 | 13 | func TestNewStdout(t *testing.T) { 14 | config := stdoutConfig{FORMAT: "json"} 15 | assert.IsType(t, jSONStdout{}, newStdout(config)) 16 | 17 | config = stdoutConfig{FORMAT: "template", TEMPLATE: "{{.}}"} 18 | assert.IsType(t, templateStdout{}, newStdout(config)) 19 | } 20 | 21 | func TestJSONStdout(t *testing.T) { 22 | buf := &bytes.Buffer{} 23 | 24 | s := jSONStdout{buf} 25 | 26 | c := types.Changelog{ 27 | Datas: []map[string]any{}, 28 | Metadatas: map[string]any{}, 29 | } 30 | 31 | c.Datas = []map[string]any{ 32 | { 33 | "id": 1, 34 | "test": "test", 35 | }, 36 | { 37 | "id": 2, 38 | "test": "test", 39 | }, 40 | } 41 | 42 | err := s.Send(&c) 43 | 44 | assert.NoError(t, err) 45 | assert.Equal(t, `{"datas":[{"id":1,"test":"test"},{"id":2,"test":"test"}],"metadatas":{}}`, strings.TrimRight(buf.String(), "\n")) 46 | } 47 | 48 | func TestTemplateStdout(t *testing.T) { 49 | buf := &bytes.Buffer{} 50 | 51 | s := templateStdout{buf, "{{ range $key, $value := .Datas }}{{$value.id}} : {{$value.test}} | {{ end }}"} 52 | 53 | c := types.Changelog{ 54 | Datas: []map[string]any{}, 55 | Metadatas: map[string]any{}, 56 | } 57 | 58 | c.Datas = []map[string]any{ 59 | { 60 | "id": 1, 61 | "test": "test", 62 | }, 63 | { 64 | "id": 2, 65 | "test": "test", 66 | }, 67 | } 68 | 69 | err := s.Send(&c) 70 | 71 | assert.NoError(t, err) 72 | assert.Equal(t, `1 : test | 2 : test | `, strings.TrimRight(buf.String(), "\n")) 73 | } 74 | -------------------------------------------------------------------------------- /chyle/tmplh/template_test.go: -------------------------------------------------------------------------------- 1 | package tmplh 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPopulateTemplate(t *testing.T) { 10 | tests := []struct { 11 | ID string 12 | template string 13 | data any 14 | expected string 15 | errStr string 16 | }{ 17 | { 18 | "test", 19 | "{{.test}}", 20 | map[string]string{"test": "Hello world !"}, 21 | "Hello world !", 22 | ``, 23 | }, 24 | { 25 | "test", 26 | "{{.test", 27 | map[string]string{"test": "Hello world !"}, 28 | "", 29 | `check your template is well-formed : template: test:1: unclosed action`, 30 | }, 31 | { 32 | "test", 33 | `{{ upper "hello" }}`, 34 | ``, 35 | "HELLO", 36 | ``, 37 | }, 38 | { 39 | "test", 40 | `{{ set "test" "whatever" }}{{ get "test" }}`, 41 | ``, 42 | `whatever`, 43 | ``, 44 | }, 45 | { 46 | "test", 47 | `{{ set "test" true }}{{ get "test" }}`, 48 | ``, 49 | `true`, 50 | ``, 51 | }, 52 | { 53 | "test", 54 | `{{ set "test" 1 }}{{ get "test" }}`, 55 | ``, 56 | `1`, 57 | ``, 58 | }, 59 | { 60 | "test", 61 | `{{ set "test" "whatever" }}{{ if isset "test" }}{{ get "test" }}{{ end }}`, 62 | ``, 63 | `whatever`, 64 | ``, 65 | }, 66 | { 67 | "test", 68 | `{{ if isset "test" }}{{ get "test" }}{{ end }}`, 69 | ``, 70 | ``, 71 | ``, 72 | }, 73 | } 74 | 75 | for _, test := range tests { 76 | store = map[string]any{} 77 | 78 | d, err := Build(test.ID, test.template, test.data) 79 | if err != nil { 80 | assert.EqualError(t, err, test.errStr) 81 | 82 | continue 83 | } 84 | 85 | assert.Equal(t, test.expected, d) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /chyle/config/stdout_sender.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/antham/envh" 8 | ) 9 | 10 | type stdoutSenderConfigurator struct { 11 | config *envh.EnvTree 12 | } 13 | 14 | func (s *stdoutSenderConfigurator) process(config *CHYLE) (bool, error) { 15 | if s.isDisabled() { 16 | return false, nil 17 | } 18 | 19 | config.FEATURES.SENDERS.ENABLED = true 20 | config.FEATURES.SENDERS.STDOUT = true 21 | 22 | return false, s.validateFormat() 23 | } 24 | 25 | func (s *stdoutSenderConfigurator) isDisabled() bool { 26 | return featureDisabled(s.config, [][]string{{"CHYLE", "SENDERS", "STDOUT"}}) 27 | } 28 | 29 | func (s *stdoutSenderConfigurator) validateFormat() error { 30 | var err error 31 | var format string 32 | keyChain := []string{"CHYLE", "SENDERS", "STDOUT"} 33 | 34 | if format, err = s.config.FindString(append(keyChain, "FORMAT")...); err != nil { 35 | return MissingEnvError{[]string{strings.Join(append(keyChain, "FORMAT"), "_")}} 36 | } 37 | 38 | switch format { 39 | case "json": 40 | return nil 41 | case "template": 42 | return s.validateTemplateFormat() 43 | } 44 | 45 | return EnvValidationError{fmt.Sprintf(`"CHYLE_SENDERS_STDOUT_FORMAT" "%s" doesn't exist`, format), "CHYLE_SENDERS_STDOUT_FORMAT"} 46 | } 47 | 48 | func (s *stdoutSenderConfigurator) validateTemplateFormat() error { 49 | tmplKeyChain := []string{"CHYLE", "SENDERS", "STDOUT", "TEMPLATE"} 50 | 51 | if ok, err := s.config.HasSubTreeValue(tmplKeyChain...); !ok || err != nil { 52 | return MissingEnvError{[]string{strings.Join(tmplKeyChain, "_")}} 53 | } 54 | 55 | if err := validateTemplate(s.config, tmplKeyChain); err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /chyle/config/env_decorator.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/antham/envh" 5 | ) 6 | 7 | type envDecoratorConfigurator struct { 8 | config *envh.EnvTree 9 | } 10 | 11 | func (e *envDecoratorConfigurator) process(config *CHYLE) (bool, error) { 12 | if e.isDisabled() { 13 | return true, nil 14 | } 15 | 16 | config.FEATURES.DECORATORS.ENABLED = true 17 | config.FEATURES.DECORATORS.ENV = true 18 | 19 | for _, f := range []func() error{ 20 | e.validateEnvironmentVariables, 21 | } { 22 | if err := f(); err != nil { 23 | return true, err 24 | } 25 | } 26 | 27 | e.setEnvConfigs(config) 28 | 29 | return true, nil 30 | } 31 | 32 | func (e *envDecoratorConfigurator) isDisabled() bool { 33 | return featureDisabled(e.config, [][]string{{"CHYLE", "DECORATORS", "ENV"}}) 34 | } 35 | 36 | func (e *envDecoratorConfigurator) validateEnvironmentVariables() error { 37 | for _, key := range e.config.FindChildrenKeysUnsecured("CHYLE", "DECORATORS", "ENV") { 38 | if err := validateEnvironmentVariablesDefinition(e.config, [][]string{{"CHYLE", "DECORATORS", "ENV", key, "DESTKEY"}, {"CHYLE", "DECORATORS", "ENV", key, "VARNAME"}}); err != nil { 39 | return err 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (e *envDecoratorConfigurator) setEnvConfigs(config *CHYLE) { 47 | config.DECORATORS.ENV = map[string]struct { 48 | DESTKEY string 49 | VARNAME string 50 | }{} 51 | 52 | for _, key := range e.config.FindChildrenKeysUnsecured("CHYLE", "DECORATORS", "ENV") { 53 | config.DECORATORS.ENV[key] = struct { 54 | DESTKEY string 55 | VARNAME string 56 | }{ 57 | e.config.FindStringUnsecured("CHYLE", "DECORATORS", "ENV", key, "DESTKEY"), 58 | e.config.FindStringUnsecured("CHYLE", "DECORATORS", "ENV", key, "VARNAME"), 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /chyle/senders/fixtures/github-tag-creation-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/test/test/releases/1", 3 | "html_url": "https://github.com/test/test/releases/v1.0.0", 4 | "assets_url": "https://api.github.com/repos/test/test/releases/1/assets", 5 | "upload_url": "https://uploads.github.com/repos/test/test/releases/1/assets{?name,label}", 6 | "tarball_url": "https://api.github.com/repos/test/test/tarball/v1.0.0", 7 | "zipball_url": "https://api.github.com/repos/test/test/zipball/v1.0.0", 8 | "id": 1, 9 | "tag_name": "v1.0.0", 10 | "target_commitish": "master", 11 | "name": "v1.0.0", 12 | "body": "Description of the release", 13 | "draft": false, 14 | "prerelease": false, 15 | "created_at": "2013-02-27T19:35:32Z", 16 | "published_at": "2013-02-27T19:35:32Z", 17 | "author": { 18 | "login": "test", 19 | "id": 1, 20 | "avatar_url": "https://github.com/images/error/test_happy.gif", 21 | "gravatar_id": "", 22 | "url": "https://api.github.com/users/test", 23 | "html_url": "https://github.com/test", 24 | "followers_url": "https://api.github.com/users/test/followers", 25 | "following_url": "https://api.github.com/users/test/following{/other_user}", 26 | "gists_url": "https://api.github.com/users/test/gists{/gist_id}", 27 | "starred_url": "https://api.github.com/users/test/starred{/owner}{/repo}", 28 | "subscriptions_url": "https://api.github.com/users/test/subscriptions", 29 | "organizations_url": "https://api.github.com/users/test/orgs", 30 | "repos_url": "https://api.github.com/users/test/repos", 31 | "events_url": "https://api.github.com/users/test/events{/privacy}", 32 | "received_events_url": "https://api.github.com/users/test/received_events", 33 | "type": "User", 34 | "site_admin": false 35 | }, 36 | "assets": [] 37 | } 38 | -------------------------------------------------------------------------------- /features/merge-commits.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gitRepositoryPath=testing-repository 4 | 5 | cd $gitRepositoryPath || exit 1 6 | 7 | # Create branch test 8 | git checkout --quiet -b test 9 | 10 | # Create several commits on test branch 11 | touch file1 12 | git add file1 13 | git commit --quiet -F- < dario.cat/mergo v1.0.0 60 | -------------------------------------------------------------------------------- /prompt/internal/builder/prompt_group_env.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/antham/chyle/prompt/internal/counter" 7 | "github.com/antham/strumt/v2" 8 | ) 9 | 10 | // NewGroupEnvPromptWithCounter gives the ability to create several group of related environment variable, a common prefix provided as a number from an internal counter tied variable together. For instance in variables environments TEST_*_KEY and TEST_*_VALUE, * is replaced with a number, it becomes TEST_0_KEY and TEST_0_VALUE another call would give TEST_1_VALUE and TEST_1_KEY 11 | func NewGroupEnvPromptWithCounter(configs []EnvConfig, store *Store) []strumt.Prompter { 12 | results := []strumt.Prompter{} 13 | c := &counter.Counter{} 14 | 15 | for i, config := range configs { 16 | f := parseEnvWithCounter(config.Validator, config.Env, config.DefaultValue, c, store) 17 | 18 | if i == len(configs)-1 { 19 | f = parseEnvWithCounterAndIncrement(config.Validator, config.Env, config.DefaultValue, c, store) 20 | } 21 | 22 | p := GenericPrompt{ 23 | config.ID, 24 | config.PromptString, 25 | func(NextID string) func(string) string { return func(string) string { return NextID } }(config.NextID), 26 | func(ID string) func(error) string { return func(error) string { return ID } }(config.ID), 27 | f, 28 | } 29 | 30 | results = append(results, &p) 31 | } 32 | 33 | return results 34 | } 35 | 36 | func parseEnvWithCounter(validator func(string) error, env string, defaultValue string, counter *counter.Counter, store *Store) func(value string) error { 37 | return func(value string) error { 38 | if value == "" && defaultValue != "" { 39 | (*store)[strings.ReplaceAll(env, "*", counter.Get())] = defaultValue 40 | 41 | return nil 42 | } 43 | 44 | if err := validator(value); err != nil { 45 | return err 46 | } 47 | 48 | if value != "" { 49 | (*store)[strings.ReplaceAll(env, "*", counter.Get())] = value 50 | } 51 | 52 | return nil 53 | } 54 | } 55 | 56 | func parseEnvWithCounterAndIncrement(validator func(string) error, env string, defaultValue string, counter *counter.Counter, store *Store) func(value string) error { 57 | return func(value string) error { 58 | if value == "" && defaultValue != "" { 59 | (*store)[strings.ReplaceAll(env, "*", counter.Get())] = defaultValue 60 | 61 | return nil 62 | } 63 | 64 | if err := validator(value); err != nil { 65 | return err 66 | } 67 | 68 | if value != "" { 69 | (*store)[strings.ReplaceAll(env, "*", counter.Get())] = value 70 | } 71 | 72 | counter.Increment() 73 | 74 | return nil 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

chyle

2 | 3 | # [![codecov](https://codecov.io/gh/antham/chyle/branch/master/graph/badge.svg)](https://codecov.io/gh/antham/chyle) [![Go Report Card](https://goreportcard.com/badge/github.com/antham/chyle)](https://goreportcard.com/report/github.com/antham/chyle) [![GitHub tag](https://img.shields.io/github/tag/antham/chyle.svg)]() 4 | 5 | Chyle produces a changelog from a git repository. 6 | 7 | [![asciicast](https://asciinema.org/a/o2PDZ4ELfUP3F1eKWl1IqirzU.png)](https://asciinema.org/a/o2PDZ4ELfUP3F1eKWl1IqirzU) 8 | 9 | --- 10 | 11 | - [Usage](#usage) 12 | - [How it works ?](#how-it-works-) 13 | - [Setup](#setup) 14 | - [Documentation and examples](#documentation-and-examples) 15 | - [Contribute](#contribute) 16 | 17 | --- 18 | 19 | ## Usage 20 | 21 | ``` 22 | Create a changelog from your commit history 23 | 24 | Usage: 25 | chyle [command] 26 | 27 | Available Commands: 28 | config Configuration prompt 29 | create Create a new changelog 30 | help Help about any command 31 | 32 | Flags: 33 | --debug enable debugging 34 | -h, --help help for chyle 35 | 36 | Use "chyle [command] --help" for more information about a command. 37 | ``` 38 | 39 | ### config 40 | 41 | Run a serie of prompt to help generate quickly and easily a configuration. 42 | 43 | ### create 44 | 45 | Generate changelog. 46 | 47 | ## How it works ? 48 | 49 | Chyle fetch a range of commits using given criterias from a git repository. From those commits you can extract relevant datas from commit message, author, and so on, and add it to original payload. You can afterwards if needed, enrich your payload with various useful datas contacting external apps (shell command, apis, ....) and finally, you can publish what you harvested (to an external api, stdout, ....). You can mix all steps together, avoid some, combine some, it's up to you. 50 | 51 | ## Setup 52 | 53 | Download from release page according to your architecture chyle binary : https://github.com/antham/chyle/releases 54 | 55 | Look at the documentation and examples, run `chyle config` to launch the configuration prompt. 56 | 57 | ## Documentation and examples 58 | 59 | Have a look to the [wiki of this project](https://github.com/antham/chyle/wiki). 60 | 61 | ## Contribute 62 | 63 | If you want to add a new feature to chyle project, the best way is to open a ticket first to know exactly how to implement your changes in code. 64 | 65 | ### Setup 66 | 67 | After cloning the repository you need to install vendors with `go mod vendor` 68 | To test your changes locally you can run go tests with : `make test-all` 69 | -------------------------------------------------------------------------------- /prompt/matcher.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/antham/strumt/v2" 7 | 8 | "github.com/antham/chyle/prompt/internal/builder" 9 | ) 10 | 11 | func newMatchers(store *builder.Store) []strumt.Prompter { 12 | return mergePrompters( 13 | matcherChoice, 14 | builder.NewEnvPrompts(matcher, store), 15 | ) 16 | } 17 | 18 | var matcherChoice = []strumt.Prompter{ 19 | builder.NewSwitchPrompt( 20 | "matcherChoice", 21 | addMainMenuAndQuitChoice( 22 | []builder.SwitchConfig{ 23 | { 24 | Choice: "1", 25 | PromptString: "Add a type matcher, it's used to match merge commit or regular one", 26 | NextPromptID: "matcherType", 27 | }, 28 | { 29 | Choice: "2", 30 | PromptString: "Add a message matcher, it's used to match a commit according to a pattern found in commit message", 31 | NextPromptID: "matcherMessage", 32 | }, 33 | { 34 | Choice: "3", 35 | PromptString: "Add a committer matcher, it's used to match a commit according to a pattern apply to the committer field", 36 | NextPromptID: "matcherCommitter", 37 | }, 38 | { 39 | Choice: "4", 40 | PromptString: "Add an author matcher, it's used to match a commit according to a pattern apply to the author field", 41 | NextPromptID: "matcherAuthor", 42 | }, 43 | }, 44 | ), 45 | ), 46 | } 47 | 48 | var matcher = []builder.EnvConfig{ 49 | { 50 | ID: "matcherType", 51 | NextID: "matcherChoice", 52 | Env: "CHYLE_MATCHERS_TYPE", 53 | PromptString: "Enter a matcher type (regular or merge)", 54 | Validator: validateMatcherType, 55 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 56 | }, 57 | { 58 | ID: "matcherMessage", 59 | NextID: "matcherChoice", 60 | Env: "CHYLE_MATCHERS_MESSAGE", 61 | PromptString: "Enter a regexp to match commit message", 62 | Validator: validateRegexp, 63 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 64 | }, 65 | { 66 | ID: "matcherCommitter", 67 | NextID: "matcherChoice", 68 | Env: "CHYLE_MATCHERS_COMMITTER", 69 | PromptString: "Enter a regexp to match git committer", 70 | Validator: validateRegexp, 71 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 72 | }, 73 | { 74 | ID: "matcherAuthor", 75 | NextID: "matcherChoice", 76 | Env: "CHYLE_MATCHERS_AUTHOR", 77 | PromptString: "Enter a regexp to match git author", 78 | Validator: validateRegexp, 79 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 80 | }, 81 | } 82 | 83 | func validateMatcherType(value string) error { 84 | if value != "regular" && value != "merge" { 85 | return fmt.Errorf(`must be "regular" or "merge"`) 86 | } 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /chyle/senders/custom_api_test.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/h2non/gock.v0" 10 | 11 | "github.com/antham/chyle/chyle/types" 12 | ) 13 | 14 | func TestCustomAPI(t *testing.T) { 15 | config := customAPIConfig{} 16 | config.ENDPOINT.URL = "https://test.com/releases" 17 | config.CREDENTIALS.TOKEN = "d41d8cd98f00b204e9800998ecf8427e" 18 | 19 | defer gock.Off() 20 | 21 | gock.New("https://test.com"). 22 | Post("/releases"). 23 | MatchHeader("Authorization", "token d41d8cd98f00b204e9800998ecf8427e"). 24 | MatchHeader("Content-Type", "application/json"). 25 | JSON(map[string]string{"test": "Hello world !"}). 26 | Reply(201) 27 | 28 | client := &http.Client{Transport: &http.Transport{}} 29 | gock.InterceptClient(client) 30 | 31 | s := newCustomAPI(config).(customAPI) 32 | s.client = client 33 | 34 | c := types.Changelog{ 35 | Datas: []map[string]any{}, 36 | Metadatas: map[string]any{}, 37 | } 38 | 39 | c.Datas = append(c.Datas, map[string]any{"test": "Hello world !"}) 40 | 41 | err := s.Send(&c) 42 | 43 | assert.NoError(t, err) 44 | assert.True(t, gock.IsDone(), "Must have no pending requests") 45 | } 46 | 47 | func TestCustomAPIWithWrongCredentials(t *testing.T) { 48 | config := customAPIConfig{} 49 | config.ENDPOINT.URL = "https://test.com/releases" 50 | config.CREDENTIALS.TOKEN = "d41d8cd98f00b204e9800998ecf8427e" 51 | 52 | defer gock.Off() 53 | 54 | gock.New("https://test.com"). 55 | Post("/releases"). 56 | MatchHeader("Authorization", "token d41d8cd98f00b204e9800998ecf8427e"). 57 | MatchHeader("Content-Type", "application/json"). 58 | ReplyError(fmt.Errorf(`{"error":"You don't have correct credentials"}`)) 59 | 60 | client := &http.Client{Transport: &http.Transport{}} 61 | gock.InterceptClient(client) 62 | 63 | s := newCustomAPI(config).(customAPI) 64 | s.client = client 65 | 66 | c := types.Changelog{ 67 | Datas: []map[string]any{}, 68 | Metadatas: map[string]any{}, 69 | } 70 | 71 | c.Datas = append(c.Datas, map[string]any{"test": "Hello world !"}) 72 | 73 | err := s.Send(&c) 74 | 75 | assert.EqualError(t, err, `can't call custom api to send release : Post "https://test.com/releases": {"error":"You don't have correct credentials"}`) 76 | assert.True(t, gock.IsDone(), "Must have no pending requests") 77 | } 78 | 79 | func TestCustomAPIWithWrongURL(t *testing.T) { 80 | config := customAPIConfig{} 81 | config.ENDPOINT.URL = ":test" 82 | config.CREDENTIALS.TOKEN = "d41d8cd98f00b204e9800998ecf8427e" 83 | 84 | client := &http.Client{Transport: &http.Transport{}} 85 | 86 | s := newCustomAPI(config).(customAPI) 87 | s.client = client 88 | 89 | c := types.Changelog{ 90 | Datas: []map[string]any{}, 91 | Metadatas: map[string]any{}, 92 | } 93 | 94 | c.Datas = append(c.Datas, map[string]any{"test": "Hello world !"}) 95 | 96 | err := s.Send(&c) 97 | 98 | assert.EqualError(t, err, `can't call custom api to send release : parse ":test": missing protocol scheme`) 99 | assert.True(t, gock.IsDone(), "Must have no pending requests") 100 | } 101 | -------------------------------------------------------------------------------- /cmd/create_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func TestCreate(t *testing.T) { 17 | var code int 18 | var w sync.WaitGroup 19 | 20 | exitError = func() { 21 | panic(1) 22 | } 23 | 24 | exitSuccess = func() { 25 | panic(0) 26 | } 27 | 28 | restoreEnvs() 29 | setenv("CHYLE_GIT_REPOSITORY_PATH", gitRepositoryPath) 30 | setenv("CHYLE_GIT_REFERENCE_FROM", getCommitFromRef("HEAD~3")) 31 | setenv("CHYLE_GIT_REFERENCE_TO", getCommitFromRef("test~2^2")) 32 | 33 | w.Add(1) 34 | 35 | go func() { 36 | defer func() { 37 | if r := recover(); r != nil { 38 | code = r.(int) 39 | } 40 | 41 | w.Done() 42 | }() 43 | 44 | os.Args = []string{"", "create"} 45 | 46 | Execute() 47 | }() 48 | 49 | w.Wait() 50 | 51 | assert.EqualValues(t, 0, code, "Must exit with no errors (exit 0)") 52 | } 53 | 54 | func TestCreateWithErrors(t *testing.T) { 55 | for _, filename := range []string{"../features/init.sh", "../features/merge-commits.sh"} { 56 | err := exec.Command(filename).Run() 57 | if err != nil { 58 | logrus.Fatal(err) 59 | } 60 | } 61 | 62 | var code int 63 | var w sync.WaitGroup 64 | 65 | exitError = func() { 66 | panic(1) 67 | } 68 | 69 | exitSuccess = func() { 70 | panic(0) 71 | } 72 | 73 | writer = &bytes.Buffer{} 74 | 75 | fixtures := map[string]func(){ 76 | `environment variable missing : "CHYLE_GIT_REPOSITORY_PATH"`: func() { 77 | }, 78 | `environments variables missing : "CHYLE_GIT_REFERENCE_FROM", "CHYLE_GIT_REFERENCE_TO"`: func() { 79 | setenv("CHYLE_GIT_REPOSITORY_PATH", "whatever") 80 | }, 81 | `environment variable missing : "CHYLE_GIT_REFERENCE_TO"`: func() { 82 | setenv("CHYLE_GIT_REPOSITORY_PATH", "whatever") 83 | setenv("CHYLE_GIT_REFERENCE_FROM", "ref1") 84 | }, 85 | `check "whatever" is an existing git repository path`: func() { 86 | setenv("CHYLE_GIT_REPOSITORY_PATH", "whatever") 87 | setenv("CHYLE_GIT_REFERENCE_FROM", "ref1") 88 | setenv("CHYLE_GIT_REFERENCE_TO", "ref2") 89 | }, 90 | `reference "ref1" can't be found in git repository`: func() { 91 | setenv("CHYLE_GIT_REPOSITORY_PATH", gitRepositoryPath) 92 | setenv("CHYLE_GIT_REFERENCE_FROM", "ref1") 93 | setenv("CHYLE_GIT_REFERENCE_TO", "ref2") 94 | }, 95 | `reference "ref2" can't be found in git repository`: func() { 96 | setenv("CHYLE_GIT_REPOSITORY_PATH", gitRepositoryPath) 97 | setenv("CHYLE_GIT_REFERENCE_FROM", "HEAD") 98 | setenv("CHYLE_GIT_REFERENCE_TO", "ref2") 99 | }, 100 | } 101 | 102 | for errStr, fun := range fixtures { 103 | w.Add(1) 104 | 105 | go func() { 106 | defer func() { 107 | if r := recover(); r != nil { 108 | code = r.(int) 109 | } 110 | 111 | w.Done() 112 | }() 113 | 114 | restoreEnvs() 115 | fun() 116 | 117 | os.Args = []string{"", "create"} 118 | 119 | Execute() 120 | }() 121 | 122 | w.Wait() 123 | 124 | output, err := io.ReadAll(writer.(*bytes.Buffer)) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | assert.EqualValues(t, 1, code, "Must exit with an error (exit 1)") 130 | assert.Contains(t, string(output), errStr) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /chyle/process_test.go: -------------------------------------------------------------------------------- 1 | package chyle 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/antham/chyle/chyle/config" 9 | "github.com/antham/chyle/chyle/decorators" 10 | "github.com/antham/chyle/chyle/extractors" 11 | "github.com/antham/chyle/chyle/matchers" 12 | "github.com/antham/chyle/chyle/senders" 13 | "github.com/antham/chyle/chyle/types" 14 | 15 | "github.com/go-git/go-git/v5/plumbing/object" 16 | 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func TestBuildProcessWithAnEmptyConfig(t *testing.T) { 21 | conf := config.CHYLE{} 22 | 23 | p := newProcess(&conf) 24 | 25 | expected := process{ 26 | &[]matchers.Matcher{}, 27 | &[]extractors.Extracter{}, 28 | &map[string][]decorators.Decorater{"metadatas": {}, "datas": {}}, 29 | &[]senders.Sender{}, 30 | } 31 | 32 | assert.EqualValues(t, expected, *p) 33 | } 34 | 35 | func TestBuildProcessWithAFullConfig(t *testing.T) { 36 | conf := config.CHYLE{} 37 | 38 | conf.FEATURES.MATCHERS.ENABLED = true 39 | conf.FEATURES.MATCHERS.TYPE = true 40 | conf.MATCHERS = matchers.Config{TYPE: "merge"} 41 | 42 | conf.FEATURES.EXTRACTORS.ENABLED = true 43 | conf.EXTRACTORS = map[string]struct { 44 | ORIGKEY string 45 | DESTKEY string 46 | REG *regexp.Regexp 47 | }{ 48 | "TEST": { 49 | "TEST", 50 | "test", 51 | regexp.MustCompile(".*"), 52 | }, 53 | } 54 | 55 | conf.FEATURES.DECORATORS.ENABLED = true 56 | conf.FEATURES.DECORATORS.ENABLED = true 57 | conf.DECORATORS.ENV = map[string]struct { 58 | DESTKEY string 59 | VARNAME string 60 | }{ 61 | "TEST": { 62 | "test", 63 | "TEST", 64 | }, 65 | } 66 | 67 | conf.FEATURES.SENDERS.ENABLED = true 68 | conf.FEATURES.SENDERS.STDOUT = true 69 | conf.SENDERS.STDOUT.FORMAT = "json" 70 | 71 | p := newProcess(&conf) 72 | 73 | assert.Len(t, *(p.matchers), 1) 74 | assert.Len(t, *(p.extractors), 1) 75 | assert.Len(t, *(p.decorators), 2) 76 | assert.Len(t, *(p.senders), 1) 77 | } 78 | 79 | type mockDecorator struct{} 80 | 81 | func (m mockDecorator) Decorate(*map[string]any) (*map[string]any, error) { 82 | return &map[string]any{}, fmt.Errorf("An error occured from mock decorator") 83 | } 84 | 85 | type mockSender struct{} 86 | 87 | func (m mockSender) Send(changelog *types.Changelog) error { 88 | return fmt.Errorf("An error occured from mock sender") 89 | } 90 | 91 | func (m mockSender) Decorate(*map[string]any) (*map[string]any, error) { 92 | return &map[string]any{}, fmt.Errorf("An error occured from mock decorator") 93 | } 94 | 95 | func TestBuildProcessWithErrorsFromDecorator(t *testing.T) { 96 | p := process{ 97 | &[]matchers.Matcher{}, 98 | &[]extractors.Extracter{}, 99 | &map[string][]decorators.Decorater{"metadatas": {}, "datas": {mockDecorator{}}}, 100 | &[]senders.Sender{}, 101 | } 102 | 103 | err := proceed(&p, &[]object.Commit{{}}) 104 | 105 | assert.Error(t, err) 106 | assert.EqualError(t, err, "An error occured from mock decorator") 107 | } 108 | 109 | func TestBuildProcessWithErrorsFromSender(t *testing.T) { 110 | p := process{ 111 | &[]matchers.Matcher{}, 112 | &[]extractors.Extracter{}, 113 | &map[string][]decorators.Decorater{"metadatas": {}, "datas": {}}, 114 | &[]senders.Sender{mockSender{}}, 115 | } 116 | 117 | err := proceed(&p, &[]object.Commit{{}}) 118 | 119 | assert.Error(t, err) 120 | assert.EqualError(t, err, "An error occured from mock sender") 121 | } 122 | -------------------------------------------------------------------------------- /chyle/senders/fixtures/github-release-fetch-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/antham/chyle/releases/1", 3 | "html_url": "https://github.com/antham/chyle/releases/v1.0.0", 4 | "assets_url": "https://api.github.com/repos/antham/chyle/releases/1/assets", 5 | "upload_url": "https://uploads.github.com/repos/antham/chyle/releases/1/assets{?name,label}", 6 | "tarball_url": "https://api.github.com/repos/antham/chyle/tarball/v1.0.0", 7 | "zipball_url": "https://api.github.com/repos/antham/chyle/zipball/v1.0.0", 8 | "id": 1, 9 | "tag_name": "v1.0.0", 10 | "target_commitish": "master", 11 | "name": "v1.0.0", 12 | "body": "Description of the release", 13 | "draft": false, 14 | "prerelease": false, 15 | "created_at": "2013-02-27T19:35:32Z", 16 | "published_at": "2013-02-27T19:35:32Z", 17 | "author": { 18 | "login": "antham", 19 | "id": 1, 20 | "avatar_url": "https://github.com/images/error/antham_happy.gif", 21 | "gravatar_id": "", 22 | "url": "https://api.github.com/users/antham", 23 | "html_url": "https://github.com/antham", 24 | "followers_url": "https://api.github.com/users/antham/followers", 25 | "following_url": "https://api.github.com/users/antham/following{/other_user}", 26 | "gists_url": "https://api.github.com/users/antham/gists{/gist_id}", 27 | "starred_url": "https://api.github.com/users/antham/starred{/owner}{/repo}", 28 | "subscriptions_url": "https://api.github.com/users/antham/subscriptions", 29 | "organizations_url": "https://api.github.com/users/antham/orgs", 30 | "repos_url": "https://api.github.com/users/antham/repos", 31 | "events_url": "https://api.github.com/users/antham/events{/privacy}", 32 | "received_events_url": "https://api.github.com/users/antham/received_events", 33 | "type": "User", 34 | "site_admin": false 35 | }, 36 | "assets": [ 37 | { 38 | "url": "https://api.github.com/repos/antham/chyle/releases/assets/1", 39 | "browser_download_url": "https://github.com/antham/chyle/releases/download/v1.0.0/example.zip", 40 | "id": 1, 41 | "name": "example.zip", 42 | "label": "short description", 43 | "state": "uploaded", 44 | "content_type": "application/zip", 45 | "size": 1024, 46 | "download_count": 42, 47 | "created_at": "2013-02-27T19:35:32Z", 48 | "updated_at": "2013-02-27T19:35:32Z", 49 | "uploader": { 50 | "login": "antham", 51 | "id": 1, 52 | "avatar_url": "https://github.com/images/error/antham_happy.gif", 53 | "gravatar_id": "", 54 | "url": "https://api.github.com/users/antham", 55 | "html_url": "https://github.com/antham", 56 | "followers_url": "https://api.github.com/users/antham/followers", 57 | "following_url": "https://api.github.com/users/antham/following{/other_user}", 58 | "gists_url": "https://api.github.com/users/antham/gists{/gist_id}", 59 | "starred_url": "https://api.github.com/users/antham/starred{/owner}{/repo}", 60 | "subscriptions_url": "https://api.github.com/users/antham/subscriptions", 61 | "organizations_url": "https://api.github.com/users/antham/orgs", 62 | "repos_url": "https://api.github.com/users/antham/repos", 63 | "events_url": "https://api.github.com/users/antham/events{/privacy}", 64 | "received_events_url": "https://api.github.com/users/antham/received_events", 65 | "type": "User", 66 | "site_admin": false 67 | } 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /chyle/extractors/extractor_test.go: -------------------------------------------------------------------------------- 1 | package extractors 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/antham/chyle/chyle/types" 10 | ) 11 | 12 | func TestExtract(t *testing.T) { 13 | extractors := []Extracter{ 14 | regex{ 15 | "id", 16 | "serviceId", 17 | regexp.MustCompile(`(\#\d+)`), 18 | }, 19 | regex{ 20 | "id", 21 | "booleanValue", 22 | regexp.MustCompile(`(true|false)`), 23 | }, 24 | regex{ 25 | "id", 26 | "intValue", 27 | regexp.MustCompile(` (\d+)`), 28 | }, 29 | regex{ 30 | "id", 31 | "floatValue", 32 | regexp.MustCompile(`(\d+\.\d+)`), 33 | }, 34 | regex{ 35 | "secondIdentifier", 36 | "secondServiceId", 37 | regexp.MustCompile(`(#\d+)`), 38 | }, 39 | } 40 | 41 | commitMaps := []map[string]any{ 42 | { 43 | "id": "Whatever #30 whatever true 12345 whatever 12345.12", 44 | "secondIdentifier": "test #12345", 45 | }, 46 | { 47 | "id": "Whatever #40 whatever false whatever 78910 whatever 78910.12", 48 | "secondIdentifier": "test #45678", 49 | }, 50 | { 51 | "id": "Whatever whatever whatever", 52 | }, 53 | } 54 | 55 | results := Extract(&extractors, &commitMaps) 56 | 57 | expected := types.Changelog{ 58 | Datas: []map[string]any{ 59 | { 60 | "id": "Whatever #30 whatever true 12345 whatever 12345.12", 61 | "secondIdentifier": "test #12345", 62 | "serviceId": "#30", 63 | "secondServiceId": "#12345", 64 | "booleanValue": true, 65 | "intValue": int64(12345), 66 | "floatValue": 12345.12, 67 | }, 68 | { 69 | "id": "Whatever #40 whatever false whatever 78910 whatever 78910.12", 70 | "secondIdentifier": "test #45678", 71 | "serviceId": "#40", 72 | "secondServiceId": "#45678", 73 | "booleanValue": false, 74 | "intValue": int64(78910), 75 | "floatValue": 78910.12, 76 | }, 77 | { 78 | "id": "Whatever whatever whatever", 79 | "serviceId": "", 80 | "booleanValue": "", 81 | "intValue": "", 82 | "floatValue": "", 83 | }, 84 | }, 85 | Metadatas: map[string]any{}, 86 | } 87 | 88 | assert.Equal(t, expected, *results) 89 | } 90 | 91 | func TestCreate(t *testing.T) { 92 | extractors := Config{ 93 | "ID": { 94 | "id", 95 | "test", 96 | regexp.MustCompile(".*"), 97 | }, 98 | "AUTHORNAME": { 99 | "authorName", 100 | "test2", 101 | regexp.MustCompile(".*"), 102 | }, 103 | } 104 | 105 | e := Create(Features{ENABLED: true}, extractors) 106 | 107 | assert.Len(t, *e, 2) 108 | 109 | expected := map[string]map[string]string{ 110 | "id": { 111 | "index": "id", 112 | "identifier": "test", 113 | "regexp": ".*", 114 | }, 115 | "authorName": { 116 | "index": "authorName", 117 | "identifier": "test2", 118 | "regexp": ".*", 119 | }, 120 | } 121 | 122 | for i := 0; i < 2; i++ { 123 | index := (*e)[0].(regex).index 124 | 125 | v, ok := expected[index] 126 | 127 | if !ok { 128 | assert.Fail(t, "Index must exists in expected") 129 | } 130 | 131 | assert.Equal(t, (*e)[0].(regex).index, v["index"]) 132 | assert.Equal(t, (*e)[0].(regex).identifier, v["identifier"]) 133 | assert.Equal(t, (*e)[0].(regex).re, regexp.MustCompile(v["regexp"])) 134 | } 135 | } 136 | 137 | func TestCreateWithFeatureDisabled(t *testing.T) { 138 | e := Create(Features{}, Config{ 139 | "ID": { 140 | "id", 141 | "test", 142 | regexp.MustCompile(".*"), 143 | }, 144 | }) 145 | 146 | assert.Len(t, *e, 0) 147 | } 148 | -------------------------------------------------------------------------------- /chyle/config/primitives.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/antham/chyle/chyle/tmplh" 10 | 11 | "github.com/antham/envh" 12 | ) 13 | 14 | // MissingEnvError is called when one or several 15 | // environment variables are missing 16 | type MissingEnvError struct { 17 | envs []string 18 | } 19 | 20 | // Envs returns environment variables missing 21 | func (e MissingEnvError) Envs() []string { 22 | return e.envs 23 | } 24 | 25 | // Errors returns error as string 26 | func (e MissingEnvError) Error() string { 27 | switch len(e.envs) { 28 | case 1: 29 | return fmt.Sprintf(`environment variable missing : "%s"`, e.envs[0]) 30 | default: 31 | return fmt.Sprintf(`environments variables missing : "%s"`, strings.Join(e.envs, `", "`)) 32 | } 33 | } 34 | 35 | func validateEnvironmentVariablesDefinition(conf *envh.EnvTree, keyChains [][]string) error { 36 | undefinedKeys := []string{} 37 | 38 | for _, keyChain := range keyChains { 39 | ok, err := conf.HasSubTreeValue(keyChain...) 40 | 41 | if !ok || err != nil { 42 | undefinedKeys = append(undefinedKeys, strings.Join(keyChain, "_")) 43 | } 44 | } 45 | 46 | if len(undefinedKeys) > 0 { 47 | return MissingEnvError{undefinedKeys} 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func validateStringValue(value string, conf *envh.EnvTree, keyChain []string) error { 54 | if conf.FindStringUnsecured(keyChain...) != value { 55 | return EnvValidationError{fmt.Sprintf(`variable %s must be equal to "%s"`, strings.Join(keyChain, "_"), value), strings.Join(keyChain, "_")} 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func validateURL(fullconfig *envh.EnvTree, chain []string) error { 62 | if _, err := url.ParseRequestURI(fullconfig.FindStringUnsecured(chain...)); err != nil { 63 | return EnvValidationError{fmt.Sprintf(`provide a valid URL for "%s", "%s" given`, strings.Join(chain, "_"), fullconfig.FindStringUnsecured(chain...)), strings.Join(chain, "_")} 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func validateRegexp(fullconfig *envh.EnvTree, keyChain []string) error { 70 | if _, err := regexp.Compile(fullconfig.FindStringUnsecured(keyChain...)); err != nil { 71 | return EnvValidationError{fmt.Sprintf(`provide a valid regexp for "%s", "%s" given`, strings.Join(keyChain, "_"), fullconfig.FindStringUnsecured(keyChain...)), strings.Join(keyChain, "_")} 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func validateOneOf(fullconfig *envh.EnvTree, keyChain []string, choices []string) error { 78 | val := fullconfig.FindStringUnsecured(keyChain...) 79 | 80 | for _, choice := range choices { 81 | if choice == val { 82 | return nil 83 | } 84 | } 85 | 86 | return EnvValidationError{fmt.Sprintf(`provide a value for "%s" from one of those values : ["%s"], "%s" given`, strings.Join(keyChain, "_"), strings.Join(choices, `", "`), val), strings.Join(keyChain, "_")} 87 | } 88 | 89 | func validateTemplate(fullconfig *envh.EnvTree, keyChain []string) error { 90 | val := fullconfig.FindStringUnsecured(keyChain...) 91 | 92 | _, err := tmplh.Parse("test", val) 93 | if err != nil { 94 | return EnvValidationError{fmt.Sprintf(`provide a valid template string for "%s" : "%s", "%s" given`, strings.Join(keyChain, "_"), err.Error(), val), strings.Join(keyChain, "_")} 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // featureDisabled return false if one subtree declared in keyChains exists 101 | func featureDisabled(fullconfig *envh.EnvTree, keyChains [][]string) bool { 102 | for _, keyChain := range keyChains { 103 | if fullconfig.IsExistingSubTree(keyChain...) { 104 | return false 105 | } 106 | } 107 | 108 | return true 109 | } 110 | -------------------------------------------------------------------------------- /prompt/internal/builder/prompt_group_env_test.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/antham/strumt/v2" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewGroupEnvPromptWithCounter(t *testing.T) { 13 | store := &Store{} 14 | 15 | var stdout bytes.Buffer 16 | buf := "test0\ntest1\n1\ntest2\ntest3\nq\n" 17 | prompts := NewGroupEnvPromptWithCounter( 18 | []EnvConfig{ 19 | {"TEST_0", "TEST_1", "TEST_*_0", "Enter a value", func(value string) error { return nil }, "", func(value string, store *Store) {}}, 20 | {"TEST_1", "choice", "TEST_*_1", "Enter a value", func(value string) error { return nil }, "", func(value string, store *Store) {}}, 21 | }, store) 22 | 23 | choice := []strumt.Prompter{ 24 | &switchPrompt{ 25 | "choice", 26 | []SwitchConfig{ 27 | { 28 | "1", "Add new test values", "TEST_0", 29 | }, 30 | { 31 | "q", "Quit", "", 32 | }, 33 | }, 34 | }, 35 | } 36 | 37 | prompts = append(prompts, choice...) 38 | 39 | s := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 40 | 41 | for _, item := range prompts { 42 | switch prompt := item.(type) { 43 | case strumt.LinePrompter: 44 | s.AddLinePrompter(prompt) 45 | case strumt.MultilinePrompter: 46 | s.AddMultilinePrompter(prompt) 47 | } 48 | } 49 | 50 | s.SetFirst("TEST_0") 51 | s.Run() 52 | 53 | scenario := s.Scenario() 54 | 55 | steps := []struct { 56 | input string 57 | err error 58 | }{ 59 | { 60 | "test0", 61 | nil, 62 | }, 63 | { 64 | "test1", 65 | nil, 66 | }, 67 | { 68 | "1", 69 | nil, 70 | }, 71 | { 72 | "test2", 73 | nil, 74 | }, 75 | { 76 | "test3", 77 | nil, 78 | }, 79 | { 80 | "q", 81 | nil, 82 | }, 83 | } 84 | 85 | for i, step := range steps { 86 | assert.Nil(t, step.err) 87 | assert.Len(t, scenario[i].Inputs(), 1) 88 | assert.Equal(t, scenario[i].Inputs()[0], step.input) 89 | } 90 | 91 | assert.Equal(t, &Store{"TEST_0_0": "test0", "TEST_0_1": "test1", "TEST_1_0": "test2", "TEST_1_1": "test3"}, store) 92 | } 93 | 94 | func TestNewGroupEnvPromptWithAnEmptyValueAndValidationRules(t *testing.T) { 95 | store := &Store{} 96 | 97 | var stdout bytes.Buffer 98 | buf := "test0\ntest1\ntest2\n1\ntest3\n\nq\n" 99 | prompts := NewGroupEnvPromptWithCounter( 100 | []EnvConfig{ 101 | {"TEST_0", "TEST_1", "TEST_*_0", "Enter a value", func(value string) error { 102 | if value == "test0" { 103 | return errors.New("Must be different value than test0") 104 | } 105 | return nil 106 | }, "", func(value string, store *Store) {}}, 107 | {"TEST_1", "choice", "TEST_*_1", "Enter a value", func(value string) error { return nil }, "test4", func(value string, store *Store) {}}, 108 | }, store) 109 | 110 | choice := []strumt.Prompter{ 111 | &switchPrompt{ 112 | "choice", 113 | []SwitchConfig{ 114 | { 115 | "1", "Add new test values", "TEST_0", 116 | }, 117 | { 118 | "q", "Quit", "", 119 | }, 120 | }, 121 | }, 122 | } 123 | 124 | prompts = append(prompts, choice...) 125 | 126 | s := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 127 | 128 | for _, item := range prompts { 129 | switch prompt := item.(type) { 130 | case strumt.LinePrompter: 131 | s.AddLinePrompter(prompt) 132 | case strumt.MultilinePrompter: 133 | s.AddMultilinePrompter(prompt) 134 | } 135 | } 136 | 137 | s.SetFirst("TEST_0") 138 | s.Run() 139 | 140 | scenario := s.Scenario() 141 | 142 | steps := []struct { 143 | input string 144 | err error 145 | }{ 146 | { 147 | "test0", 148 | nil, 149 | }, 150 | { 151 | "test1", 152 | nil, 153 | }, 154 | { 155 | "test2", 156 | nil, 157 | }, 158 | { 159 | "1", 160 | nil, 161 | }, 162 | { 163 | "test3", 164 | nil, 165 | }, 166 | { 167 | "", 168 | nil, 169 | }, 170 | { 171 | "q", 172 | nil, 173 | }, 174 | } 175 | 176 | for i, step := range steps { 177 | assert.Nil(t, step.err) 178 | assert.Len(t, scenario[i].Inputs(), 1) 179 | assert.Equal(t, scenario[i].Inputs()[0], step.input) 180 | } 181 | 182 | assert.Equal(t, &Store{"TEST_0_0": "test1", "TEST_0_1": "test2", "TEST_1_0": "test3", "TEST_1_1": "test4"}, store) 183 | } 184 | -------------------------------------------------------------------------------- /chyle/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "strings" 7 | 8 | "github.com/antham/envh" 9 | 10 | "github.com/antham/chyle/chyle/decorators" 11 | "github.com/antham/chyle/chyle/extractors" 12 | "github.com/antham/chyle/chyle/matchers" 13 | "github.com/antham/chyle/chyle/senders" 14 | ) 15 | 16 | var chyleConfig CHYLE 17 | 18 | // EnvValidationError is called when validating a 19 | // configuration failed, it keeps a track of which 20 | // environment variable is actually tested 21 | type EnvValidationError struct { 22 | message string 23 | env string 24 | } 25 | 26 | // Env returns environment variable currently tested 27 | func (v EnvValidationError) Env() string { 28 | return v.env 29 | } 30 | 31 | // Error returns error as string 32 | func (v EnvValidationError) Error() string { 33 | return v.message 34 | } 35 | 36 | // configurater must be implemented to process custom config 37 | type configurater interface { 38 | process(config *CHYLE) (bool, error) 39 | } 40 | 41 | type ref struct { 42 | ref *string 43 | keyChain []string 44 | } 45 | 46 | // codebeat:disable[TOO_MANY_IVARS] 47 | 48 | // CHYLE hold config extracted from environment variables 49 | type CHYLE struct { 50 | FEATURES struct { 51 | MATCHERS matchers.Features 52 | EXTRACTORS extractors.Features 53 | DECORATORS decorators.Features 54 | SENDERS senders.Features 55 | } `json:"-"` 56 | GIT struct { 57 | REPOSITORY struct { 58 | PATH string 59 | } 60 | REFERENCE struct { 61 | FROM string 62 | TO string 63 | } 64 | } 65 | MATCHERS matchers.Config 66 | EXTRACTORS extractors.Config 67 | DECORATORS decorators.Config 68 | SENDERS senders.Config 69 | } 70 | 71 | // codebeat:enable[TOO_MANY_IVARS] 72 | 73 | // Walk traverses struct to populate or validate fields 74 | func (c *CHYLE) Walk(fullconfig *envh.EnvTree, keyChain []string) (bool, error) { 75 | if walker, ok := map[string]func(*envh.EnvTree, []string) (bool, error){ 76 | "CHYLE_FEATURES": func(*envh.EnvTree, []string) (bool, error) { return true, nil }, 77 | "CHYLE_GIT_REFERENCE": c.validateChyleGitReference, 78 | "CHYLE_GIT_REPOSITORY": c.validateChyleGitRepository, 79 | }[strings.Join(keyChain, "_")]; ok { 80 | return walker(fullconfig, keyChain) 81 | } 82 | 83 | if processor, ok := map[string]func() configurater{ 84 | "CHYLE_DECORATORS_ENV": func() configurater { return &envDecoratorConfigurator{fullconfig} }, 85 | "CHYLE_DECORATORS_CUSTOMAPI": func() configurater { return newCustomAPIDecoratorConfigurator(fullconfig) }, 86 | "CHYLE_DECORATORS_GITHUBISSUE": func() configurater { return newGithubIssueDecoratorConfigurator(fullconfig) }, 87 | "CHYLE_DECORATORS_JIRAISSUE": func() configurater { return newJiraIssueDecoratorConfigurator(fullconfig) }, 88 | "CHYLE_DECORATORS_SHELL": func() configurater { return &shellDecoratorConfigurator{fullconfig} }, 89 | "CHYLE_EXTRACTORS": func() configurater { return &extractorsConfigurator{fullconfig} }, 90 | "CHYLE_MATCHERS": func() configurater { return &matchersConfigurator{fullconfig} }, 91 | "CHYLE_SENDERS_GITHUBRELEASE": func() configurater { return &githubReleaseSenderConfigurator{fullconfig} }, 92 | "CHYLE_SENDERS_CUSTOMAPI": func() configurater { return &customAPISenderConfigurator{fullconfig} }, 93 | "CHYLE_SENDERS_STDOUT": func() configurater { return &stdoutSenderConfigurator{fullconfig} }, 94 | }[strings.Join(keyChain, "_")]; ok { 95 | return processor().process(c) 96 | } 97 | 98 | return false, nil 99 | } 100 | 101 | func (c *CHYLE) validateChyleGitRepository(fullconfig *envh.EnvTree, keyChain []string) (bool, error) { 102 | return false, validateEnvironmentVariablesDefinition(fullconfig, [][]string{{"CHYLE", "GIT", "REPOSITORY", "PATH"}}) 103 | } 104 | 105 | func (c *CHYLE) validateChyleGitReference(fullconfig *envh.EnvTree, keyChain []string) (bool, error) { 106 | return false, validateEnvironmentVariablesDefinition(fullconfig, [][]string{{"CHYLE", "GIT", "REFERENCE", "FROM"}, {"CHYLE", "GIT", "REFERENCE", "TO"}}) 107 | } 108 | 109 | // Create returns app config from an EnvTree object 110 | func Create(envConfig *envh.EnvTree) (*CHYLE, error) { 111 | chyleConfig = CHYLE{} 112 | return &chyleConfig, envConfig.PopulateStruct(&chyleConfig) 113 | } 114 | 115 | // Debug dumps given CHYLE config as JSON structure 116 | func Debug(config *CHYLE, logger *log.Logger) { 117 | if d, err := json.MarshalIndent(config, "", " "); err == nil { 118 | logger.Println(string(d)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /chyle/senders/github_release.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/antham/chyle/chyle/apih" 10 | "github.com/antham/chyle/chyle/errh" 11 | "github.com/antham/chyle/chyle/tmplh" 12 | "github.com/antham/chyle/chyle/types" 13 | ) 14 | 15 | type githubReleaseConfig struct { 16 | REPOSITORY struct { 17 | NAME string 18 | } 19 | CREDENTIALS struct { 20 | OAUTHTOKEN string 21 | OWNER string 22 | } 23 | RELEASE struct { 24 | DRAFT bool 25 | UPDATE bool 26 | PRERELEASE bool 27 | NAME string 28 | TAGNAME string 29 | TARGETCOMMITISH string 30 | TEMPLATE string 31 | } 32 | } 33 | 34 | // codebeat:disable[TOO_MANY_IVARS] 35 | 36 | // githubReleasePayload follows https://developer.github.com/v3/repos/releases/#create-a-release 37 | type githubReleasePayload struct { 38 | TagName string `json:"tag_name"` 39 | TargetCommitish string `json:"target_commitish,omitempty"` 40 | Name string `json:"name,omitempty"` 41 | Body string `json:"body,omitempty"` 42 | Draft bool `json:"draft,omitempty"` 43 | PreRelease bool `json:"prerelease,omitempty"` 44 | } 45 | 46 | // codebeat:enable[TOO_MANY_IVARS] 47 | 48 | func newGithubRelease(config githubReleaseConfig) Sender { 49 | return githubRelease{&http.Client{}, config} 50 | } 51 | 52 | // githubRelease fetch data using jira issue api 53 | type githubRelease struct { 54 | client *http.Client 55 | config githubReleaseConfig 56 | } 57 | 58 | // buildBody creates a request body from changelog 59 | func (g githubRelease) buildBody(changelog *types.Changelog) ([]byte, error) { 60 | body, err := tmplh.Build("github-release-template", g.config.RELEASE.TEMPLATE, changelog) 61 | if err != nil { 62 | return []byte{}, err 63 | } 64 | 65 | r := githubReleasePayload{ 66 | g.config.RELEASE.TAGNAME, 67 | g.config.RELEASE.TARGETCOMMITISH, 68 | g.config.RELEASE.NAME, 69 | body, 70 | g.config.RELEASE.DRAFT, 71 | g.config.RELEASE.PRERELEASE, 72 | } 73 | 74 | return json.Marshal(r) 75 | } 76 | 77 | func (g githubRelease) createRelease(body []byte) error { 78 | URL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", g.config.CREDENTIALS.OWNER, g.config.REPOSITORY.NAME) 79 | 80 | req, err := http.NewRequest("POST", URL, bytes.NewBuffer(body)) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | apih.SetHeaders(req, map[string]string{ 86 | "Authorization": "token " + g.config.CREDENTIALS.OAUTHTOKEN, 87 | "Content-Type": "application/json", 88 | "Accept": "application/vnd.github.v3+json", 89 | }) 90 | 91 | _, _, err = apih.SendRequest(g.client, req) 92 | 93 | return errh.AddCustomMessageToError("can't create github release", err) 94 | } 95 | 96 | // getReleaseID retrieves github release ID from a given tag name 97 | func (g githubRelease) getReleaseID() (int, error) { 98 | type s struct { 99 | ID int `json:"id"` 100 | } 101 | 102 | release := s{} 103 | 104 | errMsg := fmt.Sprintf("can't retrieve github release %s", g.config.RELEASE.TAGNAME) 105 | URL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", g.config.CREDENTIALS.OWNER, g.config.REPOSITORY.NAME, g.config.RELEASE.TAGNAME) 106 | 107 | req, err := http.NewRequest("GET", URL, nil) 108 | if err != nil { 109 | return 0, err 110 | } 111 | 112 | apih.SetHeaders(req, map[string]string{ 113 | "Authorization": "token " + g.config.CREDENTIALS.OAUTHTOKEN, 114 | "Content-Type": "application/json", 115 | "Accept": "application/vnd.github.v3+json", 116 | }) 117 | 118 | _, body, err := apih.SendRequest(g.client, req) 119 | if err != nil { 120 | return 0, errh.AddCustomMessageToError(errMsg, err) 121 | } 122 | 123 | err = json.Unmarshal(body, &release) 124 | if err != nil { 125 | return 0, errh.AddCustomMessageToError(errMsg, err) 126 | } 127 | 128 | return release.ID, nil 129 | } 130 | 131 | // updateRelease updates an existing release from a tag name 132 | func (g githubRelease) updateRelease(body []byte) error { 133 | ID, err := g.getReleaseID() 134 | if err != nil { 135 | return err 136 | } 137 | 138 | URL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/%d", g.config.CREDENTIALS.OWNER, g.config.REPOSITORY.NAME, ID) 139 | 140 | req, err := http.NewRequest("PATCH", URL, bytes.NewBuffer(body)) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | apih.SetHeaders(req, map[string]string{ 146 | "Authorization": "token " + g.config.CREDENTIALS.OAUTHTOKEN, 147 | "Content-Type": "application/json", 148 | "Accept": "application/vnd.github.v3+json", 149 | }) 150 | 151 | _, _, err = apih.SendRequest(g.client, req) 152 | 153 | return errh.AddCustomMessageToError(fmt.Sprintf("can't update github release %s", g.config.RELEASE.TAGNAME), err) 154 | } 155 | 156 | func (g githubRelease) Send(changelog *types.Changelog) error { 157 | body, err := g.buildBody(changelog) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | if g.config.RELEASE.UPDATE { 163 | return g.updateRelease(body) 164 | } 165 | 166 | return g.createRelease(body) 167 | } 168 | -------------------------------------------------------------------------------- /chyle/decorators/github_issue_test.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/h2non/gock.v0" 10 | ) 11 | 12 | func TestGithubIssue(t *testing.T) { 13 | config := githubIssueConfig{} 14 | config.CREDENTIALS.OAUTHTOKEN = "d41d8cd98f00b204e9800998ecf8427e" 15 | config.CREDENTIALS.OWNER = "user" 16 | config.REPOSITORY.NAME = "repository" 17 | config.KEYS = map[string]struct { 18 | DESTKEY string 19 | FIELD string 20 | }{ 21 | "MILESTONE": { 22 | "milestoneCreator", 23 | "milestone.creator.id", 24 | }, 25 | "WHATEVER": { 26 | "whatever", 27 | "whatever", 28 | }, 29 | } 30 | 31 | defer gock.Off() 32 | 33 | issueResponse, err := os.ReadFile("fixtures/github-issue-fetch-response.json") 34 | 35 | assert.NoError(t, err, "Must read json fixture file") 36 | 37 | gock.New("https://api.github.com/repos/user/repository/issues/10000"). 38 | MatchHeader("Authorization", "token d41d8cd98f00b204e9800998ecf8427e"). 39 | MatchHeader("Content-Type", "application/json"). 40 | HeaderPresent("Accept"). 41 | Reply(200). 42 | JSON(string(issueResponse)) 43 | 44 | client := &http.Client{Transport: &http.Transport{}} 45 | gock.InterceptClient(client) 46 | 47 | j := githubIssue{*client, config} 48 | 49 | result, err := j.Decorate(&map[string]any{"test": "test", "githubIssueId": int64(10000)}) 50 | 51 | expected := map[string]any{ 52 | "test": "test", 53 | "githubIssueId": int64(10000), 54 | "milestoneCreator": float64(1), 55 | } 56 | 57 | assert.NoError(t, err) 58 | assert.Equal(t, expected, *result) 59 | assert.True(t, gock.IsDone(), "Must have no pending requests") 60 | } 61 | 62 | func TestGithubWithNoGithubIssueIdDefined(t *testing.T) { 63 | defer gock.Off() 64 | 65 | issueResponse, err := os.ReadFile("fixtures/github-issue-fetch-response.json") 66 | 67 | assert.NoError(t, err, "Must read json fixture file") 68 | 69 | gock.New("https://api.github.com/repos/user/repository/issues/10000"). 70 | Reply(200). 71 | JSON(string(issueResponse)) 72 | 73 | client := &http.Client{Transport: &http.Transport{}} 74 | gock.InterceptClient(client) 75 | 76 | j := githubIssue{*client, githubIssueConfig{}} 77 | 78 | result, err := j.Decorate(&map[string]any{"test": "test"}) 79 | 80 | expected := map[string]any{ 81 | "test": "test", 82 | } 83 | 84 | assert.NoError(t, err) 85 | assert.Equal(t, expected, *result) 86 | assert.False(t, gock.IsDone(), "Must have one pending request") 87 | } 88 | 89 | func TestGithubIssueWithAnErrorStatusCode(t *testing.T) { 90 | config := githubIssueConfig{} 91 | config.CREDENTIALS.OAUTHTOKEN = "d41d8cd98f00b204e9800998ecf8427e" 92 | config.CREDENTIALS.OWNER = "user" 93 | config.REPOSITORY.NAME = "repository" 94 | config.KEYS = map[string]struct { 95 | DESTKEY string 96 | FIELD string 97 | }{ 98 | "MILESTONE": { 99 | "milestoneCreator", 100 | "milestone.creator.id", 101 | }, 102 | "WHATEVER": { 103 | "whatever", 104 | "whatever", 105 | }, 106 | } 107 | 108 | defer gock.Off() 109 | 110 | gock.New("https://api.github.com/repos/user/repository/issues/10000"). 111 | MatchHeader("Authorization", "token d41d8cd98f00b204e9800998ecf8427e"). 112 | MatchHeader("Content-Type", "application/json"). 113 | HeaderPresent("Accept"). 114 | Reply(401). 115 | JSON(`{"message": "Bad credentials","documentation_url": "https://developer.github.com/v3"}`) 116 | 117 | client := &http.Client{Transport: &http.Transport{}} 118 | gock.InterceptClient(client) 119 | 120 | j := githubIssue{*client, config} 121 | 122 | _, err := j.Decorate(&map[string]any{"test": "test", "githubIssueId": int64(10000)}) 123 | 124 | assert.EqualError(t, err, `an error occurred when contacting remote api through https://api.github.com/repos/user/repository/issues/10000, status code 401, body {"message": "Bad credentials","documentation_url": "https://developer.github.com/v3"}`) 125 | assert.True(t, gock.IsDone(), "Must have no pending requests") 126 | } 127 | 128 | func TestGithubIssueWhenIssueIsNotFound(t *testing.T) { 129 | config := githubIssueConfig{} 130 | config.CREDENTIALS.OAUTHTOKEN = "d41d8cd98f00b204e9800998ecf8427e" 131 | config.CREDENTIALS.OWNER = "user" 132 | config.REPOSITORY.NAME = "repository" 133 | config.KEYS = map[string]struct { 134 | DESTKEY string 135 | FIELD string 136 | }{ 137 | "MILESTONE": { 138 | "milestoneCreator", 139 | "milestone.creator.id", 140 | }, 141 | "WHATEVER": { 142 | "whatever", 143 | "whatever", 144 | }, 145 | } 146 | 147 | defer gock.Off() 148 | 149 | gock.New("https://api.github.com/repos/user/repository/issues/10000"). 150 | MatchHeader("Authorization", "token d41d8cd98f00b204e9800998ecf8427e"). 151 | MatchHeader("Content-Type", "application/json"). 152 | HeaderPresent("Accept"). 153 | Reply(404). 154 | JSON(`{"message": "Not Found","documentation_url": "https://developer.github.com/v3"}`) 155 | 156 | client := &http.Client{Transport: &http.Transport{}} 157 | gock.InterceptClient(client) 158 | 159 | j := githubIssue{*client, config} 160 | 161 | result, err := j.Decorate(&map[string]any{"test": "test", "githubIssueId": int64(10000)}) 162 | 163 | expected := map[string]any{ 164 | "test": "test", 165 | "githubIssueId": int64(10000), 166 | } 167 | 168 | assert.NoError(t, err) 169 | assert.Equal(t, expected, *result) 170 | assert.True(t, gock.IsDone(), "Must have no pending requests") 171 | } 172 | -------------------------------------------------------------------------------- /chyle/config/api_decorator.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/antham/envh" 8 | ) 9 | 10 | // customAPIValidators defines validators called when last key of a key chain matches 11 | // a key defined in map 12 | var customAPIValidators = map[string]func(*envh.EnvTree, []string) error{ 13 | "URL": validateURL, 14 | } 15 | 16 | // codebeat:disable[TOO_MANY_IVARS] 17 | 18 | type apiDecoratorConfig struct { 19 | extractorKey string 20 | extractorDestKeyValue string 21 | decoratorKey string 22 | keysRef *map[string]struct { 23 | DESTKEY string 24 | FIELD string 25 | } 26 | mandatoryParamsRefs []ref 27 | featureRefs []*bool 28 | customValidationFuncs []func() error 29 | customSetterFuncs []func(*CHYLE) 30 | } 31 | 32 | // codebeat:enable[TOO_MANY_IVARS] 33 | 34 | // apiDecoratorConfigurator is a generic api 35 | // decorator configurator 36 | type apiDecoratorConfigurator struct { 37 | config *envh.EnvTree 38 | apiDecoratorConfig 39 | } 40 | 41 | func (a *apiDecoratorConfigurator) process(config *CHYLE) (bool, error) { 42 | if a.isDisabled() { 43 | return true, nil 44 | } 45 | 46 | for _, featureRef := range a.featureRefs { 47 | *featureRef = true 48 | } 49 | 50 | if err := a.validate(); err != nil { 51 | return true, err 52 | } 53 | 54 | a.set(config) 55 | 56 | return true, nil 57 | } 58 | 59 | func (a *apiDecoratorConfigurator) isDisabled() bool { 60 | return featureDisabled(a.config, [][]string{ 61 | {"CHYLE", "DECORATORS", a.decoratorKey}, 62 | {"CHYLE", "EXTRACTORS", a.extractorKey}, 63 | }) 64 | } 65 | 66 | func (a *apiDecoratorConfigurator) validate() error { 67 | for _, f := range append([]func() error{ 68 | a.validateMandatoryParameters, 69 | a.validateKeys, 70 | a.validateExtractor, 71 | }, a.customValidationFuncs...) { 72 | if err := f(); err != nil { 73 | return err 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (a *apiDecoratorConfigurator) set(config *CHYLE) { 81 | for _, f := range append([]func(*CHYLE){ 82 | a.setKeys, 83 | a.setMandatoryParameters, 84 | }, a.customSetterFuncs...) { 85 | f(config) 86 | } 87 | } 88 | 89 | // validateExtractor checks if an extractor is defined to get 90 | // data needed to contact remote api 91 | func (a *apiDecoratorConfigurator) validateExtractor() error { 92 | if err := validateEnvironmentVariablesDefinition( 93 | a.config, 94 | [][]string{ 95 | {"CHYLE", "EXTRACTORS", a.extractorKey, "ORIGKEY"}, 96 | {"CHYLE", "EXTRACTORS", a.extractorKey, "DESTKEY"}, 97 | {"CHYLE", "EXTRACTORS", a.extractorKey, "REG"}, 98 | }, 99 | ); err != nil { 100 | return err 101 | } 102 | 103 | if err := validateStringValue(a.extractorDestKeyValue, a.config, []string{"CHYLE", "EXTRACTORS", a.extractorKey, "DESTKEY"}); err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (a *apiDecoratorConfigurator) validateMandatoryParameters() error { 111 | keyChains := [][]string{} 112 | 113 | for _, ref := range a.mandatoryParamsRefs { 114 | keyChains = append(keyChains, ref.keyChain) 115 | } 116 | 117 | if err := validateEnvironmentVariablesDefinition(a.config, keyChains); err != nil { 118 | return err 119 | } 120 | 121 | return a.applyCustomValidators(&keyChains) 122 | } 123 | 124 | // applyCustomValidators applies validators defined in map customAPIValidators 125 | func (a *apiDecoratorConfigurator) applyCustomValidators(keyChains *[][]string) error { 126 | for _, keyChain := range *keyChains { 127 | f, ok := customAPIValidators[keyChain[len(keyChain)-1]] 128 | 129 | if !ok { 130 | continue 131 | } 132 | 133 | if err := f(a.config, keyChain); err != nil { 134 | return err 135 | } 136 | } 137 | 138 | return nil 139 | } 140 | 141 | // validateKeys checks key mapping between fields extracted from api and fields added to final struct 142 | func (a *apiDecoratorConfigurator) validateKeys() error { 143 | keys, err := a.config.FindChildrenKeys("CHYLE", "DECORATORS", a.decoratorKey, "KEYS") 144 | if err != nil { 145 | return EnvValidationError{fmt.Sprintf(`define at least one environment variable couple "CHYLE_DECORATORS_%s_KEYS_*_DESTKEY" and "CHYLE_DECORATORS_%s_KEYS_*_FIELD", replace "*" with your own naming`, a.decoratorKey, a.decoratorKey), strings.Join([]string{"CHYLE", "DECORATORS", a.decoratorKey, "KEYS"}, "_")} 146 | } 147 | 148 | for _, key := range keys { 149 | if err := validateEnvironmentVariablesDefinition(a.config, [][]string{{"CHYLE", "DECORATORS", a.decoratorKey, "KEYS", key, "DESTKEY"}, {"CHYLE", "DECORATORS", a.decoratorKey, "KEYS", key, "FIELD"}}); err != nil { 150 | return err 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | 157 | func (a *apiDecoratorConfigurator) setMandatoryParameters(config *CHYLE) { 158 | for _, c := range a.mandatoryParamsRefs { 159 | *(c.ref) = a.config.FindStringUnsecured(c.keyChain...) 160 | } 161 | } 162 | 163 | func (a *apiDecoratorConfigurator) setKeys(config *CHYLE) { 164 | ref := a.keysRef 165 | *ref = map[string]struct { 166 | DESTKEY string 167 | FIELD string 168 | }{} 169 | 170 | for _, key := range a.config.FindChildrenKeysUnsecured("CHYLE", "DECORATORS", a.decoratorKey, "KEYS") { 171 | (*ref)[key] = struct { 172 | DESTKEY string 173 | FIELD string 174 | }{ 175 | a.config.FindStringUnsecured("CHYLE", "DECORATORS", a.decoratorKey, "KEYS", key, "DESTKEY"), 176 | a.config.FindStringUnsecured("CHYLE", "DECORATORS", a.decoratorKey, "KEYS", key, "FIELD"), 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /chyle/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/go-git/go-git/v5" 8 | "github.com/go-git/go-git/v5/plumbing" 9 | "github.com/go-git/go-git/v5/plumbing/object" 10 | ) 11 | 12 | // node is a tree node in commit tree 13 | type node struct { 14 | value *object.Commit 15 | parent *node 16 | } 17 | 18 | // errNoDiffBetweenReferences is triggered when we can't 19 | // produce any diff between 2 references 20 | type errNoDiffBetweenReferences struct { 21 | from string 22 | to string 23 | } 24 | 25 | func (e errNoDiffBetweenReferences) Error() string { 26 | return fmt.Sprintf(`can't produce a diff between %s and %s, check your range is correct by running "git log %[1]s..%[2]s" command`, e.from, e.to) 27 | } 28 | 29 | // errRepositoryPath is triggered when repository path can't be opened 30 | type errRepositoryPath struct { 31 | path string 32 | } 33 | 34 | func (e errRepositoryPath) Error() string { 35 | return fmt.Sprintf(`check "%s" is an existing git repository path`, e.path) 36 | } 37 | 38 | // errReferenceNotFound is triggered when reference can't be 39 | // found in git repository 40 | type errReferenceNotFound struct { 41 | ref string 42 | } 43 | 44 | func (e errReferenceNotFound) Error() string { 45 | return fmt.Sprintf(`reference "%s" can't be found in git repository`, e.ref) 46 | } 47 | 48 | // errBrowsingTree is triggered when something wrong occurred during commit analysis process 49 | var errBrowsingTree = fmt.Errorf("an issue occurred during tree analysis") 50 | 51 | // resolveRef gives hash commit for a given string reference 52 | func resolveRef(refCommit string, repository *git.Repository) (*object.Commit, error) { 53 | hash := plumbing.Hash{} 54 | 55 | if strings.ToLower(refCommit) == "head" { 56 | head, err := repository.Head() 57 | 58 | if err == nil { 59 | return repository.CommitObject(head.Hash()) 60 | } 61 | } 62 | 63 | iter, err := repository.References() 64 | if err != nil { 65 | return &object.Commit{}, errReferenceNotFound{refCommit} 66 | } 67 | 68 | err = iter.ForEach(func(ref *plumbing.Reference) error { 69 | if ref.Name().Short() == refCommit { 70 | hash = ref.Hash() 71 | } 72 | 73 | return nil 74 | }) 75 | 76 | if err == nil && !hash.IsZero() { 77 | return repository.CommitObject(hash) 78 | } 79 | 80 | hash = plumbing.NewHash(refCommit) 81 | 82 | if !hash.IsZero() { 83 | return repository.CommitObject(hash) 84 | } 85 | 86 | return &object.Commit{}, errReferenceNotFound{refCommit} 87 | } 88 | 89 | // FetchCommits retrieves commits in a reference range 90 | func FetchCommits(repoPath string, fromRef string, toRef string) (*[]object.Commit, error) { 91 | rep, err := git.PlainOpen(repoPath) 92 | if err != nil { 93 | return nil, errRepositoryPath{repoPath} 94 | } 95 | 96 | fromCommit, err := resolveRef(fromRef, rep) 97 | if err != nil { 98 | return &[]object.Commit{}, err 99 | } 100 | 101 | toCommit, err := resolveRef(toRef, rep) 102 | if err != nil { 103 | return &[]object.Commit{}, err 104 | } 105 | 106 | var ok bool 107 | var commits *[]object.Commit 108 | 109 | exclusionList, err := buildOriginCommitList(fromCommit) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | if _, ok = exclusionList[toCommit.ID().String()]; ok { 115 | return nil, errNoDiffBetweenReferences{fromRef, toRef} 116 | } 117 | 118 | commits, err = findDiffCommits(toCommit, &exclusionList) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | if len(*commits) == 0 { 124 | return nil, errNoDiffBetweenReferences{fromRef, toRef} 125 | } 126 | 127 | return commits, nil 128 | } 129 | 130 | // buildOriginCommitList browses git tree from a given commit 131 | // till root commit using kind of breadth first search algorithm 132 | // and grab commit ID to a map with ID as key 133 | func buildOriginCommitList(commit *object.Commit) (map[string]bool, error) { 134 | queue := append([]*object.Commit{}, commit) 135 | seen := map[string]bool{commit.ID().String(): true} 136 | 137 | for len(queue) > 0 { 138 | current := queue[0] 139 | queue = append([]*object.Commit{}, queue[1:]...) 140 | 141 | err := current.Parents().ForEach( 142 | func(c *object.Commit) error { 143 | if _, ok := seen[c.ID().String()]; !ok { 144 | seen[c.ID().String()] = true 145 | queue = append(queue, c) 146 | } 147 | 148 | return nil 149 | }) 150 | 151 | if err != nil && err.Error() != plumbing.ErrObjectNotFound.Error() { 152 | return seen, errBrowsingTree 153 | } 154 | } 155 | 156 | return seen, nil 157 | } 158 | 159 | // findDiffCommits extracts commits that are no part of a given commit list 160 | // using kind of depth first search algorithm to keep commits ordered 161 | func findDiffCommits(commit *object.Commit, exclusionList *map[string]bool) (*[]object.Commit, error) { 162 | commits := []object.Commit{} 163 | queue := append([]*node{}, &node{value: commit}) 164 | seen := map[string]bool{commit.ID().String(): true} 165 | var current *node 166 | 167 | for len(queue) > 0 { 168 | current = queue[0] 169 | queue = append([]*node{}, queue[1:]...) 170 | 171 | if _, ok := (*exclusionList)[current.value.ID().String()]; !ok { 172 | commits = append(commits, *(current.value)) 173 | } 174 | 175 | err := current.value.Parents().ForEach( 176 | func(c *object.Commit) error { 177 | if _, ok := seen[c.ID().String()]; !ok { 178 | seen[c.ID().String()] = true 179 | n := &node{value: c, parent: current} 180 | queue = append([]*node{n}, queue...) 181 | } 182 | 183 | return nil 184 | }) 185 | 186 | if err != nil && err.Error() != plumbing.ErrObjectNotFound.Error() { 187 | return &commits, errBrowsingTree 188 | } 189 | } 190 | 191 | return &commits, nil 192 | } 193 | -------------------------------------------------------------------------------- /chyle/decorators/decorator_test.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/h2non/gock.v0" 9 | 10 | "github.com/antham/chyle/chyle/types" 11 | ) 12 | 13 | func TestDecorator(t *testing.T) { 14 | config := jiraIssueConfig{} 15 | config.CREDENTIALS.USERNAME = "test" 16 | config.CREDENTIALS.PASSWORD = "test" 17 | config.ENDPOINT.URL = "http://test.com" 18 | config.KEYS = map[string]struct { 19 | DESTKEY string 20 | FIELD string 21 | }{ 22 | "KEY": { 23 | "jiraIssueKey", 24 | "key", 25 | }, 26 | } 27 | 28 | defer gock.Off() 29 | 30 | gock.New("http://test.com/rest/api/2/issue/10000"). 31 | Reply(200). 32 | BodyString(`{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://test.com/jira/rest/api/2/issue/10000","key":"EX-1","names":{"watcher":"watcher","attachment":"attachment","sub-tasks":"sub-tasks","description":"description","project":"project","comment":"comment","issuelinks":"issuelinks","worklog":"worklog","updated":"updated","timetracking":"timetracking" }}`) 33 | 34 | gock.New("http://test.com/rest/api/2/issue/ABC-123"). 35 | Reply(200). 36 | BodyString(`{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10001","self":"http://test.com/jira/rest/api/2/issue/10001","key":"ABC-123","names":{"watcher":"watcher","attachment":"attachment","sub-tasks":"sub-tasks","description":"description","project":"project","comment":"comment","issuelinks":"issuelinks","worklog":"worklog","updated":"updated","timetracking":"timetracking" }}`) 37 | 38 | client := &http.Client{Transport: &http.Transport{}} 39 | gock.InterceptClient(client) 40 | 41 | j := jiraIssue{*client, config} 42 | 43 | decorators := map[string][]Decorater{ 44 | "datas": {j}, 45 | "metadatas": {}, 46 | } 47 | 48 | changelog := types.Changelog{ 49 | Datas: []map[string]any{ 50 | { 51 | "test": "test1", 52 | "jiraIssueId": "10000", 53 | }, 54 | { 55 | "test": "test2", 56 | "jiraIssueId": "ABC-123", 57 | }, 58 | }, 59 | Metadatas: map[string]any{}, 60 | } 61 | 62 | result, err := Decorate(&decorators, &changelog) 63 | 64 | expected := types.Changelog{ 65 | Datas: []map[string]any{ 66 | { 67 | "test": "test1", 68 | "jiraIssueId": "10000", 69 | "jiraIssueKey": "EX-1", 70 | }, 71 | { 72 | "test": "test2", 73 | "jiraIssueId": "ABC-123", 74 | "jiraIssueKey": "ABC-123", 75 | }, 76 | }, 77 | Metadatas: map[string]any{}, 78 | } 79 | 80 | assert.NoError(t, err) 81 | assert.Equal(t, expected, *result) 82 | assert.True(t, gock.IsDone(), "Must have no pending requests") 83 | } 84 | 85 | func TestCreateDataDecorators(t *testing.T) { 86 | tests := []func() (Features, Config){ 87 | func() (Features, Config) { 88 | config := customAPIConfig{} 89 | config.CREDENTIALS.TOKEN = "da39a3ee5e6b4b0d3255bfef95601890afd80709" 90 | config.ENDPOINT.URL = "http://test.com" 91 | config.KEYS = map[string]struct { 92 | DESTKEY string 93 | FIELD string 94 | }{ 95 | "DESCRIPTION": { 96 | "githubTicketDescription", 97 | "fields.summary", 98 | }, 99 | } 100 | 101 | return Features{ENABLED: true, CUSTOMAPI: true}, Config{CUSTOMAPI: config} 102 | }, 103 | func() (Features, Config) { 104 | config := jiraIssueConfig{} 105 | config.CREDENTIALS.USERNAME = "test" 106 | config.CREDENTIALS.PASSWORD = "test" 107 | config.ENDPOINT.URL = "http://test.com" 108 | config.KEYS = map[string]struct { 109 | DESTKEY string 110 | FIELD string 111 | }{ 112 | "DESCRIPTION": { 113 | "jiraTicketDescription", 114 | "fields.summary", 115 | }, 116 | } 117 | 118 | return Features{ENABLED: true, JIRAISSUE: true}, Config{JIRAISSUE: config} 119 | }, 120 | func() (Features, Config) { 121 | config := githubIssueConfig{} 122 | config.CREDENTIALS.OWNER = "test" 123 | config.CREDENTIALS.OAUTHTOKEN = "test" 124 | config.REPOSITORY.NAME = "test" 125 | config.KEYS = map[string]struct { 126 | DESTKEY string 127 | FIELD string 128 | }{ 129 | "DESCRIPTION": { 130 | "githubTicketDescription", 131 | "fields.summary", 132 | }, 133 | } 134 | 135 | return Features{ENABLED: true, GITHUBISSUE: true}, Config{GITHUBISSUE: config} 136 | }, 137 | func() (Features, Config) { 138 | config := shellConfig{ 139 | "TEST": { 140 | `tr -s "a" "b"`, 141 | "message", 142 | "messageTransformed", 143 | }, 144 | } 145 | 146 | return Features{ENABLED: true, SHELL: true}, Config{SHELL: config} 147 | }, 148 | } 149 | 150 | for _, f := range tests { 151 | features, config := f() 152 | 153 | d := Create(features, config) 154 | 155 | assert.Len(t, (*d)["datas"], 1) 156 | assert.Len(t, (*d)["metadatas"], 0) 157 | } 158 | } 159 | 160 | func TestCreateMetadataDecorators(t *testing.T) { 161 | tests := []func() (Features, Config){ 162 | func() (Features, Config) { 163 | config := envConfig{ 164 | "TEST": { 165 | "TEST", 166 | "test", 167 | }, 168 | } 169 | 170 | return Features{ENABLED: true, ENV: true}, Config{ENV: config} 171 | }, 172 | } 173 | 174 | for _, f := range tests { 175 | features, config := f() 176 | 177 | d := Create(features, config) 178 | 179 | assert.Len(t, (*d)["datas"], 0) 180 | assert.Len(t, (*d)["metadatas"], 1) 181 | } 182 | } 183 | 184 | func TestCreateWithFeatureDisabled(t *testing.T) { 185 | config := jiraIssueConfig{} 186 | config.CREDENTIALS.USERNAME = "test" 187 | config.CREDENTIALS.PASSWORD = "test" 188 | config.ENDPOINT.URL = "http://test.com" 189 | config.KEYS = map[string]struct { 190 | DESTKEY string 191 | FIELD string 192 | }{ 193 | "DESCRIPTION": { 194 | "jiraTicketDescription", 195 | "fields.summary", 196 | }, 197 | } 198 | 199 | d := Create(Features{JIRAISSUE: true}, Config{JIRAISSUE: config}) 200 | 201 | assert.Len(t, (*d)["datas"], 0) 202 | assert.Len(t, (*d)["metadatas"], 0) 203 | } 204 | -------------------------------------------------------------------------------- /chyle/chyle_test.go: -------------------------------------------------------------------------------- 1 | package chyle 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/antham/envh" 13 | ) 14 | 15 | func TestBuildChangelog(t *testing.T) { 16 | restoreEnvs() 17 | p, err := os.Getwd() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | setenv("CHYLE_GIT_REPOSITORY_PATH", p+"/testing-repository") 23 | setenv("CHYLE_GIT_REFERENCE_FROM", "test2") 24 | setenv("CHYLE_GIT_REFERENCE_TO", "head") 25 | setenv("CHYLE_MATCHERS_TYPE", "regular") 26 | setenv("CHYLE_EXTRACTORS_MESSAGE_ORIGKEY", "message") 27 | setenv("CHYLE_EXTRACTORS_MESSAGE_DESTKEY", "subject") 28 | setenv("CHYLE_EXTRACTORS_MESSAGE_REG", "(.{1,50})") 29 | setenv("CHYLE_SENDERS_STDOUT_FORMAT", "json") 30 | 31 | f, err := os.CreateTemp(p+"/testing-repository", "test") 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | defer func() { 37 | if err = f.Close(); err != nil { 38 | log.Fatal(err) 39 | } 40 | }() 41 | 42 | config, err := envh.NewEnvTree("CHYLE", "_") 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | oldStdout := os.Stdout 48 | os.Stdout = f 49 | 50 | err = BuildChangelog(&config) 51 | 52 | os.Stdout = oldStdout 53 | 54 | assert.NoError(t, err) 55 | 56 | b, err := os.ReadFile(f.Name()) 57 | 58 | assert.NoError(t, err) 59 | 60 | type Data struct { 61 | ID string `json:"id"` 62 | AuthorDate string `json:"authorDate"` 63 | AuthorEmail string `json:"authorEmail"` 64 | AuthorName string `json:"authorName"` 65 | Type string `json:"type"` 66 | CommitterEmail string `json:"committerEmail"` 67 | CommitterName string `json:"committerName"` 68 | Message string `json:"message"` 69 | Subject string `json:"subject"` 70 | } 71 | 72 | results := struct { 73 | Datas []Data `json:"datas"` 74 | Metadatas map[string]string `json:"metadatas"` 75 | }{} 76 | 77 | j := json.NewDecoder(bytes.NewBuffer(b)) 78 | err = j.Decode(&results) 79 | 80 | assert.NoError(t, err) 81 | assert.Len(t, results.Datas, 2) 82 | assert.Len(t, results.Metadatas, 0) 83 | 84 | expected := []map[string]string{ 85 | { 86 | "authorEmail": "whatever@example.com", 87 | "authorName": "whatever", 88 | "committerEmail": "whatever@example.com", 89 | "committerName": "whatever", 90 | "type": "regular", 91 | "message": "feat(file8) : new file 8\n\ncreate a new file 8\n", 92 | "subject": "feat(file8) : new file 8", 93 | }, 94 | { 95 | "authorEmail": "whatever@example.com", 96 | "authorName": "whatever", 97 | "committerEmail": "whatever@example.com", 98 | "committerName": "whatever", 99 | "type": "regular", 100 | "message": "feat(file7) : new file 7\n\ncreate a new file 7\n", 101 | "subject": "feat(file7) : new file 7", 102 | }, 103 | } 104 | 105 | for i, r := range results.Datas { 106 | assert.Equal(t, expected[i]["authorEmail"], r.AuthorEmail) 107 | assert.Equal(t, expected[i]["authorName"], r.AuthorName) 108 | assert.Equal(t, expected[i]["type"], r.Type) 109 | assert.Equal(t, expected[i]["committerEmail"], r.CommitterEmail) 110 | assert.Equal(t, expected[i]["committerName"], r.CommitterName) 111 | assert.Equal(t, expected[i]["message"], r.Message) 112 | assert.Equal(t, expected[i]["subject"], r.Subject) 113 | } 114 | } 115 | 116 | func TestBuildChangelogWithAnErrorFromGitPackage(t *testing.T) { 117 | restoreEnvs() 118 | p, err := os.Getwd() 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | 123 | setenv("CHYLE_GIT_REPOSITORY_PATH", p+"/whatever") 124 | setenv("CHYLE_GIT_REFERENCE_FROM", "test2") 125 | setenv("CHYLE_GIT_REFERENCE_TO", "head") 126 | setenv("CHYLE_MATCHERS_TYPE", "regular") 127 | setenv("CHYLE_EXTRACTORS_MESSAGE_ORIGKEY", "message") 128 | setenv("CHYLE_EXTRACTORS_MESSAGE_DESTKEY", "subject") 129 | setenv("CHYLE_EXTRACTORS_MESSAGE_REG", "(.{1,50})") 130 | setenv("CHYLE_SENDERS_STDOUT_FORMAT", "json") 131 | 132 | config, err := envh.NewEnvTree("CHYLE", "_") 133 | if err != nil { 134 | log.Fatal(err) 135 | } 136 | 137 | err = BuildChangelog(&config) 138 | 139 | assert.Error(t, err) 140 | assert.Regexp(t, `check ".*?" is an existing git repository path`, err.Error()) 141 | } 142 | 143 | func TestBuildChangelogWithAnErrorFromConfigPackage(t *testing.T) { 144 | restoreEnvs() 145 | p, err := os.Getwd() 146 | if err != nil { 147 | log.Fatal(err) 148 | } 149 | 150 | setenv("CHYLE_GIT_REPOSITORY_PATH", p+"/testing-repository") 151 | setenv("CHYLE_GIT_REFERENCE_FROM", "test2") 152 | setenv("CHYLE_GIT_REFERENCE_TO", "head") 153 | setenv("CHYLE_SENDERS_STDOUT_FORMAT", "whatever") 154 | 155 | config, err := envh.NewEnvTree("CHYLE", "_") 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | 160 | err = BuildChangelog(&config) 161 | 162 | assert.Error(t, err) 163 | assert.EqualError(t, err, `"CHYLE_SENDERS_STDOUT_FORMAT" "whatever" doesn't exist`) 164 | } 165 | 166 | func TestBuildChangelogWithDebuggingEnabled(t *testing.T) { 167 | restoreEnvs() 168 | p, err := os.Getwd() 169 | if err != nil { 170 | log.Fatal(err) 171 | } 172 | 173 | EnableDebugging = true 174 | 175 | setenv("CHYLE_GIT_REPOSITORY_PATH", p+"/testing-repository") 176 | setenv("CHYLE_GIT_REFERENCE_FROM", "test2") 177 | setenv("CHYLE_GIT_REFERENCE_TO", "head") 178 | setenv("CHYLE_MATCHERS_TYPE", "regular") 179 | setenv("CHYLE_EXTRACTORS_MESSAGE_ORIGKEY", "message") 180 | setenv("CHYLE_EXTRACTORS_MESSAGE_DESTKEY", "subject") 181 | setenv("CHYLE_EXTRACTORS_MESSAGE_REG", "(.{1,50})") 182 | 183 | config, err := envh.NewEnvTree("CHYLE", "_") 184 | if err != nil { 185 | log.Fatal(err) 186 | } 187 | 188 | assert.NoError(t, err) 189 | 190 | tmpLogger := logger 191 | 192 | b := []byte{} 193 | buffer := bytes.NewBuffer(b) 194 | 195 | logger = log.New(buffer, "CHYLE - ", log.Ldate|log.Ltime) 196 | 197 | err = BuildChangelog(&config) 198 | assert.NoError(t, err) 199 | 200 | logger = tmpLogger 201 | 202 | EnableDebugging = false 203 | 204 | assert.Regexp(t, `CHYLE - \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} {\n\s+"GIT": {\n\s+"REPOSITORY": {\n`, buffer.String()) 205 | } 206 | -------------------------------------------------------------------------------- /chyle/decorators/fixtures/github-issue-fetch-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", 4 | "repository_url": "https://api.github.com/repos/octocat/Hello-World", 5 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}", 6 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", 7 | "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events", 8 | "html_url": "https://github.com/octocat/Hello-World/issues/1347", 9 | "number": 1347, 10 | "state": "open", 11 | "title": "Found a bug", 12 | "body": "I'm having a problem with this.", 13 | "user": { 14 | "login": "octocat", 15 | "id": 1, 16 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 17 | "gravatar_id": "", 18 | "url": "https://api.github.com/users/octocat", 19 | "html_url": "https://github.com/octocat", 20 | "followers_url": "https://api.github.com/users/octocat/followers", 21 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 22 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 23 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 24 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 25 | "organizations_url": "https://api.github.com/users/octocat/orgs", 26 | "repos_url": "https://api.github.com/users/octocat/repos", 27 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 28 | "received_events_url": "https://api.github.com/users/octocat/received_events", 29 | "type": "User", 30 | "site_admin": false 31 | }, 32 | "labels": [ 33 | { 34 | "id": 208045946, 35 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 36 | "name": "bug", 37 | "color": "f29513", 38 | "default": true 39 | } 40 | ], 41 | "assignee": { 42 | "login": "octocat", 43 | "id": 1, 44 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 45 | "gravatar_id": "", 46 | "url": "https://api.github.com/users/octocat", 47 | "html_url": "https://github.com/octocat", 48 | "followers_url": "https://api.github.com/users/octocat/followers", 49 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 50 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 51 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 52 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 53 | "organizations_url": "https://api.github.com/users/octocat/orgs", 54 | "repos_url": "https://api.github.com/users/octocat/repos", 55 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 56 | "received_events_url": "https://api.github.com/users/octocat/received_events", 57 | "type": "User", 58 | "site_admin": false 59 | }, 60 | "milestone": { 61 | "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", 62 | "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", 63 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", 64 | "id": 1002604, 65 | "number": 1, 66 | "state": "open", 67 | "title": "v1.0", 68 | "description": "Tracking milestone for version 1.0", 69 | "creator": { 70 | "login": "octocat", 71 | "id": 1, 72 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 73 | "gravatar_id": "", 74 | "url": "https://api.github.com/users/octocat", 75 | "html_url": "https://github.com/octocat", 76 | "followers_url": "https://api.github.com/users/octocat/followers", 77 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 78 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 79 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 80 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 81 | "organizations_url": "https://api.github.com/users/octocat/orgs", 82 | "repos_url": "https://api.github.com/users/octocat/repos", 83 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 84 | "received_events_url": "https://api.github.com/users/octocat/received_events", 85 | "type": "User", 86 | "site_admin": false 87 | }, 88 | "open_issues": 4, 89 | "closed_issues": 8, 90 | "created_at": "2011-04-10T20:09:31Z", 91 | "updated_at": "2014-03-03T18:58:10Z", 92 | "closed_at": "2013-02-12T13:22:01Z", 93 | "due_on": "2012-10-09T23:39:01Z" 94 | }, 95 | "locked": false, 96 | "comments": 0, 97 | "pull_request": { 98 | "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", 99 | "html_url": "https://github.com/octocat/Hello-World/pull/1347", 100 | "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", 101 | "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch" 102 | }, 103 | "closed_at": null, 104 | "created_at": "2011-04-22T13:33:48Z", 105 | "updated_at": "2011-04-22T13:33:48Z", 106 | "closed_by": { 107 | "login": "octocat", 108 | "id": 1, 109 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 110 | "gravatar_id": "", 111 | "url": "https://api.github.com/users/octocat", 112 | "html_url": "https://github.com/octocat", 113 | "followers_url": "https://api.github.com/users/octocat/followers", 114 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 115 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 116 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 117 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 118 | "organizations_url": "https://api.github.com/users/octocat/orgs", 119 | "repos_url": "https://api.github.com/users/octocat/repos", 120 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 121 | "received_events_url": "https://api.github.com/users/octocat/received_events", 122 | "type": "User", 123 | "site_admin": false 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /chyle/decorators/jira_issue_test.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/h2non/gock.v0" 9 | ) 10 | 11 | func TestJira(t *testing.T) { 12 | config := jiraIssueConfig{} 13 | config.CREDENTIALS.USERNAME = "test" 14 | config.CREDENTIALS.PASSWORD = "test" 15 | config.ENDPOINT.URL = "http://test.com" 16 | config.KEYS = map[string]struct { 17 | DESTKEY string 18 | FIELD string 19 | }{ 20 | "ISSUEKEY": { 21 | "jiraIssueKey", 22 | "key", 23 | }, 24 | "WHATEVER": { 25 | "whatever", 26 | "whatever", 27 | }, 28 | } 29 | 30 | defer gock.Off() 31 | 32 | gock.New("http://test.com/rest/api/2/issue/10000"). 33 | Reply(200). 34 | JSON(`{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://test.com/jira/rest/api/2/issue/10000","key":"EX-1","names":{"watcher":"watcher","attachment":"attachment","sub-tasks":"sub-tasks","description":"description","project":"project","comment":"comment","issuelinks":"issuelinks","worklog":"worklog","updated":"updated","timetracking":"timetracking" }}`) 35 | 36 | gock.New("http://test.com/rest/api/2/issue/EX-1"). 37 | Reply(200). 38 | JSON(`{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://test.com/jira/rest/api/2/issue/10000","key":"EX-1","names":{"watcher":"watcher","attachment":"attachment","sub-tasks":"sub-tasks","description":"description","project":"project","comment":"comment","issuelinks":"issuelinks","worklog":"worklog","updated":"updated","timetracking":"timetracking" }}`) 39 | 40 | client := &http.Client{Transport: &http.Transport{}} 41 | gock.InterceptClient(client) 42 | 43 | j := jiraIssue{*client, config} 44 | 45 | // request with issue id 46 | result, err := j.Decorate(&map[string]any{"test": "test", "jiraIssueId": int64(10000)}) 47 | 48 | expected := map[string]any{ 49 | "test": "test", 50 | "jiraIssueId": int64(10000), 51 | "jiraIssueKey": "EX-1", 52 | } 53 | 54 | assert.NoError(t, err) 55 | assert.Equal(t, expected, *result) 56 | 57 | // request with issue key 58 | result, err = j.Decorate(&map[string]any{"test": "test", "jiraIssueId": "EX-1"}) 59 | 60 | expected = map[string]any{ 61 | "test": "test", 62 | "jiraIssueId": "EX-1", 63 | "jiraIssueKey": "EX-1", 64 | } 65 | 66 | assert.NoError(t, err) 67 | assert.Equal(t, expected, *result) 68 | assert.True(t, gock.IsDone(), "Must have no pending requests") 69 | } 70 | 71 | func TestJiraWithNoJiraIssueIdDefined(t *testing.T) { 72 | defer gock.Off() 73 | 74 | gock.New("http://test.com/rest/api/2/issue/10000"). 75 | Reply(200). 76 | JSON(`{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://test.com/jira/rest/api/2/issue/10000","key":"EX-1","names":{"watcher":"watcher","attachment":"attachment","sub-tasks":"sub-tasks","description":"description","project":"project","comment":"comment","issuelinks":"issuelinks","worklog":"worklog","updated":"updated","timetracking":"timetracking" }}`) 77 | 78 | client := &http.Client{Transport: &http.Transport{}} 79 | gock.InterceptClient(client) 80 | 81 | j := jiraIssue{*client, jiraIssueConfig{}} 82 | 83 | result, err := j.Decorate(&map[string]any{"test": "test"}) 84 | 85 | expected := map[string]any{ 86 | "test": "test", 87 | } 88 | 89 | assert.NoError(t, err, "Must return no errors") 90 | assert.Equal(t, expected, *result, "Must return same struct than the one submitted") 91 | assert.False(t, gock.IsDone(), "Must have one pending request") 92 | } 93 | 94 | func TestJiraWhenIssueIsNotFound(t *testing.T) { 95 | config := jiraIssueConfig{} 96 | config.CREDENTIALS.USERNAME = "test" 97 | config.CREDENTIALS.PASSWORD = "test" 98 | config.ENDPOINT.URL = "http://test.com" 99 | config.KEYS = map[string]struct { 100 | DESTKEY string 101 | FIELD string 102 | }{ 103 | "ISSUEKEY": { 104 | "jiraIssueKey", 105 | "key", 106 | }, 107 | "WHATEVER": { 108 | "whatever", 109 | "whatever", 110 | }, 111 | } 112 | 113 | defer gock.Off() 114 | 115 | gock.New("http://test.com/rest/api/2/issue/10000"). 116 | Reply(404). 117 | JSON(`{"errorMessages":["Issue does not exist or you do not have permission to see it."],"errors":{}}`) 118 | 119 | client := &http.Client{Transport: &http.Transport{}} 120 | gock.InterceptClient(client) 121 | 122 | j := jiraIssue{*client, config} 123 | 124 | result, err := j.Decorate(&map[string]any{"test": "test", "jiraIssueId": int64(10000)}) 125 | 126 | expected := map[string]any{ 127 | "test": "test", 128 | "jiraIssueId": int64(10000), 129 | } 130 | 131 | assert.NoError(t, err) 132 | assert.Equal(t, expected, *result) 133 | assert.True(t, gock.IsDone(), "Must have no pending requests") 134 | } 135 | 136 | func TestJiraWithJiraIssueIdMissing(t *testing.T) { 137 | config := jiraIssueConfig{} 138 | config.CREDENTIALS.USERNAME = "test" 139 | config.CREDENTIALS.PASSWORD = "test" 140 | config.ENDPOINT.URL = "http://test.com" 141 | config.KEYS = map[string]struct { 142 | DESTKEY string 143 | FIELD string 144 | }{ 145 | "ISSUEKEY": { 146 | "jiraIssueKey", 147 | "key", 148 | }, 149 | "WHATEVER": { 150 | "whatever", 151 | "whatever", 152 | }, 153 | } 154 | 155 | client := &http.Client{Transport: &http.Transport{}} 156 | gock.InterceptClient(client) 157 | 158 | j := jiraIssue{*client, config} 159 | 160 | result, err := j.Decorate(&map[string]any{"test": "test"}) 161 | 162 | expected := map[string]any{ 163 | "test": "test", 164 | } 165 | 166 | assert.NoError(t, err) 167 | assert.Equal(t, expected, *result) 168 | } 169 | 170 | func TestJiraWithJiraIssueIdEmpty(t *testing.T) { 171 | config := jiraIssueConfig{} 172 | config.CREDENTIALS.USERNAME = "test" 173 | config.CREDENTIALS.PASSWORD = "test" 174 | config.ENDPOINT.URL = "http://test.com" 175 | config.KEYS = map[string]struct { 176 | DESTKEY string 177 | FIELD string 178 | }{ 179 | "ISSUEKEY": { 180 | "jiraIssueKey", 181 | "key", 182 | }, 183 | "WHATEVER": { 184 | "whatever", 185 | "whatever", 186 | }, 187 | } 188 | 189 | client := &http.Client{Transport: &http.Transport{}} 190 | gock.InterceptClient(client) 191 | 192 | j := jiraIssue{*client, config} 193 | 194 | result, err := j.Decorate(&map[string]any{"test": "test", "jiraIssueId": ""}) 195 | 196 | expected := map[string]any{ 197 | "test": "test", 198 | "jiraIssueId": "", 199 | } 200 | 201 | assert.NoError(t, err) 202 | assert.Equal(t, expected, *result) 203 | } 204 | -------------------------------------------------------------------------------- /chyle/senders/github_release_test.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "gopkg.in/h2non/gock.v0" 11 | 12 | "github.com/antham/chyle/chyle/types" 13 | ) 14 | 15 | func TestGithubReleaseCreateRelease(t *testing.T) { 16 | config := githubReleaseConfig{} 17 | config.RELEASE.TEMPLATE = "{{ range $key, $value := .Datas }}{{$value.test}}{{ end }}" 18 | config.RELEASE.TAGNAME = "v1.0.0" 19 | config.RELEASE.NAME = "TEST" 20 | config.CREDENTIALS.OWNER = "user" 21 | config.REPOSITORY.NAME = "test" 22 | config.CREDENTIALS.OAUTHTOKEN = "d41d8cd98f00b204e9800998ecf8427e" 23 | 24 | defer gock.Off() 25 | 26 | tagCreationResponse, err := os.ReadFile("fixtures/github-tag-creation-response.json") 27 | 28 | assert.NoError(t, err, "Must read json fixture file") 29 | 30 | gock.New("https://api.github.com"). 31 | Post("/repos/user/test/releases"). 32 | MatchHeader("Authorization", "token d41d8cd98f00b204e9800998ecf8427e"). 33 | MatchHeader("Content-Type", "application/json"). 34 | HeaderPresent("Accept"). 35 | JSON(githubReleasePayload{TagName: "v1.0.0", Name: "TEST", Body: "Hello world !"}). 36 | Reply(201). 37 | JSON(string(tagCreationResponse)) 38 | 39 | client := &http.Client{Transport: &http.Transport{}} 40 | gock.InterceptClient(client) 41 | 42 | s := newGithubRelease(config).(githubRelease) 43 | s.client = client 44 | 45 | c := types.Changelog{ 46 | Datas: []map[string]any{}, 47 | Metadatas: map[string]any{}, 48 | } 49 | 50 | c.Datas = append(c.Datas, map[string]any{"test": "Hello world !"}) 51 | 52 | err = s.Send(&c) 53 | 54 | assert.NoError(t, err, "Must return no errors") 55 | assert.True(t, gock.IsDone(), "Must have no pending requests") 56 | } 57 | 58 | func TestGithubReleaseCreateReleaseWithWrongCredentials(t *testing.T) { 59 | config := githubReleaseConfig{} 60 | config.RELEASE.TEMPLATE = "{{ range $key, $value := .Datas }}{{$value.test}}{{ end }}" 61 | config.RELEASE.TAGNAME = "v1.0.0" 62 | config.RELEASE.NAME = "TEST" 63 | config.CREDENTIALS.OWNER = "test" 64 | config.REPOSITORY.NAME = "test" 65 | config.CREDENTIALS.OAUTHTOKEN = "d0b934ea223577f7e5cc6599e40b1822" 66 | 67 | defer gock.Off() 68 | 69 | gock.New("https://api.github.com"). 70 | Post("/repos/test/test/releases"). 71 | MatchHeader("Authorization", "token d0b934ea223577f7e5cc6599e40b1822"). 72 | MatchHeader("Content-Type", "application/json"). 73 | HeaderPresent("Accept"). 74 | JSON(githubReleasePayload{TagName: "v1.0.0", Name: "TEST", Body: "Hello world !"}). 75 | ReplyError(fmt.Errorf("an error occurred")) 76 | 77 | client := &http.Client{Transport: &http.Transport{}} 78 | gock.InterceptClient(client) 79 | 80 | s := newGithubRelease(config).(githubRelease) 81 | s.client = client 82 | 83 | c := types.Changelog{ 84 | Datas: []map[string]any{}, 85 | Metadatas: map[string]any{}, 86 | } 87 | 88 | c.Datas = append(c.Datas, map[string]any{"test": "Hello world !"}) 89 | 90 | err := s.Send(&c) 91 | 92 | assert.EqualError(t, err, `can't create github release : Post "https://api.github.com/repos/test/test/releases": an error occurred`, "Must return an error when api response something wrong") 93 | assert.True(t, gock.IsDone(), "Must have no pending requests") 94 | } 95 | 96 | func TestGithubReleaseUpdateRelease(t *testing.T) { 97 | config := githubReleaseConfig{} 98 | config.RELEASE.TEMPLATE = "{{ range $key, $value := .Datas }}{{$value.test}}{{ end }}" 99 | config.RELEASE.TAGNAME = "v1.0.0" 100 | config.RELEASE.NAME = "TEST" 101 | config.CREDENTIALS.OWNER = "test" 102 | config.REPOSITORY.NAME = "test" 103 | config.CREDENTIALS.OAUTHTOKEN = "d41d8cd98f00b204e9800998ecf8427e" 104 | config.RELEASE.UPDATE = true 105 | 106 | defer gock.Off() 107 | 108 | fetchReleaseResponse, err := os.ReadFile("fixtures/github-release-fetch-response.json") 109 | 110 | assert.NoError(t, err, "Must read json fixture file") 111 | 112 | gock.New("https://api.github.com"). 113 | Get("/repos/test/test/releases/tags/v1.0.0"). 114 | MatchHeader("Authorization", "token d41d8cd98f00b204e9800998ecf8427e"). 115 | MatchHeader("Content-Type", "application/json"). 116 | HeaderPresent("Accept"). 117 | Reply(200). 118 | JSON(string(fetchReleaseResponse)) 119 | 120 | gock.New("https://api.github.com"). 121 | Patch("/repos/test/test/releases/1"). 122 | MatchHeader("Authorization", "token d41d8cd98f00b204e9800998ecf8427e"). 123 | MatchHeader("Content-Type", "application/json"). 124 | HeaderPresent("Accept"). 125 | JSON(githubReleasePayload{TagName: "v1.0.0", Name: "TEST", Body: "Hello world !"}). 126 | Reply(200) 127 | 128 | client := &http.Client{Transport: &http.Transport{}} 129 | gock.InterceptClient(client) 130 | 131 | s := newGithubRelease(config).(githubRelease) 132 | s.client = client 133 | 134 | c := types.Changelog{ 135 | Datas: []map[string]any{}, 136 | Metadatas: map[string]any{}, 137 | } 138 | 139 | c.Datas = append(c.Datas, map[string]any{"test": "Hello world !"}) 140 | 141 | err = s.Send(&c) 142 | 143 | assert.NoError(t, err, "Must return no errors") 144 | assert.True(t, gock.IsDone(), "Must have no pending requests") 145 | } 146 | 147 | func TestGithubReleaseUpdateReleaseWithWrongCredentials(t *testing.T) { 148 | config := githubReleaseConfig{} 149 | config.RELEASE.TEMPLATE = "{{ range $key, $value := .Datas }}{{$value.test}}{{ end }}" 150 | config.RELEASE.TAGNAME = "v1.0.0" 151 | config.RELEASE.NAME = "TEST" 152 | config.CREDENTIALS.OWNER = "test" 153 | config.REPOSITORY.NAME = "test" 154 | config.CREDENTIALS.OAUTHTOKEN = "d0b934ea223577f7e5cc6599e40b1822" 155 | config.RELEASE.UPDATE = true 156 | 157 | defer gock.Off() 158 | 159 | gock.New("https://api.github.com"). 160 | Get("/repos/test/test/releases/tags/v1.0.0"). 161 | MatchHeader("Authorization", "token d0b934ea223577f7e5cc6599e40b1822"). 162 | MatchHeader("Content-Type", "application/json"). 163 | HeaderPresent("Accept"). 164 | ReplyError(fmt.Errorf("an error occurred")) 165 | 166 | client := &http.Client{Transport: &http.Transport{}} 167 | gock.InterceptClient(client) 168 | 169 | s := newGithubRelease(config).(githubRelease) 170 | s.client = client 171 | 172 | c := types.Changelog{ 173 | Datas: []map[string]any{}, 174 | Metadatas: map[string]any{}, 175 | } 176 | 177 | c.Datas = append(c.Datas, map[string]any{"test": "Hello world !"}) 178 | 179 | err := s.Send(&c) 180 | 181 | assert.EqualError(t, err, `can't retrieve github release v1.0.0 : Get "https://api.github.com/repos/test/test/releases/tags/v1.0.0": an error occurred`) 182 | assert.True(t, gock.IsDone(), "Must have no pending requests") 183 | } 184 | -------------------------------------------------------------------------------- /prompt/internal/builder/prompt_env_test.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/antham/strumt/v2" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewEnvPrompt(t *testing.T) { 13 | store := &Store{} 14 | 15 | var stdout bytes.Buffer 16 | buf := "1\n" 17 | p := NewEnvPrompt(EnvConfig{"TEST", "NEXT_TEST", "TEST_NEW_ENV_PROMPT", "Enter a value", func(value string) error { return nil }, "", func(value string, store *Store) {}}, store) 18 | 19 | s := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 20 | s.AddLinePrompter(p.(strumt.LinePrompter)) 21 | s.SetFirst("TEST") 22 | s.Run() 23 | 24 | scenario := s.Scenario() 25 | 26 | assert.Len(t, scenario, 1) 27 | assert.Equal(t, scenario[0].PromptString(), "Enter a value") 28 | assert.Len(t, scenario[0].Inputs(), 1) 29 | assert.Equal(t, scenario[0].Inputs()[0], "1") 30 | assert.Nil(t, scenario[0].Error()) 31 | 32 | assert.Equal(t, &Store{"TEST_NEW_ENV_PROMPT": "1"}, store) 33 | } 34 | 35 | func TestNewEnvPromptWithAnEmptyValueAndNoValidationRules(t *testing.T) { 36 | store := &Store{} 37 | 38 | var stdout bytes.Buffer 39 | buf := "\n" 40 | p := NewEnvPrompt(EnvConfig{"TEST", "NEXT_TEST", "TEST_NEW_ENV_PROMPT", "Enter a value", func(value string) error { return nil }, "", func(value string, store *Store) {}}, store) 41 | 42 | s := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 43 | s.AddLinePrompter(p.(strumt.LinePrompter)) 44 | s.SetFirst("TEST") 45 | s.Run() 46 | 47 | scenario := s.Scenario() 48 | 49 | assert.Len(t, scenario, 1) 50 | assert.Equal(t, scenario[0].PromptString(), "Enter a value") 51 | assert.Len(t, scenario[0].Inputs(), 1) 52 | assert.Equal(t, scenario[0].Inputs()[0], "") 53 | assert.Nil(t, scenario[0].Error()) 54 | 55 | assert.Equal(t, &Store{}, store) 56 | } 57 | 58 | func TestNewEnvPromptWithAnEmptyValueAndValidationRulesAndDefaultValue(t *testing.T) { 59 | store := &Store{} 60 | 61 | var stdout bytes.Buffer 62 | buf := "\n" 63 | p := NewEnvPrompt(EnvConfig{"TEST", "NEXT_TEST", "TEST_NEW_ENV_PROMPT", "Enter a value", func(value string) error { return errors.New("An error occured") }, "DEFAULT_VALUE", func(value string, store *Store) {}}, store) 64 | 65 | s := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 66 | s.AddLinePrompter(p.(strumt.LinePrompter)) 67 | s.SetFirst("TEST") 68 | s.Run() 69 | 70 | scenario := s.Scenario() 71 | 72 | assert.Len(t, scenario, 1) 73 | assert.Equal(t, scenario[0].PromptString(), "Enter a value") 74 | assert.Len(t, scenario[0].Inputs(), 1) 75 | assert.Equal(t, scenario[0].Inputs()[0], "") 76 | assert.Nil(t, scenario[0].Error()) 77 | 78 | assert.Equal(t, &Store{"TEST_NEW_ENV_PROMPT": "DEFAULT_VALUE"}, store) 79 | } 80 | 81 | func TestNewEnvPromptWithAPromptHook(t *testing.T) { 82 | store := &Store{} 83 | 84 | var stdout bytes.Buffer 85 | buf := "TEST\n" 86 | p := NewEnvPrompt(EnvConfig{"TEST", "NEXT_TEST", "TEST_NEW_ENV_PROMPT", "Enter a value", func(value string) error { return nil }, "", func(value string, store *Store) { (*store)["TEST_NEW_ENV_PROMPT_2"] = "TEST_2" }}, store) 87 | 88 | s := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 89 | s.AddLinePrompter(p.(strumt.LinePrompter)) 90 | s.SetFirst("TEST") 91 | s.Run() 92 | 93 | scenario := s.Scenario() 94 | 95 | assert.Len(t, scenario, 1) 96 | assert.Equal(t, scenario[0].PromptString(), "Enter a value") 97 | assert.Len(t, scenario[0].Inputs(), 1) 98 | assert.Equal(t, scenario[0].Inputs()[0], "TEST") 99 | assert.Nil(t, scenario[0].Error()) 100 | 101 | assert.Equal(t, &Store{"TEST_NEW_ENV_PROMPT": "TEST", "TEST_NEW_ENV_PROMPT_2": "TEST_2"}, store) 102 | } 103 | 104 | func TestNewEnvPromptWithEmptyValueAndCustomErrorGiven(t *testing.T) { 105 | store := &Store{} 106 | 107 | var stdout bytes.Buffer 108 | buf := "\nfalse\ntrue\n" 109 | p := NewEnvPrompt(EnvConfig{"TEST", "NEXT_TEST", "TEST_NEW_ENV_PROMPT", "Enter a value", func(value string) error { 110 | if value != "true" { 111 | return errors.New("Value must be true") 112 | } 113 | return nil 114 | }, "", func(value string, store *Store) {}}, store) 115 | 116 | s := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 117 | s.AddLinePrompter(p.(strumt.LinePrompter)) 118 | s.SetFirst("TEST") 119 | s.Run() 120 | 121 | scenario := s.Scenario() 122 | 123 | assert.Len(t, scenario, 3) 124 | assert.Equal(t, scenario[0].PromptString(), "Enter a value") 125 | assert.Len(t, scenario[0].Inputs(), 1) 126 | assert.Equal(t, scenario[0].Inputs()[0], "") 127 | assert.EqualError(t, scenario[0].Error(), "Value must be true") 128 | assert.Equal(t, scenario[1].PromptString(), "Enter a value") 129 | assert.Len(t, scenario[1].Inputs(), 1) 130 | assert.Equal(t, scenario[1].Inputs()[0], "false") 131 | assert.Equal(t, scenario[1].Error().Error(), "Value must be true") 132 | assert.Equal(t, scenario[2].PromptString(), "Enter a value") 133 | assert.Len(t, scenario[2].Inputs(), 1) 134 | assert.Equal(t, scenario[2].Inputs()[0], "true") 135 | assert.Nil(t, scenario[2].Error()) 136 | 137 | assert.Equal(t, &Store{"TEST_NEW_ENV_PROMPT": "true"}, store) 138 | } 139 | 140 | func TestNewEnvPrompts(t *testing.T) { 141 | store := &Store{} 142 | 143 | var stdout bytes.Buffer 144 | buf := "1\n2\n" 145 | p := NewEnvPrompts([]EnvConfig{ 146 | {"TEST1", "TEST2", "TEST_PROMPT_1", "Enter a value for prompt 1", func(value string) error { return nil }, "", func(value string, store *Store) {}}, 147 | {"TEST2", "", "TEST_PROMPT_2", "Enter a value for prompt 2", func(value string) error { return nil }, "", func(value string, store *Store) {}}, 148 | }, store) 149 | 150 | s := strumt.NewPromptsFromReaderAndWriter(bytes.NewBufferString(buf), &stdout) 151 | for _, item := range p { 152 | switch prompt := item.(type) { 153 | case strumt.LinePrompter: 154 | s.AddLinePrompter(prompt) 155 | case strumt.MultilinePrompter: 156 | s.AddMultilinePrompter(prompt) 157 | } 158 | } 159 | s.SetFirst("TEST1") 160 | s.Run() 161 | 162 | scenario := s.Scenario() 163 | 164 | assert.Len(t, scenario, 2) 165 | assert.Equal(t, scenario[0].PromptString(), "Enter a value for prompt 1") 166 | assert.Len(t, scenario[0].Inputs(), 1) 167 | assert.Equal(t, scenario[0].Inputs()[0], "1") 168 | assert.Nil(t, scenario[0].Error()) 169 | assert.Equal(t, scenario[1].PromptString(), "Enter a value for prompt 2") 170 | assert.Len(t, scenario[1].Inputs(), 1) 171 | assert.Equal(t, scenario[1].Inputs()[0], "2") 172 | assert.Nil(t, scenario[1].Error()) 173 | 174 | assert.Equal(t, &Store{"TEST_PROMPT_1": "1", "TEST_PROMPT_2": "2"}, store) 175 | } 176 | -------------------------------------------------------------------------------- /chyle/decorators/custom_api_test.go: -------------------------------------------------------------------------------- 1 | package decorators 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/h2non/gock.v0" 9 | ) 10 | 11 | func TestCustomAPI(t *testing.T) { 12 | config := customAPIConfig{} 13 | config.CREDENTIALS.TOKEN = "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f" 14 | config.ENDPOINT.URL = "http://test.com/api/issue/{{ID}}" 15 | config.KEYS = map[string]struct { 16 | DESTKEY string 17 | FIELD string 18 | }{ 19 | "KEY": { 20 | "authorEmail", 21 | "author.email", 22 | }, 23 | "WHATEVER": { 24 | "whatever", 25 | "whatever", 26 | }, 27 | } 28 | 29 | defer gock.Off() 30 | 31 | gock.New("http://test.com/api/issue/1"). 32 | MatchHeader("Authorization", "token d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"). 33 | MatchHeader("Content-Type", "application/json"). 34 | Reply(200). 35 | JSON(`{"id":"1","author":{"email":"test@test.com","name":"test"}}`) 36 | 37 | gock.New("http://test.com/api/issue/145d5926-2c7b-42c5-b0ff-41cd9b73c56c"). 38 | MatchHeader("Authorization", "token d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"). 39 | MatchHeader("Content-Type", "application/json"). 40 | Reply(200). 41 | JSON(`{"id":"145d5926-2c7b-42c5-b0ff-41cd9b73c56c","author":{"email":"test@test.com","name":"test"}}`) 42 | 43 | client := &http.Client{Transport: &http.Transport{}} 44 | gock.InterceptClient(client) 45 | 46 | c := customAPI{*client, config} 47 | 48 | // request with int id 49 | result, err := c.Decorate(&map[string]any{"test": "test", "customApiId": int64(1)}) 50 | 51 | expected := map[string]any{ 52 | "test": "test", 53 | "customApiId": int64(1), 54 | "authorEmail": "test@test.com", 55 | } 56 | 57 | assert.NoError(t, err) 58 | assert.Equal(t, expected, *result) 59 | 60 | // request with string id 61 | result, err = c.Decorate(&map[string]any{"test": "test", "customApiId": "145d5926-2c7b-42c5-b0ff-41cd9b73c56c"}) 62 | 63 | expected = map[string]any{ 64 | "test": "test", 65 | "customApiId": "145d5926-2c7b-42c5-b0ff-41cd9b73c56c", 66 | "authorEmail": "test@test.com", 67 | } 68 | 69 | assert.NoError(t, err) 70 | assert.Equal(t, expected, *result) 71 | assert.True(t, gock.IsDone(), "Must have no pending requests") 72 | } 73 | 74 | func TestCustomAPIWithUnvalidResponse(t *testing.T) { 75 | config := customAPIConfig{} 76 | config.CREDENTIALS.TOKEN = "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f" 77 | config.ENDPOINT.URL = "http://test.com/api/issue/{{ID}}" 78 | config.KEYS = map[string]struct { 79 | DESTKEY string 80 | FIELD string 81 | }{ 82 | "WHATEVER": { 83 | "whatever", 84 | "whatever", 85 | }, 86 | } 87 | 88 | defer gock.Off() 89 | 90 | gock.New("http://test.com/api/issue/5b23f37a-7404-49ce-812e-e7b3595ac721"). 91 | MatchHeader("Authorization", "token d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"). 92 | MatchHeader("Content-Type", "application/json"). 93 | Reply(200). 94 | BodyString("test") 95 | 96 | client := &http.Client{Transport: &http.Transport{}} 97 | gock.InterceptClient(client) 98 | 99 | c := customAPI{*client, config} 100 | 101 | result, err := c.Decorate(&map[string]any{"test": "test", "customApiId": "5b23f37a-7404-49ce-812e-e7b3595ac721"}) 102 | 103 | expected := map[string]any{ 104 | "test": "test", 105 | "customApiId": "5b23f37a-7404-49ce-812e-e7b3595ac721", 106 | } 107 | 108 | assert.NoError(t, err) 109 | assert.Equal(t, expected, *result) 110 | assert.True(t, gock.IsDone(), "Must have no pending requests") 111 | } 112 | 113 | func TestCustomAPIWithNoCustomApiIdDefined(t *testing.T) { 114 | defer gock.Off() 115 | 116 | gock.New("http://test.com/api/issue/5b23f37a-7404-49ce-812e-e7b3595ac721"). 117 | MatchHeader("Authorization", "token d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"). 118 | MatchHeader("Content-Type", "application/json"). 119 | Reply(200). 120 | JSON(`{}`) 121 | 122 | client := &http.Client{Transport: &http.Transport{}} 123 | gock.InterceptClient(client) 124 | 125 | c := customAPI{*client, customAPIConfig{}} 126 | 127 | result, err := c.Decorate(&map[string]any{"test": "test"}) 128 | 129 | expected := map[string]any{ 130 | "test": "test", 131 | } 132 | 133 | assert.NoError(t, err) 134 | assert.Equal(t, expected, *result) 135 | assert.False(t, gock.IsDone(), "Must have one pending request") 136 | } 137 | 138 | func TestCustomAPIWithAnErrorStatusCode(t *testing.T) { 139 | config := customAPIConfig{} 140 | config.CREDENTIALS.TOKEN = "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f" 141 | config.ENDPOINT.URL = "http://test.com/api/issue/{{ID}}" 142 | config.KEYS = map[string]struct { 143 | DESTKEY string 144 | FIELD string 145 | }{ 146 | "WHATEVER": { 147 | "whatever", 148 | "whatever", 149 | }, 150 | } 151 | 152 | defer gock.Off() 153 | 154 | gock.New("http://test.com/api/issue/5b23f37a-7404-49ce-812e-e7b3595ac721"). 155 | MatchHeader("Authorization", "token d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"). 156 | MatchHeader("Content-Type", "application/json"). 157 | Reply(401). 158 | JSON(`{"error":"not found"}`) 159 | 160 | client := &http.Client{Transport: &http.Transport{}} 161 | gock.InterceptClient(client) 162 | 163 | c := customAPI{*client, config} 164 | 165 | result, err := c.Decorate(&map[string]any{"test": "test", "customApiId": "5b23f37a-7404-49ce-812e-e7b3595ac721"}) 166 | 167 | expected := map[string]any{ 168 | "test": "test", 169 | "customApiId": "5b23f37a-7404-49ce-812e-e7b3595ac721", 170 | } 171 | 172 | assert.EqualError(t, err, `an error occurred when contacting remote api through http://test.com/api/issue/5b23f37a-7404-49ce-812e-e7b3595ac721, status code 401, body {"error":"not found"}`) 173 | assert.Equal(t, expected, *result) 174 | assert.True(t, gock.IsDone(), "Must have no pending requests") 175 | } 176 | 177 | func TestCustomAPIWhenEntryIsNotFound(t *testing.T) { 178 | config := customAPIConfig{} 179 | config.CREDENTIALS.TOKEN = "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f" 180 | config.ENDPOINT.URL = "http://test.com/api/issue/{{ID}}" 181 | config.KEYS = map[string]struct { 182 | DESTKEY string 183 | FIELD string 184 | }{ 185 | "WHATEVER": { 186 | "whatever", 187 | "whatever", 188 | }, 189 | } 190 | 191 | defer gock.Off() 192 | 193 | gock.New("http://test.com/api/issue/5b23f37a-7404-49ce-812e-e7b3595ac721"). 194 | MatchHeader("Authorization", "token d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"). 195 | MatchHeader("Content-Type", "application/json"). 196 | Reply(404). 197 | JSON(`{"error":"not found"}`) 198 | 199 | client := &http.Client{Transport: &http.Transport{}} 200 | gock.InterceptClient(client) 201 | 202 | c := customAPI{*client, config} 203 | 204 | result, err := c.Decorate(&map[string]any{"test": "test", "customApiId": "5b23f37a-7404-49ce-812e-e7b3595ac721"}) 205 | 206 | expected := map[string]any{ 207 | "test": "test", 208 | "customApiId": "5b23f37a-7404-49ce-812e-e7b3595ac721", 209 | } 210 | 211 | assert.NoError(t, err) 212 | assert.Equal(t, expected, *result) 213 | assert.True(t, gock.IsDone(), "Must have no pending requests") 214 | } 215 | -------------------------------------------------------------------------------- /chyle/git/git_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/go-git/go-git/v5" 12 | "github.com/go-git/go-git/v5/plumbing" 13 | "github.com/go-git/go-git/v5/plumbing/object" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var ( 18 | repo *git.Repository 19 | gitRepositoryPath = "testing-repository" 20 | ) 21 | 22 | func setup() { 23 | path, err := os.Getwd() 24 | if err != nil { 25 | fmt.Println(err) 26 | os.Exit(1) 27 | } 28 | 29 | repo, err = git.PlainOpen(path + "/" + gitRepositoryPath) 30 | if err != nil { 31 | fmt.Println(err) 32 | os.Exit(1) 33 | } 34 | } 35 | 36 | func getCommitFromRef(ref string) *object.Commit { 37 | cmd := exec.Command("git", "rev-parse", ref) 38 | cmd.Dir = gitRepositoryPath 39 | 40 | ID, err := cmd.Output() 41 | ID = ID[:len(ID)-1] 42 | 43 | if err != nil { 44 | logrus.WithField("ID", string(ID)).Fatal(err) 45 | } 46 | 47 | c, err := repo.CommitObject(plumbing.NewHash(string(ID))) 48 | if err != nil { 49 | logrus.WithField("ID", ID).Fatal(err) 50 | } 51 | 52 | return c 53 | } 54 | 55 | func TestMain(m *testing.M) { 56 | setup() 57 | code := m.Run() 58 | os.Exit(code) 59 | } 60 | 61 | func TestResolveRef(t *testing.T) { 62 | type g struct { 63 | ref string 64 | f func(*object.Commit, error) 65 | } 66 | 67 | tests := []g{ 68 | { 69 | "HEAD", 70 | func(o *object.Commit, err error) { 71 | assert.NoError(t, err) 72 | assert.True(t, o.ID().String() == getCommitFromRef("HEAD").ID().String(), "Must resolve HEAD reference") 73 | }, 74 | }, 75 | { 76 | "test1", 77 | func(o *object.Commit, err error) { 78 | assert.NoError(t, err) 79 | assert.True(t, o.ID().String() == getCommitFromRef("test1").ID().String(), "Must resolve branch reference") 80 | }, 81 | }, 82 | { 83 | getCommitFromRef("test1").ID().String(), 84 | func(o *object.Commit, err error) { 85 | assert.NoError(t, err) 86 | assert.True(t, o.ID().String() == getCommitFromRef("test1").ID().String(), "Must resolve commit id") 87 | }, 88 | }, 89 | { 90 | "whatever", 91 | func(o *object.Commit, err error) { 92 | assert.Error(t, err) 93 | assert.EqualError(t, err, `reference "whatever" can't be found in git repository`) 94 | }, 95 | }, 96 | } 97 | 98 | for _, test := range tests { 99 | test.f(resolveRef(test.ref, repo)) 100 | } 101 | } 102 | 103 | func TestResolveRefWithErrors(t *testing.T) { 104 | type g struct { 105 | ref string 106 | repo *git.Repository 107 | f func(*object.Commit, error) 108 | } 109 | 110 | tests := []g{ 111 | { 112 | "whatever", 113 | repo, 114 | func(o *object.Commit, err error) { 115 | assert.Error(t, err) 116 | assert.EqualError(t, err, `reference "whatever" can't be found in git repository`) 117 | }, 118 | }, 119 | } 120 | 121 | for _, test := range tests { 122 | test.f(resolveRef(test.ref, test.repo)) 123 | } 124 | } 125 | 126 | func TestFetchCommits(t *testing.T) { 127 | type g struct { 128 | path string 129 | toRef string 130 | fromRef string 131 | f func(*[]object.Commit, error) 132 | } 133 | 134 | tests := []g{ 135 | { 136 | gitRepositoryPath, 137 | getCommitFromRef("HEAD").ID().String(), 138 | getCommitFromRef("test").ID().String(), 139 | func(cs *[]object.Commit, err error) { 140 | assert.Error(t, err) 141 | assert.Regexp(t, `can't produce a diff between .*? and .*?, check your range is correct by running "git log .*?\.\..*?" command`, err.Error()) 142 | }, 143 | }, 144 | { 145 | gitRepositoryPath, 146 | getCommitFromRef("HEAD~1").ID().String(), 147 | getCommitFromRef("HEAD~3").ID().String(), 148 | func(cs *[]object.Commit, err error) { 149 | assert.Error(t, err) 150 | assert.Regexp(t, `can't produce a diff between .*? and .*?, check your range is correct by running "git log .*?\.\..*?" command`, err.Error()) 151 | }, 152 | }, 153 | { 154 | gitRepositoryPath, 155 | getCommitFromRef("HEAD~3").ID().String(), 156 | getCommitFromRef("test~2^2").ID().String(), 157 | func(cs *[]object.Commit, err error) { 158 | assert.NoError(t, err) 159 | assert.Len(t, *cs, 5) 160 | 161 | commitTests := []string{ 162 | "Merge branch 'test2' into test1\n", 163 | "feat(file6) : new file 6\n\ncreate a new file 6\n", 164 | "feat(file5) : new file 5\n\ncreate a new file 5\n", 165 | "feat(file4) : new file 4\n\ncreate a new file 4\n", 166 | "feat(file3) : new file 3\n\ncreate a new file 3\n", 167 | } 168 | 169 | for i, c := range *cs { 170 | assert.Equal(t, commitTests[i], c.Message) 171 | } 172 | }, 173 | }, 174 | { 175 | gitRepositoryPath, 176 | getCommitFromRef("HEAD~4").ID().String(), 177 | getCommitFromRef("test~2^2^2").ID().String(), 178 | func(cs *[]object.Commit, err error) { 179 | assert.NoError(t, err) 180 | assert.Len(t, *cs, 5) 181 | 182 | commitTests := []string{ 183 | "feat(file6) : new file 6\n\ncreate a new file 6\n", 184 | "feat(file5) : new file 5\n\ncreate a new file 5\n", 185 | "feat(file4) : new file 4\n\ncreate a new file 4\n", 186 | "feat(file3) : new file 3\n\ncreate a new file 3\n", 187 | "feat(file2) : new file 2\n\ncreate a new file 2\n", 188 | } 189 | 190 | for i, c := range *cs { 191 | assert.Equal(t, commitTests[i], c.Message) 192 | } 193 | }, 194 | }, 195 | { 196 | "whatever", 197 | getCommitFromRef("HEAD").ID().String(), 198 | getCommitFromRef("HEAD~1").ID().String(), 199 | func(cs *[]object.Commit, err error) { 200 | assert.EqualError(t, err, `check "whatever" is an existing git repository path`) 201 | }, 202 | }, 203 | { 204 | gitRepositoryPath, 205 | "whatever", 206 | getCommitFromRef("HEAD~1").ID().String(), 207 | func(cs *[]object.Commit, err error) { 208 | assert.EqualError(t, err, `reference "whatever" can't be found in git repository`) 209 | }, 210 | }, 211 | { 212 | gitRepositoryPath, 213 | getCommitFromRef("HEAD~1").ID().String(), 214 | "whatever", 215 | func(cs *[]object.Commit, err error) { 216 | assert.EqualError(t, err, `reference "whatever" can't be found in git repository`) 217 | }, 218 | }, 219 | { 220 | gitRepositoryPath, 221 | "HEAD", 222 | "HEAD", 223 | func(cs *[]object.Commit, err error) { 224 | assert.EqualError(t, err, `can't produce a diff between HEAD and HEAD, check your range is correct by running "git log HEAD..HEAD" command`) 225 | }, 226 | }, 227 | } 228 | 229 | for _, test := range tests { 230 | test.f(FetchCommits(test.path, test.toRef, test.fromRef)) 231 | } 232 | } 233 | 234 | func TestShallowCloneProducesNoErrors(t *testing.T) { 235 | path := "shallow-repository-test" 236 | cmd := exec.Command("rm", "-rf", path) 237 | _, err := cmd.Output() 238 | 239 | assert.NoError(t, err) 240 | 241 | cmd = exec.Command("git", "clone", "--depth", "2", "https://github.com/octocat/Spoon-Knife.git", path) 242 | _, err = cmd.Output() 243 | 244 | assert.NoError(t, err) 245 | 246 | cmd = exec.Command("git", "rev-parse", "HEAD~1") 247 | cmd.Dir = path 248 | 249 | fromRef, err := cmd.Output() 250 | fromRef = fromRef[:len(fromRef)-1] 251 | 252 | assert.NoError(t, err, "Must extract HEAD~1") 253 | 254 | cmd = exec.Command("git", "rev-parse", "HEAD") 255 | cmd.Dir = path 256 | 257 | toRef, err := cmd.Output() 258 | toRef = toRef[:len(toRef)-1] 259 | 260 | assert.NoError(t, err) 261 | 262 | commits, err := FetchCommits("shallow-repository-test", string(fromRef), string(toRef)) 263 | 264 | assert.NoError(t, err) 265 | assert.Len(t, *commits, 1, "Must fetch commits in shallow clone") 266 | } 267 | -------------------------------------------------------------------------------- /prompt/sender.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/antham/strumt/v2" 7 | 8 | "github.com/antham/chyle/prompt/internal/builder" 9 | ) 10 | 11 | const ( 12 | json = "json" 13 | template = "template" 14 | ) 15 | 16 | func newSenders(store *builder.Store) []strumt.Prompter { 17 | return mergePrompters( 18 | senderChoice, 19 | newStdoutSender(store), 20 | newCustomAPISender(store), 21 | newGithubReleaseSender(store), 22 | ) 23 | } 24 | 25 | var senderChoice = []strumt.Prompter{ 26 | builder.NewSwitchPrompt("senderChoice", addMainMenuAndQuitChoice( 27 | []builder.SwitchConfig{ 28 | { 29 | Choice: "1", 30 | PromptString: "Add an stdout sender", 31 | NextPromptID: "senderStdoutFormat", 32 | }, 33 | { 34 | Choice: "2", 35 | PromptString: "Add a github release sender", 36 | NextPromptID: "githubReleaseSenderCredentialsToken", 37 | }, 38 | { 39 | Choice: "3", 40 | PromptString: "Add a custom api sender", 41 | NextPromptID: "customAPISenderToken", 42 | }, 43 | }, 44 | )), 45 | } 46 | 47 | func newStdoutSender(store *builder.Store) []strumt.Prompter { 48 | return []strumt.Prompter{ 49 | &builder.GenericPrompt{ 50 | PromptID: "senderStdoutFormat", 51 | PromptStr: "Set output format : json or template", 52 | OnSuccess: func(val string) string { 53 | if val == json { 54 | return "senderChoice" 55 | } 56 | return "senderStdoutTemplate" 57 | }, 58 | OnError: func(err error) string { 59 | return "senderStdoutFormat" 60 | }, 61 | ParseValue: func(val string) error { 62 | if val != json && val != template { 63 | return fmt.Errorf(`"%s" is not a valid format, it must be either "json" or "template"`, val) 64 | } 65 | 66 | return builder.ParseEnv(func(value string) error { return nil }, "CHYLE_SENDERS_STDOUT_FORMAT", "", store)(val) 67 | }, 68 | }, 69 | builder.NewEnvPrompt( 70 | builder.EnvConfig{ 71 | ID: "senderStdoutTemplate", 72 | NextID: "senderChoice", 73 | Env: "CHYLE_SENDERS_STDOUT_TEMPLATE", 74 | PromptString: "Set a template used to dump to stdout. The syntax follows the golang template (more information here : https://github.com/antham/chyle/wiki/6-Templates)", 75 | Validator: validateTemplate, 76 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 77 | }, store, 78 | ), 79 | } 80 | } 81 | 82 | func newGithubReleaseSender(store *builder.Store) []strumt.Prompter { 83 | return builder.NewEnvPrompts(githubReleaseSender, store) 84 | } 85 | 86 | var githubReleaseSender = []builder.EnvConfig{ 87 | { 88 | ID: "githubReleaseSenderCredentialsToken", 89 | NextID: "githubReleaseSenderCredentialsOwer", 90 | Env: "CHYLE_SENDERS_GITHUBRELEASE_CREDENTIALS_OAUTHTOKEN", 91 | PromptString: "Set github oauth token used to publish a release", 92 | Validator: validateDefinedValue, 93 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 94 | }, 95 | { 96 | ID: "githubReleaseSenderCredentialsOwer", 97 | NextID: "githubReleaseSenderRepositoryName", 98 | Env: "CHYLE_SENDERS_GITHUBRELEASE_CREDENTIALS_OWNER", 99 | PromptString: "Set github owner used in credentials", 100 | Validator: validateDefinedValue, 101 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 102 | }, 103 | { 104 | ID: "githubReleaseSenderRepositoryName", 105 | NextID: "githubReleaseSenderReleaseDraft", 106 | Env: "CHYLE_SENDERS_GITHUBRELEASE_REPOSITORY_NAME", 107 | PromptString: "Set github repository where we will publish the release", 108 | Validator: validateDefinedValue, 109 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 110 | }, 111 | { 112 | ID: "githubReleaseSenderReleaseDraft", 113 | NextID: "githubReleaseSenderReleaseName", 114 | Env: "CHYLE_SENDERS_GITHUBRELEASE_RELEASE_DRAFT", 115 | PromptString: "Set if release must be marked as a draft (default: false)", 116 | Validator: validateBoolean, 117 | DefaultValue: "false", 118 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 119 | }, 120 | { 121 | ID: "githubReleaseSenderReleaseName", 122 | NextID: "githubReleaseSenderReleasePrerelease", 123 | Env: "CHYLE_SENDERS_GITHUBRELEASE_RELEASE_NAME", 124 | PromptString: "Set the title of the release, just return if you don't want to give a title", 125 | Validator: noOpValidator, 126 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 127 | }, 128 | { 129 | ID: "githubReleaseSenderReleasePrerelease", 130 | NextID: "githubReleaseSenderReleaseTagName", 131 | Env: "CHYLE_SENDERS_GITHUBRELEASE_RELEASE_PRERELEASE", 132 | PromptString: "Set if the release must be marked as a prerelease (default: false)", 133 | Validator: validateBoolean, 134 | DefaultValue: "false", 135 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 136 | }, 137 | { 138 | ID: "githubReleaseSenderReleaseTagName", 139 | NextID: "githubReleaseSenderReleaseTargetCommit", 140 | Env: "CHYLE_SENDERS_GITHUBRELEASE_RELEASE_TAGNAME", 141 | PromptString: "Set the release tag to create, if you update a release instead, it will be used to find out the release tied to this tag", 142 | Validator: validateDefinedValue, 143 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 144 | }, 145 | { 146 | ID: "githubReleaseSenderReleaseTargetCommit", 147 | NextID: "githubReleaseSenderReleaseTemplate", 148 | Env: "CHYLE_SENDERS_GITHUBRELEASE_RELEASE_TARGETCOMMITISH", 149 | PromptString: "Set the commitish value that determines where the git tag must created from (default: master)", 150 | Validator: noOpValidator, 151 | DefaultValue: "master", 152 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 153 | }, 154 | { 155 | ID: "githubReleaseSenderReleaseTemplate", 156 | NextID: "githubReleaseSenderReleaseUpdate", 157 | Env: "CHYLE_SENDERS_GITHUBRELEASE_RELEASE_TEMPLATE", 158 | PromptString: "Set a template used to dump the release body. The syntax follows the golang template (more information here : https://github.com/antham/chyle/wiki/6-Templates)", 159 | Validator: validateTemplate, 160 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 161 | }, 162 | { 163 | ID: "githubReleaseSenderReleaseUpdate", 164 | NextID: "senderChoice", 165 | Env: "CHYLE_SENDERS_GITHUBRELEASE_RELEASE_UPDATE", 166 | PromptString: "Set if you want to update an existing changelog, typical usage would be when you produce a release through GUI github release system (default: false)", 167 | Validator: validateBoolean, 168 | DefaultValue: "false", 169 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 170 | }, 171 | } 172 | 173 | func newCustomAPISender(store *builder.Store) []strumt.Prompter { 174 | return builder.NewEnvPrompts(customAPISender, store) 175 | } 176 | 177 | var customAPISender = []builder.EnvConfig{ 178 | { 179 | ID: "customAPISenderToken", 180 | NextID: "customAPISenderURL", 181 | Env: "CHYLE_SENDERS_CUSTOMAPI_CREDENTIALS_TOKEN", 182 | PromptString: `Set an access token that would be given in authorization header when calling your API`, 183 | Validator: validateDefinedValue, 184 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 185 | }, 186 | { 187 | ID: "customAPISenderURL", 188 | NextID: "senderChoice", 189 | Env: "CHYLE_SENDERS_CUSTOMAPI_ENDPOINT_URL", 190 | PromptString: "Set the URL endpoint where the POST request will be sent", 191 | Validator: validateURL, 192 | RunBeforeNextPrompt: noOpRunBeforeNextPrompt, 193 | }, 194 | } 195 | --------------------------------------------------------------------------------