@mintlify/cli 4.0.1725 → 4.0.1727
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/__test__/deploy-github-actions.test.ts +202 -0
- package/__test__/deploy-github-setup.test.ts +239 -0
- package/bin/agent-deployment.js +22 -0
- package/bin/cli.js +34 -0
- package/bin/deploy-github-actions.js +102 -0
- package/bin/deploy-github-setup.js +165 -0
- package/bin/deploy-instructions.js +7 -4
- package/bin/deploy.js +7 -4
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/agent-deployment.ts +35 -0
- package/src/cli.tsx +50 -0
- package/src/deploy-github-actions.ts +139 -0
- package/src/deploy-github-setup.ts +189 -0
- package/src/deploy-instructions.ts +8 -4
- package/src/deploy.ts +9 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintlify/cli",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1727",
|
|
4
4
|
"description": "The Mintlify CLI",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@inquirer/prompts": "7.9.0",
|
|
48
|
-
"@mintlify/common": "1.0.
|
|
49
|
-
"@mintlify/link-rot": "3.0.
|
|
48
|
+
"@mintlify/common": "1.0.1302",
|
|
49
|
+
"@mintlify/link-rot": "3.0.1529",
|
|
50
50
|
"@mintlify/models": "0.0.378",
|
|
51
|
-
"@mintlify/og": "0.0.
|
|
52
|
-
"@mintlify/prebuild": "1.0.
|
|
53
|
-
"@mintlify/previewing": "4.0.
|
|
51
|
+
"@mintlify/og": "0.0.101",
|
|
52
|
+
"@mintlify/prebuild": "1.0.1468",
|
|
53
|
+
"@mintlify/previewing": "4.0.1547",
|
|
54
54
|
"@mintlify/validation": "0.1.943",
|
|
55
55
|
"adm-zip": "0.6.0",
|
|
56
56
|
"chalk": "5.2.0",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"keytar": "7.9.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
|
-
"@mintlify/editor": "0.0.
|
|
85
|
+
"@mintlify/editor": "0.0.572",
|
|
86
86
|
"@mintlify/ts-config": "2.0.2",
|
|
87
87
|
"@tsconfig/recommended": "1.0.2",
|
|
88
88
|
"@types/detect-port": "1.3.2",
|
|
@@ -101,5 +101,5 @@
|
|
|
101
101
|
"vitest": "2.1.9",
|
|
102
102
|
"vitest-mock-process": "1.0.4"
|
|
103
103
|
},
|
|
104
|
-
"gitHead": "
|
|
104
|
+
"gitHead": "0acd59c1a76613a44dab176301efe476263b8f51"
|
|
105
105
|
}
|
package/src/agent-deployment.ts
CHANGED
|
@@ -34,6 +34,13 @@ const writeTokenSchema = z.object({
|
|
|
34
34
|
tokenExpiresAt: z.iso.datetime(),
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
const trustedReposSchema = z.object({
|
|
38
|
+
subdomain: z.string().min(1),
|
|
39
|
+
trustedRepos: z.array(
|
|
40
|
+
z.object({ repositoryId: z.string(), repository: z.string(), branch: z.string() })
|
|
41
|
+
),
|
|
42
|
+
});
|
|
43
|
+
|
|
37
44
|
export type AgentSite = z.infer<typeof readySchema>;
|
|
38
45
|
export type AgentWriteToken = z.infer<typeof writeTokenSchema>;
|
|
39
46
|
export type AgentSiteSettings = { subdomain: string; orgName?: string };
|
|
@@ -73,3 +80,31 @@ export async function getAgentWriteToken(
|
|
|
73
80
|
writeTokenSchema
|
|
74
81
|
);
|
|
75
82
|
}
|
|
83
|
+
|
|
84
|
+
export async function getGithubActionsWriteToken(
|
|
85
|
+
apiUrl: string,
|
|
86
|
+
oidcToken: string,
|
|
87
|
+
subdomain: string
|
|
88
|
+
): Promise<AgentWriteToken> {
|
|
89
|
+
return agentResponse(
|
|
90
|
+
await agentRequest(`${apiUrl}/api/agent/deployments/github-actions/token`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { Authorization: `Bearer ${oidcToken}`, 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ subdomain }),
|
|
94
|
+
}),
|
|
95
|
+
writeTokenSchema
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type TrustedRepo = { repositoryId: string; repository: string; branch: string };
|
|
100
|
+
|
|
101
|
+
export async function trustGithubRepo(apiUrl: string, accessToken: string, repo: TrustedRepo) {
|
|
102
|
+
return agentResponse(
|
|
103
|
+
await agentRequest(`${apiUrl}/api/agent/deployments/trusted-repos`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify(repo),
|
|
107
|
+
}),
|
|
108
|
+
trustedReposSchema
|
|
109
|
+
);
|
|
110
|
+
}
|
package/src/cli.tsx
CHANGED
|
@@ -24,6 +24,8 @@ import { comingSoon } from './comingSoon.js';
|
|
|
24
24
|
import { setTelemetryEnabled } from './config.js';
|
|
25
25
|
import { getConfigValue, setConfigValue, clearConfigValue } from './config.js';
|
|
26
26
|
import { API_URL } from './constants.js';
|
|
27
|
+
import { deployFromGithubActions, isGithubActions } from './deploy-github-actions.js';
|
|
28
|
+
import { formatGithubSetup, setupGithubDeployment } from './deploy-github-setup.js';
|
|
27
29
|
import { DEPLOY_HELP, deploymentInstructions } from './deploy-instructions.js';
|
|
28
30
|
import { formatDeployment, prepareAgentDeployment } from './deploy.js';
|
|
29
31
|
import { formatHandler } from './format.js';
|
|
@@ -175,15 +177,63 @@ export const cli = ({ packageName = 'mint' }: { packageName?: string }) => {
|
|
|
175
177
|
type: 'string',
|
|
176
178
|
description: 'User email for an optional claim link',
|
|
177
179
|
})
|
|
180
|
+
.option('github', {
|
|
181
|
+
type: 'boolean',
|
|
182
|
+
default: false,
|
|
183
|
+
description:
|
|
184
|
+
'Trust this GitHub repository to deploy the site and write a GitHub Actions workflow',
|
|
185
|
+
})
|
|
186
|
+
.option('force', {
|
|
187
|
+
type: 'boolean',
|
|
188
|
+
default: false,
|
|
189
|
+
description: 'With --github, overwrite an existing .github/workflows/mintlify.yml',
|
|
190
|
+
})
|
|
191
|
+
.conflicts('github', 'email')
|
|
178
192
|
.example('$0 deploy --subdomain my-docs', 'Provision a site without signing in')
|
|
179
193
|
.example(
|
|
180
194
|
'$0 deploy --email you@example.com',
|
|
181
195
|
'Get repository access and a claim link for the current site'
|
|
182
196
|
)
|
|
183
197
|
.example('$0 deploy --new --subdomain another-site', 'Create another anonymous site')
|
|
198
|
+
.example(
|
|
199
|
+
'$0 deploy --github --subdomain my-docs',
|
|
200
|
+
'Deploy the site from GitHub Actions on every push to the default branch'
|
|
201
|
+
)
|
|
202
|
+
.example(
|
|
203
|
+
'$0 deploy --subdomain my-docs',
|
|
204
|
+
'In GitHub Actions with id-token: write, push the current commit to a trusted site'
|
|
205
|
+
)
|
|
184
206
|
.epilog(DEPLOY_HELP),
|
|
185
207
|
async (argv) => {
|
|
186
208
|
try {
|
|
209
|
+
if (isGithubActions()) {
|
|
210
|
+
const siteUrl = await deployFromGithubActions(
|
|
211
|
+
{ apiUrl: API_URL, directory: argv.directory, subdomain: argv.subdomain },
|
|
212
|
+
(message) => process.stderr.write(`${message}\n`)
|
|
213
|
+
);
|
|
214
|
+
process.stdout.write(
|
|
215
|
+
`Pushed to ${argv.subdomain}; the build starts now.\nSite: ${siteUrl}\n`
|
|
216
|
+
);
|
|
217
|
+
await terminate(0);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (argv.github) {
|
|
221
|
+
const setup = await setupGithubDeployment(
|
|
222
|
+
{
|
|
223
|
+
apiUrl: API_URL,
|
|
224
|
+
directory: argv.directory,
|
|
225
|
+
subdomain: argv.subdomain,
|
|
226
|
+
orgName: argv.orgName,
|
|
227
|
+
newSite: argv.new,
|
|
228
|
+
force: argv.force,
|
|
229
|
+
},
|
|
230
|
+
(message) => process.stderr.write(`Warning: ${message}\n`),
|
|
231
|
+
(message) => process.stderr.write(`${message}\n`)
|
|
232
|
+
);
|
|
233
|
+
process.stdout.write(`${formatGithubSetup(setup)}\n`);
|
|
234
|
+
await terminate(0);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
187
237
|
const result = await prepareAgentDeployment(
|
|
188
238
|
{
|
|
189
239
|
apiUrl: API_URL,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { getGithubActionsWriteToken, type AgentWriteToken } from './agent-deployment.js';
|
|
6
|
+
import { AgentApiError } from './agent-http.js';
|
|
7
|
+
import { agentSiteUrl } from './deploy.js';
|
|
8
|
+
|
|
9
|
+
const oidcSchema = z.object({ value: z.string().min(1) });
|
|
10
|
+
|
|
11
|
+
export type GithubActionsDeployOptions = {
|
|
12
|
+
apiUrl: string;
|
|
13
|
+
directory: string;
|
|
14
|
+
subdomain?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function isGithubActions(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
18
|
+
return env.GITHUB_ACTIONS === 'true';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function getGithubOidcToken(env: NodeJS.ProcessEnv, audience: string): Promise<string> {
|
|
22
|
+
const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
23
|
+
const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
24
|
+
if (!requestUrl || !requestToken) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'GitHub Actions did not provide an OIDC token. Add `permissions: id-token: write` to the workflow or job.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
let response: Response;
|
|
30
|
+
try {
|
|
31
|
+
response = await fetch(`${requestUrl}&audience=${encodeURIComponent(audience)}`, {
|
|
32
|
+
headers: { Authorization: `Bearer ${requestToken}` },
|
|
33
|
+
signal: AbortSignal.timeout(30_000),
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error('Could not request a GitHub Actions OIDC token. Retry the workflow.');
|
|
37
|
+
}
|
|
38
|
+
const parsed = oidcSchema.safeParse(await response.json().catch(() => null));
|
|
39
|
+
if (!response.ok || !parsed.success) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Could not get a GitHub Actions OIDC token (HTTP ${response.status}). Check that the workflow has \`permissions: id-token: write\`.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return parsed.data.value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function git(args: string[], cwd: string, env: NodeJS.ProcessEnv): Promise<string> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn('git', args, { cwd, env, stdio: ['ignore', 'pipe', 'inherit'] });
|
|
50
|
+
let out = '';
|
|
51
|
+
child.stdout.on('data', (chunk: Buffer) => (out += chunk.toString()));
|
|
52
|
+
child.on('error', reject);
|
|
53
|
+
child.on('close', (code) =>
|
|
54
|
+
code === 0 ? resolve(out.trim()) : reject(new Error(`git ${args[0]} failed`))
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function commitDirectory(directory: string, env: NodeJS.ProcessEnv): Promise<string> {
|
|
60
|
+
const prefix = await git(['rev-parse', '--show-prefix'], directory, env);
|
|
61
|
+
const tree = await git(['rev-parse', `HEAD:${prefix}`], directory, env);
|
|
62
|
+
const [name = '', email = '', date = '', sha = '', subject = ''] = (
|
|
63
|
+
await git(['log', '-1', '--format=%an%n%ae%n%aI%n%H%n%s'], directory, env)
|
|
64
|
+
).split('\n');
|
|
65
|
+
const repository = env.GITHUB_REPOSITORY ? `${env.GITHUB_REPOSITORY}@` : '';
|
|
66
|
+
return git(
|
|
67
|
+
['commit-tree', tree, '-m', subject, '-m', `Deployed from ${repository}${sha}`],
|
|
68
|
+
directory,
|
|
69
|
+
{
|
|
70
|
+
...env,
|
|
71
|
+
GIT_AUTHOR_NAME: name,
|
|
72
|
+
GIT_AUTHOR_EMAIL: email,
|
|
73
|
+
GIT_AUTHOR_DATE: date,
|
|
74
|
+
GIT_COMMITTER_NAME: name,
|
|
75
|
+
GIT_COMMITTER_EMAIL: email,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pushCommit(
|
|
81
|
+
directory: string,
|
|
82
|
+
commit: string,
|
|
83
|
+
write: AgentWriteToken,
|
|
84
|
+
env: NodeJS.ProcessEnv
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const index = Number(env.GIT_CONFIG_COUNT) || 0;
|
|
87
|
+
const credentials = Buffer.from(`x-access-token:${write.token}`).toString('base64');
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const child = spawn(
|
|
90
|
+
'git',
|
|
91
|
+
['push', '--force', write.remote, `${commit}:refs/heads/${write.branch}`],
|
|
92
|
+
{
|
|
93
|
+
cwd: directory,
|
|
94
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
95
|
+
env: {
|
|
96
|
+
...env,
|
|
97
|
+
GIT_CONFIG_COUNT: String(index + 1),
|
|
98
|
+
[`GIT_CONFIG_KEY_${index}`]: `http.${write.remote}.extraHeader`,
|
|
99
|
+
[`GIT_CONFIG_VALUE_${index}`]: `Authorization: Basic ${credentials}`,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
const failed = () => reject(new Error('git push to the site repository failed.'));
|
|
104
|
+
child.on('error', failed);
|
|
105
|
+
child.on('close', (code) => (code === 0 ? resolve() : failed()));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function deployFromGithubActions(
|
|
110
|
+
options: GithubActionsDeployOptions,
|
|
111
|
+
progress: (message: string) => void = () => {},
|
|
112
|
+
env: NodeJS.ProcessEnv = process.env
|
|
113
|
+
): Promise<string> {
|
|
114
|
+
const { apiUrl, subdomain } = options;
|
|
115
|
+
if (!subdomain) throw new Error('Pass the site name with --subdomain <name>.');
|
|
116
|
+
const directory = path.resolve(options.directory);
|
|
117
|
+
const commit = await commitDirectory(directory, env).catch(() => {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Could not read ${options.directory} from the checked-out commit. Run mint deploy after actions/checkout, with --directory inside the repository.`
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
progress('Requesting a GitHub Actions identity token...');
|
|
123
|
+
const oidcToken = await getGithubOidcToken(env, apiUrl);
|
|
124
|
+
progress('Getting repository write access...');
|
|
125
|
+
let write: AgentWriteToken;
|
|
126
|
+
try {
|
|
127
|
+
write = await getGithubActionsWriteToken(apiUrl, oidcToken, subdomain);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof AgentApiError && error.status === 403) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`This repository/branch isn't trusted to deploy ${subdomain}. Run \`mint deploy --github\` locally from this repository to trust it.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
progress('Pushing to the site repository...');
|
|
137
|
+
await pushCommit(directory, commit, write, env);
|
|
138
|
+
return agentSiteUrl(apiUrl, subdomain);
|
|
139
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import { trustGithubRepo } from './agent-deployment.js';
|
|
7
|
+
import { CLAIM_INSTRUCTIONS } from './deploy-instructions.js';
|
|
8
|
+
import { agentSiteUrl, prepareAgentDeployment, type DeployOptions } from './deploy.js';
|
|
9
|
+
|
|
10
|
+
const githubRepoSchema = z.object({
|
|
11
|
+
id: z.number().int().positive(),
|
|
12
|
+
full_name: z.string().regex(/^[\w.-]+\/[\w.-]+$/),
|
|
13
|
+
default_branch: z.string().min(1),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type GithubRepo = z.infer<typeof githubRepoSchema>;
|
|
17
|
+
|
|
18
|
+
export const GITHUB_WORKFLOW_PATH = path.join('.github', 'workflows', 'mintlify.yml');
|
|
19
|
+
|
|
20
|
+
function run(command: string, args: string[], cwd: string): Promise<string> {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
execFile(command, args, { cwd }, (error, stdout) =>
|
|
23
|
+
error ? reject(error) : resolve(String(stdout).trim())
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseGithubRemote(remote: string): { owner: string; repo: string } | null {
|
|
29
|
+
const match = remote
|
|
30
|
+
.trim()
|
|
31
|
+
.match(
|
|
32
|
+
/^(?:https:\/\/(?:[^@/]+@)?github\.com\/|ssh:\/\/git@github\.com(?::\d+)?\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/
|
|
33
|
+
);
|
|
34
|
+
return match?.[1] && match[2] ? { owner: match[1], repo: match[2] } : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function githubWorkflow(branch: string, subdomain: string, docsPath = ''): string {
|
|
38
|
+
const directory = /^[\w./-]+$/.test(docsPath) ? docsPath : JSON.stringify(docsPath);
|
|
39
|
+
return [
|
|
40
|
+
'name: Deploy to Mintlify',
|
|
41
|
+
'on:',
|
|
42
|
+
' push:',
|
|
43
|
+
` branches: [${JSON.stringify(branch)}]`,
|
|
44
|
+
...(docsPath
|
|
45
|
+
? [` paths: [${JSON.stringify(`${docsPath}/**`)}, ".github/workflows/mintlify.yml"]`]
|
|
46
|
+
: []),
|
|
47
|
+
'permissions:',
|
|
48
|
+
' id-token: write',
|
|
49
|
+
' contents: read',
|
|
50
|
+
'concurrency:',
|
|
51
|
+
' group: mintlify-deploy',
|
|
52
|
+
' cancel-in-progress: true',
|
|
53
|
+
'jobs:',
|
|
54
|
+
' deploy:',
|
|
55
|
+
' runs-on: ubuntu-latest',
|
|
56
|
+
' steps:',
|
|
57
|
+
' - uses: actions/checkout@v4',
|
|
58
|
+
` - run: npx mint@latest deploy --subdomain ${subdomain}${docsPath ? ` --directory ${directory}` : ''}`,
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readFileIfExists(file: string): Promise<string | undefined> {
|
|
64
|
+
return fs.readFile(file, 'utf8').catch((error: NodeJS.ErrnoException) => {
|
|
65
|
+
if (error.code === 'ENOENT') return undefined;
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function writeGithubWorkflow(
|
|
71
|
+
root: string,
|
|
72
|
+
contents: string,
|
|
73
|
+
force = false
|
|
74
|
+
): Promise<'created' | 'unchanged' | 'updated'> {
|
|
75
|
+
const file = path.join(root, GITHUB_WORKFLOW_PATH);
|
|
76
|
+
const existing = await readFileIfExists(file);
|
|
77
|
+
if (existing === contents) return 'unchanged';
|
|
78
|
+
if (existing !== undefined && !force) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${GITHUB_WORKFLOW_PATH} already exists with different contents. Pass --force to overwrite it.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
84
|
+
await fs.writeFile(file, contents);
|
|
85
|
+
return existing === undefined ? 'created' : 'updated';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function findGitRoot(directory: string): Promise<string> {
|
|
89
|
+
try {
|
|
90
|
+
return await run('git', ['rev-parse', '--show-toplevel'], directory);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error('--github requires a Git repository. Run it inside your docs repository.');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function resolveGithubRepo(root: string): Promise<GithubRepo> {
|
|
97
|
+
const remote = await run('git', ['remote', 'get-url', 'origin'], root).catch(() => {
|
|
98
|
+
throw new Error('This repository has no origin remote. Add a github.com origin and retry.');
|
|
99
|
+
});
|
|
100
|
+
const parsed = parseGithubRemote(remote);
|
|
101
|
+
if (!parsed) throw new Error(`The origin remote (${remote}) is not a github.com repository.`);
|
|
102
|
+
const output = await run(
|
|
103
|
+
'gh',
|
|
104
|
+
[
|
|
105
|
+
'api',
|
|
106
|
+
`repos/${parsed.owner}/${parsed.repo}`,
|
|
107
|
+
'--jq',
|
|
108
|
+
'{id: .id, full_name: .full_name, default_branch: .default_branch}',
|
|
109
|
+
],
|
|
110
|
+
root
|
|
111
|
+
).catch((error: NodeJS.ErrnoException) => {
|
|
112
|
+
if (error.code === 'ENOENT')
|
|
113
|
+
throw new Error('The GitHub CLI (gh) is required. Install it and run `gh auth login`.');
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Could not read ${parsed.owner}/${parsed.repo} with gh. Run \`gh auth login\` with access to the repository and retry.`
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
return githubRepoSchema.parse(JSON.parse(output));
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error('gh returned an unexpected repository response.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type GithubSetupResult = {
|
|
126
|
+
siteUrl: string;
|
|
127
|
+
subdomain: string;
|
|
128
|
+
repository: string;
|
|
129
|
+
branch: string;
|
|
130
|
+
workflowPath: string;
|
|
131
|
+
workflow: 'created' | 'unchanged' | 'updated';
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export async function setupGithubDeployment(
|
|
135
|
+
options: DeployOptions & { force?: boolean },
|
|
136
|
+
warn: (message: string) => void,
|
|
137
|
+
progress: (message: string) => void = () => {}
|
|
138
|
+
): Promise<GithubSetupResult> {
|
|
139
|
+
const directory = path.resolve(options.directory);
|
|
140
|
+
const root = await findGitRoot(directory);
|
|
141
|
+
const [realRoot, realDirectory] = await Promise.all([fs.realpath(root), fs.realpath(directory)]);
|
|
142
|
+
const docsPath = path.relative(realRoot, realDirectory).split(path.sep).join('/');
|
|
143
|
+
if (!(await readFileIfExists(path.join(directory, 'docs.json')))) {
|
|
144
|
+
throw new Error(`No docs.json in ${directory}. Pass the docs folder with --directory.`);
|
|
145
|
+
}
|
|
146
|
+
progress('Resolving the GitHub repository...');
|
|
147
|
+
const repo = await resolveGithubRepo(root);
|
|
148
|
+
const result = await prepareAgentDeployment(options, warn, progress);
|
|
149
|
+
if (result.status === 'claimed') {
|
|
150
|
+
throw new Error(
|
|
151
|
+
'This site has been claimed. Setting up GitHub deploys for claimed sites is not supported yet; connect the repository from the Mintlify dashboard.'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const accessToken = result.credentials.accessToken;
|
|
155
|
+
if (!accessToken) throw new Error('Agent authentication did not return an access token.');
|
|
156
|
+
progress(`Trusting ${repo.full_name}@${repo.default_branch} to deploy...`);
|
|
157
|
+
await trustGithubRepo(options.apiUrl, accessToken, {
|
|
158
|
+
repositoryId: String(repo.id),
|
|
159
|
+
repository: repo.full_name,
|
|
160
|
+
branch: repo.default_branch,
|
|
161
|
+
});
|
|
162
|
+
const workflow = await writeGithubWorkflow(
|
|
163
|
+
root,
|
|
164
|
+
githubWorkflow(repo.default_branch, result.site.subdomain, docsPath),
|
|
165
|
+
options.force
|
|
166
|
+
);
|
|
167
|
+
return {
|
|
168
|
+
siteUrl: agentSiteUrl(options.apiUrl, result.site.subdomain),
|
|
169
|
+
subdomain: result.site.subdomain,
|
|
170
|
+
repository: repo.full_name,
|
|
171
|
+
branch: repo.default_branch,
|
|
172
|
+
workflowPath: GITHUB_WORKFLOW_PATH,
|
|
173
|
+
workflow,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function formatGithubSetup(result: GithubSetupResult): string {
|
|
178
|
+
return [
|
|
179
|
+
`Site: ${result.siteUrl}`,
|
|
180
|
+
`Trusted repository: ${result.repository} (branch ${result.branch})`,
|
|
181
|
+
`Workflow: ${result.workflowPath} (${result.workflow})`,
|
|
182
|
+
'',
|
|
183
|
+
'Next steps:',
|
|
184
|
+
`1. Commit and push ${result.workflowPath} to ${result.branch}.`,
|
|
185
|
+
`2. Every push to ${result.branch} then deploys ${result.subdomain} through GitHub Actions. No secrets are stored in the repository.`,
|
|
186
|
+
'',
|
|
187
|
+
...CLAIM_INSTRUCTIONS,
|
|
188
|
+
].join('\n');
|
|
189
|
+
}
|
|
@@ -8,8 +8,15 @@ export const DEPLOY_HELP = [
|
|
|
8
8
|
'Pass --email to obtain a claim link for the user. If it is absent, ask the user for their email and rerun with --email from the same directory. Never invent an email address.',
|
|
9
9
|
'To claim an existing site without provisioning, run mint claim --email <email>. Show the link to the user and ask them to return the displayed code. Then run mint claim --code <code>. Use the original documentation directory for both commands, or pass --directory.',
|
|
10
10
|
'The output contains a short-lived write token. Do not paste it into public logs or save it in a Git remote.',
|
|
11
|
+
'Pass --github from the root of a GitHub repository to deploy on every push: it trusts the repository (via the gh CLI and the origin remote) to deploy the site and writes .github/workflows/mintlify.yml. Commit and push that workflow. Pass --directory to deploy a docs subdirectory of the repository as the site root. Use --force to overwrite a different existing workflow.',
|
|
12
|
+
'In GitHub Actions (GITHUB_ACTIONS=true), no agent credentials are used: the workflow needs `permissions: id-token: write`, --subdomain is required, and the current commit is force-pushed to the site from a trusted repository and branch.',
|
|
11
13
|
].join('\n\n');
|
|
12
14
|
|
|
15
|
+
export const CLAIM_INSTRUCTIONS = [
|
|
16
|
+
'Ask the user for the email address they want to use to claim this site. Never invent an email address.',
|
|
17
|
+
'After they provide it, run mint deploy --email <their-email> from the same documentation directory (or repeat --directory). This reuses the site and returns its claim link.',
|
|
18
|
+
];
|
|
19
|
+
|
|
13
20
|
export function deploymentInstructions(result: DeployResult, claimUrl?: string): string {
|
|
14
21
|
if (result.status !== 'ready') return '';
|
|
15
22
|
return [
|
|
@@ -33,9 +40,6 @@ export function deploymentInstructions(result: DeployResult, claimUrl?: string):
|
|
|
33
40
|
'Show this link to the user. Ask them to open it, sign in, and return the displayed code. Do not sign in on their behalf.',
|
|
34
41
|
'After the user returns the code, run mint claim --code <code> from the same documentation directory (or repeat --directory). The CLI completes the claim using its saved credentials.',
|
|
35
42
|
]
|
|
36
|
-
:
|
|
37
|
-
'Ask the user for the email address they want to use to claim this site. Never invent an email address.',
|
|
38
|
-
'After they provide it, run mint deploy --email <their-email> from the same documentation directory (or repeat --directory). This reuses the site and returns its claim link.',
|
|
39
|
-
]),
|
|
43
|
+
: CLAIM_INSTRUCTIONS),
|
|
40
44
|
].join('\n');
|
|
41
45
|
}
|
package/src/deploy.ts
CHANGED
|
@@ -105,15 +105,19 @@ export async function prepareAgentDeployment(
|
|
|
105
105
|
throw new Error('The new anonymous site is already expired. Please retry later.');
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function
|
|
109
|
-
if (result.status === 'claimed')
|
|
110
|
-
return 'This site has been claimed. Manage it in the Mintlify dashboard. Use mint deploy --new --subdomain <name> to create another site.';
|
|
111
|
-
const { site, write } = result;
|
|
108
|
+
export function agentSiteUrl(apiUrl: string, subdomain: string): string {
|
|
112
109
|
const domain =
|
|
113
110
|
new URL(apiUrl).hostname === 'leaves-dev.mintlify.com'
|
|
114
111
|
? 'mint-dev.mintlify.me'
|
|
115
112
|
: 'mintlify.site';
|
|
116
|
-
|
|
113
|
+
return new URL(`https://${subdomain}.${domain}`).toString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatDeployment(result: DeployResult, apiUrl: string): string {
|
|
117
|
+
if (result.status === 'claimed')
|
|
118
|
+
return 'This site has been claimed. Manage it in the Mintlify dashboard. Use mint deploy --new --subdomain <name> to create another site.';
|
|
119
|
+
const { site, write } = result;
|
|
120
|
+
const siteUrl = agentSiteUrl(apiUrl, site.subdomain);
|
|
117
121
|
return [
|
|
118
122
|
result.created
|
|
119
123
|
? 'Repository provisioned; push required to build the site.'
|