diff --git a/go.mod b/go.mod index 640fd16..b3d43a0 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module box -go 1.23.0 +go 1.26.0 -toolchain go1.24.5 +toolchain go1.26.3 require ( github.com/jackc/pgx/v5 v5.7.6 diff --git a/internal/app/db.go b/internal/app/db.go index 379c3b1..e0688a7 100644 --- a/internal/app/db.go +++ b/internal/app/db.go @@ -26,6 +26,8 @@ func (a *App) migrate(ctx context.Context) error { deploy_token TEXT NOT NULL DEFAULT '', provision_state TEXT NOT NULL DEFAULT 'provisioned', provision_error TEXT NOT NULL DEFAULT '', + last_deployed_at TIMESTAMPTZ NULL, + last_deployed_commit TEXT NOT NULL DEFAULT '', db_name TEXT NOT NULL DEFAULT '', db_user TEXT NOT NULL DEFAULT '', db_password TEXT NOT NULL DEFAULT '', @@ -52,6 +54,8 @@ func (a *App) migrate(ctx context.Context) error { `ALTER TABLE projects ADD COLUMN IF NOT EXISTS repo_private BOOLEAN NOT NULL DEFAULT true;`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_state TEXT NOT NULL DEFAULT 'provisioned';`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_error TEXT NOT NULL DEFAULT '';`, + `ALTER TABLE projects ADD COLUMN IF NOT EXISTS last_deployed_at TIMESTAMPTZ NULL;`, + `ALTER TABLE projects ADD COLUMN IF NOT EXISTS last_deployed_commit TEXT NOT NULL DEFAULT '';`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS db_name TEXT NOT NULL DEFAULT '';`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS db_user TEXT NOT NULL DEFAULT '';`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS db_password TEXT NOT NULL DEFAULT '';`, diff --git a/internal/app/deploy_service.go b/internal/app/deploy_service.go index 6431ccf..16d6151 100644 --- a/internal/app/deploy_service.go +++ b/internal/app/deploy_service.go @@ -13,6 +13,7 @@ import ( "path/filepath" "strconv" "strings" + "time" ) func (a *App) handleDeployWebhook(w http.ResponseWriter, r *http.Request) { @@ -125,9 +126,21 @@ func (a *App) buildProjectBinaryFromRepoDir(p Project, repoDir string) error { if err := a.syncStaticAssetsForProject(p); err != nil { return err } + commit, err := runCmdOut("git", "-C", repoDir, "rev-parse", "HEAD") + if err != nil { + return fmt.Errorf("resolve latest commit failed: %w", err) + } + if err := a.updateLastDeployment(p.ID, strings.TrimSpace(commit), time.Now().UTC()); err != nil { + return err + } return nil } +func (a *App) updateLastDeployment(projectID int64, commit string, deployedAt time.Time) error { + _, err := a.db.Exec(`UPDATE projects SET last_deployed_at=$1, last_deployed_commit=$2 WHERE id=$3`, deployedAt, strings.TrimSpace(commit), projectID) + return err +} + func (a *App) syncProjectRepoFromMain(p Project, projectDir string) error { repoDir := filepath.Join(projectDir, "repo") plainRepoURL := strings.TrimSpace(p.RepoURL) diff --git a/internal/app/project_service.go b/internal/app/project_service.go index 71b92cf..f169e64 100644 --- a/internal/app/project_service.go +++ b/internal/app/project_service.go @@ -9,7 +9,7 @@ import ( ) func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error) { - rows, err := a.db.QueryContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.db_name, p.db_user, p.db_password, p.created_at + rows, err := a.db.QueryContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.last_deployed_at, p.last_deployed_commit, p.db_name, p.db_user, p.db_password, p.created_at FROM projects p JOIN workspaces w ON w.id = p.workspace_id WHERE p.user_id=$1 ORDER BY p.created_at DESC`, userID) @@ -20,9 +20,14 @@ func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error var out []Project for rows.Next() { var p Project - if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt); err != nil { + var deployedAt sql.NullTime + if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &deployedAt, &p.LastDeployedCommit, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt); err != nil { return nil, err } + if deployedAt.Valid { + t := deployedAt.Time.UTC() + p.LastDeployedAt = &t + } p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken) p.AppURL = a.appURL(p.RouteHost) out = append(out, p) @@ -55,15 +60,20 @@ func (a *App) createProject(ctx context.Context, user User, workspaceID int64, n } defer tx.Rollback() - err = tx.QueryRowContext(ctx, `INSERT INTO projects (user_id, workspace_id, name, slug, description, repo_url, repo_private, service_name, route_host, deploy_token, provision_state, provision_error) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'pending','') RETURNING id, user_id, name, slug, description, repo_url, repo_private, service_name, route_host, workspace_id, deploy_token, provision_state, provision_error, db_name, db_user, db_password, created_at`, + var createdDeployedAt sql.NullTime + err = tx.QueryRowContext(ctx, `INSERT INTO projects (user_id, workspace_id, name, slug, description, repo_url, repo_private, service_name, route_host, deploy_token, provision_state, provision_error) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'pending','') RETURNING id, user_id, name, slug, description, repo_url, repo_private, service_name, route_host, workspace_id, deploy_token, provision_state, provision_error, last_deployed_at, last_deployed_commit, db_name, db_user, db_password, created_at`, user.Username, workspaceID, name, slug, description, repoURL, private, serviceName, routeHost, deployToken, - ).Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.WorkspaceID, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt) + ).Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.WorkspaceID, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &createdDeployedAt, &p.LastDeployedCommit, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt) if err != nil { if strings.Contains(err.Error(), "duplicate key") { return Project{}, errors.New("project with this name already exists") } return Project{}, err } + if createdDeployedAt.Valid { + t := createdDeployedAt.Time.UTC() + p.LastDeployedAt = &t + } if err := tx.Commit(); err != nil { return Project{}, err } @@ -144,15 +154,20 @@ func (a *App) provisionProject(ctx context.Context, p *Project) error { func (a *App) getProject(ctx context.Context, userID string, projectID int64) (Project, error) { var p Project - err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.db_name, p.db_user, p.db_password, p.created_at + var deployedAt sql.NullTime + err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.last_deployed_at, p.last_deployed_commit, p.db_name, p.db_user, p.db_password, p.created_at FROM projects p JOIN workspaces w ON w.id=p.workspace_id WHERE p.user_id=$1 AND p.id=$2`, userID, projectID). - Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt) + Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &deployedAt, &p.LastDeployedCommit, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return Project{}, errors.New("project not found") } return Project{}, err } + if deployedAt.Valid { + t := deployedAt.Time.UTC() + p.LastDeployedAt = &t + } p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken) p.AppURL = a.appURL(p.RouteHost) return p, nil diff --git a/internal/app/types.go b/internal/app/types.go index 0182c24..c1f5f8d 100644 --- a/internal/app/types.go +++ b/internal/app/types.go @@ -45,27 +45,29 @@ type User struct { } type Project struct { - ID int64 `json:"id"` - UserID string `json:"-"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` - RepoURL string `json:"repo_url"` - RepoPrivate bool `json:"repo_private"` - ServiceName string `json:"service_name"` - RouteHost string `json:"route_host"` - AppURL string `json:"app_url"` - WorkspaceID int64 `json:"workspace_id"` - Workspace string `json:"workspace"` - UnixUser string `json:"unix_user"` - WebhookURL string `json:"webhook_url"` - DeployToken string `json:"-"` - ProvisionState string `json:"provision_state"` - ProvisionError string `json:"provision_error,omitempty"` - DBName string `json:"db_name,omitempty"` - DBUser string `json:"db_user,omitempty"` - DBPassword string `json:"-"` - CreatedAt time.Time `json:"created_at"` + ID int64 `json:"id"` + UserID string `json:"-"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + RepoURL string `json:"repo_url"` + RepoPrivate bool `json:"repo_private"` + ServiceName string `json:"service_name"` + RouteHost string `json:"route_host"` + AppURL string `json:"app_url"` + WorkspaceID int64 `json:"workspace_id"` + Workspace string `json:"workspace"` + UnixUser string `json:"unix_user"` + WebhookURL string `json:"webhook_url"` + DeployToken string `json:"-"` + ProvisionState string `json:"provision_state"` + ProvisionError string `json:"provision_error,omitempty"` + LastDeployedAt *time.Time `json:"last_deployed_at,omitempty"` + LastDeployedCommit string `json:"last_deployed_commit,omitempty"` + DBName string `json:"db_name,omitempty"` + DBUser string `json:"db_user,omitempty"` + DBPassword string `json:"-"` + CreatedAt time.Time `json:"created_at"` } type EnvVar struct { diff --git a/web/app.js b/web/app.js index f39a58f..9f3522a 100644 --- a/web/app.js +++ b/web/app.js @@ -55,6 +55,8 @@ function renderProjects() {
+ +${escapeHtml(p.description || '')}