netlify 27.10.1 → 27.11.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.
- package/dist/commands/agents/agents-create.js +19 -33
- package/dist/commands/base-command.d.ts +1 -1
- package/dist/commands/base-command.js +8 -11
- package/dist/commands/clone/clone.js +5 -10
- package/dist/commands/completion/completion.js +5 -9
- package/dist/commands/create/create-action.js +16 -28
- package/dist/commands/database/db-init.js +17 -31
- package/dist/commands/database/db-migration-new.js +13 -23
- package/dist/commands/database/db-migration-pull.js +5 -9
- package/dist/commands/database/db-reset.js +5 -9
- package/dist/commands/deploy/deploy.js +23 -36
- package/dist/commands/env/env-list.js +5 -9
- package/dist/commands/functions/functions-create.js +35 -71
- package/dist/commands/functions/functions-invoke.js +5 -10
- package/dist/commands/init/init.js +12 -25
- package/dist/commands/link/link.js +26 -52
- package/dist/commands/logs/build.js +2 -4
- package/dist/commands/main.js +10 -17
- package/dist/commands/recipes/recipes.js +6 -16
- package/dist/commands/sites/sites-create.js +12 -22
- package/dist/commands/sites/sites-delete.js +3 -7
- package/dist/commands/switch/switch.js +5 -9
- package/dist/lib/edge-functions/editor-helper.js +3 -5
- package/dist/recipes/ai-context/index.js +9 -17
- package/dist/recipes/blobs-migrate/index.js +3 -7
- package/dist/recipes/vscode/index.js +6 -10
- package/dist/utils/build-info.js +3 -6
- package/dist/utils/deploy/deploy-site.js +21 -12
- package/dist/utils/deploy/upload-files.js +6 -2
- package/dist/utils/deploy/util.d.ts +1 -0
- package/dist/utils/deploy/util.js +1 -0
- package/dist/utils/exit-codes.d.ts +2 -0
- package/dist/utils/exit-codes.js +2 -0
- package/dist/utils/gh-auth.js +10 -22
- package/dist/utils/github-api.d.ts +22 -0
- package/dist/utils/github-api.js +47 -0
- package/dist/utils/init/config-github.js +29 -45
- package/dist/utils/init/config-manual.js +8 -28
- package/dist/utils/init/utils.js +14 -38
- package/dist/utils/prompts/confirm-prompt.js +4 -15
- package/dist/utils/proxy.js +7 -48
- package/dist/utils/redirects.js +3 -2
- package/dist/utils/rules-proxy.js +31 -29
- package/dist/utils/types.d.ts +2 -2
- package/package.json +10 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import
|
|
2
|
+
import { input, select } from '@inquirer/prompts';
|
|
3
3
|
import execa from 'execa';
|
|
4
4
|
import { logAndThrowError, log, version } from '../../utils/command-helpers.js';
|
|
5
5
|
import { track } from '../../utils/telemetry/index.js';
|
|
@@ -25,26 +25,18 @@ const presets = cliContextConsumers.map((consumer) => ({
|
|
|
25
25
|
// always add the custom location option (not preset from API)
|
|
26
26
|
presets.push({ name: 'Custom location', value: rulesForDefaultConsumer.key });
|
|
27
27
|
const promptForContextConsumerSelection = async () => {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
type: 'list',
|
|
33
|
-
choices: presets,
|
|
34
|
-
},
|
|
35
|
-
]);
|
|
28
|
+
const consumerKey = await select({
|
|
29
|
+
message: 'Where should we put the context files?',
|
|
30
|
+
choices: presets,
|
|
31
|
+
});
|
|
36
32
|
const contextConsumer = consumerKey ? cliContextConsumers.find((consumer) => consumer.key === consumerKey) : null;
|
|
37
33
|
if (contextConsumer) {
|
|
38
34
|
return contextConsumer;
|
|
39
35
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
message: 'Enter the path, relative to the project root, where the context files should be placed',
|
|
45
|
-
default: './ai-context',
|
|
46
|
-
},
|
|
47
|
-
]);
|
|
36
|
+
const customPath = await input({
|
|
37
|
+
message: 'Enter the path, relative to the project root, where the context files should be placed',
|
|
38
|
+
default: './ai-context',
|
|
39
|
+
});
|
|
48
40
|
if (customPath) {
|
|
49
41
|
return { ...rulesForDefaultConsumer, path: customPath || rulesForDefaultConsumer.path };
|
|
50
42
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getStore, listStores } from '@netlify/blobs';
|
|
2
|
-
import
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
3
|
import pMap from 'p-map';
|
|
4
4
|
import { logAndThrowError, log } from '../../utils/command-helpers.js';
|
|
5
5
|
export const description = 'Migrate legacy Netlify Blobs stores';
|
|
@@ -32,9 +32,7 @@ export const run = async ({ args, command }) => {
|
|
|
32
32
|
}
|
|
33
33
|
const { stores } = await listStores(clientOptions);
|
|
34
34
|
if (stores.includes(storeName)) {
|
|
35
|
-
const
|
|
36
|
-
type: 'confirm',
|
|
37
|
-
name: 'confirmExistingStore',
|
|
35
|
+
const confirmExistingStore = await confirm({
|
|
38
36
|
message: `The store '${storeName}' already exists in the new format, which means it has already been migrated or it has been used with a newer version of the Netlify Blobs client. If you continue with the migration, any blobs from the legacy store will overwrite newer entries that have the same key. Do you want to proceed?`,
|
|
39
37
|
default: false,
|
|
40
38
|
});
|
|
@@ -42,9 +40,7 @@ export const run = async ({ args, command }) => {
|
|
|
42
40
|
return;
|
|
43
41
|
}
|
|
44
42
|
}
|
|
45
|
-
const
|
|
46
|
-
type: 'confirm',
|
|
47
|
-
name: 'confirmMigration',
|
|
43
|
+
const confirmMigration = await confirm({
|
|
48
44
|
message: `You're about to migrate the store '${storeName}' with ${blobs.length} blobs. Do you want to proceed?`,
|
|
49
45
|
default: true,
|
|
50
46
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { DenoBridge } from '@netlify/edge-bundler';
|
|
3
3
|
import execa from 'execa';
|
|
4
|
-
import
|
|
4
|
+
import { confirm } from '@inquirer/prompts';
|
|
5
5
|
import { NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, logAndThrowError, log } from '../../utils/command-helpers.js';
|
|
6
6
|
import { applySettings, getSettings, writeSettings } from './settings.js';
|
|
7
7
|
export const description = 'Create VS Code settings for an optimal experience with Netlify projects';
|
|
@@ -11,9 +11,7 @@ const getPrompt = ({ fileExists, path }) => {
|
|
|
11
11
|
const message = fileExists
|
|
12
12
|
? `There is a VS Code settings file at ${formattedPath}. Can we update it?`
|
|
13
13
|
: `A new VS Code settings file will be created at ${formattedPath}`;
|
|
14
|
-
return
|
|
15
|
-
type: 'confirm',
|
|
16
|
-
name: 'confirm',
|
|
14
|
+
return confirm({
|
|
17
15
|
message,
|
|
18
16
|
default: true,
|
|
19
17
|
});
|
|
@@ -42,9 +40,7 @@ const getDenoVSCodeExt = async (repositoryRoot) => {
|
|
|
42
40
|
};
|
|
43
41
|
const getDenoExtPrompt = () => {
|
|
44
42
|
const message = 'The Deno VS Code extension is recommended. Would you like to install it now?';
|
|
45
|
-
return
|
|
46
|
-
type: 'confirm',
|
|
47
|
-
name: 'confirm',
|
|
43
|
+
return confirm({
|
|
48
44
|
message,
|
|
49
45
|
default: true,
|
|
50
46
|
});
|
|
@@ -67,13 +63,13 @@ export const run = async ({ config, repositoryRoot }) => {
|
|
|
67
63
|
const edgeFunctionsPath = getEdgeFunctionsPath({ config, repositoryRoot });
|
|
68
64
|
const { fileExists, settings: existingSettings } = await getSettings(settingsPath);
|
|
69
65
|
const settings = applySettings(existingSettings, { denoBinary, edgeFunctionsPath, repositoryRoot });
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
66
|
+
const confirmed = await getPrompt({ fileExists, path: settingsPath });
|
|
67
|
+
if (!confirmed) {
|
|
72
68
|
return;
|
|
73
69
|
}
|
|
74
70
|
try {
|
|
75
71
|
if (!(await hasDenoVSCodeExt(repositoryRoot))) {
|
|
76
|
-
const
|
|
72
|
+
const denoExtConfirm = await getDenoExtPrompt();
|
|
77
73
|
if (denoExtConfirm) {
|
|
78
74
|
getDenoVSCodeExt(repositoryRoot);
|
|
79
75
|
}
|
package/dist/utils/build-info.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { search } from '@inquirer/prompts';
|
|
1
2
|
import { isCI } from 'ci-info';
|
|
2
3
|
import fuzzy from 'fuzzy';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
4
|
import { chalk, log } from './command-helpers.js';
|
|
5
5
|
/**
|
|
6
6
|
* Filters the inquirer settings based on the input
|
|
@@ -73,12 +73,9 @@ export const detectFrameworkSettings = async (command, type = 'dev') => {
|
|
|
73
73
|
}
|
|
74
74
|
// multiple matching detectors, make the user choose
|
|
75
75
|
const scriptInquirerOptions = formatSettingsArrForInquirer(settings, type);
|
|
76
|
-
const
|
|
77
|
-
name: 'chosenSettings',
|
|
76
|
+
const chosenSettings = await search({
|
|
78
77
|
message: `Multiple possible ${type} commands found`,
|
|
79
|
-
|
|
80
|
-
type: 'autocomplete',
|
|
81
|
-
source(_, input = '') {
|
|
78
|
+
source(input = '') {
|
|
82
79
|
if (!input)
|
|
83
80
|
return scriptInquirerOptions;
|
|
84
81
|
// only show filtered results
|
|
@@ -9,12 +9,14 @@ import hashFiles from './hash-files.js';
|
|
|
9
9
|
import hashFns from './hash-fns.js';
|
|
10
10
|
import { deployFileNormalizer, getDbMigrationsDistPathIfExists, getDeployConfigPathIfExists, getEdgeFunctionsDistPathIfExists, isEdgeFunctionFile, } from './process-files.js';
|
|
11
11
|
import uploadFiles from './upload-files.js';
|
|
12
|
-
import { getUploadList, waitForDeploy, waitForDiff } from './util.js';
|
|
12
|
+
import { getUploadList, pluralize, waitForDeploy, waitForDiff } from './util.js';
|
|
13
13
|
import { temporaryDirectory } from '../temporary-file.js';
|
|
14
14
|
const buildStatsString = (possibleParts) => {
|
|
15
15
|
const parts = possibleParts.filter(Boolean);
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
if (parts.length < 2) {
|
|
17
|
+
return parts.join('');
|
|
18
|
+
}
|
|
19
|
+
return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;
|
|
18
20
|
};
|
|
19
21
|
export const deploySite = async (command, api,
|
|
20
22
|
// @ts-expect-error TS(7006) FIXME: Parameter 'siteId' implicitly has an 'any' type.
|
|
@@ -45,7 +47,7 @@ skipFunctionsCache, statusCb = () => {
|
|
|
45
47
|
}, syncFileLimit = DEFAULT_SYNC_LIMIT, tmpDir = temporaryDirectory(), workingDir, }) => {
|
|
46
48
|
statusCb({
|
|
47
49
|
type: 'hashing',
|
|
48
|
-
msg: `
|
|
50
|
+
msg: `Preparing deploy...`,
|
|
49
51
|
phase: 'start',
|
|
50
52
|
});
|
|
51
53
|
const edgeFunctionsDistPath = await getEdgeFunctionsDistPathIfExists(workingDir);
|
|
@@ -83,17 +85,18 @@ skipFunctionsCache, statusCb = () => {
|
|
|
83
85
|
const filesCount = Object.keys(files).length - edgeFunctionsCount;
|
|
84
86
|
const functionsCount = Object.keys(functions).length;
|
|
85
87
|
const stats = buildStatsString([
|
|
86
|
-
filesCount > 0 &&
|
|
87
|
-
functionsCount > 0 &&
|
|
88
|
-
edgeFunctionsCount > 0 && 'edge
|
|
88
|
+
filesCount > 0 && pluralize(filesCount, 'file'),
|
|
89
|
+
functionsCount > 0 && pluralize(functionsCount, 'function'),
|
|
90
|
+
edgeFunctionsCount > 0 && pluralize(edgeFunctionsCount, 'edge function'),
|
|
91
|
+
server && 'a server',
|
|
89
92
|
]);
|
|
90
93
|
statusCb({
|
|
91
94
|
type: 'hashing',
|
|
92
|
-
msg: `
|
|
95
|
+
msg: `Deploying ${stats}`,
|
|
93
96
|
phase: 'stop',
|
|
94
97
|
});
|
|
95
|
-
if (filesCount === 0 && functionsCount === 0) {
|
|
96
|
-
throw new Error('
|
|
98
|
+
if (filesCount === 0 && functionsCount === 0 && edgeFunctionsCount === 0 && !server) {
|
|
99
|
+
throw new Error('Nothing to deploy');
|
|
97
100
|
}
|
|
98
101
|
if (functionsWithNativeModules.length !== 0) {
|
|
99
102
|
const functionsWithNativeModulesMessage = functionsWithNativeModules
|
|
@@ -112,7 +115,7 @@ For more information, visit https://ntl.fyi/cli-native-modules.`);
|
|
|
112
115
|
}
|
|
113
116
|
statusCb({
|
|
114
117
|
type: 'create-deploy',
|
|
115
|
-
msg: '
|
|
118
|
+
msg: 'Checking what needs to be uploaded...',
|
|
116
119
|
phase: 'start',
|
|
117
120
|
});
|
|
118
121
|
const packageFrameworks = command.project.frameworks.get(command.workspacePackage ?? '');
|
|
@@ -146,9 +149,15 @@ For more information, visit https://ntl.fyi/cli-native-modules.`);
|
|
|
146
149
|
if (deployParams.body.async)
|
|
147
150
|
deploy = await waitForDiff(api, deploy.id, siteId, deployTimeout);
|
|
148
151
|
const { required: requiredFiles, required_functions: requiredFns, required_edge_functions: requiredEdgeFns, required_server: requiredServer, } = deploy;
|
|
152
|
+
const newStats = buildStatsString([
|
|
153
|
+
requiredFiles.length > 0 && pluralize(requiredFiles.length, 'file'),
|
|
154
|
+
(requiredFns?.length ?? 0) > 0 && pluralize(requiredFns.length, 'function'),
|
|
155
|
+
(requiredEdgeFns?.length ?? 0) > 0 && pluralize(requiredEdgeFns.length, 'edge function'),
|
|
156
|
+
(requiredServer?.length ?? 0) > 0 && 'a server',
|
|
157
|
+
]);
|
|
149
158
|
statusCb({
|
|
150
159
|
type: 'create-deploy',
|
|
151
|
-
msg:
|
|
160
|
+
msg: newStats ? `Found ${newStats} to upload` : 'Everything is uploaded',
|
|
152
161
|
phase: 'stop',
|
|
153
162
|
});
|
|
154
163
|
const filesUploadList = getUploadList(requiredFiles, filesShaMap);
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import backoff from 'backoff';
|
|
3
3
|
import pMap from 'p-map';
|
|
4
4
|
import { UPLOAD_INITIAL_DELAY, UPLOAD_MAX_DELAY, UPLOAD_RANDOM_FACTOR } from './constants.js';
|
|
5
|
+
import { pluralize } from './util.js';
|
|
5
6
|
class MissingAssetTypeError extends Error {
|
|
6
7
|
fileObj;
|
|
7
8
|
constructor(fileObj) {
|
|
@@ -12,9 +13,12 @@ class MissingAssetTypeError extends Error {
|
|
|
12
13
|
const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRetry, statusCb }) => {
|
|
13
14
|
if (!concurrentUpload || !maxRetry)
|
|
14
15
|
throw new Error('Missing required option concurrentUpload');
|
|
16
|
+
if (uploadList.length === 0) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
15
19
|
statusCb({
|
|
16
20
|
type: 'upload',
|
|
17
|
-
msg: `Uploading ${uploadList.length}
|
|
21
|
+
msg: `Uploading ${pluralize(uploadList.length, 'file')}`,
|
|
18
22
|
phase: 'start',
|
|
19
23
|
});
|
|
20
24
|
const uploadFile = async (fileObj, index) => {
|
|
@@ -84,7 +88,7 @@ const uploadFiles = async (api, deployId, uploadList, { concurrentUpload, maxRet
|
|
|
84
88
|
const results = await pMap(uploadList, uploadFile, { concurrency: concurrentUpload });
|
|
85
89
|
statusCb({
|
|
86
90
|
type: 'upload',
|
|
87
|
-
msg: `Finished uploading ${uploadList.length}
|
|
91
|
+
msg: `Finished uploading ${pluralize(uploadList.length, 'asset')}`,
|
|
88
92
|
phase: 'stop',
|
|
89
93
|
});
|
|
90
94
|
return results;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const pluralize: (amount: number, noun: string) => string;
|
|
1
2
|
export declare const normalizePath: (relname: string) => string;
|
|
2
3
|
export declare const waitForDiff: (api: any, deployId: any, siteId: any, timeout: any) => Promise<undefined>;
|
|
3
4
|
export declare const waitForDeploy: (api: any, deployId: any, siteId: any, timeout: any) => Promise<undefined>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sep } from 'path';
|
|
2
2
|
import pWaitFor from 'p-wait-for';
|
|
3
3
|
import { DEPLOY_POLL } from './constants.js';
|
|
4
|
+
export const pluralize = (amount, noun) => `${amount} ${noun}${amount === 1 ? '' : 's'}`;
|
|
4
5
|
// normalize windows paths to unix paths
|
|
5
6
|
export const normalizePath = (relname) => {
|
|
6
7
|
if (relname.includes('#') || relname.includes('?')) {
|
|
@@ -20,6 +20,8 @@ export declare const EXIT_CODES: {
|
|
|
20
20
|
readonly USAGE_ERROR: 2;
|
|
21
21
|
/** An interactive prompt was required but the session is non-interactive (CI or `--non-interactive`) */
|
|
22
22
|
readonly NON_INTERACTIVE_PROMPT: 4;
|
|
23
|
+
/** The user cancelled an interactive prompt with Ctrl+C, matching a SIGINT-terminated process */
|
|
24
|
+
readonly INTERRUPTED: 130;
|
|
23
25
|
};
|
|
24
26
|
export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
|
|
25
27
|
//# sourceMappingURL=exit-codes.d.ts.map
|
package/dist/utils/exit-codes.js
CHANGED
|
@@ -20,5 +20,7 @@ export const EXIT_CODES = {
|
|
|
20
20
|
USAGE_ERROR: 2,
|
|
21
21
|
/** An interactive prompt was required but the session is non-interactive (CI or `--non-interactive`) */
|
|
22
22
|
NON_INTERACTIVE_PROMPT: 4,
|
|
23
|
+
/** The user cancelled an interactive prompt with Ctrl+C, matching a SIGINT-terminated process */
|
|
24
|
+
INTERRUPTED: 130,
|
|
23
25
|
};
|
|
24
26
|
//# sourceMappingURL=exit-codes.js.map
|
package/dist/utils/gh-auth.js
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
// A simple ghauth inspired library for getting a personal access token
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import process from 'process';
|
|
4
|
-
import {
|
|
4
|
+
import { password, select } from '@inquirer/prompts';
|
|
5
5
|
import getPort from 'get-port';
|
|
6
|
-
import inquirer from 'inquirer';
|
|
7
6
|
import { log } from './command-helpers.js';
|
|
8
7
|
import createDeferred from './create-deferred.js';
|
|
8
|
+
import { requestGitHub } from './github-api.js';
|
|
9
9
|
import openBrowser from './open-browser.js';
|
|
10
10
|
const SERVER_PORT = 3000;
|
|
11
11
|
const promptForAuthMethod = async () => {
|
|
12
12
|
const authChoiceNetlify = 'Authorize with GitHub through app.netlify.com';
|
|
13
13
|
const authChoiceToken = 'Authorize with a GitHub personal access token';
|
|
14
14
|
const authChoices = [authChoiceNetlify, authChoiceToken];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'What would you like to do?',
|
|
21
|
-
choices: authChoices,
|
|
22
|
-
},
|
|
23
|
-
]);
|
|
15
|
+
const authMethod = await select({
|
|
16
|
+
message: 'Netlify CLI needs access to your GitHub account to configure Webhooks and Deploy Keys. ' +
|
|
17
|
+
'What would you like to do?',
|
|
18
|
+
choices: authChoices,
|
|
19
|
+
});
|
|
24
20
|
return authMethod === authChoiceNetlify;
|
|
25
21
|
};
|
|
26
22
|
/**
|
|
@@ -59,15 +55,8 @@ export const authWithNetlify = async () => {
|
|
|
59
55
|
return deferredPromise;
|
|
60
56
|
};
|
|
61
57
|
const getPersonalAccessToken = async () => {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
type: 'password',
|
|
65
|
-
name: 'token',
|
|
66
|
-
message: 'Your GitHub personal access token:',
|
|
67
|
-
filter: (input) => input.trim(),
|
|
68
|
-
},
|
|
69
|
-
]);
|
|
70
|
-
return { token };
|
|
58
|
+
const token = await password({ message: 'Your GitHub personal access token:' });
|
|
59
|
+
return { token: token.trim() };
|
|
71
60
|
};
|
|
72
61
|
/**
|
|
73
62
|
* Authenticate with the netlify app
|
|
@@ -77,8 +66,7 @@ const authWithToken = async () => {
|
|
|
77
66
|
if (!token) {
|
|
78
67
|
throw new Error('GitHub authentication failed');
|
|
79
68
|
}
|
|
80
|
-
const
|
|
81
|
-
const { data: { login: user }, } = await octokit.users.getAuthenticated();
|
|
69
|
+
const { login: user } = await requestGitHub(token, 'GET', '/user');
|
|
82
70
|
return { token, user, provider: 'github' };
|
|
83
71
|
};
|
|
84
72
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare class GitHubAPIError extends Error {
|
|
2
|
+
status: number;
|
|
3
|
+
constructor(message: string, status: number);
|
|
4
|
+
}
|
|
5
|
+
export interface GitHubUser {
|
|
6
|
+
login: string;
|
|
7
|
+
}
|
|
8
|
+
export interface GitHubRepo {
|
|
9
|
+
id: number;
|
|
10
|
+
full_name: string;
|
|
11
|
+
default_branch: string;
|
|
12
|
+
}
|
|
13
|
+
export interface GitHubWebhook {
|
|
14
|
+
config: {
|
|
15
|
+
url?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare const requestGitHub: <T>(token: string, method: string, path: string, { query, body }?: {
|
|
19
|
+
query?: Record<string, string | number>;
|
|
20
|
+
body?: unknown;
|
|
21
|
+
}) => Promise<T>;
|
|
22
|
+
//# sourceMappingURL=github-api.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const GITHUB_API_URL = 'https://api.github.com';
|
|
2
|
+
export class GitHubAPIError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
// Keeps error output identical to what Octokit printed before it was replaced.
|
|
7
|
+
this.name = 'HttpError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// Mirrors Octokit's message format, which callers rely on to detect specific validation errors.
|
|
12
|
+
const readErrorMessage = async (response) => {
|
|
13
|
+
const text = await response.text();
|
|
14
|
+
let body;
|
|
15
|
+
try {
|
|
16
|
+
body = JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return text || response.statusText;
|
|
20
|
+
}
|
|
21
|
+
const message = body.message ?? response.statusText;
|
|
22
|
+
const details = body.errors?.length ? `: ${body.errors.map((error) => JSON.stringify(error)).join(', ')}` : '';
|
|
23
|
+
const suffix = body.documentation_url ? ` - ${body.documentation_url}` : '';
|
|
24
|
+
return `${message}${details}${suffix}`;
|
|
25
|
+
};
|
|
26
|
+
export const requestGitHub = async (token, method, path, { query, body } = {}) => {
|
|
27
|
+
const url = new URL(path, GITHUB_API_URL);
|
|
28
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
29
|
+
url.searchParams.set(key, String(value));
|
|
30
|
+
}
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
method,
|
|
33
|
+
headers: {
|
|
34
|
+
Accept: 'application/vnd.github.v3+json',
|
|
35
|
+
Authorization: `token ${token}`,
|
|
36
|
+
'User-Agent': 'netlify-cli',
|
|
37
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }),
|
|
38
|
+
},
|
|
39
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new GitHubAPIError(await readErrorMessage(response), response.status);
|
|
43
|
+
}
|
|
44
|
+
const text = await response.text();
|
|
45
|
+
return (text ? JSON.parse(text) : undefined);
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=github-api.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Octokit } from '@octokit/rest';
|
|
2
1
|
import { chalk, logAndThrowError, log } from '../command-helpers.js';
|
|
3
2
|
import { getGitHubToken as ghauth } from '../gh-auth.js';
|
|
3
|
+
import { requestGitHub } from '../github-api.js';
|
|
4
4
|
import { createDeployKey, formatErrorMessage, getBuildSettings, saveNetlifyToml, setupSite } from './utils.js';
|
|
5
5
|
const formatRepoAndOwner = ({ repoName, repoOwner }) => ({
|
|
6
6
|
name: chalk.magenta(repoName),
|
|
@@ -15,11 +15,8 @@ export const getGitHubToken = async ({ globalConfig }) => {
|
|
|
15
15
|
const githubToken = globalConfig.get(`users.${userId}.auth.github`);
|
|
16
16
|
if (githubToken?.user && githubToken.token) {
|
|
17
17
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (status < 400) {
|
|
21
|
-
return githubToken.token;
|
|
22
|
-
}
|
|
18
|
+
await requestGitHub(githubToken.token, 'GET', '/user');
|
|
19
|
+
return githubToken.token;
|
|
23
20
|
}
|
|
24
21
|
catch {
|
|
25
22
|
log(chalk.yellow('Token is expired or invalid!'));
|
|
@@ -30,19 +27,16 @@ export const getGitHubToken = async ({ globalConfig }) => {
|
|
|
30
27
|
globalConfig.set(`users.${userId}.auth.github`, newToken);
|
|
31
28
|
return newToken.token;
|
|
32
29
|
};
|
|
33
|
-
const
|
|
34
|
-
auth: `token ${token}`,
|
|
35
|
-
});
|
|
36
|
-
const addDeployKey = async ({ api, octokit, repoName, repoOwner, }) => {
|
|
30
|
+
const addDeployKey = async ({ api, repoName, repoOwner, token, }) => {
|
|
37
31
|
log('Adding deploy key to repository...');
|
|
38
32
|
const key = await createDeployKey({ api });
|
|
39
33
|
try {
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
await requestGitHub(token, 'POST', `/repos/${repoOwner}/${repoName}/keys`, {
|
|
35
|
+
body: {
|
|
36
|
+
title: 'Netlify Deploy Key',
|
|
37
|
+
key: key.public_key ?? '',
|
|
38
|
+
read_only: true,
|
|
39
|
+
},
|
|
46
40
|
});
|
|
47
41
|
log('Deploy key added!');
|
|
48
42
|
return key;
|
|
@@ -57,13 +51,9 @@ const addDeployKey = async ({ api, octokit, repoName, repoOwner, }) => {
|
|
|
57
51
|
return logAndThrowError(message);
|
|
58
52
|
}
|
|
59
53
|
};
|
|
60
|
-
const getGitHubRepo = async ({
|
|
54
|
+
const getGitHubRepo = async ({ repoName, repoOwner, token, }) => {
|
|
61
55
|
try {
|
|
62
|
-
|
|
63
|
-
owner: repoOwner,
|
|
64
|
-
repo: repoName,
|
|
65
|
-
});
|
|
66
|
-
return data;
|
|
56
|
+
return await requestGitHub(token, 'GET', `/repos/${repoOwner}/${repoName}`);
|
|
67
57
|
}
|
|
68
58
|
catch (error) {
|
|
69
59
|
let message = formatErrorMessage({ message: 'Failed retrieving GitHub repository information', error });
|
|
@@ -75,15 +65,11 @@ const getGitHubRepo = async ({ octokit, repoName, repoOwner, }) => {
|
|
|
75
65
|
return logAndThrowError(message);
|
|
76
66
|
}
|
|
77
67
|
};
|
|
78
|
-
|
|
79
|
-
const hookExists = async ({ deployHook, octokit, repoName, repoOwner }) => {
|
|
68
|
+
const hookExists = async ({ deployHook, repoName, repoOwner, token }) => {
|
|
80
69
|
try {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
repo: repoName,
|
|
84
|
-
per_page: PAGE_SIZE,
|
|
70
|
+
const hooks = await requestGitHub(token, 'GET', `/repos/${repoOwner}/${repoName}/hooks`, {
|
|
71
|
+
query: { per_page: PAGE_SIZE },
|
|
85
72
|
});
|
|
86
|
-
// @ts-expect-error TS(7006) FIXME: Parameter 'hook' implicitly has an 'any' type.
|
|
87
73
|
const exists = hooks.some((hook) => hook.config.url === deployHook);
|
|
88
74
|
return exists;
|
|
89
75
|
}
|
|
@@ -92,21 +78,20 @@ const hookExists = async ({ deployHook, octokit, repoName, repoOwner }) => {
|
|
|
92
78
|
return false;
|
|
93
79
|
}
|
|
94
80
|
};
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const exists = await hookExists({ deployHook, octokit, repoOwner, repoName });
|
|
81
|
+
const addDeployHook = async ({ deployHook, repoName, repoOwner, token }) => {
|
|
82
|
+
const exists = await hookExists({ deployHook, repoOwner, repoName, token });
|
|
98
83
|
if (!exists) {
|
|
99
84
|
try {
|
|
100
|
-
await
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
85
|
+
await requestGitHub(token, 'POST', `/repos/${repoOwner}/${repoName}/hooks`, {
|
|
86
|
+
body: {
|
|
87
|
+
name: 'web',
|
|
88
|
+
config: {
|
|
89
|
+
url: deployHook,
|
|
90
|
+
content_type: 'json',
|
|
91
|
+
},
|
|
92
|
+
events: ['push', 'pull_request', 'delete'],
|
|
93
|
+
active: true,
|
|
107
94
|
},
|
|
108
|
-
events: ['push', 'pull_request', 'delete'],
|
|
109
|
-
active: true,
|
|
110
95
|
});
|
|
111
96
|
}
|
|
112
97
|
catch (error) {
|
|
@@ -183,10 +168,9 @@ export const configGithub = async ({ command, repoName, repoOwner, siteId, }) =>
|
|
|
183
168
|
});
|
|
184
169
|
await saveNetlifyToml({ repositoryRoot, config, configPath, baseDir, buildCmd, buildDir, functionsDir });
|
|
185
170
|
log();
|
|
186
|
-
const octokit = getGitHubClient(token);
|
|
187
171
|
const [deployKey, githubRepo] = await Promise.all([
|
|
188
|
-
addDeployKey({ api,
|
|
189
|
-
getGitHubRepo({
|
|
172
|
+
addDeployKey({ api, repoOwner, repoName, token }),
|
|
173
|
+
getGitHubRepo({ repoOwner, repoName, token }),
|
|
190
174
|
]);
|
|
191
175
|
const repo = {
|
|
192
176
|
id: githubRepo.id,
|
|
@@ -207,7 +191,7 @@ export const configGithub = async ({ command, repoName, repoOwner, siteId, }) =>
|
|
|
207
191
|
configPlugins: config.plugins ?? [],
|
|
208
192
|
pluginsToInstall,
|
|
209
193
|
});
|
|
210
|
-
await addDeployHook({ deployHook: updatedSite.deploy_hook,
|
|
194
|
+
await addDeployHook({ deployHook: updatedSite.deploy_hook, repoOwner, repoName, token });
|
|
211
195
|
log();
|
|
212
196
|
await addNotificationHooks({ siteId, api, token });
|
|
213
197
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
2
|
import { exit, log } from '../command-helpers.js';
|
|
3
3
|
import { createDeployKey, getBuildSettings, saveNetlifyToml, setupSite } from './utils.js';
|
|
4
4
|
/**
|
|
@@ -9,44 +9,24 @@ const addDeployKey = async (deployKey) => {
|
|
|
9
9
|
// FIXME(serhalp): Handle nullish `deployKey.public_key` by throwing user-facing error or fixing upstream type.
|
|
10
10
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
11
11
|
log(`\n${deployKey.public_key}\n\n`);
|
|
12
|
-
const
|
|
13
|
-
{
|
|
14
|
-
type: 'confirm',
|
|
15
|
-
name: 'sshKeyAdded',
|
|
16
|
-
message: 'Continue?',
|
|
17
|
-
default: true,
|
|
18
|
-
},
|
|
19
|
-
]));
|
|
12
|
+
const sshKeyAdded = await confirm({ message: 'Continue?', default: true });
|
|
20
13
|
if (!sshKeyAdded) {
|
|
21
14
|
return exit();
|
|
22
15
|
}
|
|
23
16
|
};
|
|
24
17
|
const getRepoPath = async ({ repoData }) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
default: repoData.url,
|
|
31
|
-
validate: (url) => (SSH_URL_REGEXP.test(url) ? true : 'The URL provided does not use the SSH protocol'),
|
|
32
|
-
},
|
|
33
|
-
]);
|
|
34
|
-
return repoPath;
|
|
18
|
+
return await input({
|
|
19
|
+
message: 'The SSH URL of the remote git repo:',
|
|
20
|
+
default: repoData.url,
|
|
21
|
+
validate: (url) => (SSH_URL_REGEXP.test(url) ? true : 'The URL provided does not use the SSH protocol'),
|
|
22
|
+
});
|
|
35
23
|
};
|
|
36
24
|
const addDeployHook = async (deployHook) => {
|
|
37
25
|
log('\nConfigure the following webhook for your repository:\n');
|
|
38
26
|
// FIXME(serhalp): Handle nullish `deployHook` by throwing user-facing error or fixing upstream type.
|
|
39
27
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
40
28
|
log(`\n${deployHook}\n\n`);
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
type: 'confirm',
|
|
44
|
-
name: 'deployHookAdded',
|
|
45
|
-
message: 'Continue?',
|
|
46
|
-
default: true,
|
|
47
|
-
},
|
|
48
|
-
]);
|
|
49
|
-
return deployHookAdded;
|
|
29
|
+
return await confirm({ message: 'Continue?', default: true });
|
|
50
30
|
};
|
|
51
31
|
const isSupportedProvider = (provider) => provider === 'github' || provider === 'gitlab';
|
|
52
32
|
export default async function configManual({ command, repoData, siteId, }) {
|