@curvenote/cli
Version:
CLI Client library for Curvenote
232 lines (231 loc) • 11.8 kB
JavaScript
import { keyFromTransferFile, writeKeyToConfig } from './utils.transfer.js';
import { confirmOrExit, writeJsonLogs, addOxaTransformersToOpts } from '../utils/utils.js';
import chalk from 'chalk';
import { postNewCliCheckJob, patchUpdateCliCheckJob, exitOnInvalidKeyOption } from './utils.js';
import { ensureVenue, checkVenueExists, confirmUpdateToExistingSubmission, updateExistingSubmission, createNewSubmission, checkForSubmissionKeyInUse, determineCollectionAndKind, collectionMoniker, promptForNewKey, getAllSubmissionsUsingKey, getSubmissionToUpdate, checkVenueSubmitAccess, getVenueCollections, } from './submit.utils.js';
import { submissionRuleChecks } from '@curvenote/check-implementations';
import { logCheckReport, runChecks } from '../check/index.js';
import path from 'node:path';
import fs from 'node:fs';
import { prepareChecksForSubmission } from './check.js';
import { getGitRepoInfo } from './utils.git.js';
import * as uploads from '../uploads/index.js';
import { workKeyFromConfig } from '../works/utils.js';
import { buildSite, clean, collectAllBuildExportOptions, localArticleExport } from 'myst-cli';
export const CDN_KEY_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
export const DEV_CDN_KEY = 'ad7fa60f-5460-4bf9-96ea-59be87944e41';
export async function performCleanRebuild(session, opts) {
session.log.info('\n\n\t✨✨✨ performing a clean re-build of your work ✨✨✨\n\n');
await clean(session, [], { site: true, html: true, temp: true, exports: true, yes: true });
const exportOptionsList = await collectAllBuildExportOptions(session, [], { all: true });
const exportLogList = exportOptionsList.map((exportOptions) => {
return `${path.relative('.', exportOptions.$file)} -> ${exportOptions.output}`;
});
session.log.info(`📬 Performing exports:\n ${exportLogList.join('\n ')}`);
await localArticleExport(session, exportOptionsList, {});
session.log.info(`⛴ Exports complete`);
// Build the files in the content folder and process them
await buildSite(session, addOxaTransformersToOpts(session, opts !== null && opts !== void 0 ? opts : {}));
session.log.info(`✅ Work rebuild complete`);
}
export async function submit(session, venue, opts) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const submitLog = {
input: {
venue,
opts,
},
};
if (session.isAnon) {
throw new Error('⛔️ You must be authenticated for this command. Use `curvenote token set [token]`');
}
// TODO upload preflight checks
// TODO check the venue allows for submissions & updates to the submission
// TODO check user has permission to submit / update a submission
venue = await ensureVenue(session, venue, opts);
await checkVenueExists(session, venue);
await checkVenueSubmitAccess(session, venue);
const collections = await getVenueCollections(session, venue);
let key = workKeyFromConfig(session);
// Deprecation step to handle old transfer.yml files
key = (_a = (await keyFromTransferFile(session, venue, key, opts))) !== null && _a !== void 0 ? _a : key;
if (!key) {
key = await promptForNewKey(session, opts);
await writeKeyToConfig(session, key);
}
exitOnInvalidKeyOption(session, key);
const gitInfo = await getGitRepoInfo();
const source = {
repo: gitInfo === null || gitInfo === void 0 ? void 0 : gitInfo.repo,
branch: gitInfo === null || gitInfo === void 0 ? void 0 : gitInfo.branch,
path: gitInfo === null || gitInfo === void 0 ? void 0 : gitInfo.path,
commit: gitInfo === null || gitInfo === void 0 ? void 0 : gitInfo.commit,
};
session.log.info(`📍 Submitting using key: ${chalk.bold(key)}`);
let existing;
// Only check for submissions to update if we are not creating a new draft
if (!(opts === null || opts === void 0 ? void 0 : opts.draft) && !(opts === null || opts === void 0 ? void 0 : opts.new)) {
session.log.info(`📡 Checking submission status...`);
const allExisting = await getAllSubmissionsUsingKey(session, venue, key);
if (!(allExisting === null || allExisting === void 0 ? void 0 : allExisting.length)) {
const exists = await checkForSubmissionKeyInUse(session, venue, key);
if (exists) {
session.log.warn(`⛔️ This work has already been submitted to a Curvenote site, but you don't have permission to access that submission.`);
session.log.info('If you still want to make a new submission, you may explicitly add flag "--new"');
process.exit(1);
}
else {
session.log.info(`🔍 No existing submission found at "${venue}" using the key "${key}"`);
}
}
else {
existing = await getSubmissionToUpdate(session, allExisting);
session.log.info(`🔍 Found an existing submission using this key, the existing submission will be updated.`);
}
}
//
// Options, checks and prompts
//
let kind;
let collection;
if (existing) {
const confirmed = await confirmUpdateToExistingSubmission(session, venue, collections, existing, key, opts);
kind = confirmed.kind;
collection = confirmed.collection;
}
else {
//
// NEW SUBMISSIONS
//
session.log.debug('Making a new submission...');
const determined = await determineCollectionAndKind(session, venue, collections, opts);
kind = determined.kind;
collection = determined.collection;
session.log.info(`📚 Submitting a "${kind === null || kind === void 0 ? void 0 : kind.name}" to the "${collectionMoniker(collection)}"`);
if (opts === null || opts === void 0 ? void 0 : opts.draft)
session.log.info(`📝 ${chalk.bold.yellow(`Making a draft submission, this will not be processed by "${venue}".`)}`);
await confirmOrExit((opts === null || opts === void 0 ? void 0 : opts.draft)
? `Submit your draft to "${venue}" based on the contents of your local folder?`
: `Start a new submission to "${venue}" based on the contents of your local folder?`, opts);
}
const checks = prepareChecksForSubmission(session, venue, kind);
//
// Process local folder and upload stuff
//
await performCleanRebuild(session, opts);
session.log.info('🪩 Successfully built your work!');
//
// run checks
//
let report;
if (checks && checks.length > 0) {
session.log.info(`🕵️♀️ Running checks...`);
report = await runChecks(session, checks, submissionRuleChecks);
const reportFilename = path.join(session.buildPath(), 'site', 'checks.json');
session.log.debug(`💼 adding check report to ${reportFilename} for upload...`);
fs.writeFileSync(reportFilename, JSON.stringify({ venue, kind, report }, null, 2));
logCheckReport(session, report, false);
session.log.info(`🏁 Checks completed`);
}
if (!(opts === null || opts === void 0 ? void 0 : opts.draft)) {
await confirmOrExit(checks
? `Build and submission checks completed, are you happy to proceed with submission to "${venue}"?`
: `Build completed, are you happy to proceed with submission to "${venue}"?`, opts);
}
//
// Create a job to track the build and checks
//
session.log.info(`⛴ OK! Starting the submission process...`);
let job = await postNewCliCheckJob(session, {
site: venue,
collection,
kind,
source,
key,
}, {
checks: {
venue,
kind,
report,
},
});
try {
job = await patchUpdateCliCheckJob(session, job.id, 'RUNNING', 'Uploading work files to cdn', {
...job.results,
});
const cdn = (opts === null || opts === void 0 ? void 0 : opts.draft) ? session.config.tempCdnUrl : session.config.privateCdnUrl;
let cdnKey;
if (!process.env.DEV_CDN || process.env.DEV_CDN === 'false') {
const uploadResult = await uploads.uploadToCdn(session, cdn, opts);
cdnKey = uploadResult.cdnKey;
}
else if (process.env.DEV_CDN.match(CDN_KEY_RE)) {
cdnKey = process.env.DEV_CDN;
}
else {
cdnKey = DEV_CDN_KEY;
}
session.log.info(`🚀 ${chalk.bold.green(`Content uploaded with key ${cdnKey}`)}.`);
job = await patchUpdateCliCheckJob(session, job.id, 'RUNNING', 'Creating new work and submission entry', {
...job.results,
cdnKey,
});
const buildUrl = `${session.config.adminUrl}/build/${job.id}`;
session.log.info(`🤖 created a job to track this build: ${buildUrl}`);
//
// Create work and submission
//
if (existing) {
await updateExistingSubmission(session, submitLog, venue, cdnKey, existing, job.id);
}
else {
if (opts === null || opts === void 0 ? void 0 : opts.draft) {
session.log.info(`${chalk.bold(`🖐 Making a draft submission`)}`);
}
else {
session.log.info(`✨ Making a new submission`);
}
if (!kind) {
session.log.error('🚨 No submission kind found.');
process.exit(1);
}
await createNewSubmission(session, submitLog, venue, collection, kind, cdn, cdnKey, job.id, key, opts);
}
session.log.debug(`generating a build artifact for the submission...`);
if (!((_b = submitLog.work) === null || _b === void 0 ? void 0 : _b.id) ||
!((_c = submitLog.workVersion) === null || _c === void 0 ? void 0 : _c.id) ||
!((_d = submitLog.submission) === null || _d === void 0 ? void 0 : _d.id) ||
!((_e = submitLog.submissionVersion) === null || _e === void 0 ? void 0 : _e.id)) {
// This is just a safety net - it will not be encountered unless we change
// implementation of create/update submission functions or change the shape
// of successful API responses.
throw new Error(`work/submission ids not found from submission response`);
}
job = await patchUpdateCliCheckJob(session, job.id, 'COMPLETED', 'Submission completed', {
...job.results,
submissionId: (_f = submitLog.submission) === null || _f === void 0 ? void 0 : _f.id,
submissionVersionId: (_g = submitLog.submissionVersion) === null || _g === void 0 ? void 0 : _g.id,
workId: (_h = submitLog.work) === null || _h === void 0 ? void 0 : _h.id,
workVersionId: (_j = submitLog.workVersion) === null || _j === void 0 ? void 0 : _j.id,
});
submitLog.key = key;
submitLog.venue = venue;
submitLog.kind = kind;
submitLog.source = source;
submitLog.report = report;
submitLog.job = job;
submitLog.buildUrl = buildUrl;
session.log.info(chalk.bold.green(`🔗 build report url: ${buildUrl}`));
writeJsonLogs(session, 'curvenote.submit.json', submitLog);
}
catch (err) {
await patchUpdateCliCheckJob(session, job.id, 'FAILED', 'Submission from CLI failed', {
...job.results,
error: err.message,
});
session.log.error(`📣 ${chalk.bold.red(err.message)}`);
session.log.info('📨 Please contact support@curvenote.com');
writeJsonLogs(session, 'curvenote.submit.json', submitLog);
process.exit(1);
}
}