2024-02-06 08:48:06 +00:00
import assert from 'assert' ;
2024-02-12 06:04:01 +00:00
import debug from 'debug' ;
2024-02-06 08:48:06 +00:00
import { DeepPartial , FindOptionsWhere } from 'typeorm' ;
2024-02-15 11:54:57 +00:00
import { Octokit , RequestError } from 'octokit' ;
2024-02-05 10:51:55 +00:00
2024-02-08 09:29:19 +00:00
import { OAuthApp } from '@octokit/oauth-app' ;
2024-02-02 09:32:12 +00:00
import { Database } from './database' ;
2024-02-12 06:04:01 +00:00
import { Deployment , DeploymentStatus , Environment } from './entity/Deployment' ;
2024-02-02 09:32:12 +00:00
import { Domain } from './entity/Domain' ;
import { EnvironmentVariable } from './entity/EnvironmentVariable' ;
import { Organization } from './entity/Organization' ;
import { Project } from './entity/Project' ;
2024-02-05 10:51:55 +00:00
import { Permission , ProjectMember } from './entity/ProjectMember' ;
2024-02-02 09:32:12 +00:00
import { User } from './entity/User' ;
2024-02-12 06:04:01 +00:00
import { Registry } from './registry' ;
2024-02-19 08:13:29 +00:00
import { GitHubConfig , RegistryConfig } from './config' ;
2024-04-15 14:06:04 +00:00
import { AppDeploymentRecord , AppDeploymentRemovalRecord , GitPushEventPayload , PackageJSON } from './types' ;
2024-02-22 11:56:26 +00:00
import { Role } from './entity/UserOrganization' ;
2024-02-12 06:04:01 +00:00
const log = debug ( 'snowball:service' ) ;
2024-02-19 08:13:29 +00:00
2024-02-15 11:54:57 +00:00
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository' ;
2024-03-29 17:45:54 +00:00
// Define a constant for an hour in milliseconds
const HOUR = 1000 * 60 * 60 ;
2024-02-15 11:54:57 +00:00
interface Config {
2024-02-22 05:45:17 +00:00
gitHubConfig : GitHubConfig ;
registryConfig : RegistryConfig ;
2024-02-15 11:54:57 +00:00
}
2024-02-12 06:04:01 +00:00
2024-02-02 09:32:12 +00:00
export class Service {
private db : Database ;
2024-02-14 05:33:22 +00:00
private oauthApp : OAuthApp ;
2024-02-12 06:04:01 +00:00
private registry : Registry ;
2024-02-15 11:54:57 +00:00
private config : Config ;
2024-02-02 09:32:12 +00:00
2024-02-19 08:13:29 +00:00
private deployRecordCheckTimeout? : NodeJS.Timeout ;
2024-02-15 11:54:57 +00:00
constructor ( config : Config , db : Database , app : OAuthApp , registry : Registry ) {
2024-02-02 09:32:12 +00:00
this . db = db ;
2024-02-14 05:33:22 +00:00
this . oauthApp = app ;
2024-02-12 06:04:01 +00:00
this . registry = registry ;
2024-02-15 11:54:57 +00:00
this . config = config ;
2024-02-19 08:13:29 +00:00
this . init ( ) ;
}
/ * *
* Initialize services
* /
init ( ) : void {
// Start check for ApplicationDeploymentRecords asynchronously
this . checkDeployRecordsAndUpdate ( ) ;
2024-04-24 06:38:56 +00:00
// Start check for ApplicationDeploymentRemovalRecords asynchronously
this . checkDeploymentRemovalRecordsAndUpdate ( ) ;
2024-02-19 08:13:29 +00:00
}
/ * *
* Destroy services
* /
destroy ( ) : void {
clearTimeout ( this . deployRecordCheckTimeout ) ;
}
/ * *
* Checks for ApplicationDeploymentRecord and update corresponding deployments
2024-04-15 14:06:04 +00:00
* Continues check in loop after a delay of registryConfig . fetchDeploymentRecordDelay
2024-02-19 08:13:29 +00:00
* /
async checkDeployRecordsAndUpdate ( ) : Promise < void > {
// Fetch deployments in building state
const deployments = await this . db . getDeployments ( {
where : {
status : DeploymentStatus.Building
}
} ) ;
if ( deployments . length ) {
2024-02-22 05:45:17 +00:00
log (
` Found ${ deployments . length } deployments in ${ DeploymentStatus . Building } state `
) ;
2024-02-19 08:13:29 +00:00
2024-03-29 17:45:54 +00:00
// Calculate a timestamp for one hour ago
const anHourAgo = Date . now ( ) - HOUR ;
// Filter out deployments started more than an hour ago and mark them as Error
const oldDeploymentsToUpdate = deployments . filter (
deployment = > ( Number ( deployment . updatedAt ) < anHourAgo )
)
. map ( ( deployment ) = > {
return this . db . updateDeploymentById ( deployment . id , {
status : DeploymentStatus.Error ,
isCurrent : false
} ) ;
} ) ;
// If there are old deployments to update, log and perform the updates
if ( oldDeploymentsToUpdate . length > 0 ) {
log (
` Cleaning up ${ oldDeploymentsToUpdate . length } deployments stuck in ${ DeploymentStatus . Building } state for over an hour `
) ;
await Promise . all ( oldDeploymentsToUpdate ) ;
}
2024-02-19 08:13:29 +00:00
// Fetch ApplicationDeploymentRecord for deployments
const records = await this . registry . getDeploymentRecords ( deployments ) ;
log ( ` Found ${ records . length } ApplicationDeploymentRecords ` ) ;
// Update deployments for which ApplicationDeploymentRecords were returned
2024-02-20 05:23:42 +00:00
if ( records . length ) {
await this . updateDeploymentsWithRecordData ( records ) ;
}
2024-02-19 08:13:29 +00:00
}
this . deployRecordCheckTimeout = setTimeout ( ( ) = > {
this . checkDeployRecordsAndUpdate ( ) ;
} , this . config . registryConfig . fetchDeploymentRecordDelay ) ;
}
2024-04-15 14:06:04 +00:00
/ * *
* Checks for ApplicationDeploymentRemovalRecord and remove corresponding deployments
* Continues check in loop after a delay of registryConfig . fetchDeploymentRecordDelay
* /
async checkDeploymentRemovalRecordsAndUpdate ( ) : Promise < void > {
// Fetch deployments in deleting state
const deployments = await this . db . getDeployments ( {
where : {
status : DeploymentStatus.Deleting
}
} ) ;
if ( deployments . length ) {
log (
` Found ${ deployments . length } deployments in ${ DeploymentStatus . Deleting } state `
) ;
// Fetch ApplicationDeploymentRemovalRecords for deployments
const records = await this . registry . getDeploymentRemovalRecords ( deployments ) ;
log ( ` Found ${ records . length } ApplicationDeploymentRemovalRecords ` ) ;
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
if ( records . length ) {
await this . deleteDeploymentsWithRecordData ( records , deployments ) ;
}
}
this . deployRecordCheckTimeout = setTimeout ( ( ) = > {
this . checkDeploymentRemovalRecordsAndUpdate ( ) ;
} , this . config . registryConfig . fetchDeploymentRecordDelay ) ;
}
2024-02-19 08:13:29 +00:00
/ * *
* Update deployments with ApplicationDeploymentRecord data
* /
2024-02-22 05:45:17 +00:00
async updateDeploymentsWithRecordData (
records : AppDeploymentRecord [ ]
) : Promise < void > {
2024-02-19 08:13:29 +00:00
// Get deployments for ApplicationDeploymentRecords
const deployments = await this . db . getDeployments ( {
2024-02-22 05:45:17 +00:00
where : records.map ( ( record ) = > ( {
2024-02-19 08:13:29 +00:00
applicationRecordId : record.attributes.application
} ) ) ,
order : {
createdAt : 'DESC'
}
} ) ;
// Get project IDs of deployments that are in production environment
2024-02-22 05:45:17 +00:00
const productionDeploymentProjectIds = deployments . reduce (
( acc , deployment ) : Set < string > = > {
if ( deployment . environment === Environment . Production ) {
acc . add ( deployment . projectId ) ;
}
2024-02-19 08:13:29 +00:00
2024-02-22 05:45:17 +00:00
return acc ;
} ,
new Set < string > ( )
) ;
2024-02-19 08:13:29 +00:00
// Set old deployments isCurrent to false
2024-02-22 05:45:17 +00:00
await this . db . updateDeploymentsByProjectIds (
Array . from ( productionDeploymentProjectIds ) ,
{ isCurrent : false }
) ;
2024-02-19 08:13:29 +00:00
2024-02-22 05:45:17 +00:00
const recordToDeploymentsMap = deployments . reduce (
( acc : { [ key : string ] : Deployment } , deployment ) = > {
acc [ deployment . applicationRecordId ] = deployment ;
return acc ;
} ,
{ }
) ;
2024-02-19 08:13:29 +00:00
// Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records . map ( async ( record ) = > {
const deployment = recordToDeploymentsMap [ record . attributes . application ] ;
2024-02-22 05:45:17 +00:00
await this . db . updateDeploymentById ( deployment . id , {
applicationDeploymentRecordId : record.id ,
applicationDeploymentRecordData : record.attributes ,
url : record.attributes.url ,
status : DeploymentStatus.Ready ,
isCurrent : deployment.environment === Environment . Production
} ) ;
2024-02-19 08:13:29 +00:00
2024-02-22 05:45:17 +00:00
log (
` Updated deployment ${ deployment . id } with URL ${ record . attributes . url } `
) ;
2024-02-19 08:13:29 +00:00
} ) ;
await Promise . all ( deploymentUpdatePromises ) ;
2024-02-02 09:32:12 +00:00
}
2024-04-15 14:06:04 +00:00
/ * *
* Delete deployments with ApplicationDeploymentRemovalRecord data
* /
async deleteDeploymentsWithRecordData (
records : AppDeploymentRemovalRecord [ ] ,
deployments : Deployment [ ] ,
) : Promise < void > {
const removedApplicationDeploymentRecordIds = records . map ( record = > record . attributes . deployment ) ;
// Get removed deployments for ApplicationDeploymentRecords
const removedDeployments = deployments . filter ( deployment = > removedApplicationDeploymentRecordIds . includes ( deployment . applicationDeploymentRecordId ! ) )
const recordToDeploymentsMap = removedDeployments . reduce (
( acc : { [ key : string ] : Deployment } , deployment ) = > {
acc [ deployment . applicationDeploymentRecordId ! ] = deployment ;
return acc ;
} ,
{ }
) ;
// Update deployment data for ApplicationDeploymentRecords and delete
const deploymentUpdatePromises = records . map ( async ( record ) = > {
const deployment = recordToDeploymentsMap [ record . attributes . deployment ] ;
await this . db . updateDeploymentById ( deployment . id , {
applicationDeploymentRemovalRecordId : record.id ,
applicationDeploymentRemovalRecordData : record.attributes ,
} ) ;
log (
` Updated deployment ${ deployment . id } with ApplicationDeploymentRemovalRecord ${ record . id } `
) ;
await this . db . deleteDeploymentById ( deployment . id )
} ) ;
await Promise . all ( deploymentUpdatePromises ) ;
}
2024-02-06 13:41:53 +00:00
async getUser ( userId : string ) : Promise < User | null > {
2024-02-05 10:51:55 +00:00
return this . db . getUser ( {
where : {
id : userId
}
} ) ;
2024-02-02 09:32:12 +00:00
}
2024-02-22 11:56:26 +00:00
async loadOrCreateUser ( ethAddress : string ) : Promise < User > {
// Get user by ETH address
let user = await this . db . getUser ( {
where : {
ethAddress
}
} ) ;
if ( ! user ) {
const [ org ] = await this . db . getOrganizations ( { } ) ;
assert ( org , 'No organizations exists in database' ) ;
// Create user with new address
user = await this . db . addUser ( {
email : ` ${ ethAddress } @example.com ` ,
name : ethAddress ,
isVerified : true ,
ethAddress
} ) ;
await this . db . addUserOrganization ( {
member : user ,
organization : org ,
role : Role.Owner
} ) ;
}
return user ;
}
2024-02-14 05:33:22 +00:00
async getOctokit ( userId : string ) : Promise < Octokit > {
const user = await this . db . getUser ( { where : { id : userId } } ) ;
2024-02-22 05:45:17 +00:00
assert (
user && user . gitHubToken ,
'User needs to be authenticated with GitHub token'
) ;
2024-02-14 05:33:22 +00:00
return new Octokit ( { auth : user.gitHubToken } ) ;
}
2024-02-22 11:56:26 +00:00
async getOrganizationsByUserId ( user : User ) : Promise < Organization [ ] > {
const dbOrganizations = await this . db . getOrganizationsByUserId ( user . id ) ;
2024-02-02 09:32:12 +00:00
return dbOrganizations ;
}
async getProjectById ( projectId : string ) : Promise < Project | null > {
const dbProject = await this . db . getProjectById ( projectId ) ;
return dbProject ;
}
2024-02-22 11:56:26 +00:00
async getProjectsInOrganization ( user : User , organizationSlug : string ) : Promise < Project [ ] > {
const dbProjects = await this . db . getProjectsInOrganization ( user . id , organizationSlug ) ;
2024-02-02 09:32:12 +00:00
return dbProjects ;
}
2024-02-19 08:13:29 +00:00
async getDeploymentsByProjectId ( projectId : string ) : Promise < Deployment [ ] > {
2024-02-02 09:32:12 +00:00
const dbDeployments = await this . db . getDeploymentsByProjectId ( projectId ) ;
return dbDeployments ;
}
2024-02-22 05:45:17 +00:00
async getEnvironmentVariablesByProjectId (
projectId : string
) : Promise < EnvironmentVariable [ ] > {
const dbEnvironmentVariables =
await this . db . getEnvironmentVariablesByProjectId ( projectId ) ;
2024-02-02 09:32:12 +00:00
return dbEnvironmentVariables ;
}
2024-02-22 05:45:17 +00:00
async getProjectMembersByProjectId (
projectId : string
) : Promise < ProjectMember [ ] > {
const dbProjectMembers =
await this . db . getProjectMembersByProjectId ( projectId ) ;
2024-02-02 09:32:12 +00:00
return dbProjectMembers ;
}
2024-02-22 11:56:26 +00:00
async searchProjects ( user : User , searchText : string ) : Promise < Project [ ] > {
const dbProjects = await this . db . getProjectsBySearchText ( user . id , searchText ) ;
2024-02-02 09:32:12 +00:00
return dbProjects ;
}
2024-02-22 05:45:17 +00:00
async getDomainsByProjectId (
projectId : string ,
filter? : FindOptionsWhere < Domain >
) : Promise < Domain [ ] > {
2024-02-06 08:48:06 +00:00
const dbDomains = await this . db . getDomainsByProjectId ( projectId , filter ) ;
2024-02-02 09:32:12 +00:00
return dbDomains ;
}
2024-02-05 10:51:55 +00:00
2024-02-22 05:45:17 +00:00
async updateProjectMember (
projectMemberId : string ,
data : { permissions : Permission [ ] }
) : Promise < boolean > {
2024-02-06 08:48:06 +00:00
return this . db . updateProjectMemberById ( projectMemberId , data ) ;
2024-02-05 10:51:55 +00:00
}
2024-02-22 05:45:17 +00:00
async addProjectMember (
projectId : string ,
2024-02-05 10:51:55 +00:00
data : {
2024-02-22 05:45:17 +00:00
email : string ;
permissions : Permission [ ] ;
}
) : Promise < ProjectMember > {
2024-02-06 08:48:06 +00:00
// TODO: Send invitation
let user = await this . db . getUser ( {
where : {
email : data.email
}
} ) ;
if ( ! user ) {
user = await this . db . addUser ( {
email : data.email
2024-02-05 10:51:55 +00:00
} ) ;
2024-02-06 08:48:06 +00:00
}
2024-02-05 10:51:55 +00:00
2024-02-06 08:48:06 +00:00
const newProjectMember = await this . db . addProjectMember ( {
project : {
id : projectId
} ,
permissions : data.permissions ,
isPending : true ,
member : {
id : user.id
2024-02-05 10:51:55 +00:00
}
2024-02-06 08:48:06 +00:00
} ) ;
2024-02-05 10:51:55 +00:00
2024-02-06 08:48:06 +00:00
return newProjectMember ;
}
2024-02-22 11:56:26 +00:00
async removeProjectMember ( user : User , projectMemberId : string ) : Promise < boolean > {
2024-02-06 08:48:06 +00:00
const member = await this . db . getProjectMemberById ( projectMemberId ) ;
2024-02-22 11:56:26 +00:00
if ( String ( member . member . id ) === user . id ) {
2024-02-06 08:48:06 +00:00
throw new Error ( 'Invalid operation: cannot remove self' ) ;
}
const memberProject = member . project ;
assert ( memberProject ) ;
2024-02-05 10:51:55 +00:00
2024-02-22 11:56:26 +00:00
if ( String ( user . id ) === String ( memberProject . owner . id ) ) {
2024-02-06 08:48:06 +00:00
return this . db . removeProjectMemberById ( projectMemberId ) ;
} else {
throw new Error ( 'Invalid operation: not authorized' ) ;
2024-02-05 10:51:55 +00:00
}
}
2024-02-22 05:45:17 +00:00
async addEnvironmentVariables (
projectId : string ,
data : { environments : string [ ] ; key : string ; value : string } [ ]
) : Promise < EnvironmentVariable [ ] > {
const formattedEnvironmentVariables = data
. map ( ( environmentVariable ) = > {
return environmentVariable . environments . map ( ( environment ) = > {
return {
key : environmentVariable.key ,
value : environmentVariable.value ,
environment : environment as Environment ,
project : Object.assign ( new Project ( ) , {
id : projectId
} )
} ;
2024-02-05 10:51:55 +00:00
} ) ;
2024-02-22 05:45:17 +00:00
} )
. flat ( ) ;
2024-02-05 10:51:55 +00:00
2024-02-22 05:45:17 +00:00
const savedEnvironmentVariables = await this . db . addEnvironmentVariables (
formattedEnvironmentVariables
) ;
2024-02-06 08:48:06 +00:00
return savedEnvironmentVariables ;
2024-02-05 10:51:55 +00:00
}
2024-02-22 05:45:17 +00:00
async updateEnvironmentVariable (
environmentVariableId : string ,
data : DeepPartial < EnvironmentVariable >
) : Promise < boolean > {
2024-02-06 08:48:06 +00:00
return this . db . updateEnvironmentVariable ( environmentVariableId , data ) ;
2024-02-05 10:51:55 +00:00
}
2024-02-22 05:45:17 +00:00
async removeEnvironmentVariable (
environmentVariableId : string
) : Promise < boolean > {
2024-02-06 08:48:06 +00:00
return this . db . deleteEnvironmentVariable ( environmentVariableId ) ;
2024-02-05 10:51:55 +00:00
}
2024-02-22 11:56:26 +00:00
async updateDeploymentToProd ( user : User , deploymentId : string ) : Promise < Deployment > {
2024-02-12 06:04:01 +00:00
const oldDeployment = await this . db . getDeployment ( {
where : { id : deploymentId } ,
relations : {
project : true
}
} ) ;
2024-02-06 08:48:06 +00:00
2024-02-12 06:04:01 +00:00
if ( ! oldDeployment ) {
2024-02-06 08:48:06 +00:00
throw new Error ( 'Deployment does not exist' ) ;
}
2024-02-22 05:45:17 +00:00
const prodBranchDomains = await this . db . getDomainsByProjectId (
oldDeployment . project . id ,
{ branch : oldDeployment.project.prodBranch }
) ;
2024-02-06 08:48:06 +00:00
2024-02-22 11:56:26 +00:00
const octokit = await this . getOctokit ( user . id ) ;
2024-02-14 05:33:22 +00:00
2024-02-22 11:56:26 +00:00
const newDeployment = await this . createDeployment ( user . id ,
2024-02-14 05:33:22 +00:00
octokit ,
2024-02-12 06:04:01 +00:00
{
project : oldDeployment.project ,
branch : oldDeployment.branch ,
environment : Environment.Production ,
domain : prodBranchDomains [ 0 ] ,
2024-02-14 08:55:50 +00:00
commitHash : oldDeployment.commitHash ,
commitMessage : oldDeployment.commitMessage
2024-02-12 06:04:01 +00:00
} ) ;
2024-02-08 09:29:19 +00:00
2024-02-19 08:13:29 +00:00
return newDeployment ;
2024-02-12 06:04:01 +00:00
}
2024-02-15 11:54:57 +00:00
async createDeployment (
userId : string ,
octokit : Octokit ,
2024-02-29 13:03:34 +00:00
data : DeepPartial < Deployment >
2024-02-15 11:54:57 +00:00
) : Promise < Deployment > {
2024-02-14 05:33:22 +00:00
assert ( data . project ? . repository , 'Project repository not found' ) ;
2024-02-22 05:45:17 +00:00
log (
` Creating deployment in project ${ data . project . name } from branch ${ data . branch } `
) ;
2024-02-14 05:33:22 +00:00
const [ owner , repo ] = data . project . repository . split ( '/' ) ;
const { data : packageJSONData } = await octokit . rest . repos . getContent ( {
owner ,
repo ,
path : 'package.json' ,
ref : data.commitHash
} ) ;
if ( ! packageJSONData ) {
throw new Error ( 'Package.json file not found' ) ;
}
assert ( ! Array . isArray ( packageJSONData ) && packageJSONData . type === 'file' ) ;
2024-02-22 04:34:33 +00:00
const packageJSON : PackageJSON = JSON . parse ( atob ( packageJSONData . content ) ) ;
assert ( packageJSON . name , "name field doesn't exist in package.json" ) ;
2024-02-14 05:33:22 +00:00
2024-02-29 13:03:34 +00:00
const repoUrl = ( await octokit . rest . repos . get ( {
owner ,
repo
} ) ) . data . html_url ;
2024-02-15 11:54:57 +00:00
2024-02-15 12:32:37 +00:00
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
2024-02-22 05:45:17 +00:00
const { applicationRecordId , applicationRecordData } =
await this . registry . createApplicationRecord ( {
appName : repo ,
packageJSON ,
appType : data.project ! . template ! ,
commitHash : data.commitHash ! ,
2024-02-29 13:03:34 +00:00
repoUrl
2024-02-22 05:45:17 +00:00
} ) ;
2024-02-15 11:54:57 +00:00
// Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain
2024-02-22 07:20:35 +00:00
if ( data . domain ) {
await this . db . updateDeployment ( {
domainId : data.domain.id
} , {
domain : null
} ) ;
}
2024-02-06 08:48:06 +00:00
2024-02-19 08:13:29 +00:00
const newDeployment = await this . db . addDeployment ( {
2024-02-12 06:04:01 +00:00
project : data.project ,
branch : data.branch ,
commitHash : data.commitHash ,
2024-02-14 08:55:50 +00:00
commitMessage : data.commitMessage ,
2024-02-12 06:04:01 +00:00
environment : data.environment ,
status : DeploymentStatus.Building ,
2024-02-19 08:13:29 +00:00
applicationRecordId ,
applicationRecordData ,
2024-02-12 06:04:01 +00:00
domain : data.domain ,
createdBy : Object.assign ( new User ( ) , {
id : userId
} )
} ) ;
2024-02-08 09:29:19 +00:00
2024-02-19 08:13:29 +00:00
log ( ` Created deployment ${ newDeployment . id } and published application record ${ applicationRecordId } ` ) ;
2024-02-22 07:20:35 +00:00
const environmentVariables = await this . db . getEnvironmentVariablesByProjectId ( data . project . id ! , { environment : Environment.Production } ) ;
const environmentVariablesObj = environmentVariables . reduce ( ( acc , env ) = > {
acc [ env . key ] = env . value ;
return acc ;
} , { } as { [ key : string ] : string } ) ;
2024-02-29 13:03:34 +00:00
// To set project DNS
if ( data . environment === Environment . Production ) {
2024-04-25 11:21:22 +00:00
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
2024-02-29 13:03:34 +00:00
await this . registry . createApplicationDeploymentRequest (
{
deployment : newDeployment ,
appName : repo ,
repository : repoUrl ,
environmentVariables : environmentVariablesObj ,
dns : ` ${ newDeployment . project . name } `
} ) ;
}
2024-04-25 11:21:22 +00:00
const { applicationDeploymentRequestId , applicationDeploymentRequestData } = await this . registry . createApplicationDeploymentRequest (
{
deployment : newDeployment ,
appName : repo ,
repository : repoUrl ,
environmentVariables : environmentVariablesObj ,
dns : ` ${ newDeployment . project . name } - ${ newDeployment . id } `
} ) ;
2024-02-22 07:20:35 +00:00
await this . db . updateDeploymentById ( newDeployment . id , { applicationDeploymentRequestId , applicationDeploymentRequestData } ) ;
2024-02-19 08:13:29 +00:00
return newDeployment ;
2024-02-05 10:51:55 +00:00
}
2024-02-22 11:56:26 +00:00
async addProject ( user : User , organizationSlug : string , data : DeepPartial < Project > ) : Promise < Project | undefined > {
2024-02-07 13:11:54 +00:00
const organization = await this . db . getOrganization ( {
where : {
slug : organizationSlug
}
} ) ;
if ( ! organization ) {
throw new Error ( 'Organization does not exist' ) ;
}
2024-02-22 11:56:26 +00:00
const project = await this . db . addProject ( user , organization . id , data ) ;
2024-02-12 06:04:01 +00:00
2024-02-22 11:56:26 +00:00
const octokit = await this . getOctokit ( user . id ) ;
2024-02-14 05:33:22 +00:00
const [ owner , repo ] = project . repository . split ( '/' ) ;
2024-02-22 05:45:17 +00:00
const {
data : [ latestCommit ]
} = await octokit . rest . repos . listCommits ( {
2024-02-14 05:33:22 +00:00
owner ,
repo ,
sha : project.prodBranch ,
per_page : 1
} ) ;
// Create deployment with prod branch and latest commit
2024-02-22 11:56:26 +00:00
await this . createDeployment ( user . id ,
2024-02-14 05:33:22 +00:00
octokit ,
2024-02-12 06:04:01 +00:00
{
project ,
branch : project.prodBranch ,
environment : Environment.Production ,
2024-02-14 05:33:22 +00:00
domain : null ,
2024-02-14 08:55:50 +00:00
commitHash : latestCommit.sha ,
commitMessage : latestCommit.commit.message
2024-02-15 11:54:57 +00:00
}
) ;
2024-02-12 06:04:01 +00:00
2024-02-15 11:54:57 +00:00
await this . createRepoHook ( octokit , project ) ;
2024-02-14 05:33:22 +00:00
2024-02-12 06:04:01 +00:00
return project ;
2024-02-05 10:51:55 +00:00
}
2024-02-15 11:54:57 +00:00
async createRepoHook ( octokit : Octokit , project : Project ) : Promise < void > {
try {
const [ owner , repo ] = project . repository . split ( '/' ) ;
await octokit . rest . repos . createWebhook ( {
owner ,
repo ,
config : {
2024-02-22 05:45:17 +00:00
url : new URL (
'api/github/webhook' ,
this . config . gitHubConfig . webhookUrl
) . href ,
2024-02-15 11:54:57 +00:00
content_type : 'json'
} ,
events : [ 'push' ]
} ) ;
} catch ( err ) {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
if (
2024-02-22 05:45:17 +00:00
! (
err instanceof RequestError &&
err . status === 422 &&
( err . response ? . data as any ) . errors . some (
( err : any ) = > err . message === GITHUB_UNIQUE_WEBHOOK_ERROR
)
)
2024-02-15 12:32:37 +00:00
) {
2024-02-15 11:54:57 +00:00
throw err ;
}
log ( GITHUB_UNIQUE_WEBHOOK_ERROR ) ;
}
}
async handleGitHubPush ( data : GitPushEventPayload ) : Promise < void > {
2024-02-29 13:03:34 +00:00
const { repository , ref , head_commit : headCommit , deleted } = data ;
if ( deleted ) {
log ( ` Branch ${ ref } deleted for project ${ repository . full_name } ` ) ;
return ;
}
log ( ` Handling GitHub push event from repository: ${ repository . full_name } , branch: ${ ref } ` ) ;
2024-02-22 05:45:17 +00:00
const projects = await this . db . getProjects ( {
where : { repository : repository.full_name }
} ) ;
2024-02-15 11:54:57 +00:00
if ( ! projects . length ) {
log ( ` No projects found for repository ${ repository . full_name } ` ) ;
}
// The `ref` property contains the full reference, including the branch name
// For example, "refs/heads/main" or "refs/heads/feature-branch"
const branch = ref . split ( '/' ) . pop ( ) ;
for await ( const project of projects ) {
const octokit = await this . getOctokit ( project . ownerId ) ;
2024-02-22 05:45:17 +00:00
const [ domain ] = await this . db . getDomainsByProjectId ( project . id , {
branch
} ) ;
2024-02-15 11:54:57 +00:00
// Create deployment with branch and latest commit in GitHub data
2024-02-22 05:45:17 +00:00
await this . createDeployment ( project . ownerId , octokit , {
project ,
branch ,
environment :
project . prodBranch === branch
? Environment . Production
: Environment . Preview ,
domain ,
commitHash : headCommit.id ,
commitMessage : headCommit.message
} ) ;
2024-02-15 11:54:57 +00:00
}
}
2024-02-22 05:45:17 +00:00
async updateProject (
projectId : string ,
data : DeepPartial < Project >
) : Promise < boolean > {
2024-02-06 08:48:06 +00:00
return this . db . updateProjectById ( projectId , data ) ;
2024-02-05 10:51:55 +00:00
}
async deleteProject ( projectId : string ) : Promise < boolean > {
2024-02-15 11:54:57 +00:00
// TODO: Remove GitHub repo hook
2024-02-06 08:48:06 +00:00
return this . db . deleteProjectById ( projectId ) ;
2024-02-05 10:51:55 +00:00
}
async deleteDomain ( domainId : string ) : Promise < boolean > {
2024-02-06 08:48:06 +00:00
const domainsRedirectedFrom = await this . db . getDomains ( {
where : {
2024-02-06 13:41:53 +00:00
redirectToId : domainId
2024-02-06 08:48:06 +00:00
}
} ) ;
if ( domainsRedirectedFrom . length > 0 ) {
2024-02-22 05:45:17 +00:00
throw new Error (
'Cannot delete domain since it has redirects from other domains'
) ;
2024-02-06 08:48:06 +00:00
}
return this . db . deleteDomainById ( domainId ) ;
}
2024-02-22 11:56:26 +00:00
async redeployToProd ( user : User , deploymentId : string ) : Promise < Deployment > {
2024-02-12 06:04:01 +00:00
const oldDeployment = await this . db . getDeployment ( {
2024-02-06 08:48:06 +00:00
relations : {
project : true ,
domain : true ,
createdBy : true
} ,
where : {
id : deploymentId
}
} ) ;
2024-02-12 06:04:01 +00:00
if ( oldDeployment === null ) {
2024-02-06 08:48:06 +00:00
throw new Error ( 'Deployment not found' ) ;
}
2024-02-22 11:56:26 +00:00
const octokit = await this . getOctokit ( user . id ) ;
2024-02-14 05:33:22 +00:00
2024-02-22 11:56:26 +00:00
const newDeployment = await this . createDeployment ( user . id ,
2024-02-14 05:33:22 +00:00
octokit ,
2024-02-12 06:04:01 +00:00
{
project : oldDeployment.project ,
// TODO: Put isCurrent field in project
branch : oldDeployment.branch ,
environment : Environment.Production ,
domain : oldDeployment.domain ,
2024-02-14 08:55:50 +00:00
commitHash : oldDeployment.commitHash ,
commitMessage : oldDeployment.commitMessage
2024-02-05 10:51:55 +00:00
} ) ;
2024-02-06 08:48:06 +00:00
2024-02-19 08:13:29 +00:00
return newDeployment ;
2024-02-06 08:48:06 +00:00
}
2024-02-05 10:51:55 +00:00
2024-02-22 05:45:17 +00:00
async rollbackDeployment (
projectId : string ,
deploymentId : string
) : Promise < boolean > {
2024-02-06 08:48:06 +00:00
// TODO: Implement transactions
const oldCurrentDeployment = await this . db . getDeployment ( {
relations : {
domain : true
} ,
where : {
project : {
id : projectId
} ,
isCurrent : true
2024-02-05 10:51:55 +00:00
}
2024-02-06 08:48:06 +00:00
} ) ;
2024-02-05 10:51:55 +00:00
2024-02-06 08:48:06 +00:00
if ( ! oldCurrentDeployment ) {
2024-02-19 08:13:29 +00:00
throw new Error ( 'Current deployment doesnot exist' ) ;
2024-02-05 10:51:55 +00:00
}
2024-02-06 08:48:06 +00:00
2024-02-22 05:45:17 +00:00
const oldCurrentDeploymentUpdate = await this . db . updateDeploymentById (
oldCurrentDeployment . id ,
{ isCurrent : false , domain : null }
) ;
2024-02-06 08:48:06 +00:00
2024-02-22 05:45:17 +00:00
const newCurrentDeploymentUpdate = await this . db . updateDeploymentById (
deploymentId ,
{ isCurrent : true , domain : oldCurrentDeployment?.domain }
) ;
2024-02-06 08:48:06 +00:00
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate ;
2024-02-05 10:51:55 +00:00
}
2024-03-27 16:54:34 +00:00
async deleteDeployment ( deploymentId : string ) : Promise < boolean > {
2024-03-29 19:31:49 +00:00
const deployment = await this . db . getDeployment ( {
where : {
id : deploymentId
2024-04-25 11:21:22 +00:00
} ,
relations : {
project : true
2024-03-29 19:31:49 +00:00
}
} ) ;
2024-04-15 14:06:04 +00:00
2024-03-29 19:31:49 +00:00
if ( deployment && deployment . applicationDeploymentRecordId ) {
2024-04-25 11:21:22 +00:00
// If deployment is current, remove deployment for project subdomain as well
if ( deployment . isCurrent ) {
const currentDeploymentURL = ` https:// ${ deployment . project . subDomain } ` ;
const deploymentRecords = await this . registry . getDeploymentRecordsByFilter ( {
application : deployment.applicationRecordId ,
url : currentDeploymentURL
} )
if ( ! deploymentRecords . length ) {
log ( ` No ApplicationDeploymentRecord found for URL ${ currentDeploymentURL } and ApplicationDeploymentRecord id ${ deployment . applicationDeploymentRecordId } ` ) ;
return false ;
}
await this . registry . createApplicationDeploymentRemovalRequest ( { deploymentId : deploymentRecords [ 0 ] . id } ) ;
}
2024-03-29 19:31:49 +00:00
const result = await this . registry . createApplicationDeploymentRemovalRequest ( { deploymentId : deployment.applicationDeploymentRecordId } ) ;
2024-04-15 14:06:04 +00:00
await this . db . updateDeploymentById (
deployment . id ,
{
status : DeploymentStatus.Deleting ,
applicationDeploymentRemovalRequestId : result.applicationDeploymentRemovalRequestId ,
applicationDeploymentRemovalRequestData : result.applicationDeploymentRemovalRequestData
}
) ;
2024-03-29 19:31:49 +00:00
return ( result !== undefined || result !== null ) ;
}
2024-04-15 14:06:04 +00:00
2024-03-29 19:31:49 +00:00
return false ;
2024-03-27 16:54:34 +00:00
}
2024-02-22 05:45:17 +00:00
async addDomain (
projectId : string ,
data : { name : string }
) : Promise < {
primaryDomain : Domain ;
redirectedDomain : Domain ;
2024-02-06 08:48:06 +00:00
} > {
const currentProject = await this . db . getProjectById ( projectId ) ;
if ( currentProject === null ) {
throw new Error ( ` Project with ${ projectId } not found ` ) ;
}
const primaryDomainDetails = {
2024-02-14 12:05:02 +00:00
. . . data ,
2024-02-06 08:48:06 +00:00
branch : currentProject.prodBranch ,
project : currentProject
} ;
const savedPrimaryDomain = await this . db . addDomain ( primaryDomainDetails ) ;
2024-02-14 12:05:02 +00:00
const domainArr = data . name . split ( 'www.' ) ;
2024-02-06 08:48:06 +00:00
const redirectedDomainDetails = {
name : domainArr.length > 1 ? domainArr [ 1 ] : ` www. ${ domainArr [ 0 ] } ` ,
branch : currentProject.prodBranch ,
project : currentProject ,
redirectTo : savedPrimaryDomain
} ;
2024-02-22 05:45:17 +00:00
const savedRedirectedDomain = await this . db . addDomain (
redirectedDomainDetails
) ;
2024-02-06 08:48:06 +00:00
2024-02-22 05:45:17 +00:00
return {
primaryDomain : savedPrimaryDomain ,
redirectedDomain : savedRedirectedDomain
} ;
2024-02-06 08:48:06 +00:00
}
2024-02-22 05:45:17 +00:00
async updateDomain (
domainId : string ,
data : DeepPartial < Domain >
) : Promise < boolean > {
2024-02-06 08:48:06 +00:00
const domain = await this . db . getDomain ( {
where : {
2024-02-06 13:41:53 +00:00
id : domainId
2024-02-06 08:48:06 +00:00
}
} ) ;
if ( domain === null ) {
throw new Error ( ` Error finding domain with id ${ domainId } ` ) ;
}
const newDomain = {
2024-02-14 12:05:02 +00:00
. . . data
2024-02-06 08:48:06 +00:00
} ;
const domainsRedirectedFrom = await this . db . getDomains ( {
where : {
project : {
id : domain.projectId
2024-02-05 10:51:55 +00:00
} ,
2024-02-06 08:48:06 +00:00
redirectToId : domain.id
}
} ) ;
// If there are domains redirecting to current domain, only branch of current domain can be updated
2024-02-14 12:05:02 +00:00
if ( domainsRedirectedFrom . length > 0 && data . branch === domain . branch ) {
2024-02-06 08:48:06 +00:00
throw new Error ( 'Remove all redirects to this domain before updating' ) ;
}
2024-02-14 12:05:02 +00:00
if ( data . redirectToId ) {
2024-02-06 08:48:06 +00:00
const redirectedDomain = await this . db . getDomain ( {
2024-02-05 10:51:55 +00:00
where : {
2024-02-14 12:05:02 +00:00
id : data.redirectToId
2024-02-05 10:51:55 +00:00
}
} ) ;
2024-02-06 08:48:06 +00:00
if ( redirectedDomain === null ) {
throw new Error ( 'Could not find Domain to redirect to' ) ;
2024-02-05 10:51:55 +00:00
}
2024-02-06 08:48:06 +00:00
if ( redirectedDomain . redirectToId ) {
2024-02-22 05:45:17 +00:00
throw new Error (
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
) ;
2024-02-05 10:51:55 +00:00
}
2024-02-06 08:48:06 +00:00
newDomain . redirectTo = redirectedDomain ;
}
2024-02-05 10:51:55 +00:00
2024-02-06 08:48:06 +00:00
const updateResult = await this . db . updateDomainById ( domainId , newDomain ) ;
2024-02-05 10:51:55 +00:00
2024-02-06 08:48:06 +00:00
return updateResult ;
2024-02-05 10:51:55 +00:00
}
2024-02-08 09:29:19 +00:00
2024-02-22 11:56:26 +00:00
async authenticateGitHub ( code :string , user : User ) : Promise < { token : string } > {
2024-02-14 05:33:22 +00:00
const { authentication : { token } } = await this . oauthApp . createToken ( {
2024-02-08 09:29:19 +00:00
code
} ) ;
2024-02-22 11:56:26 +00:00
await this . db . updateUser ( user , { gitHubToken : token } ) ;
2024-02-08 09:29:19 +00:00
return { token } ;
}
2024-02-22 11:56:26 +00:00
async unauthenticateGitHub ( user : User , data : DeepPartial < User > ) : Promise < boolean > {
return this . db . updateUser ( user , data ) ;
2024-02-08 09:29:19 +00:00
}
2024-02-02 09:32:12 +00:00
}