forked from cerc-io/snowballtools-base
		
	Publish app deployment record in Laconic registry on creating new deployments (#62)
* Publish record in laconic registry on creating project and deployment * Refactor publish record method * Set name for the published record * Publish application deployment request * Add README for publishing record * Add await in add project resolver method * Update meta data for deployment request record * Remove title field from deployment entity * Refactor service and registry class for publishing record * Add record data to project and deployment entity * Set record id and data as nullable in project entity --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
		
							parent
							
								
									bd6a6b330c
								
							
						
					
					
						commit
						a58b9b255e
					
				
							
								
								
									
										1
									
								
								.npmrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.npmrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| @cerc-io:registry=https://git.vdb.to/api/packages/cerc-io/npm/ | ||||
							
								
								
									
										55
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								README.md
									
									
									
									
									
								
							| @ -41,6 +41,56 @@ | ||||
|     - In "Authorization callback URL", type `http://localhost:3000/projects/create` | ||||
|     - Generate a new client secret after app is created | ||||
| 
 | ||||
| - Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md) | ||||
| 
 | ||||
| - Create the bond and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml) | ||||
| 
 | ||||
|   ```bash | ||||
|   laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns bond create --type aphoton --quantity 1000000000 --gas 200000 --fees 200000aphoton" | ||||
| 
 | ||||
|   # {"bondId":"b40f1308510f799860fb6f1ede47245a2d59f336631158f25ae0eec30aabaf89"} | ||||
|   ``` | ||||
| 
 | ||||
|   - Export the bond id that is generated | ||||
| 
 | ||||
|     ```bash | ||||
|     export BOND_ID=<BOND-ID> | ||||
|     ``` | ||||
| 
 | ||||
| - Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml) | ||||
| 
 | ||||
|   ```bash | ||||
|   laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe" | ||||
|   # WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y | ||||
|   # 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81 | ||||
|   ``` | ||||
| 
 | ||||
| - Get the rest and GQL endpoint of laconicd and set it to `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml) | ||||
| 
 | ||||
|   ```bash | ||||
|   # For registryConfig.restEndpoint | ||||
|   laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317 | ||||
|   # 0.0.0.0:32777 | ||||
| 
 | ||||
|   # For registryConfig.gqlEndpoint | ||||
|   laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473 | ||||
|   # 0.0.0.0:32771 | ||||
|   ``` | ||||
| 
 | ||||
| - Reserve authority for `snowball` | ||||
| 
 | ||||
|   ```bash | ||||
|   laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns authority reserve snowball" | ||||
|   # {"success":true} | ||||
|   ``` | ||||
| 
 | ||||
| - Set authority bond | ||||
| 
 | ||||
|   ```bash | ||||
|   laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns authority bond set snowball $BOND_ID" | ||||
|   # {"success":true} | ||||
|   ``` | ||||
| 
 | ||||
| - Start the server | ||||
| 
 | ||||
|   ```bash | ||||
| @ -57,13 +107,13 @@ | ||||
| 
 | ||||
| - Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend` | ||||
| 
 | ||||
|   ``` | ||||
|   ```env | ||||
|   REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql' | ||||
|   ``` | ||||
| 
 | ||||
| - Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file | ||||
| 
 | ||||
|   ``` | ||||
|   ```env | ||||
|   REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID> | ||||
|   ``` | ||||
| 
 | ||||
| @ -90,3 +140,4 @@ | ||||
|   ```bash | ||||
|   python3 -m http.server -d build 3000 | ||||
|   ``` | ||||
| 
 | ||||
|  | ||||
| @ -9,3 +9,14 @@ | ||||
| [githubOauth] | ||||
|   clientId = "" | ||||
|   clientSecret = "" | ||||
| 
 | ||||
| [registryConfig] | ||||
|   restEndpoint = "http://localhost:1317" | ||||
|   gqlEndpoint = "http://localhost:9473/api" | ||||
|   chainId = "laconic_9000-1" | ||||
|   privateKey = "" | ||||
|   bondId = "" | ||||
|   [registryConfig.fee] | ||||
|     amount = "200000" | ||||
|     denom = "aphoton" | ||||
|     gas = "550000" | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
|   "version": "1.0.0", | ||||
|   "main": "index.js", | ||||
|   "dependencies": { | ||||
|     "@cerc-io/laconic-sdk": "^0.1.14", | ||||
|     "@graphql-tools/schema": "^10.0.2", | ||||
|     "@graphql-tools/utils": "^10.0.12", | ||||
|     "@octokit/oauth-app": "^6.1.0", | ||||
| @ -18,6 +19,7 @@ | ||||
|     "nanoid": "3", | ||||
|     "nanoid-dictionary": "^5.0.0-beta.1", | ||||
|     "reflect-metadata": "^0.2.1", | ||||
|     "semver": "^7.6.0", | ||||
|     "toml": "^3.0.0", | ||||
|     "ts-node": "^10.9.2", | ||||
|     "typeorm": "^0.3.19", | ||||
|  | ||||
| @ -13,8 +13,22 @@ export interface GithubOauthConfig { | ||||
|   clientSecret: string; | ||||
| } | ||||
| 
 | ||||
| export interface RegistryConfig { | ||||
|   restEndpoint: string; | ||||
|   gqlEndpoint: string; | ||||
|   chainId: string; | ||||
|   privateKey: string; | ||||
|   bondId: string; | ||||
|   fee: { | ||||
|     amount: string; | ||||
|     denom: string; | ||||
|     gas: string; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export interface Config { | ||||
|   server: ServerConfig; | ||||
|   database: DatabaseConfig; | ||||
|   githubOauth: GithubOauthConfig; | ||||
|   registryConfig: RegistryConfig; | ||||
| } | ||||
|  | ||||
| @ -19,12 +19,26 @@ export enum Environment { | ||||
|   Development = 'Development', | ||||
| } | ||||
| 
 | ||||
| enum Status { | ||||
| export enum DeploymentStatus { | ||||
|   Building = 'Building', | ||||
|   Ready = 'Ready', | ||||
|   Error = 'Error', | ||||
| } | ||||
| 
 | ||||
| export interface ApplicationRecord { | ||||
|   type: string; | ||||
|   version:string | ||||
|   name: string | ||||
|   description: string | ||||
|   homepage: string | ||||
|   license: string | ||||
|   author: string | ||||
|   repository: string, | ||||
|   app_version: string | ||||
|   repository_ref: string | ||||
|   app_type: string | ||||
| } | ||||
| 
 | ||||
| @Entity() | ||||
| export class Deployment { | ||||
|   // TODO: set custom generated id
 | ||||
| @ -46,10 +60,13 @@ export class Deployment { | ||||
|     commitHash!: string; | ||||
| 
 | ||||
|   @Column('varchar') | ||||
|     title!: string; | ||||
|     url!: string; | ||||
| 
 | ||||
|   @Column('varchar') | ||||
|     url!: string; | ||||
|     recordId!: string; | ||||
| 
 | ||||
|   @Column('simple-json') | ||||
|     recordData!: ApplicationRecord; | ||||
| 
 | ||||
|   @Column({ | ||||
|     enum: Environment | ||||
| @ -60,9 +77,9 @@ export class Deployment { | ||||
|     isCurrent!: boolean; | ||||
| 
 | ||||
|   @Column({ | ||||
|     enum: Status | ||||
|     enum: DeploymentStatus | ||||
|   }) | ||||
|     status!: Status; | ||||
|     status!: DeploymentStatus; | ||||
| 
 | ||||
|   @ManyToOne(() => User) | ||||
|   @JoinColumn({ name: 'createdBy' }) | ||||
|  | ||||
| @ -15,6 +15,21 @@ import { Organization } from './Organization'; | ||||
| import { ProjectMember } from './ProjectMember'; | ||||
| import { Deployment } from './Deployment'; | ||||
| 
 | ||||
| export interface ApplicationDeploymentRequest { | ||||
|   type: string | ||||
|   version: string | ||||
|   name: string | ||||
|   application: string | ||||
|   config: { | ||||
|     env: {[key:string]: string} | ||||
|   }, | ||||
|   meta: { | ||||
|     note: string | ||||
|     repository: string | ||||
|     repository_ref: string | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Entity() | ||||
| export class Project { | ||||
|   @PrimaryGeneratedColumn('uuid') | ||||
| @ -40,9 +55,16 @@ export class Project { | ||||
|   @Column('varchar', { length: 255, default: 'main' }) | ||||
|     prodBranch!: string; | ||||
| 
 | ||||
|   @Column('varchar', { nullable: true }) | ||||
|     recordId!: string | null; | ||||
| 
 | ||||
|   @Column('simple-json', { nullable: true }) | ||||
|     recordData!: ApplicationDeploymentRequest | null; | ||||
| 
 | ||||
|   @Column('text', { default: '' }) | ||||
|     description!: string; | ||||
| 
 | ||||
|   // TODO: Compute template & framework in import repository
 | ||||
|   @Column('varchar', { nullable: true }) | ||||
|     template!: string | null; | ||||
| 
 | ||||
|  | ||||
| @ -12,23 +12,26 @@ import { getConfig } from './utils'; | ||||
| import { Config } from './config'; | ||||
| import { DEFAULT_CONFIG_FILE_PATH } from './constants'; | ||||
| import { Service } from './service'; | ||||
| import { Registry } from './registry'; | ||||
| 
 | ||||
| const log = debug('snowball:server'); | ||||
| const OAUTH_CLIENT_TYPE = 'oauth-app'; | ||||
| 
 | ||||
| export const main = async (): Promise<void> => { | ||||
|   // TODO: get config path using cli
 | ||||
|   const { server, database, githubOauth } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); | ||||
|   const { server, database, githubOauth, registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); | ||||
| 
 | ||||
|   // TODO: Move to Service class
 | ||||
|   const app = new OAuthApp({ | ||||
|     clientType: 'oauth-app', | ||||
|     clientType: OAUTH_CLIENT_TYPE, | ||||
|     clientId: githubOauth.clientId, | ||||
|     clientSecret: githubOauth.clientSecret | ||||
|   }); | ||||
| 
 | ||||
|   const db = new Database(database); | ||||
|   await db.init(); | ||||
|   const service = new Service(db, app); | ||||
| 
 | ||||
|   const registry = new Registry(registryConfig); | ||||
|   const service = new Service(db, app, registry); | ||||
| 
 | ||||
|   const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString(); | ||||
|   const resolvers = await createResolvers(service); | ||||
|  | ||||
							
								
								
									
										133
									
								
								packages/backend/src/registry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								packages/backend/src/registry.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,133 @@ | ||||
| import debug from 'debug'; | ||||
| import { inc as semverInc } from 'semver'; | ||||
| 
 | ||||
| import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk'; | ||||
| 
 | ||||
| import { RegistryConfig } from './config'; | ||||
| import { ApplicationDeploymentRequest } from './entity/Project'; | ||||
| import { ApplicationRecord } from './entity/Deployment'; | ||||
| 
 | ||||
| const log = debug('snowball:registry'); | ||||
| 
 | ||||
| const APP_RECORD_TYPE = 'ApplicationRecord'; | ||||
| const DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRequest'; | ||||
| const AUTHORITY_NAME = 'snowball'; | ||||
| 
 | ||||
| export class Registry { | ||||
|   private registry: LaconicRegistry; | ||||
|   private registryConfig: RegistryConfig; | ||||
| 
 | ||||
|   constructor (registryConfig : RegistryConfig) { | ||||
|     this.registryConfig = registryConfig; | ||||
|     this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId); | ||||
|   } | ||||
| 
 | ||||
|   async createApplicationRecord (data: { recordName: string, appType: string }): Promise<{recordId: string, recordData: ApplicationRecord}> { | ||||
|     // TODO: Get record name from repo package.json name
 | ||||
|     const recordName = data.recordName; | ||||
| 
 | ||||
|     // Use laconic-sdk to publish record
 | ||||
|     // Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
 | ||||
|     // Fetch previous records
 | ||||
|     const records = await this.registry.queryRecords({ | ||||
|       type: APP_RECORD_TYPE, | ||||
|       name: recordName | ||||
|     }, true); | ||||
| 
 | ||||
|     // Get next version of record
 | ||||
|     const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId); | ||||
|     const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); | ||||
|     const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch'); | ||||
| 
 | ||||
|     // Create record of type ApplicationRecord and publish
 | ||||
|     const applicationRecord = { | ||||
|       type: APP_RECORD_TYPE, | ||||
|       version: nextVersion ?? '', | ||||
|       name: recordName, | ||||
| 
 | ||||
|       // TODO: Get data from repo package.json
 | ||||
|       description: '', | ||||
|       homepage: '', | ||||
|       license: '', | ||||
|       author: '', | ||||
|       repository: '', | ||||
|       app_version: '0.1.0', | ||||
| 
 | ||||
|       // TODO: Get latest commit hash from repo production branch / deployment
 | ||||
|       repository_ref: '10ac6678e8372a05ad5bb1c34c34', | ||||
|       app_type: data.appType | ||||
|     }; | ||||
| 
 | ||||
|     const result = await this.registry.setRecord( | ||||
|       { | ||||
|         privateKey: this.registryConfig.privateKey, | ||||
|         record: applicationRecord, | ||||
|         bondId: this.registryConfig.bondId | ||||
|       }, | ||||
|       '', | ||||
|       this.registryConfig.fee | ||||
|     ); | ||||
| 
 | ||||
|     log('Application record data:', applicationRecord); | ||||
| 
 | ||||
|     // TODO: Discuss computation of crn
 | ||||
|     const crn = this.getCrn(data.recordName); | ||||
| 
 | ||||
|     await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee); | ||||
|     await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee); | ||||
|     await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee); | ||||
| 
 | ||||
|     return { recordId: result.data.id, recordData: applicationRecord }; | ||||
|   } | ||||
| 
 | ||||
|   async createApplicationDeploymentRequest (data: { appName: string }): Promise<{recordId: string, recordData: ApplicationDeploymentRequest}> { | ||||
|     const crn = this.getCrn(data.appName); | ||||
|     const records = await this.registry.resolveNames([crn]); | ||||
|     const applicationRecord = records[0]; | ||||
| 
 | ||||
|     if (!applicationRecord) { | ||||
|       throw new Error(`No record found for ${crn}`); | ||||
|     } | ||||
| 
 | ||||
|     // Create record of type ApplicationDeploymentRequest and publish
 | ||||
|     const applicationDeploymentRequest = { | ||||
|       type: DEPLOYMENT_RECORD_TYPE, | ||||
|       version: '1.0.0', | ||||
|       name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`, | ||||
|       application: `${crn}@${applicationRecord.attributes.app_version}`, | ||||
| 
 | ||||
|       // TODO: Not set in test-progressive-web-app CI
 | ||||
|       // dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
 | ||||
|       // deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
 | ||||
| 
 | ||||
|       config: { | ||||
|         env: { | ||||
|           CERC_WEBAPP_DEBUG: `${applicationRecord.attributes.app_version}` | ||||
|         } | ||||
|       }, | ||||
|       meta: { | ||||
|         note: `Added by Snowball @ ${(new Date()).toISOString()}`, | ||||
|         repository: applicationRecord.attributes.repository, | ||||
|         repository_ref: applicationRecord.attributes.repository_ref | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     const result = await this.registry.setRecord( | ||||
|       { | ||||
|         privateKey: this.registryConfig.privateKey, | ||||
|         record: applicationDeploymentRequest, | ||||
|         bondId: this.registryConfig.bondId | ||||
|       }, | ||||
|       '', | ||||
|       this.registryConfig.fee | ||||
|     ); | ||||
|     log(`Application deployment request record published: ${result.data.id}`); | ||||
|     log('Application deployment request data:', applicationDeploymentRequest); | ||||
| 
 | ||||
|     return { recordId: result.data.id, recordData: applicationDeploymentRequest }; | ||||
|   } | ||||
| 
 | ||||
|   getCrn (appName: string): string { | ||||
|     return `crn://${AUTHORITY_NAME}/applications/${appName}`; | ||||
|   } | ||||
| } | ||||
| @ -7,9 +7,8 @@ import { Domain } from './entity/Domain'; | ||||
| import { Project } from './entity/Project'; | ||||
| import { EnvironmentVariable } from './entity/EnvironmentVariable'; | ||||
| 
 | ||||
| const log = debug('snowball:database'); | ||||
| const log = debug('snowball:resolver'); | ||||
| 
 | ||||
| // TODO: Remove Database argument and refactor code to Service
 | ||||
| export const createResolvers = async (service: Service): Promise<any> => { | ||||
|   return { | ||||
|     Query: { | ||||
| @ -129,9 +128,10 @@ export const createResolvers = async (service: Service): Promise<any> => { | ||||
| 
 | ||||
|       addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => { | ||||
|         try { | ||||
|           return service.addProject(context.userId, organizationSlug, data); | ||||
|           return await service.addProject(context.userId, organizationSlug, data); | ||||
|         } catch (err) { | ||||
|           log(err); | ||||
|           throw err; | ||||
|         } | ||||
|       }, | ||||
| 
 | ||||
| @ -146,7 +146,7 @@ export const createResolvers = async (service: Service): Promise<any> => { | ||||
| 
 | ||||
|       redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => { | ||||
|         try { | ||||
|           return await service.redeployToProd(context.userId, deploymentId); | ||||
|           return Boolean(await service.redeployToProd(context.userId, deploymentId)); | ||||
|         } catch (err) { | ||||
|           log(err); | ||||
|           return false; | ||||
|  | ||||
| @ -90,7 +90,6 @@ type Deployment { | ||||
|   domain: Domain | ||||
|   branch: String! | ||||
|   commitHash: String! | ||||
|   title: String! | ||||
|   url: String! | ||||
|   environment: Environment! | ||||
|   isCurrent: Boolean! | ||||
| @ -133,6 +132,7 @@ input AddProjectInput { | ||||
|   name: String! | ||||
|   repository: String! | ||||
|   prodBranch: String! | ||||
|   template: String | ||||
| } | ||||
| 
 | ||||
| input UpdateProjectInput { | ||||
|  | ||||
| @ -1,23 +1,30 @@ | ||||
| import assert from 'assert'; | ||||
| import debug from 'debug'; | ||||
| import { DeepPartial, FindOptionsWhere } from 'typeorm'; | ||||
| 
 | ||||
| import { OAuthApp } from '@octokit/oauth-app'; | ||||
| 
 | ||||
| import { Database } from './database'; | ||||
| import { Deployment, Environment } from './entity/Deployment'; | ||||
| import { Deployment, DeploymentStatus, Environment } from './entity/Deployment'; | ||||
| import { Domain } from './entity/Domain'; | ||||
| import { EnvironmentVariable } from './entity/EnvironmentVariable'; | ||||
| import { Organization } from './entity/Organization'; | ||||
| import { Project } from './entity/Project'; | ||||
| import { Permission, ProjectMember } from './entity/ProjectMember'; | ||||
| import { User } from './entity/User'; | ||||
| import { Registry } from './registry'; | ||||
| 
 | ||||
| const log = debug('snowball:service'); | ||||
| 
 | ||||
| export class Service { | ||||
|   private db: Database; | ||||
|   private app: OAuthApp; | ||||
|   private registry: Registry; | ||||
| 
 | ||||
|   constructor (db: Database, app: OAuthApp) { | ||||
|   constructor (db: Database, app: OAuthApp, registry: Registry) { | ||||
|     this.db = db; | ||||
|     this.app = app; | ||||
|     this.registry = registry; | ||||
|   } | ||||
| 
 | ||||
|   async getUser (userId: string): Promise<User | null> { | ||||
| @ -148,15 +155,21 @@ export class Service { | ||||
|   } | ||||
| 
 | ||||
|   async updateDeploymentToProd (userId: string, deploymentId: string): Promise<Deployment> { | ||||
|     const deployment = await this.db.getDeployment({ where: { id: deploymentId }, relations: { project: true } }); | ||||
|     const oldDeployment = await this.db.getDeployment({ | ||||
|       where: { id: deploymentId }, | ||||
|       relations: { | ||||
|         project: true | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (!deployment) { | ||||
|     if (!oldDeployment) { | ||||
|       throw new Error('Deployment does not exist'); | ||||
|     } | ||||
| 
 | ||||
|     const prodBranchDomains = await this.db.getDomainsByProjectId(deployment.project.id, { branch: deployment.project.prodBranch }); | ||||
|     const prodBranchDomains = await this.db.getDomainsByProjectId(oldDeployment.project.id, { branch: oldDeployment.project.prodBranch }); | ||||
| 
 | ||||
|     const oldDeployment = await this.db.getDeployment({ | ||||
|     // TODO: Fix unique constraint error for domain
 | ||||
|     const deploymentWithProdBranchDomain = await this.db.getDeployment({ | ||||
|       where: { | ||||
|         domain: { | ||||
|           id: prodBranchDomains[0].id | ||||
| @ -164,24 +177,64 @@ export class Service { | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (oldDeployment) { | ||||
|       await this.db.updateDeploymentById(oldDeployment.id, { | ||||
|     if (deploymentWithProdBranchDomain) { | ||||
|       await this.db.updateDeploymentById(deploymentWithProdBranchDomain.id, { | ||||
|         domain: null, | ||||
|         isCurrent: false | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     const { createdAt, updatedAt, ...updatedDeployment } = deployment; | ||||
| 
 | ||||
|     updatedDeployment.isCurrent = true; | ||||
|     updatedDeployment.environment = Environment.Production; | ||||
|     updatedDeployment.domain = prodBranchDomains[0]; | ||||
|     updatedDeployment.createdBy = Object.assign(new User(), { | ||||
|       id: userId | ||||
|     const oldCurrentDeployment = await this.db.getDeployment({ | ||||
|       relations: { | ||||
|         domain: true | ||||
|       }, | ||||
|       where: { | ||||
|         project: { | ||||
|           id: oldDeployment.project.id | ||||
|         }, | ||||
|         isCurrent: true | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     const newDeployement = await this.db.addDeployement(updatedDeployment); | ||||
|     if (oldCurrentDeployment) { | ||||
|       await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null }); | ||||
|     } | ||||
| 
 | ||||
|     const newDeployement = await this.createDeployment(userId, | ||||
|       { | ||||
|         project: oldDeployment.project, | ||||
|         isCurrent: true, | ||||
|         branch: oldDeployment.branch, | ||||
|         environment: Environment.Production, | ||||
|         domain: prodBranchDomains[0], | ||||
|         commitHash: oldDeployment.commitHash | ||||
|       }); | ||||
| 
 | ||||
|     return newDeployement; | ||||
|   } | ||||
| 
 | ||||
|   async createDeployment (userId: string, data: DeepPartial<Deployment>): Promise<Deployment> { | ||||
|     const { recordId, recordData } = await this.registry.createApplicationRecord({ | ||||
|       recordName: data.project?.name ?? '', | ||||
|       appType: data.project?.template ?? '' | ||||
|     }); | ||||
| 
 | ||||
|     const newDeployement = await this.db.addDeployement({ | ||||
|       project: data.project, | ||||
|       branch: data.branch, | ||||
|       commitHash: data.commitHash, | ||||
|       environment: data.environment, | ||||
|       isCurrent: data.isCurrent, | ||||
|       status: DeploymentStatus.Building, | ||||
|       recordId, | ||||
|       recordData, | ||||
|       domain: data.domain, | ||||
|       createdBy: Object.assign(new User(), { | ||||
|         id: userId | ||||
|       }) | ||||
|     }); | ||||
| 
 | ||||
|     log(`Application record ${recordId} published for deployment ${newDeployement.id}`); | ||||
|     return newDeployement; | ||||
|   } | ||||
| 
 | ||||
| @ -195,7 +248,27 @@ export class Service { | ||||
|       throw new Error('Organization does not exist'); | ||||
|     } | ||||
| 
 | ||||
|     return this.db.addProject(userId, organization.id, data); | ||||
|     const project = await this.db.addProject(userId, organization.id, data); | ||||
| 
 | ||||
|     // TODO: Get repository details from github
 | ||||
|     await this.createDeployment(userId, | ||||
|       { | ||||
|         project, | ||||
|         isCurrent: true, | ||||
|         branch: project.prodBranch, | ||||
|         environment: Environment.Production, | ||||
|         // TODO: Set latest commit hash
 | ||||
|         commitHash: '', | ||||
|         domain: null | ||||
|       }); | ||||
| 
 | ||||
|     const { recordId, recordData } = await this.registry.createApplicationDeploymentRequest({ appName: project.name }); | ||||
|     await this.db.updateProjectById(project.id, { | ||||
|       recordId, | ||||
|       recordData | ||||
|     }); | ||||
| 
 | ||||
|     return project; | ||||
|   } | ||||
| 
 | ||||
|   async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> { | ||||
| @ -220,8 +293,8 @@ export class Service { | ||||
|     return this.db.deleteDomainById(domainId); | ||||
|   } | ||||
| 
 | ||||
|   async redeployToProd (userId: string, deploymentId: string): Promise<Deployment| boolean> { | ||||
|     const deployment = await this.db.getDeployment({ | ||||
|   async redeployToProd (userId: string, deploymentId: string): Promise<Deployment> { | ||||
|     const oldDeployment = await this.db.getDeployment({ | ||||
|       relations: { | ||||
|         project: true, | ||||
|         domain: true, | ||||
| @ -232,24 +305,24 @@ export class Service { | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (deployment === null) { | ||||
|     if (oldDeployment === null) { | ||||
|       throw new Error('Deployment not found'); | ||||
|     } | ||||
| 
 | ||||
|     const { createdAt, updatedAt, ...updatedDeployment } = deployment; | ||||
|     await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false }); | ||||
| 
 | ||||
|     if (updatedDeployment.environment === Environment.Production) { | ||||
|       // TODO: Put isCurrent field in project
 | ||||
|       updatedDeployment.isCurrent = true; | ||||
|       updatedDeployment.createdBy = Object.assign(new User(), { | ||||
|         id: userId | ||||
|     const newDeployement = await this.createDeployment(userId, | ||||
|       { | ||||
|         project: oldDeployment.project, | ||||
|         // TODO: Put isCurrent field in project
 | ||||
|         branch: oldDeployment.branch, | ||||
|         isCurrent: true, | ||||
|         environment: Environment.Production, | ||||
|         domain: oldDeployment.domain, | ||||
|         commitHash: oldDeployment.commitHash | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     const oldDeployment = await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false }); | ||||
|     const newDeployement = await this.db.addDeployement(updatedDeployment); | ||||
| 
 | ||||
|     return oldDeployment && Boolean(newDeployement); | ||||
|     return newDeployement; | ||||
|   } | ||||
| 
 | ||||
|   async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> { | ||||
|  | ||||
							
								
								
									
										30
									
								
								packages/backend/test/fixtures/deployments.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								packages/backend/test/fixtures/deployments.json
									
									
									
									
										vendored
									
									
								
							| @ -4,10 +4,11 @@ | ||||
|     "domainIndex":0, | ||||
|     "createdByIndex": 0, | ||||
|     "id":"ffhae3zq", | ||||
|     "title": "nextjs-boilerplate-1", | ||||
|     "status": "Building", | ||||
|     "environment": "Production", | ||||
|     "isCurrent": true, | ||||
|     "recordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "main", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "testProject-ffhae3zq.snowball.xyz" | ||||
| @ -17,10 +18,11 @@ | ||||
|     "domainIndex":1, | ||||
|     "createdByIndex": 0, | ||||
|     "id":"vehagei8", | ||||
|     "title": "nextjs-boilerplate-2", | ||||
|     "status": "Ready", | ||||
|     "environment": "Preview", | ||||
|     "isCurrent": false, | ||||
|     "recordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "test", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "testProject-vehagei8.snowball.xyz" | ||||
| @ -30,10 +32,11 @@ | ||||
|     "domainIndex":2, | ||||
|     "createdByIndex": 0, | ||||
|     "id":"qmgekyte", | ||||
|     "title": "nextjs-boilerplate-3", | ||||
|     "status": "Error", | ||||
|     "environment": "Development", | ||||
|     "isCurrent": false, | ||||
|     "recordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "test", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "testProject-qmgekyte.snowball.xyz" | ||||
| @ -43,10 +46,11 @@ | ||||
|     "domainIndex": null, | ||||
|     "createdByIndex": 0, | ||||
|     "id":"f8wsyim6", | ||||
|     "title": "nextjs-boilerplate-4", | ||||
|     "status": "Ready", | ||||
|     "environment": "Production", | ||||
|     "isCurrent": false, | ||||
|     "recordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "prod", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "testProject-f8wsyim6.snowball.xyz" | ||||
| @ -56,10 +60,11 @@ | ||||
|     "domainIndex":3, | ||||
|     "createdByIndex": 1, | ||||
|     "id":"eO8cckxk", | ||||
|     "title": "nextjs-boilerplate-1", | ||||
|     "status": "Building", | ||||
|     "environment": "Production", | ||||
|     "isCurrent": true, | ||||
|     "recordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "main", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "testProject-2-eO8cckxk.snowball.xyz" | ||||
| @ -69,10 +74,11 @@ | ||||
|     "domainIndex":4, | ||||
|     "createdByIndex": 1, | ||||
|     "id":"yaq0t5yw", | ||||
|     "title": "nextjs-boilerplate-2", | ||||
|     "status": "Ready", | ||||
|     "environment": "Preview", | ||||
|     "isCurrent": false, | ||||
|     "recordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "test", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "testProject-2-yaq0t5yw.snowball.xyz" | ||||
| @ -82,10 +88,11 @@ | ||||
|     "domainIndex":5, | ||||
|     "createdByIndex": 1, | ||||
|     "id":"hwwr6sbx", | ||||
|     "title": "nextjs-boilerplate-3", | ||||
|     "status": "Error", | ||||
|     "environment": "Development", | ||||
|     "isCurrent": false, | ||||
|     "recordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "test", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "testProject-2-hwwr6sbx.snowball.xyz" | ||||
| @ -95,10 +102,11 @@ | ||||
|     "domainIndex":6, | ||||
|     "createdByIndex": 2, | ||||
|     "id":"ndxje48a", | ||||
|     "title": "nextjs-boilerplate-1", | ||||
|     "status": "Building", | ||||
|     "environment": "Production", | ||||
|     "isCurrent": true, | ||||
|     "recordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "main", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "iglootools-ndxje48a.snowball.xyz" | ||||
| @ -108,10 +116,11 @@ | ||||
|     "domainIndex":7, | ||||
|     "createdByIndex": 2, | ||||
|     "id":"gtgpgvei", | ||||
|     "title": "nextjs-boilerplate-2", | ||||
|     "status": "Ready", | ||||
|     "environment": "Preview", | ||||
|     "isCurrent": false, | ||||
|     "recordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "branch": "test", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "iglootools-gtgpgvei.snowball.xyz" | ||||
| @ -121,10 +130,11 @@ | ||||
|     "domainIndex":8, | ||||
|     "createdByIndex": 2, | ||||
|     "id":"b4bpthjr", | ||||
|     "title": "nextjs-boilerplate-3", | ||||
|     "status": "Error", | ||||
|     "environment": "Development", | ||||
|     "isCurrent": false, | ||||
|     "recordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", | ||||
|     "recordData": {}, | ||||
|     "branch": "test", | ||||
|     "commitHash": "testXyz", | ||||
|     "url": "iglootools-b4bpthjr.snowball.xyz" | ||||
|  | ||||
							
								
								
									
										10
									
								
								packages/backend/test/fixtures/projects.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								packages/backend/test/fixtures/projects.json
									
									
									
									
										vendored
									
									
								
							| @ -10,6 +10,8 @@ | ||||
|     "framework": "test", | ||||
|     "webhooks": [], | ||||
|     "icon": "", | ||||
|     "recordId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "subDomain": "testProject.snowball.xyz" | ||||
|   }, | ||||
|   { | ||||
| @ -23,6 +25,8 @@ | ||||
|     "framework": "test-2", | ||||
|     "webhooks": [], | ||||
|     "icon": "", | ||||
|     "recordId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "subDomain": "testProject-2.snowball.xyz" | ||||
|   }, | ||||
|   { | ||||
| @ -36,6 +40,8 @@ | ||||
|     "framework": "test-3", | ||||
|     "webhooks": [], | ||||
|     "icon": "", | ||||
|     "recordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "subDomain": "iglootools.snowball.xyz" | ||||
|   }, | ||||
|   { | ||||
| @ -49,6 +55,8 @@ | ||||
|     "framework": "test-4", | ||||
|     "webhooks": [], | ||||
|     "icon": "", | ||||
|     "recordId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "subDomain": "iglootools-2.snowball.xyz" | ||||
|   }, | ||||
|   { | ||||
| @ -62,6 +70,8 @@ | ||||
|     "framework": "test-5", | ||||
|     "webhooks": [], | ||||
|     "icon": "", | ||||
|     "recordId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", | ||||
|     "recordData": {}, | ||||
|     "subDomain": "snowball-2.snowball.xyz" | ||||
|   } | ||||
| ] | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| [ | ||||
|   { | ||||
|     "id": 1, | ||||
|     "title": "nextjs-boilerplate-9t44zbky4dg-bygideon-projects", | ||||
|     "status": "Building", | ||||
|     "isProduction": true, | ||||
|     "isCurrent": false, | ||||
| @ -15,7 +14,6 @@ | ||||
|   }, | ||||
|   { | ||||
|     "id": 2, | ||||
|     "title": "nextjs-boilerplate-9232dwky4dg-bygideon-projects", | ||||
|     "status": "Ready", | ||||
|     "isProduction": false, | ||||
|     "isCurrent": false, | ||||
| @ -29,7 +27,6 @@ | ||||
|   }, | ||||
|   { | ||||
|     "id": 3, | ||||
|     "title": "nextjs-boilerplate-9saa22y4dg-bygideon-projects", | ||||
|     "status": "Error", | ||||
|     "isProduction": false, | ||||
|     "isCurrent": false, | ||||
|  | ||||
| @ -26,6 +26,8 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => { | ||||
|       name: `${repository.owner!.login}-${repository.name}`, | ||||
|       prodBranch: repository.default_branch!, | ||||
|       repository: repository.full_name, | ||||
|       // TODO: Compute template from repo
 | ||||
|       template: 'webapp', | ||||
|     }); | ||||
| 
 | ||||
|     navigate(`import?projectId=${addProject.id}`); | ||||
|  | ||||
| @ -52,6 +52,8 @@ const CreateRepo = () => { | ||||
|           name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`, | ||||
|           prodBranch: gitRepo.data.default_branch ?? 'main', | ||||
|           repository: gitRepo.data.full_name, | ||||
|           // TODO: Set selected template
 | ||||
|           template: 'webapp', | ||||
|         }); | ||||
| 
 | ||||
|         navigate( | ||||
|  | ||||
| @ -42,7 +42,6 @@ query ($projectId: String!) { | ||||
|       branch | ||||
|       isCurrent | ||||
|       status | ||||
|       title | ||||
|       updatedAt | ||||
|       commitHash | ||||
|       createdAt | ||||
| @ -83,7 +82,6 @@ query ($organizationSlug: String!) { | ||||
|       branch | ||||
|       isCurrent | ||||
|       status | ||||
|       title | ||||
|       updatedAt | ||||
|       commitHash | ||||
|       createdAt | ||||
| @ -127,7 +125,6 @@ query ($projectId: String!)  { | ||||
|     } | ||||
|     branch | ||||
|     commitHash | ||||
|     title | ||||
|     url | ||||
|     environment | ||||
|     isCurrent | ||||
|  | ||||
| @ -62,7 +62,6 @@ export type Deployment = { | ||||
|   domain: Domain | ||||
|   branch: string | ||||
|   commitHash: string | ||||
|   title: string | ||||
|   url: string | ||||
|   environment: Environment | ||||
|   isCurrent: boolean | ||||
| @ -244,6 +243,7 @@ export type AddProjectInput = { | ||||
|   name: string; | ||||
|   repository: string; | ||||
|   prodBranch: string; | ||||
|   template?: string; | ||||
| } | ||||
| 
 | ||||
| export type UpdateProjectInput = { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user