@@ -3,20 +3,21 @@ import * as clc from "colorette";
3
3
import { Options } from "../../../options" ;
4
4
import { logger } from "../../../logger" ;
5
5
import { reduceFlat } from "../../../functional" ;
6
+ import * as utils from "../../../utils" ;
6
7
import * as args from "../args" ;
7
8
import * as backend from "../backend" ;
8
- import * as containerCleaner from "../containerCleaner" ;
9
9
import * as planner from "./planner" ;
10
10
import * as fabricator from "./fabricator" ;
11
11
import * as reporter from "./reporter" ;
12
12
import * as executor from "./executor" ;
13
13
import * as prompts from "../prompts" ;
14
- import * as experiments from "../../../experiments" ;
15
14
import { getAppEngineLocation } from "../../../functionsConfig" ;
16
15
import { getFunctionLabel } from "../functionsDeployHelper" ;
17
16
import { FirebaseError } from "../../../error" ;
18
17
import { getProjectNumber } from "../../../getProjectNumber" ;
19
18
import { release as extRelease } from "../../extensions" ;
19
+ import * as artifactregistry from "../../../gcp/artifactregistry" ;
20
+ import * as artifacts from "../../../functions/artifacts" ;
20
21
21
22
/** Releases new versions of functions and extensions to prod. */
22
23
export async function release (
@@ -105,12 +106,8 @@ export async function release(
105
106
printTriggerUrls ( wantBackend ) ;
106
107
107
108
const haveEndpoints = backend . allEndpoints ( wantBackend ) ;
108
- const deletedEndpoints = Object . values ( plan )
109
- . map ( ( r ) => r . endpointsToDelete )
110
- . reduce ( reduceFlat , [ ] ) ;
111
- if ( experiments . isEnabled ( "automaticallydeletegcfartifacts" ) ) {
112
- await containerCleaner . cleanupBuildImages ( haveEndpoints , deletedEndpoints ) ;
113
- }
109
+
110
+ await checkArtifactCleanupPolicies ( options . projectId ! , haveEndpoints ) ;
114
111
115
112
const allErrors = summary . results . filter ( ( r ) => r . error ) . map ( ( r ) => r . error ) as Error [ ] ;
116
113
if ( allErrors . length ) {
@@ -144,3 +141,119 @@ export function printTriggerUrls(results: backend.Backend): void {
144
141
logger . info ( clc . bold ( "Function URL" ) , `(${ getFunctionLabel ( httpsFunc ) } ):` , httpsFunc . uri ) ;
145
142
}
146
143
}
144
+
145
+ /**
146
+ * Checks if artifact cleanup policies are set for the regions where functions are deployed
147
+ * and automatically sets up policies where needed.
148
+ *
149
+ * The policy is only set up when:
150
+ * 1. No cleanup policy exists yet
151
+ * 2. No other cleanup policies exist (beyond our own if we previously set one)
152
+ * 3. User has not explicitly opted out
153
+ */
154
+ async function checkArtifactCleanupPolicies (
155
+ projectId : string ,
156
+ endpoints : backend . Endpoint [ ] ,
157
+ ) : Promise < void > {
158
+ if ( endpoints . length === 0 ) {
159
+ return ;
160
+ }
161
+
162
+ const uniqueRegions = new Set < string > ( ) ;
163
+ for ( const endpoint of endpoints ) {
164
+ uniqueRegions . add ( endpoint . region ) ;
165
+ }
166
+
167
+ const regionResults = await Promise . all (
168
+ Array . from ( uniqueRegions ) . map ( async ( region ) => {
169
+ try {
170
+ const repoPath = artifacts . makeRepoPath ( projectId , region ) ;
171
+ const repository = await artifactregistry . getRepository ( repoPath ) ;
172
+ const existingPolicy = artifacts . findExistingPolicy ( repository ) ;
173
+ const hasPolicy = ! ! existingPolicy ;
174
+ const hasOptOut = artifacts . hasCleanupOptOut ( repository ) ;
175
+
176
+ // Check if there are any other cleanup policies beyond our own
177
+ const hasOtherPolicies =
178
+ repository . cleanupPolicies &&
179
+ Object . keys ( repository . cleanupPolicies ) . some (
180
+ ( key ) => key !== artifacts . CLEANUP_POLICY_ID ,
181
+ ) ;
182
+
183
+ return {
184
+ region,
185
+ repository,
186
+ hasPolicy,
187
+ hasOptOut,
188
+ hasOtherPolicies,
189
+ } ;
190
+ } catch ( err ) {
191
+ logger . debug ( `Failed to check artifact cleanup policy for region ${ region } :` , err ) ;
192
+ return {
193
+ region,
194
+ hasPolicy : false ,
195
+ hasOptOut : false ,
196
+ hasOtherPolicies : false ,
197
+ error : err ,
198
+ } ;
199
+ }
200
+ } ) ,
201
+ ) ;
202
+
203
+ const regionsWithErrors = regionResults
204
+ . filter ( ( result ) => result . error )
205
+ . map ( ( result ) => result . region ) ;
206
+
207
+ const regionsToSetup = regionResults . filter (
208
+ ( result ) => ! result . hasPolicy && ! result . hasOptOut && ! result . hasOtherPolicies && ! result . error ,
209
+ ) ;
210
+
211
+ const regionsNeedingWarning : string [ ] = [ ] ;
212
+
213
+ if ( regionsToSetup . length > 0 ) {
214
+ utils . logLabeledSuccess (
215
+ "functions" ,
216
+ `Configuring a cleanup policy for repositories in ${ regionsToSetup . join ( ", " ) } . ` +
217
+ `Images older than ${ artifacts . DEFAULT_CLEANUP_DAYS } days will be automatically deleted.` ,
218
+ ) ;
219
+ const setupResults = await Promise . all (
220
+ regionsToSetup . map ( async ( result ) => {
221
+ try {
222
+ logger . debug ( `Setting up artifact cleanup policy for region ${ result . region } ` ) ;
223
+ await artifacts . setCleanupPolicy ( result . repository , artifacts . DEFAULT_CLEANUP_DAYS ) ;
224
+ return { region : result . region , success : true } ;
225
+ } catch ( err ) {
226
+ logger . debug (
227
+ `Failed to set up artifact cleanup policy for region ${ result . region } :` ,
228
+ err ,
229
+ ) ;
230
+ regionsNeedingWarning . push ( result . region ) ;
231
+ return { region : result . region , success : false , error : err } ;
232
+ }
233
+ } ) ,
234
+ ) ;
235
+
236
+ const failedSetups = setupResults . filter ( ( r ) => ! r . success ) ;
237
+ if ( failedSetups . length > 0 ) {
238
+ logger . debug (
239
+ `Failed to set up artifact cleanup policies for ${ failedSetups . length } regions:` ,
240
+ failedSetups . map ( ( f ) => f . region ) . join ( ", " ) ,
241
+ ) ;
242
+ }
243
+ }
244
+
245
+ const regionsToWarn = [ ...regionsNeedingWarning , ...regionsWithErrors ] ;
246
+
247
+ if ( regionsToWarn . length > 0 ) {
248
+ utils . logLabeledWarning (
249
+ "functions" ,
250
+ `No cleanup policy detected for repositories in ${ regionsToWarn . length > 1 ? "regions" : "region" } ` +
251
+ `${ regionsToWarn . join ( ", " ) } . ` +
252
+ "This could result in a small monthly bill as container images accumulate over time." ,
253
+ ) ;
254
+ utils . logLabeledBullet (
255
+ "functions" ,
256
+ "Run 'firebase functions:artifacts:setpolicy' to set up a cleanup policy to automatically delete old images." ,
257
+ ) ;
258
+ }
259
+ }
0 commit comments