1
1
import type { NetlifyAPI } from 'netlify'
2
2
3
- import { $TSFixMe } from '../../commands/types.js'
4
3
import { logAndThrowError } from '../command-helpers.js'
5
4
import type { SiteInfo , EnvironmentVariableSource } from '../../utils/types.js'
6
5
7
- export const AVAILABLE_CONTEXTS = [ 'all' , 'production' , 'deploy-preview' , 'branch-deploy' , 'dev' ]
8
- export const AVAILABLE_SCOPES = [ 'builds' , 'functions' , 'runtime' , 'post_processing' ]
9
-
10
- type EnvironmentVariableContext = 'all' | 'production' | 'deploy-preview' | 'branch-deploy' | 'dev'
11
-
12
- type EnvironmentVariableScope = 'builds' | 'functions' | 'runtime' | 'post_processing'
6
+ /**
7
+ * Supported values for the user-provided env `context` option.
8
+ * These all match possible `context` values returned by the Envelope API.
9
+ * Note that a user may also specify a branch name with the special `branch:my-branch-name` format.
10
+ */
11
+ export const SUPPORTED_CONTEXTS = [ 'all' , 'production' , 'deploy-preview' , 'branch-deploy' , 'dev' ] as const
12
+ /**
13
+ * Additional aliases for the user-provided env `context` option.
14
+ */
15
+ const SUPPORTED_CONTEXT_ALIASES = {
16
+ dp : 'deploy-preview' ,
17
+ prod : 'production' ,
18
+ }
19
+ /**
20
+ * Supported values for the user-provided env `scope` option.
21
+ * These exactly match possible `scope` values returned by the Envelope API.
22
+ * Note that `any` is also supported.
23
+ */
24
+ export const ALL_ENVELOPE_SCOPES = [ 'builds' , 'functions' , 'runtime' , 'post_processing' ] as const
13
25
14
- type EnvironmentVariableValue = {
15
- context : EnvironmentVariableContext
26
+ // TODO(serhalp) Netlify API is incorrect - the returned scope is `post_processing`, not `post-processing`
27
+ type EnvelopeEnvVarScope =
28
+ | Exclude < NonNullable < Awaited < ReturnType < NetlifyAPI [ 'getEnvVars' ] > > [ number ] [ 'scopes' ] > [ number ] , 'post-processing' >
29
+ | 'post_processing'
30
+ type EnvelopeEnvVar = Awaited < ReturnType < NetlifyAPI [ 'getEnvVars' ] > > [ number ] & {
31
+ scopes : EnvelopeEnvVarScope [ ]
32
+ }
33
+ type EnvelopeEnvVarContext = NonNullable < NonNullable < EnvelopeEnvVar [ 'values' ] > [ number ] [ 'context' ] >
34
+ export type EnvelopeEnvVarValue = {
35
+ /**
36
+ * The deploy context of the this env var value
37
+ */
38
+ context ?: EnvelopeEnvVarContext
39
+ /**
40
+ * For parameterized contexts (i.e. only `branch`), context parameter (i.e. the branch name)
41
+ */
16
42
context_parameter ?: string | undefined
17
- value : string
43
+ /**
44
+ * The value of the environment variable for this context. Note that this appears to be an empty string
45
+ * when the env var is not set for this context.
46
+ */
47
+ value ?: string | undefined
48
+ }
49
+
50
+ export type EnvelopeItem = {
51
+ // FIXME(serhalp) Netlify API types claim this is optional. Investigate and fix here or there.
52
+ key : string
53
+ scopes : EnvelopeEnvVarScope [ ]
54
+ values : EnvelopeEnvVarValue [ ]
18
55
}
19
56
57
+ // AFAICT, Envelope uses only `post_processing` on returned env vars; the CLI documents and expects
58
+ // only `post-processing` as a valid user-provided scope; the code handles both everywhere. Consider
59
+ // explicitly normalizing and dropping undocumented support for user-provided `post_processing`.
60
+ type SupportedScope = EnvelopeEnvVarScope | 'post_processing' | 'any'
61
+
62
+ type ContextOrBranch = string
63
+
20
64
/**
21
- * @param context The deploy context or branch of the environment variable value
22
- * @returns The normalized context or branch name
65
+ * Normalizes a user-provided "context". Note that this may be the special `branch:my-branch-name` format.
66
+ *
67
+ * - If this is a supported alias of a context, it will be normalized to the canonical context.
68
+ * - Valid canonical contexts are returned as is.
69
+ * - If this starts with `branch:`, it will be normalized to the branch name.
70
+ *
71
+ * @param context A user-provided context, context alias, or a string in the `branch:my-branch-name` format.
72
+ *
73
+ * @returns The normalized context name or just the branch name
23
74
*/
24
- export const normalizeContext = ( context : string ) : string => {
75
+ export const normalizeContext = ( context : string ) : ContextOrBranch => {
25
76
if ( ! context ) {
26
77
return context
27
78
}
28
- const CONTEXT_SYNONYMS = {
29
- dp : 'deploy-preview' ,
30
- prod : 'production' ,
31
- }
79
+
32
80
context = context . toLowerCase ( )
33
- if ( context in CONTEXT_SYNONYMS ) {
34
- context = CONTEXT_SYNONYMS [ context as keyof typeof CONTEXT_SYNONYMS ]
81
+ if ( context in SUPPORTED_CONTEXT_ALIASES ) {
82
+ context = SUPPORTED_CONTEXT_ALIASES [ context as keyof typeof SUPPORTED_CONTEXT_ALIASES ]
35
83
}
36
- const forbiddenContexts = AVAILABLE_CONTEXTS . map ( ( ctx ) => `branch:${ ctx } ` )
84
+ const forbiddenContexts = SUPPORTED_CONTEXTS . map ( ( ctx ) => `branch:${ ctx } ` )
37
85
if ( forbiddenContexts . includes ( context ) ) {
38
86
return logAndThrowError ( `The context ${ context } includes a reserved keyword and is not allowed` )
39
87
}
40
88
return context . replace ( / ^ b r a n c h : / , '' )
41
89
}
42
90
43
91
/**
44
- * Finds a matching environment variable value from a given context
92
+ * Finds a matching environment variable value for a given context
93
+ * @private
45
94
*/
46
- export const findValueInValues = (
95
+ export const getValueForContext = (
47
96
/**
48
97
* An array of environment variable values from Envelope
49
98
*/
50
- values : EnvironmentVariableValue [ ] ,
99
+ values : EnvelopeEnvVarValue [ ] ,
51
100
/**
52
101
* The deploy context or branch of the environment variable value
53
102
*/
54
- context : string ,
55
- ) =>
56
- values . find ( ( val ) => {
57
- if ( ! AVAILABLE_CONTEXTS . includes ( context ) ) {
103
+ contextOrBranch : ContextOrBranch ,
104
+ ) : EnvelopeEnvVarValue | undefined => {
105
+ const valueForContext = values . find ( ( val ) => {
106
+ const isSupportedContext = ( SUPPORTED_CONTEXTS as readonly string [ ] ) . includes ( contextOrBranch )
107
+ if ( ! isSupportedContext ) {
58
108
// the "context" option passed in is actually the name of a branch
59
- return val . context === 'all' || val . context_parameter === context
109
+ return val . context === 'all' || val . context_parameter === contextOrBranch
60
110
}
61
- return [ context , 'all' ] . includes ( val . context )
111
+ return val . context === 'all' || val . context === contextOrBranch
62
112
} )
113
+ return valueForContext ?? undefined
114
+ }
63
115
64
116
/**
65
117
* Finds environment variables that match a given source
@@ -70,7 +122,6 @@ export const findValueInValues = (
70
122
export const filterEnvBySource = ( env : object , source : EnvironmentVariableSource ) : typeof env =>
71
123
Object . fromEntries ( Object . entries ( env ) . filter ( ( [ , variable ] ) => variable . sources [ 0 ] === source ) )
72
124
73
- // Fetches data from Envelope
74
125
const fetchEnvelopeItems = async function ( {
75
126
accountId,
76
127
api,
@@ -81,19 +132,21 @@ const fetchEnvelopeItems = async function ({
81
132
api : NetlifyAPI
82
133
key : string
83
134
siteId ?: string | undefined
84
- } ) : Promise < Awaited < ReturnType < NetlifyAPI [ 'getEnvVar' ] > > [ ] > {
135
+ } ) : Promise < EnvelopeItem [ ] > {
85
136
if ( accountId === undefined ) {
86
137
return [ ]
87
138
}
88
139
try {
89
140
// if a single key is passed, fetch that single env var
90
141
if ( key ) {
91
142
const envelopeItem = await api . getEnvVar ( { accountId, key, siteId } )
92
- return [ envelopeItem ]
143
+ // See FIXME(serhalp) above
144
+ return [ envelopeItem as EnvelopeItem ]
93
145
}
94
146
// otherwise, fetch the entire list of env vars
95
147
const envelopeItems = await api . getEnvVars ( { accountId, siteId } )
96
- return envelopeItems
148
+ // See FIXME(serhalp) above
149
+ return envelopeItems as EnvelopeItem [ ]
97
150
} catch {
98
151
// Collaborators aren't allowed to read shared env vars,
99
152
// so return an empty array silently in that case
@@ -130,38 +183,38 @@ export const formatEnvelopeData = ({
130
183
scope = 'any' ,
131
184
source,
132
185
} : {
133
- context ?: string
134
- envelopeItems : $TSFixMe [ ]
135
- scope ?: string
186
+ context ?: ContextOrBranch
187
+ envelopeItems : EnvelopeItem [ ]
188
+ scope ?: SupportedScope
136
189
source : string
137
190
} ) : Record <
138
191
string ,
139
192
{
140
- context : string
141
- branch : string
193
+ context : ContextOrBranch
194
+ branch : string | undefined
142
195
scopes : string [ ]
143
196
sources : string [ ]
144
197
value : string
145
198
}
146
199
> =>
147
200
envelopeItems
148
201
// filter by context
149
- . filter ( ( { values } ) => Boolean ( findValueInValues ( values , context ) ) )
202
+ . filter ( ( { values } ) => Boolean ( getValueForContext ( values , context ) ) )
150
203
// filter by scope
151
204
. filter ( ( { scopes } ) => ( scope === 'any' ? true : scopes . includes ( scope ) ) )
152
205
// sort alphabetically, case insensitive
153
206
. sort ( ( left , right ) => ( left . key . toLowerCase ( ) < right . key . toLowerCase ( ) ? - 1 : 1 ) )
154
207
// format the data
155
208
. reduce ( ( acc , cur ) => {
156
- const val = findValueInValues ( cur . values , context )
209
+ const val = getValueForContext ( cur . values , context )
157
210
if ( val === undefined ) {
158
211
throw new TypeError ( `failed to locate environment variable value for ${ context } context` )
159
212
}
160
- const { context : ctx , context_parameter : branch , value } = val
213
+ const { context : itemContext , context_parameter : branch , value } = val
161
214
return {
162
215
...acc ,
163
216
[ cur . key ] : {
164
- context : ctx ,
217
+ context : itemContext ,
165
218
branch,
166
219
scopes : cur . scopes ,
167
220
sources : [ source ] ,
@@ -191,11 +244,11 @@ export const getEnvelopeEnv = async ({
191
244
siteInfo,
192
245
} : {
193
246
api : NetlifyAPI
194
- context ?: string | undefined
247
+ context ?: ContextOrBranch | undefined
195
248
env : object
196
249
key ?: string | undefined
197
250
raw ?: boolean | undefined
198
- scope ?: string | undefined
251
+ scope ?: SupportedScope | undefined
199
252
siteInfo : SiteInfo
200
253
} ) => {
201
254
const { account_slug : accountId , id : siteId } = siteInfo
@@ -243,13 +296,15 @@ export const getEnvelopeEnv = async ({
243
296
* @param scopes An array of scopes
244
297
* @returns A human-readable, comma-separated list of scopes
245
298
*/
246
- export const getHumanReadableScopes = ( scopes ?: ( EnvironmentVariableScope | 'post-processing' ) [ ] ) : string => {
299
+ export const getHumanReadableScopes = ( scopes ?: EnvelopeEnvVarScope [ ] ) : string => {
247
300
const HUMAN_SCOPES = [ 'Builds' , 'Functions' , 'Runtime' , 'Post processing' ]
248
301
const SCOPES_MAP = {
249
302
builds : HUMAN_SCOPES [ 0 ] ,
250
303
functions : HUMAN_SCOPES [ 1 ] ,
251
304
runtime : HUMAN_SCOPES [ 2 ] ,
252
305
post_processing : HUMAN_SCOPES [ 3 ] ,
306
+ // TODO(serhalp) I believe this isn't needed, as `post-processing` is a user-provided
307
+ // CLI option, not a scope returned by the Envelope API.
253
308
'post-processing' : HUMAN_SCOPES [ 3 ] ,
254
309
}
255
310
if ( ! scopes ) {
@@ -272,10 +327,10 @@ export const getHumanReadableScopes = (scopes?: (EnvironmentVariableScope | 'pos
272
327
export const translateFromMongoToEnvelope = ( env : Record < string , string > = { } ) => {
273
328
const envVars = Object . entries ( env ) . map ( ( [ key , value ] ) => ( {
274
329
key,
275
- scopes : AVAILABLE_SCOPES ,
330
+ scopes : ALL_ENVELOPE_SCOPES ,
276
331
values : [
277
332
{
278
- context : 'all' ,
333
+ context : 'all' as const ,
279
334
value,
280
335
} ,
281
336
] ,
0 commit comments