netlify-cli 27.12.1 → 27.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,8 +38,8 @@ export const agentsStop = async (id, options, command) => {
38
38
  }
39
39
  // Stop the agent task
40
40
  const stopSpinnerInstance = startSpinner({ text: 'Stopping agent task...' });
41
- const response = await fetch(`${apiOpts.scheme ?? 'https'}://${apiOpts.host ?? api.host}/api/v1/agent_runners/${id}`, {
42
- method: 'DELETE',
41
+ const response = await fetch(`${apiOpts.scheme ?? 'https'}://${apiOpts.host ?? api.host}/api/v1/agent_runners/${id}/stop`, {
42
+ method: 'POST',
43
43
  headers: {
44
44
  Authorization: `Bearer ${api.accessToken ?? ''}`,
45
45
  'User-Agent': apiOpts.userAgent,
@@ -234,6 +234,8 @@ export default class BaseCommand extends Command {
234
234
  const getCommands = (command) => {
235
235
  const parentCommand = this.name() === 'netlify' ? command : command.parent;
236
236
  return (parentCommand?.commands
237
+ // subcommands nested with `.command()` rather than named `parent:child`, e.g. `database connect`
238
+ .concat(this.name() === 'netlify' ? [] : command.commands)
237
239
  .filter((cmd) => {
238
240
  if (cmd._hidden)
239
241
  return false;
@@ -242,11 +244,13 @@ export default class BaseCommand extends Command {
242
244
  // don't include subcommands on the main page
243
245
  return !cmd.name().includes(':');
244
246
  }
245
- return cmd.name().startsWith(`${command.name()}:`);
247
+ return cmd.name().startsWith(`${command.name()}:`) || cmd.parent === command;
246
248
  })
247
249
  .sort((a, b) => a.name().localeCompare(b.name())) || []);
248
250
  };
249
- help.longestSubcommandTermLength = (command) => getCommands(command).reduce((max, cmd) => Math.max(max, cmd.name().length), 0);
251
+ /** nested subcommands are shown by their full path, e.g. `database migrations apply` */
252
+ const getCommandTerm = (cmd) => cmd.parent && cmd.parent.name() !== 'netlify' ? `${getCommandTerm(cmd.parent)} ${cmd.name()}` : cmd.name();
253
+ help.longestSubcommandTermLength = (command) => getCommands(command).reduce((max, cmd) => Math.max(max, getCommandTerm(cmd).length), 0);
250
254
  /** override the longestOptionTermLength to react on hide options flag */
251
255
  help.longestOptionTermLength = (command, helper) => (!command.noBaseOptions &&
252
256
  helper.visibleOptions(command).reduce((max, option) => Math.max(max, helper.optionTerm(option).length), 0)) ||
@@ -313,7 +317,7 @@ export default class BaseCommand extends Command {
313
317
  '',
314
318
  ];
315
319
  }
316
- const commandList = getCommands(command).map((cmd) => formatItem(cmd.name(), helper.subcommandDescription(cmd).split('\n')[0], true));
320
+ const commandList = getCommands(command).map((cmd) => formatItem(getCommandTerm(cmd), helper.subcommandDescription(cmd).split('\n')[0], true));
317
321
  if (commandList.length !== 0) {
318
322
  output = [...output, chalk.bold('COMMANDS'), formatHelpList(commandList), ''];
319
323
  }
@@ -72,7 +72,7 @@ const getDeployFolder = async ({ command, config, options, site, siteData, }) =>
72
72
  ? resolve(command.jsWorkspaceRoot || site.root, options.dir)
73
73
  : resolve(command.workingDir, options.dir);
74
74
  }
75
- else if (config?.build?.publish) {
75
+ else if (config.build.publish) {
76
76
  deployFolder = resolve(site.root, config.build.publish);
77
77
  }
78
78
  else if (siteData?.build_settings?.dir) {
@@ -172,9 +172,6 @@ const getDeployFilesFilter = ({ deployFolder, site }) => {
172
172
  // when site.root !== deployFolder the behaviour matches our buildbot
173
173
  const skipNodeModules = site.root === deployFolder;
174
174
  return (filename) => {
175
- if (filename == null) {
176
- return false;
177
- }
178
175
  if (filename === deployFolder) {
179
176
  return true;
180
177
  }
@@ -252,10 +249,7 @@ const generateDeployCommand = (options, availableTeams, command) => {
252
249
  return parts.join(' ');
253
250
  };
254
251
  const prepareProductionDeploy = async ({ api, siteData, options, command, }) => {
255
- if (typeof siteData.published_deploy === 'object' &&
256
- // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- FIXME: a non-object `published_deploy` must not count as locked
257
- siteData.published_deploy !== null &&
258
- siteData.published_deploy.locked) {
252
+ if (siteData.published_deploy?.locked) {
259
253
  log(`\n${NETLIFYDEVERR} Deployments are "locked" for production context of this project\n`);
260
254
  const overrideCommand = generateDeployCommand({ ...options, prodIfUnlocked: true, prod: false }, [], command);
261
255
  if (!isInteractive()) {
@@ -275,6 +269,19 @@ const prepareProductionDeploy = async ({ api, siteData, options, command, }) =>
275
269
  log(`\n${NETLIFYDEVLOG} "Auto publishing" has been enabled for production context\n`);
276
270
  }
277
271
  };
272
+ const createDeploy = async ({ alias, api, command, deployToProduction, options, siteData, siteId, }) => {
273
+ if (deployToProduction) {
274
+ await prepareProductionDeploy({ siteData, api, options, command });
275
+ }
276
+ const draft = options.draft || (!deployToProduction && !alias);
277
+ const createDeployBody = {
278
+ draft,
279
+ branch: alias,
280
+ include_upload_url: options.uploadSourceZip,
281
+ ...getDeploySourceFields(),
282
+ };
283
+ return (await api.createSiteDeploy({ siteId, title: options.message, body: createDeployBody }));
284
+ };
278
285
  const hasErrorMessage = (actual, expected) => {
279
286
  if (typeof actual === 'string') {
280
287
  return actual.includes(expected);
@@ -372,7 +379,7 @@ const uploadDeployBlobs = async ({ cachedConfig, deployId, options, packagePath,
372
379
  phase: 'stop',
373
380
  });
374
381
  };
375
- const runDeploy = async ({ alias, api, command, config, deployFolder, deployTimeout, deployToProduction, functionsConfig, functionsFolder, options, packagePath, silent, site, siteData, siteId, skipFunctionsCache, title, deployId: existingDeployId, }) => {
382
+ const runDeploy = async ({ alias, api, command, config, deployFolder, deployTimeout, deployToProduction, functionsConfig, functionsFolder, options, packagePath, silent, site, siteData, siteId, skipFunctionsCache, deployId: existingDeployId, }) => {
376
383
  let results;
377
384
  let deployId = existingDeployId;
378
385
  let uploadSourceZipResult;
@@ -380,21 +387,15 @@ const runDeploy = async ({ alias, api, command, config, deployFolder, deployTime
380
387
  // We won't have a deploy ID if we run the command with `--no-build`.
381
388
  // In this case, we must create the deploy.
382
389
  if (!deployId) {
383
- if (deployToProduction) {
384
- await prepareProductionDeploy({ siteData, api, options, command });
385
- }
386
- const draft = options.draft || (!deployToProduction && !alias);
387
- const createDeployBody = {
388
- draft,
389
- branch: alias,
390
- include_upload_url: options.uploadSourceZip,
391
- ...getDeploySourceFields(),
392
- };
393
- const createDeployResponse = (await api.createSiteDeploy({
390
+ const createDeployResponse = await createDeploy({
391
+ alias,
392
+ api,
393
+ command,
394
+ deployToProduction,
395
+ options,
396
+ siteData,
394
397
  siteId,
395
- title,
396
- body: createDeployBody,
397
- }));
398
+ });
398
399
  deployId = createDeployResponse.id;
399
400
  if (options.uploadSourceZip &&
400
401
  createDeployResponse.source_zip_upload_url &&
@@ -458,7 +459,7 @@ const runDeploy = async ({ alias, api, command, config, deployFolder, deployTime
458
459
  workingDir: command.workingDir,
459
460
  manifestPath: manifestPath ?? undefined,
460
461
  packagePath,
461
- serverEnabled: Boolean(siteData?.feature_flags?.netlify_build_server_standalone),
462
+ serverEnabled: Boolean(siteData.feature_flags?.netlify_build_server_standalone),
462
463
  serverManifestPath: serverManifestPath ?? undefined,
463
464
  skipFunctionsCache,
464
465
  siteRoot: site.root,
@@ -708,7 +709,6 @@ const prepAndRunDeploy = async ({ api, command, config, deployToProduction, opti
708
709
  siteData,
709
710
  siteId,
710
711
  skipFunctionsCache: options.skipFunctionsCache,
711
- title: options.message,
712
712
  deployId,
713
713
  });
714
714
  return results;
@@ -831,7 +831,7 @@ const anonymousDeploy = async (options, command) => {
831
831
  }
832
832
  };
833
833
  const checkForFunctions = async () => {
834
- const functionsFolder = getFunctionsFolder({ config, options, site, siteData: {}, workingDir });
834
+ const functionsFolder = getFunctionsFolder({ config, options, site, workingDir });
835
835
  const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root });
836
836
  const frameworksFunctionsDir = command.netlify.frameworksAPIPaths.functions.path;
837
837
  const hasFunctions = (await dirHasFiles(functionsFolder)) ||
@@ -861,10 +861,9 @@ const anonymousDeploy = async (options, command) => {
861
861
  }
862
862
  const deployFolder = await getDeployFolder({
863
863
  command,
864
- config: command.netlify.config,
864
+ config,
865
865
  options,
866
866
  site,
867
- siteData: {},
868
867
  });
869
868
  await validateDeployFolder(deployFolder);
870
869
  const edgeFunctionsDistPath = await getEdgeFunctionsDistPathIfExists(workingDir);
@@ -983,22 +982,8 @@ export const deploy = async (options, command) => {
983
982
  const deployToProduction = !options.draft && (options.prod || (options.prodIfUnlocked && !(siteData.published_deploy?.locked ?? false)));
984
983
  let results = {};
985
984
  if (options.build) {
986
- if (deployToProduction) {
987
- await prepareProductionDeploy({ siteData, api, options, command });
988
- }
989
- const draft = options.draft || (!deployToProduction && !alias);
990
- const createDeployBody = {
991
- draft,
992
- branch: alias,
993
- include_upload_url: options.uploadSourceZip,
994
- ...getDeploySourceFields(),
995
- };
996
- const deployMetadata = (await api.createSiteDeploy({
997
- siteId,
998
- title: options.message,
999
- body: createDeployBody,
1000
- }));
1001
- const deployId = deployMetadata.id || '';
985
+ const deployMetadata = await createDeploy({ alias, api, command, deployToProduction, options, siteData, siteId });
986
+ const deployId = deployMetadata.id;
1002
987
  const skewProtectionToken = deployMetadata.skew_protection_token;
1003
988
  let sourceZipFileName;
1004
989
  if (options.uploadSourceZip &&
@@ -5,7 +5,7 @@ import { chalk, logAndThrowError, log, logJson, warn } from '../../utils/command
5
5
  import getRepoData from '../../utils/get-repo-data.js';
6
6
  import { configureRepo } from '../../utils/init/config.js';
7
7
  import { isInteractive } from '../../utils/scripted-commands.js';
8
- import { resolveTeamForNonInteractive } from '../../utils/team.js';
8
+ import { requireTeams, resolveTeamForNonInteractive } from '../../utils/team.js';
9
9
  import { track } from '../../utils/telemetry/index.js';
10
10
  import { MAX_SITE_NAME_LENGTH } from '../../utils/validation.js';
11
11
  import { link } from '../link/link.js';
@@ -29,9 +29,10 @@ export const sitesCreate = async (options, command) => {
29
29
  log(`Using team: ${team.name}`);
30
30
  }
31
31
  else {
32
+ const teams = requireTeams(accounts);
32
33
  accountSlug = await select({
33
34
  message: 'Team:',
34
- choices: accounts.map((account) => ({
35
+ choices: teams.map((account) => ({
35
36
  value: account.slug,
36
37
  name: account.name,
37
38
  })),
@@ -7,7 +7,6 @@ import type { DeployConfig } from './types.js';
7
7
  import type { DeployEnvironmentVariable } from '../env/deploy-env-vars.js';
8
8
  export type { DeployEvent };
9
9
  export interface DeploySiteOptions {
10
- assetType?: 'file' | undefined;
11
10
  branch?: string;
12
11
  concurrentHash?: number;
13
12
  concurrentUpload?: number;
@@ -33,7 +32,7 @@ export interface DeploySiteOptions {
33
32
  tmpDir?: string;
34
33
  workingDir: string;
35
34
  }
36
- export declare const deploySite: (command: BaseCommand, api: NetlifyAPI, siteId: string, dir: string, { assetType, branch, concurrentHash, concurrentUpload, config, deployId, deployTimeout, draft, environment, filter, fnDir, functionsConfig, hashAlgorithm, manifestPath, maxRetry, packagePath, serverEnabled, serverManifestPath, siteRoot, skipFunctionsCache, statusCb, syncFileLimit, tmpDir, workingDir, }: DeploySiteOptions) => Promise<{
35
+ export declare const deploySite: (command: BaseCommand, api: NetlifyAPI, siteId: string, dir: string, { branch, concurrentHash, concurrentUpload, config, deployId, deployTimeout, draft, environment, filter, fnDir, functionsConfig, hashAlgorithm, manifestPath, maxRetry, packagePath, serverEnabled, serverManifestPath, siteRoot, skipFunctionsCache, statusCb, syncFileLimit, tmpDir, workingDir, }: DeploySiteOptions) => Promise<{
37
36
  deployId: string;
38
37
  deploy: import("./util.js").ReadyDeploy;
39
38
  uploadList: (import("./upload-files.js").EdgeFunctionUploadFile | import("./upload-files.js").ServerUploadFile | File | (import("./upload-files.js").FunctionUploadFile & {
@@ -18,7 +18,7 @@ const buildStatsString = (possibleParts) => {
18
18
  }
19
19
  return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;
20
20
  };
21
- export const deploySite = async (command, api, siteId, dir, { assetType, branch, concurrentHash = DEFAULT_CONCURRENT_HASH, concurrentUpload = DEFAULT_CONCURRENT_UPLOAD, config, deployId, deployTimeout = DEFAULT_DEPLOY_TIMEOUT, draft = false, environment, filter, fnDir = [], functionsConfig, hashAlgorithm, manifestPath, maxRetry = DEFAULT_MAX_RETRY, packagePath, serverEnabled, serverManifestPath, siteRoot, skipFunctionsCache, statusCb = () => {
21
+ export const deploySite = async (command, api, siteId, dir, { branch, concurrentHash = DEFAULT_CONCURRENT_HASH, concurrentUpload = DEFAULT_CONCURRENT_UPLOAD, config, deployId, deployTimeout = DEFAULT_DEPLOY_TIMEOUT, draft = false, environment, filter, fnDir = [], functionsConfig, hashAlgorithm, manifestPath, maxRetry = DEFAULT_MAX_RETRY, packagePath, serverEnabled, serverManifestPath, siteRoot, skipFunctionsCache, statusCb = () => {
22
22
  /* default to noop */
23
23
  }, syncFileLimit = DEFAULT_SYNC_LIMIT, tmpDir = temporaryDirectory(), workingDir, }) => {
24
24
  statusCb({
@@ -31,7 +31,6 @@ export const deploySite = async (command, api, siteId, dir, { assetType, branch,
31
31
  const dbMigrationsDistPath = await getDbMigrationsDistPathIfExists(workingDir);
32
32
  const [{ files: staticFiles, filesShaMap: staticShaMap }, { fnConfig, fnShaMap, functionSchedules, functions, functionsWithNativeModules, server, serverShaMap }, configFile, { edgeFunctions, edgeFnShaMap },] = await Promise.all([
33
33
  hashFiles({
34
- assetType,
35
34
  concurrentHash,
36
35
  directories: [dir, edgeFunctionsDistPath, deployConfigPath, dbMigrationsDistPath].filter((d) => Boolean(d)),
37
36
  filter,
@@ -47,6 +46,7 @@ export const deploySite = async (command, api, siteId, dir, { assetType, branch,
47
46
  statusCb,
48
47
  manifestPath,
49
48
  packagePath,
49
+ serverConfig: config.server,
50
50
  serverEnabled,
51
51
  serverManifestPath,
52
52
  skipFunctionsCache,
@@ -62,37 +62,29 @@ export const uploadDropFile = async ({ apiBase, userAgent }, deployId, filePath,
62
62
  }
63
63
  };
64
64
  // TODO: Migrate to @netlify/api when Drop endpoints are in the OpenAPI spec.
65
- export const waitForDropDeploy = async ({ apiBase, userAgent }, siteId, deployId, timeout = DEFAULT_DEPLOY_TIMEOUT) => {
66
- let deploy;
67
- const checkDeploy = async () => {
68
- const response = await fetch(`${apiBase}/sites/${siteId}/deploys/${deployId}`, {
69
- headers: makeHeaders(userAgent),
70
- });
71
- if (!response.ok) {
72
- return false;
73
- }
74
- const data = (await response.json());
75
- if (data.state === 'ready') {
76
- deploy = data;
77
- return true;
78
- }
79
- if (data.state === 'error') {
80
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- an empty error message falls back to the generic one
81
- throw new Error(data.error_message || `Deploy ${deployId} had an error`);
82
- }
83
- return false;
84
- };
85
- await pWaitFor(checkDeploy, {
86
- interval: DEPLOY_POLL,
87
- timeout: {
88
- milliseconds: timeout,
89
- message: 'Timeout while waiting for deploy',
90
- },
65
+ export const waitForDropDeploy = async ({ apiBase, userAgent }, siteId, deployId, timeout = DEFAULT_DEPLOY_TIMEOUT) => await pWaitFor(async () => {
66
+ const response = await fetch(`${apiBase}/sites/${siteId}/deploys/${deployId}`, {
67
+ headers: makeHeaders(userAgent),
91
68
  });
92
- // deploy is guaranteed to be set when pWaitFor resolves
93
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
94
- return deploy;
95
- };
69
+ if (!response.ok) {
70
+ return false;
71
+ }
72
+ const data = (await response.json());
73
+ if (data.state === 'ready') {
74
+ return pWaitFor.resolveWith(data);
75
+ }
76
+ if (data.state === 'error') {
77
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- an empty error message falls back to the generic one
78
+ throw new Error(data.error_message || `Deploy ${deployId} had an error`);
79
+ }
80
+ return false;
81
+ }, {
82
+ interval: DEPLOY_POLL,
83
+ timeout: {
84
+ milliseconds: timeout,
85
+ message: 'Timeout while waiting for deploy',
86
+ },
87
+ });
96
88
  // TODO: Migrate to @netlify/api when Drop endpoints are in the OpenAPI spec.
97
89
  export const claimDropSite = async ({ apiBase, userAgent }, siteId, dropToken, authToken) => {
98
90
  const response = await fetch(`${apiBase}/drop/claim`, {
@@ -123,7 +115,6 @@ export const uploadDropFiles = async (apiOptions, deployId, uploadList, token, {
123
115
  msg: `(${String(index + 1)}/${String(uploadList.length)}) Uploading ${fileObj.normalizedPath}...`,
124
116
  phase: 'progress',
125
117
  });
126
- let lastError;
127
118
  for (let attempt = 0; attempt <= maxRetry; attempt++) {
128
119
  try {
129
120
  const body = fs.createReadStream(fileObj.filepath);
@@ -131,18 +122,13 @@ export const uploadDropFiles = async (apiOptions, deployId, uploadList, token, {
131
122
  return;
132
123
  }
133
124
  catch (error) {
134
- lastError = error;
135
- if (lastError.status === 400 || lastError.status === 422) {
125
+ const isPermanentFailure = error instanceof Error && 'status' in error && (error.status === 400 || error.status === 422);
126
+ if (isPermanentFailure || attempt + 1 > maxRetry) {
136
127
  throw error;
137
128
  }
138
- if (attempt < maxRetry) {
139
- await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
140
- }
129
+ await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
141
130
  }
142
131
  }
143
- if (lastError) {
144
- throw lastError;
145
- }
146
132
  };
147
133
  await pMap(uploadList, uploadFile, { concurrency: concurrentUpload });
148
134
  statusCb({
@@ -1,7 +1,7 @@
1
1
  import type { DeployConfig } from './types.js';
2
2
  import type { InlineUploadFile } from './upload-files.js';
3
3
  export declare const hashConfig: ({ config }: {
4
- config: DeployConfig | undefined;
4
+ config: DeployConfig;
5
5
  }) => InlineUploadFile & {
6
6
  hash: string;
7
7
  };
@@ -1,8 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import tomlify from 'tomlify-j0.4';
3
3
  export const hashConfig = ({ config }) => {
4
- if (!config)
5
- throw new Error('Missing config option');
6
4
  const configString = serializeToml(config);
7
5
  const hash = createHash('sha1').update(configString).digest('hex');
8
6
  return {
@@ -1,10 +1,9 @@
1
1
  import type { File } from './file.js';
2
2
  import type { StatusCallback } from './status-cb.js';
3
- declare const hashFiles: ({ assetType, concurrentHash, directories, filter, hashAlgorithm, normalizer, statusCb, }: {
4
- assetType?: 'file' | undefined;
3
+ declare const hashFiles: ({ concurrentHash, directories, filter, hashAlgorithm, normalizer, statusCb, }: {
5
4
  concurrentHash: number;
6
5
  directories: string[];
7
- filter: ((filename: string) => boolean) | undefined;
6
+ filter: (filename: string) => boolean;
8
7
  hashAlgorithm?: string | undefined;
9
8
  normalizer?: (file: File) => File;
10
9
  statusCb: StatusCallback;
@@ -1,13 +1,11 @@
1
1
  import { pipeline } from 'stream/promises';
2
2
  import walker from 'folder-walker';
3
3
  import { fileFilterCtor, fileNormalizerCtor, hasherCtor, manifestCollectorCtor } from './hasher-segments.js';
4
- const hashFiles = async ({ assetType = 'file', concurrentHash, directories, filter, hashAlgorithm = 'sha1', normalizer, statusCb, }) => {
5
- if (!filter)
6
- throw new Error('Missing filter function option');
4
+ const hashFiles = async ({ concurrentHash, directories, filter, hashAlgorithm = 'sha1', normalizer, statusCb, }) => {
7
5
  const fileStream = walker(directories, { filter });
8
6
  const fileFilter = fileFilterCtor();
9
7
  const hasher = hasherCtor({ concurrentHash, hashAlgorithm });
10
- const fileNormalizer = fileNormalizerCtor({ assetType, normalizer });
8
+ const fileNormalizer = fileNormalizerCtor({ normalizer });
11
9
  // Written to by manifestCollector
12
10
  // normalizedPath: hash (wanted by deploy API)
13
11
  const files = {};
@@ -1,53 +1,62 @@
1
- import { type Config as FunctionsConfig, type FunctionResult, type TrafficRules } from '@netlify/zip-it-and-ship-it';
1
+ import { type Config as FunctionsConfig, type FunctionResult, type NetlifyServerConfig, type ServerResult } from '@netlify/zip-it-and-ship-it';
2
2
  import type BaseCommand from '../../commands/base-command.js';
3
3
  import type { StatusCallback } from './status-cb.js';
4
4
  import type { FunctionUploadFile, ServerUploadFile } from './upload-files.js';
5
+ interface ServerPayload {
6
+ sha: string;
7
+ region?: string;
8
+ instance_size?: ServerResult['instanceSize'];
9
+ routes?: ServerResult['routes'];
10
+ excluded_routes?: ServerResult['excludedRoutes'];
11
+ runtime_version?: ServerResult['runtimeVersion'];
12
+ }
5
13
  type HashedFunctionFile = FunctionUploadFile & {
6
14
  hash: string;
7
15
  relname: string;
8
16
  };
9
- declare const trafficRulesConfig: (trafficRules?: TrafficRules) => {
10
- action: {
11
- type: string;
12
- config: {
13
- rate_limit_config: {
14
- algorithm: string;
15
- window_size: number;
16
- window_limit: number;
17
- };
18
- aggregate: {
19
- keys: {
20
- type: string;
21
- }[];
17
+ declare const toFunctionConfigPayload: (func: FunctionResult) => {
18
+ display_name: string | undefined;
19
+ excluded_routes: import("@netlify/zip-it-and-ship-it").Route[] | undefined;
20
+ generator: string | undefined;
21
+ memory: number | undefined;
22
+ region: string | undefined;
23
+ routes: import("@netlify/zip-it-and-ship-it").ExtendedRoute[] | undefined;
24
+ build_data: import("@netlify/zip-it-and-ship-it/dist/utils/format_result.js").BuildData | undefined;
25
+ priority: number | undefined;
26
+ traffic_rules: {
27
+ action: {
28
+ type: string;
29
+ config: {
30
+ rate_limit_config: {
31
+ algorithm: string;
32
+ window_size: number;
33
+ window_limit: number;
34
+ };
35
+ aggregate: {
36
+ keys: {
37
+ type: string;
38
+ }[];
39
+ };
40
+ to: string | undefined;
22
41
  };
23
- to: string | undefined;
24
42
  };
25
- };
26
- } | undefined;
27
- interface FunctionConfigPayload {
28
- display_name: FunctionResult['displayName'];
29
- excluded_routes: FunctionResult['excludedRoutes'];
30
- generator: FunctionResult['generator'];
31
- memory: FunctionResult['memory'];
32
- region: FunctionResult['region'];
33
- routes: FunctionResult['routes'];
34
- build_data: FunctionResult['buildData'];
35
- priority: FunctionResult['priority'];
36
- traffic_rules: ReturnType<typeof trafficRulesConfig>;
37
- vcpu: FunctionResult['vcpu'];
38
- }
39
- declare const hashFns: (command: BaseCommand, directories: string[], { concurrentHash, functionsConfig, hashAlgorithm, manifestPath, packagePath, rootDir, serverEnabled, serverManifestPath, skipFunctionsCache, statusCb, tmpDir, }: {
43
+ } | undefined;
44
+ vcpu: number | undefined;
45
+ };
46
+ type FunctionConfigPayload = ReturnType<typeof toFunctionConfigPayload>;
47
+ declare const hashFns: (command: BaseCommand, directories: string[], { concurrentHash, functionsConfig, hashAlgorithm, manifestPath, packagePath, rootDir, serverConfig, serverEnabled, serverManifestPath, skipFunctionsCache, statusCb, tmpDir, }: {
40
48
  concurrentHash?: number;
41
49
  functionsConfig?: FunctionsConfig | undefined;
42
50
  hashAlgorithm?: string | undefined;
43
51
  manifestPath?: string | undefined;
44
52
  packagePath?: string | undefined;
45
53
  rootDir?: string | undefined;
54
+ serverConfig?: NetlifyServerConfig | undefined;
46
55
  serverEnabled?: boolean | undefined;
47
56
  serverManifestPath?: string | undefined;
48
57
  skipFunctionsCache?: boolean | undefined;
49
58
  statusCb: StatusCallback;
50
- tmpDir?: string | undefined;
59
+ tmpDir: string;
51
60
  }) => Promise<{
52
61
  functionSchedules?: {
53
62
  name: string;
@@ -55,13 +64,9 @@ declare const hashFns: (command: BaseCommand, directories: string[], { concurren
55
64
  }[] | undefined;
56
65
  functions: Record<string, string>;
57
66
  functionsWithNativeModules: FunctionResult[];
58
- shaMap?: Record<string, never> | undefined;
59
67
  fnShaMap?: Record<string, HashedFunctionFile[]> | undefined;
60
68
  fnConfig?: Record<string, FunctionConfigPayload> | undefined;
61
- server?: {
62
- sha: string;
63
- region?: string;
64
- } | undefined;
69
+ server?: ServerPayload | undefined;
65
70
  serverShaMap?: Record<string, ServerUploadFile[]> | undefined;
66
71
  }>;
67
72
  export default hashFns;
@@ -52,7 +52,7 @@ const getFunctionZips = async ({ command, directories, functionsConfig, manifest
52
52
  config: functionsConfig,
53
53
  });
54
54
  };
55
- const getServerBundle = async ({ packagePath, rootDir, serverEnabled, serverManifestPath, tmpDir, }) => {
55
+ const getServerBundle = async ({ packagePath, rootDir, serverConfig, serverEnabled, serverManifestPath, tmpDir, }) => {
56
56
  if (!serverEnabled) {
57
57
  return undefined;
58
58
  }
@@ -72,7 +72,7 @@ const getServerBundle = async ({ packagePath, rootDir, serverEnabled, serverMani
72
72
  if (entryPath === undefined) {
73
73
  return undefined;
74
74
  }
75
- return await zipServer(entryPath, join(tmpDir, 'server'), { basePath: rootDir });
75
+ return await zipServer(entryPath, join(tmpDir, 'server'), { basePath: rootDir, config: serverConfig });
76
76
  };
77
77
  const trafficRulesConfig = (trafficRules) => {
78
78
  if (!trafficRules) {
@@ -93,14 +93,23 @@ const trafficRulesConfig = (trafficRules) => {
93
93
  },
94
94
  };
95
95
  };
96
- const hashFns = async (command, directories, { concurrentHash, functionsConfig, hashAlgorithm = 'sha256', manifestPath, packagePath, rootDir, serverEnabled, serverManifestPath, skipFunctionsCache, statusCb, tmpDir, }) => {
96
+ const toFunctionConfigPayload = (func) => ({
97
+ display_name: func.displayName,
98
+ excluded_routes: func.excludedRoutes,
99
+ generator: func.generator,
100
+ memory: func.memory,
101
+ region: func.region,
102
+ routes: func.routes,
103
+ build_data: func.buildData,
104
+ priority: func.priority,
105
+ traffic_rules: trafficRulesConfig(func.trafficRules),
106
+ vcpu: func.vcpu,
107
+ });
108
+ const hashFns = async (command, directories, { concurrentHash, functionsConfig, hashAlgorithm = 'sha256', manifestPath, packagePath, rootDir, serverConfig, serverEnabled, serverManifestPath, skipFunctionsCache, statusCb, tmpDir, }) => {
97
109
  // Exit early if there is nothing to bundle. A site can have a server without
98
110
  // any functions directory.
99
111
  if (directories.length === 0 && !serverEnabled) {
100
- return { functions: {}, functionsWithNativeModules: [], shaMap: {} };
101
- }
102
- if (!tmpDir) {
103
- throw new Error('Missing tmpDir directory for zipping files');
112
+ return { functions: {}, functionsWithNativeModules: [] };
104
113
  }
105
114
  const [functionZips, serverBundle] = await Promise.all([
106
115
  directories.length === 0
@@ -115,7 +124,7 @@ const hashFns = async (command, directories, { concurrentHash, functionsConfig,
115
124
  statusCb,
116
125
  tmpDir,
117
126
  }),
118
- getServerBundle({ packagePath, rootDir, serverEnabled, serverManifestPath, tmpDir }),
127
+ getServerBundle({ packagePath, rootDir, serverConfig, serverEnabled, serverManifestPath, tmpDir }),
119
128
  ]);
120
129
  // ZISI's in-memory FunctionResult only nests bootstrap/runtime version into
121
130
  // buildData when writing the manifest cache. Reconstruct it for direct-zip paths.
@@ -145,7 +154,7 @@ const hashFns = async (command, directories, { concurrentHash, functionsConfig,
145
154
  priority,
146
155
  trafficRules,
147
156
  }));
148
- const fnConfig = functionZips
157
+ const fnConfig = Object.fromEntries(functionZips
149
158
  .filter((func) => Boolean(func.displayName ||
150
159
  func.generator ||
151
160
  func.routes ||
@@ -155,21 +164,7 @@ const hashFns = async (command, directories, { concurrentHash, functionsConfig,
155
164
  func.region ||
156
165
  func.memory ||
157
166
  func.vcpu))
158
- .reduce((funcs, curr) => ({
159
- ...funcs,
160
- [curr.name]: {
161
- display_name: curr.displayName,
162
- excluded_routes: curr.excludedRoutes,
163
- generator: curr.generator,
164
- memory: curr.memory,
165
- region: curr.region,
166
- routes: curr.routes,
167
- build_data: curr.buildData,
168
- priority: curr.priority,
169
- traffic_rules: trafficRulesConfig(curr.trafficRules),
170
- vcpu: curr.vcpu,
171
- },
172
- }), {});
167
+ .map((func) => [func.name, toFunctionConfigPayload(func)]));
173
168
  const functionSchedules = functionZips
174
169
  .map(({ name, schedule }) => schedule && { name, cron: schedule })
175
170
  .filter((schedule) => schedule !== '' && schedule !== undefined);
@@ -210,7 +205,16 @@ const hashServer = async (serverBundle, { concurrentHash, hashAlgorithm, statusC
210
205
  hasherCtor({ concurrentHash, hashAlgorithm }),
211
206
  manifestCollectorCtor(servers, serverShaMap, { statusCb }),
212
207
  ]);
213
- return { server: { sha: Object.values(servers)[0], region: serverBundle.region }, serverShaMap };
208
+ const { excludedRoutes, instanceSize, region, routes, runtimeVersion } = serverBundle;
209
+ const server = {
210
+ sha: Object.values(servers)[0],
211
+ ...(region === undefined ? {} : { region }),
212
+ ...(instanceSize === undefined ? {} : { instance_size: instanceSize }),
213
+ ...(routes === undefined ? {} : { routes }),
214
+ ...(excludedRoutes === undefined ? {} : { excluded_routes: excludedRoutes }),
215
+ ...(runtimeVersion === undefined ? {} : { runtime_version: runtimeVersion }),
216
+ };
217
+ return { server, serverShaMap };
214
218
  };
215
219
  export default hashFns;
216
220
  //# sourceMappingURL=hash-fns.js.map
@@ -5,8 +5,7 @@ export declare const hasherCtor: ({ concurrentHash, hashAlgorithm }: {
5
5
  concurrentHash?: number;
6
6
  hashAlgorithm: string;
7
7
  }) => Transform;
8
- export declare const fileNormalizerCtor: ({ assetType, normalizer: normalizeFunction, }: {
9
- assetType: 'file';
8
+ export declare const fileNormalizerCtor: ({ normalizer: normalizeFunction }: {
10
9
  normalizer?: (file: File) => File;
11
10
  }) => Transform;
12
11
  export declare const manifestCollectorCtor: <T extends {
@@ -28,11 +28,11 @@ export const hasherCtor = ({ concurrentHash, hashAlgorithm }) => {
28
28
  });
29
29
  };
30
30
  // Inject normalized file names into normalizedPath and assetType
31
- export const fileNormalizerCtor = ({ assetType, normalizer: normalizeFunction, }) => {
31
+ export const fileNormalizerCtor = ({ normalizer: normalizeFunction }) => {
32
32
  return new Transform({
33
33
  objectMode: true,
34
34
  transform(fileObj, _, callback) {
35
- const normalizedFile = { ...fileObj, assetType, normalizedPath: normalizePath(fileObj.relname) };
35
+ const normalizedFile = { ...fileObj, assetType: 'file', normalizedPath: normalizePath(fileObj.relname) };
36
36
  const result = normalizeFunction !== undefined ? normalizeFunction(normalizedFile) : normalizedFile;
37
37
  this.push(result);
38
38
  callback();
@@ -13,5 +13,6 @@ export interface DeployConfig {
13
13
  functionsDirectory?: string;
14
14
  headers?: NetlifyConfig['headers'] | MinimalHeader[];
15
15
  redirects?: unknown[];
16
+ server?: NetlifyConfig['server'];
16
17
  }
17
18
  //# sourceMappingURL=types.d.ts.map
@@ -4,10 +4,10 @@ export type UploadApi = Pick<NetlifyAPI, 'uploadDeployFile' | 'uploadDeployFunct
4
4
  interface UploadFileBase {
5
5
  filepath: string;
6
6
  normalizedPath: string;
7
- body?: undefined;
8
7
  }
9
8
  export interface StaticUploadFile extends UploadFileBase {
10
9
  assetType: 'file';
10
+ body?: undefined;
11
11
  }
12
12
  export interface InlineUploadFile {
13
13
  assetType: 'file';
@@ -22,9 +22,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
22
22
  phase: 'start',
23
23
  });
24
24
  const uploadFile = async (fileObj, index) => {
25
- const { body, filepath, normalizedPath } = fileObj;
26
- // FIXME(@netlify/api): octet-stream `body` is typed as `ReadStream` only, but an inline string body works too
27
- const readStreamCtor = () => (body ?? fs.createReadStream(filepath));
25
+ const { normalizedPath } = fileObj;
28
26
  statusCb({
29
27
  type: 'upload',
30
28
  msg: `(${index}/${uploadList.length}) Uploading ${normalizedPath}...`,
@@ -32,8 +30,11 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
32
30
  });
33
31
  switch (fileObj.assetType) {
34
32
  case 'file': {
33
+ const { body, filepath } = fileObj;
35
34
  return await retryUpload(() => api.uploadDeployFile({
36
- body: readStreamCtor,
35
+ // FIXME(@netlify/api): `types.d.ts` hardcodes every `application/octet-stream` body as `ReadStream | (() => ReadStream)`,
36
+ // ignoring the spec's `string` type; a string body works at runtime
37
+ body: () => (body ?? fs.createReadStream(filepath)),
37
38
  deployId,
38
39
  path: encodeURI(normalizedPath),
39
40
  }), maxRetry);
@@ -42,7 +43,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
42
43
  const { invocationMode, runtime, timeout } = fileObj;
43
44
  return await retryUpload((retryCount) => {
44
45
  const params = {
45
- body: readStreamCtor,
46
+ body: () => fs.createReadStream(fileObj.filepath),
46
47
  deployId,
47
48
  invocationMode,
48
49
  timeout,
@@ -58,7 +59,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
58
59
  case 'edge-function': {
59
60
  return await retryUpload((retryCount) => {
60
61
  const params = {
61
- body: readStreamCtor,
62
+ body: () => fs.createReadStream(fileObj.filepath),
62
63
  deployId,
63
64
  codeSha: normalizedPath,
64
65
  };
@@ -71,7 +72,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
71
72
  case 'server': {
72
73
  return await retryUpload((retryCount) => {
73
74
  const params = {
74
- body: readStreamCtor,
75
+ body: () => fs.createReadStream(fileObj.filepath),
75
76
  deployId,
76
77
  codeSha: fileObj.hash,
77
78
  };
@@ -132,8 +133,9 @@ const retryUpload = (uploadFn, maxRetry) => new Promise((resolve, reject) => {
132
133
  // Do something when backoff starts, e.g. show to the
133
134
  // user the delay before next reconnection attempt.
134
135
  });
135
- // eslint-disable-next-line @typescript-eslint/no-misused-promises -- FIXME: the listener's promise is discarded
136
- fibonacciBackoff.on('ready', tryUpload);
136
+ fibonacciBackoff.on('ready', (retryIndex) => {
137
+ void tryUpload(retryIndex);
138
+ });
137
139
  fibonacciBackoff.on('fail', () => {
138
140
  reject(lastError);
139
141
  });
@@ -2,6 +2,10 @@ import type { NetlifyAPI } from '@netlify/api';
2
2
  export declare const pluralize: (amount: number, noun: string) => string;
3
3
  export type Deploy = Awaited<ReturnType<NetlifyAPI['getSiteDeploy']>>;
4
4
  export type ReadyDeploy = Deploy & Required<Pick<Deploy, 'id' | 'site_id' | 'name' | 'admin_url' | 'url' | 'ssl_url' | 'deploy_url' | 'deploy_ssl_url'>>;
5
+ export declare class DeployStateError extends Error {
6
+ readonly deploy: Deploy;
7
+ constructor(message: string, deploy: Deploy);
8
+ }
5
9
  export declare const normalizePath: (relname: string) => string;
6
10
  export declare const waitForDiff: (api: Pick<NetlifyAPI, 'getSiteDeploy'>, deployId: string, siteId: string, timeout: number) => Promise<Deploy>;
7
11
  export declare const waitForDeploy: (api: Pick<NetlifyAPI, 'getSiteDeploy'>, deployId: string, siteId: string, timeout: number) => Promise<ReadyDeploy>;
@@ -2,6 +2,13 @@ import { sep } from 'path';
2
2
  import pWaitFor from 'p-wait-for';
3
3
  import { DEPLOY_POLL } from './constants.js';
4
4
  export const pluralize = (amount, noun) => `${amount} ${noun}${amount === 1 ? '' : 's'}`;
5
+ export class DeployStateError extends Error {
6
+ deploy;
7
+ constructor(message, deploy) {
8
+ super(message);
9
+ this.deploy = deploy;
10
+ }
11
+ }
5
12
  // normalize windows paths to unix paths
6
13
  export const normalizePath = (relname) => {
7
14
  if (relname.includes('#') || relname.includes('?')) {
@@ -9,70 +16,26 @@ export const normalizePath = (relname) => {
9
16
  }
10
17
  return relname.split(sep).join('/');
11
18
  };
19
+ // https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33
20
+ const DIFFED_STATES = new Set(['prepared', 'uploading', 'uploaded', 'ready']);
21
+ const READY_STATES = new Set(['ready']);
22
+ const pollDeployUntil = async (api, deployId, siteId, timeout, targetStates) => await pWaitFor(async () => {
23
+ const siteDeploy = await api.getSiteDeploy({ siteId, deployId });
24
+ if (siteDeploy.state === 'error') {
25
+ throw new DeployStateError(siteDeploy.error_message || `Deploy ${deployId} had an error`, siteDeploy);
26
+ }
27
+ return siteDeploy.state !== undefined && targetStates.has(siteDeploy.state) && pWaitFor.resolveWith(siteDeploy);
28
+ }, {
29
+ interval: DEPLOY_POLL,
30
+ timeout: {
31
+ milliseconds: timeout,
32
+ message: 'Timeout while waiting for deploy',
33
+ },
34
+ });
12
35
  // poll an async deployId until its done diffing
13
- export const waitForDiff = async (api, deployId, siteId, timeout) => {
14
- const loadDeploy = async () => {
15
- const siteDeploy = await api.getSiteDeploy({ siteId, deployId });
16
- switch (siteDeploy.state) {
17
- // https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33
18
- case 'error': {
19
- const deployError = new Error(siteDeploy.error_message || `Deploy ${deployId} had an error`);
20
- deployError.deploy = siteDeploy;
21
- throw deployError;
22
- }
23
- case 'prepared':
24
- case 'uploading':
25
- case 'uploaded':
26
- case 'ready': {
27
- return pWaitFor.resolveWith(siteDeploy);
28
- }
29
- case 'preparing':
30
- default: {
31
- return false;
32
- }
33
- }
34
- };
35
- const deploy = await pWaitFor(loadDeploy, {
36
- interval: DEPLOY_POLL,
37
- timeout: {
38
- milliseconds: timeout,
39
- message: 'Timeout while waiting for deploy',
40
- },
41
- });
42
- return deploy;
43
- };
36
+ export const waitForDiff = async (api, deployId, siteId, timeout) => await pollDeployUntil(api, deployId, siteId, timeout, DIFFED_STATES);
44
37
  // Poll a deployId until its ready
45
- export const waitForDeploy = async (api, deployId, siteId, timeout) => {
46
- const loadDeploy = async () => {
47
- const siteDeploy = await api.getSiteDeploy({ siteId, deployId });
48
- switch (siteDeploy.state) {
49
- // https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33
50
- case 'error': {
51
- const deployError = new Error(siteDeploy.error_message || `Deploy ${deployId} had an error`);
52
- deployError.deploy = siteDeploy;
53
- throw deployError;
54
- }
55
- case 'ready': {
56
- return pWaitFor.resolveWith(siteDeploy);
57
- }
58
- case 'preparing':
59
- case 'prepared':
60
- case 'uploaded':
61
- case 'uploading':
62
- default: {
63
- return false;
64
- }
65
- }
66
- };
67
- const deploy = await pWaitFor(loadDeploy, {
68
- interval: DEPLOY_POLL,
69
- timeout: {
70
- milliseconds: timeout,
71
- message: 'Timeout while waiting for deploy',
72
- },
73
- });
74
- return deploy;
75
- };
38
+ export const waitForDeploy = async (api, deployId, siteId, timeout) => (await pollDeployUntil(api, deployId, siteId, timeout, READY_STATES));
76
39
  // Transform the fileShaMap and fnShaMap into a generic shaMap that file-uploader.js can use
77
40
  export const getUploadList = (required, shaMap) => {
78
41
  if (!required || !shaMap)
@@ -199,8 +199,9 @@ const isOlderThan = async (file, ageMs) => {
199
199
  };
200
200
  const isSameEntry = async (a, b) => {
201
201
  try {
202
- const [statA, statB] = await Promise.all([fs.lstat(a), fs.lstat(b)]);
203
- return statA.ino !== 0 && statA.ino === statB.ino && statA.dev === statB.dev;
202
+ // As numbers, Windows file IDs lose precision and two directories can share one.
203
+ const [statA, statB] = await Promise.all([fs.lstat(a, { bigint: true }), fs.lstat(b, { bigint: true })]);
204
+ return statA.ino !== 0n && statA.ino === statB.ino && statA.dev === statB.dev;
204
205
  }
205
206
  catch {
206
207
  return false;
@@ -1,4 +1,5 @@
1
1
  import type { MinimalAccount } from './types.js';
2
+ export declare const requireTeams: (accounts: MinimalAccount[] | undefined) => MinimalAccount[];
2
3
  export declare const resolveTeam: (accounts: MinimalAccount[]) => MinimalAccount | undefined;
3
4
  export declare const resolveTeamForNonInteractive: (accounts: MinimalAccount[], commandExample: string) => MinimalAccount;
4
5
  //# sourceMappingURL=team.d.ts.map
@@ -1,4 +1,11 @@
1
1
  import { logAndThrowError } from './command-helpers.js';
2
+ export const requireTeams = (accounts) => {
3
+ const teams = accounts ?? [];
4
+ if (teams.length === 0) {
5
+ return logAndThrowError('No teams available. Please ensure you have access to at least one team.');
6
+ }
7
+ return teams;
8
+ };
2
9
  export const resolveTeam = (accounts) => {
3
10
  if (accounts.length === 1) {
4
11
  return accounts[0];
@@ -6,14 +13,12 @@ export const resolveTeam = (accounts) => {
6
13
  return accounts.find((acc) => acc.default);
7
14
  };
8
15
  export const resolveTeamForNonInteractive = (accounts, commandExample) => {
9
- if (accounts.length === 0) {
10
- return logAndThrowError('No teams available. Please ensure you have access to at least one team.');
11
- }
12
- const team = resolveTeam(accounts);
16
+ const teams = requireTeams(accounts);
17
+ const team = resolveTeam(teams);
13
18
  if (team) {
14
19
  return team;
15
20
  }
16
- const availableTeams = accounts.map((t) => t.slug).join(', ');
21
+ const availableTeams = teams.map((t) => t.slug).join(', ');
17
22
  return logAndThrowError(`Multiple teams available. Please specify which team to use.\n` +
18
23
  `Available teams: ${availableTeams}\n\n` +
19
24
  `Example: ${commandExample}\n\n` +
@@ -3,9 +3,8 @@ import { dirname, join } from 'path';
3
3
  import process, { version as nodejsVersion } from 'process';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { getGlobalConfigStore } from '@netlify/dev-utils';
6
- import { isCI } from 'ci-info';
7
6
  import execa from '../execa.js';
8
- import { cliVersion } from './utils.js';
7
+ import { cliVersion, isTelemetryDisabledByEnv } from './utils.js';
9
8
  const dirPath = dirname(fileURLToPath(import.meta.url));
10
9
  let currentCommand;
11
10
  export const setCommandForErrorReporting = (command) => {
@@ -21,7 +20,7 @@ export const setCommandForErrorReporting = (command) => {
21
20
  */
22
21
  // @ts-expect-error TS(7006) FIXME: Parameter 'error' implicitly has an 'any' type.
23
22
  export const reportError = async function (error, config = {}) {
24
- if (isCI) {
23
+ if (isTelemetryDisabledByEnv()) {
25
24
  return;
26
25
  }
27
26
  // convert a NotifiableError to an error class
@@ -2,10 +2,10 @@ import { dirname, join } from 'path';
2
2
  import process, { version as nodejsVersion } from 'process';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { getGlobalConfigStore } from '@netlify/dev-utils';
5
- import { isCI } from 'ci-info';
5
+ import { id as ciVendor, isPR } from 'ci-info';
6
6
  import { getDrivingAgent } from '../agent-detection.js';
7
7
  import execa from '../execa.js';
8
- import { isTelemetryDisabled, cliVersion } from './utils.js';
8
+ import { isTelemetryDisabled, isTelemetryDisabledByEnv, cliVersion } from './utils.js';
9
9
  import isValidEventName from './validation.js';
10
10
  const dirPath = dirname(fileURLToPath(import.meta.url));
11
11
  function send(type, payload) {
@@ -49,11 +49,15 @@ const getAgentProperties = () => {
49
49
  agent_other_value: agent?.otherValue,
50
50
  };
51
51
  };
52
+ const getCIProperties = () => ({
53
+ ci_vendor: ciVendor ?? undefined,
54
+ ci_is_pr: isPR ?? undefined,
55
+ });
52
56
  /**
53
57
  * Tracks a custom event with the provided payload
54
58
  */
55
59
  export async function track(eventName, payload = {}) {
56
- if (isCI) {
60
+ if (isTelemetryDisabledByEnv()) {
57
61
  return;
58
62
  }
59
63
  const globalConfig = await getGlobalConfigStore();
@@ -78,12 +82,12 @@ export async function track(eventName, payload = {}) {
78
82
  anonymousId: cliId,
79
83
  duration,
80
84
  status,
81
- properties: { ...properties, nodejsVersion, cliVersion, ...getAgentProperties() },
85
+ properties: { ...properties, nodejsVersion, cliVersion, ...getAgentProperties(), ...getCIProperties() },
82
86
  };
83
87
  return send('track', defaultData);
84
88
  }
85
89
  export async function identify(payload) {
86
- if (isCI) {
90
+ if (isTelemetryDisabledByEnv()) {
87
91
  return;
88
92
  }
89
93
  const globalConfig = await getGlobalConfigStore();
@@ -1,3 +1,4 @@
1
1
  export declare const cliVersion: string;
2
2
  export declare const isTelemetryDisabled: (config: any) => any;
3
+ export declare const isTelemetryDisabledByEnv: () => boolean;
3
4
  //# sourceMappingURL=utils.d.ts.map
@@ -1,7 +1,11 @@
1
+ import process from 'process';
1
2
  import getCLIPackageJson from '../get-cli-package-json.js';
2
3
  export const { version: cliVersion } = await getCLIPackageJson();
3
4
  // @ts-expect-error TS(7006) FIXME: Parameter 'config' implicitly has an 'any' type.
4
5
  export const isTelemetryDisabled = function (config) {
5
6
  return config.get('telemetryDisabled');
6
7
  };
8
+ const isEnabledFlag = (value) => value !== undefined && !['', '0', 'false'].includes(value);
9
+ // An env var opt-out matters in CI, where the global config written by `--telemetry-disable` rarely persists.
10
+ export const isTelemetryDisabledByEnv = () => isEnabledFlag(process.env.NETLIFY_TELEMETRY_DISABLED) || isEnabledFlag(process.env.DO_NOT_TRACK);
7
11
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "27.12.1",
3
+ "version": "27.14.0",
4
4
  "description": "Netlify command line tool",
5
5
  "keywords": [
6
6
  "api",
@@ -55,11 +55,11 @@
55
55
  "@fastify/static": "^10.0.0",
56
56
  "@inquirer/prompts": "^8.7.2",
57
57
  "@netlify/ai": "^1.0.1",
58
- "@netlify/api": "^15.1.2",
58
+ "@netlify/api": "^15.1.3",
59
59
  "@netlify/blobs": "^11.1.3",
60
- "@netlify/build": "^37.4.0",
60
+ "@netlify/build": "^37.5.0",
61
61
  "@netlify/build-info": "^11.3.0",
62
- "@netlify/config": "^25.2.6",
62
+ "@netlify/config": "^25.3.0",
63
63
  "@netlify/dev": "^5.1.6",
64
64
  "@netlify/dev-utils": "^6.0.3",
65
65
  "@netlify/edge-bundler": "^16.1.2",
@@ -71,14 +71,14 @@
71
71
  "@netlify/redirect-matcher": "^0.4.2",
72
72
  "@netlify/redirect-parser": "^16.1.1",
73
73
  "@netlify/server-dev": "^0.1.3",
74
- "@netlify/zip-it-and-ship-it": "^16.3.0",
74
+ "@netlify/zip-it-and-ship-it": "^16.4.0",
75
75
  "@opentelemetry/api": "~1.9.0",
76
76
  "@pnpm/tabtab": "^0.5.4",
77
77
  "ansi-escapes": "^7.3.0",
78
78
  "ansi-to-html": "^0.7.2",
79
79
  "ascii-table": "^0.0.9",
80
80
  "backoff": "^2.5.0",
81
- "boxen": "^8.0.1",
81
+ "boxen": "^9.0.0",
82
82
  "chalk": "^6.0.0",
83
83
  "chokidar": "^5.0.0",
84
84
  "ci-info": "^4.4.0",
@@ -182,7 +182,7 @@
182
182
  "@types/write-file-atomic": "^4.0.3",
183
183
  "@types/ws": "^8.18.1",
184
184
  "@types/yauzl": "^2.10.3",
185
- "@vitest/coverage-v8": "^4.1.11",
185
+ "@vitest/coverage-v8": "^5.0.0",
186
186
  "@vitest/eslint-plugin": "1.3.13",
187
187
  "c8": "^10.1.3",
188
188
  "cheerio": "^1.1.2",
@@ -196,7 +196,7 @@
196
196
  "memfs": "^4.56.10",
197
197
  "nock": "^14.0.10",
198
198
  "npm-run-all2": "^8.0.4",
199
- "oxfmt": "0.72.0",
199
+ "oxfmt": "0.73.0",
200
200
  "p-timeout": "^7.0.0",
201
201
  "picomatch": "^4.0.4",
202
202
  "pkg-pr-new": "^0.0.86",
@@ -210,7 +210,7 @@
210
210
  "typescript-eslint": "~8.65.0",
211
211
  "typescript-native": "npm:typescript@~7.0.2",
212
212
  "verdaccio": "^6.3.2",
213
- "vitest": "^4.1.11"
213
+ "vitest": "^5.0.0"
214
214
  },
215
215
  "engines": {
216
216
  "node": ">=22.13.0"