@halospv3/hce.shared-config
Version:
Automate commit message quality, changelogs, and CI/CD releases. Its `main` entry point is a Semantic Release config. Functions and classes are exposed for customization. An ESLint config, a Commitlint config, and addl. resources for .NET projects are als
267 lines (266 loc) • 16.6 kB
JavaScript
import { baseConfig } from "./semanticReleaseConfig.mjs";
import _debug from "./debug.mjs";
import { CaseInsensitiveMap } from "./CaseInsensitiveMap.mjs";
import { MSBuildProject } from "./dotnet/MSBuildProject.mjs";
import { getEnvVarValue } from "./utils/env.mjs";
import { NugetRegistryInfo } from "./dotnet/NugetRegistryInfo.mjs";
import { configureDotnetNugetPush, configurePrepareCmd } from "./dotnet/helpers.mjs";
import { insertPlugin } from "./insertPlugins.mjs";
import { inspect } from "node:util";
import * as console from "node:console";
//#region src/semanticReleaseConfigDotnet.ts
/**
* # Semantic-Release Config Factory (dotnet)
* A functional Semantic-Release configuration for dotnet projects
*
* extends {@link baseConfig }
*
* <-- TABLE OF CONTENTS -->
*
* EASY: {@link getConfig}
* Just provide the paths of the project file(s) and keep your API tokens ready.
* ADVANCED: {@link SemanticReleaseConfigDotnet}
* Allows for a hands-on customization if {@link getConfig} doesn't meet your needs.
* Use a copy of {@link getConfig} as the starting point of a new function and make changes from there.
* - {@link SemanticReleaseConfigDotnet#splicePlugin splicePlugin (insert/edit plugins)}
* - {@link SemanticReleaseConfigDotnet#setupDotnetCommands setupDotnetCommands}
* - {@link SemanticReleaseConfigDotnet#getTokenTestingCommands getTokenTestingCommands}
*/
var SemanticReleaseConfigDotnet = class {
options;
_projectsToPublish;
_projectsToPackAndPush;
_evaluatedProjects;
/**
* Creates an instance of SemanticReleaseConfigDotnet.
* Configures {@link baseConfig} with `@semantic-release/exec` to `dotnet` publish, pack, and push.
*
* Note: To sign packages, create a Target in the corresponding project(s) e.g.
* ```xml
* <Target Name="SignNupkgs" AfterTargets="Pack">
* <Exec Command="dotnet nuget sign $(PackageOutputPath) [remaining args]" ConsoleToMsBuild="true" />
* </Target>
* ```
* Alternatively, splice your signing commands into the publishCmd string,
* inserting them before `dotnet nuget push`.
* If you sign different signatures depending on the NuGet registry,
* splice your signing command (with "overwrite signature" enabled, if
* desired) before the corresponding registry's `dotnet nuget push` command.
* @param projectsToPublish An array of dotnet projects' relative paths. If
* empty or unspecified, tries getting projects' semi-colon-separated relative
* paths from the `PROJECTS_TO_PUBLISH` environment variable. If configured as
* recommended, the projects' publish outputs will be zipped to '$PWD/publish'
* for use in the `publish` semantic-release step (typically, GitHub release).
* @param projectsToPackAndPush An array of dotnet projects' relative paths.
* If `null`, `undefined`, or `[]`; tries getting projects' semi-colon-separated
* relative paths from the `PROJECTS_TO_PACK_AND_PUSH` environment variable.
* If neither is defined, no packages will be packed and pushed.
* If configured as recommended, `dotnet pack` will output the nupkg/snupkg
* files to `$PWD/publish` where they will be globbed by `dotnet nuget push`.
*/
constructor(projectsToPublish, projectsToPackAndPush) {
this.options = baseConfig;
this.options.plugins = this.options.plugins.map((pluginSpec) => typeof pluginSpec === "string" ? [pluginSpec, {}] : pluginSpec);
this._projectsToPublish = projectsToPublish;
if (this._projectsToPublish.length === 0) {
const p = getEnvVarValue("PROJECTS_TO_PUBLISH")?.split(";");
if (p && p.length > 0) this._projectsToPublish = p;
else if (_debug.enabled) _debug(/* @__PURE__ */ new Error("At least one project must be published. `projectsToPackAndPush` is empty and environment variable `PROJECTS_TO_PUBLISH` is undefined or empty."));
}
this._projectsToPackAndPush = projectsToPackAndPush ?? [];
if (this._projectsToPackAndPush.length === 0) {
const p = getEnvVarValue("PROJECTS_TO_PACK_AND_PUSH")?.split(";");
if (p && p.length > 0) this._projectsToPackAndPush = p;
else if (_debug.enabled) _debug("projectsToPackAndPush is undefined. dotnet-pack and dotnet-nuget-push setup will be skipped.");
}
this._evaluatedProjects = [...this._projectsToPublish.filter((v) => v instanceof MSBuildProject), ...this._projectsToPackAndPush.filter((v) => v instanceof NugetRegistryInfo).map((v) => v.project)];
}
get ProjectsToPublish() {
return this._projectsToPublish;
}
get ProjectsToPackAndPush() {
return this._projectsToPackAndPush;
}
get EvaluatedProjects() {
return this._evaluatedProjects;
}
/** @deprecated Superseded by {@link splicePlugin} */
insertPlugin(afterPluginsIDs, insertPluginIDs, beforePluginsIDs) {
this.options.plugins = insertPlugin(this.options.plugins, afterPluginsIDs, insertPluginIDs, beforePluginsIDs);
}
/**
* generate dotnet commands for \@semantic-release/exec, appending commands with ' && ' when necessary.
*
* Note: All strings in {@link this.ProjectsToPackAndPush} will be converted to basic {@link NugetRegistryInfo} instances with default values.
* If you need specific NRI settings or you need to push to GitLab-like or GitHub-like registries, instantiate them instead of passing their paths.
* @todo change to builder method? e.g. static async SetupDotnetCommands(this: SemanticReleaseConfigDotnet): Promise<SemanticReleaseConfigDotnet>
* @todo Add options param to allow users to enable pushing to GitLab, GitHub, NuGet.org with default settings -OR- with entirely custom settings.
* @see https://github.com/semantic-release/exec#usage
*/
async setupDotnetCommands() {
let srExecIndex = this.options.plugins.findIndex((v) => v[0] === "@semantic-release/exec");
if (srExecIndex === -1) {
console.warn(`\
Unable to find\`['@semantic-release/exec', unknown]\` in plugins array!
Appending it to the end of the array...This may cause an unexpected order of operations!`);
this.options.plugins.push(["@semantic-release/exec", {}]);
srExecIndex = this.options.plugins.length - 1;
}
const execOptions = this.options.plugins[srExecIndex][1];
_debug("Evaluating all projects with dotnet CLI...This may take a while.");
const referenceCounter = new CaseInsensitiveMap();
const projectPromiseArrayArray = await Promise.all(this._projectsToPackAndPush.map(async (project) => {
if (typeof project === "string") {
_debug(`Evaluating path "${project}" with "PackableProjectsToMSBuildProjects"...`);
const packableProjects = await Promise.all(await MSBuildProject.PackableProjectsToMSBuildProjects([project]));
if (packableProjects.length === 0) throw new Error("No MSBuildProject instances were returned!");
this._evaluatedProjects.push(...packableProjects);
const variant = (referenceCounter.get(project) ?? 0) + packableProjects.length;
referenceCounter.set(project, variant);
_debug(`Done. Path "${project}" evaluated for ${packableProjects.length.toString()} MSBuildProject instances for a total of ${variant.toString()} variants of the given path.`);
return packableProjects.map((project) => new NugetRegistryInfo({ project }));
}
const path = project.project.Properties.MSBuildProjectFullPath;
const variant = (referenceCounter.get(path) ?? 0) + 1;
referenceCounter.set(path, variant);
_debug(`Done. Path "${path}" (variant ${variant.toString()}) is pre-evaluated. Skipping re-evaluation.`);
return [project];
}));
this._projectsToPackAndPush = projectPromiseArrayArray.flat();
_debug("[exec:verifyConditionsCmd] Packing \"Dummy\" packages and generating \"Push Dummy Package\" commands for API token tests...");
const verifyConditionsCommandAppendix = (await Promise.all(this._projectsToPackAndPush.map(async (project) => {
const path = project.project.Properties.MSBuildProjectFullPath;
_debug(`[exec:verifyConditionsCmd] Packing dummy package for "${path}"...`);
await project.PackDummyPackage({});
_debug(`[exec:verifyConditionsCmd] Generating "Push Dummy Package" command for "${path}"...`);
return project.GetPushDummyCommand({});
}))).join(" && ");
execOptions.verifyConditionsCmd = execOptions.verifyConditionsCmd && execOptions.verifyConditionsCmd.trim().length > 0 ? `${execOptions.verifyConditionsCmd} && ${verifyConditionsCommandAppendix}` : verifyConditionsCommandAppendix;
if (execOptions.verifyConditionsCmd.length === 0) execOptions.verifyConditionsCmd = void 0;
_debug("[exec:verifyConditionsCmd] Done");
const verifyReleaseCommandAppendix = this._projectsToPackAndPush.map((project) => project.GetIsNextVersionAlreadyPublishedCommand()).join(" && ");
execOptions.verifyReleaseCmd = execOptions.verifyReleaseCmd && execOptions.verifyReleaseCmd.trim().length > 0 ? `${execOptions.verifyReleaseCmd} && ${verifyReleaseCommandAppendix}` : verifyConditionsCommandAppendix;
if (execOptions.verifyReleaseCmd.length === 0) execOptions.verifyReleaseCmd = void 0;
_debug("[exec:verifyReleaseCmd] Done");
const prepareCommandAppendix = await configurePrepareCmd(this._projectsToPublish, this._projectsToPackAndPush);
execOptions.prepareCmd = execOptions.prepareCmd && execOptions.prepareCmd.trim().length > 0 ? `${execOptions.prepareCmd} && ${prepareCommandAppendix}` : prepareCommandAppendix;
if (execOptions.prepareCmd.length === 0) execOptions.prepareCmd = void 0;
_debug("[exec:prepareCmd] Done");
if (this._projectsToPackAndPush.length > 0) {
const publishCommandAppendix = configureDotnetNugetPush(this._projectsToPackAndPush);
execOptions.publishCmd = execOptions.publishCmd && execOptions.publishCmd.trim().length > 0 ? `${execOptions.publishCmd} && ${publishCommandAppendix}` : publishCommandAppendix;
if (execOptions.publishCmd.length === 0) execOptions.publishCmd = void 0;
}
_debug("[exec:publishCmd] Done");
}
/**
* Insert a plugin into the plugins array.
* @param insertAfterPluginIDs Plugins which should appear BEFORE
* {@link insertPluginIDs}.
* @param insertPluginIDs The plugin(s) to insert into the plugins array.
* @param insertBeforePluginsIDs plugins which should appear AFTER the
* inserted plugin(s).
*/
splicePlugin(insertAfterPluginIDs, insertPluginIDs, insertBeforePluginsIDs) {
const errors = [];
const pluginIDs = this.options.plugins.map((v) => typeof v === "string" ? v : v[0]);
const indexOfLastPreceding = insertAfterPluginIDs.filter((v) => pluginIDs.includes(v)).map((v) => pluginIDs.indexOf(v)).sort((a, b) => a - b).find((_v, index, object) => index === object.length - 1);
if (!indexOfLastPreceding) throw new ReferenceError("An attempt to get the last element of indexOfLastAfter returned undefined.");
const indicesOfBefore = insertBeforePluginsIDs.filter((v) => pluginIDs.includes(v)).map((v) => pluginIDs.indexOf(v)).sort((a, b) => a - b);
for (const index of indicesOfBefore) {
if (index > indexOfLastPreceding) continue;
const formattedInsertIds = "[" + insertPluginIDs.map((v) => `"${v}"`).join(", ") + "]";
const formattedAfterIds = "[" + insertAfterPluginIDs.map((v) => `"${v}"`).join(", ") + "]";
const formattedBeforeIds = "[" + insertBeforePluginsIDs.map((v) => `"${v}"`).join(", ") + "]";
errors.push(/* @__PURE__ */ new Error(`insertPlugin was instructed to insert ${formattedInsertIds} after ${formattedAfterIds} and before ${formattedBeforeIds}, but ${JSON.stringify(pluginIDs[indexOfLastPreceding])} is ordered after ${JSON.stringify(pluginIDs[index])}!`));
}
if (errors.length > 0) throw new AggregateError(errors, "One or more errors occurred while splicing plugin-option tuples into the Semantic Release config!");
this.options.plugins.splice(indexOfLastPreceding + 1, 0, ...insertPluginIDs.map((v) => [v, {}]));
}
async getTokenTestingCommands() {
let projects;
if (this._projectsToPackAndPush.every((nri) => nri instanceof NugetRegistryInfo)) {
_debug("[SemanticReleaseConfigDotnet#getTokenTestingCommands] All projects already evaluated.");
projects = this._projectsToPackAndPush.map((nri) => nri.project);
} else {
_debug(`[SemanticReleaseConfigDotnet#getTokenTestingCommands] Evaluating up to ${this._projectsToPackAndPush.length.toString()} projects...`);
projects = await Promise.all(await MSBuildProject.PackableProjectsToMSBuildProjects(this._projectsToPackAndPush));
}
/** if a project is not in {@link EvaluatedProjects}, add it */
for (const project of projects) if (!this.EvaluatedProjects.includes(project)) this.EvaluatedProjects.push(project);
const regInfos = projects.map((project) => new NugetRegistryInfo({ project }));
return (await Promise.all(regInfos.map((nri) => nri.PackDummyPackage({}).then((nupkgs) => {
const mainNupkg = nupkgs.find((nupkg) => (/* @__PURE__ */ new RegExp(/(?<!symbols)\.nupkg$/)).test(nupkg));
if (mainNupkg !== void 0) return {
nri,
nupkgPath: mainNupkg
};
throw new Error("None of the following dummy packages are non-symbol .nupkg files:\n" + nupkgs.map((nupkg) => ` - ${nupkg}`).join("\n") + "\nIf you intended to push only symbol packages, check if a feature request already exists (https://github.com/HaloSPV3/HCE.Shared/issues?q=push+snupkg) and, if one does not exist, create one containing the keywords \"push snupkg\".");
})))).map((pair) => pair.nri.GetPushDummyCommand({})).join(" && ");
}
toOptions() {
return this.options;
}
};
/**
* Configures {@link baseConfig} with `@semantic-release/exec` to `dotnet`
* publish, pack, and nuget-push.
* @param projectsToPublish
* An array of dotnet projects' relative paths -OR- an array of
* {@link MSBuildProject} instances.
* - If `MSBuildProject[]`, the instances will be used as-is.
* - If `[]`, tries getting projects' semi-colon-separated relative paths from
* the `PROJECTS_TO_PUBLISH` environment variable.
* - If configured as recommended, the projects' publish outputs will be zipped
* to '$PWD/publish' for use in the `publish` semantic-release step e.g. for a
* GitHub release.
* @param projectsToPackAndPush An array of dotnet projects' relative paths -OR-
* an array of instances of {@link NugetRegistryInfo} and/or derived classes.
* - If `NugetRegistryInfo[]`, no conversions or modifications will occur.
* - If `string[]`, the project paths will be converted to
* {@link NugetRegistryInfo} instances with default values. This may be undesired.
* - If `[]`, `dotnet pack` and `dotnet nuget push` commands will not be configured.
* - If `undefined`, tries getting projects' semi-colon-separated relative paths
* from the `PROJECTS_TO_PACK_AND_PUSH` environment variable.
* With the recommended configuration, `dotnet pack` will write the nupkg/snupkg
* files to `$PWD/publish` where they will be globbed by `dotnet nuget push`.
* @returns a semantic-release Options object, based on
* `@halospv3/hce.shared-config` (our base config), with the
* `@semantic-release/exec` plugin configured to `dotnet publish`, `pack`, and
* `push` the specified projects.
*/
async function getConfig(projectsToPublish, projectsToPackAndPush) {
if (_debug.enabled) _debug("hce.shared-config:\n" + inspect(baseConfig, false, Infinity, true));
const errors = [];
if (projectsToPublish.length === 0) {
_debug("projectsToPublish is empty. Checking PROJECTS_TO_PUBLISH...");
const _ = getEnvVarValue("PROJECTS_TO_PUBLISH");
if (_ === void 0) errors.push(/* @__PURE__ */ new Error("projectsToPublish.length must be > 0 or PROJECTS_TO_PUBLISH must be defined and contain at least one path."));
else projectsToPublish = _.split(";");
}
_debug(`${projectsToPublish.length.toString()} projects found to dotnet-publish.`);
if (!projectsToPackAndPush) {
_debug("projectsToPackAndPush is empty. Checking PROJECTS_TO_PACK_AND_PUSH...");
const _ = getEnvVarValue("PROJECTS_TO_PACK_AND_PUSH");
if (_ === void 0) _debug("projectsToPackAndPush and PROJECTS_TO_PACK_AND_PUSH are empty and/or undefined.");
else projectsToPackAndPush = _.split(";");
}
_debug(`${(projectsToPackAndPush?.length ?? 0).toString()} projects found to dotnet-pack and dotnet-nuget-push.`);
if (errors.length > 0) throw new AggregateError(errors, "getConfig cannot continue. One or more errors occurred.");
_debug(`Instantiating ${SemanticReleaseConfigDotnet.name}...`);
const config = new SemanticReleaseConfigDotnet(projectsToPublish, projectsToPackAndPush);
_debug("Setting up Dotnet commands...");
await config.setupDotnetCommands();
const options = config.toOptions();
if (_debug.enabled) {
_debug("modified plugins array:");
_debug(inspect(options.plugins, false, Infinity));
}
return options;
}
/**
* @module semanticReleaseConfigDotnet
*/
//#endregion
export { SemanticReleaseConfigDotnet, getConfig };
//# sourceMappingURL=semanticReleaseConfigDotnet.mjs.map