@@ -5,8 +5,11 @@ import * as deploymentTool from "../../deploymentTool";
55import { Context } from "./context" ;
66import { Options } from "../../options" ;
77import { HostingOptions } from "../../hosting/options" ;
8- import { zipIn } from "../../functional" ;
8+ import { assertExhaustive , zipIn } from "../../functional" ;
99import { track } from "../../track" ;
10+ import * as utils from "../../utils" ;
11+ import { HostingSource } from "../../firebaseConfig" ;
12+ import * as backend from "../functions/backend" ;
1013
1114/**
1215 * Prepare creates versions for each Hosting site to be deployed.
@@ -34,6 +37,12 @@ export async function prepare(context: Context, options: HostingOptions & Option
3437 if ( config . webFramework ) {
3538 labels [ "firebase-web-framework" ] = config . webFramework ;
3639 }
40+ const unsafe = await unsafePins ( context , config ) ;
41+ if ( unsafe . length ) {
42+ const msg = `Cannot deploy site ${ config . site } to channel ${ context . hostingChannel } because it would modify one or more rewrites in "live" that are not pinned, breaking production. Please pin "live" before pinning other channels.` ;
43+ utils . logLabeledError ( "Hosting" , msg ) ;
44+ throw new Error ( msg ) ;
45+ }
3746 const version : Omit < api . Version , api . VERSION_OUTPUT_FIELDS > = {
3847 status : "CREATED" ,
3948 labels,
@@ -52,3 +61,73 @@ export async function prepare(context: Context, options: HostingOptions & Option
5261 context . hosting . deploys . push ( { config, version } ) ;
5362 }
5463}
64+
65+ function rewriteTarget ( source : HostingSource ) : string {
66+ if ( "glob" in source ) {
67+ return source . glob ;
68+ } else if ( "source" in source ) {
69+ return source . source ;
70+ } else if ( "regex" in source ) {
71+ return source . regex ;
72+ } else {
73+ assertExhaustive ( source ) ;
74+ }
75+ }
76+
77+ /**
78+ * Returns a list of rewrite targets that would break in prod if deployed.
79+ * People use tag pinning so that they can deploy to preview channels without
80+ * modifying production. This assumption is violated if the live channel isn't
81+ * actually pinned. This method returns "unsafe" pins, where we're deploying to
82+ * a non-live channel with a rewrite that is pinned but haven't yet pinned live.
83+ */
84+ export async function unsafePins (
85+ context : Context ,
86+ config : config . HostingResolved
87+ ) : Promise < string [ ] > {
88+ // Overwriting prod won't break prod
89+ if ( ( context . hostingChannel || "live" ) === "live" ) {
90+ return [ ] ;
91+ }
92+
93+ const targetTaggedRewrites : Record < string , string > = { } ;
94+ for ( const rewrite of config . rewrites || [ ] ) {
95+ const target = rewriteTarget ( rewrite ) ;
96+ if ( "run" in rewrite && rewrite . run . pinTag ) {
97+ targetTaggedRewrites [ target ] = `${ rewrite . run . region || "us-central1" } /${
98+ rewrite . run . serviceId
99+ } `;
100+ }
101+ if ( "function" in rewrite && typeof rewrite . function === "object" && rewrite . function . pinTag ) {
102+ const region = rewrite . function . region || "us-central1" ;
103+ const endpoint = ( await backend . existingBackend ( context ) ) . endpoints [ region ] ?. [
104+ rewrite . function . functionId
105+ ] ;
106+ // This function is new. It can't be pinned elsewhere
107+ if ( ! endpoint ) {
108+ continue ;
109+ }
110+ targetTaggedRewrites [ target ] = `${ region } /${ endpoint . runServiceId || endpoint . id } ` ;
111+ }
112+ }
113+
114+ if ( ! Object . keys ( targetTaggedRewrites ) . length ) {
115+ return [ ] ;
116+ }
117+
118+ const channelConfig = await api . getChannel ( context . projectId , config . site , "live" ) ;
119+ const existingUntaggedRewrites : Record < string , string > = { } ;
120+ for ( const rewrite of channelConfig ?. release ?. version ?. config ?. rewrites || [ ] ) {
121+ if ( "run" in rewrite && ! rewrite . run . tag ) {
122+ existingUntaggedRewrites [
123+ rewriteTarget ( rewrite )
124+ ] = `${ rewrite . run . region } /${ rewrite . run . serviceId } ` ;
125+ }
126+ }
127+
128+ // There is only a problem if we're targeting the same exact run service but
129+ // live isn't tagged.
130+ return Object . keys ( targetTaggedRewrites ) . filter (
131+ ( target ) => targetTaggedRewrites [ target ] === existingUntaggedRewrites [ target ]
132+ ) ;
133+ }
0 commit comments