Skip to content

Commit 23a21f1

Browse files
authored
Merge pull request #131 from trymist/github-deployment
GitHub deployment
2 parents 13fce82 + 5b32e2f commit 23a21f1

9 files changed

Lines changed: 180 additions & 82 deletions

File tree

server/api/handlers/deployments/AddDeployHandler.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/corecollectives/mist/api/handlers"
88
"github.com/corecollectives/mist/api/middleware"
99
"github.com/corecollectives/mist/git"
10+
"github.com/corecollectives/mist/github"
1011
"github.com/corecollectives/mist/models"
1112
"github.com/corecollectives/mist/queue"
1213
"github.com/rs/zerolog/log"
@@ -74,6 +75,19 @@ func AddDeployHandler(w http.ResponseWriter, r *http.Request) {
7475
return
7576
}
7677

78+
// create github deployment
79+
if app.GitRepository != nil {
80+
depId, err := github.CreateDeployment(*app.GitRepository, app.GitBranch, int(user.ID))
81+
if err != nil {
82+
log.Err(err).Msg("failed to create github deployment")
83+
}
84+
85+
deployment.GithubDepId = &depId
86+
err = deployment.UpdateDeployment()
87+
if err != nil {
88+
log.Err(err).Msg("failed to update deployment with GH dep id")
89+
}
90+
}
7791
if err := queue.AddJob(int64(deployment.ID)); err != nil {
7892
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to add job to queue", err.Error())
7993
return

server/api/handlers/github/createApp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ func CreateGithubApp(w http.ResponseWriter, r *http.Request) {
7878
DefaultPermissions: map[string]string{
7979
"contents": "read",
8080
"metadata": "read",
81-
"pull_requests": "read",
82-
"deployments": "read",
81+
"pull_requests": "write",
82+
"deployments": "write",
8383
"administration": "write",
8484
"repository_hooks": "write",
8585
},

server/api/handlers/github/getBranches.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func GetBranches(w http.ResponseWriter, r *http.Request) {
3030
return
3131
}
3232

33-
token, err := github.GetGitHubAccessToken(int(userInfo.ID))
33+
token, _, err := github.GetGitHubAccessToken(int(userInfo.ID))
3434
if err != nil {
3535
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to get GitHub access token", err.Error())
3636
return

server/api/handlers/github/repositories.go

Lines changed: 36 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,12 @@ package github
22

33
import (
44
"encoding/json"
5-
"errors"
65
"fmt"
76
"net/http"
8-
"time"
97

108
"github.com/corecollectives/mist/api/handlers"
119
"github.com/corecollectives/mist/api/middleware"
1210
"github.com/corecollectives/mist/github"
13-
"github.com/corecollectives/mist/models"
14-
"gorm.io/gorm"
1511
)
1612

1713
type RepoListResponse struct {
@@ -26,40 +22,45 @@ func GetRepositories(w http.ResponseWriter, r *http.Request) {
2622
return
2723
}
2824

29-
installationID, err := models.GetInstallationID(int(userData.ID))
30-
if errors.Is(err, gorm.ErrRecordNotFound) {
31-
handlers.SendResponse(w, http.StatusNotFound, false, nil, "no installation found for user", "No installation found")
32-
return
33-
}
34-
if err != nil {
35-
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "database error", err.Error())
36-
return
37-
}
38-
39-
token, tokenExpires, appID, err := models.GetInstallationToken(installationID)
25+
// installationID, err := models.GetInstallationID(int(userData.ID))
26+
// if errors.Is(err, gorm.ErrRecordNotFound) {
27+
// handlers.SendResponse(w, http.StatusNotFound, false, nil, "no installation found for user", "No installation found")
28+
// return
29+
// }
30+
// if err != nil {
31+
// handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "database error", err.Error())
32+
// return
33+
// }
34+
35+
token, _, err := github.GetGitHubAccessToken(int(userData.ID))
4036
if err != nil {
41-
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to fetch installation info", err.Error())
42-
return
37+
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to fetch GH installation info", err.Error())
4338
}
4439

45-
expiry, _ := time.Parse(time.RFC3339, tokenExpires)
46-
if time.Now().After(expiry) {
47-
appJWT, err := github.GenerateGithubJwt(appID)
48-
if err != nil {
49-
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to generate app jwt", err.Error())
50-
return
51-
}
52-
53-
newToken, newExpiry, err := regenerateInstallationToken(appJWT, installationID)
54-
if err != nil {
55-
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to refresh token", err.Error())
56-
return
57-
}
58-
59-
_ = models.UpdateInstallationToken(int64(installationID), newToken, newExpiry)
60-
61-
token = newToken
62-
}
40+
// token, tokenExpires, appID, err := models.GetInstallationToken(installationID)
41+
// if err != nil {
42+
// handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to fetch installation info", err.Error())
43+
// return
44+
// }
45+
//
46+
// expiry, _ := time.Parse(time.RFC3339, tokenExpires)
47+
// if time.Now().After(expiry) {
48+
// appJWT, err := github.GenerateGithubJwt(appID)
49+
// if err != nil {
50+
// handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to generate app jwt", err.Error())
51+
// return
52+
// }
53+
//
54+
// newToken, newExpiry, err := regenerateInstallationToken(appJWT, installationID)
55+
// if err != nil {
56+
// handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to refresh token", err.Error())
57+
// return
58+
// }
59+
//
60+
// _ = models.UpdateInstallationToken(int64(installationID), newToken, newExpiry)
61+
//
62+
// token = newToken
63+
// }
6364

6465
allRepos := []any{}
6566
page := 1
@@ -99,32 +100,3 @@ func GetRepositories(w http.ResponseWriter, r *http.Request) {
99100
w.Header().Set("Content-Type", "application/json")
100101
json.NewEncoder(w).Encode(allRepos)
101102
}
102-
103-
func regenerateInstallationToken(appJWT string, installationID int64) (string, time.Time, error) {
104-
url := fmt.Sprintf("https://api.github.com/app/installations/%d/access_tokens", installationID)
105-
106-
req, _ := http.NewRequest("POST", url, nil)
107-
req.Header.Set("Authorization", "Bearer "+appJWT)
108-
req.Header.Set("Accept", "application/vnd.github+json")
109-
110-
resp, err := http.DefaultClient.Do(req)
111-
if err != nil {
112-
return "", time.Time{}, err
113-
}
114-
defer resp.Body.Close()
115-
116-
if resp.StatusCode != http.StatusCreated {
117-
return "", time.Time{}, fmt.Errorf("failed to create token, status %d", resp.StatusCode)
118-
}
119-
120-
var tokenResp struct {
121-
Token string `json:"token"`
122-
ExpiresAt time.Time `json:"expires_at"`
123-
}
124-
125-
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
126-
return "", time.Time{}, err
127-
}
128-
129-
return tokenResp.Token, tokenResp.ExpiresAt, nil
130-
}

server/git/commit.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ func GetLatestCommit(appID int64, userID int64) (*models.LatestCommit, error) {
2424
return nil, err
2525
}
2626
if gitProvider == nil && gitCloneUrl != nil {
27-
fmt.Printf("no provider found")
2827
return latestRemoteCommit(*gitCloneUrl)
2928
} else if gitProvider == nil && gitCloneUrl == nil {
3029
return nil, fmt.Errorf("git url or provider not given")
3130
}
3231

3332
if *gitProvider == models.GitProviderGitHub {
34-
fmt.Printf("github provider found")
3533
return github.GetLatestCommit(appID, userID)
3634
}
3735
return nil, fmt.Errorf("failed to get latest commit")

server/github/deployment.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package github
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"strings"
9+
"time"
10+
)
11+
12+
func CreateDeployment(repo string, branch string, userId int) (int64, error) {
13+
14+
token, _, err := GetGitHubAccessToken(userId)
15+
if err != nil {
16+
return 0, fmt.Errorf("error getting GH token %w", err)
17+
}
18+
url := fmt.Sprintf("https://api.github.com/repos/%s/deployments", repo)
19+
body := fmt.Sprintf(`{
20+
"ref":"%s",
21+
"environment":"production",
22+
"required_contexts":[]
23+
}`, branch)
24+
25+
req, _ := http.NewRequest("POST", url, strings.NewReader(body))
26+
req.Header.Set("Authorization", "Bearer "+token)
27+
req.Header.Set("Accept", "application/vnd.github+json")
28+
29+
client := &http.Client{Timeout: 30 * time.Second}
30+
res, err := client.Do(req)
31+
if err != nil {
32+
return 0, err
33+
}
34+
defer res.Body.Close()
35+
36+
if res.StatusCode >= 300 {
37+
b, _ := io.ReadAll(res.Body)
38+
return 0, fmt.Errorf("github api error: %s", b)
39+
}
40+
41+
var out struct {
42+
ID int64 `json:"id"`
43+
}
44+
45+
json.NewDecoder(res.Body).Decode(&out)
46+
47+
return out.ID, nil
48+
}
49+
50+
func UpdateDeployment(repo string, depID int64, state string, message string, userID int) error {
51+
token, _, err := GetGitHubAccessToken(userID)
52+
if err != nil {
53+
return fmt.Errorf("error getting GH token %w", err)
54+
}
55+
url := fmt.Sprintf(
56+
"https://api.github.com/repos/%s/deployments/%d/statuses",
57+
repo,
58+
depID,
59+
)
60+
body := fmt.Sprintf(`{
61+
"state":"%s",
62+
"description":"%s"
63+
}`, state, message)
64+
65+
req, _ := http.NewRequest("POST", url, strings.NewReader(body))
66+
req.Header.Set("Authorization", "Bearer "+token)
67+
req.Header.Set("Accept", "application/vnd.github+json")
68+
req.Header.Set("Content-Type", "application/json")
69+
70+
client := &http.Client{Timeout: 30 * time.Second}
71+
res, err := client.Do(req)
72+
if err != nil {
73+
return err
74+
}
75+
defer res.Body.Close()
76+
77+
if res.StatusCode >= 300 {
78+
b, _ := io.ReadAll(res.Body)
79+
return fmt.Errorf("github api error: %s", b)
80+
}
81+
return nil
82+
83+
}

server/github/getAccessToken.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,24 @@ import (
1010
"github.com/corecollectives/mist/models"
1111
)
1212

13-
func GetGitHubAccessToken(userID int) (string, error) {
14-
13+
func GetGitHubAccessToken(userID int) (string, time.Time, error) {
1514
app, _, err := models.GetApp(userID)
1615
if err != nil {
17-
return "", fmt.Errorf("failed to fetch github app credentials: %w", err)
16+
return "", time.Time{}, fmt.Errorf("failed to fetch github app credentials: %w", err)
1817
}
1918

2019
inst, err := models.GetInstallationByUserID(userID)
2120
if err != nil {
22-
return "", fmt.Errorf("failed to fetch github installation: %w", err)
21+
return "", time.Time{}, fmt.Errorf("failed to fetch github installation: %w", err)
2322
}
2423

2524
if time.Until(inst.TokenExpiresAt) > 5*time.Minute {
26-
return inst.AccessToken, nil
25+
return inst.AccessToken, inst.TokenExpiresAt, nil
2726
}
2827

2928
jwt, err := GenerateGithubJwt(int(app.AppID))
3029
if err != nil {
31-
return "", fmt.Errorf("failed to create JWT: %w", err)
30+
return "", time.Time{}, fmt.Errorf("failed to create JWT: %w", err)
3231
}
3332

3433
url := fmt.Sprintf("https://api.github.com/app/installations/%d/access_tokens", inst.InstallationID)
@@ -39,25 +38,25 @@ func GetGitHubAccessToken(userID int) (string, error) {
3938
client := &http.Client{}
4039
resp, err := client.Do(req)
4140
if err != nil {
42-
return "", fmt.Errorf("failed to request installation token: %w", err)
41+
return "", time.Time{}, fmt.Errorf("failed to request installation token: %w", err)
4342
}
4443
defer resp.Body.Close()
4544

4645
if resp.StatusCode != 201 {
4746
var body bytes.Buffer
4847
body.ReadFrom(resp.Body)
49-
return "", fmt.Errorf("GitHub API error (%d): %s", resp.StatusCode, body.String())
48+
return "", time.Time{}, fmt.Errorf("GitHub API error (%d): %s", resp.StatusCode, body.String())
5049
}
5150

5251
var result struct {
5352
Token string `json:"token"`
5453
ExpiresAt time.Time `json:"expires_at"`
5554
}
5655
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
57-
return "", fmt.Errorf("failed to decode token response: %w", err)
56+
return "", time.Time{}, fmt.Errorf("failed to decode token response: %w", err)
5857
}
5958

6059
err = models.UpdateInstallationToken(inst.InstallationID, result.Token, result.ExpiresAt)
6160

62-
return result.Token, nil
61+
return result.Token, result.ExpiresAt, nil
6362
}

server/models/deployment.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type Deployment struct {
3030
CommitMessage *string `json:"commit_message,omitempty"`
3131
CommitAuthor *string `json:"commit_author,omitempty"`
3232

33+
GithubDepId *int64 `json:"github_dep_id,omitempty"`
34+
3335
TriggeredBy *int64 `gorm:"constraint:OnDelete:SET NULL" json:"triggered_by,omitempty"`
3436

3537
DeploymentNumber *int `json:"deployment_number,omitempty"`
@@ -158,6 +160,10 @@ func GetCommitHashByDeploymentID(depID int64) (string, error) {
158160
return d.CommitHash, nil
159161
}
160162

163+
func (d *Deployment) UpdateDeployment() error {
164+
return db.Model(&Deployment{}).Where("id = ?", d.ID).Updates(d).Error
165+
}
166+
161167
func UpdateDeploymentStatus(depID int64, status, stage string, progress int, errorMsg *string) error {
162168
var d Deployment
163169
result := db.First(&d, "id = ?", depID)

0 commit comments

Comments
 (0)