Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/publish/deployment.ts
3562 views
1
/*
2
* deployment.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { warning } from "../../deno_ral/log.ts";
8
9
import { Confirm, prompt, Select } from "cliffy/prompt/mod.ts";
10
11
import { findProvider, publishProviders } from "../../publish/provider.ts";
12
13
import {
14
AccountToken,
15
PublishDeploymentWithAccount,
16
PublishProvider,
17
} from "../../publish/provider-types.ts";
18
19
import { PublishOptions, PublishRecord } from "../../publish/types.ts";
20
import {
21
readProjectPublishDeployments,
22
readPublishDeployments,
23
} from "../../publish/config.ts";
24
import {
25
publishRecordIdentifier,
26
readAccountsPublishedTo,
27
} from "../../publish/common/data.ts";
28
import { kGhpages } from "../../publish/gh-pages/gh-pages.ts";
29
30
export async function resolveDeployment(
31
options: PublishOptions,
32
providerFilter?: string,
33
): Promise<PublishDeploymentWithAccount | undefined> {
34
// enumerate any existing deployments
35
const deployments = await publishDeployments(
36
options,
37
providerFilter,
38
);
39
40
if (deployments && deployments.length > 0) {
41
// if a site-id was passed then try to match it
42
const siteId = options.id;
43
if (siteId) {
44
const deployment = deployments.find((deployment) => {
45
return deployment.target.id === siteId;
46
});
47
if (deployment) {
48
if (options.prompt) {
49
const confirmed = await Confirm.prompt({
50
indent: "",
51
message: `Update site at ${deployment.target.url}?`,
52
default: true,
53
});
54
if (!confirmed) {
55
throw new Error();
56
}
57
}
58
return deployment;
59
} else {
60
throw new Error(
61
`No previous publish with site-id ${siteId} was found`,
62
);
63
}
64
} else if (options.prompt) {
65
// confirm prompt
66
const confirmPrompt = async (hint?: string) => {
67
return await Confirm.prompt({
68
indent: "",
69
message: `Update site at ${deployments[0].target.url}?`,
70
default: true,
71
hint,
72
});
73
};
74
75
if (
76
deployments.length === 1 && deployments[0].provider.publishRecord &&
77
providerFilter === deployments[0].provider.name
78
) {
79
const confirmed = await confirmPrompt();
80
if (confirmed) {
81
return deployments[0];
82
} else {
83
throw new Error();
84
}
85
} else {
86
return await chooseDeployment(deployments);
87
}
88
} else if (deployments.length === 1) {
89
return deployments[0];
90
} else {
91
throw new Error(
92
`Multiple previous publishes exist (specify one with --id when using --no-prompt)`,
93
);
94
}
95
} else if (!options.prompt) {
96
// if we get this far then an existing deployment has not been chosen,
97
// if --no-prompt is specified then this is an error state
98
if (providerFilter === kGhpages) {
99
// special case for gh-pages where no _publish.yml is required but a gh-pages branch is
100
throw new Error(
101
`Unable to publish to GitHub Pages (the remote origin does not have a branch named "gh-pages". Use first \`quarto publish gh-pages\` locally to initialize the remote repository for publishing.)`,
102
);
103
} else {
104
throw new Error(
105
`No _publish.yml file available (_publish.yml specifying a destination required for non-interactive publish)`,
106
);
107
}
108
}
109
}
110
111
export async function publishDeployments(
112
options: PublishOptions,
113
providerFilter?: string,
114
): Promise<PublishDeploymentWithAccount[]> {
115
const deployments: PublishDeploymentWithAccount[] = [];
116
117
// see if there are any static publish records for this directory
118
for (const provider of publishProviders()) {
119
if (
120
(!providerFilter || providerFilter === provider.name) &&
121
provider.publishRecord
122
) {
123
const record = await (provider.publishRecord(options.input));
124
if (record) {
125
deployments.push({
126
provider,
127
target: record,
128
});
129
}
130
}
131
}
132
133
// read config
134
const config = typeof (options.input) === "string"
135
? readPublishDeployments(options.input)
136
: readProjectPublishDeployments(options.input);
137
for (const providerName of Object.keys(config.records)) {
138
if (providerFilter && (providerName !== providerFilter)) {
139
continue;
140
}
141
142
const provider = findProvider(providerName);
143
if (provider) {
144
// try to update urls if we have an account to bind to
145
for (const record of config.records[providerName]) {
146
let account: AccountToken | undefined;
147
const publishedToAccounts = await readAccountsPublishedTo(
148
options.input,
149
provider,
150
record,
151
);
152
153
if (publishedToAccounts.length === 1) {
154
account = publishedToAccounts[0];
155
}
156
157
if (account) {
158
const target = await resolveDeploymentTarget(
159
provider,
160
account,
161
record,
162
);
163
if (target) {
164
deployments.push({
165
provider,
166
target,
167
account,
168
});
169
}
170
} else {
171
deployments.push({ provider, target: record });
172
}
173
}
174
} else {
175
warning(`Unkonwn provider ${providerName}`);
176
}
177
}
178
179
return deployments;
180
}
181
182
export async function chooseDeployment(
183
depoyments: PublishDeploymentWithAccount[],
184
): Promise<PublishDeploymentWithAccount | undefined> {
185
// filter out deployments w/o target url (provided from cli)
186
depoyments = depoyments.filter((deployment) => !!deployment.target.url);
187
188
// collect unique origins
189
const originCounts = depoyments.reduce((origins, deployment) => {
190
try {
191
const originUrl = new URL(deployment.target.url!).origin;
192
const count = origins.get(originUrl) || 0;
193
origins.set(originUrl, count + 1);
194
} catch {
195
// url may not be valid and that shouldn't cause an error
196
}
197
return origins;
198
}, new Map<string, number>());
199
200
const kOther = "other";
201
const options = depoyments
202
.map((deployment) => {
203
let url = deployment.target.url;
204
try {
205
const targetOrigin = new URL(deployment.target.url!).origin;
206
if (
207
originCounts.get(targetOrigin) === 1 &&
208
(deployment.provider?.listOriginOnly ?? false)
209
) {
210
url = targetOrigin;
211
}
212
} catch {
213
// url may not be valid and that shouldn't cause an error
214
}
215
216
return {
217
name: `${url} (${deployment.provider.description}${
218
deployment.account ? (" - " + deployment.account.name) : ""
219
})`,
220
value: publishRecordIdentifier(deployment.target, deployment.account),
221
};
222
});
223
options.push({
224
name: "Add a new destination...",
225
value: kOther,
226
});
227
228
const confirm = await prompt([{
229
name: "destination",
230
indent: "",
231
message: "Publish update to:",
232
options,
233
type: Select,
234
}]);
235
236
if (confirm.destination !== kOther) {
237
return depoyments.find((deployment) =>
238
publishRecordIdentifier(deployment.target, deployment.account) ===
239
confirm.destination
240
);
241
} else {
242
return undefined;
243
}
244
}
245
246
async function resolveDeploymentTarget(
247
provider: PublishProvider,
248
account: AccountToken,
249
record: PublishRecord,
250
) {
251
try {
252
return await provider.resolveTarget(account, record);
253
} catch (err) {
254
if (!(err instanceof Error)) {
255
// shouldn't ever happen
256
throw err;
257
}
258
if (provider.isNotFound(err)) {
259
warning(
260
`${record.url} not found (you may need to remove it from the publish configuration)`,
261
);
262
return undefined;
263
} else if (!provider.isUnauthorized(err)) {
264
throw err;
265
}
266
}
267
268
return record;
269
}
270
271