Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Deploy Frontend to Vercel

on:
push:
branches: [main]
paths:
- "cohesion_frontend/**"
- ".github/workflows/deploy.yml"
pull_request:
branches: [main]
paths:
- "cohesion_frontend/**"
- ".github/workflows/deploy.yml"

env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

jobs:
deploy:
runs-on: ubuntu-latest
defaults:
run:
working-directory: cohesion_frontend

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Vercel CLI
run: npm install -g vercel

- name: Pull Vercel Environment
run: vercel pull --yes --environment=${{ github.event_name == 'push' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }}

- name: Build
run: vercel build ${{ github.event_name == 'push' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }}

- name: Deploy
id: deploy
run: |
URL=$(vercel deploy --prebuilt ${{ github.event_name == 'push' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }})
echo "url=$URL" >> "$GITHUB_OUTPUT"

- name: Comment Preview URL on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `**Vercel Preview:** ${{ steps.deploy.outputs.url }}`
})
8 changes: 7 additions & 1 deletion cohesion_backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ ENVIRONMENT=development
GEMINI_API_KEY=your-gemini-api-key
GEMINI_MODEL=gemini-2.0-flash
CLERK_SECRET_KEY=sk_test_...
CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_PUBLISHABLE_KEY=pk_test_...
GITHUB_APP_ID=123456
GITHUB_APP_PRIVATE_KEY_PATH=./github-app-private-key.pem
GITHUB_APP_CLIENT_ID=Iv1.abc123
GITHUB_APP_CLIENT_SECRET=secret
GITHUB_APP_SLUG=cohesion
FRONTEND_URL=http://localhost:3000
6 changes: 5 additions & 1 deletion cohesion_backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.env
.env.local
.env.*
*.exe
*.exe~
*.dll
Expand All @@ -13,3 +13,7 @@ vendor/
.vscode/
*.log
tmp/
bin/
server
cohesion-server
.next/
23 changes: 16 additions & 7 deletions cohesion_backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/cohesion-api/cohesion_backend/internal/services"
"github.com/cohesion-api/cohesion_backend/pkg/analyzer"
geminianalyzer "github.com/cohesion-api/cohesion_backend/pkg/analyzer/gemini"
ghpkg "github.com/cohesion-api/cohesion_backend/pkg/github"
)

func main() {
Expand All @@ -39,26 +40,34 @@ func main() {
schemaRepo := repository.NewSchemaRepository(db)
diffRepo := repository.NewDiffRepository(db)
userSettingsRepo := repository.NewUserSettingsRepository(db)
ghInstallRepo := repository.NewGitHubInstallationRepository(db)

projectService := services.NewProjectService(projectRepo, endpointRepo)
endpointService := services.NewEndpointService(endpointRepo, schemaRepo)
schemaService := services.NewSchemaService(schemaRepo, endpointRepo)
diffService := services.NewDiffService(diffRepo, schemaRepo, endpointRepo)
liveService := services.NewLiveService()
userSettingsService := services.NewUserSettingsService(userSettingsRepo)
ghInstallService := services.NewGitHubInstallationService(ghInstallRepo)
var codeAnalyzer analyzer.Analyzer
if cfg.GeminiAPIKey != "" {
codeAnalyzer = geminianalyzer.New(cfg.GeminiAPIKey, cfg.GeminiModel)
}

ghAppAuth := ghpkg.NewAppAuth(cfg.GitHubAppID, cfg.GitHubAppPrivateKey)

svc := &controlplane.Services{
ProjectService: projectService,
EndpointService: endpointService,
SchemaService: schemaService,
DiffService: diffService,
LiveService: liveService,
UserSettingsService: userSettingsService,
Analyzer: codeAnalyzer,
ProjectService: projectService,
EndpointService: endpointService,
SchemaService: schemaService,
DiffService: diffService,
LiveService: liveService,
UserSettingsService: userSettingsService,
GitHubInstallationService: ghInstallService,
Analyzer: codeAnalyzer,
GitHubAppAuth: ghAppAuth,
GitHubAppSlug: cfg.GitHubAppSlug,
FrontendURL: cfg.FrontendURL,
}

router := controlplane.NewRouter(svc)
Expand Down
3 changes: 3 additions & 0 deletions cohesion_backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/cohesion-api/cohesion_backend
go 1.25.0

require (
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
github.com/clerk/clerk-sdk-go/v2 v2.5.1
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1
Expand All @@ -27,6 +28,8 @@ require (
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/go-github/v75 v75.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
Expand Down
6 changes: 6 additions & 0 deletions cohesion_backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clerk/clerk-sdk-go/v2 v2.5.1 h1:RsakGNW6ie83b9KIRtKzqDXBJ//cURy9SJUbGhrsIKg=
Expand Down Expand Up @@ -37,6 +39,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ=
Expand All @@ -47,6 +51,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s=
github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68=
github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=
github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
Expand Down
20 changes: 20 additions & 0 deletions cohesion_backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,39 @@ type Config struct {
Environment string
GeminiAPIKey string
GeminiModel string

GitHubAppID int64
GitHubAppPrivateKey []byte
GitHubAppClientID string
GitHubAppClientSecret string
GitHubAppSlug string
FrontendURL string
}

func Load() *Config {
godotenv.Load()

port, _ := strconv.Atoi(getEnv("PORT", "8080"))
appID, _ := strconv.ParseInt(getEnv("GITHUB_APP_ID", "0"), 10, 64)

var privateKey []byte
if path := getEnv("GITHUB_APP_PRIVATE_KEY_PATH", ""); path != "" {
privateKey, _ = os.ReadFile(path)
}

return &Config{
DatabaseURL: getEnv("DATABASE_URL", ""),
Port: port,
Environment: getEnv("ENVIRONMENT", "development"),
GeminiAPIKey: getEnv("GEMINI_API_KEY", ""),
GeminiModel: getEnv("GEMINI_MODEL", "gemini-2.0-flash"),

GitHubAppID: appID,
GitHubAppPrivateKey: privateKey,
GitHubAppClientID: getEnv("GITHUB_APP_CLIENT_ID", ""),
GitHubAppClientSecret: getEnv("GITHUB_APP_CLIENT_SECRET", ""),
GitHubAppSlug: getEnv("GITHUB_APP_SLUG", ""),
FrontendURL: getEnv("FRONTEND_URL", "http://localhost:3000"),
}
}

Expand Down
117 changes: 117 additions & 0 deletions cohesion_backend/internal/controlplane/handlers/github_app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package handlers

import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"

"github.com/cohesion-api/cohesion_backend/internal/auth"
"github.com/go-chi/chi/v5"
)

func (h *Handlers) GitHubAppStatus(w http.ResponseWriter, r *http.Request) {
configured := h.githubAppAuth.IsConfigured()
resp := map[string]interface{}{
"configured": configured,
}
if configured && h.githubAppSlug != "" {
resp["install_url"] = fmt.Sprintf("https://github.com/apps/%s/installations/new", h.githubAppSlug)
}
respondJSON(w, http.StatusOK, resp)
}

type SaveInstallationRequest struct {
InstallationID int64 `json:"installation_id"`
}

func (h *Handlers) SaveGitHubInstallation(w http.ResponseWriter, r *http.Request) {
userID := auth.UserID(r.Context())
if userID == "" {
respondError(w, http.StatusUnauthorized, "Not authenticated")
return
}

var req SaveInstallationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}

if req.InstallationID == 0 {
respondError(w, http.StatusBadRequest, "installation_id is required")
return
}

if !h.githubAppAuth.IsConfigured() {
respondError(w, http.StatusBadRequest, "GitHub App is not configured")
return
}

// Verify the installation exists via the GitHub API
appClient, err := h.githubAppAuth.AppClient()
if err != nil {
log.Printf("Failed to create GitHub App client: %v", err)
respondError(w, http.StatusInternalServerError, "Failed to verify installation")
return
}

installation, _, err := appClient.Apps.GetInstallation(r.Context(), req.InstallationID)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid installation — make sure you completed the GitHub App install flow")
return
}

accountLogin := installation.GetAccount().GetLogin()
accountType := installation.GetAccount().GetType()

if err := h.ghInstallService.SaveInstallation(r.Context(), userID, req.InstallationID, accountLogin, accountType); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to save installation")
return
}

respondJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Installation saved",
"installation_id": req.InstallationID,
"github_account_login": accountLogin,
"github_account_type": accountType,
})
}

func (h *Handlers) ListGitHubInstallations(w http.ResponseWriter, r *http.Request) {
userID := auth.UserID(r.Context())
if userID == "" {
respondError(w, http.StatusUnauthorized, "Not authenticated")
return
}

installations, err := h.ghInstallService.List(r.Context(), userID)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to list installations")
return
}

respondJSON(w, http.StatusOK, installations)
}

func (h *Handlers) RemoveGitHubInstallation(w http.ResponseWriter, r *http.Request) {
userID := auth.UserID(r.Context())
if userID == "" {
respondError(w, http.StatusUnauthorized, "Not authenticated")
return
}

installationID, err := strconv.ParseInt(chi.URLParam(r, "installationID"), 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid installation ID")
return
}

if err := h.ghInstallService.Remove(r.Context(), userID, installationID); err != nil {
respondError(w, http.StatusNotFound, "Installation not found")
return
}

w.WriteHeader(http.StatusNoContent)
}
Loading
Loading