netlify 27.16.1 → 27.16.3

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.
@@ -66,7 +66,7 @@ export default class BaseCommand extends Command {
66
66
  /** Will be called on the end of an action to track the metrics */
67
67
  onEnd(error_?: unknown): Promise<undefined>;
68
68
  authenticate(tokenFromFlag?: string): Promise<string>;
69
- private refreshAccounts;
69
+ refreshAccounts(): Promise<void>;
70
70
  expensivelyAuthenticate(): Promise<string>;
71
71
  /** Adds some data to the analytics payload */
72
72
  setAnalyticsPayload(payload: Record<string, unknown>): void;
@@ -1,6 +1,6 @@
1
1
  import { confirm, select } from '@inquirer/prompts';
2
2
  import { isEmpty } from '../../utils/object-utilities.js';
3
- import { chalk, exit, log, netlifyCommand } from '../../utils/command-helpers.js';
3
+ import { chalk, exit, log, logAndThrowError, netlifyCommand, warn } from '../../utils/command-helpers.js';
4
4
  import getRepoData from '../../utils/get-repo-data.js';
5
5
  import { ensureNetlifyIgnore } from '../../utils/gitignore.js';
6
6
  import { configureRepo } from '../../utils/init/config.js';
@@ -30,6 +30,71 @@ const logExistingAndExit = ({ siteInfo }) => {
30
30
  log(`2. Then run ${chalk.cyanBright.bold(`${netlifyCommand()} init`)} again`);
31
31
  return exit();
32
32
  };
33
+ const logLinkedWithoutGitAndExit = ({ siteInfo, repoError, }) => {
34
+ if (repoError.reason !== 'no-remote') {
35
+ warn(`Could not read the git remote: ${repoError.error}`);
36
+ log(`Fix the git remote, then run ${chalk.cyanBright.bold(`${netlifyCommand()} init`)} again to connect it.`);
37
+ }
38
+ log();
39
+ log(`This folder is already linked to the Netlify project ${chalk.cyan(siteInfo.name)}`);
40
+ log();
41
+ log(`Project Url: ${chalk.cyan(siteInfo.ssl_url || siteInfo.url)}`);
42
+ log();
43
+ log(`To deploy to this project, run ${chalk.cyanBright.bold(`${netlifyCommand()} deploy`)}.`);
44
+ log(`To link this folder to a different project, run ${chalk.cyanBright.bold(`${netlifyCommand()} unlink`)}, then ${chalk.cyanBright.bold(`${netlifyCommand()} init`)}.`);
45
+ return exit();
46
+ };
47
+ const ignoreLinkedSiteForThisRun = (command) => {
48
+ const { site } = command.netlify;
49
+ command.siteId = undefined;
50
+ command.netlify.siteInfo = {};
51
+ command.netlify.site = {
52
+ root: site.root,
53
+ configPath: site.configPath,
54
+ get id() {
55
+ return command.siteId;
56
+ },
57
+ set id(id) {
58
+ site.id = id;
59
+ },
60
+ };
61
+ };
62
+ const getLinkedSiteInfo = async (command) => {
63
+ const { api, siteInfo } = command.netlify;
64
+ if (isEmpty(siteInfo)) {
65
+ return undefined;
66
+ }
67
+ if (siteInfo.name) {
68
+ return siteInfo;
69
+ }
70
+ try {
71
+ return (await api.getSite({ siteId: siteInfo.id }));
72
+ }
73
+ catch (error_) {
74
+ const { status, message } = error_;
75
+ const unusableLinks = {
76
+ 403: {
77
+ reason: 'forbidden',
78
+ warning: `You don't have access to the Netlify project ${siteInfo.id} linked to this folder. Continuing without it.`,
79
+ },
80
+ 404: {
81
+ reason: 'not_found',
82
+ warning: `The Netlify project ${siteInfo.id} linked to this folder no longer exists. Continuing without it.`,
83
+ },
84
+ };
85
+ const unusableLink = unusableLinks[status];
86
+ if (unusableLink === undefined) {
87
+ return logAndThrowError(`Could not load the Netlify project ${siteInfo.id} linked to this folder (${status ? `status ${status.toString()}` : message}). Try again, or run ${chalk.cyanBright.bold(`${netlifyCommand()} unlink`)} to start over.`);
88
+ }
89
+ warn(unusableLink.warning);
90
+ await track('sites_unlinked', { siteId: siteInfo.id, kind: 'init', reason: unusableLink.reason });
91
+ ignoreLinkedSiteForThisRun(command);
92
+ if (command.netlify.accounts.length === 0) {
93
+ await command.refreshAccounts();
94
+ }
95
+ return undefined;
96
+ }
97
+ };
33
98
  /**
34
99
  * Creates and new site and exits the process
35
100
  */
@@ -167,7 +232,6 @@ export const init = async (options, command, { customizeExitMessage, exitAfterCo
167
232
  resetContext: options.resetContext,
168
233
  });
169
234
  const { repositoryRoot, state } = command.netlify;
170
- const { siteInfo: existingSiteInfo } = command.netlify;
171
235
  // Check logged in status
172
236
  await command.authenticate();
173
237
  // Add .netlify to .gitignore file
@@ -175,13 +239,16 @@ export const init = async (options, command, { customizeExitMessage, exitAfterCo
175
239
  if (shouldSetupAgentSkills) {
176
240
  await installAgentSkills(command, resetContext);
177
241
  }
178
- const repoUrl = getRepoUrl(existingSiteInfo);
179
- if (repoUrl && !options.force) {
180
- logExistingAndExit({ siteInfo: existingSiteInfo });
242
+ const linkedSiteInfo = await getLinkedSiteInfo(command);
243
+ if (linkedSiteInfo && getRepoUrl(linkedSiteInfo) && !options.force) {
244
+ logExistingAndExit({ siteInfo: linkedSiteInfo });
181
245
  }
182
246
  // Look for local repo
183
247
  const repoData = await getRepoData({ workingDir: command.workingDir, remoteName: options.gitRemoteName });
184
248
  if ('error' in repoData) {
249
+ if (linkedSiteInfo) {
250
+ return logLinkedWithoutGitAndExit({ siteInfo: linkedSiteInfo, repoError: repoData });
251
+ }
185
252
  // TODO(ndhoule): Custom error messaage here
186
253
  return await handleNoGitRemoteAndExit({
187
254
  command,
@@ -191,7 +258,7 @@ export const init = async (options, command, { customizeExitMessage, exitAfterCo
191
258
  customizeExitMessage,
192
259
  });
193
260
  }
194
- const siteInfo = isEmpty(existingSiteInfo) ? await createOrLinkSiteToRepo(command) : existingSiteInfo;
261
+ const siteInfo = linkedSiteInfo ?? (await createOrLinkSiteToRepo(command));
195
262
  log();
196
263
  // Check for existing CI setup
197
264
  const remoteBuildRepo = getRepoUrl(siteInfo);
@@ -57,6 +57,7 @@ export const debugFetch = async (url, init) => {
57
57
  };
58
58
  export const fetchHistoricalLogs = async ({ baseUrl, accessToken, from, to, deployId, }) => {
59
59
  const entries = [];
60
+ const requestedCursors = new Set();
60
61
  let cursor;
61
62
  do {
62
63
  const parsedUrl = new URL(baseUrl);
@@ -79,7 +80,13 @@ export const fetchHistoricalLogs = async ({ baseUrl, accessToken, from, to, depl
79
80
  }
80
81
  const body = (await response.json());
81
82
  entries.push(...(body.logs ?? []));
82
- cursor = body.pagination?.next;
83
+ cursor = body.next;
84
+ if (cursor) {
85
+ if (requestedCursors.has(cursor)) {
86
+ throw new Error(`Failed to fetch logs: pagination repeated cursor ${cursor}`);
87
+ }
88
+ requestedCursors.add(cursor);
89
+ }
83
90
  } while (cursor);
84
91
  return entries;
85
92
  };
@@ -258,7 +258,9 @@ const runHistoricalMode = async ({ sources, client, siteId, accessToken, deployI
258
258
  to,
259
259
  deployId,
260
260
  });
261
- allEntries.push(...entries);
261
+ for (const entry of entries) {
262
+ allEntries.push(entry);
263
+ }
262
264
  }
263
265
  }
264
266
  if (sources.includes('edge-functions')) {
@@ -270,7 +272,9 @@ const runHistoricalMode = async ({ sources, client, siteId, accessToken, deployI
270
272
  deployId,
271
273
  filterNames: edgeFunctionNames,
272
274
  });
273
- allEntries.push(...entries);
275
+ for (const entry of entries) {
276
+ allEntries.push(entry);
277
+ }
274
278
  }
275
279
  let serverLogsTruncated = false;
276
280
  if (sources.includes('server')) {
@@ -7,11 +7,14 @@ export interface RepoData {
7
7
  provider: string | null;
8
8
  httpsUrl: string;
9
9
  }
10
+ export type RepoDataErrorReason = 'no-remote' | 'unknown-remote' | 'invalid-url' | 'unreadable';
11
+ export interface RepoDataError {
12
+ error: string;
13
+ reason: RepoDataErrorReason;
14
+ }
10
15
  declare const getRepoData: ({ remoteName, workingDir, }: {
11
16
  remoteName?: string;
12
17
  workingDir: string;
13
- }) => Promise<RepoData | {
14
- error: string;
15
- }>;
18
+ }) => Promise<RepoData | RepoDataError>;
16
19
  export default getRepoData;
17
20
  //# sourceMappingURL=get-repo-data.d.ts.map
@@ -5,14 +5,23 @@ import gitRepoInfo from 'git-repo-info';
5
5
  import gitconfiglocal from 'gitconfiglocal';
6
6
  import parseGithubUrl from 'parse-github-url';
7
7
  import { log } from './command-helpers.js';
8
+ class RepoDataLookupError extends Error {
9
+ reason;
10
+ constructor(message, reason) {
11
+ super(message);
12
+ this.reason = reason;
13
+ }
14
+ }
15
+ const redactCredentials = (url) => url.replace(/\/\/[^/@]*@/, '//***@');
8
16
  const getRepoData = async ({ remoteName, workingDir, }) => {
9
17
  try {
10
- const [gitConfig, gitDirectory] = await Promise.all([
11
- util.promisify(gitconfiglocal)(workingDir),
12
- findUp('.git', { cwd: workingDir, type: 'directory' }),
13
- ]);
14
- if (!gitDirectory || !gitConfig?.remote || Object.keys(gitConfig.remote).length === 0) {
15
- throw new Error('No Git remote found');
18
+ const gitDirectory = await findUp('.git', { cwd: workingDir, type: 'directory' });
19
+ if (!gitDirectory) {
20
+ throw new RepoDataLookupError('No Git remote found', 'no-remote');
21
+ }
22
+ const gitConfig = await util.promisify(gitconfiglocal)(workingDir);
23
+ if (!gitConfig?.remote || Object.keys(gitConfig.remote).length === 0) {
24
+ throw new RepoDataLookupError('No Git remote found', 'no-remote');
16
25
  }
17
26
  const baseGitPath = dirname(gitDirectory);
18
27
  if (workingDir !== baseGitPath) {
@@ -25,13 +34,13 @@ const getRepoData = async ({ remoteName, workingDir, }) => {
25
34
  if (!Object.prototype.hasOwnProperty.call(gitConfig.remote, remoteName) ||
26
35
  !gitConfig.remote[remoteName] ||
27
36
  Object.keys(gitConfig.remote[remoteName]).length === 0) {
28
- throw new Error(`The specified remote "${remoteName}" is not defined in Git repo. Please use --git-remote-name flag to specify a remote.`);
37
+ throw new RepoDataLookupError(`The specified remote "${remoteName}" is not defined in Git repo. Please use --git-remote-name flag to specify a remote.`, 'unknown-remote');
29
38
  }
30
39
  const { url } = gitConfig.remote[remoteName];
31
40
  const parsedUrl = parseGithubUrl(url);
32
41
  // TODO(serhalp): Validate more aggressively? We should probably require `owner`, `repo`, `host`?
33
42
  if (parsedUrl == null) {
34
- throw new Error(`The specified Git remote ${remoteName} is not a valid URL: ${url}`);
43
+ throw new RepoDataLookupError(`The specified Git remote ${remoteName} is not a valid URL: ${redactCredentials(String(url))}`, 'invalid-url');
35
44
  }
36
45
  const { host, name, owner, repo } = parsedUrl;
37
46
  const { branch } = gitRepoInfo();
@@ -48,6 +57,7 @@ const getRepoData = async ({ remoteName, workingDir, }) => {
48
57
  catch (error) {
49
58
  return {
50
59
  error: error instanceof Error ? error.message : (error?.toString() ?? 'Failed to get repo data'),
60
+ reason: error instanceof RepoDataLookupError ? error.reason : 'unreadable',
51
61
  };
52
62
  }
53
63
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "netlify",
3
- "version": "27.16.1",
3
+ "version": "27.16.3",
4
4
  "description": "Netlify command line tool",
5
5
  "keywords": [
6
6
  "api",
@@ -54,11 +54,11 @@
54
54
  "@fastify/static": "^10.0.0",
55
55
  "@inquirer/prompts": "^8.7.2",
56
56
  "@netlify/ai": "^1.0.2",
57
- "@netlify/api": "^15.1.5",
57
+ "@netlify/api": "^15.1.6",
58
58
  "@netlify/blobs": "^11.1.7",
59
- "@netlify/build": "^37.7.2",
59
+ "@netlify/build": "^37.7.6",
60
60
  "@netlify/build-info": "^11.3.1",
61
- "@netlify/config": "^25.5.1",
61
+ "@netlify/config": "^25.6.2",
62
62
  "@netlify/dev": "^5.1.12",
63
63
  "@netlify/dev-utils": "^6.0.4",
64
64
  "@netlify/edge-bundler": "^16.2.0",