From 02ceef5d45a76363cbe63000a454ca36ed9851e7 Mon Sep 17 00:00:00 2001 From: Mohamed Habib Date: Thu, 26 Sep 2024 11:56:12 -0700 Subject: [PATCH] feat drift app (#1724) * modify drift app --- .gitignore | 1 + ee/drift/controllers/github.go | 67 ++- ee/drift/dbmodels/github.go | 23 +- ee/drift/dbmodels/projects.go | 51 +++ ee/drift/dbmodels/repos.go | 53 +++ ee/drift/dbmodels/storage.go | 25 +- ee/drift/main.go | 1 + ee/drift/model/projects.gen.go | 1 + ee/drift/model/repos.gen.go | 2 +- ee/drift/model/user_settings.gen.go | 26 ++ ee/drift/model/users.gen.go | 4 +- ee/drift/models_generated/gen.go | 8 + ee/drift/models_generated/projects.gen.go | 6 +- ee/drift/models_generated/repos.gen.go | 6 +- .../models_generated/user_settings.gen.go | 400 ++++++++++++++++++ ee/drift/models_generated/users.gen.go | 18 +- ee/drift/tasks/github.go | 59 +++ ee/drift/utils/github.go | 87 ++++ 18 files changed, 817 insertions(+), 21 deletions(-) create mode 100644 ee/drift/dbmodels/projects.go create mode 100644 ee/drift/dbmodels/repos.go create mode 100644 ee/drift/model/user_settings.gen.go create mode 100644 ee/drift/models_generated/user_settings.gen.go create mode 100644 ee/drift/tasks/github.go create mode 100644 ee/drift/utils/github.go diff --git a/.gitignore b/.gitignore index bfa2ff3b8..de2ce9b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ **/.env +**/.env* .DS_Store venv/ **/__pycache__/ diff --git a/ee/drift/controllers/github.go b/ee/drift/controllers/github.go index 0c7337b5e..c87d745f8 100644 --- a/ee/drift/controllers/github.go +++ b/ee/drift/controllers/github.go @@ -4,9 +4,11 @@ import ( "context" "encoding/json" "fmt" + "github.com/diggerhq/digger/backend/utils" "github.com/diggerhq/digger/ee/drift/dbmodels" "github.com/diggerhq/digger/ee/drift/middleware" "github.com/diggerhq/digger/ee/drift/model" + "github.com/diggerhq/digger/ee/drift/tasks" next_utils "github.com/diggerhq/digger/next/utils" "github.com/gin-gonic/gin" "github.com/google/go-github/v61/github" @@ -14,10 +16,65 @@ import ( "log" "net/http" "os" + "reflect" "strconv" "strings" ) +func (mc MainController) GithubAppWebHook(c *gin.Context) { + c.Header("Content-Type", "application/json") + gh := mc.GithubClientProvider + log.Printf("GithubAppWebHook") + + payload, err := github.ValidatePayload(c.Request, []byte(os.Getenv("GITHUB_WEBHOOK_SECRET"))) + if err != nil { + log.Printf("Error validating github app webhook's payload: %v", err) + c.String(http.StatusBadRequest, "Error validating github app webhook's payload") + return + } + + webhookType := github.WebHookType(c.Request) + event, err := github.ParseWebHook(webhookType, payload) + if err != nil { + log.Printf("Failed to parse Github Event. :%v\n", err) + c.String(http.StatusInternalServerError, "Failed to parse Github Event") + return + } + + log.Printf("github event type: %v\n", reflect.TypeOf(event)) + + switch event := event.(type) { + case *github.PushEvent: + log.Printf("Got push event for %d", event.Repo.URL) + err := handlePushEvent(gh, event) + if err != nil { + log.Printf("handlePushEvent error: %v", err) + c.String(http.StatusInternalServerError, err.Error()) + return + } + default: + log.Printf("Unhandled event, event type %v", reflect.TypeOf(event)) + } + + c.JSON(200, "ok") +} + +func handlePushEvent(gh utils.GithubClientProvider, payload *github.PushEvent) error { + installationId := *payload.Installation.ID + repoName := *payload.Repo.Name + repoFullName := *payload.Repo.FullName + repoOwner := *payload.Repo.Owner.Login + cloneURL := *payload.Repo.CloneURL + ref := *payload.Ref + defaultBranch := *payload.Repo.DefaultBranch + + if strings.HasSuffix(ref, defaultBranch) { + go tasks.LoadProjectsFromGithubRepo(gh, strconv.FormatInt(installationId, 10), repoFullName, repoOwner, repoName, cloneURL, defaultBranch) + } + + return nil +} + func (mc MainController) GithubAppCallbackPage(c *gin.Context) { installationId := c.Request.URL.Query()["installation_id"][0] //setupAction := c.Request.URL.Query()["setup_action"][0] @@ -80,7 +137,7 @@ func (mc MainController) GithubAppCallbackPage(c *gin.Context) { // reset all existing repos (soft delete) var ExistingRepos []model.Repo - err = dbmodels.DB.GormDB.Delete(ExistingRepos, "organization_id=?", orgId).Error + err = dbmodels.DB.GormDB.Delete(ExistingRepos, "organisation_id=?", orgId).Error if err != nil { log.Printf("could not delete repos: %v", err) c.String(http.StatusInternalServerError, "could not delete repos: %v", err) @@ -89,20 +146,24 @@ func (mc MainController) GithubAppCallbackPage(c *gin.Context) { // here we mark repos that are available one by one for _, repo := range repos { + cloneUrl := *repo.CloneURL + defaultBranch := *repo.DefaultBranch repoFullName := *repo.FullName repoOwner := strings.Split(*repo.FullName, "/")[0] repoName := *repo.Name repoUrl := fmt.Sprintf("https://github.com/%v", repoFullName) - _, _, err = dbmodels.CreateOrGetDiggerRepoForGithubRepo(repoFullName, repoOwner, repoName, repoUrl, installationId64, *installation.AppID, *installation.Account.ID, *installation.Account.Login) + _, _, err = dbmodels.CreateOrGetDiggerRepoForGithubRepo(repoFullName, repoOwner, repoName, repoUrl, installationId, *installation.AppID, *installation.Account.ID, *installation.Account.Login) if err != nil { log.Printf("createOrGetDiggerRepoForGithubRepo error: %v", err) c.String(http.StatusInternalServerError, "createOrGetDiggerRepoForGithubRepo error: %v", err) return } + + go tasks.LoadProjectsFromGithubRepo(mc.GithubClientProvider, installationId, repoFullName, repoOwner, repoName, cloneUrl, defaultBranch) } - c.HTML(http.StatusOK, "github_success.tmpl", gin.H{}) + c.String(http.StatusOK, "success", gin.H{}) } // why this validation is needed: https://roadie.io/blog/avoid-leaking-github-org-data/ diff --git a/ee/drift/dbmodels/github.go b/ee/drift/dbmodels/github.go index 2bdb512c4..dbcc55b56 100644 --- a/ee/drift/dbmodels/github.go +++ b/ee/drift/dbmodels/github.go @@ -16,7 +16,7 @@ const ( GithubAppInstallationLinkInactive GithubAppInstallationLinkStatus = "inactive" ) -func (db *Database) GetGithubInstallationLinkForInstallationId(installationId int64) (*model.GithubAppInstallationLink, error) { +func (db *Database) GetGithubInstallationLinkForInstallationId(installationId string) (*model.GithubAppInstallationLink, error) { l := model.GithubAppInstallationLink{} err := db.GormDB.Where("github_installation_id = ? AND status=?", installationId, GithubAppInstallationLinkActive).Find(&l).Error if err != nil { @@ -28,7 +28,7 @@ func (db *Database) GetGithubInstallationLinkForInstallationId(installationId in return &l, nil } -func CreateOrGetDiggerRepoForGithubRepo(ghRepoFullName string, ghRepoOrganisation string, ghRepoName string, ghRepoUrl string, installationId int64, githubAppId int64, accountId int64, login string) (*model.Repo, *model.Organisation, error) { +func CreateOrGetDiggerRepoForGithubRepo(ghRepoFullName string, ghRepoOrganisation string, ghRepoName string, ghRepoUrl string, installationId string, githubAppId int64, accountId int64, login string) (*model.Repo, *model.Organisation, error) { link, err := DB.GetGithubInstallationLinkForInstallationId(installationId) if err != nil { log.Printf("Error fetching installation link: %v", err) @@ -45,7 +45,7 @@ func CreateOrGetDiggerRepoForGithubRepo(ghRepoFullName string, ghRepoOrganisatio // using Unscoped because we also need to include deleted repos (and undelete them if they exist) var existingRepo model.Repo - r := DB.GormDB.Unscoped().Where("organization_id=? AND repos.name=?", orgId, diggerRepoName).Find(&existingRepo) + r := DB.GormDB.Unscoped().Where("organisation_id=? AND repos.name=?", orgId, diggerRepoName).Find(&existingRepo) if r.Error != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -71,3 +71,20 @@ func CreateOrGetDiggerRepoForGithubRepo(ghRepoFullName string, ghRepoOrganisatio log.Printf("Created digger repo: %v", repo) return repo, org, nil } + +// GetGithubAppInstallationLink repoFullName should be in the following format: org/repo_name, for example "diggerhq/github-job-scheduler" +func (db *Database) GetGithubAppInstallationLink(installationId string) (*model.GithubAppInstallationLink, error) { + var link model.GithubAppInstallationLink + result := db.GormDB.Where("github_installation_id = ? AND status=?", installationId, GithubAppInstallationLinkActive).Find(&link) + if result.Error != nil { + if !errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, result.Error + } + } + + // If not found, the values will be default values, which means ID will be 0 + if link.ID == "" { + return nil, nil + } + return &link, nil +} diff --git a/ee/drift/dbmodels/projects.go b/ee/drift/dbmodels/projects.go new file mode 100644 index 000000000..c7ceaf344 --- /dev/null +++ b/ee/drift/dbmodels/projects.go @@ -0,0 +1,51 @@ +package dbmodels + +import ( + "errors" + "github.com/diggerhq/digger/ee/drift/model" + "gorm.io/gorm" + "log" +) + +type DriftStatus string + +var DriftStatusNewDrift = "new drift" +var DriftStatusNoDrift = "no drift" +var DriftStatusAcknowledgeDrift = "acknowledged drift" + +// GetProjectByName return project for specified org and repo +// if record doesn't exist return nil +func (db *Database) GetProjectByName(orgId any, repo *model.Repo, name string) (*model.Project, error) { + log.Printf("GetProjectByName, org id: %v, project name: %v\n", orgId, name) + var project model.Project + + err := db.GormDB. + Joins("INNER JOIN repos ON projects.repo_id = repos.id"). + Where("repos.id = ?", repo.ID). + Where("projects.name = ?", name).First(&project).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + log.Printf("Unknown error occurred while fetching database, %v\n", err) + return nil, err + } + + return &project, nil +} + +func (db *Database) CreateProject(name string, repo *model.Repo) (*model.Project, error) { + project := &model.Project{ + Name: name, + RepoID: repo.ID, + DriftStatus: DriftStatusNewDrift, + } + result := db.GormDB.Save(project) + if result.Error != nil { + log.Printf("Failed to create project: %v, error: %v\n", name, result.Error) + return nil, result.Error + } + log.Printf("Project %s, (id: %v) has been created successfully\n", name, project.ID) + return project, nil +} diff --git a/ee/drift/dbmodels/repos.go b/ee/drift/dbmodels/repos.go new file mode 100644 index 000000000..88f8e23c4 --- /dev/null +++ b/ee/drift/dbmodels/repos.go @@ -0,0 +1,53 @@ +package dbmodels + +import ( + "errors" + "fmt" + "github.com/diggerhq/digger/ee/drift/model" + configuration "github.com/diggerhq/digger/libs/digger_config" + "gorm.io/gorm" + "log" +) + +// GetRepo returns digger repo by organisationId and repo name (diggerhq-digger) +// it will return an empty object if record doesn't exist in database +func (db *Database) GetRepo(orgIdKey any, repoName string) (*model.Repo, error) { + var repo model.Repo + + err := db.GormDB.Where("organisation_id = ? AND repos.name=?", orgIdKey, repoName).First(&repo).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + log.Printf("Failed to find digger repo for orgId: %v, and repoName: %v, error: %v\n", orgIdKey, repoName, err) + return nil, err + } + return &repo, nil +} + +func (db *Database) RefreshProjectsFromRepo(orgId string, config configuration.DiggerConfigYaml, repo *model.Repo) error { + log.Printf("UpdateRepoDiggerConfig, repo: %v\n", repo) + + err := db.GormDB.Transaction(func(tx *gorm.DB) error { + for _, dc := range config.Projects { + projectName := dc.Name + p, err := db.GetProjectByName(orgId, repo, projectName) + if err != nil { + return fmt.Errorf("error retriving project by name: %v", err) + } + if p == nil { + _, err := db.CreateProject(projectName, repo) + if err != nil { + return fmt.Errorf("could not create project: %v", err) + } + } + } + return nil + }) + + if err != nil { + return fmt.Errorf("error while updating projects from config: %v", err) + } + return nil +} diff --git a/ee/drift/dbmodels/storage.go b/ee/drift/dbmodels/storage.go index f0991bae8..c80f3168f 100644 --- a/ee/drift/dbmodels/storage.go +++ b/ee/drift/dbmodels/storage.go @@ -21,7 +21,7 @@ func (db *Database) GetOrganisationById(orgId any) (*model.Organisation, error) func (db *Database) CreateGithubInstallationLink(orgId string, installationId string) (*model.GithubAppInstallationLink, error) { l := model.GithubAppInstallationLink{} // check if there is already a link to another org, and throw an error in this case - result := db.GormDB.Preload("Organisation").Where("github_installation_id = ? AND status=?", installationId, GithubAppInstallationLinkActive).Find(&l) + result := db.GormDB.Where("github_installation_id = ? AND status=?", installationId, GithubAppInstallationLinkActive).Find(&l) if result.Error != nil { if !errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, result.Error @@ -38,7 +38,7 @@ func (db *Database) CreateGithubInstallationLink(orgId string, installationId st var list []model.GithubAppInstallationLink // if there are other installation for this org, we need to make them inactive - result = db.GormDB.Preload("Organisation").Where("github_installation_id <> ? AND organisation_id = ? AND status=?", installationId, orgId, GithubAppInstallationLinkActive).Find(&list) + result = db.GormDB.Where("github_installation_id <> ? AND organisation_id = ? AND status=?", installationId, orgId, GithubAppInstallationLinkActive).Find(&list) if result.Error != nil { if !errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, result.Error @@ -58,10 +58,10 @@ func (db *Database) CreateGithubInstallationLink(orgId string, installationId st return &link, nil } -func (db *Database) CreateRepo(name string, repoFullName string, repoOrganisation string, repoName string, repoUrl string, org *model.Organisation, diggerConfig string, githubInstallationId int64, githubAppId int64, accountId int64, login string) (*model.Repo, error) { +func (db *Database) CreateRepo(name string, repoFullName string, repoOrganisation string, repoName string, repoUrl string, org *model.Organisation, diggerConfig string, githubInstallationId string, githubAppId int64, accountId int64, login string) (*model.Repo, error) { var repo model.Repo // check if repo exist already, do nothing in this case - result := db.GormDB.Where("name = ? AND organization_id=?", name, org.ID).Find(&repo) + result := db.GormDB.Where("name = ? AND organisation_id=?", name, org.ID).Find(&repo) if result.Error != nil { if !errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, result.Error @@ -92,3 +92,20 @@ func (db *Database) CreateRepo(name string, repoFullName string, repoOrganisatio log.Printf("Repo %s, (id: %v) has been created successfully\n", name, repo.ID) return &repo, nil } + +// GetGithubAppInstallationByIdAndRepo repoFullName should be in the following format: org/repo_name, for example "diggerhq/github-job-scheduler" +func (db *Database) GetRepoByInstllationIdAndRepoFullName(installationId string, repoFullName string) (*model.Repo, error) { + repo := model.Repo{} + result := db.GormDB.Where("github_installation_id = ? AND repo_full_name=?", installationId, repoFullName).Find(&repo) + if result.Error != nil { + if !errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, result.Error + } + } + + // If not found, the values will be default values, which means ID will be 0 + if repo.ID == "" { + return nil, fmt.Errorf("GithubAppInstallation with id=%v doesn't exist", installationId) + } + return &repo, nil +} diff --git a/ee/drift/main.go b/ee/drift/main.go index 2c1133542..445a3de78 100644 --- a/ee/drift/main.go +++ b/ee/drift/main.go @@ -69,6 +69,7 @@ func main() { //authorized := r.Group("/") //authorized.Use(middleware.GetApiMiddleware(), middleware.AccessLevel(dbmodels.CliJobAccessType, dbmodels.AccessPolicyType, models.AdminPolicyType)) + r.POST("github-app-webhook", controller.GithubAppWebHook) r.GET("/github/callback_fe", middleware.WebhookAuth(), controller.GithubAppCallbackPage) port := os.Getenv("DIGGER_PORT") diff --git a/ee/drift/model/projects.gen.go b/ee/drift/model/projects.gen.go index 0af271a0c..7f7d8524a 100644 --- a/ee/drift/model/projects.gen.go +++ b/ee/drift/model/projects.gen.go @@ -27,6 +27,7 @@ type Project struct { ToUpdate int32 `gorm:"column:to_update" json:"to_update"` ToChange int32 `gorm:"column:to_change" json:"to_change"` ToDelete int32 `gorm:"column:to_delete" json:"to_delete"` + IsAcknowledged bool `gorm:"column:is_acknowledged;not null" json:"is_acknowledged"` } // TableName Project's table name diff --git a/ee/drift/model/repos.gen.go b/ee/drift/model/repos.gen.go index eb4d0d56e..355149923 100644 --- a/ee/drift/model/repos.gen.go +++ b/ee/drift/model/repos.gen.go @@ -25,7 +25,7 @@ type Repo struct { RepoOrganisation string `gorm:"column:repo_organisation" json:"repo_organisation"` RepoName string `gorm:"column:repo_name" json:"repo_name"` RepoURL string `gorm:"column:repo_url" json:"repo_url"` - GithubInstallationID int64 `gorm:"column:github_installation_id" json:"github_installation_id"` + GithubInstallationID string `gorm:"column:github_installation_id" json:"github_installation_id"` GithubAppID int64 `gorm:"column:github_app_id" json:"github_app_id"` AccountID int64 `gorm:"column:account_id" json:"account_id"` Login string `gorm:"column:login" json:"login"` diff --git a/ee/drift/model/user_settings.gen.go b/ee/drift/model/user_settings.gen.go new file mode 100644 index 000000000..132f40102 --- /dev/null +++ b/ee/drift/model/user_settings.gen.go @@ -0,0 +1,26 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameUserSetting = "user_settings" + +// UserSetting mapped from table +type UserSetting struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` + CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"created_at"` + ScheduleType string `gorm:"column:schedule_type" json:"schedule_type"` + Schedule string `gorm:"column:schedule" json:"schedule"` + SlackNotificationURL string `gorm:"column:slack_notification_url" json:"slack_notification_url"` + UserID string `gorm:"column:user_id;not null" json:"user_id"` +} + +// TableName UserSetting's table name +func (*UserSetting) TableName() string { + return TableNameUserSetting +} diff --git a/ee/drift/model/users.gen.go b/ee/drift/model/users.gen.go index bee150336..e171f3d91 100644 --- a/ee/drift/model/users.gen.go +++ b/ee/drift/model/users.gen.go @@ -19,8 +19,10 @@ type User struct { UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"` Email string `gorm:"column:email;not null" json:"email"` - Name string `gorm:"column:name" json:"name"` OrganisationID string `gorm:"column:organisation_id" json:"organisation_id"` + ExternalID string `gorm:"column:external_id" json:"external_id"` + FirstName string `gorm:"column:first_name" json:"first_name"` + LastName string `gorm:"column:last_name" json:"last_name"` } // TableName User's table name diff --git a/ee/drift/models_generated/gen.go b/ee/drift/models_generated/gen.go index c2b5c2441..69e7b24a7 100644 --- a/ee/drift/models_generated/gen.go +++ b/ee/drift/models_generated/gen.go @@ -22,6 +22,7 @@ var ( Project *project Repo *repo User *user + UserSetting *userSetting ) func SetDefault(db *gorm.DB, opts ...gen.DOOption) { @@ -31,6 +32,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { Project = &Q.Project Repo = &Q.Repo User = &Q.User + UserSetting = &Q.UserSetting } func Use(db *gorm.DB, opts ...gen.DOOption) *Query { @@ -41,6 +43,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query { Project: newProject(db, opts...), Repo: newRepo(db, opts...), User: newUser(db, opts...), + UserSetting: newUserSetting(db, opts...), } } @@ -52,6 +55,7 @@ type Query struct { Project project Repo repo User user + UserSetting userSetting } func (q *Query) Available() bool { return q.db != nil } @@ -64,6 +68,7 @@ func (q *Query) clone(db *gorm.DB) *Query { Project: q.Project.clone(db), Repo: q.Repo.clone(db), User: q.User.clone(db), + UserSetting: q.UserSetting.clone(db), } } @@ -83,6 +88,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query { Project: q.Project.replaceDB(db), Repo: q.Repo.replaceDB(db), User: q.User.replaceDB(db), + UserSetting: q.UserSetting.replaceDB(db), } } @@ -92,6 +98,7 @@ type queryCtx struct { Project IProjectDo Repo IRepoDo User IUserDo + UserSetting IUserSettingDo } func (q *Query) WithContext(ctx context.Context) *queryCtx { @@ -101,6 +108,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx { Project: q.Project.WithContext(ctx), Repo: q.Repo.WithContext(ctx), User: q.User.WithContext(ctx), + UserSetting: q.UserSetting.WithContext(ctx), } } diff --git a/ee/drift/models_generated/projects.gen.go b/ee/drift/models_generated/projects.gen.go index 5bd74222f..e786492af 100644 --- a/ee/drift/models_generated/projects.gen.go +++ b/ee/drift/models_generated/projects.gen.go @@ -40,6 +40,7 @@ func newProject(db *gorm.DB, opts ...gen.DOOption) project { _project.ToUpdate = field.NewInt32(tableName, "to_update") _project.ToChange = field.NewInt32(tableName, "to_change") _project.ToDelete = field.NewInt32(tableName, "to_delete") + _project.IsAcknowledged = field.NewBool(tableName, "is_acknowledged") _project.fillFieldMap() @@ -63,6 +64,7 @@ type project struct { ToUpdate field.Int32 ToChange field.Int32 ToDelete field.Int32 + IsAcknowledged field.Bool fieldMap map[string]field.Expr } @@ -92,6 +94,7 @@ func (p *project) updateTableName(table string) *project { p.ToUpdate = field.NewInt32(table, "to_update") p.ToChange = field.NewInt32(table, "to_change") p.ToDelete = field.NewInt32(table, "to_delete") + p.IsAcknowledged = field.NewBool(table, "is_acknowledged") p.fillFieldMap() @@ -108,7 +111,7 @@ func (p *project) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (p *project) fillFieldMap() { - p.fieldMap = make(map[string]field.Expr, 13) + p.fieldMap = make(map[string]field.Expr, 14) p.fieldMap["id"] = p.ID p.fieldMap["created_at"] = p.CreatedAt p.fieldMap["updated_at"] = p.UpdatedAt @@ -122,6 +125,7 @@ func (p *project) fillFieldMap() { p.fieldMap["to_update"] = p.ToUpdate p.fieldMap["to_change"] = p.ToChange p.fieldMap["to_delete"] = p.ToDelete + p.fieldMap["is_acknowledged"] = p.IsAcknowledged } func (p project) clone(db *gorm.DB) project { diff --git a/ee/drift/models_generated/repos.gen.go b/ee/drift/models_generated/repos.gen.go index 50b52023f..447edfe58 100644 --- a/ee/drift/models_generated/repos.gen.go +++ b/ee/drift/models_generated/repos.gen.go @@ -38,7 +38,7 @@ func newRepo(db *gorm.DB, opts ...gen.DOOption) repo { _repo.RepoOrganisation = field.NewString(tableName, "repo_organisation") _repo.RepoName = field.NewString(tableName, "repo_name") _repo.RepoURL = field.NewString(tableName, "repo_url") - _repo.GithubInstallationID = field.NewInt64(tableName, "github_installation_id") + _repo.GithubInstallationID = field.NewString(tableName, "github_installation_id") _repo.GithubAppID = field.NewInt64(tableName, "github_app_id") _repo.AccountID = field.NewInt64(tableName, "account_id") _repo.Login = field.NewString(tableName, "login") @@ -63,7 +63,7 @@ type repo struct { RepoOrganisation field.String RepoName field.String RepoURL field.String - GithubInstallationID field.Int64 + GithubInstallationID field.String GithubAppID field.Int64 AccountID field.Int64 Login field.String @@ -94,7 +94,7 @@ func (r *repo) updateTableName(table string) *repo { r.RepoOrganisation = field.NewString(table, "repo_organisation") r.RepoName = field.NewString(table, "repo_name") r.RepoURL = field.NewString(table, "repo_url") - r.GithubInstallationID = field.NewInt64(table, "github_installation_id") + r.GithubInstallationID = field.NewString(table, "github_installation_id") r.GithubAppID = field.NewInt64(table, "github_app_id") r.AccountID = field.NewInt64(table, "account_id") r.Login = field.NewString(table, "login") diff --git a/ee/drift/models_generated/user_settings.gen.go b/ee/drift/models_generated/user_settings.gen.go new file mode 100644 index 000000000..6396392c2 --- /dev/null +++ b/ee/drift/models_generated/user_settings.gen.go @@ -0,0 +1,400 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package models_generated + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "gorm.io/gen" + "gorm.io/gen/field" + + "gorm.io/plugin/dbresolver" + + "github.com/diggerhq/digger/ee/drift/model" +) + +func newUserSetting(db *gorm.DB, opts ...gen.DOOption) userSetting { + _userSetting := userSetting{} + + _userSetting.userSettingDo.UseDB(db, opts...) + _userSetting.userSettingDo.UseModel(&model.UserSetting{}) + + tableName := _userSetting.userSettingDo.TableName() + _userSetting.ALL = field.NewAsterisk(tableName) + _userSetting.ID = field.NewInt64(tableName, "id") + _userSetting.CreatedAt = field.NewTime(tableName, "created_at") + _userSetting.ScheduleType = field.NewString(tableName, "schedule_type") + _userSetting.Schedule = field.NewString(tableName, "schedule") + _userSetting.SlackNotificationURL = field.NewString(tableName, "slack_notification_url") + _userSetting.UserID = field.NewString(tableName, "user_id") + + _userSetting.fillFieldMap() + + return _userSetting +} + +type userSetting struct { + userSettingDo + + ALL field.Asterisk + ID field.Int64 + CreatedAt field.Time + ScheduleType field.String + Schedule field.String + SlackNotificationURL field.String + UserID field.String + + fieldMap map[string]field.Expr +} + +func (u userSetting) Table(newTableName string) *userSetting { + u.userSettingDo.UseTable(newTableName) + return u.updateTableName(newTableName) +} + +func (u userSetting) As(alias string) *userSetting { + u.userSettingDo.DO = *(u.userSettingDo.As(alias).(*gen.DO)) + return u.updateTableName(alias) +} + +func (u *userSetting) updateTableName(table string) *userSetting { + u.ALL = field.NewAsterisk(table) + u.ID = field.NewInt64(table, "id") + u.CreatedAt = field.NewTime(table, "created_at") + u.ScheduleType = field.NewString(table, "schedule_type") + u.Schedule = field.NewString(table, "schedule") + u.SlackNotificationURL = field.NewString(table, "slack_notification_url") + u.UserID = field.NewString(table, "user_id") + + u.fillFieldMap() + + return u +} + +func (u *userSetting) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := u.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (u *userSetting) fillFieldMap() { + u.fieldMap = make(map[string]field.Expr, 6) + u.fieldMap["id"] = u.ID + u.fieldMap["created_at"] = u.CreatedAt + u.fieldMap["schedule_type"] = u.ScheduleType + u.fieldMap["schedule"] = u.Schedule + u.fieldMap["slack_notification_url"] = u.SlackNotificationURL + u.fieldMap["user_id"] = u.UserID +} + +func (u userSetting) clone(db *gorm.DB) userSetting { + u.userSettingDo.ReplaceConnPool(db.Statement.ConnPool) + return u +} + +func (u userSetting) replaceDB(db *gorm.DB) userSetting { + u.userSettingDo.ReplaceDB(db) + return u +} + +type userSettingDo struct{ gen.DO } + +type IUserSettingDo interface { + gen.SubQuery + Debug() IUserSettingDo + WithContext(ctx context.Context) IUserSettingDo + WithResult(fc func(tx gen.Dao)) gen.ResultInfo + ReplaceDB(db *gorm.DB) + ReadDB() IUserSettingDo + WriteDB() IUserSettingDo + As(alias string) gen.Dao + Session(config *gorm.Session) IUserSettingDo + Columns(cols ...field.Expr) gen.Columns + Clauses(conds ...clause.Expression) IUserSettingDo + Not(conds ...gen.Condition) IUserSettingDo + Or(conds ...gen.Condition) IUserSettingDo + Select(conds ...field.Expr) IUserSettingDo + Where(conds ...gen.Condition) IUserSettingDo + Order(conds ...field.Expr) IUserSettingDo + Distinct(cols ...field.Expr) IUserSettingDo + Omit(cols ...field.Expr) IUserSettingDo + Join(table schema.Tabler, on ...field.Expr) IUserSettingDo + LeftJoin(table schema.Tabler, on ...field.Expr) IUserSettingDo + RightJoin(table schema.Tabler, on ...field.Expr) IUserSettingDo + Group(cols ...field.Expr) IUserSettingDo + Having(conds ...gen.Condition) IUserSettingDo + Limit(limit int) IUserSettingDo + Offset(offset int) IUserSettingDo + Count() (count int64, err error) + Scopes(funcs ...func(gen.Dao) gen.Dao) IUserSettingDo + Unscoped() IUserSettingDo + Create(values ...*model.UserSetting) error + CreateInBatches(values []*model.UserSetting, batchSize int) error + Save(values ...*model.UserSetting) error + First() (*model.UserSetting, error) + Take() (*model.UserSetting, error) + Last() (*model.UserSetting, error) + Find() ([]*model.UserSetting, error) + FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UserSetting, err error) + FindInBatches(result *[]*model.UserSetting, batchSize int, fc func(tx gen.Dao, batch int) error) error + Pluck(column field.Expr, dest interface{}) error + Delete(...*model.UserSetting) (info gen.ResultInfo, err error) + Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error) + UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error) + Updates(value interface{}) (info gen.ResultInfo, err error) + UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error) + UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error) + UpdateColumns(value interface{}) (info gen.ResultInfo, err error) + UpdateFrom(q gen.SubQuery) gen.Dao + Attrs(attrs ...field.AssignExpr) IUserSettingDo + Assign(attrs ...field.AssignExpr) IUserSettingDo + Joins(fields ...field.RelationField) IUserSettingDo + Preload(fields ...field.RelationField) IUserSettingDo + FirstOrInit() (*model.UserSetting, error) + FirstOrCreate() (*model.UserSetting, error) + FindByPage(offset int, limit int) (result []*model.UserSetting, count int64, err error) + ScanByPage(result interface{}, offset int, limit int) (count int64, err error) + Scan(result interface{}) (err error) + Returning(value interface{}, columns ...string) IUserSettingDo + UnderlyingDB() *gorm.DB + schema.Tabler +} + +func (u userSettingDo) Debug() IUserSettingDo { + return u.withDO(u.DO.Debug()) +} + +func (u userSettingDo) WithContext(ctx context.Context) IUserSettingDo { + return u.withDO(u.DO.WithContext(ctx)) +} + +func (u userSettingDo) ReadDB() IUserSettingDo { + return u.Clauses(dbresolver.Read) +} + +func (u userSettingDo) WriteDB() IUserSettingDo { + return u.Clauses(dbresolver.Write) +} + +func (u userSettingDo) Session(config *gorm.Session) IUserSettingDo { + return u.withDO(u.DO.Session(config)) +} + +func (u userSettingDo) Clauses(conds ...clause.Expression) IUserSettingDo { + return u.withDO(u.DO.Clauses(conds...)) +} + +func (u userSettingDo) Returning(value interface{}, columns ...string) IUserSettingDo { + return u.withDO(u.DO.Returning(value, columns...)) +} + +func (u userSettingDo) Not(conds ...gen.Condition) IUserSettingDo { + return u.withDO(u.DO.Not(conds...)) +} + +func (u userSettingDo) Or(conds ...gen.Condition) IUserSettingDo { + return u.withDO(u.DO.Or(conds...)) +} + +func (u userSettingDo) Select(conds ...field.Expr) IUserSettingDo { + return u.withDO(u.DO.Select(conds...)) +} + +func (u userSettingDo) Where(conds ...gen.Condition) IUserSettingDo { + return u.withDO(u.DO.Where(conds...)) +} + +func (u userSettingDo) Order(conds ...field.Expr) IUserSettingDo { + return u.withDO(u.DO.Order(conds...)) +} + +func (u userSettingDo) Distinct(cols ...field.Expr) IUserSettingDo { + return u.withDO(u.DO.Distinct(cols...)) +} + +func (u userSettingDo) Omit(cols ...field.Expr) IUserSettingDo { + return u.withDO(u.DO.Omit(cols...)) +} + +func (u userSettingDo) Join(table schema.Tabler, on ...field.Expr) IUserSettingDo { + return u.withDO(u.DO.Join(table, on...)) +} + +func (u userSettingDo) LeftJoin(table schema.Tabler, on ...field.Expr) IUserSettingDo { + return u.withDO(u.DO.LeftJoin(table, on...)) +} + +func (u userSettingDo) RightJoin(table schema.Tabler, on ...field.Expr) IUserSettingDo { + return u.withDO(u.DO.RightJoin(table, on...)) +} + +func (u userSettingDo) Group(cols ...field.Expr) IUserSettingDo { + return u.withDO(u.DO.Group(cols...)) +} + +func (u userSettingDo) Having(conds ...gen.Condition) IUserSettingDo { + return u.withDO(u.DO.Having(conds...)) +} + +func (u userSettingDo) Limit(limit int) IUserSettingDo { + return u.withDO(u.DO.Limit(limit)) +} + +func (u userSettingDo) Offset(offset int) IUserSettingDo { + return u.withDO(u.DO.Offset(offset)) +} + +func (u userSettingDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IUserSettingDo { + return u.withDO(u.DO.Scopes(funcs...)) +} + +func (u userSettingDo) Unscoped() IUserSettingDo { + return u.withDO(u.DO.Unscoped()) +} + +func (u userSettingDo) Create(values ...*model.UserSetting) error { + if len(values) == 0 { + return nil + } + return u.DO.Create(values) +} + +func (u userSettingDo) CreateInBatches(values []*model.UserSetting, batchSize int) error { + return u.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (u userSettingDo) Save(values ...*model.UserSetting) error { + if len(values) == 0 { + return nil + } + return u.DO.Save(values) +} + +func (u userSettingDo) First() (*model.UserSetting, error) { + if result, err := u.DO.First(); err != nil { + return nil, err + } else { + return result.(*model.UserSetting), nil + } +} + +func (u userSettingDo) Take() (*model.UserSetting, error) { + if result, err := u.DO.Take(); err != nil { + return nil, err + } else { + return result.(*model.UserSetting), nil + } +} + +func (u userSettingDo) Last() (*model.UserSetting, error) { + if result, err := u.DO.Last(); err != nil { + return nil, err + } else { + return result.(*model.UserSetting), nil + } +} + +func (u userSettingDo) Find() ([]*model.UserSetting, error) { + result, err := u.DO.Find() + return result.([]*model.UserSetting), err +} + +func (u userSettingDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UserSetting, err error) { + buf := make([]*model.UserSetting, 0, batchSize) + err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (u userSettingDo) FindInBatches(result *[]*model.UserSetting, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return u.DO.FindInBatches(result, batchSize, fc) +} + +func (u userSettingDo) Attrs(attrs ...field.AssignExpr) IUserSettingDo { + return u.withDO(u.DO.Attrs(attrs...)) +} + +func (u userSettingDo) Assign(attrs ...field.AssignExpr) IUserSettingDo { + return u.withDO(u.DO.Assign(attrs...)) +} + +func (u userSettingDo) Joins(fields ...field.RelationField) IUserSettingDo { + for _, _f := range fields { + u = *u.withDO(u.DO.Joins(_f)) + } + return &u +} + +func (u userSettingDo) Preload(fields ...field.RelationField) IUserSettingDo { + for _, _f := range fields { + u = *u.withDO(u.DO.Preload(_f)) + } + return &u +} + +func (u userSettingDo) FirstOrInit() (*model.UserSetting, error) { + if result, err := u.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*model.UserSetting), nil + } +} + +func (u userSettingDo) FirstOrCreate() (*model.UserSetting, error) { + if result, err := u.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*model.UserSetting), nil + } +} + +func (u userSettingDo) FindByPage(offset int, limit int) (result []*model.UserSetting, count int64, err error) { + result, err = u.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = u.Offset(-1).Limit(-1).Count() + return +} + +func (u userSettingDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = u.Count() + if err != nil { + return + } + + err = u.Offset(offset).Limit(limit).Scan(result) + return +} + +func (u userSettingDo) Scan(result interface{}) (err error) { + return u.DO.Scan(result) +} + +func (u userSettingDo) Delete(models ...*model.UserSetting) (result gen.ResultInfo, err error) { + return u.DO.Delete(models) +} + +func (u *userSettingDo) withDO(do gen.Dao) *userSettingDo { + u.DO = *do.(*gen.DO) + return u +} diff --git a/ee/drift/models_generated/users.gen.go b/ee/drift/models_generated/users.gen.go index 7e1a531f0..0cfc80088 100644 --- a/ee/drift/models_generated/users.gen.go +++ b/ee/drift/models_generated/users.gen.go @@ -32,8 +32,10 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user { _user.UpdatedAt = field.NewTime(tableName, "updated_at") _user.DeletedAt = field.NewField(tableName, "deleted_at") _user.Email = field.NewString(tableName, "email") - _user.Name = field.NewString(tableName, "name") _user.OrganisationID = field.NewString(tableName, "organisation_id") + _user.ExternalID = field.NewString(tableName, "external_id") + _user.FirstName = field.NewString(tableName, "first_name") + _user.LastName = field.NewString(tableName, "last_name") _user.fillFieldMap() @@ -49,8 +51,10 @@ type user struct { UpdatedAt field.Time DeletedAt field.Field Email field.String - Name field.String OrganisationID field.String + ExternalID field.String + FirstName field.String + LastName field.String fieldMap map[string]field.Expr } @@ -72,8 +76,10 @@ func (u *user) updateTableName(table string) *user { u.UpdatedAt = field.NewTime(table, "updated_at") u.DeletedAt = field.NewField(table, "deleted_at") u.Email = field.NewString(table, "email") - u.Name = field.NewString(table, "name") u.OrganisationID = field.NewString(table, "organisation_id") + u.ExternalID = field.NewString(table, "external_id") + u.FirstName = field.NewString(table, "first_name") + u.LastName = field.NewString(table, "last_name") u.fillFieldMap() @@ -90,14 +96,16 @@ func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (u *user) fillFieldMap() { - u.fieldMap = make(map[string]field.Expr, 7) + u.fieldMap = make(map[string]field.Expr, 9) u.fieldMap["id"] = u.ID u.fieldMap["created_at"] = u.CreatedAt u.fieldMap["updated_at"] = u.UpdatedAt u.fieldMap["deleted_at"] = u.DeletedAt u.fieldMap["email"] = u.Email - u.fieldMap["name"] = u.Name u.fieldMap["organisation_id"] = u.OrganisationID + u.fieldMap["external_id"] = u.ExternalID + u.fieldMap["first_name"] = u.FirstName + u.fieldMap["last_name"] = u.LastName } func (u user) clone(db *gorm.DB) user { diff --git a/ee/drift/tasks/github.go b/ee/drift/tasks/github.go new file mode 100644 index 000000000..b7b51d1b4 --- /dev/null +++ b/ee/drift/tasks/github.go @@ -0,0 +1,59 @@ +package tasks + +import ( + "fmt" + utils3 "github.com/diggerhq/digger/backend/utils" + "github.com/diggerhq/digger/ee/drift/dbmodels" + "github.com/diggerhq/digger/ee/drift/utils" + dg_configuration "github.com/diggerhq/digger/libs/digger_config" + utils2 "github.com/diggerhq/digger/next/utils" + "log" + "strconv" + "strings" +) + +func LoadProjectsFromGithubRepo(gh utils2.GithubClientProvider, installationId string, repoFullName string, repoOwner string, repoName string, cloneUrl string, branch string) error { + link, err := dbmodels.DB.GetGithubAppInstallationLink(installationId) + if err != nil { + log.Printf("Error getting GetGithubAppInstallationLink: %v", err) + return fmt.Errorf("error getting github app link") + } + + orgId := link.OrganisationID + diggerRepoName := strings.ReplaceAll(repoFullName, "/", "-") + repo, err := dbmodels.DB.GetRepo(orgId, diggerRepoName) + if err != nil { + log.Printf("Error getting Repo: %v", err) + return fmt.Errorf("error getting github app link") + } + if repo == nil { + log.Printf("Repo not found: Org: %v | repo: %v", orgId, diggerRepoName) + return fmt.Errorf("Repo not found: Org: %v | repo: %v", orgId, diggerRepoName) + } + + installationId64, err := strconv.ParseInt(installationId, 10, 64) + if err != nil { + log.Printf("failed to convert installation id %v to int64", installationId) + return fmt.Errorf("failed to convert installation id to int64") + } + _, token, err := utils.GetGithubService(gh, installationId64, repoFullName, repoOwner, repoName) + if err != nil { + log.Printf("Error getting github service: %v", err) + return fmt.Errorf("error getting github service") + } + + err = utils3.CloneGitRepoAndDoAction(cloneUrl, branch, *token, func(dir string) error { + config, err := dg_configuration.LoadDiggerConfigYaml(dir, true, nil) + if err != nil { + log.Printf("ERROR load digger.yml: %v", err) + return fmt.Errorf("error loading digger.yml %v", err) + } + dbmodels.DB.RefreshProjectsFromRepo(link.OrganisationID, *config, repo) + return nil + }) + if err != nil { + return fmt.Errorf("error while cloning repo: %v", err) + } + + return nil +} diff --git a/ee/drift/utils/github.go b/ee/drift/utils/github.go new file mode 100644 index 000000000..048d33114 --- /dev/null +++ b/ee/drift/utils/github.go @@ -0,0 +1,87 @@ +package utils + +import ( + "context" + "encoding/base64" + "fmt" + "github.com/bradleyfalzon/ghinstallation/v2" + "github.com/diggerhq/digger/ee/drift/dbmodels" + github2 "github.com/diggerhq/digger/libs/ci/github" + "github.com/diggerhq/digger/next/utils" + "github.com/google/go-github/v61/github" + "log" + net "net/http" + "os" + "strconv" +) + +func GetGithubClient(gh utils.GithubClientProvider, installationId int64, repoFullName string) (*github.Client, *string, error) { + repo, err := dbmodels.DB.GetRepoByInstllationIdAndRepoFullName(strconv.FormatInt(installationId, 10), repoFullName) + if err != nil { + log.Printf("Error getting repo: %v", err) + return nil, nil, fmt.Errorf("Error getting repo: %v", err) + } + + ghClient, token, err := gh.Get(repo.GithubAppID, installationId) + return ghClient, token, err +} + +func GetGithubService(gh utils.GithubClientProvider, installationId int64, repoFullName string, repoOwner string, repoName string) (*github2.GithubService, *string, error) { + ghClient, token, err := GetGithubClient(gh, installationId, repoFullName) + if err != nil { + log.Printf("Error creating github app client: %v", err) + return nil, nil, fmt.Errorf("Error creating github app client: %v", err) + } + + ghService := github2.GithubService{ + Client: ghClient, + RepoName: repoName, + Owner: repoOwner, + } + + return &ghService, token, nil +} + +type DiggerGithubRealClientProvider struct { +} + +func (gh DiggerGithubRealClientProvider) NewClient(netClient *net.Client) (*github.Client, error) { + ghClient := github.NewClient(netClient) + return ghClient, nil +} + +func (gh DiggerGithubRealClientProvider) Get(githubAppId int64, installationId int64) (*github.Client, *string, error) { + githubAppPrivateKey := "" + githubAppPrivateKeyB64 := os.Getenv("GITHUB_APP_PRIVATE_KEY_BASE64") + if githubAppPrivateKeyB64 != "" { + decodedBytes, err := base64.StdEncoding.DecodeString(githubAppPrivateKeyB64) + if err != nil { + return nil, nil, fmt.Errorf("error initialising github app installation: please set GITHUB_APP_PRIVATE_KEY_BASE64 env variable\n") + } + githubAppPrivateKey = string(decodedBytes) + } else { + githubAppPrivateKey = os.Getenv("GITHUB_APP_PRIVATE_KEY") + if githubAppPrivateKey != "" { + log.Printf("WARNING: GITHUB_APP_PRIVATE_KEY will be deprecated in future releases, " + + "please use GITHUB_APP_PRIVATE_KEY_BASE64 instead") + } else { + return nil, nil, fmt.Errorf("error initialising github app installation: please set GITHUB_APP_PRIVATE_KEY_BASE64 env variable\n") + } + } + + tr := net.DefaultTransport + itr, err := ghinstallation.New(tr, githubAppId, installationId, []byte(githubAppPrivateKey)) + if err != nil { + return nil, nil, fmt.Errorf("error initialising github app installation: %v\n", err) + } + + token, err := itr.Token(context.Background()) + if err != nil { + return nil, nil, fmt.Errorf("error initialising git app token: %v\n", err) + } + ghClient, err := gh.NewClient(&net.Client{Transport: itr}) + if err != nil { + log.Printf("error creating new client: %v", err) + } + return ghClient, &token, nil +}