@@ -38,7 +38,92 @@ export default async function handler(
3838 const event = req . headers [ "x-gitlab-event" ] as string ;
3939 const body = req . body ;
4040
41- if ( event === "Push Hook" || event === "Tag Push Hook" ) {
41+ if ( event === "Tag Push Hook" ) {
42+ try {
43+ const tagName = body ?. ref ?. replace ( "refs/tags/" , "" ) ;
44+ const deploymentHash = body ?. checkout_sha ;
45+ const pathNamespace = body ?. project ?. path_with_namespace ;
46+
47+ const apps = await db . query . applications . findMany ( {
48+ where : and (
49+ eq ( applications . sourceType , "gitlab" ) ,
50+ eq ( applications . autoDeploy , true ) ,
51+ eq ( applications . triggerType , "tag" ) ,
52+ eq ( applications . gitlabPathNamespace , pathNamespace ) ,
53+ eq ( applications . gitlabId , gitlabProvider . gitlabId ) ,
54+ ) ,
55+ } ) ;
56+
57+ for ( const app of apps ) {
58+ const jobData : DeploymentJob = {
59+ applicationId : app . applicationId as string ,
60+ titleLog : `Tag created: ${ tagName } ` ,
61+ descriptionLog : `Hash: ${ deploymentHash } ` ,
62+ type : "deploy" ,
63+ applicationType : "application" ,
64+ server : ! ! app . serverId ,
65+ } ;
66+
67+ if ( IS_CLOUD && app . serverId ) {
68+ jobData . serverId = app . serverId ;
69+ deploy ( jobData ) . catch ( ( error ) => {
70+ console . error ( "Background deployment failed:" , error ) ;
71+ } ) ;
72+ continue ;
73+ }
74+ await myQueue . add ( "deployments" , { ...jobData } , {
75+ removeOnComplete : true ,
76+ removeOnFail : true ,
77+ } ) ;
78+ }
79+
80+ const composeApps = await db . query . compose . findMany ( {
81+ where : and (
82+ eq ( compose . sourceType , "gitlab" ) ,
83+ eq ( compose . autoDeploy , true ) ,
84+ eq ( compose . triggerType , "tag" ) ,
85+ eq ( compose . gitlabPathNamespace , pathNamespace ) ,
86+ eq ( compose . gitlabId , gitlabProvider . gitlabId ) ,
87+ ) ,
88+ } ) ;
89+
90+ for ( const composeApp of composeApps ) {
91+ const jobData : DeploymentJob = {
92+ composeId : composeApp . composeId as string ,
93+ titleLog : `Tag created: ${ tagName } ` ,
94+ descriptionLog : `Hash: ${ deploymentHash } ` ,
95+ type : "deploy" ,
96+ applicationType : "compose" ,
97+ server : ! ! composeApp . serverId ,
98+ } ;
99+
100+ if ( IS_CLOUD && composeApp . serverId ) {
101+ jobData . serverId = composeApp . serverId ;
102+ deploy ( jobData ) . catch ( ( error ) => {
103+ console . error ( "Background deployment failed:" , error ) ;
104+ } ) ;
105+ continue ;
106+ }
107+ await myQueue . add ( "deployments" , { ...jobData } , {
108+ removeOnComplete : true ,
109+ removeOnFail : true ,
110+ } ) ;
111+ }
112+
113+ const totalApps = apps . length + composeApps . length ;
114+ res . status ( 200 ) . json ( {
115+ message :
116+ totalApps === 0
117+ ? "No apps configured to deploy on tag"
118+ : `Deployed ${ totalApps } apps based on tag ${ tagName } ` ,
119+ } ) ;
120+ } catch ( error ) {
121+ res . status ( 400 ) . json ( { message : "Error deploying application" , error } ) ;
122+ }
123+ return ;
124+ }
125+
126+ if ( event === "Push Hook" ) {
42127 try {
43128 const branchName = body ?. ref ?. replace ( "refs/heads/" , "" ) ;
44129 const deploymentHash = body ?. checkout_sha ;
@@ -55,6 +140,7 @@ export default async function handler(
55140 where : and (
56141 eq ( applications . sourceType , "gitlab" ) ,
57142 eq ( applications . autoDeploy , true ) ,
143+ eq ( applications . triggerType , "push" ) ,
58144 eq ( applications . gitlabPathNamespace , pathNamespace ) ,
59145 eq ( applications . gitlabBranch , branchName ) ,
60146 eq ( applications . gitlabId , gitlabProvider . gitlabId ) ,
@@ -92,6 +178,7 @@ export default async function handler(
92178 where : and (
93179 eq ( compose . sourceType , "gitlab" ) ,
94180 eq ( compose . autoDeploy , true ) ,
181+ eq ( compose . triggerType , "push" ) ,
95182 eq ( compose . gitlabPathNamespace , pathNamespace ) ,
96183 eq ( compose . gitlabBranch , branchName ) ,
97184 eq ( compose . gitlabId , gitlabProvider . gitlabId ) ,
@@ -126,7 +213,12 @@ export default async function handler(
126213 }
127214
128215 const totalApps = apps . length + composeApps . length ;
129- res . status ( 200 ) . json ( { message : totalApps === 0 ? "No apps to deploy" : `Deployed ${ totalApps } apps` } ) ;
216+ res . status ( 200 ) . json ( {
217+ message :
218+ totalApps === 0
219+ ? "No apps to deploy"
220+ : `Deployed ${ totalApps } apps` ,
221+ } ) ;
130222 } catch ( error ) {
131223 res . status ( 400 ) . json ( { message : "Error deploying application" , error } ) ;
132224 }
@@ -173,15 +265,8 @@ export default async function handler(
173265 action === "open" ||
174266 action === "update" ||
175267 action === "reopen" ||
176- action === "labeled" ||
177- action === "unlabeled"
268+ action === "labeled"
178269 ) {
179- const shouldCreateDeployment =
180- action === "open" ||
181- action === "update" ||
182- action === "reopen" ||
183- action === "labeled" ;
184-
185270 const targetBranch = body ?. object_attributes ?. target_branch as string ;
186271 const sourceBranch = body ?. object_attributes ?. source_branch as string ;
187272 const mrTitle = body ?. object_attributes ?. title as string ;
@@ -202,36 +287,46 @@ export default async function handler(
202287 } ,
203288 } ) ;
204289
205- // Security: check member permissions per app
290+ // Permission check is per-MR-author, not per-app — check once before the loop
291+ const requiresPermissionCheck = apps . some (
292+ ( app ) => app . previewRequireCollaboratorPermissions !== false ,
293+ ) ;
294+ let permissionResult : Awaited <
295+ ReturnType < typeof checkGitlabMemberPermissions >
296+ > | null = null ;
297+ let permissionError : unknown = null ;
298+
299+ if ( requiresPermissionCheck ) {
300+ try {
301+ permissionResult = await checkGitlabMemberPermissions (
302+ gitlabProvider . gitlabId ,
303+ projectId ,
304+ mrAuthor ,
305+ ) ;
306+ } catch ( error ) {
307+ permissionError = error ;
308+ console . error ( "Error validating MR author permissions:" , error ) ;
309+ }
310+ }
311+
206312 const secureApps : typeof apps = [ ] ;
207313 let blockedAccessLevel : number | null = null ;
208314 let blocked = false ;
209315
210316 for ( const app of apps ) {
211317 if ( app . previewRequireCollaboratorPermissions !== false ) {
212- try {
213- const { hasWriteAccess, accessLevel } =
214- await checkGitlabMemberPermissions (
215- gitlabProvider . gitlabId ,
216- projectId ,
217- mrAuthor ,
218- ) ;
219-
220- if ( ! hasWriteAccess ) {
221- console . warn (
222- `🚨 SECURITY: Blocked preview deployment for ${ app . name } from ${ mrAuthor } . Access level: ${ accessLevel } ` ,
223- ) ;
224- if ( ! blocked ) {
225- blockedAccessLevel = accessLevel ;
226- blocked = true ;
227- }
228- continue ;
229- }
230- } catch ( error ) {
231- console . error (
232- `Error validating MR author permissions for ${ app . name } :` ,
233- error ,
318+ if ( permissionError ) {
319+ continue ;
320+ }
321+ const { hasWriteAccess, accessLevel } = permissionResult ! ;
322+ if ( ! hasWriteAccess ) {
323+ console . warn (
324+ `🚨 SECURITY: Blocked preview deployment for ${ app . name } from ${ mrAuthor } . Access level: ${ accessLevel } ` ,
234325 ) ;
326+ if ( ! blocked ) {
327+ blockedAccessLevel = accessLevel ;
328+ blocked = true ;
329+ }
235330 continue ;
236331 }
237332 }
@@ -275,7 +370,7 @@ export default async function handler(
275370 let previewDeploymentId =
276371 existingDeployment ?. previewDeploymentId ?? "" ;
277372
278- if ( ! existingDeployment && shouldCreateDeployment ) {
373+ if ( ! existingDeployment ) {
279374 const newDeployment = await createPreviewDeployment ( {
280375 applicationId : app . applicationId as string ,
281376 branch : sourceBranch ,
0 commit comments