yaba-release-cli
Version:
Yaba is a simple CLI tool that helps you manage releases of your Github projects.
305 lines (274 loc) • 12.6 kB
JavaScript
import * as helper from "../utils/helper.js";
import * as flow from "../utils/flow.js";
import * as git from "../utils/git.js";
import * as templateUtils from "../utils/template-utils.js";
import { exitCodes } from "../utils/exit-codes.js";
import { createError } from "../utils/errors.js";
import { isNonEmptyString } from "../utils/runtime-config.js";
import { publishReleaseNotifications } from "../notifications/publisher.js";
import { printChangelog, printJson, printReleasePreview, printNotificationPreview } from "../services/command-output.js";
import { buildReleaseNotesBundle } from "../services/release-notes-service.js";
import { resolveReleaseTag } from "../services/tag-resolution-service.js";
import {
enforceReleaseSafety,
shouldCreateRelease,
shouldShowChangelog
} from "../services/release-safety-service.js";
import {
resolveOwner,
resolveReleaseContext,
resolveReleaseRepo
} from "../services/runtime-config-service.js";
import { verifyStatusChecks } from "../services/status-check-service.js";
export async function runReleaseCommand(options, runtimeConfig, isJsonOutput) {
const releaseRepo = resolveReleaseRepo(options, runtimeConfig);
checkDirectory(options, releaseRepo, runtimeConfig);
await flow.checkInternetConnection();
const repoOwner = await resolveOwner(options, runtimeConfig);
const releaseContext = resolveReleaseContext(options, runtimeConfig);
const headBranch = releaseContext.target ? null : await checkHeadBranch(repoOwner, releaseRepo);
const releaseTarget = await resolveReleaseTarget(repoOwner, releaseRepo, releaseContext, headBranch);
const targetReference = isNonEmptyString(releaseContext.target) ? releaseContext.target : headBranch;
await verifyStatusChecks(repoOwner, releaseRepo, releaseTarget, releaseContext.noStatusChecks);
const lastRelease = await flow.fetchLastRelease(repoOwner, releaseRepo);
const releaseTag = await resolveTagName(repoOwner, releaseRepo, releaseContext, releaseTarget, targetReference);
const changeLog = await flow.prepareChangeLog(repoOwner, releaseRepo, releaseTarget, lastRelease);
enforceReleaseSafety(changeLog.length, releaseContext);
const preparedChangeLog = helper.prepareChangeLog(releaseContext.body, changeLog);
const releasePreview = buildReleasePreview(
preparedChangeLog,
repoOwner,
releaseRepo,
lastRelease,
releaseTarget,
releaseContext,
releaseTag
);
if (options.releaseCommand === "preview") {
const lastReleaseTag = resolveLastReleaseTag(lastRelease, releaseTarget);
const releaseName = helper.releaseName(releaseContext.releaseName);
const releaseNotes = await buildReleaseNotesBundle({
owner: repoOwner,
repo: releaseRepo,
previousTag: lastReleaseTag,
currentTag: releaseTag,
releaseName: releaseName,
preparedChangeLog: preparedChangeLog,
changeLog: changeLog,
fetchPullRequest: async pullNumber =>
await flow.fetchPullRequestByNumber(repoOwner, releaseRepo, pullNumber),
labelBuckets: releaseContext.labelBuckets
});
let notificationPreview = null;
if (options.notifications) {
notificationPreview = {
provider: options.notifications,
body: options.notifications === "slack"
? releaseNotes.slackNewsletterBody
: releaseNotes.githubReleaseBody
};
}
if (isJsonOutput) {
printJson({
...buildReleaseJsonBase("release.preview", releasePreview.owner, releasePreview.repo, {
releaseName: releasePreview.releaseName,
newTag: releasePreview.releaseTag,
previousTag: releasePreview.lastReleaseTag,
draft: releasePreview.draft,
targetCommitish: releasePreview.targetCommitish
}),
previousTagSource: releasePreview.releaseTagSource,
changelog: releaseNotes.githubReleaseBody,
notificationPreview: notificationPreview
});
} else {
printReleasePreview({ ...releasePreview, changelogBody: releaseNotes.githubReleaseBody });
if (notificationPreview) {
printNotificationPreview(notificationPreview);
}
}
return exitCodes.SUCCESS;
}
if (shouldShowChangelog(changeLog.length, options.changelog === true, releaseContext.allowEmpty === true)) {
if (isJsonOutput) {
printJson({
command: "release.changelog",
status: "success",
owner: repoOwner,
repo: releaseRepo,
changelog: preparedChangeLog,
commitCount: changeLog.length
});
} else {
printChangelog(preparedChangeLog);
}
} else if (isJsonOutput && options.changelog) {
printJson({
command: "release.changelog",
status: "noop",
owner: repoOwner,
repo: releaseRepo,
reason: "No changes found to release."
});
}
if (shouldCreateRelease(changeLog.length, options.changelog === true, releaseContext.allowEmpty === true)) {
const lastReleaseTag = resolveLastReleaseTag(lastRelease, releaseTarget);
const releaseResult = await prepareRelease(
changeLog,
preparedChangeLog,
repoOwner,
releaseRepo,
lastReleaseTag,
releaseContext,
releaseTarget,
releaseTag,
releaseContext.labelBuckets
);
if (isJsonOutput) {
printJson({
...buildReleaseJsonBase("release.create", repoOwner, releaseRepo, {
releaseName: releaseResult.releaseName,
newTag: releaseResult.releaseTag,
previousTag: releaseResult.previousTag,
draft: releaseResult.draft,
targetCommitish: releaseResult.targetCommitish
}),
releaseUrl: releaseResult.releaseUrl,
notificationProviders: releaseResult.notificationProviders,
releaseNotesMode: releaseResult.releaseNotesMode,
allowEmpty: releaseContext.allowEmpty === true,
failOnEmpty: releaseContext.failOnEmpty === true,
maxCommits: releaseContext.maxCommits ?? null,
notified: releaseResult.publishRequested
});
}
} else if (isJsonOutput && !options.changelog) {
printJson({
command: "release.create",
status: "noop",
owner: repoOwner,
repo: releaseRepo,
reason: "No changes found to release. Use '--allow-empty' to proceed or '--fail-on-empty' to fail explicitly."
});
}
return exitCodes.SUCCESS;
}
async function prepareRelease(changeLog, preparedChangeLog, repoOwner, releaseRepo, lastReleaseTag, releaseContext, targetCommitish, releaseTag, labelBuckets) {
const hasReleaseCreatePermission = await helper.releaseCreatePermit(releaseContext.interactive);
if (!hasReleaseCreatePermission) {
throw createError("Release was not prepared. Confirmation prompt was declined.", exitCodes.VALIDATION);
}
const releaseName = helper.releaseName(releaseContext.releaseName);
const releaseNotes = await buildReleaseNotesBundle({
owner: repoOwner,
repo: releaseRepo,
previousTag: lastReleaseTag,
currentTag: releaseTag,
releaseName: releaseName,
preparedChangeLog: preparedChangeLog,
changeLog: changeLog,
fetchPullRequest: async pullNumber => {
return await flow.fetchPullRequestByNumber(repoOwner, releaseRepo, pullNumber);
},
labelBuckets: labelBuckets
});
const releaseUrl = await flow.createRelease(
repoOwner,
releaseRepo,
releaseContext.draft,
releaseName,
releaseNotes.githubReleaseBody,
releaseTag,
targetCommitish
);
const notificationResult = await publishReleaseNotifications({
publish: releaseContext.publish,
providerNames: releaseContext.notificationProviders,
context: {
repo: releaseRepo,
changelog: releaseNotes.slackNewsletterBody,
releaseUrl: releaseUrl,
releaseName: releaseName,
compareUrl: releaseNotes.compareUrl
}
});
return {
releaseName: releaseName,
releaseTag: releaseTag,
previousTag: lastReleaseTag,
targetCommitish: targetCommitish,
releaseUrl: releaseUrl,
notificationProviders: notificationResult.providers,
releaseNotesMode: releaseNotes.mode,
draft: releaseContext.draft === true,
publishRequested: releaseContext.publish === true
};
}
async function resolveReleaseTarget(repoOwner, releaseRepo, releaseContext, fallbackHeadBranch) {
if (isNonEmptyString(releaseContext.target)) {
return await flow.resolveTargetCommitish(repoOwner, releaseRepo, releaseContext.target.trim());
}
const requiresCommitSha = releaseContext.tagStrategy === "sha"
|| (isNonEmptyString(releaseContext.tagPattern) && releaseContext.tagPattern.includes("{shortSha}"));
if (requiresCommitSha) {
return await flow.resolveTargetCommitish(repoOwner, releaseRepo, fallbackHeadBranch);
}
return fallbackHeadBranch;
}
async function checkHeadBranch(repoOwner, releaseRepo) {
const headBranch = await flow.fetchHeadBranch(repoOwner, releaseRepo);
if (headBranch === null) {
throw createError("Head branch can not be found! The release has been interrupted!", exitCodes.UPSTREAM);
}
return headBranch;
}
function checkDirectory(options, releaseRepo, runtimeConfig) {
const hasRepoFromConfig = isNonEmptyString(runtimeConfig?.release?.repo);
if (options.repo == undefined && !hasRepoFromConfig && !git.isGitRepo()) {
throw createError(`The directory '${git.retrieveCurrentDirectory()}' is not a Git repo.`, exitCodes.VALIDATION);
}
if (!isNonEmptyString(releaseRepo) || releaseRepo === "not a git repo") {
throw createError("Repository name could not be resolved. Use --repo or set release.repo in config.", exitCodes.VALIDATION);
}
}
function resolveLastReleaseTag(lastRelease, fallbackRef) {
return lastRelease?.tag_name || fallbackRef;
}
async function resolveTagName(repoOwner, releaseRepo, releaseContext, releaseTarget, targetReference) {
return await resolveReleaseTag({
explicitTag: releaseContext.tag,
tagStrategy: releaseContext.tagStrategy,
tagPattern: releaseContext.tagPattern,
tagOnConflict: releaseContext.tagOnConflict,
tagMaxAttempts: releaseContext.tagMaxAttempts,
targetCommitish: releaseTarget,
targetReference: targetReference,
tagExists: async tagName => {
return await flow.tagExists(repoOwner, releaseRepo, tagName);
}
});
}
function buildReleaseJsonBase(command, owner, repo, { releaseName, newTag, previousTag, draft, targetCommitish }) {
return { command, status: "success", owner, repo, releaseName, newTag, previousTag, draft, targetCommitish };
}
function buildReleasePreview(preparedChangeLog, repoOwner, releaseRepo, lastRelease, releaseTarget, releaseContext, releaseTag) {
const lastReleaseTag = resolveLastReleaseTag(lastRelease, releaseTarget);
const releaseName = helper.releaseName(releaseContext.releaseName);
const changelogBody = templateUtils.generateChangelog(preparedChangeLog, repoOwner, releaseRepo, lastReleaseTag, releaseTag);
const releaseTagSource = lastRelease?.tag_name
? "latest release tag"
: isNonEmptyString(releaseContext.target)
? "target reference (--target fallback)"
: "head branch (fallback)";
return {
owner: repoOwner,
repo: releaseRepo,
releaseName: releaseName,
releaseTag: releaseTag,
lastReleaseTag: lastReleaseTag,
releaseTagSource: releaseTagSource,
targetCommitish: releaseTarget,
draft: releaseContext.draft === true,
changelogBody: changelogBody
};
}