@@ -95,7 +95,12 @@ async function createProject(
9595async function createDeployment (
9696 prisma : any ,
9797 project : { id : string ; organizationId : string } ,
98- options : { status ?: "DEPLOYING" | "DEPLOYED" ; imageReference ?: string ; templateKey ?: string } = { }
98+ options : {
99+ status ?: "DEPLOYING" | "DEPLOYED" ;
100+ imageReference ?: string ;
101+ templateKey ?: string ;
102+ deployedAt ?: Date ;
103+ } = { }
99104) {
100105 const id = randomUUID ( ) ;
101106 const environment = await prisma . runtimeEnvironment . create ( {
@@ -120,6 +125,7 @@ async function createDeployment(
120125 status : options . status ?? "DEPLOYING" ,
121126 imageReference : options . imageReference ?? IMAGE ,
122127 templateKey : options . templateKey ,
128+ deployedAt : options . deployedAt ,
123129 } ,
124130 } ) ;
125131}
@@ -667,3 +673,119 @@ describe("prebuild", () => {
667673 ) . resolves . toEqual ( { mode : "skip" } ) ;
668674 } ) ;
669675} ) ;
676+
677+ describe ( "requestBuilds" , ( ) => {
678+ const PINNED = `${ IMAGE } @${ DIGEST } ` ;
679+
680+ postgresTest (
681+ "builds deployments by id or friendly id and reports each outcome" ,
682+ async ( { prisma } ) => {
683+ const server = await builder ( reply ( 200 , { template_id : TEMPLATE_KEY } ) ) ;
684+ try {
685+ const project = await createProject ( prisma ) ;
686+ const byId = await createDeployment ( prisma , project , {
687+ status : "DEPLOYED" ,
688+ imageReference : PINNED ,
689+ } ) ;
690+ const old = await createDeployment ( prisma , project , {
691+ status : "DEPLOYED" ,
692+ imageReference : PINNED ,
693+ deployedAt : new Date ( "2025-01-01T00:00:00Z" ) ,
694+ } ) ;
695+ const keyed = await createDeployment ( prisma , project , {
696+ status : "DEPLOYED" ,
697+ imageReference : PINNED ,
698+ templateKey : "b" . repeat ( 64 ) ,
699+ } ) ;
700+ const unpinned = await createDeployment ( prisma , project , { status : "DEPLOYED" } ) ;
701+ const service = new TemplatePrebuildService ( prisma , config ( { url : server . url } ) ) ;
702+
703+ const results = await service . requestBuilds ( {
704+ deploymentIds : [ byId . id , old . friendlyId , keyed . id , unpinned . id , "deployment_missing" ] ,
705+ } ) ;
706+
707+ expect ( results ) . toEqual ( [
708+ { deploymentId : byId . id , friendlyId : byId . friendlyId , outcome : "built" } ,
709+ { deploymentId : old . friendlyId , friendlyId : old . friendlyId , outcome : "built" } ,
710+ {
711+ deploymentId : keyed . id ,
712+ friendlyId : keyed . friendlyId ,
713+ outcome : "skipped" ,
714+ reason : "has_key" ,
715+ } ,
716+ {
717+ deploymentId : unpinned . id ,
718+ friendlyId : unpinned . friendlyId ,
719+ outcome : "skipped" ,
720+ reason : "no_pinned_image" ,
721+ } ,
722+ { deploymentId : "deployment_missing" , outcome : "not_found" } ,
723+ ] ) ;
724+ expect ( server . requests ) . toHaveLength ( 2 ) ;
725+ expect ( JSON . parse ( server . requests [ 0 ] . body ) . image ) . toBe ( PINNED ) ;
726+ expect ( await storedTemplateKey ( prisma , byId . id ) ) . toBe ( TEMPLATE_KEY ) ;
727+ expect ( await storedTemplateKey ( prisma , old . id ) ) . toBe ( TEMPLATE_KEY ) ;
728+ expect ( await storedTemplateKey ( prisma , keyed . id ) ) . toBe ( "b" . repeat ( 64 ) ) ;
729+ } finally {
730+ await server . close ( ) ;
731+ }
732+ }
733+ ) ;
734+
735+ postgresTest ( "force rebuilds a deployment that has a key" , async ( { prisma } ) => {
736+ const server = await builder ( reply ( 200 , { template_id : TEMPLATE_KEY } ) ) ;
737+ try {
738+ const project = await createProject ( prisma ) ;
739+ const keyed = await createDeployment ( prisma , project , {
740+ status : "DEPLOYED" ,
741+ imageReference : PINNED ,
742+ templateKey : "b" . repeat ( 64 ) ,
743+ } ) ;
744+ const service = new TemplatePrebuildService ( prisma , config ( { url : server . url } ) ) ;
745+
746+ const [ result ] = await service . requestBuilds ( { deploymentIds : [ keyed . id ] , force : true } ) ;
747+
748+ expect ( result ) . toMatchObject ( { outcome : "built" } ) ;
749+ expect ( await storedTemplateKey ( prisma , keyed . id ) ) . toBe ( TEMPLATE_KEY ) ;
750+ } finally {
751+ await server . close ( ) ;
752+ }
753+ } ) ;
754+
755+ postgresTest ( "reports a 429 from the builder as rate_limited" , async ( { prisma } ) => {
756+ const server = await builder ( reply ( 429 , { error : "slow down" } ) ) ;
757+ try {
758+ const project = await createProject ( prisma ) ;
759+ const row = await createDeployment ( prisma , project , {
760+ status : "DEPLOYED" ,
761+ imageReference : PINNED ,
762+ } ) ;
763+ const service = new TemplatePrebuildService ( prisma , config ( { url : server . url } ) ) ;
764+
765+ const [ result ] = await service . requestBuilds ( { deploymentIds : [ row . id ] } ) ;
766+
767+ expect ( result ) . toMatchObject ( { outcome : "rate_limited" } ) ;
768+ expect ( await storedTemplateKey ( prisma , row . id ) ) . toBeNull ( ) ;
769+ } finally {
770+ await server . close ( ) ;
771+ }
772+ } ) ;
773+
774+ postgresTest ( "skips every deployment when no builder is configured" , async ( { prisma } ) => {
775+ const project = await createProject ( prisma ) ;
776+ const row = await createDeployment ( prisma , project , {
777+ status : "DEPLOYED" ,
778+ imageReference : PINNED ,
779+ } ) ;
780+ const service = new TemplatePrebuildService ( prisma , config ( { url : undefined } ) ) ;
781+
782+ await expect ( service . requestBuilds ( { deploymentIds : [ row . id ] } ) ) . resolves . toEqual ( [
783+ {
784+ deploymentId : row . id ,
785+ friendlyId : row . friendlyId ,
786+ outcome : "skipped" ,
787+ reason : "no_builder" ,
788+ } ,
789+ ] ) ;
790+ } ) ;
791+ } ) ;
0 commit comments