@mintlify/cli 4.0.1725 → 4.0.1726
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 +2 -2
- 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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { trustGithubRepo } from './agent-deployment.js';
|
|
15
|
+
import { CLAIM_INSTRUCTIONS } from './deploy-instructions.js';
|
|
16
|
+
import { agentSiteUrl, prepareAgentDeployment } from './deploy.js';
|
|
17
|
+
const githubRepoSchema = z.object({
|
|
18
|
+
id: z.number().int().positive(),
|
|
19
|
+
full_name: z.string().regex(/^[\w.-]+\/[\w.-]+$/),
|
|
20
|
+
default_branch: z.string().min(1),
|
|
21
|
+
});
|
|
22
|
+
export const GITHUB_WORKFLOW_PATH = path.join('.github', 'workflows', 'mintlify.yml');
|
|
23
|
+
function run(command, args, cwd) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
execFile(command, args, { cwd }, (error, stdout) => error ? reject(error) : resolve(String(stdout).trim()));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function parseGithubRemote(remote) {
|
|
29
|
+
const match = remote
|
|
30
|
+
.trim()
|
|
31
|
+
.match(/^(?:https:\/\/(?:[^@/]+@)?github\.com\/|ssh:\/\/git@github\.com(?::\d+)?\/|git@github\.com:)([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/);
|
|
32
|
+
return (match === null || match === void 0 ? void 0 : match[1]) && match[2] ? { owner: match[1], repo: match[2] } : null;
|
|
33
|
+
}
|
|
34
|
+
export function githubWorkflow(branch, subdomain, docsPath = '') {
|
|
35
|
+
const directory = /^[\w./-]+$/.test(docsPath) ? docsPath : JSON.stringify(docsPath);
|
|
36
|
+
return [
|
|
37
|
+
'name: Deploy to Mintlify',
|
|
38
|
+
'on:',
|
|
39
|
+
' push:',
|
|
40
|
+
` branches: [${JSON.stringify(branch)}]`,
|
|
41
|
+
...(docsPath
|
|
42
|
+
? [` paths: [${JSON.stringify(`${docsPath}/**`)}, ".github/workflows/mintlify.yml"]`]
|
|
43
|
+
: []),
|
|
44
|
+
'permissions:',
|
|
45
|
+
' id-token: write',
|
|
46
|
+
' contents: read',
|
|
47
|
+
'concurrency:',
|
|
48
|
+
' group: mintlify-deploy',
|
|
49
|
+
' cancel-in-progress: true',
|
|
50
|
+
'jobs:',
|
|
51
|
+
' deploy:',
|
|
52
|
+
' runs-on: ubuntu-latest',
|
|
53
|
+
' steps:',
|
|
54
|
+
' - uses: actions/checkout@v4',
|
|
55
|
+
` - run: npx mint@latest deploy --subdomain ${subdomain}${docsPath ? ` --directory ${directory}` : ''}`,
|
|
56
|
+
'',
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
function readFileIfExists(file) {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
return fs.readFile(file, 'utf8').catch((error) => {
|
|
62
|
+
if (error.code === 'ENOENT')
|
|
63
|
+
return undefined;
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function writeGithubWorkflow(root_1, contents_1) {
|
|
69
|
+
return __awaiter(this, arguments, void 0, function* (root, contents, force = false) {
|
|
70
|
+
const file = path.join(root, GITHUB_WORKFLOW_PATH);
|
|
71
|
+
const existing = yield readFileIfExists(file);
|
|
72
|
+
if (existing === contents)
|
|
73
|
+
return 'unchanged';
|
|
74
|
+
if (existing !== undefined && !force) {
|
|
75
|
+
throw new Error(`${GITHUB_WORKFLOW_PATH} already exists with different contents. Pass --force to overwrite it.`);
|
|
76
|
+
}
|
|
77
|
+
yield fs.mkdir(path.dirname(file), { recursive: true });
|
|
78
|
+
yield fs.writeFile(file, contents);
|
|
79
|
+
return existing === undefined ? 'created' : 'updated';
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function findGitRoot(directory) {
|
|
83
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
try {
|
|
85
|
+
return yield run('git', ['rev-parse', '--show-toplevel'], directory);
|
|
86
|
+
}
|
|
87
|
+
catch (_a) {
|
|
88
|
+
throw new Error('--github requires a Git repository. Run it inside your docs repository.');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function resolveGithubRepo(root) {
|
|
93
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
94
|
+
const remote = yield run('git', ['remote', 'get-url', 'origin'], root).catch(() => {
|
|
95
|
+
throw new Error('This repository has no origin remote. Add a github.com origin and retry.');
|
|
96
|
+
});
|
|
97
|
+
const parsed = parseGithubRemote(remote);
|
|
98
|
+
if (!parsed)
|
|
99
|
+
throw new Error(`The origin remote (${remote}) is not a github.com repository.`);
|
|
100
|
+
const output = yield run('gh', [
|
|
101
|
+
'api',
|
|
102
|
+
`repos/${parsed.owner}/${parsed.repo}`,
|
|
103
|
+
'--jq',
|
|
104
|
+
'{id: .id, full_name: .full_name, default_branch: .default_branch}',
|
|
105
|
+
], root).catch((error) => {
|
|
106
|
+
if (error.code === 'ENOENT')
|
|
107
|
+
throw new Error('The GitHub CLI (gh) is required. Install it and run `gh auth login`.');
|
|
108
|
+
throw new Error(`Could not read ${parsed.owner}/${parsed.repo} with gh. Run \`gh auth login\` with access to the repository and retry.`);
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
return githubRepoSchema.parse(JSON.parse(output));
|
|
112
|
+
}
|
|
113
|
+
catch (_a) {
|
|
114
|
+
throw new Error('gh returned an unexpected repository response.');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function setupGithubDeployment(options_1, warn_1) {
|
|
119
|
+
return __awaiter(this, arguments, void 0, function* (options, warn, progress = () => { }) {
|
|
120
|
+
const directory = path.resolve(options.directory);
|
|
121
|
+
const root = yield findGitRoot(directory);
|
|
122
|
+
const [realRoot, realDirectory] = yield Promise.all([fs.realpath(root), fs.realpath(directory)]);
|
|
123
|
+
const docsPath = path.relative(realRoot, realDirectory).split(path.sep).join('/');
|
|
124
|
+
if (!(yield readFileIfExists(path.join(directory, 'docs.json')))) {
|
|
125
|
+
throw new Error(`No docs.json in ${directory}. Pass the docs folder with --directory.`);
|
|
126
|
+
}
|
|
127
|
+
progress('Resolving the GitHub repository...');
|
|
128
|
+
const repo = yield resolveGithubRepo(root);
|
|
129
|
+
const result = yield prepareAgentDeployment(options, warn, progress);
|
|
130
|
+
if (result.status === 'claimed') {
|
|
131
|
+
throw new Error('This site has been claimed. Setting up GitHub deploys for claimed sites is not supported yet; connect the repository from the Mintlify dashboard.');
|
|
132
|
+
}
|
|
133
|
+
const accessToken = result.credentials.accessToken;
|
|
134
|
+
if (!accessToken)
|
|
135
|
+
throw new Error('Agent authentication did not return an access token.');
|
|
136
|
+
progress(`Trusting ${repo.full_name}@${repo.default_branch} to deploy...`);
|
|
137
|
+
yield trustGithubRepo(options.apiUrl, accessToken, {
|
|
138
|
+
repositoryId: String(repo.id),
|
|
139
|
+
repository: repo.full_name,
|
|
140
|
+
branch: repo.default_branch,
|
|
141
|
+
});
|
|
142
|
+
const workflow = yield writeGithubWorkflow(root, githubWorkflow(repo.default_branch, result.site.subdomain, docsPath), options.force);
|
|
143
|
+
return {
|
|
144
|
+
siteUrl: agentSiteUrl(options.apiUrl, result.site.subdomain),
|
|
145
|
+
subdomain: result.site.subdomain,
|
|
146
|
+
repository: repo.full_name,
|
|
147
|
+
branch: repo.default_branch,
|
|
148
|
+
workflowPath: GITHUB_WORKFLOW_PATH,
|
|
149
|
+
workflow,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export function formatGithubSetup(result) {
|
|
154
|
+
return [
|
|
155
|
+
`Site: ${result.siteUrl}`,
|
|
156
|
+
`Trusted repository: ${result.repository} (branch ${result.branch})`,
|
|
157
|
+
`Workflow: ${result.workflowPath} (${result.workflow})`,
|
|
158
|
+
'',
|
|
159
|
+
'Next steps:',
|
|
160
|
+
`1. Commit and push ${result.workflowPath} to ${result.branch}.`,
|
|
161
|
+
`2. Every push to ${result.branch} then deploys ${result.subdomain} through GitHub Actions. No secrets are stored in the repository.`,
|
|
162
|
+
'',
|
|
163
|
+
...CLAIM_INSTRUCTIONS,
|
|
164
|
+
].join('\n');
|
|
165
|
+
}
|
|
@@ -6,7 +6,13 @@ export const DEPLOY_HELP = [
|
|
|
6
6
|
'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.',
|
|
7
7
|
'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.',
|
|
8
8
|
'The output contains a short-lived write token. Do not paste it into public logs or save it in a Git remote.',
|
|
9
|
+
'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.',
|
|
10
|
+
'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.',
|
|
9
11
|
].join('\n\n');
|
|
12
|
+
export const CLAIM_INSTRUCTIONS = [
|
|
13
|
+
'Ask the user for the email address they want to use to claim this site. Never invent an email address.',
|
|
14
|
+
'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.',
|
|
15
|
+
];
|
|
10
16
|
export function deploymentInstructions(result, claimUrl) {
|
|
11
17
|
if (result.status !== 'ready')
|
|
12
18
|
return '';
|
|
@@ -31,9 +37,6 @@ export function deploymentInstructions(result, claimUrl) {
|
|
|
31
37
|
'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.',
|
|
32
38
|
'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.',
|
|
33
39
|
]
|
|
34
|
-
:
|
|
35
|
-
'Ask the user for the email address they want to use to claim this site. Never invent an email address.',
|
|
36
|
-
'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.',
|
|
37
|
-
]),
|
|
40
|
+
: CLAIM_INSTRUCTIONS),
|
|
38
41
|
].join('\n');
|
|
39
42
|
}
|
package/bin/deploy.js
CHANGED
|
@@ -73,14 +73,17 @@ export function prepareAgentDeployment(options_1, warn_1) {
|
|
|
73
73
|
throw new Error('The new anonymous site is already expired. Please retry later.');
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
+
export function agentSiteUrl(apiUrl, subdomain) {
|
|
77
|
+
const domain = new URL(apiUrl).hostname === 'leaves-dev.mintlify.com'
|
|
78
|
+
? 'mint-dev.mintlify.me'
|
|
79
|
+
: 'mintlify.site';
|
|
80
|
+
return new URL(`https://${subdomain}.${domain}`).toString();
|
|
81
|
+
}
|
|
76
82
|
export function formatDeployment(result, apiUrl) {
|
|
77
83
|
if (result.status === 'claimed')
|
|
78
84
|
return 'This site has been claimed. Manage it in the Mintlify dashboard. Use mint deploy --new --subdomain <name> to create another site.';
|
|
79
85
|
const { site, write } = result;
|
|
80
|
-
const
|
|
81
|
-
? 'mint-dev.mintlify.me'
|
|
82
|
-
: 'mintlify.site';
|
|
83
|
-
const siteUrl = new URL(`https://${site.subdomain}.${domain}`).toString();
|
|
86
|
+
const siteUrl = agentSiteUrl(apiUrl, site.subdomain);
|
|
84
87
|
return [
|
|
85
88
|
result.created
|
|
86
89
|
? 'Repository provisioned; push required to build the site.'
|