Path: blob/main/src/command/publish/deployment.ts
3562 views
/*1* deployment.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { warning } from "../../deno_ral/log.ts";78import { Confirm, prompt, Select } from "cliffy/prompt/mod.ts";910import { findProvider, publishProviders } from "../../publish/provider.ts";1112import {13AccountToken,14PublishDeploymentWithAccount,15PublishProvider,16} from "../../publish/provider-types.ts";1718import { PublishOptions, PublishRecord } from "../../publish/types.ts";19import {20readProjectPublishDeployments,21readPublishDeployments,22} from "../../publish/config.ts";23import {24publishRecordIdentifier,25readAccountsPublishedTo,26} from "../../publish/common/data.ts";27import { kGhpages } from "../../publish/gh-pages/gh-pages.ts";2829export async function resolveDeployment(30options: PublishOptions,31providerFilter?: string,32): Promise<PublishDeploymentWithAccount | undefined> {33// enumerate any existing deployments34const deployments = await publishDeployments(35options,36providerFilter,37);3839if (deployments && deployments.length > 0) {40// if a site-id was passed then try to match it41const siteId = options.id;42if (siteId) {43const deployment = deployments.find((deployment) => {44return deployment.target.id === siteId;45});46if (deployment) {47if (options.prompt) {48const confirmed = await Confirm.prompt({49indent: "",50message: `Update site at ${deployment.target.url}?`,51default: true,52});53if (!confirmed) {54throw new Error();55}56}57return deployment;58} else {59throw new Error(60`No previous publish with site-id ${siteId} was found`,61);62}63} else if (options.prompt) {64// confirm prompt65const confirmPrompt = async (hint?: string) => {66return await Confirm.prompt({67indent: "",68message: `Update site at ${deployments[0].target.url}?`,69default: true,70hint,71});72};7374if (75deployments.length === 1 && deployments[0].provider.publishRecord &&76providerFilter === deployments[0].provider.name77) {78const confirmed = await confirmPrompt();79if (confirmed) {80return deployments[0];81} else {82throw new Error();83}84} else {85return await chooseDeployment(deployments);86}87} else if (deployments.length === 1) {88return deployments[0];89} else {90throw new Error(91`Multiple previous publishes exist (specify one with --id when using --no-prompt)`,92);93}94} else if (!options.prompt) {95// if we get this far then an existing deployment has not been chosen,96// if --no-prompt is specified then this is an error state97if (providerFilter === kGhpages) {98// special case for gh-pages where no _publish.yml is required but a gh-pages branch is99throw new Error(100`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.)`,101);102} else {103throw new Error(104`No _publish.yml file available (_publish.yml specifying a destination required for non-interactive publish)`,105);106}107}108}109110export async function publishDeployments(111options: PublishOptions,112providerFilter?: string,113): Promise<PublishDeploymentWithAccount[]> {114const deployments: PublishDeploymentWithAccount[] = [];115116// see if there are any static publish records for this directory117for (const provider of publishProviders()) {118if (119(!providerFilter || providerFilter === provider.name) &&120provider.publishRecord121) {122const record = await (provider.publishRecord(options.input));123if (record) {124deployments.push({125provider,126target: record,127});128}129}130}131132// read config133const config = typeof (options.input) === "string"134? readPublishDeployments(options.input)135: readProjectPublishDeployments(options.input);136for (const providerName of Object.keys(config.records)) {137if (providerFilter && (providerName !== providerFilter)) {138continue;139}140141const provider = findProvider(providerName);142if (provider) {143// try to update urls if we have an account to bind to144for (const record of config.records[providerName]) {145let account: AccountToken | undefined;146const publishedToAccounts = await readAccountsPublishedTo(147options.input,148provider,149record,150);151152if (publishedToAccounts.length === 1) {153account = publishedToAccounts[0];154}155156if (account) {157const target = await resolveDeploymentTarget(158provider,159account,160record,161);162if (target) {163deployments.push({164provider,165target,166account,167});168}169} else {170deployments.push({ provider, target: record });171}172}173} else {174warning(`Unkonwn provider ${providerName}`);175}176}177178return deployments;179}180181export async function chooseDeployment(182depoyments: PublishDeploymentWithAccount[],183): Promise<PublishDeploymentWithAccount | undefined> {184// filter out deployments w/o target url (provided from cli)185depoyments = depoyments.filter((deployment) => !!deployment.target.url);186187// collect unique origins188const originCounts = depoyments.reduce((origins, deployment) => {189try {190const originUrl = new URL(deployment.target.url!).origin;191const count = origins.get(originUrl) || 0;192origins.set(originUrl, count + 1);193} catch {194// url may not be valid and that shouldn't cause an error195}196return origins;197}, new Map<string, number>());198199const kOther = "other";200const options = depoyments201.map((deployment) => {202let url = deployment.target.url;203try {204const targetOrigin = new URL(deployment.target.url!).origin;205if (206originCounts.get(targetOrigin) === 1 &&207(deployment.provider?.listOriginOnly ?? false)208) {209url = targetOrigin;210}211} catch {212// url may not be valid and that shouldn't cause an error213}214215return {216name: `${url} (${deployment.provider.description}${217deployment.account ? (" - " + deployment.account.name) : ""218})`,219value: publishRecordIdentifier(deployment.target, deployment.account),220};221});222options.push({223name: "Add a new destination...",224value: kOther,225});226227const confirm = await prompt([{228name: "destination",229indent: "",230message: "Publish update to:",231options,232type: Select,233}]);234235if (confirm.destination !== kOther) {236return depoyments.find((deployment) =>237publishRecordIdentifier(deployment.target, deployment.account) ===238confirm.destination239);240} else {241return undefined;242}243}244245async function resolveDeploymentTarget(246provider: PublishProvider,247account: AccountToken,248record: PublishRecord,249) {250try {251return await provider.resolveTarget(account, record);252} catch (err) {253if (!(err instanceof Error)) {254// shouldn't ever happen255throw err;256}257if (provider.isNotFound(err)) {258warning(259`${record.url} not found (you may need to remove it from the publish configuration)`,260);261return undefined;262} else if (!provider.isUnauthorized(err)) {263throw err;264}265}266267return record;268}269270271