├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go ├── pkg ├── commandLine │ └── client.go └── workers │ └── worker.go └── systemd_example /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | logs/* 3 | bin/process_git_trigger 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values. 3 | 4 | # options for analysis running 5 | run: 6 | # timeout for analysis, e.g. 30s, 5m, default is 1m 7 | timeout: 5m 8 | 9 | # include test files or not, default is true 10 | tests: false 11 | 12 | linters: 13 | disable: 14 | - scopelint 15 | - contextcheck 16 | enable: 17 | - errcheck 18 | - goimports 19 | - gofmt 20 | - revive 21 | - exportloopref 22 | - prealloc 23 | - lll 24 | - whitespace 25 | - unconvert 26 | - goconst 27 | - staticcheck 28 | - govet 29 | - gocritic 30 | - deadcode 31 | - godox 32 | presets: 33 | - bugs 34 | - unused 35 | linters-settings: 36 | lll: 37 | line-length: 1000 38 | revive: 39 | ignore-generated-header: true 40 | rules: 41 | - name: unexported-return 42 | disabled: true 43 | goimports: 44 | local-prefixes: github.com/tarmalonchik/git_trigger 45 | 46 | issues: 47 | exclude-rules: 48 | - linters: 49 | - lll 50 | source: "^//go:generate " 51 | - path: _enum\.go 52 | linters: 53 | - all 54 | - path: pkg/api/sdk/ 55 | linters: 56 | - all 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kochiev Alan 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint 2 | lint: 3 | golangci-lint run 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git_trigger 2 | 3 | you can run command with 4 arguments 4 | 5 | 1. arg is repo name example: `tarmalonchik/git_trigger` 6 | 7 | 2. arg is the path to place where you want to store pulled project, example: `/root` 8 | 9 | 3. arg is command needed to add to make command example: `build` 10 | 11 | 4. arg is the branch you would like to use, example: `master` 12 | 13 | result command: 14 | ``` 15 | go run cmd/main.go tarmalonchik/project_name /root build master 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tarmalonchik/git_trigger 2 | 3 | go 1.18 4 | 5 | require github.com/sirupsen/logrus v1.8.1 6 | 7 | require ( 8 | github.com/stretchr/testify v1.3.0 // indirect 9 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 7 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 10 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 11 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 12 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= 14 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/tarmalonchik/git_trigger/pkg/commandLine" 10 | "github.com/tarmalonchik/git_trigger/pkg/workers" 11 | ) 12 | 13 | func main() { 14 | ctx := context.Background() 15 | 16 | args := os.Args 17 | 18 | if len(args) != 5 { 19 | logrus.Errorf("command format should have 4 params repoName, destPath, makeCommand, and branchName") 20 | return 21 | } 22 | 23 | if err := initDirsSystem(); err != nil { 24 | logrus.Errorf("error while initing dirs: %v", err) 25 | return 26 | } 27 | 28 | repoName := args[1] 29 | destPath := args[2] 30 | makeCommand := args[3] 31 | branchName := args[4] 32 | 33 | consoleConf, err := commandLine.NewClient(repoName, destPath, makeCommand, branchName) 34 | if err != nil { 35 | logrus.Errorf("error init consoleConf: %v", err) 36 | return 37 | } 38 | 39 | worker := workers.NewWorker(consoleConf) 40 | if err = worker.Run(ctx); err != nil { 41 | logrus.Errorf("error Runner: %v", err) 42 | } 43 | } 44 | 45 | func initDirsSystem() error { 46 | if err := createFolders([]string{ 47 | "logs/clone", 48 | "logs/maker", 49 | "logs/pull", 50 | "logs/checkout", 51 | "logs/pull_all", 52 | }); err != nil { 53 | return fmt.Errorf("error creating fodlers: %w", err) 54 | } 55 | 56 | if err := createFiles([]string{ 57 | "logs/clone/errors", 58 | "logs/clone/info", 59 | "logs/clone/errors", 60 | "logs/clone/info", 61 | "logs/maker/errors", 62 | "logs/maker/info", 63 | "logs/pull/errors", 64 | "logs/pull/info", 65 | "logs/checkout/errors", 66 | "logs/checkout/info", 67 | "logs/pull_all/errors", 68 | "logs/pull_all/info", 69 | }); err != nil { 70 | return fmt.Errorf("error creating files: %w", err) 71 | } 72 | return nil 73 | } 74 | 75 | func createFolders(names []string) error { 76 | for i := range names { 77 | if err := os.MkdirAll(names[i], 0777); err != nil { 78 | return err 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | func createFiles(files []string) error { 85 | for i := range files { 86 | if err := createFile(files[i]); err != nil { 87 | return err 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | func createFile(name string) error { 94 | file, err := os.Create(name) 95 | if err != nil { 96 | return err 97 | } 98 | if err := file.Close(); err != nil { 99 | return err 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /pkg/commandLine/client.go: -------------------------------------------------------------------------------- 1 | package commandLine 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | type Client struct { 12 | pullString string 13 | projectName string 14 | destPath string 15 | makeCommand string 16 | branchName string 17 | } 18 | 19 | func NewClient(repoName, destPath, makeCommand, branchName string) (*Client, error) { 20 | spt := strings.Split(repoName, "/") 21 | if len(spt) != 2 { 22 | return nil, fmt.Errorf("commandLine.NewClient bad repo") 23 | } 24 | 25 | return &Client{ 26 | destPath: destPath, 27 | pullString: "git@github.com:" + repoName + ".git", 28 | projectName: spt[1], 29 | makeCommand: makeCommand, 30 | branchName: branchName, 31 | }, nil 32 | } 33 | 34 | func (c *Client) Clone(ctx context.Context) error { 35 | const ( 36 | infoFileName = "logs/clone/info" 37 | errorsFileName = "logs/clone/errors" 38 | lookForString = "already exists" 39 | ) 40 | 41 | infoFile, err := os.OpenFile(infoFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 42 | if err != nil { 43 | return fmt.Errorf("commandLine.Clone error making info file: %w", err) 44 | } 45 | defer func() { _ = infoFile.Close() }() 46 | 47 | errorsFile, err := os.OpenFile(errorsFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 48 | if err != nil { 49 | return fmt.Errorf("commandLine.Clone error making errors file: %w", err) 50 | } 51 | defer func() { _ = errorsFile.Close() }() 52 | 53 | cmd := exec.CommandContext(ctx, "git", "clone", c.pullString, "--progress") 54 | cmd.Dir = c.destPath 55 | cmd.Stdout = infoFile 56 | cmd.Stderr = errorsFile 57 | 58 | if err := cmd.Start(); err != nil { 59 | return fmt.Errorf("commandLine.Clone error start: %w", err) 60 | } 61 | 62 | if err := cmd.Wait(); err != nil { 63 | data, err := os.ReadFile(errorsFileName) 64 | if strings.Contains(string(data), lookForString) { 65 | return nil 66 | } 67 | return fmt.Errorf("commandLine.Clone error wait: %w", err) 68 | } 69 | return nil 70 | } 71 | 72 | func (c *Client) PullBranch(ctx context.Context) (bool, error) { 73 | const ( 74 | infoFileName = "logs/pull/info" 75 | errorsFileName = "logs/pull/errors" 76 | lookForString = "Updating" 77 | ) 78 | 79 | infoFile, err := os.OpenFile(infoFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 80 | if err != nil { 81 | return false, fmt.Errorf("commandLine.Pull error making info file: %w", err) 82 | } 83 | defer func() { _ = infoFile.Close() }() 84 | errorsFile, err := os.OpenFile(errorsFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 85 | if err != nil { 86 | return false, fmt.Errorf("commandLine.Pull error making errors file: %w", err) 87 | } 88 | defer func() { _ = errorsFile.Close() }() 89 | 90 | cmd := exec.CommandContext(ctx, "git", "pull", "origin", c.branchName, "--progress") 91 | cmd.Dir = c.getProjectPath() 92 | cmd.Stdout = infoFile 93 | cmd.Stderr = errorsFile 94 | 95 | if err := cmd.Start(); err != nil { 96 | return false, fmt.Errorf("commandLine.Pull error start: %w", err) 97 | } 98 | 99 | if err := cmd.Wait(); err != nil { 100 | return false, fmt.Errorf("commandLine.Pull error wait: %w", err) 101 | } 102 | 103 | data, err := os.ReadFile(infoFileName) 104 | if err != nil { 105 | return false, fmt.Errorf("commandLine.Pull error reading file: %w", err) 106 | } 107 | 108 | if strings.Contains(string(data), lookForString) { 109 | return true, nil 110 | } 111 | return false, nil 112 | } 113 | 114 | func (c *Client) PullAll(ctx context.Context) error { 115 | const ( 116 | infoFileName = "logs/pull_all/info" 117 | errorsFileName = "logs/pull_all/errors" 118 | ) 119 | 120 | infoFile, err := os.OpenFile(infoFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 121 | if err != nil { 122 | return fmt.Errorf("commandLine.PullAll error making info file: %w", err) 123 | } 124 | defer func() { _ = infoFile.Close() }() 125 | errorsFile, err := os.OpenFile(errorsFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 126 | if err != nil { 127 | return fmt.Errorf("commandLine.PullAll error making errors file: %w", err) 128 | } 129 | defer func() { _ = errorsFile.Close() }() 130 | 131 | cmd := exec.CommandContext(ctx, "git", "pull", "--all", "--progress") 132 | cmd.Dir = c.getProjectPath() 133 | cmd.Stdout = infoFile 134 | cmd.Stderr = errorsFile 135 | 136 | if err := cmd.Start(); err != nil { 137 | return fmt.Errorf("commandLine.PullAll error start: %w", err) 138 | } 139 | 140 | if err := cmd.Wait(); err != nil { 141 | return fmt.Errorf("commandLine.PullAll error wait: %w", err) 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func (c *Client) Maker(ctx context.Context) error { 148 | const ( 149 | infoFileName = "logs/maker/info" 150 | errorsFileName = "logs/maker/errors" 151 | ) 152 | 153 | infoFile, err := os.OpenFile(infoFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 154 | if err != nil { 155 | return fmt.Errorf("commandLine.Maker error making info file: %w", err) 156 | } 157 | defer func() { _ = infoFile.Close() }() 158 | errorsFile, err := os.OpenFile(errorsFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 159 | if err != nil { 160 | return fmt.Errorf("commandLine.Maker error making errors file: %w", err) 161 | } 162 | defer func() { _ = errorsFile.Close() }() 163 | 164 | cmd := exec.CommandContext(ctx, "make", c.makeCommand) 165 | cmd.Dir = c.getProjectPath() 166 | cmd.Stdout = infoFile 167 | cmd.Stderr = errorsFile 168 | 169 | if err := cmd.Start(); err != nil { 170 | return fmt.Errorf("commandLine.Maker error start fullPath = %s: %w", c.projectName, err) 171 | } 172 | if err := cmd.Wait(); err != nil { 173 | return fmt.Errorf("commandLine.Maker error wait: %w", err) 174 | } 175 | return nil 176 | } 177 | 178 | func (c *Client) Checkout(ctx context.Context) error { 179 | const ( 180 | lookForString = "did not match any file(s) known to git" 181 | infoFileName = "logs/checkout/info" 182 | errorsFileName = "logs/checkout/errors" 183 | ) 184 | 185 | infoFile, err := os.OpenFile(infoFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 186 | if err != nil { 187 | return fmt.Errorf("commandLine.Maker error making info file: %w", err) 188 | } 189 | defer func() { _ = infoFile.Close() }() 190 | errorsFile, err := os.OpenFile(errorsFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 191 | if err != nil { 192 | return fmt.Errorf("commandLine.Maker error making errors file: %w", err) 193 | } 194 | defer func() { _ = errorsFile.Close() }() 195 | 196 | cmd := exec.CommandContext(ctx, "git", "checkout") 197 | cmd.Dir = c.getProjectPath() 198 | cmd.Stdout = infoFile 199 | cmd.Stderr = errorsFile 200 | 201 | if err := cmd.Start(); err != nil { 202 | return fmt.Errorf("commandLine.Checkout error start: %w", err) 203 | } 204 | 205 | if err := cmd.Wait(); err != nil { 206 | return fmt.Errorf("commandLine.Checkout error wait: %w", err) 207 | } 208 | 209 | data, err := os.ReadFile(errorsFileName) 210 | if err != nil { 211 | return fmt.Errorf("commandLine.Checkout error reading file: %w", err) 212 | } 213 | 214 | if strings.Contains(string(data), lookForString) { 215 | return nil 216 | } 217 | return nil 218 | } 219 | 220 | func (c *Client) getProjectPath() string { 221 | if c.destPath[len(c.destPath)-1] == '/' { 222 | return c.destPath + c.projectName 223 | } 224 | return c.destPath + "/" + c.projectName 225 | } 226 | -------------------------------------------------------------------------------- /pkg/workers/worker.go: -------------------------------------------------------------------------------- 1 | package workers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "runtime" 9 | "time" 10 | 11 | "github.com/sirupsen/logrus" 12 | "github.com/tarmalonchik/git_trigger/pkg/commandLine" 13 | ) 14 | 15 | type Worker struct { 16 | commandLineClient *commandLine.Client 17 | 18 | globalStop context.CancelFunc 19 | smallStopFunc context.CancelFunc 20 | smallStopCtx context.Context 21 | } 22 | 23 | func NewWorker(commandLineClient *commandLine.Client) *Worker { 24 | return &Worker{ 25 | commandLineClient: commandLineClient, 26 | } 27 | } 28 | 29 | func (t *Worker) Run(ctx context.Context) error { 30 | if err := t.commandLineClient.Clone(ctx); err != nil { 31 | return fmt.Errorf("workers.Run error cloning: %w", err) 32 | } 33 | 34 | if err := t.commandLineClient.PullAll(ctx); err != nil { 35 | logrus.Errorf("workers.check error pulling first time: %v", err) 36 | } 37 | 38 | if err := t.commandLineClient.Checkout(ctx); err != nil { 39 | return fmt.Errorf("workers.Run error checkout: %w", err) 40 | } 41 | 42 | if err := t.commandLineClient.Maker(ctx); err != nil { 43 | logrus.Errorf("workers.Run error while making first make command: %v", err) 44 | } 45 | 46 | ctx, t.globalStop = context.WithCancel(ctx) 47 | t.smallStopCtx, t.smallStopFunc = context.WithCancel(ctx) 48 | t.smallStopFunc() 49 | 50 | go t.waitForInterruption() 51 | 52 | go t.check(ctx) 53 | 54 | for { 55 | runtime.GC() 56 | select { 57 | case <-ctx.Done(): 58 | logrus.Info("workers.Run successful stop") 59 | return nil 60 | case <-t.smallStopCtx.Done(): 61 | time.Sleep(10 * time.Second) 62 | smallStopCtx, smallStopFunc := context.WithCancel(ctx) 63 | t.smallStopCtx = smallStopCtx 64 | t.smallStopFunc = smallStopFunc 65 | if err := t.commandLineClient.Maker(t.smallStopCtx); err != nil { 66 | t.smallStopFunc() 67 | logrus.Errorf("workers.Run error while making make command: %v", err) 68 | } 69 | } 70 | } 71 | } 72 | 73 | func (t *Worker) check(ctx context.Context) { 74 | for { 75 | time.Sleep(1 * time.Second) 76 | action, err := t.commandLineClient.PullBranch(ctx) 77 | if err != nil { 78 | logrus.Errorf("workers.check error getting action: %v", err) 79 | continue 80 | } 81 | if action { 82 | t.smallStopFunc() 83 | } 84 | } 85 | } 86 | 87 | func (t *Worker) waitForInterruption() { 88 | c := make(chan os.Signal, 1) 89 | signal.Notify(c, os.Interrupt) 90 | <-c 91 | t.globalStop() 92 | } 93 | -------------------------------------------------------------------------------- /systemd_example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=script which runs git trigger 3 | Wants=network-online.target ssh-agent.service 4 | After=network-online.target ssh-agent.service 5 | 6 | [Service] 7 | Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/local/go/bin 8 | Environment=HOME=/root 9 | ExecStart=/root/go/bin/git_trigger tarmalonchik/project_name /root master master 10 | 11 | [Install] 12 | WantedBy=multi-user.target --------------------------------------------------------------------------------