UNPKG

@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
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